The Onchange Problem
You write an @api.onchange method, save the module, refresh the form, change the field — and nothing happens. The method does not fire, or it fires but the values do not stick. Onchange is one of the most misunderstood mechanisms in Odoo, and getting it wrong leads to frustrating bugs.
How @api.onchange Works
Key facts about onchange that explain most issues:
- Onchange runs only in the UI (form view). It does NOT run when writing via code, API, or import.
- Onchange runs on a virtual record (NewId), not a saved database record.
- Onchange can set field values and return warnings/domains.
- Values set by onchange are not saved until the user clicks Save.
- Onchange does NOT fire during ORM
write()orcreate()calls.
Cause 1: Field Not in the View
Onchange only fires for fields that are present in the current form view.
# Your onchange:
@api.onchange('partner_id')
def _onchange_partner(self):
self.delivery_address = self.partner_id.street
# But partner_id is NOT in the form view → onchange never fires
# Fix: Add the field to the form view (can be invisible):
<field name="partner_id"/>
<!-- Or hidden but present: -->
<field name="partner_id" invisible="1"/>Cause 2: Wrong Field Name in Decorator
# Typo in the field name — onchange is registered but never triggered:
@api.onchange('parter_id') # TYPO: 'parter' instead of 'partner'
def _onchange_partner(self):
self.delivery_address = self.partner_id.street
# Fix: Check spelling matches the exact field name
# Odoo does NOT warn about onchange on non-existent fields
# Verify field names:
for field_name in ['partner_id', 'parter_id']:
if field_name in self._fields:
print(f'{field_name}: exists')
else:
print(f'{field_name}: DOES NOT EXIST')Cause 3: Onchange on Computed Field
# Onchange on a computed field behaves unexpectedly:
@api.onchange('computed_total') # computed_total is a computed field
def _onchange_total(self):
# This may not fire because computed fields are
# recalculated, not manually changed by the user
pass
# Fix: Trigger on the SOURCE fields, not the computed field
@api.onchange('line_ids', 'line_ids.price', 'line_ids.quantity')
def _onchange_lines(self):
self.computed_total = sum(line.price * line.quantity for line in self.line_ids)Cause 4: Onchange in Inherited Model Not Working
# Your module inherits a model and adds an onchange,
# but the parent module's view doesn't have the field:
class SaleOrderInherit(models.Model):
_inherit = 'sale.order'
custom_field = fields.Char()
@api.onchange('custom_field')
def _onchange_custom(self):
self.note = f'Custom: {self.custom_field}'
# Fix: Add the field to the view via inheritance:
<record id="view_order_form_inherit" model="ir.ui.view">
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<field name="partner_id" position="after">
<field name="custom_field"/>
</field>
</field>
</record>Cause 5: Onchange vs ORM Write
This is the biggest source of confusion. Onchange does NOT fire when you set values programmatically.
# This does NOT trigger onchange:
record.write({'partner_id': new_partner_id})
# This does NOT trigger onchange either:
record.partner_id = new_partner_id
# Onchange ONLY fires in the web UI form view
# If you need the same logic to run during ORM operations:
# Use @api.depends (for computed fields) or override write/create
# Pattern: shared logic method
def _update_from_partner(self):
self.delivery_address = self.partner_id.street
@api.onchange('partner_id')
def _onchange_partner(self):
self._update_from_partner()
def write(self, vals):
res = super().write(vals)
if 'partner_id' in vals:
self._update_from_partner()
return resCause 6: Returning Values vs Setting Values
# In Odoo 13+, set values directly on self:
@api.onchange('partner_id')
def _onchange_partner(self):
self.phone = self.partner_id.phone # Direct assignment
# In older Odoo versions, you returned a dict:
# return {'value': {'phone': self.partner_id.phone}} # OLD STYLE
# The old style still works but is deprecatedCause 7: Onchange on One2many/Many2many Lines
# Onchange on line-level fields requires special syntax:
@api.onchange('order_line_ids') # Fires when lines are added/removed
def _onchange_lines(self):
self.line_count = len(self.order_line_ids)
# To watch a specific field on lines:
@api.onchange('order_line_ids.product_id') # Does NOT work!
# Odoo does not support dotted paths in @api.onchange
# Fix: Put the onchange on the child model instead:
class OrderLine(models.Model):
_inherit = 'sale.order.line'
@api.onchange('product_id')
def _onchange_product(self):
self.name = self.product_id.display_nameWhen to Use @api.depends Instead
# Use @api.depends when:
# - The value should be computed automatically (no user action needed)
# - The logic should run on ORM writes, not just UI changes
# - The value should be stored in the database
# - The value should update on related record changes
# Use @api.onchange when:
# - You want to suggest a value the user can override
# - You want to show a warning message
# - You want to change the domain of another field dynamically
# - The logic should ONLY run during UI form editingDebugging Onchange
# 1. Add a log statement to verify the method is called:
import logging
_logger = logging.getLogger(__name__)
@api.onchange('partner_id')
def _onchange_partner(self):
_logger.info('ONCHANGE FIRED for partner_id = %s', self.partner_id)
self.phone = self.partner_id.phone
# 2. Check browser Network tab (F12):
# When you change the field, a call to /web/dataset/call_kw
# with method 'onchange' should appear
# Check the response for your field values
# 3. Verify the onchange is registered:
env['sale.order']._onchange_methods
# Returns dict of {field: [methods]}