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_appmodule 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.membermodel with name, email, phone, address… (duplicating fields that already exist inres.partner) - Option B: Add
membership_numberandborrow_historyto the existingres.partnermodel 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.partnerautomatically 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.partnermodel - It adds the
membership_numbercolumn to theres_partnertable - ALL existing
res.partnerrecords now have this field (it’sNULL/Falsefor existing records) - The
res.partnermodel 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
- Always call
super()— If you forget, the original behavior is lost (data not saved, security not checked, etc.) - Return the correct value —
create()returns recordset,write()returnsTrue,unlink()returnsTrue,copy()returns the new recordset - 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 - Be careful with
selfincreate()— Insidecreate(),selfis an empty recordset (the records don’t exist yet). Usevals_listfor pre-processing andrecords(the return value) for post-processing - Performance in loops — If you override
write(), it’s called for everywrite()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_archiveis 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.bookin 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.memberinheritsres.partner— a member IS a contacthr.employeeinheritsres.partner— an employee IS a contact (this is how Odoo HR works!)product.productinheritsproduct.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:
- Contacts → Open any contact — You should see a “Library Member” checkbox near the phone field
- Check “Library Member” — A “Library” tab should appear with membership fields
- An info banner should appear at the top showing the membership details
- Library → Members — Shows only contacts with
is_library_member = True - Contact search — A “Library Members” filter should be available
Summary & What’s Next
Key Takeaways
- Extension inheritance (
_inheritwithout_name) adds fields and methods to existing models. Same table, same model. This is used 90% of the time. - Prototype inheritance (
_inherit+_name) copies a model to create a new one with its own table. Rarely used. - Delegation inheritance (
_inheritswith ‘s’) embeds another model via a link field. Creates transparent field access. Used for IS-A relationships. - Override methods with
super()to extend behavior. Always callsuper(), always return the correct value. The most commonly overridden methods arecreate(),write(),unlink(), andcopy(). @api.model_create_multiis required when overridingcreate()in Odoo 18. It receives a list of value dicts.- View inheritance uses
inherit_id+ XPath to modify existing views. Useposition(inside, before, after, replace, attributes) to control where changes go. - Extending
res.partneris 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
datavsdemoin__manifest__.pynoupdate="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 onlibrary.bookto prevent deleting books that are currently borrowed. Raise aUserErrorwith a helpful message. 2. Override thecopy()method onlibrary.bookto add “(Copy)” to the title and reset the state to ‘draft’ when duplicating a book. 3. Create a prototype inheritance:library.book.wishlistthat inherits fromlibrary.bookand adds arequested_byfield (Many2one tores.partner) and arequest_datefield. This creates a separate wishlist table with the same structure as books. 4. Extend theres.partnersearch view to add a “Group By → Membership Type” filter for library members. 5. Overridewrite()onres.partnerto automatically setmember_sinceto today’s date whenis_library_memberchanges fromFalsetoTrue. 6. Advanced: Use delegation inheritance to create alibrary.membermodel that_inheritsfromres.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
