All posts
Developer9 min read

Abstract models and mixins in Odoo: mail.thread, mail.activity.mixin, and when to use each

Odoo ships several reusable abstract models that add chatter, activity tracking, and other cross-cutting features to any custom model. Understanding when to inherit from models.AbstractModel versus models.Model, and how mixins compose, saves hours of debugging and keeps your module architecture clean.

What are abstract models?#

In Odoo, every model that stores data inherits from models.Model. But the framework also provides models.AbstractModel - a base class for models that are never instantiated directly and have no database table. Abstract models exist purely to be inherited by concrete models, contributing fields, methods, and computed logic without creating their own table.

The most important abstract models in the Odoo core are the messaging mixins: mail.thread and mail.activity.mixin.

models.AbstractModel vs models.Model#

The distinction is straightforward:

  • models.Model - creates a PostgreSQL table, stores records, supports all ORM operations
  • models.AbstractModel - no table, no records; used exclusively as a mixin base

When you define a class that inherits from models.AbstractModel, Odoo registers it in the registry but creates no database structure. Any model that later does _inherit = ['your.abstract.model'] will pick up all fields and methods.

A practical example: the mail.thread model itself is defined as an abstract model in addons/mail/models/mail_thread.py. Your sale.order model picks it up by listing it in _inherit.

Adding chatter to a custom model#

The chatter widget - the log + follower + message panel - comes from mail.thread. Adding it to a custom model takes two steps.

Step 1: Python

python
from odoo import models, fields

class ProjectTask(models.Model):
    _name = 'my.task'
    _description = 'My Task'
    _inherit = ['mail.thread', 'mail.activity.mixin']

    name = fields.Char(required=True, tracking=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('done', 'Done'),
    ], default='draft', tracking=True)

Setting tracking=True on a field tells mail.thread to log a chatter message whenever that field changes. It works on Char, Selection, Many2one, and most scalar field types.

Step 2: XML view

xml
<form string="My Task">
  <sheet>
    <!-- your fields -->
  </sheet>
  <div class="oe_chatter">
    <field name="message_follower_ids"/>
    <field name="activity_ids"/>
    <field name="message_ids"/>
  </div>
</form>

The three fields (message_follower_ids, activity_ids, message_ids) are all contributed by the mixins. Without them in the view, the chatter panel will not render even if the mixin is inherited.

mail.activity.mixin#

mail.activity.mixin adds the activity scheduling panel - the buttons for scheduling calls, emails, to-dos, and custom activity types. It depends on mail.thread being present, so always inherit both together.

Key methods it exposes:

  • activity_schedule(act_type_xmlid, date_deadline, note, user_id) - create an activity programmatically
  • activity_feedback(act_type_xmlid) - mark activities of a type as done
  • activity_unlink(act_type_xmlid) - cancel and remove activities

Calling self.activity_schedule('mail.mail_activity_data_todo', date_deadline=fields.Date.today()) from any model method will create a to-do activity visible in the activity view and the chatter.

Writing your own abstract model#

Abstract models are useful when you want to share logic across several unrelated models. A common pattern is a "timestamped" mixin:

python
class TimestampedMixin(models.AbstractModel):
    _name = 'my.timestamped.mixin'
    _description = 'Adds created/updated timestamps with user tracking'

    created_by = fields.Many2one('res.users', default=lambda self: self.env.user, readonly=True)
    updated_by = fields.Many2one('res.users', readonly=True)

    def write(self, vals):
        vals['updated_by'] = self.env.uid
        return super().write(vals)

Any model that does _inherit = ['my.timestamped.mixin'] gets these fields and the overridden write automatically. The mixin itself never appears in menus or search views.

Common pitfalls#

Forgetting the view fields. Inheriting mail.thread without adding message_ids to the view will silently skip the chatter UI. Always add all three chatter fields.

Double-inheriting the same abstract. If model A inherits mail.thread and model B inherits A and also lists mail.thread in its own _inherit, the fields are registered twice. Python's MRO handles this correctly, but it generates a warning. Remove the redundant entry from B.

_name vs _inherit in abstract models. When you write your own abstract model, give it a _name. When a concrete model picks it up, add that name to _inherit. Do not set _name in a concrete model that is only extending an existing model - that would create a new model instead of extending.

Tracking on Many2many fields. tracking=True does not work on Many2many. It only works on scalar fields and Many2one. For tracking Many2many changes, override write and post a message manually using self.message_post().

When to use AbstractModel vs plain Model#

Use AbstractModel when the behavior needs to be grafted onto multiple unrelated models (like chatter, timestamps, or archival logic). Use a regular Model when the feature has its own records and business logic that stands alone.

A good rule of thumb: if you would never query the model directly (no self.env['your.model'].search(...) that makes sense in isolation), it belongs as an abstract model.

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

Get started free