Inheritance — Odoo’s Most Powerful Pattern

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–8 completed, library_app module with views, menus, and security working


Why Inheritance is Central to Odoo Development

In most Python frameworks, you build applications from scratch. In Odoo, you extend what already exists. This is the fundamental philosophy we introduced in Lesson 1, and inheritance is the mechanism that makes it work.

Real-World Scenario

Your client says: “I want library members to be tracked as contacts in Odoo. Each contact should have a membership number and borrowing history.”

You have two options:

  • Option A: Create a separate library.member model with name, email, phone, address… (duplicating fields that already exist in res.partner)
  • Option B: Add membership_number and borrow_history to the existing res.partner model using inheritance

Option B is the Odoo way. It means:

  • Members show up in the existing Contacts app
  • You get all existing features for free (email, phone, address, invoicing, etc.)
  • Other modules that work with res.partner automatically work with your members
  • Zero code duplication

The Inheritance Mindset

Django/Flask thinking:
  "I need a customer model" → Create Customer model from scratch

Odoo thinking:
  "I need a customer model" → res.partner already exists → Extend it with my fields

This mindset shift is what separates beginner Odoo developers from productive ones. Let’s learn the tools to make it happen.


The Three Types of Inheritance — Overview

Odoo has three distinct inheritance mechanisms. They look similar in syntax but do very different things:

Type Syntax What It Does New Table?
Extension _inherit = 'existing.model' (no _name) Adds fields/methods to an existing model No (same table)
Prototype _inherit = 'existing.model' + _name = 'new.model' Creates a copy of the model with a new name Yes (new table)
Delegation _inherits = {'existing.model': 'link_field_id'} Embeds another model via a link field Yes (new table, linked)

Let’s learn each one with concrete examples.


Classical Inheritance (Extension): _inherit Without _name

This is the most common type of inheritance — you’ll use it in almost every module. It adds fields and methods to an existing model without creating a new table.

The Syntax

class ResPartner(models.Model):
    _inherit = 'res.partner'
    # No _name = '...' → we're EXTENDING res.partner, not creating a new model

    membership_number = fields.Char(string='Membership Number')

What happens:

  • Odoo finds the existing res.partner model
  • It adds the membership_number column to the res_partner table
  • ALL existing res.partner records now have this field (it’s NULL/False for existing records)
  • The res.partner model everywhere in Odoo now includes your new field
  • No new table is created

Visual Explanation

BEFORE your module:
┌─────────────────────────┐
│      res.partner         │
├─────────────────────────┤
│ id                       │
│ name                     │
│ email                    │
│ phone                    │
│ ...                      │
└─────────────────────────┘

AFTER your module adds _inherit = 'res.partner':
┌─────────────────────────┐
│      res.partner         │
├─────────────────────────┤
│ id                       │
│ name                     │
│ email                    │
│ phone                    │
│ ...                      │
│ membership_number  ← NEW │
│ is_library_member  ← NEW │
└─────────────────────────┘

Same table, same model, just more columns.

The Python Class Name Doesn’t Matter

# These all do the same thing — extend res.partner:

class ResPartner(models.Model):        # ← Convention: match the model name
    _inherit = 'res.partner'

class PartnerLibraryExtension(models.Model):  # ← Also works
    _inherit = 'res.partner'

class Xyz(models.Model):              # ← Works too, but please don't
    _inherit = 'res.partner'

Odoo only cares about _inherit. The Python class name is just for your code readability. The convention is to use the CamelCase version of the model name.

You Can Inherit the Same Model Multiple Times

Different modules can independently extend the same model:

# In module 'library_app':
class ResPartner(models.Model):
    _inherit = 'res.partner'
    membership_number = fields.Char()

# In module 'gym_app' (completely separate module):
class ResPartner(models.Model):
    _inherit = 'res.partner'
    gym_membership_id = fields.Char()

# In module 'parking_app':
class ResPartner(models.Model):
    _inherit = 'res.partner'
    parking_spot = fields.Char()

All three modules add their fields to res.partner. They don’t conflict. The final res.partner has ALL the fields from ALL modules. This is how Odoo’s modular architecture works.

Inheriting from Multiple Models (Mixins)

You can also use _inherit as a list to inherit from multiple models:

class LibraryBook(models.Model):
    _name = 'library.book'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    # This is NOT extension inheritance — it's MIXIN inheritance
    # We used this in Lesson 7 for chatter support

When _name IS set and _inherit is a list, the model gets fields and methods from all listed models. This is how we added chatter to library.book — by mixing in mail.thread and mail.activity.mixin.


Adding Fields and Methods to Existing Models

Adding Fields

class ResPartner(models.Model):
    _inherit = 'res.partner'

    # Add new fields to the contact form
    is_library_member = fields.Boolean(
        string='Library Member',
        default=False,
        help='Check if this contact is a library member',
    )

    membership_number = fields.Char(
        string='Membership Number',
        copy=False,
    )

    member_since = fields.Date(
        string='Member Since',
    )

    # Add a relation: which books has this member borrowed?
    borrowed_book_ids = fields.Many2many(
        'library.book',
        string='Borrowed Books',
    )

    # Computed field
    borrowed_count = fields.Integer(
        string='Books Borrowed',
        compute='_compute_borrowed_count',
    )

    @api.depends('borrowed_book_ids')
    def _compute_borrowed_count(self):
        for partner in self:
            partner.borrowed_count = len(partner.borrowed_book_ids)

After upgrading your module, every contact in Odoo now has these fields. You can see them in Settings → Technical → Fields if you search for res.partner.

Adding Methods

class ResPartner(models.Model):
    _inherit = 'res.partner'

    def action_make_library_member(self):
        """Convert this contact into a library member."""
        for partner in self:
            if not partner.is_library_member:
                partner.write({
                    'is_library_member': True,
                    'member_since': fields.Date.today(),
                    'membership_number': self.env['ir.sequence'].next_by_code('library.member'),
                    # We'll create this sequence in Lesson 10
                })

This method can be called from a button in the contact form, from Python code, or from an automated action.


Overriding Methods: The super() Chain

The real power of extension inheritance comes from overriding methods. You can modify the behavior of existing methods without replacing them entirely.

The Pattern

class ResPartner(models.Model):
    _inherit = 'res.partner'

    def write(self, vals):
        # YOUR CODE: runs BEFORE the original write
        if 'is_library_member' in vals and vals['is_library_member']:
            if not vals.get('membership_number'):
                vals['membership_number'] = self.env['ir.sequence'].next_by_code('library.member')

        # Call the ORIGINAL method (super)
        result = super().write(vals)

        # YOUR CODE: runs AFTER the original write
        if 'is_library_member' in vals:
            # Log a message in chatter
            for partner in self:
                if partner.is_library_member:
                    partner.message_post(body="This contact is now a library member!")

        return result

How super() works in Odoo:

When partner.write({'name': 'New Name'}) is called:

1. YOUR write() runs first
2. super().write(vals) calls the PARENT's write()
3. The parent might also call super(), chaining to the ORIGINAL write()
4. The original write() updates the database
5. Control returns back through the chain
Your module's write()
    │
    ├── Your pre-processing code
    │
    ├── super().write(vals)  ──────►  Another module's write()
    │                                      │
    │                                      ├── super().write(vals) ──► Base write()
    │                                      │                              │
    │                                      │                              └── SQL UPDATE
    │                                      │
    │                                      └── Post-processing
    │
    └── Your post-processing code

Commonly Overridden Methods

class LibraryBook(models.Model):
    _inherit = 'library.book'  # or _name = 'library.book' in the original definition

    # ---- Override create() ----
    @api.model_create_multi
    def create(self, vals_list):
        """Called when new records are created."""
        # Pre-processing: modify values before creation
        for vals in vals_list:
            if not vals.get('isbn'):
                vals['isbn'] = 'PENDING'

        # Call the original create
        records = super().create(vals_list)

        # Post-processing: do something with the new records
        for record in records:
            record.message_post(body=f"Book '{record.name}' was added to the library.")

        return records

    # ---- Override write() ----
    def write(self, vals):
        """Called when existing records are updated."""
        # Check what's changing
        old_states = {book.id: book.state for book in self}

        result = super().write(vals)

        # Post-processing: check if state changed
        if 'state' in vals:
            for book in self:
                old_state = old_states.get(book.id)
                if old_state != book.state:
                    book.message_post(
                        body=f"Status changed from {old_state} to {book.state}"
                    )

        return result

    # ---- Override unlink() ----
    def unlink(self):
        """Called when records are deleted."""
        # Pre-check: prevent deletion of borrowed books
        for book in self:
            if book.state == 'borrowed':
                raise UserError(
                    f"Cannot delete '{book.name}' — it is currently borrowed!"
                )

        return super().unlink()

    # ---- Override copy() ----
    def copy(self, default=None):
        """Called when a record is duplicated."""
        default = dict(default or {})
        default['name'] = f"{self.name} (Copy)"
        default['state'] = 'draft'
        return super().copy(default)

@api.model_create_multi Explained

In Odoo 18, create() uses the @api.model_create_multi decorator:

@api.model_create_multi
def create(self, vals_list):
    # vals_list is a LIST of dicts (even for single record creation)
    # [{'name': 'Book A'}, {'name': 'Book B'}]
    records = super().create(vals_list)
    return records

This decorator tells Odoo that the method accepts a list of value dicts, enabling batch creation. Always use it when overriding create().

Critical Rules for Overriding

  1. Always call super() — If you forget, the original behavior is lost (data not saved, security not checked, etc.)
  2. Return the correct valuecreate() returns recordset, write() returns True, unlink() returns True, copy() returns the new recordset
  3. Don’t break the chain — If your code raises an exception, the super() call never happens. Use try/except if needed, or validate before calling super
  4. Be careful with self in create() — Inside create(), self is an empty recordset (the records don’t exist yet). Use vals_list for pre-processing and records (the return value) for post-processing
  5. Performance in loops — If you override write(), it’s called for every write() operation. Keep your code efficient. Avoid expensive operations inside overrides

Prototype Inheritance (Copy): _inherit With New _name

Prototype inheritance creates a new model that is a copy of an existing model. It copies all fields, methods, and constraints — but the new model has its own database table.

The Syntax

class LibraryBookArchive(models.Model):
    _name = 'library.book.archive'        # NEW model name
    _inherit = 'library.book'             # Copy everything from library.book
    _description = 'Archived Library Book'

    archive_date = fields.Date(string='Archived On')
    archive_reason = fields.Text(string='Reason for Archiving')

What happens:

  • A new table library_book_archive is created in the database
  • It has ALL the fields from library.book (name, isbn, pages, price, state, etc.)
  • PLUS the new fields (archive_date, archive_reason)
  • It has ALL the methods from library.book
  • Changes to library.book in the future do NOT automatically propagate to the copy

When to Use Prototype Inheritance

Prototype inheritance is rarely used. Common scenarios:

  • Creating an archive/history version of a model
  • Creating a simplified version of a complex model
  • When you need the same fields but in a completely separate table

In most cases, extension inheritance is better. Prototype inheritance creates code duplication — if you fix a bug in library.book, you need to fix it in library.book.archive too.

Comparison: Extension vs Prototype

# EXTENSION: adds to existing model (same table)
class ResPartner(models.Model):
    _inherit = 'res.partner'                # No _name → extend res.partner
    new_field = fields.Char()               # Added to res_partner table

# PROTOTYPE: creates a new model (new table)
class LibraryBookArchive(models.Model):
    _name = 'library.book.archive'          # _name + _inherit → new model
    _inherit = 'library.book'               # Copy fields/methods from library.book
    extra_field = fields.Char()             # Added to library_book_archive table

Delegation Inheritance: _inherits — Multi-Table

Delegation inheritance (note the s at the end: _inherits) creates a new model that embeds another model through a Many2one link. The child model transparently accesses the parent’s fields.

The Syntax

class LibraryMember(models.Model):
    _name = 'library.member'
    _inherits = {'res.partner': 'partner_id'}
    _description = 'Library Member'

    partner_id = fields.Many2one(
        'res.partner',
        string='Related Contact',
        required=True,
        ondelete='cascade',
        # cascade: if the partner is deleted, the member is deleted too
    )

    # Fields specific to library members
    membership_number = fields.Char(string='Membership Number')
    member_since = fields.Date(string='Member Since', default=fields.Date.today)
    membership_type = fields.Selection([
        ('basic', 'Basic'),
        ('premium', 'Premium'),
    ], string='Membership Type', default='basic')

How It Works

┌─────────────────────┐         ┌─────────────────────┐
│  library_member      │         │  res_partner         │
├─────────────────────┤         ├─────────────────────┤
│ id                   │         │ id                   │
│ partner_id ──────────┼────────►│ name                 │
│ membership_number    │         │ email                │
│ member_since         │         │ phone                │
│ membership_type      │         │ street               │
│                      │         │ city                  │
│                      │         │ ...                   │
└─────────────────────┘         └─────────────────────┘

The magic: When you access member.name, Odoo transparently reads it from res.partner through the partner_id link. You don’t need to write member.partner_id.name — just member.name works.

# Create a member — automatically creates a res.partner too!
member = env['library.member'].create({
    'name': 'John Doe',               # Stored in res.partner
    'email': 'john@example.com',      # Stored in res.partner
    'membership_number': 'LIB-001',   # Stored in library.member
    'membership_type': 'premium',     # Stored in library.member
})

# Access partner fields directly
print(member.name)        # 'John Doe' — reads from res.partner
print(member.email)       # 'john@example.com' — reads from res.partner
print(member.membership_number)  # 'LIB-001' — reads from library.member

# The underlying partner is accessible too
print(member.partner_id)  # res.partner(42,)
print(member.partner_id.name)  # 'John Doe' (same as member.name)

When to Use Delegation

Use _inherits when:

  • Your model IS a specialized type of another model
  • You want to reuse all fields from the parent
  • You need a separate table for the child-specific fields
  • Creating a child record should also create a parent record

Examples:

  • library.member inherits res.partner — a member IS a contact
  • hr.employee inherits res.partner — an employee IS a contact (this is how Odoo HR works!)
  • product.product inherits product.template — a variant IS a template variant

Delegation vs Extension: When to Use Which

Scenario Use
Add a few fields to res.partner for ALL contacts Extension (_inherit)
Create a specialized entity that IS a contact Delegation (_inherits)
Customize behavior of an existing model Extension (_inherit)
Need a separate record that links to a contact Delegation (_inherits)

For our library, we’ll use extension inheritance to add member fields directly to res.partner. This is simpler and more practical — we don’t need a separate library.member model. We’ll do this in the hands-on section.


View Inheritance Revisited: XPath Selectors in Depth

We introduced view inheritance in Lesson 7. Now let’s go deeper with more advanced XPath patterns.

XPath Cheat Sheet

<!-- ============ FINDING ELEMENTS ============ -->

<!-- By field name -->
//field[@name='isbn']

<!-- By tag and string attribute -->
//group[@string='Book Details']
//page[@string='Description']

<!-- By tag and name attribute -->
//page[@name='page_description']
//button[@name='action_mark_available']

<!-- By tag only (first match) -->
//sheet
//header
//notebook
//form

<!-- By tag and class -->
//div[@class='oe_title']
//div[hasclass('oe_title')]    <!-- Odoo's XPath extension for class matching -->

<!-- Nested elements -->
//notebook/page[@name='page_description']
//header/button[@name='action_mark_available']

<!-- By position (nth element) -->
//group[1]           <!-- First <group> element -->
//group[last()]      <!-- Last <group> element -->
//field[@name='name']/following-sibling::field[1]   <!-- First field after 'name' -->

Position Reference

Position Action Where
inside Add as last child Inside the matched element, at the end
before Add as sibling before Just before the matched element
after Add as sibling after Just after the matched element
replace Replace entirely Remove matched element, put new content
attributes Modify attributes Change attributes of the matched element

Advanced Examples

Add a field after another field:

<xpath expr="//field[@name='phone']" position="after">
    <field name="membership_number"/>
</xpath>

Add a new tab to a notebook:

<xpath expr="//notebook" position="inside">
    <page string="Library" name="page_library">
        <group>
            <field name="is_library_member"/>
            <field name="membership_number"
                   invisible="not is_library_member"/>
            <field name="member_since"
                   invisible="not is_library_member"/>
        </group>
    </page>
</xpath>

Add a button to the header:

<xpath expr="//header" position="inside">
    <button name="action_make_library_member"
            type="object"
            string="Make Library Member"
            class="btn-primary"
            invisible="is_library_member"/>
</xpath>

Replace a field with a different widget:

<xpath expr="//field[@name='category_id']" position="replace">
    <field name="category_id" widget="many2many_tags"
           options="{'color_field': 'color'}"/>
</xpath>

Modify attributes of an existing element:

<xpath expr="//field[@name='email']" position="attributes">
    <attribute name="required">is_library_member</attribute>
    <!-- Email is required if the contact is a library member -->
</xpath>

Remove an element:

<xpath expr="//field[@name='unwanted_field']" position="replace"/>
<!-- Empty replace = remove the element -->

Add content before the sheet:

<xpath expr="//sheet" position="before">
    <div class="alert alert-warning"
         invisible="not is_library_member"
         role="alert">
        <strong>Library Member</strong> —
        Membership #<field name="membership_number"/>
        since <field name="member_since"/>
    </div>
</xpath>

Common XPath Mistakes

Mistake Problem Fix
expr="field[@name='isbn']" Missing // prefix expr="//field[@name='isbn']"
Element not found XPath doesn’t match anything Use Developer Mode → Edit View to inspect the current XML
Multiple matches XPath matches more than one element Make the XPath more specific (add parent context)
Wrong position Content appears in the wrong place Double-check: inside adds children, after adds siblings

Inheriting and Extending Security, Data, and Reports

Inheritance isn’t limited to models and views. You can extend security rules, data files, and reports from other modules.

Extending Security Groups

<!-- Add your model's ACL to an existing group from another module -->
<!-- In ir.model.access.csv: -->
access_library_book_sale_user,library.book.sale.user,model_library_book,sale.group_sale_salesman,1,0,0,0
<!-- Sales people can now READ library books -->

Adding Users to Groups via XML

<!-- Add the admin user to the library manager group -->
<record id="base.user_admin" model="res.users">
    <field name="groups_id" eval="[(4, ref('library_app.group_library_manager'))]"/>
</record>

Extending Existing Record Rules

If you need to modify a record rule from another module:

<!-- Extend the domain of an existing rule -->
<record id="base.res_partner_rule" model="ir.rule">
    <field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>

Caution: Modifying rules from other modules can break things. Prefer creating new rules over modifying existing ones.

Extending Reports

QWeb report templates can be inherited just like views:

<template id="report_invoice_inherit_library"
          inherit_id="account.report_invoice_document">
    <xpath expr="//div[@name='invoice_header']" position="after">
        <div t-if="o.partner_id.is_library_member">
            <strong>Library Member Discount Applied</strong>
        </div>
    </xpath>
</template>

Hands-on: Extend res.partner to Add Library Member Features

Let’s put it all together. We’ll extend the Contacts model to add library member functionality.

Step 1: Create models/res_partner.py

# library_app/models/res_partner.py

from odoo import models, fields, api


class ResPartner(models.Model):
    """Extend the Contact model to add library member features."""
    _inherit = 'res.partner'

    # ---------------------------
    # New Fields
    # ---------------------------
    is_library_member = fields.Boolean(
        string='Library Member',
        default=False,
        help='Check if this contact is a registered library member',
    )

    membership_number = fields.Char(
        string='Membership Number',
        copy=False,
        readonly=True,
        help='Auto-generated membership number',
    )

    member_since = fields.Date(
        string='Member Since',
        readonly=True,
    )

    membership_type = fields.Selection(
        selection=[
            ('basic', 'Basic'),
            ('premium', 'Premium'),
            ('student', 'Student'),
        ],
        string='Membership Type',
        default='basic',
    )

    borrowed_book_ids = fields.Many2many(
        'library.book',
        string='Currently Borrowed Books',
    )

    # ---------------------------
    # Computed Fields
    # ---------------------------
    borrowed_count = fields.Integer(
        string='Books Borrowed',
        compute='_compute_borrowed_count',
    )

    @api.depends('borrowed_book_ids')
    def _compute_borrowed_count(self):
        for partner in self:
            partner.borrowed_count = len(partner.borrowed_book_ids)

    # ---------------------------
    # Business Methods
    # ---------------------------
    def action_make_library_member(self):
        """Register this contact as a library member."""
        for partner in self:
            if not partner.is_library_member:
                # Generate a membership number
                # Format: LIB-XXXX (incrementing)
                last_member = self.env['res.partner'].search(
                    [('membership_number', '!=', False)],
                    order='membership_number DESC',
                    limit=1,
                )
                if last_member and last_member.membership_number:
                    try:
                        last_num = int(last_member.membership_number.split('-')[1])
                        new_num = last_num + 1
                    except (ValueError, IndexError):
                        new_num = 1
                else:
                    new_num = 1

                partner.write({
                    'is_library_member': True,
                    'membership_number': f'LIB-{new_num:04d}',
                    'member_since': fields.Date.today(),
                })

    def action_revoke_membership(self):
        """Revoke this contact's library membership."""
        for partner in self:
            if partner.is_library_member:
                if partner.borrowed_book_ids:
                    raise models.ValidationError(
                        f"Cannot revoke membership for {partner.name} — "
                        f"they still have {len(partner.borrowed_book_ids)} borrowed books!"
                    )
                partner.write({
                    'is_library_member': False,
                })

Step 2: Update models/__init__.py

# library_app/models/__init__.py

from . import book
from . import author
from . import tag
from . import publisher
from . import res_partner    # ← Add this line

Step 3: Create views/res_partner_views.xml

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

    <!-- ============================================ -->
    <!--   EXTEND THE CONTACT FORM VIEW               -->
    <!-- ============================================ -->
    <record id="res_partner_view_form_inherit_library" model="ir.ui.view">
        <field name="name">res.partner.form.inherit.library</field>
        <field name="model">res.partner</field>
        <field name="inherit_id" ref="base.view_partner_form"/>
        <field name="arch" type="xml">

            <!-- Add "Make Library Member" button to the header -->
            <!-- Use the contact form's existing button area -->
            <xpath expr="//div[hasclass('oe_title')]" position="before">
                <div class="alert alert-info text-center"
                     invisible="not is_library_member"
                     role="alert">
                    <strong>Library Member</strong> —
                    #<field name="membership_number" readonly="1"/>
                    | Type: <field name="membership_type"/>
                    | Since: <field name="member_since"/>
                    | Books borrowed: <field name="borrowed_count"/>
                </div>
            </xpath>

            <!-- Add a "Library" tab to the contact form -->
            <xpath expr="//notebook" position="inside">
                <page string="Library" name="page_library"
                      invisible="not is_library_member">
                    <group>
                        <group string="Membership">
                            <field name="is_library_member"/>
                            <field name="membership_number"/>
                            <field name="member_since"/>
                            <field name="membership_type"/>
                        </group>
                        <group string="Statistics">
                            <field name="borrowed_count"/>
                        </group>
                    </group>
                    <group string="Currently Borrowed Books">
                        <field name="borrowed_book_ids" nolabel="1">
                            <list>
                                <field name="name"/>
                                <field name="isbn"/>
                                <field name="author_id"/>
                                <field name="state" widget="badge"/>
                            </list>
                        </field>
                    </group>
                </page>
            </xpath>

            <!-- Add is_library_member to the main section (for visibility toggle) -->
            <xpath expr="//field[@name='phone']" position="after">
                <field name="is_library_member"/>
            </xpath>

        </field>
    </record>

    <!-- ============================================ -->
    <!--   EXTEND THE CONTACT LIST VIEW               -->
    <!-- ============================================ -->
    <record id="res_partner_view_list_inherit_library" model="ir.ui.view">
        <field name="name">res.partner.list.inherit.library</field>
        <field name="model">res.partner</field>
        <field name="inherit_id" ref="base.view_partner_tree"/>
        <field name="arch" type="xml">

            <xpath expr="//field[@name='email']" position="after">
                <field name="membership_number" optional="hide"/>
                <field name="is_library_member" optional="hide"/>
            </xpath>

        </field>
    </record>

    <!-- ============================================ -->
    <!--   EXTEND THE CONTACT SEARCH VIEW             -->
    <!-- ============================================ -->
    <record id="res_partner_view_search_inherit_library" model="ir.ui.view">
        <field name="name">res.partner.search.inherit.library</field>
        <field name="model">res.partner</field>
        <field name="inherit_id" ref="base.view_res_partner_filter"/>
        <field name="arch" type="xml">

            <xpath expr="//filter[@name='type_person']" position="after">
                <separator/>
                <filter name="filter_library_members"
                        string="Library Members"
                        domain="[('is_library_member', '=', True)]"/>
            </xpath>

        </field>
    </record>

    <!-- ============================================ -->
    <!--   ACTION: Library Members (filtered contacts) -->
    <!-- ============================================ -->
    <record id="library_member_action" model="ir.actions.act_window">
        <field name="name">Library Members</field>
        <field name="res_model">res.partner</field>
        <field name="view_mode">list,form</field>
        <field name="domain">[('is_library_member', '=', True)]</field>
        <field name="context">{'default_is_library_member': True}</field>
        <field name="help" type="html">
            <p class="o_view_nocontent_smiling_face">
                No library members yet!
            </p>
            <p>
                Go to Contacts, open a contact, and click
                the "Library Member" checkbox to register them.
            </p>
        </field>
    </record>

</odoo>

Step 4: Add to Menu

In views/menu_views.xml, add a “Members” menu item:

<menuitem
    id="library_member_menu"
    name="Members"
    parent="library_catalog_menu"
    action="library_member_action"
    sequence="30"/>

Step 5: Update __manifest__.py

Add the new view file:

'data': [
    'security/library_security.xml',
    'security/ir.model.access.csv',
    'views/book_views.xml',
    'views/author_views.xml',
    'views/tag_views.xml',
    'views/res_partner_views.xml',     # ← Add this
    'views/menu_views.xml',
],

Step 6: Upgrade and Test

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

What to test:

  1. Contacts → Open any contact — You should see a “Library Member” checkbox near the phone field
  2. Check “Library Member” — A “Library” tab should appear with membership fields
  3. An info banner should appear at the top showing the membership details
  4. Library → Members — Shows only contacts with is_library_member = True
  5. Contact search — A “Library Members” filter should be available

Summary & What’s Next

Key Takeaways

  1. Extension inheritance (_inherit without _name) adds fields and methods to existing models. Same table, same model. This is used 90% of the time.
  2. Prototype inheritance (_inherit + _name) copies a model to create a new one with its own table. Rarely used.
  3. Delegation inheritance (_inherits with ‘s’) embeds another model via a link field. Creates transparent field access. Used for IS-A relationships.
  4. Override methods with super() to extend behavior. Always call super(), always return the correct value. The most commonly overridden methods are create(), write(), unlink(), and copy().
  5. @api.model_create_multi is required when overriding create() in Odoo 18. It receives a list of value dicts.
  6. View inheritance uses inherit_id + XPath to modify existing views. Use position (inside, before, after, replace, attributes) to control where changes go.
  7. Extending res.partner is the most common real-world example. It lets you add business-specific fields to contacts without creating a separate model.

Inheritance Decision Flowchart

Do you want to modify an EXISTING model?
├── Yes → Extension inheritance (_inherit, no _name)
│         "Add fields/methods to res.partner"
│
└── No → Do you want to CREATE a new model?
         ├── That is a SPECIALIZED VERSION of another model?
         │   ├── Shares the same identity (IS the parent)?
         │   │   └── Delegation (_inherits)
         │   │       "library.member IS a res.partner"
         │   │
         │   └── Copies structure but is independent?
         │       └── Prototype (_inherit + _name)
         │           "library.book.archive is LIKE library.book"
         │
         └── That is completely NEW?
             └── Regular model (_name, no _inherit)
                 "library.book is a new concept"

What’s Next?

In Lesson 10: Data Files, Demo Data & Sequences, we’ll learn:

  • XML vs CSV data files — when to use which
  • data vs demo in __manifest__.py
  • noupdate="1" — protecting user-modified data
  • External IDs (XML IDs) — the glue of Odoo data
  • Sequences (ir.sequence) — auto-numbering records
  • Server actions and automated actions

Our library module is now feature-rich. In the next lesson, we’ll populate it with meaningful default data and automate repetitive tasks.


Exercises: 1. Override the unlink() method on library.book to prevent deleting books that are currently borrowed. Raise a UserError with a helpful message. 2. Override the copy() method on library.book to add “(Copy)” to the title and reset the state to ‘draft’ when duplicating a book. 3. Create a prototype inheritance: library.book.wishlist that inherits from library.book and adds a requested_by field (Many2one to res.partner) and a request_date field. This creates a separate wishlist table with the same structure as books. 4. Extend the res.partner search view to add a “Group By → Membership Type” filter for library members. 5. Override write() on res.partner to automatically set member_since to today’s date when is_library_member changes from False to True. 6. Advanced: Use delegation inheritance to create a library.member model that _inherits from res.partner. Add member-specific fields. Create views for it. Compare the user experience with the extension approach we used in the hands-on. Which do you prefer and why?


Previous lesson: Lesson 8 — Security: Access Control in Odoo Next lesson: Lesson 10 — Data Files, Demo Data & Sequences

Leave a Reply

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