For Developers

Why your computed field isn't recomputing

A computed field that refuses to update is almost always a missing @api.depends. But about 10% of the time, it's something weirder.

A computed field that isn’t recomputing is almost always a missing dependency in @api.depends. But about 10% of the time, it’s something weirder: a stored computed field that depends on a non-stored computed field, a field that changes through sudo(), or a cache that hasn’t been flushed. This post is about the 10%.

First, the obvious 90%. Make sure your decorator is right.

from odoo import api, fields, models

class SaleOrder(models.Model):
    _inherit = 'sale.order'

    commission_total = fields.Monetary(
        string='Commission',
        compute='_compute_commission_total',
        store=True,
        currency_field='currency_id',
    )

    @api.depends('order_line.price_subtotal', 'user_id.commission_rate')
    def _compute_commission_total(self):
        for order in self:
            rate = order.user_id.commission_rate or 0.0
            order.commission_total = order.amount_untaxed * (rate / 100.0)

If commission_total isn’t updating when a line’s price_subtotal changes, start here. Common fixes:

  • Missing the dotted path: order_line alone won’t trigger on line edits, you need order_line.price_subtotal.
  • Dependency on a field that itself doesn’t exist on the related model.
  • A typo. Odoo won’t warn you; it’ll silently never recompute.

Now the 10%.

Case 1: the stored-depends-on-non-stored trap

A stored computed field that depends on a non-stored computed field is a footgun. Odoo will compute the stored field when the record is created, but the dependency graph only fires properly for stored-to-stored chains. If the dependency is non-stored, the recompute doesn’t propagate reliably.

# Broken:
partner_score = fields.Float(compute='_compute_partner_score')  # not stored
partner_tier = fields.Selection(
    [...], compute='_compute_partner_tier', store=True,  # stored
)

@api.depends('partner_score')
def _compute_partner_tier(self):
    for rec in self:
        rec.partner_tier = ...

partner_tier is stored. partner_score is not. When partner_score “changes” — it doesn’t really, because it isn’t stored, it’s computed on read — partner_tier has no way to know. The dependency exists in the graph, but there’s no stored value for Odoo to compare against.

Fix: either make partner_score stored too, or make partner_tier depend on the underlying fields that partner_score depends on.

Case 2: fields changed through sudo()

If your code does record.sudo().write({'field': value}), the recompute will happen — but in the sudo context. If another computed field elsewhere depends on this and has record rules or access controls, it may silently fail to trigger because the user doing the write lacks the privilege to see the dependent records.

This is rare but brutal when it happens. Signs:

  • Computed field updates correctly in admin but not for a specific group.
  • The field updates if you run the same action as a superuser.
  • No errors in the log — Odoo doesn’t raise on silent access filtering during recompute.

Fix: keep sudo() inside compute_sudo=True on the field itself, not scattered through the write paths.

internal_status = fields.Selection(
    [...],
    compute='_compute_internal_status',
    store=True,
    compute_sudo=True,  # <-- this
)

Case 3: the flush-before-read problem

Odoo’s ORM batches writes and recomputes. Between the moment you do record.write(...) and the moment the compute method runs, the value may not yet be in the database. If another piece of code reads the field in that window, it can see stale data.

Common place this bites you: a controller or external API call that does record.write({'x': 1}) and then immediately record.y where y is a computed field depending on x. If y is stored, Odoo usually handles the flush. If y is non-stored and you’ve cached the recordset across the write, you can get the old value.

Fix: call self.env.flush_all() (Odoo 16+) or invalidate the cache before the dependent read.

record.write({'amount_untaxed': 500})
record.env.flush_all()
print(record.commission_total)  # now reliably recomputed

In Odoo 15 and earlier, the incantation is self.env['res.partner'].flush() or record.invalidate_cache(['commission_total']). The API changed across versions — check odoo/models.py in your source tree for your specific version.

Case 4: inverse fields and the double-write

If your computed field has an inverse= method, Odoo will call the inverse on every write, including writes that come from its own compute method. This can create a loop where the compute writes, the inverse fires, the inverse writes again, and one of those writes silently skips the recompute.

This is subtle and hard to debug. If you have compute= + inverse= + store=True on the same field, trace through the sequence carefully. Often the fix is to remove store=True and let the field compute on read, or to protect the inverse method with a context flag:

def _inverse_commission_total(self):
    if self.env.context.get('skip_inverse'):
        return
    for rec in self:
        ...

def _compute_commission_total(self):
    for rec in self.with_context(skip_inverse=True):
        ...

Case 5: the one-way depends chain

Dependencies in @api.depends are directional. A depends on B means: when B changes, recompute A. It does not mean: when A is read, check if B is current.

This matters when B is itself a compute whose dependencies changed in a way that didn’t trigger B’s own recompute. A doesn’t know B is stale. A happily uses B’s cached-stale value.

The fix here is almost always to make the dependency chain explicit all the way down. Don’t depend on a computed field; depend on the underlying stored fields. Your graph is longer but correct.

How to debug this in practice

When a computed field misbehaves, I run through this list in order:

  1. Is the @api.depends string a valid path? Check with record.mapped('the.path.from.depends') in a shell and see if it returns what you expect.
  2. Is the field stored? If yes, does a record.recompute(['field_name']) fix it?
  3. Is it a sudo() or access-rule issue? Run as odoo.SUPERUSER_ID and see if the problem disappears.
  4. Is there a flush/invalidate needed? Add self.env.flush_all() and retest.
  5. Is it a compute-inverse loop? Trace the calls with _logger.info('compute called with ids=%s', self.ids) on both methods and watch the log.

About once a quarter I hit a sixth case that isn’t in this list. Odoo’s ORM has a lot of corners. When that happens, the Odoo source is the source of truth — odoo/models.py and odoo/fields.py in your installed version. Grep for _compute_field_value and _recompute_recordset and read the surrounding code. It’s dense but it’s all there.

The rule I keep coming back to: if a computed field is doing something weird, assume the ORM is behaving correctly and your mental model is wrong. It almost always is.

ormodoo-18computed-fieldsdebugging