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 operationsmodels.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
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
<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 programmaticallyactivity_feedback(act_type_xmlid)- mark activities of a type as doneactivity_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:
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.

