Data Files, Demo Data & Sequences

Series: Odoo 18 Development for Python Developers Target Audience: Python developers who are new to web development and want to learn Odoo Prerequisites: Lessons 1–9 completed, library_app module with views, security, chatter, and res.partner extension


XML Data Files vs CSV Data Files — When to Use Which

Odoo supports two file formats for loading data: XML and CSV. Both are listed in __manifest__.py, but they serve different purposes.

CSV Files

CSV files are best for loading simple tabular data — records with flat fields, no nesting, no complex relations.

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_book_user,book.user,model_library_book,group_library_user,1,1,0,0

Best for:

  • Access Control Lists (ir.model.access.csv) — this is almost always CSV
  • Simple data imports (list of countries, categories, codes)
  • Any data that’s naturally a spreadsheet/table

Limitations:

  • Can’t define nested structures (e.g., a record with child records)
  • Can’t use eval for Python expressions
  • Can’t set noupdate on individual records
  • Can’t create complex Many2many or One2many relations easily

XML Files

XML files handle everything CSV can do, plus complex structures:

<odoo>
    <record id="tag_fiction" model="library.tag">
        <field name="name">Fiction</field>
        <field name="color">1</field>
    </record>
</odoo>

Best for:

  • Views, actions, menus (always XML)
  • Security groups and record rules (always XML)
  • Records with relational fields
  • Records with noupdate requirements
  • Demo data with interconnected records
  • Any data that’s more complex than a flat table

Decision Guide

Data Type Format Why
ACLs (ir.model.access) CSV Flat table, standard convention
Views, Actions, Menus XML Complex nested structures
Security Groups, Record Rules XML Need eval, relational fields
Default tags/categories XML or CSV Either works; XML if colors/relations needed
Demo data XML Usually has relations between records
Translation files PO Special format (.po files in i18n/)

Loading Data: data vs demo in __manifest__.py

The __manifest__.py has two lists for data files: data and demo. They behave differently.

data — Always Loaded

'data': [
    'security/library_security.xml',
    'security/ir.model.access.csv',
    'views/book_views.xml',
    'views/menu_views.xml',
    'data/library_tag_data.xml',      # Default tags
    'data/library_sequence_data.xml', # Sequences
],

Data files are loaded:

  • When the module is installed for the first time
  • When the module is upgraded
  • In every database (production, staging, development)

Use for: Views, menus, security, default configuration, sequences — anything that’s essential for the module to function.

demo — Only in Demo Mode

'demo': [
    'demo/library_demo.xml',    # Sample books, authors, tags
],

Demo files are loaded:

  • Only when the database was created with the “Load demonstration data” checkbox enabled
  • On module install (not on upgrade)
  • Typically only in development/testing databases

Use for: Sample records that help showcase the module — example books, test authors, sample members. Never put essential data here.

How to Check if Demo Mode is Enabled

In the Odoo Shell:

# Check if the current database has demo data enabled
demo_enabled = self.env['ir.module.module'].search([
    ('name', '=', 'base'),
    ('demo', '=', True),
])
print(bool(demo_enabled))  # True if demo mode is on

Loading Order Summary

When a module is installed:

1. Python code (via __init__.py chain)
2. 'data' files (in the order listed in __manifest__.py)
3. 'demo' files (only if demo mode is on, in order listed)

When a module is upgraded:

1. Python code (via __init__.py chain)
2. 'data' files (re-loaded, respecting noupdate flags)
3. 'demo' files are NOT re-loaded on upgrade

noupdate="1" — Protecting User-Modified Data

This is one of the most confusing aspects of Odoo data management for beginners. Let’s demystify it.

The Problem

Imagine you ship a module with default tag data:

<record id="tag_fiction" model="library.tag">
    <field name="name">Fiction</field>
    <field name="color">1</field>
</record>

After installation, a user renames the tag from “Fiction” to “Novels”. Then you upgrade the module. What should happen?

  • Option A: Overwrite the user’s change back to “Fiction” — BAD (user loses their customization)
  • Option B: Keep the user’s version “Novels” — GOOD (respect user modifications)

The noupdate flag controls this behavior.

The noupdate Flag

<odoo>
    <!-- noupdate="0" (default): Records ARE overwritten on upgrade -->
    <data noupdate="0">
        <record id="my_view" model="ir.ui.view">
            <!-- This view will be updated every time the module is upgraded -->
        </record>
    </data>

    <!-- noupdate="1": Records are NOT overwritten on upgrade -->
    <data noupdate="1">
        <record id="tag_fiction" model="library.tag">
            <!-- This tag is created on install, then NEVER touched again -->
            <!-- User modifications are preserved -->
        </record>
    </data>
</odoo>

When to Use Each

Content noupdate Why
Views 0 (default) Views should always be updated to the latest version
Actions 0 (default) Actions should stay in sync with the code
Menus 0 (default) Menus should reflect the latest structure
Security groups 0 (default) Groups should be updated with new permissions
Record rules 1 Users/admins may customize rules
Default data (tags, categories) 1 Users may rename or modify defaults
Sequences 1 Users may change the format/numbering
Email templates 1 Users heavily customize email content
Server actions 1 Users may modify automation logic

A Common Pattern

Many XML files mix both noupdate values:

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <!-- Views: always update -->
    <data noupdate="0">
        <record id="my_view" model="ir.ui.view">
            <!-- ... -->
        </record>
    </data>

    <!-- Default data: don't overwrite user changes -->
    <data noupdate="1">
        <record id="tag_fiction" model="library.tag">
            <field name="name">Fiction</field>
        </record>
    </data>
</odoo>

The Development Headache

During development, noupdate="1" can be frustrating because your data changes don’t take effect on upgrade. Solutions:

Option 1: Temporarily use noupdate="0" during development, then switch to "1" before release.

Option 2: Delete the record manually, then upgrade:

# In the Odoo Shell:
record = env.ref('library_app.tag_fiction', raise_if_not_found=False)
if record:
    record.unlink()
# Now upgrade the module — the record will be recreated

Option 3: Use the --init flag instead of --update:

# -i forces full reinstall (ignores noupdate)
docker compose exec odoo odoo -d odoo18dev -i library_app --stop-after-init
# WARNING: This may reset ALL data in the module, including user modifications

External IDs (XML IDs) — The Glue of Odoo Data

External IDs (also called XML IDs) are one of the most important concepts in Odoo. They provide a stable way to reference records across modules and upgrades.

What is an External ID?

Every tag has an id attribute. This creates an External ID:

<record id="tag_fiction" model="library.tag">
    <field name="name">Fiction</field>
</record>

The full External ID is: library_app.tag_fiction

library_app.tag_fiction
│           │
│           └── The id you wrote in the XML
└── Your module name (added automatically)

Where External IDs are Stored

External IDs are stored in the ir.model.data table:

SELECT * FROM ir_model_data WHERE module = 'library_app' AND name = 'tag_fiction';
-- Returns: module='library_app', name='tag_fiction', model='library.tag', res_id=1

This mapping tells Odoo: “The XML ID library_app.tag_fiction refers to library.tag record with id=1.”

Why External IDs Matter

  1. Stable references: Database IDs (1, 2, 3) can differ between databases. External IDs are consistent everywhere.
  2. Cross-module references: Module A can reference records from Module B using External IDs.
  3. Upgrade safety: When you upgrade a module, Odoo uses External IDs to find and update existing records (instead of creating duplicates).
  4. Data deletion: When you uninstall a module, Odoo deletes all records with that module’s External IDs.

Viewing External IDs

In Developer Mode, you can see any record’s External ID:

  1. Open any record in form view
  2. Click the debug icon (bug icon) in the top menu
  3. Click “View Metadata”
  4. You’ll see the External ID (XML ID) and other technical info

Or in the Odoo Shell:

# Find External ID for a record
tag = env['library.tag'].search([('name', '=', 'Fiction')], limit=1)
external_id = tag.get_external_id()
print(external_id)  # {1: 'library_app.tag_fiction'}

# Get a record by its External ID
fiction_tag = env.ref('library_app.tag_fiction')
print(fiction_tag.name)  # 'Fiction'

The ref() Function and Cross-Module References

Using ref in XML

The ref attribute links to another record by its External ID:

<!-- Reference a record from the SAME module -->
<field name="group_id" ref="group_library_user"/>
<!-- Odoo expands this to 'library_app.group_library_user' -->

<!-- Reference a record from ANOTHER module -->
<field name="group_id" ref="base.group_user"/>
<!-- 'base' is the module, 'group_user' is the record -->

<!-- In a Many2many field using eval -->
<field name="groups" eval="[(4, ref('group_library_user'))]"/>
<!-- ref() inside eval returns the database ID -->

Using ref in eval

For fields that need Python expressions, use eval with ref():

<!-- Set a Many2one field -->
<field name="author_id" ref="author_tolkien"/>

<!-- Set a Many2many field: add multiple records -->
<field name="tag_ids" eval="[
    (4, ref('tag_fiction')),
    (4, ref('tag_fantasy')),
    (4, ref('tag_classic')),
]"/>

<!-- Set a Many2many field: replace all with specific records -->
<field name="tag_ids" eval="[(6, 0, [
    ref('tag_fiction'),
    ref('tag_fantasy'),
])]"/>

<!-- Set a default value referencing another record -->
<field name="user_id" eval="ref('base.user_admin')"/>

Using env.ref() in Python

# Get a record by External ID
admin_user = self.env.ref('base.user_admin')

# Safe version: returns False if not found
record = self.env.ref('library_app.tag_fiction', raise_if_not_found=False)

# Use in a default value
class LibraryBook(models.Model):
    _name = 'library.book'

    tag_ids = fields.Many2many(
        'library.tag',
        default=lambda self: self.env.ref(
            'library_app.tag_fiction', raise_if_not_found=False
        ),
    )

Cross-Module Reference Examples

<!-- Reference the admin user from the 'base' module -->
<field name="user_id" ref="base.user_admin"/>

<!-- Reference a view from 'base' for inheritance -->
<field name="inherit_id" ref="base.view_partner_form"/>

<!-- Reference a group from the 'sale' module -->
<field name="groups" eval="[(4, ref('sale.group_sale_salesman'))]"/>

<!-- Reference a mail template from 'mail' module -->
<field name="template_id" ref="mail.mail_template_data_notification_email_default"/>

Important: When referencing records from other modules, make sure that module is in your depends list in __manifest__.py. Otherwise, the reference might not exist.


Sequences (ir.sequence) — Auto-Numbering Records

Sequences generate auto-incrementing identifiers like BOOK-0001, BOOK-0002, LIB-MEM-0001, etc. They’re used for reference numbers, membership IDs, invoice numbers, and more.

Defining a Sequence

<data noupdate="1">
    <record id="sequence_library_book" model="ir.sequence">
        <field name="name">Library Book Reference</field>
        <field name="code">library.book</field>
        <field name="prefix">BOOK-</field>
        <field name="padding">4</field>
        <field name="number_next">1</field>
        <field name="number_increment">1</field>
    </record>
</data>

Fields explained:

Field Value Result
name Human-readable name Shown in Settings → Technical → Sequences
code Technical identifier Used in Python to fetch the sequence
prefix Text before the number BOOK-
padding Number of digits (zero-padded) 4 → 0001, 0002, …
number_next Next number to generate Starts at 1
number_increment Step between numbers 1 → 1, 2, 3; 10 → 10, 20, 30

This sequence generates: BOOK-0001, BOOK-0002, BOOK-0003, …

Advanced Sequence Patterns

<!-- Date-based prefix -->
<record id="sequence_library_borrow" model="ir.sequence">
    <field name="name">Library Borrow Reference</field>
    <field name="code">library.borrow</field>
    <field name="prefix">BRW/%(year)s/%(month)s/</field>
    <field name="padding">4</field>
</record>
<!-- Generates: BRW/2024/03/0001, BRW/2024/03/0002, BRW/2024/04/0001, ... -->

<!-- With suffix -->
<record id="sequence_library_member" model="ir.sequence">
    <field name="name">Library Member Number</field>
    <field name="code">library.member</field>
    <field name="prefix">LIB-</field>
    <field name="suffix">-MEM</field>
    <field name="padding">5</field>
</record>
<!-- Generates: LIB-00001-MEM, LIB-00002-MEM, ... -->

Available date variables for prefix/suffix:

Variable Meaning Example
%(year)s 4-digit year 2024
%(month)s 2-digit month 03
%(day)s 2-digit day 15
%(y)s 2-digit year 24
%(h24)s Hour (24h) 14
%(sec)s Seconds 30

Using Sequences in Python

class LibraryBook(models.Model):
    _name = 'library.book'

    reference = fields.Char(
        string='Reference',
        readonly=True,
        copy=False,
        default='New',
    )

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get('reference', 'New') == 'New':
                vals['reference'] = self.env['ir.sequence'].next_by_code('library.book') or 'New'
        return super().create(vals_list)

How next_by_code() works:

  1. Looks up the sequence with code='library.book'
  2. Gets the current counter value
  3. Formats the result: BOOK- + zero-padded number
  4. Increments the counter for next time
  5. Returns the formatted string (e.g., 'BOOK-0001')

If no sequence is found, next_by_code() returns False. The or 'New' fallback handles this case.

Sequence in res.partner for Membership Numbers

Let’s improve the membership number generation from Lesson 9:

<record id="sequence_library_member" model="ir.sequence">
    <field name="name">Library Membership Number</field>
    <field name="code">library.member</field>
    <field name="prefix">LIB-</field>
    <field name="padding">4</field>
</record>

Then in models/res_partner.py, replace the manual numbering with:

def action_make_library_member(self):
    for partner in self:
        if not partner.is_library_member:
            partner.write({
                'is_library_member': True,
                'membership_number': self.env['ir.sequence'].next_by_code('library.member'),
                'member_since': fields.Date.today(),
            })

Much cleaner! The sequence handles auto-incrementing and formatting.


Server Actions and Automated Actions

Server Actions

A server action is a configurable action that runs Python code, sends emails, creates records, or performs other operations. They can be triggered from buttons, menus, or automated rules.

<record id="action_mark_all_draft_available" model="ir.actions.server">
    <field name="name">Mark Draft Books as Available</field>
    <field name="model_id" ref="model_library_book"/>
    <field name="binding_model_id" ref="model_library_book"/>
    <field name="binding_view_types">list</field>
    <field name="state">code</field>
    <field name="code">
for book in records:
    if book.state == 'draft':
        book.action_mark_available()
    </field>
</record>

What this does:

  • Creates an action visible in the “Action” dropdown on the book list view
  • When a user selects books and runs this action, it marks all draft books as available
  • binding_model_id — Makes this action appear in the Action menu for this model
  • binding_view_types — Which views show this action (list, form, or list,form)
  • records — A special variable containing the selected records

Available Variables in Server Action Code

Variable Type Description
records Recordset The selected records
record Record First selected record (for single-record actions)
env Environment The Odoo environment
model Model The model class
time module Python time module
datetime module Python datetime module
dateutil module Python dateutil module
log function Logging function
UserError exception For raising user-facing errors

Automated Actions (Scheduled / Event-Triggered)

Automated actions run automatically based on triggers: time-based (cron jobs) or event-based (record created, updated, etc.).

Event-Based Automated Action

<record id="automation_new_book_notification" model="base.automation">
    <field name="name">Notify on New Book</field>
    <field name="model_id" ref="model_library_book"/>
    <field name="trigger">on_create</field>
    <field name="state">code</field>
    <field name="code">
for record in records:
    record.message_post(
        body=f"New book added: {record.name}",
        message_type='notification',
    )
    </field>
</record>

Trigger options:

Trigger Fires When
on_create A new record is created
on_write An existing record is updated
on_create_or_write A record is created or updated
on_unlink A record is deleted
on_change A specific field value changes
on_time At scheduled intervals (cron-like)

Time-Based Automated Action (Cron Job)

<record id="automation_check_overdue_books" model="base.automation">
    <field name="name">Check Overdue Books</field>
    <field name="model_id" ref="model_library_book"/>
    <field name="trigger">on_time</field>
    <field name="trg_date_id" ref="field_library_book__date_added"/>
    <field name="trg_date_range">30</field>
    <field name="trg_date_range_type">day</field>
    <field name="filter_domain">[('state', '=', 'borrowed')]</field>
    <field name="state">code</field>
    <field name="code">
for record in records:
    record.message_post(
        body="This book has been borrowed for over 30 days!",
        message_type='notification',
    )
    </field>
</record>

Time-based fields:

Field Purpose
trg_date_id The date field to watch
trg_date_range Number of units after the date
trg_date_range_type Unit: day, hour, month
filter_domain Only apply to records matching this domain

This example checks all borrowed books and posts a notification if they’ve been borrowed for more than 30 days.

Field-Change Trigger

<record id="automation_state_change_log" model="base.automation">
    <field name="name">Log Book State Changes</field>
    <field name="model_id" ref="model_library_book"/>
    <field name="trigger">on_change</field>
    <field name="trigger_field_ids" eval="[(4, ref('field_library_book__state'))]"/>
    <field name="state">code</field>
    <field name="code">
for record in records:
    log(f"Book '{record.name}' state changed to '{record.state}'")
    </field>
</record>

This fires only when the state field changes — not on every write.


Hands-on: Add Demo Books, Sequences, and an Automated Action

Step 1: Create data/library_sequence_data.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data noupdate="1">

        <!-- Sequence for book references -->
        <record id="sequence_library_book" model="ir.sequence">
            <field name="name">Library Book Reference</field>
            <field name="code">library.book</field>
            <field name="prefix">BOOK-</field>
            <field name="padding">4</field>
            <field name="number_next">1</field>
            <field name="number_increment">1</field>
        </record>

        <!-- Sequence for membership numbers -->
        <record id="sequence_library_member" model="ir.sequence">
            <field name="name">Library Membership Number</field>
            <field name="code">library.member</field>
            <field name="prefix">LIB-</field>
            <field name="padding">4</field>
            <field name="number_next">1</field>
            <field name="number_increment">1</field>
        </record>

    </data>
</odoo>

Step 2: Create data/library_tag_data.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data noupdate="1">

        <!-- Default tags loaded on module install -->
        <record id="tag_fiction" model="library.tag">
            <field name="name">Fiction</field>
            <field name="color">1</field>
        </record>

        <record id="tag_non_fiction" model="library.tag">
            <field name="name">Non-Fiction</field>
            <field name="color">2</field>
        </record>

        <record id="tag_fantasy" model="library.tag">
            <field name="name">Fantasy</field>
            <field name="color">3</field>
        </record>

        <record id="tag_science" model="library.tag">
            <field name="name">Science</field>
            <field name="color">4</field>
        </record>

        <record id="tag_history" model="library.tag">
            <field name="name">History</field>
            <field name="color">5</field>
        </record>

        <record id="tag_biography" model="library.tag">
            <field name="name">Biography</field>
            <field name="color">6</field>
        </record>

        <record id="tag_children" model="library.tag">
            <field name="name">Children's</field>
            <field name="color">7</field>
        </record>

        <record id="tag_classic" model="library.tag">
            <field name="name">Classic</field>
            <field name="color">8</field>
        </record>

    </data>
</odoo>

Step 3: Create data/library_automation_data.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data noupdate="1">

        <!-- Server Action: Mark selected draft books as available -->
        <record id="action_server_mark_available" model="ir.actions.server">
            <field name="name">Mark as Available</field>
            <field name="model_id" ref="model_library_book"/>
            <field name="binding_model_id" ref="model_library_book"/>
            <field name="binding_view_types">list,form</field>
            <field name="state">code</field>
            <field name="code">
for book in records:
    if book.state == 'draft':
        book.action_mark_available()
            </field>
        </record>

        <!-- Automated Action: Notify when a new book is added -->
        <record id="automation_new_book_notify" model="base.automation">
            <field name="name">New Book Notification</field>
            <field name="model_id" ref="model_library_book"/>
            <field name="trigger">on_create</field>
            <field name="state">code</field>
            <field name="code">
for record in records:
    record.message_post(
        body=f"📚 New book added to the library: <b>{record.name}</b>",
        message_type='notification',
        subtype_xmlid='mail.mt_note',
    )
            </field>
        </record>

    </data>
</odoo>

Step 4: Create demo/library_demo.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data noupdate="1">

        <!-- ==================== -->
        <!--       AUTHORS        -->
        <!-- ==================== -->
        <record id="author_tolkien" model="library.author">
            <field name="name">J.R.R. Tolkien</field>
            <field name="nationality">British</field>
            <field name="date_of_birth">1892-01-03</field>
            <field name="email">tolkien@example.com</field>
            <field name="biography">English writer, poet, philologist, and academic, best known as the author of the high fantasy works The Hobbit and The Lord of the Rings.</field>
        </record>

        <record id="author_herbert" model="library.author">
            <field name="name">Frank Herbert</field>
            <field name="nationality">American</field>
            <field name="date_of_birth">1920-10-08</field>
            <field name="biography">American science-fiction author best known for the novel Dune and its five sequels.</field>
        </record>

        <record id="author_orwell" model="library.author">
            <field name="name">George Orwell</field>
            <field name="nationality">British</field>
            <field name="date_of_birth">1903-06-25</field>
            <field name="biography">English novelist, essayist, journalist, and critic. Best known for the allegorical novella Animal Farm and the dystopian novel 1984.</field>
        </record>

        <record id="author_asimov" model="library.author">
            <field name="name">Isaac Asimov</field>
            <field name="nationality">American</field>
            <field name="date_of_birth">1920-01-02</field>
            <field name="biography">American writer and professor of biochemistry, known for his works of science fiction and popular science.</field>
        </record>

        <!-- ==================== -->
        <!--      PUBLISHER       -->
        <!-- ==================== -->
        <record id="publisher_allen_unwin" model="library.publisher">
            <field name="name">Allen &amp; Unwin</field>
            <field name="country">United Kingdom</field>
        </record>

        <record id="publisher_chilton" model="library.publisher">
            <field name="name">Chilton Books</field>
            <field name="country">United States</field>
        </record>

        <record id="publisher_secker" model="library.publisher">
            <field name="name">Secker &amp; Warburg</field>
            <field name="country">United Kingdom</field>
        </record>

        <!-- ==================== -->
        <!--        BOOKS         -->
        <!-- ==================== -->
        <record id="book_hobbit" model="library.book">
            <field name="name">The Hobbit</field>
            <field name="isbn">978-0547928227</field>
            <field name="pages">310</field>
            <field name="price">14.99</field>
            <field name="rating">4.7</field>
            <field name="date_published">1937-09-21</field>
            <field name="author_id" ref="author_tolkien"/>
            <field name="publisher_id" ref="publisher_allen_unwin"/>
            <field name="cover_type">paperback</field>
            <field name="state">available</field>
            <field name="synopsis">Bilbo Baggins, a comfortable hobbit, is swept into an epic quest to reclaim the lost Dwarf Kingdom of Erebor from the fearsome dragon Smaug.</field>
            <field name="tag_ids" eval="[
                (4, ref('tag_fiction')),
                (4, ref('tag_fantasy')),
                (4, ref('tag_classic')),
            ]"/>
        </record>

        <record id="book_lotr" model="library.book">
            <field name="name">The Lord of the Rings</field>
            <field name="isbn">978-0618640157</field>
            <field name="pages">1178</field>
            <field name="price">29.99</field>
            <field name="rating">4.9</field>
            <field name="date_published">1954-07-29</field>
            <field name="author_id" ref="author_tolkien"/>
            <field name="publisher_id" ref="publisher_allen_unwin"/>
            <field name="cover_type">hardcover</field>
            <field name="state">available</field>
            <field name="synopsis">An epic high-fantasy novel following the quest to destroy the One Ring and defeat the Dark Lord Sauron.</field>
            <field name="tag_ids" eval="[
                (4, ref('tag_fiction')),
                (4, ref('tag_fantasy')),
                (4, ref('tag_classic')),
            ]"/>
        </record>

        <record id="book_dune" model="library.book">
            <field name="name">Dune</field>
            <field name="isbn">978-0441013593</field>
            <field name="pages">412</field>
            <field name="price">16.99</field>
            <field name="rating">4.5</field>
            <field name="date_published">1965-08-01</field>
            <field name="author_id" ref="author_herbert"/>
            <field name="publisher_id" ref="publisher_chilton"/>
            <field name="cover_type">paperback</field>
            <field name="state">available</field>
            <field name="synopsis">Set in the distant future, the novel tells the story of young Paul Atreides on the desert planet Arrakis.</field>
            <field name="tag_ids" eval="[
                (4, ref('tag_fiction')),
                (4, ref('tag_science')),
            ]"/>
        </record>

        <record id="book_1984" model="library.book">
            <field name="name">Nineteen Eighty-Four</field>
            <field name="isbn">978-0451524935</field>
            <field name="pages">328</field>
            <field name="price">12.99</field>
            <field name="rating">4.6</field>
            <field name="date_published">1949-06-08</field>
            <field name="author_id" ref="author_orwell"/>
            <field name="publisher_id" ref="publisher_secker"/>
            <field name="cover_type">paperback</field>
            <field name="state">borrowed</field>
            <field name="synopsis">A dystopian novel set in Airstrip One, a province of the superstate Oceania, depicting a society under omnipresent government surveillance.</field>
            <field name="tag_ids" eval="[
                (4, ref('tag_fiction')),
                (4, ref('tag_classic')),
            ]"/>
        </record>

        <record id="book_foundation" model="library.book">
            <field name="name">Foundation</field>
            <field name="isbn">978-0553293357</field>
            <field name="pages">244</field>
            <field name="price">15.99</field>
            <field name="rating">4.4</field>
            <field name="date_published">1951-05-01</field>
            <field name="author_id" ref="author_asimov"/>
            <field name="cover_type">paperback</field>
            <field name="state">draft</field>
            <field name="synopsis">The story of a group of scientists who seek to preserve knowledge as the Galactic Empire crumbles.</field>
            <field name="tag_ids" eval="[
                (4, ref('tag_fiction')),
                (4, ref('tag_science')),
                (4, ref('tag_classic')),
            ]"/>
        </record>

    </data>
</odoo>

Step 5: Add reference Field to Book Model

Update models/book.py to use the sequence:

# Add this field to the LibraryBook class:

    reference = fields.Char(
        string='Reference',
        readonly=True,
        copy=False,
        default='New',
        help='Auto-generated book reference number',
    )

# Update or add the create method:

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get('reference', 'New') == 'New':
                vals['reference'] = (
                    self.env['ir.sequence'].next_by_code('library.book') or 'New'
                )
        return super().create(vals_list)

Also add the reference field to the form view (in views/book_views.xml), inside the title area:

<!-- Add after the <h1> with the name field, inside oe_title div -->
<h3>
    <field name="reference"/>
</h3>

Step 6: Update Membership Number to Use Sequence

In models/res_partner.py, simplify the action_make_library_member method:

def action_make_library_member(self):
    for partner in self:
        if not partner.is_library_member:
            partner.write({
                'is_library_member': True,
                'membership_number': self.env['ir.sequence'].next_by_code('library.member'),
                'member_since': fields.Date.today(),
            })

Step 7: Update __manifest__.py

'data': [
    'security/library_security.xml',
    'security/ir.model.access.csv',
    'data/library_sequence_data.xml',       # ← Sequences (before views)
    'data/library_tag_data.xml',            # ← Default tags
    'data/library_automation_data.xml',     # ← Server actions & automations
    'views/book_views.xml',
    'views/author_views.xml',
    'views/tag_views.xml',
    'views/res_partner_views.xml',
    'views/menu_views.xml',
],

'demo': [
    'demo/library_demo.xml',                # ← Demo data (authors + books)
],

Step 8: Upgrade and Test

docker compose exec odoo odoo -d odoo18dev -u library_app --stop-after-init
docker compose restart odoo

What to test:

  1. Default tags — Go to Library → Configuration → Tags. You should see 8 pre-created tags with colors.
  2. Demo data — If your database has demo mode enabled, go to Library → Books. You should see 5 classic books with authors, publishers, and tags already assigned.
  3. Book reference — Create a new book. The “Reference” field should auto-fill with BOOK-0001 (or the next available number).
  4. Membership sequence — Go to Contacts, open a contact, check “Library Member.” The membership number should be LIB-0001.
  5. Server action — Go to the book list, select some draft books, click Action → “Mark as Available.” They should change to Available.
  6. Automated action — Create a new book. Check the chatter — a notification should appear automatically saying “New book added.”

Summary & What’s Next

Key Takeaways

  1. XML for complex data, CSV for flat tables. ACLs go in CSV; views, actions, and default data go in XML.
  2. data is always loaded; demo is only loaded in demo databases. Never put essential data in demo.
  3. noupdate="1" protects user modifications from being overwritten on module upgrade. Use it for default data, sequences, and email templates. Don’t use it for views and menus.
  4. External IDs (XML IDs) are the stable way to reference records. Format: module_name.record_id. They survive database migrations and cross-module references.
  5. ref() is how you reference External IDs in XML (ref="tag_fiction") and Python (self.env.ref('library_app.tag_fiction')).
  6. Sequences (ir.sequence) auto-generate formatted reference numbers. Define in XML, use in Python with next_by_code().
  7. Server actions add operations to the Action dropdown menu. Automated actions run code on triggers (create, update, time-based).

What’s Next?

In Lesson 11: Business Logic Patterns — Workflows & State Machines, we’ll build on what we have and learn:

  • State machine patterns for book borrowing workflows
  • Button actions with state transition constraints
  • Wizards (TransientModel) for multi-step user interactions
  • Mail thread integration for notifications
  • The confirm attribute on dangerous buttons

Our library module now has data, sequences, and automation. Next, we’ll add real business logic for borrowing and returning books.


Exercises: 1. Add a date-based prefix to the book reference sequence: BOOK/%(year)s/ so references look like BOOK/2024/0001. Remember to delete the existing sequence record first (or use a new database). 2. Create an automated action that fires when a book’s state changes to 'lost' and posts a warning message in the chatter. 3. Add more demo data: create 5 additional books from different authors and genres. Practice using ref() to link to existing tags and authors. 4. Create a server action called “Archive Old Books” that archives (sets active=False) all books with state='lost'. Bind it to the book list view. 5. Explore the External IDs in your database: go to Settings → Technical → Sequences & Identifiers → External Identifiers. Filter by module library_app. How many records does your module manage? 6. Try the noupdate experiment: – Change a default tag name (e.g., rename “Fiction” to “Novels”) in the UI – Upgrade the module – Check if the tag was reverted to “Fiction” or kept as “Novels” – Now change noupdate="1" to noupdate="0" in the XML, upgrade again – What happened? Why?


Previous lesson: Lesson 9 — Inheritance: Odoo’s Most Powerful Pattern Next lesson: Lesson 11 — Business Logic: Workflows & State Machines

Leave a Reply

Your email address will not be published. Required fields are marked *