Ana içeriğe atla

Odoo Python Dekoratörler Kılavuzu: bağlıdır, kısıtlar, değişime bağlıdır

Odoo dekoratörlerine yönelik eksiksiz kılavuz: api.depends, api.constrains, api.onchange, api.ondelete, api.model, api.model_create_multi — her biri tetiklendiğinde.

E
ECOSIRE Research and Development Team
|4 Mayıs 20268 dk okuma1.5k Kelime|

Bu makale şu anda yalnızca İngilizce olarak mevcuttur. Çeviri yakında eklenecektir.

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.

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.depends triggers compute methods when listed fields change — must be exhaustive
  • @api.depends_context triggers re-compute when context keys change (rarely needed)
  • @api.constrains runs on every write and create; raise UserError or ValidationError to block
  • @api.onchange runs in the form view only — not on programmatic write/create
  • @api.ondelete runs before unlink to validate; can block deletion
  • @api.model marks methods that operate on the model class, not a recordset
  • @api.model_create_multi is the modern replacement for @api.model on create
  • @api.returns declares 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=True saves 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.depends for the canonical computation and @api.onchange for UI feedback.

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

Operationapi.dependsapi.constrainsapi.onchangeapi.ondelete
Form view edit (no save)Triggers computeDoes not runTriggersDoes not run
Form view saveTriggers computeRuns validationAlready triggeredDoes not run
Programmatic createTriggers computeRuns validationDoes not runDoes not run
Programmatic writeTriggers computeRuns validationDoes not runDoes not run
Programmatic unlinkDoes not runDoes not runDoes not runRuns
Module uninstallDoes not runDoes not runDoes not runSkipped (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.

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.

E

Yazan

ECOSIRE Team

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