All posts
Developer11 min read

Odoo model inheritance: _inherit vs _inherits and when to use each

Odoo has three distinct inheritance mechanisms that look similar but behave completely differently. Get any of them wrong and you end up with duplicate tables, missing fields, or broken views. This guide disambiguates all three with concrete examples.

The Three Inheritance Mechanisms#

Odoo offers three mechanisms for reusing model logic. They share similar-looking syntax but produce completely different database structures and runtime behaviour.

MechanismSyntaxDatabase effect
Classical (extension)_inherit = 'existing.model'No new table - extends in place
Prototype (copy)_inherit = 'existing.model' + _name = 'new.model'New table, copied field definitions
Delegation_inherits = {'existing.model': 'field_id'}New table + foreign key to parent

Picking the wrong one is the most common inheritance bug in custom Odoo modules.


Classical Inheritance (Extension)#

python
class SaleOrder(models.Model):
    _inherit = 'sale.order'

    project_id = fields.Many2one('project.project', string='Linked Project')

What this does:

  • Adds project_id to the existing sale_order table
  • Every SaleOrder instance anywhere in the system gains the new field
  • There is no new table

When to use it:

  • You want to extend a standard or third-party model with new fields, methods, or overrides
  • This is the 90% case in custom Odoo development

What you can override:

  • Field definitions (change string, required, domain, etc.)
  • Methods (create, write, action_confirm, etc.) using super()
  • Views (via inherit_id in XML)

Gotcha - overriding a field completely:

python
# This ADDS project_id; it does not replace the existing field
partner_id = fields.Many2one('res.partner', required=True)
# This only changes required  -  the field itself is inherited from the parent

You cannot remove a field from a model via classical inheritance. You can make it invisible in views, but the column stays in the database.


Prototype Inheritance (Copy)#

python
class HrLeaveAllocation(models.Model):
    _name = 'hr.leave.allocation'
    _inherit = ['hr.leave.mixin', 'mail.thread']
    _description = 'Time Off Allocation'

When both _name and _inherit are set, Odoo copies the field definitions from the parent model into the new model's own table. The two models then evolve independently.

What this does:

  • Creates a new table (hr_leave_allocation)
  • Copies field definitions (not data) from hr.leave.mixin at class-load time
  • The new model is completely independent at runtime - changes to parent records do NOT propagate

When to use it:

  • You need a new model that starts with the same shape as an existing one
  • Common for creating a "draft" or "template" variant of a document model
  • Inheriting from abstract models and mixins (e.g., mail.thread, mail.activity.mixin)

Gotcha - inheriting from concrete models:

If you prototype-inherit a concrete model (one with its own _name), you get its fields but not its records. Developers sometimes expect this to share data - it does not.

python
class MyInvoice(models.Model):
    _name = 'my.invoice'
    _inherit = 'account.move'   # COPIES field definitions only
    # my_invoice table is completely separate from account_move

Delegation Inheritance#

python
class ProjectTask(models.Model):
    _name = 'project.task'
    _inherits = {'project.project': 'project_id'}

    project_id = fields.Many2one('project.project', required=True, ondelete='cascade')

Delegation uses _inherits (plural). Odoo stores the child's own fields in the child table and delegates field lookups for the parent's fields via a foreign key.

What this does:

  • Child table (project_task) has its own columns
  • Field reads for parent fields transparently query the linked parent record
  • Writes to parent fields update the linked parent record (or create one if auto_join is used)

When to use it:

  • You want to compose a new model from an existing one without duplicating data
  • The canonical Odoo example is res.users delegating to res.partner - a User is a Partner, and partner fields are stored in the res_partner table

Gotcha - ORM queries do not span tables automatically:

python
# This works
task = env['project.task'].browse(42)
print(task.name)  # reads from project_project via delegation

# This does NOT work  -  domain fields must exist in the child table
env['project.task'].search([('name', 'like', 'Alpha')])
# raises: Invalid field 'name' on model 'project.task'
# unless you use auto_join or join the tables manually

Mixing Classical and Mixin Inheritance#

Most real models inherit from several sources at once:

python
class SaleOrder(models.Model):
    _name = 'sale.order'
    _inherit = [
        'mail.thread',        # adds chatter + message_post()
        'mail.activity.mixin', # adds activity buttons
        'portal.mixin',       # adds portal_url, _compute_access_url
        'rating.mixin',       # adds rating_ids + rating_get_partner_id()
    ]

When _name is set alongside _inherit as a list, each entry in the list is prototype-inherited. The mixins (mail.thread, etc.) are abstract models - they have no _name of their own - so their fields and methods are merged into the target model's table.

Order matters for MRO conflicts:

Python's Method Resolution Order applies. If two mixins define the same method, the leftmost one wins. When overriding with super(), the call chain follows MRO order.


Common Patterns and Anti-Patterns#

Pattern: safely extending create/write#

python
class SaleOrder(models.Model):
    _inherit = 'sale.order'

    def action_confirm(self):
        res = super().action_confirm()
        self._sync_project()
        return res

    def _sync_project(self):
        for order in self:
            if order.project_id:
                order.project_id.write({'partner_id': order.partner_id.id})

Always call super() and return its result unless you have a deliberate reason not to. Skipping super() breaks any other module that extended the same method.

Anti-pattern: storing computed data in delegation fields#

python
# BAD: writing to a delegation field inside a compute method
# triggers a write on the parent record for every record in self
class MyModel(models.Model):
    _inherits = {'res.partner': 'partner_id'}

    @api.depends('amount')
    def _compute_label(self):
        for rec in self:
            rec.name = f"Order {rec.amount}"  # writes to res.partner.name!

Delegation writes propagate to the parent table. Keep compute methods restricted to the child's own fields.

Anti-pattern: inheriting a concrete model to share records#

python
# BAD: expecting prototype inheritance to share data with the parent
class MyExtendedPartner(models.Model):
    _name = 'my.extended.partner'
    _inherit = 'res.partner'
    # This does NOT give you access to existing res.partner records
    # It creates a completely separate table

Use classical inheritance (_inherit without _name) when you want to share records.


Choosing the Right Mechanism: Decision Tree#

  1. Do you want to extend an existing model without creating a new one?

→ Classical: _inherit = 'existing.model' (no _name)

  1. Do you want a new model that starts with the same shape but stores separate data?

→ Prototype: _inherit = 'existing.model' + _name = 'new.model'

  1. Do you want a new model that transparently exposes an existing model's fields via a FK?

→ Delegation: _inherits = {'existing.model': 'field_id'}

  1. Do you want to add chatter, activities, or other cross-cutting behaviour?

→ Prototype + abstract mixin: _inherit = ['mail.thread', 'mail.activity.mixin'] with your own _name


Checking What a Model Actually Inherits#

ERPeek can answer "what does this model inherit?" across any installed Odoo version:

What models does sale.order inherit from, and which fields come from each?

The answer includes the full MRO chain, which fields are native vs. inherited from mixins, and which methods are overridden. Useful before adding a new _inherit to an already-complex model.


Inheritance bugs are often silent - a missing super() call or a wrong mechanism choice won't raise an error at module load time, only at runtime when a specific operation is triggered. The ERPeek auditing guide covers how to systematically surface these issues before they hit production.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free