Skip to content

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

DeployMonkey Team · March 22, 2026 12 min read

The Confusion

Both @api.onchange and @api.depends react to field changes. But they work completely differently. Using the wrong one causes silent bugs that are hard to trace.

Quick Rule

@api.onchange@api.depends
When it firesUI form change (before save)Database write (after save)
Where it runsClient-side triggered, server-side executedServer-side only
Triggered byUser editing a formAny write() — UI, API, cron, script
Can set fieldsYes (on the same record, unsaved)Yes (the computed field only)
Can show warningsYes (return {'warning': {...}})No
Stored in DBNot directly (user must save)Yes (if store=True)
Use forUX helpers, defaults, warningsCalculated values, derived data

@api.depends — Computed Fields

class SaleOrder(models.Model):
    _inherit = 'sale.order'

    total_weight = fields.Float(
        compute='_compute_total_weight',
        store=True,
    )

    @api.depends('order_line.product_id.weight', 'order_line.product_uom_qty')
    def _compute_total_weight(self):
        """Compute total order weight from line items."""
        for order in self:
            order.total_weight = sum(
                line.product_id.weight * line.product_uom_qty
                for line in order.order_line
            )

Key Properties

  • Fires on ANY write (UI, API, cron, import, script)
  • Reliably keeps data consistent
  • Works with store=True for DB persistence
  • Can depend on related model fields (dot notation)
  • Recomputes automatically when dependencies change

@api.onchange — UI Helpers

class SaleOrder(models.Model):
    _inherit = 'sale.order'

    @api.onchange('partner_id')
    def _onchange_partner_id(self):
        """When customer changes, suggest their default pricelist."""
        if self.partner_id:
            self.pricelist_id = self.partner_id.property_product_pricelist
            if self.partner_id.credit_limit and self.partner_id.total_invoiced > self.partner_id.credit_limit:
                return {
                    'warning': {
                        'title': 'Credit Limit Warning',
                        'message': f'{self.partner_id.name} has exceeded their credit limit.',
                    }
                }

Key Properties

  • ONLY fires when user changes the field in the UI form
  • Does NOT fire from API writes, imports, or cron jobs
  • Can set other fields on the same record (suggestions/defaults)
  • Can return warnings to the user
  • Changes are NOT saved until user clicks Save
  • User can override the suggested values

When to Use Each

Use @api.depends when:

  • The value MUST be correct regardless of how the record was created/updated
  • The field should be searchable/sortable (store=True)
  • Consistency matters more than UX
  • The calculation is deterministic (same inputs → same output)
# Examples:
# - Total amount = sum of line amounts (MUST be correct)
# - Margin = revenue - cost (MUST be correct)
# - Full name = first + last (MUST be correct)
# - Status flags = derived from state and dates

Use @api.onchange when:

  • You want to SUGGEST a value (user can override)
  • You want to show a WARNING before save
  • You want to set DEFAULTS based on another field
  • The behavior is a UX convenience, not a data rule
# Examples:
# - Suggest pricelist when customer changes (user may override)
# - Warn about credit limit (informational)
# - Auto-fill shipping address from customer (user may change)
# - Show estimated delivery date (suggestion)

Common Mistakes

Mistake 1: Using onchange for calculated values

# WRONG — onchange doesn't fire from API/import
@api.onchange('quantity', 'price')
def _onchange_total(self):
    self.total = self.quantity * self.price  # BUG: won't update via API

# RIGHT — use depends
@api.depends('quantity', 'price')
def _compute_total(self):
    for rec in self:
        rec.total = rec.quantity * rec.price

Mistake 2: Using depends for UX warnings

# WRONG — depends can't return warnings
@api.depends('amount_total')
def _compute_warning(self):  # Can't show warning from here
    pass

# RIGHT — use onchange for warnings
@api.onchange('amount_total')
def _onchange_amount_warning(self):
    if self.amount_total > 100000:
        return {'warning': {'title': 'Large Order', 'message': 'Requires approval'}}

Mistake 3: Mixing onchange with store=True

# WRONG — onchange sets a stored field but only from UI
@api.onchange('partner_id')
def _onchange_partner(self):
    self.sales_region = self.partner_id.state_id.name  # Stored field!
    # Bug: API-created records won't have sales_region set

# RIGHT — use depends for the stored field
@api.depends('partner_id.state_id')
def _compute_sales_region(self):
    for rec in self:
        rec.sales_region = rec.partner_id.state_id.name or ''

Can I Use Both Together?

Yes, for different purposes on the same trigger field:

# Computed field (always correct):
total = fields.Float(compute='_compute_total', store=True)

@api.depends('quantity', 'unit_price')
def _compute_total(self):
    for rec in self:
        rec.total = rec.quantity * rec.unit_price

# UX helper (warning only):
@api.onchange('quantity')
def _onchange_quantity_warning(self):
    if self.quantity > 1000:
        return {'warning': {
            'title': 'Large Quantity',
            'message': 'Are you sure? This exceeds typical order size.'
        }}

Summary

@api.depends = data integrity (computed fields, always correct)
@api.onchange = user experience (suggestions, warnings, defaults)