Abstract Models in Odoo#
An abstract model (_abstract = True) defines fields and methods that other models can inherit, without creating its own database table. When a concrete model inherits an abstract model, it gets all of its fields and methods merged in.
This is the standard pattern for adding cross-cutting features like messaging, activities, or website publication to any model.
mail.thread: the Chatter#
Inheriting mail.thread adds the chatter panel to any model - the message log, followers list, and notification system.
class MyModel(models.Model):
_name = 'my.model'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'My Model'
name = fields.Char(tracking=True)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
], tracking=True)Setting tracking=True on a field logs changes to that field in the chatter automatically. When a user writes state from 'draft' to 'confirmed', a chatter message appears: "State: Draft → Confirmed".
Posting Messages Programmatically#
record.message_post(
body="Order has been escalated to management.",
subtype_xmlid='mail.mt_note', # internal note (no email)
# subtype_xmlid='mail.mt_comment', # public comment (sends email to followers)
partner_ids=[partner.id], # additional recipients
)Adding Followers#
record.message_subscribe(partner_ids=[partner.id])
record.message_unsubscribe(partner_ids=[partner.id])Subtypes#
Message subtypes control notification routing. The two most common:
mail.mt_note: internal note - visible in chatter, not emailed to followersmail.mt_comment: public message - emailed to all followers subscribed to comments
Define custom subtypes for model-specific events (e.g., "Stage Changed"):
<record id="mt_my_model_stage_changed" model="mail.message.subtype">
<field name="name">Stage Changed</field>
<field name="res_model">my.model</field>
<field name="default" eval="True"/>
<field name="description">Stage changed</field>
</record>mail.activity.mixin: Activities#
mail.activity.mixin adds the Activities scheduling system to a model - the to-do items visible in kanban views as colored circles.
Inheriting it together with mail.thread (as shown above) is the standard pattern.
Scheduling an Activity Programmatically#
record.activity_schedule(
'mail.mail_activity_data_call', # activity type xmlid
date_deadline=fields.Date.today() + timedelta(days=3),
summary='Follow up on quote',
note='Call to confirm pricing.',
user_id=self.env.user.id,
)Common built-in activity types:
mail.mail_activity_data_call- phone callmail.mail_activity_data_email- emailmail.mail_activity_data_todo- to-domail.mail_activity_data_meeting- meeting
Marking Activities Done#
record.activity_ids.filtered(
lambda a: a.activity_type_id.xmlid == 'mail.mail_activity_data_call'
).action_done()website.published.mixin: Publication Toggle#
website.published.mixin adds a website_published boolean and the publication toggle visible in the website editor.
class MyPublicRecord(models.Model):
_name = 'my.public.record'
_inherit = ['website.published.mixin']
name = fields.Char()The mixin provides:
website_published: boolean fieldwebsite_url: computed URL (override to point to your record's page)_website_url: method returning the canonical URL
Override _website_url to point to your controller route:
def _website_url(self, name, arg):
return '/my-records/%s' % self.idimage.mixin: Image Fields#
image.mixin adds a standard set of image fields at multiple resolutions:
image_1920: full size (up to 1920px)image_1024: medium (auto-generated)image_512: thumbnail (auto-generated)image_256,image_128: smaller sizes
class MyModel(models.Model):
_name = 'my.model'
_inherit = ['image.mixin']Odoo automatically generates the smaller sizes when image_1920 is set. Display in views:
<field name="image_128" widget="image" class="oe_avatar"/>Building a Custom Abstract Model#
class ReviewMixin(models.AbstractModel):
_name = 'review.mixin'
_description = 'Review Mixin'
review_state = fields.Selection([
('pending', 'Pending Review'),
('approved', 'Approved'),
('rejected', 'Rejected'),
], default='pending')
reviewed_by = fields.Many2one('res.users')
reviewed_at = fields.Datetime()
def action_approve(self):
self.write({
'review_state': 'approved',
'reviewed_by': self.env.user.id,
'reviewed_at': fields.Datetime.now(),
})
def action_reject(self):
self.write({
'review_state': 'rejected',
'reviewed_by': self.env.user.id,
'reviewed_at': fields.Datetime.now(),
})Any model can then inherit this mixin:
class ExpenseReport(models.Model):
_name = 'expense.report'
_inherit = ['review.mixin', 'mail.thread']Mixin Combination Order#
When inheriting multiple mixins, order matters for MRO (Method Resolution Order). The leftmost class takes precedence if two mixins define the same method. As a convention:
- Put
mail.threadandmail.activity.mixinlast (they are designed to be composed) - Put your business mixins first
- Always include
models.Modelimplicitly at the end of the MRO
Common Mistakes#
1. Inheriting mail.thread without adding the view widget.
The chatter only appears if you add 2. Using Tracking requires a database column to compare old vs new values. Non-stored computed fields have no column - tracking them silently does nothing. 3. Abstract model not listed in Abstract models must still be in a Python file imported by ERPeek can identify which mixins your custom models inherit, which fields have tracking enabled, and which form views are missing the chatter widget. Useful for code audits before go-live. See the contact page. Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language. Odoo views support three visibility mechanisms: security groups (render the field or not), domain filters (filter records displayed), and dynamic attrs (conditionally show/hide elements based on field values). Each controls a different layer of what users see. Three layers of profiling cover 95% of Odoo performance problems: SQL query plans, Python execution time, and Odoo's own built-in profiler. Here is how to use all three and what each one finds.<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>tracking=True on non-stored computed fields.__manifest__.py models.__init__.py. If the file is not imported, the abstract model does not exist and _inherit references to it fail at startup.Related articles
Odoo field-level security, domain filters, and invisible conditions in views
Profiling slow Odoo modules: PostgreSQL EXPLAIN, Python profilers, and the Odoo profiler

