Bu makale şu anda yalnızca İngilizce olarak mevcuttur. Çeviri yakında eklenecektir.
Odoo Python Decorators Complete Guide: api.depends, constrains, onchange, ondelete
Half the bugs in custom Odoo modules trace back to misunderstood decorators. @api.depends not firing. @api.onchange running on a save instead of an edit. @api.constrains blocking a legitimate write. @api.ondelete triggering when the record is being unlinked via cascade vs explicit. This guide explains each decorator: what it does, when it runs, why you'd use it, and the failure modes we've debugged in production.
The decorator catalog is small but each one has subtle semantics. Get them right and your module behaves predictably; get them wrong and you'll have edge cases that show up only in production.
Key Takeaways
@api.dependstriggers compute methods when listed fields change — must be exhaustive@api.depends_contexttriggers re-compute when context keys change (rarely needed)@api.constrainsruns on every write and create; raise UserError or ValidationError to block@api.onchangeruns in the form view only — not on programmatic write/create@api.ondeleteruns before unlink to validate; can block deletion@api.modelmarks methods that operate on the model class, not a recordset@api.model_create_multiis the modern replacement for@api.modeloncreate@api.returnsdeclares what type the method returns for chaining
@api.depends — the most-used decorator
Marks a compute method's input dependencies. Odoo recomputes the field when any listed dependency changes.
from odoo import api, fields, models
class Order(models.Model):
_name = 'sale.order'
_inherit = 'sale.order'
line_ids = fields.One2many('sale.order.line', 'order_id')
total_qty = fields.Float(compute='_compute_total_qty', store=True)
@api.depends('line_ids', 'line_ids.product_uom_qty')
def _compute_total_qty(self):
for order in self:
order.total_qty = sum(order.line_ids.mapped('product_uom_qty'))
Critical rule: list every field your computation reads, including dotted paths through relations. Missing dependencies cause stale values.
Common @api.depends mistakes
- Missing dotted dependency:
@api.depends('line_ids')triggers when lines are added/removed but NOT when an existing line's qty changes. Need@api.depends('line_ids.product_uom_qty'). - Computing from another computed field: works, but recompute order matters. Both fields need correct dependencies.
- Stored vs not stored:
store=Truesaves the value; without it, every read recomputes. For frequently-read fields, store. For rarely-read, don't.
@api.depends_context
For fields whose computation depends on context keys (e.g., a field that shows different values per user or per company):
@api.depends_context('uid', 'company')
def _compute_user_specific_value(self):
user = self.env.user
for record in self:
record.user_specific = ... # depends on which user is reading
Useful but rare. Most computed fields don't need context dependency.
@api.constrains — validation that always runs
Raises an error if the constraint is violated. Runs on create and on write that touches any of the listed fields:
from odoo.exceptions import ValidationError
class Project(models.Model):
_inherit = 'project.project'
deadline = fields.Date()
start_date = fields.Date()
@api.constrains('deadline', 'start_date')
def _check_dates(self):
for project in self:
if project.deadline and project.start_date and project.deadline < project.start_date:
raise ValidationError("Deadline cannot be before start date.")
Constrains vs SQL constraints
For simple constraints (uniqueness, NOT NULL), use SQL constraints — they're faster:
class Tag(models.Model):
_name = 'my.tag'
name = fields.Char(required=True)
_sql_constraints = [
('name_uniq', 'UNIQUE(name)', 'Tag name must be unique.'),
]
For business-logic constraints (date ordering, conditional requirements), use @api.constrains.
@api.onchange — form view only
Runs on the client form when the user changes a listed field. Used for UI-side recomputation before the user saves:
class Order(models.Model):
_inherit = 'sale.order'
@api.onchange('partner_id')
def _onchange_partner_set_currency(self):
if self.partner_id:
self.currency_id = self.partner_id.property_purchase_currency_id
return {
'warning': {
'title': 'Currency Changed',
'message': f'Currency set to {self.currency_id.name}',
}
}
Critical onchange caveats
- Does NOT run on programmatic create/write. If you call
Order.create({'partner_id': p.id, ...}), the onchange does NOT fire. This is by design. - Does NOT persist. Until the user clicks Save, no DB write happens. The onchange's effects are visible to the user but unsaved.
- Can return a warning dict to show a non-blocking warning to the user.
- For computed fields that should also work programmatically, use
@api.dependsfor the canonical computation and@api.onchangefor UI feedback.
@api.ondelete — block unlink with reason
Runs before unlink. Can prevent deletion if business rules require it:
from odoo.exceptions import UserError
class Customer(models.Model):
_inherit = 'res.partner'
@api.ondelete(at_uninstall=False)
def _check_no_active_orders(self):
for partner in self:
active_orders = self.env['sale.order'].search_count([
('partner_id', '=', partner.id),
('state', 'in', ['sale', 'done']),
])
if active_orders:
raise UserError(
f"Cannot delete {partner.name}: has {active_orders} active orders."
)
The at_uninstall=False (default) means the check is skipped during module uninstall — important so cascading deletes don't get blocked.
@api.model — class-level methods
Marks a method that operates on the model class, not a specific recordset. The "self" parameter is the empty recordset of the model:
class Calculator(models.Model):
_name = 'my.calculator'
@api.model
def compute_pi(self, precision):
# self is an empty recordset of my.calculator
return round(3.14159265, precision)
# Call:
self.env['my.calculator'].compute_pi(precision=4)
Used for utility methods that don't need a record context.
@api.model_create_multi — modern create
Modern Odoo (15+) overrides create with @api.model_create_multi to support bulk creation:
class Order(models.Model):
_inherit = 'sale.order'
@api.model_create_multi
def create(self, vals_list):
# vals_list is ALWAYS a list of dicts, even for single create
for vals in vals_list:
if not vals.get('reference'):
vals['reference'] = self.env['ir.sequence'].next_by_code('sale.order')
return super().create(vals_list)
This is the right way to override create in modern Odoo. Older @api.model create that accepted a single dict is deprecated.
@api.returns — type chaining hint
Declares what type the method returns. Important for chained recordset operations:
@api.returns('self', lambda value: value.id)
def find_by_email(self, email):
return self.env['res.partner'].search([('email', '=', email)], limit=1)
# Without @api.returns, .id at the end of a chain might break
partner = self.env['res.partner'].find_by_email('foo@bar').id
Rarely needed for new code; common in legacy Odoo modules.
Decorator interaction matrix
| Operation | api.depends | api.constrains | api.onchange | api.ondelete |
|---|---|---|---|---|
| Form view edit (no save) | Triggers compute | Does not run | Triggers | Does not run |
| Form view save | Triggers compute | Runs validation | Already triggered | Does not run |
Programmatic create | Triggers compute | Runs validation | Does not run | Does not run |
Programmatic write | Triggers compute | Runs validation | Does not run | Does not run |
Programmatic unlink | Does not run | Does not run | Does not run | Runs |
| Module uninstall | Does not run | Does not run | Does not run | Skipped (at_uninstall=False) |
Common decorator anti-patterns
Side effects in @api.depends compute methods
The compute method should be pure — input fields produce output values. Don't write to other models, send emails, or call external APIs. If you do, recomputes (which can fire unexpectedly) cause side effects.
Heavy work in @api.constrains
The constraint runs on every relevant write. If the check is expensive, your write performance suffers. Either keep the check cheap or move it to @api.depends_context style (compute and store, then check).
@api.onchange that programmatic code expects to run
If your business logic relies on onchange firing, programmatic creates will silently produce records in the wrong state. Mirror the logic in a @api.depends compute or in the create/write overrides.
Forgetting at_uninstall=False on ondelete
Without it, uninstalling your module can fail because the ondelete check blocks the cascade. Almost always you want at_uninstall=False.
Frequently Asked Questions
Do I need @api.depends if my computed field doesn't store?
Yes. The decorator tells Odoo when to invalidate the cache and trigger downstream recomputes. Without it, the field may return stale values within the same request.
Can I use @api.constrains and @api.onchange on the same fields?
Yes, and it's a common pattern: onchange provides immediate feedback in the form, constrains enforces the rule at save time. The onchange fires earlier (better UX); the constrains catches programmatic writes.
What's the order of operations on save?
For a record save: 1) onchange has already fired during edits. 2) write is called. 3) Computed fields with depending dependencies recompute. 4) @api.constrains checks run. 5) Mail/audit-trail writes. 6) Transaction commits. If any constraint raises, the whole transaction rolls back.
Why isn't my @api.depends firing?
Three common causes: (1) The dependency is not exhaustive (missing a dotted path). (2) You're modifying a field via raw SQL, which bypasses the ORM. (3) The dependent field itself wasn't loaded into cache (e.g., browsed without read). Check the cache first; raw SQL bypass is the silent killer.
Should I use @api.onchange or @api.depends for default-related logic?
Use @api.depends if the computation should apply universally (programmatic and form). Use @api.onchange if it's only a UX nicety in the form. For default values that depend on other fields, prefer compute (@api.depends) because it works for API-created records too.
Decorators are where Python developers becoming Odoo developers most often stumble. ECOSIRE's Odoo developer hire service places engineers who understand these semantics from production experience. See our Odoo customization service for module development or our Odoo training service for structured developer onboarding.
Yazan
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Odoo ERP ile İşinizi Dönüştürün
Operasyonlarınızı kolaylaştırmak için uzman Odoo uygulaması, özelleştirme ve destek.
İlgili Makaleler
Odoo Form Görünümüne Özel Düğme Nasıl Eklenir (2026)
Odoo 19 form görünümlerine özel eylem düğmeleri ekleyin: Python eylem yöntemi, görünüm devralma, koşullu görünürlük, onay diyalogları. Üretimde test edilmiştir.
Odoo'da Studio Olmadan Özel Alan Nasıl Eklenir (2026)
Odoo 19'daki özel modül aracılığıyla özel alanlar ekleyin: model mirası, görünüm uzantısı, hesaplanan alanlar, mağaza/depo dışı kararlar. Kod öncelikli, sürüm kontrollü.
Odoo'da Harici Düzeni Kullanarak Özel Rapor Nasıl Eklenir?
Web.external_layout'u kullanarak Odoo 19'da markalı bir PDF raporu oluşturun: QWeb şablonu, paperformat, action bağlama. Baskı logosu + altbilgi geçersiz kılmalarıyla.