A computed field worked fine for months, and then one specific change stopped updating it — not every change, just one. Nothing errors. The field simply keeps showing what it computed the last time something in its dependency list actually changed, which by definition isn’t obvious from looking at the field itself.

The short version: a computed field only recomputes when something listed in its @api.depends() decorator changes — not "whenever anything relevant changes," literally only those exact fields. A dependency through a relation (partner_id.category_id) has to be spelled out as that full path, not just the local field, or a change on the far end of that relation never triggers a recompute at all.

The mechanism: depends, not magic

total = fields.Float(compute='_compute_total', store=True)

@api.depends('line_ids.subtotal')
def _compute_total(self):
    for record in self:
        record.total = sum(record.line_ids.mapped('subtotal'))

Per Odoo’s own developer tutorial on computed fields, the ORM uses exactly what’s listed in @api.depends() to decide when to call the compute method again — it has no independent understanding of what the method’s own code actually reads. If the compute method’s body references a field that isn’t listed in @api.depends(), the ORM has no way to know that field matters, and won’t recompute when it changes. The dependency list isn’t documentation of what the method uses — it’s the entire trigger mechanism.

store=True vs leaving it unstored

An unstored computed field (the default) recomputes on the fly every time it’s read — always current, at the cost of recalculating on every access, including in a list view rendering hundreds of rows. store=True persists the value as a real column, computed once and reused until a listed dependency changes — faster to read, but only ever as fresh as its last trigger, and only it can be used in a search domain or grouped in a list view the way a real stored field can (an unstored computed field generally can’t be searched/sorted on without also defining a search parameter).

The dependency path mistake that causes stale values

@api.depends() accepts dependency paths, not just local field names — per the same documentation, a path through a many2one, many2many, or one2many relation is valid and often necessary:

@api.depends('partner_id.category_id')
def _compute_is_vip(self):
    for record in self:
        record.is_vip = 'VIP' in record.partner_id.category_id.mapped('name')

Leaving this as @api.depends('partner_id') instead of @api.depends('partner_id.category_id') looks almost identical and behaves completely differently: the field only recomputes when partner_id itself changes (a different partner gets linked to this record) — not when that partner’s own category_id changes on the partner record itself. The visible symptom: the value was correct when first computed, and then silently stopped tracking reality the moment something changed on the related record rather than on this one directly, with nothing in the UI or logs pointing at the dependency list as the cause.

Watch out: this exact mistake is easy to miss in testing, because most manual testing changes the local field (linking a different partner) rather than editing the related record's own field afterward — the bug only shows up once a record has existed for a while and something on the far end of the relation changes later, which is precisely when it's hardest to trace back to a manifest-adjacent decorator written weeks or months earlier.

Compute chains: a computed field depending on another one

A computed field can depend on another computed field, and the ORM resolves the resulting chain in the correct order — but each one still only recomputes based on its own @api.depends(), so a chain of three computed fields needs the dependency correctly declared at every single link, not just the first and last.

Quick reference

SituationWhat to check
A computed value never updates when a local field changesIs that local field actually listed in @api.depends()?
A computed value never updates when a related record’s field changesIs the full path (related_field.the_actual_field) listed, not just related_field?
A computed field can’t be used in a search filter or list-view groupingDoes it need store=True, or a search= parameter if it must stay unstored?
Read performance is slow on a list view with this fieldConsider store=True if the dependency list is stable and well understood

Frequently asked questions

Does @api.depends need every field the method touches, or just the ones that should trigger a recompute?

In practice these are the same thing — if the method reads a field's current value to compute the result, that field's change is exactly what should trigger a recompute, so any field genuinely read by the method belongs in the dependency list.

What happens if I list a field in @api.depends that the method doesn't actually use?

The field recomputes unnecessarily whenever that unrelated dependency changes — not a correctness bug, but a performance one, since the compute method runs (and potentially writes a value) on a trigger that never actually needed it.

Is store=True required for a computed field to be included in a report?

Not strictly — an unstored computed field can still be read and displayed in a QWeb report the same as any other field, since reading it still runs the compute method live. store=True matters for search/sort/group-by support and read performance at scale, not for whether a report can display the value at all.

Can a compute method safely call sudo() or with_context() internally?

Yes, syntactically, but the same caution from the sudo()/with_user()/with_context() article applies fully inside a compute method — it runs in whatever environment triggered the recompute, and bypassing access rights inside it is just as real a bypass as anywhere else.

Does changing @api.depends on an existing field require a database migration?

No schema change is needed for the decorator itself — but if the field is store=True, existing stored values won't retroactively reflect the new dependency until each record is actually recomputed (triggered by one of its dependencies changing, or an explicit recompute), so already-stored values can stay stale until that happens even after the code is fixed.

Further reading

And on this site: sudo(), with_context(), and with_user() for what a compute method actually inherits from the environment it runs in.