All posts
Developer8 min read

Odoo mixins and abstract models: mail.thread, activity.mixin, and building reusable behavior

Odoo ships several built-in abstract models that add rich behavior to any model with a single _inherit line: chatter threads, activity scheduling, website publication, and image handling. This guide covers what each mixin provides, how to combine them, and how to build your own reusable abstract models.

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.

python
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#

python
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#

python
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 followers
  • mail.mt_comment: public message - emailed to all followers subscribed to comments

Define custom subtypes for model-specific events (e.g., "Stage Changed"):

xml
<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#

python
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 call
  • mail.mail_activity_data_email - email
  • mail.mail_activity_data_todo - to-do
  • mail.mail_activity_data_meeting - meeting

Marking Activities Done#

python
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.

python
class MyPublicRecord(models.Model):
    _name = 'my.public.record'
    _inherit = ['website.published.mixin']

    name = fields.Char()

The mixin provides:

  • website_published: boolean field
  • website_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:

python
def _website_url(self, name, arg):
    return '/my-records/%s' % self.id

image.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
python
class MyModel(models.Model):
    _name = 'my.model'
    _inherit = ['image.mixin']

Odoo automatically generates the smaller sizes when image_1920 is set. Display in views:

xml
<field name="image_128" widget="image" class="oe_avatar"/>

Building a Custom Abstract Model#

python
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:

python
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:

  1. Put mail.thread and mail.activity.mixin last (they are designed to be composed)
  2. Put your business mixins first
  3. Always include models.Model implicitly at the end of the MRO

Common Mistakes#

1. Inheriting mail.thread without adding the view widget.

The chatter only appears if you add

... to your form view. Inheriting the mixin alone does not add the UI.

xml
<div class="oe_chatter">
  <field name="message_follower_ids"/>
  <field name="activity_ids"/>
  <field name="message_ids"/>
</div>

2. Using tracking=True on non-stored computed fields.

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 __manifest__.py models.

Abstract models must still be in a Python file imported by __init__.py. If the file is not imported, the abstract model does not exist and _inherit references to it fail at startup.


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.

Get started free