Skip to content

Odoo AbstractModel Patterns: When and How to Use _abstract = True

DeployMonkey Team · March 23, 2026 10 min read

What Is AbstractModel in Odoo?

An AbstractModel in Odoo is a model that has no database table. You declare it with _name and set no _table — instead, child models that inherit from it get the fields and methods. The key attribute is using models.AbstractModel as the base class rather than models.Model.

from odoo import models, fields, api

class MailThread(models.AbstractModel):
    _name = 'mail.thread'
    _description = 'Mail Thread'

    message_ids = fields.One2many('mail.message', 'res_id', string='Messages')
    message_follower_ids = fields.One2many('mail.followers', 'res_id', string='Followers')

    def message_post(self, **kwargs):
        # shared logic for all models that inherit mail.thread
        ...

Any model that declares _inherit = 'mail.thread' gets these fields and methods added to its own table. No separate mail_thread table exists in the database.

When to Use AbstractModel vs Model

Use AbstractModel when you want to share behavior across unrelated models without creating a database table. Common scenarios:

  • Mixins: Reusable functionality like mail tracking, activity scheduling, rating, portal access
  • Shared field sets: Common fields like company_id, currency_id, active
  • Common business logic: Validation methods, compute logic, access helpers
  • Report helpers: Shared formatting or calculation methods

Do NOT use AbstractModel when:

  • You need to store records in a separate table
  • You need to query the abstract model directly
  • You want a standalone model with its own views

Pattern 1: The Mixin Pattern

The most common AbstractModel pattern is the mixin. Odoo core uses this extensively:

class TenantMixin(models.AbstractModel):
    _name = 'dm.tenant.mixin'
    _description = 'Tenant Isolation Mixin'

    tenant_id = fields.Many2one('dm.customer', required=True, index=True)

    def sudo_search_tenant(self, tenant_id, domain=None, **kwargs):
        full_domain = [('tenant_id', '=', tenant_id)] + (domain or [])
        return self.sudo().search(full_domain, **kwargs)

    def sudo_browse_tenant(self, tenant_id, record_id):
        record = self.sudo().browse(record_id)
        if not record.exists() or record.tenant_id.id != tenant_id:
            return self.browse()  # empty recordset
        return record

Now any model that needs tenant isolation simply inherits:

class DmServer(models.Model):
    _name = 'dm.server'
    _inherit = ['dm.tenant.mixin', 'mail.thread']
    # tenant_id, sudo_search_tenant, sudo_browse_tenant all available

Pattern 2: Computed Field Helpers

AbstractModels can provide shared computed field logic:

class CurrencyMixin(models.AbstractModel):
    _name = 'account.currency.mixin'
    _description = 'Currency Mixin'

    company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
    currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
    company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id')

    def _format_currency_amount(self, amount, currency=None):
        currency = currency or self.currency_id
        return formatLang(self.env, amount, currency_obj=currency)

Pattern 3: Constraint Sharing

Share validation logic across models:

class DateRangeMixin(models.AbstractModel):
    _name = 'date.range.mixin'
    _description = 'Date Range Validation'

    date_start = fields.Date(required=True)
    date_end = fields.Date(required=True)

    @api.constrains('date_start', 'date_end')
    def _check_date_range(self):
        for record in self:
            if record.date_start and record.date_end:
                if record.date_start > record.date_end:
                    raise ValidationError('End date must be after start date.')

Pattern 4: Report Abstract Model

Odoo uses AbstractModel for report classes that generate PDFs or HTML:

class ReportSaleOrder(models.AbstractModel):
    _name = 'report.sale.report_saleorder'
    _description = 'Sale Order Report'

    def _get_report_values(self, docids, data=None):
        docs = self.env['sale.order'].browse(docids)
        return {
            'doc_ids': docids,
            'doc_model': 'sale.order',
            'docs': docs,
            'data': data,
        }

This pattern is mandatory for QWeb PDF reports. The model name must follow the convention report.module_name.template_name.

Pattern 5: API Endpoint Mixin

Share API response formatting across controllers:

class ApiResponseMixin(models.AbstractModel):
    _name = 'api.response.mixin'
    _description = 'API Response Formatting'

    def _to_api_dict(self):
        """Override in each model to define API representation."""
        return {'id': self.id, 'name': self.display_name}

    def _to_api_list(self, records):
        return [rec._to_api_dict() for rec in records]

    def _paginated_response(self, domain, page=1, limit=20):
        total = self.search_count(domain)
        records = self.search(domain, offset=(page-1)*limit, limit=limit)
        return {
            'items': self._to_api_list(records),
            'total': total,
            'page': page,
            'pages': (total + limit - 1) // limit
        }

Gotchas and Pitfalls

  • No direct search: You cannot call self.env['abstract.model'].search([]) — there is no table
  • Field conflicts: If two mixins define the same field name with different types, you get errors. Always use unique prefixes
  • Compute methods: Computed fields in AbstractModels must handle all inheriting models. Do not assume specific fields exist unless declared in the abstract
  • Access rights: AbstractModels do not need ir.model.access entries — the concrete model handles access
  • _inherit vs _name: If you set both _name and _inherit to different values, you create a new model that copies the parent. If only _inherit, you extend the existing model

Core AbstractModels You Should Know

ModelPurpose
mail.threadChatter, messaging, followers
mail.activity.mixinActivity scheduling
portal.mixinPortal access URLs
rating.mixinCustomer satisfaction ratings
image.mixinImage resize handling
format.address.mixinAddress formatting per country
utm.mixinMarketing campaign tracking

Understanding AbstractModel patterns is essential for building maintainable Odoo modules. They enforce DRY principles while keeping database schemas clean.