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) |
|---|---|---|
| Executes | In the browser (via RPC call) | On server (ORM level) |
| Triggers | User changes field in form view | Dependency field changes (write/create) |
| Persists data | No — changes are in the UI only until saved | Yes (if store=True) |
| Works on new records | Yes (before save) | Only on save (stored) or read (non-stored) |
| API imports/scripts | Never fires | Always fires |
| Batch operations | Never fires | Always 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_pricelistKey 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
selfrecordset 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.0If 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.0When 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_dateMigration: onchange to compute
If you have business logic in onchange methods that should always execute:
- Create a new stored compute field
- Add
@api.dependson the same trigger fields - Move the logic from onchange to compute
- Keep the onchange only if you need UI-specific behavior (warnings, domain filtering)
- 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=Trueon stored compute fields to calculate during create without a second write