The Problem
You have a stored computed field in Odoo that should update when its dependencies change, but it stubbornly shows the old value. The formula is correct, the dependencies are listed in @api.depends(), but the field just will not recompute. This is a subtle and common issue that affects both standard and custom Odoo modules.
Common Symptoms
- Computed field shows stale/old value after dependency changes
- Field updates when you edit and save the record but not when dependencies change via code
- After module upgrade (
-u), computed fields still show old values - Field value correct for new records but wrong for existing ones
- Computed field based on One2many lines not updating when lines change
How Stored Computed Fields Work
When a stored computed field has @api.depends('field_a', 'field_b'):
- Odoo monitors writes to
field_aandfield_b - When those fields change via
write(), Odoo marks the computed field for recomputation - At the end of the current flush cycle, Odoo calls the compute method
- The new value is stored in the database
If any step in this chain breaks, the field does not update.
Causes and Fixes
1. Missing or Incomplete @api.depends()
The most common cause. If a dependency is not listed, changes to it will not trigger recomputation.
# BAD: missing dependency on 'discount'
@api.depends('price', 'quantity')
def _compute_total(self):
for rec in self:
rec.total = rec.price * rec.quantity * (1 - rec.discount / 100)
# 'discount' changes will NOT trigger recomputation!
# GOOD: all dependencies listed
@api.depends('price', 'quantity', 'discount')
def _compute_total(self):
for rec in self:
rec.total = rec.price * rec.quantity * (1 - rec.discount / 100)2. Dependency on One2many Sub-fields
For fields that depend on One2many child records, you must use dot notation to specify which child field triggers recomputation.
# BAD: does not trigger when line amounts change
@api.depends('line_ids')
def _compute_total(self):
for rec in self:
rec.total = sum(rec.line_ids.mapped('amount'))
# GOOD: triggers when any line's amount changes
@api.depends('line_ids.amount')
def _compute_total(self):
for rec in self:
rec.total = sum(rec.line_ids.mapped('amount'))Note: @api.depends('line_ids') only triggers when lines are added or removed, not when existing line fields change.
3. Direct SQL Updates Bypass Recomputation
If a dependency field is updated via raw SQL instead of ORM write(), Odoo has no way to know the field changed.
# BAD: bypasses ORM, no recomputation triggered
self.env.cr.execute(
"UPDATE sale_order SET discount = 10 WHERE id = %s", [order_id]
)
# GOOD: uses ORM, triggers recomputation
order = self.env['sale.order'].browse(order_id)
order.write({'discount': 10})4. Module Upgrade Does Not Recompute Existing Values
Running -u my_module updates the field definition but does NOT recompute values for existing records. If you change the compute method logic, old records keep their old values.
Fix: Force recomputation after upgrade:
# Option 1: In a post-init hook or migration script:
def post_init_hook(env):
records = env['my.model'].search([])
records._compute_my_field() # force recompute
# Option 2: Via Odoo shell:
records = env['my.model'].search([])
records._compute_my_field()
env.cr.commit()
# Option 3: Via SQL + ORM recompute:
env['my.model'].search([])._recompute_todo(env['my.model']._fields['my_field'])
env['my.model'].recompute()
env.cr.commit()5. Compute Method Raises Exception Silently
If the compute method raises an exception for some records, Odoo may skip those records without updating their values.
Fix: Add error handling in the compute method:
@api.depends('partner_id.country_id')
def _compute_tax_rate(self):
for rec in self:
try:
rec.tax_rate = rec.partner_id.country_id.tax_rate or 0
except Exception:
rec.tax_rate = 0 # safe default6. Cache Showing Stale Values
Odoo's ORM cache may show old values even after recomputation, especially in the same transaction.
Fix:
# Invalidate cache:
self.env.invalidate_all()
# Or invalidate specific fields:
self.invalidate_recordset(['my_computed_field'])
# Then re-read:
record = self.env['my.model'].browse(record_id)
print(record.my_computed_field) # fresh value7. depends on Related Field Chain
Long dependency chains through related fields may not always trigger correctly, especially with deep nesting.
# Potentially unreliable for deep chains:
@api.depends('order_id.partner_id.country_id.code')
def _compute_country_code(self):
...
# More reliable: use a related field as intermediary
country_code = fields.Char(
related='order_id.partner_id.country_id.code',
store=True
)8. Field Not Marked as store=True
Non-stored computed fields are never written to the database. They are computed on-the-fly for each access. If you expect persistence, you need store=True.
# Non-stored: computed every time, never saved
total = fields.Float(compute='_compute_total') # no persistence
# Stored: saved to database, updated on dependency change
total = fields.Float(compute='_compute_total', store=True)Force Recomputation of All Records
# In Odoo shell — force recompute for all records:
Model = env['my.model']
records = Model.search([])
# Method 1: call compute directly
records._compute_my_field()
# Method 2: trigger write on dependency
# (causes natural recomputation)
for rec in records:
rec.write({'dependency_field': rec.dependency_field})
# Method 3: SQL + invalidate
env.cr.execute("UPDATE my_model SET my_field = NULL")
env.invalidate_all()
records._compute_my_field()
env.cr.commit()Best Practices
- List ALL dependencies in
@api.depends()— missing one is the top cause of stale fields - Use dot notation for One2many dependencies:
line_ids.amount - Never use raw SQL to update fields that are dependencies of computed fields
- After changing compute logic, force recompute on existing records
- Add error handling in compute methods to prevent silent failures
- Use
relatedfields for simple dependency chains instead of custom compute methods