Odoo Computed Fields and @api.depends: A Developer’s Guide

Abstract technical illustration for the article: Odoo Computed Fields and @api.depends: A Developer's Guide

Half the fields on any real Odoo screen were never typed by anyone. Order totals, margins, deadline dates, display names — they are derived from other data, and the framework keeps them correct as that data changes. The mechanism is the computed field: a field whose value comes from a Python method rather than a database column. It is one of the most-used and most-misunderstood features in Odoo development. Used well, your application feels alive; used badly, you ship the worst class of bug there is — numbers that are silently wrong.

What a computed field is

Most fields are stored: their values sit in a database column and are read back directly. A computed field breaks that assumption. Instead of reading from the database, its value is produced on the fly by calling a method of the model. You declare one by passing compute the name of that method:

from odoo import api, fields, modelsclass EstateProperty(models.Model):    _name = "estate.property"    living_area = fields.Integer(string="Living Area (sqm)")    garden_area = fields.Integer(string="Garden Area (sqm)")    total_area = fields.Integer(        string="Total Area (sqm)",        compute="_compute_total_area",    )    @api.depends("living_area", "garden_area")    def _compute_total_area(self):        for record in self:            record.total_area = record.living_area + record.garden_area

The official Odoo tutorial states the contract plainly: the computation method should set the value of the computed field for every record in self. By convention the method is private — its name starts with an underscore — signalling it belongs to the business tier rather than the presentation tier. Because no column is created, a non-stored computed field costs nothing at write time and always reflects current data.

self is a collection: always loop

The first thing to internalize is that self in a compute method is a recordset — an ordered collection of records — not a single record. The tutorial spells it out: iterating over self gives the records one by one, each itself a collection of size one, and you access or assign fields on single records with dot notation.

This is why the loop is not optional. The ORM may call your compute method with dozens or hundreds of records at once, especially during batch operations. Code that assumes a single record works on singletons and misbehaves the moment the framework hands it a batch. The loop also makes the assignment contract explicit: every record in self must get a value, every time. A compute method that leaves some records unset produces undefined values, and the bugs surface far from the code that caused them.

Declaring dependencies with @api.depends

The ORM cannot know which fields your computation reads, so you declare them with the @api.depends decorator. The listed dependencies are the trigger wiring: when one is modified, the ORM marks the computed field stale and recomputes it, cascading into anything that depends on the computed field in turn.

The critical point is that depends is not documentation — it is load-bearing. Omit a field your method actually reads, and the computed value will not refresh when that field changes. The screen will show a number that was correct ten minutes ago, no error will be raised anywhere, and you will discover the problem from a confused user rather than a stack trace. List every field the method reads, on the record itself and on related records.

Dotted paths through relations

Dependencies can reach across relationships with dotted paths. If a computed description incorporates the customer's name:

description = fields.Char(compute="_compute_description")partner_id = fields.Many2one("res.partner", string="Customer")@api.depends("partner_id.name")def _compute_description(self):    for record in self:        record.description = "Order for %s" % record.partner_id.name

The official docs confirm this works through Many2one, Many2many, and One2many fields alike. The dotted path registers the dependency on the far side: change the partner's name, and every record watching partner_id.name is recomputed. That is exactly what you want for correctness — and exactly what you must respect for performance, because one edit on a popular partner can trigger recomputation across every document referencing it. Keep dependency paths as short and local as the business logic allows.

Stored or not stored: the decision that matters

Computed fields are not stored in the database by default. That single fact has three consequences the tutorial calls out explicitly:

  1. You cannot search a non-stored computed field — unless you implement a custom search method that rewrites the domain into something the database can evaluate.
  2. store=True fixes that, making the field searchable, sortable, and groupable. The ORM writes the value to the database and recomputes it when a dependency changes.
  3. Storing has a real cost. Every change to a dependency triggers recomputation for every dependent record — which, the tutorial warns, can quickly become prohibitive when millions of records need recomputation.

The decision rule: keep the field non-stored when it is only displayed and cheap to compute. Store it when you need to search, filter, sort, or group by it, or when the computation is expensive and the result is read far more often than the dependencies change. When you do store, audit the dependency graph for high-fan-out paths — a stored field watching a dotted path through a heavily referenced record is a performance incident waiting for a busy Monday.

Chains of computed fields

A computed field can depend on another computed field, and the ORM resolves the chain in the right order — recomputing upstream fields before the ones that read them. This lets you build clean layered logic: a line subtotal from quantity and price, an order total from the subtotals, a margin from the total and the cost.

The cost is that deep chains multiply recomputation work. Keep chains shallow, keep each step cheap, and be suspicious of any computed field whose dependency list reads like a tour of the whole database. When a chain gets expensive, store an intermediate result rather than recomputing the whole tower on every read.

Inverse methods: making computed fields writable

Sometimes you want both directions: a validity duration and a deadline date, where editing either updates the other. Odoo supports this with an inverse function:

total = fields.Float(compute="_compute_total", inverse="_inverse_total")amount = fields.Float()@api.depends("amount")def _compute_total(self):    for record in self:        record.total = 2.0 * record.amountdef _inverse_total(self):    for record in self:        record.amount = record.total / 2.0

The division of labour is precise: the compute method sets the field from its dependencies, while the inverse method sets the dependencies from the field. Note the timing asymmetry the tutorial highlights — the inverse method is called when the record is saved, while the compute method runs at each change of its dependencies. Use inverse methods only where the two-way relationship is genuinely part of the business meaning; a writable computed field that users misunderstand creates data that looks authoritative but was half-derived.

related: the shortcut when nothing is really computed

Not every derivation needs a compute method. If the field's value is just a value from a related record with no transformation, use the related attribute instead:

salesperson_email = fields.Char(    string="Salesperson Email",    related="user_id.partner_id.email",    readonly=True,)

A related field follows the dotted path automatically — no compute method, no @api.depends to maintain, and dependency tracking comes for free. It is stored only if you ask it to be. Reach for related whenever the logic is “show me that field from over there”; reserve compute methods for cases where you transform, combine, or conditionally derive.

Computed fields versus onchange

Newcomers often reach for @api.onchange to do what a computed field does. The official guidance is unambiguous: always prefer computed fields. An onchange only fires in the form view while the user is typing — a UI convenience for data entry. A computed field is enforced by the ORM itself, so it also runs for programmatic creation, imports, API calls, and scheduled jobs. Using an onchange for business logic is a very bad idea, precisely because onchanges never fire when records are created programmatically.

There is a practical reason too: debuggability. A computed field's value is set by one named method, so tracing where a value came from is straightforward. Onchanges can pile up — several methods setting the same field, each firing in form context only — and tracking down which one produced a value becomes genuinely difficult. Keep onchanges for form assistance, like suggesting a default garden area when a checkbox is ticked. Put the real logic in computed fields.

The silent-stale-value debugging playbook

When a computed field shows a wrong-but-plausible value and nothing raises, work through this list:

  1. Check @api.depends first. Nine times out of ten, the method reads a field that is not declared.
  2. Check that every record gets a value. A conditional that assigns only in some branches leaves the others undefined.
  3. Check for recursion. A compute method that writes a field it transitively depends on can loop or thrash.
  4. Check stored-field fan-out. If a save takes suspiciously long, a stored computed field is probably recomputing across a huge dependency fan-out.
  5. Check the batch assumption. If it works on one record in the UI but breaks in imports, the method assumed a singleton. Loop over self.

Further reading

Similar Posts

Leave a Reply