Inverse Methods in Odoo 19: When and How to Use Them

Inverse methods in Odoo 19 shown in a Python model where a computed field uses compute and inverse to stay editable.

Every Odoo developer eventually hits the same wall. You ship a clean computed field, the client opens the form, clicks into it, and nothing happens. The field is greyed out. The ORM has decided, quite reasonably, that a value it calculates is a value nobody else gets to touch. That is the moment the inverse method stops being a footnote in the documentation and starts being the tool you actually need.

I run into this on almost every custom module I build through my Odoo customization services, and I have learned that the inverse parameter is one of the most misunderstood pieces of the Odoo ORM. Developers reach for it too early, use it as a general purpose write hook, or forget that the compute method will happily overwrite whatever the user just typed. This guide covers what an inverse method really does in Odoo 19, when it is the right pattern, and how to write one that survives contact with production data.

What an Inverse Method Actually Does

A computed field in Odoo is read-only by default because the framework has no idea how to reverse your calculation. If total = quantity * price, the ORM cannot guess whether writing 500 to total should change the quantity, the price, or both. The inverse parameter is how you answer that question explicitly.

When you attach inverse="_inverse_total" to a field definition, you are telling Odoo that a write to this field is legal, and that your method is responsible for translating that written value back into the underlying source fields. The framework flips the field from read-only to writable in the UI, accepts the value into the environment cache, and then calls your inverse method during the flush. Your job inside that method is to push the value backwards through the business logic layer.

The critical detail people miss is the ordering. The inverse method runs after the value is set, not instead of the compute method. If the fields named in your @api.depends decorator change later, the compute method fires again and your user’s manual entry disappears. That is not a bug. It is the re-computation contract working exactly as declared, and designing around it is most of the work.

When to Use an Inverse Method and When to Avoid It

The inverse pattern fits well when the relationship between the computed value and its sources is genuinely reversible and there is one obvious source field to write to. Currency conversion is a good example. A discount percentage that maps cleanly to a discount amount is another. So is a convenience field that flattens a related record’s value into the current form, where writing back means updating exactly one field on exactly one related record.

I avoid the pattern in three situations. First, when the reverse mapping is ambiguous, because you will end up encoding an arbitrary business rule that nobody documented and everyone forgets. Second, when the field is really a default rather than a calculation, in which case store=True combined with readonly=False gives you a value that seeds itself and then stays put once a user edits it. Third, when what you actually want is a validation or side effect on save, which belongs in a write() override or a constraint, not in an inverse.

I walked through all three alternatives in detail in my earlier post on how to make a computed field editable in Odoo 19, and it is worth reading alongside this one if you are still deciding which approach fits your model.

How to Write an Inverse Method in Odoo 19

The mechanics are simple. Declare the field with both a compute method and an inverse method, keep the dependencies honest, and write the reverse logic.

from odoo import api, fields, models


class SaleOrderLine(models.Model):
    _inherit = "sale.order.line"

    discount_amount = fields.Monetary(
        string="Discount Amount",
        compute="_compute_discount_amount",
        inverse="_inverse_discount_amount",
        store=True,
        readonly=False,
        currency_field="currency_id",
    )

    @api.depends("price_unit", "product_uom_qty", "discount")
    def _compute_discount_amount(self):
        for line in self:
            gross = line.price_unit * line.product_uom_qty
            line.discount_amount = gross * (line.discount / 100.0)

    def _inverse_discount_amount(self):
        for line in self:
            gross = line.price_unit * line.product_uom_qty
            if gross:
                line.discount = (line.discount_amount / gross) * 100.0
            else:
                line.discount = 0.0

The compute method turns a percentage into money. The inverse method turns money back into a percentage. A salesperson can now type either one and the other follows, which is exactly the behaviour clients ask for when they say the discount field “should just work both ways.”

Handling the Recordset Correctly

Both methods must iterate over self. Odoo calls them with a recordset, not a single record, and a method that assumes one record will break the moment someone imports a CSV or runs a mass update from the list view. Assign a value to every record in the set, including the zero or false cases, or you will see the classic missing value error during flush.

Guard your division. In the example above, a line with no quantity would raise a ZeroDivisionError the first time a user cleared the field. Production data is full of half-filled records, and the inverse method is the first place that shows.

Inverse Methods, store=True, and Search Methods

An inverse method works with or without storage, but the two combinations behave differently and the choice matters for performance. With store=True and readonly=False, the value lives in a real database column, so it can be searched, sorted, and grouped, and the compute method acts as a smart default that a user can override. Without storage, the field recalculates on every read, which keeps the data consistent but removes it from the ORM’s reach for filtering and reporting.

If you keep the field unstored and users still need to filter on it, add a search method rather than switching on storage just for the search. A search method translates the user’s operator and value into a domain against real columns, which gives you filtering without paying the write cost on every dependency change. On a large sale order table, that difference is measured in seconds per operation, not milliseconds.

One more Odoo 19 detail worth knowing: compute_sudo defaults to True for stored computed fields and False for unstored ones, and the precompute flag can move calculation ahead of record insertion. Neither changes how the inverse method behaves, but both affect what your users see when access rights or record rules restrict the source fields.

Mistakes I See in Production Code

The most common one is using the inverse method as a general write hook. It fires only when that specific field is written, not on every save, so validation placed there will silently skip most updates. Use @api.constrains for validation.

The second is an incomplete @api.depends declaration. If your compute method reads a field that is not listed, the value is correct on creation and quietly goes stale afterwards. Dotted paths such as order_line.price_total are supported, and self-referential dependencies need recursive=True set explicitly.

The third is fighting the compute and inverse pair against each other in a loop, usually by writing to a field that is itself a dependency of the same compute method. When that happens, step back and ask whether the field should be a plain stored field with an onchange method instead. Not every editable value needs to be computed.

If you are weighing the inverse pattern against a write() override for a specific model and want a second opinion before it reaches production, Book a Consultation and we can look at the model, the dependency graph, and the data volume together. Getting this decision right early is considerably cheaper than untangling it after go-live.

Conclusion

Inverse methods are a precision tool, not a general purpose escape hatch from read-only fields. Use them when the reverse mapping is unambiguous and the write genuinely belongs back on the source field. Iterate over the recordset, guard your arithmetic, declare every dependency, and decide deliberately whether the field should be stored, unstored with a search method, or not computed at all. Do that, and the field behaves the way your users expect on the first day and stays predictable on the thousandth.

Frequently Asked Questions

Does an inverse method make a computed field editable in every view?

It makes the field writable at the model level, so it becomes editable in form and list views by default. You can still restrict it per view with the readonly attribute in XML if a particular screen should not accept input.

Will my compute method overwrite what the user typed?

Yes, if any field listed in your @api.depends decorator changes afterwards. The compute method reruns and replaces the manual value. If that is unacceptable, store the user’s intent in a separate field or narrow the dependency list.

Can I use an inverse method without store=True?

You can. The field recalculates on every read and cannot be searched or grouped through the ORM unless you also define a search method. Unstored plus inverse is a valid combination for lightweight, display-oriented fields.

How is an inverse method different from an onchange method?

An onchange method runs in the browser session before the record is saved and is purely a UI convenience. An inverse method runs server side during the flush and applies to any write, including imports, API calls, and automated actions.

What happens if my inverse method does not assign a value to every record?

The ORM raises a missing value error during flush for the records you skipped. Always loop over self and assign in every branch, including the empty, zero, and false cases.

Reach Out for Support

Facing a problem? Contact us and receive expert help and fast solutions.