What exactly is a Mixin in Odoo?
If you have ever spent some time developing custom Odoo modules, you have probably encountered a situation where two completely different models required the same field or method. Perhaps you have added a notes field to five models one by one, or you have copy-pasted the same validation code into three different files. That is precisely where mixins were created to solve this.
A mixin is an abstract model that defines fields and methods that you want to share with other models. A mixin does not define a database table. It just exists and waits to be added to any model that might need it.
There are several mixin models that are provided by Odoo that you are probably already using without even thinking about it:
- mail.thread — brings in the chatter panel and full message logging
- mail.activity.mixin — lets users schedule calls, tasks, and follow-up emails on any record
- portal.mixin — controls whether a record is visible through the customer portal
- sequence.mixin — auto-generates reference numbers like SO/2025/00123
- rating.mixin — adds a star rating system to any model you attach it to
Every time you wrote _inherit = ['mail.thread'] on a model, you were using a mixin. Building your own custom mixin follows the same logic — you create a reusable piece once and drop it into any model that requires it.
Why This Matters More in Odoo 19
Odoo 19 is based on Python 3.12 and continues to progress towards a more module-based approach. Projects are becoming larger, and teams are working with an increasing number of models simultaneously. The cost of duplicate logic is growing.
When you duplicate code across models, a single change becomes a multi-file edit. Fixing a bug in one copy means remembering to fix it in four other places. Missing one is exactly how regressions end up in production.
This problem is cut off in its tracks by custom mixins. Fixing the problem in the mixin ensures all models using the mixin receive the fix. Developers who have had to maintain large Odoo 19 codebases will reach for a mixin at the start of a feature, not after having to paste the same logic in six different places.
How to Create Custom Mixins in Odoo 19 — Step by Step
The walkthrough below uses a practical scenario: you need to add an internal notes field with a timestamp across several models in your project.
Step 1: Create a Dedicated Folder for Mixins
Keep your mixin files separate from your regular models. Create a mixins/ directory inside your module:
your_module/
+-- models/
+-- mixins/
¦ +-- __init__.py
¦ +-- note_mixin.py
+-- views/
+-- __manifest__.py
This layout makes it immediately clear which files are reusable and which are model-specific. Anyone new to your codebase will know exactly where to look.
Step 2: Write the Mixin Class
Open mixins/note_mixin.py and define your mixin. The critical piece is models.AbstractModel — this tells Odoo not to create a database table for this class.
# mixins/note_mixin.py
from odoo import models, fields, api
class NoteMixin(models.AbstractModel):
_name = 'note.mixin'
_description = 'Internal Note with Timestamp'
internal_note = fields.Text(
string='Internal Note',
)
note_updated_on = fields.Datetime(
string='Note Last Updated',
readonly=True,
)
def save_note(self):
"""
Stamps the current datetime onto note_updated_on.
Call this from any model that needs to record when
the internal note was last edited.
"""
for rec in self:
rec.note_updated_on = fields.Datetime.now()
The _name is the identifier Odoo uses internally — keep it lowercase, dot-separated, and specific enough to describe the mixin's purpose. Fields defined here appear on any inheriting model exactly as if you had written them there directly.
Step 3: Register the Mixin File
Odoo will not load your mixin unless you register it. Update mixins/__init__.py first:
# mixins/__init__.py
from . import note_mixin
Then import the mixins folder from your module's top-level __init__.py:
# your_module/__init__.py
from . import mixins
from . import models
Import mixins before models. Your model files reference the mixin by its _name, and that name needs to be registered before the model classes are loaded.
Step 4: Inherit the Mixin in Your Models
Now apply it. Here it is added to two completely different models, a project task and a repair order, with no repeated code:
# models/project_task.py
from odoo import models
class ProjectTask(models.Model):
_name = 'project.task'
_description = 'Project Task'
_inherit = ['project.task', 'note.mixin']
# Both models now have:
# - internal_note (Text field)
# - note_updated_on (Datetime field)
# - save_note() method
# models/repair_order.py
from odoo import models
class RepairOrder(models.Model):
_name = 'repair.order'
Two models, one definition. If the note field ever needs a default value or the timestamp logic changes, you update the mixin once.
Step 5: Wire the Mixin Into Your Action Logic
Fields from a mixin show up in views automatically once you add them to the XML. Methods need to be connected to an action. Here is an example of calling save_note() from a button on the form view:
# models/project_task.py (continued)
def action_save_internal_note(self):
"""Triggered by the Save Note button in the form view."""
self.save_note() # method lives in note.mixin
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Saved',
'message': 'Note timestamp has been updated.',
'type': 'success',
'sticky': False,
},
}
Practices Worth Following
One Mixin, One Purpose
A mixin that handles both logging and email formatting is two mixins pretending to be one. The moment a mixin starts doing two unrelated things, it becomes a problem to inherit — you pull in behaviour you did not ask for. Build each mixin to solve one specific problem and stop there.
Check What Odoo Already Has
Before writing a new mixin, spend a few minutes in the Odoo 19 source code checking whether a built-in already covers the need. Odoo ships mixins for messaging, activities, portal access, sequences, ratings, and website publishing. Reinventing any of those from scratch is a common time loss, especially for developers who are newer to the framework.
Name It Like You Will Search for It Later
In six months, you will search for a mixin by name. phone.validation.mixin, note.timestamp.mixin, and approval.state.mixin all tell you exactly what they do before you open the file. Names like common.mixin or utility.mixin tell you nothing and makes the codebase harder to navigate over time.
Document the Field Contract
Some mixins depend on fields that must already exist on the inheriting model. For example, a mixin with @api.constrains('name') assumes the inheriting model has a name field. If that assumption is not documented, someone will inherit the mixin on a model without that field and get a confusing error. Write a short docstring that lists any fields the mixin expects the inheriting model to provide.
No Model-Specific Business Logic
A mixin is the wrong place for logic that only makes sense for one model. If a rule applies specifically to sale.order, it belongs on sale.order. The test is straightforward: would this behaviour be wrong or meaningless if it ran on a completely different model? If yes, keep it out of the mixin.
Real Example: A Reference Code Validation Mixin
Here is a concrete scenario. You have several models where a code field must be uppercase, contain no spaces, and stay between 3 and 20 characters. Writing that constraint individually on each model is repetitive and means any future rule change touches multiple files. A mixin solves it in one place.
# mixins/code_validation_mixin.py
from odoo import models, api, _
from odoo.exceptions import ValidationError
import re
class CodeValidationMixin(models.AbstractModel):
_name = 'code.validation.mixin'
_description = 'Reference Code Format Validation'
@api.constrains('code')
def _validate_code_format(self):
"""
Validates the 'code' field against a standard format.
Rules: uppercase letters and digits only, 3 to 20 characters.
Requires the inheriting model to have a 'code' field.
"""
pattern = re.compile(r'^[A-Z0-9]{3,20}$')
for rec in self:
if rec.code and not pattern.match(rec.code):
raise ValidationError(
_('Code must be 3 to 20 uppercase letters or digits, no spaces.')
)
Applying it across models:
class ProductCategory(models.Model):
_name = 'product.category'
_inherit = ['product.category', 'code.validation.mixin']
class StockWarehouse(models.Model):
_name = 'stock.warehouse'
_inherit = ['stock.warehouse', 'code.validation.mixin']
When the business later decides the maximum length should drop to 15 characters, you change one line in the mixin. Both models and any others you added later update automatically.
Custom mixins are not an advanced technique reserved for senior developers. They are a practical habit: noticing repeated code and deciding to write it only once. The developers who benefit most from mixins are not necessarily the most experienced — they are the ones who got tired of fixing the same bug in multiple places and looked for a better approach.
In Odoo 19, with larger projects and faster development cycles, reaching for a mixin at the start of a feature — rather than after duplicating the code six times — is one of the higher-return habits you can build. Start with one small mixin. Inherit it in two models. That single experience usually makes the whole pattern click.
To read more about How to Create Mixins in Odoo 18, refer to our blog How to Create Mixins in Odoo 18.