Odoo 6 min read Updated
_inherit vs _inherits in Odoo, explained with examples
_inherit extends or copies a model, _inherits links to a parent record by delegation. Examples for all three patterns, view xpath inheritance and gotchas.
Written by the erpfly team, people who build on Frappe and Odoo for a living.
In Odoo, _inherit without a new _name adds fields and methods to an existing model, in the same database table. _inherit with a new _name creates a new model that copies the parent’s fields and methods into its own table. _inherits is different again: the new model gets its own table, but it links to a parent record through a required Many2one and reads and writes the parent’s fields through that link. Most customizations need the first. The other two are for specific situations, and picking them by accident is a common source of odd bugs.
Here’s each one with code that runs on Odoo 17, 18 and 19, followed by view inheritance and the mistakes we see in code reviews.
The three patterns at a glance
| Pattern | Declared with | New table? | Parent fields | Parent methods |
|---|---|---|---|---|
| Extension | _inherit = "res.partner" (no _name) |
No, same table | Added to the model | Overridable with super() |
| Prototype (copy) | _name = "x.y" plus _inherit |
Yes, full copy | Copied into new table | Copied |
| Delegation | _inherits = {"res.partner": "partner_id"} |
Yes, plus parent row | Stored on parent record | Not available |
Extension: _inherit on an existing model
This is what you want nine times out of ten. You’re adding a field to contacts, a check to sale orders, a button to invoices. The model keeps its name, its table and every place in Odoo that already uses it now sees your change.
from odoo import api, fields, models
from odoo.exceptions import ValidationError
class ResPartner(models.Model):
_inherit = "res.partner"
loyalty_tier = fields.Selection(
[("bronze", "Bronze"), ("silver", "Silver"), ("gold", "Gold")],
default="bronze",
)
credit_hold = fields.Boolean()
@api.constrains("credit_hold", "loyalty_tier")
def _check_gold_not_on_hold(self):
for partner in self:
if partner.credit_hold and partner.loyalty_tier == "gold":
raise ValidationError(self.env._("Gold customers can't be put on credit hold without downgrading first."))
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("is_company") and not vals.get("loyalty_tier"):
vals["loyalty_tier"] = "silver"
return super().create(vals_list)
The Python class name doesn’t matter to Odoo. What matters is _inherit, and that your module lists the module that defines the model in its manifest’s depends. For res.partner that’s base, which is implicit, but for sale.order it’s sale.
Always call super() when overriding. Skip it on create or write and you silently break every other module that extends the same method, including Odoo’s own.
Who runs first when several modules override the same method
Each extension becomes a class layered on top of the previous ones, in module load order. The module loaded last sits on top, so its override runs first and its super() call walks down to the modules loaded before it, ending in the original definition. Load order follows depends. If your module depends on sale, you’re guaranteed to sit above it.
What you can’t rely on is the order between two modules that don’t depend on each other. If your override of action_confirm on sale.order must run after a third-party module’s, add that module to your depends. Don’t count on the order you happened to see on your machine, because a different install order on staging can flip it.
Prototype inheritance: _inherit with a new _name
When _name is set to something new and _inherit points to an existing model, Odoo builds a brand-new model. It gets its own table, and the parent’s fields and methods are copied into it. The original model isn’t touched.
You use this all the time without thinking about it, through mixins:
class EquipmentRental(models.Model):
_name = "equipment.rental"
_description = "Equipment Rental"
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char(required=True)
mail.thread and mail.activity.mixin are abstract models, so there’s no parent table. Your rental model simply gains the chatter fields and methods.
Copying a concrete model is rarer, and we’d push back on most uses of it:
class ArchivedPartner(models.Model):
_name = "archived.partner"
_description = "Archived Partner Snapshot"
_inherit = "res.partner"
This creates a second table with every res.partner field, including fields added by modules loaded before yours. It doesn’t get the partner views, so you’re writing those from scratch. Many2many fields that declare an explicit relation table get copied with the same table name, which can collide with the original. And every future module that extends res.partner won’t extend your copy, so the two drift. If you want a small related record, create a lean model with a Many2one instead.
Delegation inheritance: _inherits
_inherits is for “this thing is a partner, plus extra data.” Each record of your model is linked to exactly one parent record. Parent fields appear on your model and are stored on the parent. Odoo’s own examples: res.users delegates to res.partner, and product.product delegates to product.template.
class LibraryMember(models.Model):
_name = "library.member"
_description = "Library Member"
_inherits = {"res.partner": "partner_id"}
partner_id = fields.Many2one(
"res.partner",
required=True,
ondelete="cascade",
)
card_number = fields.Char(required=True, copy=False)
membership_end = fields.Date()
Now you can do this:
member = env["library.member"].create({
"name": "Asha Menon", # stored on res.partner
"email": "[email protected]", # stored on res.partner
"card_number": "LIB-00042", # stored on library_member
})
member.partner_id.name # "Asha Menon"
member.email # read through the link
Creating the member created the partner too. That partner shows up in Contacts, can receive invoices and emails, and can be selected anywhere Odoo asks for a partner. This is the main reason to use delegation: your records join the rest of Odoo without you rebuilding contact handling.
The same thing can be written on the field with delegate=True, which some people find easier to read:
partner_id = fields.Many2one("res.partner", required=True, ondelete="cascade", delegate=True)
What delegation doesn’t give you
Methods. Only fields are delegated. member.message_post() won’t work just because partners have a chatter. Call it on member.partner_id, or add mail.thread to your model with _inherit.
Clean deletes in both directions. ondelete="cascade" removes the member when the partner is deleted. Deleting the member leaves the partner behind. Decide whether that’s what you want, and handle it in unlink if not.
Separate values. If two members point at the same partner, they share its name and email. Changing one changes both.
Cheap searches. Searching or sorting on a delegated field joins the parent table. On a big table, index what you filter on.
Your own field with the same name. If library.member defines its own email field, that one wins and the partner’s email is no longer reachable directly on the member. It’s legal, and it’s confusing for everyone who reads the code later. Pick a different name.
View inheritance with xpath
Models and views are inherited separately. Adding loyalty_tier to res.partner doesn’t put it on any form. For that you inherit the view:
<odoo>
<record id="view_partner_form_loyalty" model="ir.ui.view">
<field name="name">res.partner.form.loyalty</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='vat']" position="after">
<field name="loyalty_tier" invisible="not is_company"/>
<field name="credit_hold"/>
</xpath>
<xpath expr="//field[@name='website']" position="attributes">
<attribute name="required">is_company</attribute>
</xpath>
</field>
</record>
</odoo>
The position values you’ll use: after, before, inside, replace and attributes. For simple cases there’s a shorthand that skips the xpath:
<field name="vat" position="after">
<field name="loyalty_tier"/>
</field>
Remember that since Odoo 17, attrs is gone. Conditions are plain expressions in invisible, readonly and required, as above.
To build a separate view based on an existing one, rather than modifying it, set the mode to primary:
<record id="view_partner_form_member" model="ir.ui.view">
<field name="name">res.partner.form.member</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Membership" name="membership">
<field name="loyalty_tier"/>
</page>
</xpath>
</field>
</record>
Point a window action at this view and the standard partner form stays untouched for everyone else.
Gotchas we keep finding in reviews
Using position="replace" on standard views. It works until another module’s view targets the node you removed, then that module fails to load. Hide with position="attributes" and invisible="1" instead of replacing.
Fragile xpaths. //group[2]/field[3] breaks on the next Odoo version. Anchor on @name wherever possible, and when there’s no name, hasclass() is better than a position index.
An xpath that matches more than one node. Only the first match is used. If a field appears twice in the view, your change lands in one place and you’ll wonder why the other didn’t change.
Missing depends. Your extension loads before the module that defines the model or view. You get “model not found” or “Element cannot be located in parent view” on a clean install, but not on your laptop.
Using _inherits when you meant _inherit. Someone wants extra fields on partners, writes _inherits, and ends up with a new model and a second menu nobody asked for.
Overriding display_name the old way. name_get was deprecated in Odoo 17 (it still runs, with a warning) and removed in 18. Override _compute_display_name instead, and add @api.depends for the fields it uses. Be careful doing this on res.partner, whose display name already depends on context; extend it with super() rather than replacing it.
@api.depends("name", "card_number")
def _compute_display_name(self):
for member in self:
member.display_name = f"{member.name} ({member.card_number})" if member.card_number else member.name
Picking the right one
Adding to something Odoo already has: _inherit alone. Building a new document that needs chatter or activities: new _name with the mixins in _inherit. Building something that is a contact, product or user with extra data: _inherits. Copying a concrete model wholesale: almost never.
If you’re still deciding whether any code is needed, our piece on Odoo Studio vs a custom module covers when clicks are enough, and the step-by-step guide to creating an Odoo 19 module shows the files around these models.
erpfly picks between these patterns for you when it generates an addon, and explains the choice in the pull request so a reviewer can disagree. See Odoo module development for new models, or Odoo customization when you’re extending what’s there.
Sources
The official documentation and source code this page was checked against.
- 01 ORM API, Odoo 19 developer documentation odoo.com
- 02 View records, Odoo 19 developer documentation odoo.com
- 03 ORM Changelog, Odoo 19 developer documentation odoo.com
- 04 ir_ui_view.py (17.0), Odoo source code github.com
Terms in this post