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 fires | UI form change (before save) | Database write (after save) |
| Where it runs | Client-side triggered, server-side executed | Server-side only |
| Triggered by | User editing a form | Any write() — UI, API, cron, script |
| Can set fields | Yes (on the same record, unsaved) | Yes (the computed field only) |
| Can show warnings | Yes (return {'warning': {...}}) | No |
| Stored in DB | Not directly (user must save) | Yes (if store=True) |
| Use for | UX helpers, defaults, warnings | Calculated 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 datesUse @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.priceMistake 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)