Skip to content

Odoo @api.onchange vs @api.depends: When to Use Each

DeployMonkey Team · March 24, 2026 11 min read

The Core Difference

This is one of the most common sources of confusion for Odoo developers. Both @api.onchange and @api.depends (compute fields) react to field value changes, but they work fundamentally differently in terms of when they execute, where they execute, and whether they persist data.

Aspect@api.onchange@api.depends (compute)
ExecutesIn the browser (via RPC call)On server (ORM level)
TriggersUser changes field in form viewDependency field changes (write/create)
Persists dataNo — changes are in the UI only until savedYes (if store=True)
Works on new recordsYes (before save)Only on save (stored) or read (non-stored)
API imports/scriptsNever firesAlways fires
Batch operationsNever firesAlways fires

@api.onchange

When to Use

Use onchange for UI convenience only — setting default-like values when users fill out forms. The key insight: onchange is a UX feature, not a data integrity feature.

@api.onchange('partner_id')
def _onchange_partner_id(self):
    if self.partner_id:
        self.payment_term_id = self.partner_id.property_payment_term_id
        self.pricelist_id = self.partner_id.property_product_pricelist

Key Behaviors

  • Only fires when a user changes the field in a form view
  • Does NOT fire on create/write from code, API calls, or imports
  • Modifies the record in memory — changes are visible in the UI but not saved until the user clicks Save
  • Cannot use self.env.cr.commit() or write to the database
  • The self recordset is a pseudo-record (not yet in the database for new records)

Warning and Domain Returns

@api.onchange('quantity')
def _onchange_quantity(self):
    if self.quantity > 1000:
        return {
            'warning': {
                'title': 'Large Quantity',
                'message': 'You are ordering more than 1000 units.',
            }
        }

@api.depends (Compute Fields)

When to Use

Use compute fields for derived data that must always be correct — regardless of how the record was created or modified.

total_amount = fields.Float(compute='_compute_total', store=True)

@api.depends('line_ids.price', 'line_ids.quantity')
def _compute_total(self):
    for record in self:
        record.total_amount = sum(
            line.price * line.quantity
            for line in record.line_ids
        )

Key Behaviors

  • Fires whenever any dependency field changes — via UI, code, API, or import
  • Stored compute fields (store=True) write to the database automatically
  • Non-stored compute fields calculate on every read
  • Respects the dependency graph — if field A depends on field B which depends on field C, changing C triggers both B and A
  • Always iterates over self (may be a batch recordset)

Common Mistake: Using onchange for Business Logic

This is wrong:

# BAD — discount only applies when user uses form view
@api.onchange('partner_id')
def _onchange_partner_discount(self):
    if self.partner_id.is_vip:
        self.discount = 10.0

If an order is created via API, import, or script, the onchange never fires and the VIP discount is never applied. The correct approach:

# GOOD — discount always applied
discount = fields.Float(compute='_compute_discount', store=True)

@api.depends('partner_id', 'partner_id.is_vip')
def _compute_discount(self):
    for record in self:
        record.discount = 10.0 if record.partner_id.is_vip else 0.0

When Both Are Needed

Sometimes you need onchange for UX (suggesting a value the user can override) and compute for derived calculations:

# Onchange suggests payment term (user can override)
@api.onchange('partner_id')
def _onchange_partner_id(self):
    if self.partner_id:
        self.payment_term_id = self.partner_id.property_payment_term_id

# Compute calculates due date (always correct)
date_due = fields.Date(compute='_compute_date_due', store=True)

@api.depends('invoice_date', 'payment_term_id')
def _compute_date_due(self):
    for record in self:
        if record.invoice_date and record.payment_term_id:
            record.date_due = record.payment_term_id._compute_terms(...)
        else:
            record.date_due = record.invoice_date

Migration: onchange to compute

If you have business logic in onchange methods that should always execute:

  1. Create a new stored compute field
  2. Add @api.depends on the same trigger fields
  3. Move the logic from onchange to compute
  4. Keep the onchange only if you need UI-specific behavior (warnings, domain filtering)
  5. Write a migration script to populate existing records

Performance Considerations

  • Stored compute fields recompute on every write of dependency fields — expensive for frequently-written models
  • Non-stored compute fields recompute on every read — expensive for list views with many records
  • Onchange has no persistence cost but adds RPC calls during form editing
  • Use precompute=True on stored compute fields to calculate during create without a second write