What Are Mixins in Odoo?
Mixins are abstract models that add reusable behavior to other models through inheritance. Instead of duplicating code across models, you define the behavior once in a mixin and inherit it wherever needed. Odoo uses mixins extensively — the chatter (mail.thread), activity scheduling (mail.activity.mixin), and portal access (portal.mixin) are all mixins.
Built-in Mixins
mail.thread — Chatter and Messaging
The most commonly used mixin. Adds the chatter widget, message logging, email notifications, and field tracking:
class MyModel(models.Model):
_name = 'my.model'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(tracking=True)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
], tracking=True)
partner_id = fields.Many2one('res.partner', tracking=True)Key features provided:
message_post()— Send messages and log notestracking=Trueon fields — Auto-log field value changesmessage_subscribe()— Manage followersmessage_notify()— Send notifications without logging- Email gateway integration for incoming messages
mail.activity.mixin — Scheduled Activities
Adds the ability to schedule activities (to-dos, calls, meetings):
_inherit = ['mail.thread', 'mail.activity.mixin']Provides:
activity_schedule()— Create activities programmaticallyactivity_feedback()— Mark activities as done with feedbackactivity_unlink()— Cancel activities- Activity deadline tracking and overdue alerts
- Kanban activity widget with color-coded deadlines
portal.mixin — Portal Access
Enables portal access URLs and sharing:
_inherit = ['portal.mixin']
def _compute_access_url(self):
for record in self:
record.access_url = '/my/model/%s' % record.idProvides:
access_url— Computed URL for portal accessaccess_token— Security token for URL-based accessget_portal_url()— Generate shareable portal link_portal_ensure_token()— Generate token if missing (call before get_portal_url)
image.mixin — Image Handling
Adds standardized image fields with automatic resizing:
_inherit = ['image.mixin']Provides: image_1920, image_1024, image_512, image_256, image_128 — all computed from the original image.
Building Custom Mixins
Abstract Model Pattern
Create a mixin as an AbstractModel:
class TimestampMixin(models.AbstractModel):
_name = 'timestamp.mixin'
_description = 'Timestamp Mixin'
created_by = fields.Many2one(
'res.users', default=lambda self: self.env.uid,
readonly=True,
)
approved_by = fields.Many2one('res.users', readonly=True)
approved_date = fields.Datetime(readonly=True)
def action_approve(self):
self.write({
'approved_by': self.env.uid,
'approved_date': fields.Datetime.now(),
})Using Custom Mixins
class PurchaseRequest(models.Model):
_name = 'purchase.request'
_inherit = ['timestamp.mixin', 'mail.thread']
name = fields.Char(required=True)
amount = fields.Float()Mixin with Constraints
class DateRangeMixin(models.AbstractModel):
_name = 'date.range.mixin'
_description = 'Date Range Mixin'
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_end < record.date_start:
raise ValidationError(
"End date must be after start date."
)Mixin Inheritance Order
Python's MRO (Method Resolution Order) matters when using multiple mixins:
# Methods resolved left to right
_inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin']If two mixins define the same method, the leftmost one wins. Use super() to chain behavior through all parents.
Common Patterns
Soft Delete Mixin
class SoftDeleteMixin(models.AbstractModel):
_name = 'soft.delete.mixin'
_description = 'Soft Delete'
active = fields.Boolean(default=True)
deleted_at = fields.Datetime(readonly=True)
deleted_by = fields.Many2one('res.users', readonly=True)
def action_soft_delete(self):
self.write({
'active': False,
'deleted_at': fields.Datetime.now(),
'deleted_by': self.env.uid,
})Sequence Mixin
class SequenceMixin(models.AbstractModel):
_name = 'sequence.mixin'
_description = 'Auto Sequence'
reference = fields.Char(
readonly=True, copy=False, default='New',
)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('reference', 'New') == 'New':
vals['reference'] = self.env['ir.sequence'].next_by_code(
self._name
) or 'New'
return super().create(vals_list)Best Practices
- Use
AbstractModelfor mixins — they do not create database tables - Keep mixins focused on a single behavior
- Always call
super()when overriding mixin methods - Document which fields and methods the mixin provides
- Test mixins independently with a test model that inherits them