If you have ever built a custom Odoo module and found yourself staring at a computed field that refuses to accept user input, you are not alone. By default, Odoo’s ORM treats computed fields as read-only — and for good reason. But there are plenty of real-world scenarios where a business needs to override that behaviour and allow manual input alongside automatic computation. Whether you are working on ERP Implementation Australia projects or maintaining an in-house Odoo 19 instance, understanding how to make a computed field editable is a skill that saves significant debugging time.
This guide walks through the three main approaches available in Odoo 19, with Python code examples, a comparison of methods, and practical guidance for both developers writing the logic and business owners trying to understand what their technical team is doing under the hood.
What Is a Computed Field in Odoo 19?
A computed field in Odoo is a field whose value is derived automatically from other fields on the same record, rather than being stored directly by the user. Instead of the user typing a value, Odoo calls a Python method to calculate and return that value on the fly.
A basic example: if you have a quantity field and a unit_price field on a sale order line, you can define a total_price computed field that multiplies the two together. Every time either dependency changes, Odoo recalculates total_price automatically using the @api.depends decorator and the linked compute method.
from odoo import api, fields, models
class SaleOrderLine(models.Model):
_name = 'sale.order.line'
quantity = fields.Float(string='Quantity')
unit_price = fields.Float(string='Unit Price')
total_price = fields.Float(
string='Total Price',
compute='_compute_total_price' ) @api.depends('quantity', 'unit_price') def _compute_total_price(self): for record in self:
record.total_price = record.quantity * record.unit_priceThis is clean, predictable, and efficient. The problem arises when you need the user to also be able to type directly into total_price.
How Odoo Evaluates Computed Fields at Runtime
When a user loads a form view, Odoo’s ORM evaluates all computed fields based on their declared dependencies via field recomputation triggers. For non-stored fields, this happens on the fly each time the record is read. For stored fields, Odoo writes the computed value to the database column whenever a dependency changes. In both cases, the field is marked as read-only by the framework unless you explicitly tell it otherwise.
Why Computed Fields Are Read-Only by Default
The ORM design logic is straightforward: if a field’s value is derived from other fields, allowing a user to directly overwrite it creates a conflict. What should Odoo do the next time the dependency changes — overwrite the user’s manual entry with the computed result? That ambiguity is exactly why Odoo enforces read-only behaviour by default on all computed fields.
This is not a limitation. It is a deliberate Odoo ORM best practice that protects data integrity. A computed field without an inverse method will always be overwritten the moment any of its declared @api.depends dependencies change.
The ORM Design Logic Behind Read-Only Behaviour
At the ORM level, a field defined with only a compute parameter is permanently read-only. This means no user interface, no API call, and no server action can write to it directly without raising an error or being silently ignored. To change this, you need to approach the problem at the model layer, not the view layer. Applying readonly=False in the XML view alone will not solve the problem for a non-stored computed field — it simply exposes the input widget without giving Odoo any instruction on what to do with the value the user enters.
Three Ways to Make a Computed Field Editable
There is no single correct approach. The right method depends on your use case, your performance requirements, and how much control the business needs over the field’s value.
Method 1 — Using the inverse Parameter
his is the most robust and recommended approach in Odoo 19. The inverse parameter on a field definition tells the ORM the name of a Python method to call when a user writes a value to the computed field. That method is responsible for back-propagating the user’s input into the field’s dependencies, keeping the data model consistent.
discount_rate = fields.Float(
string='Discount Rate (%)',
compute='_compute_discount_rate',
inverse='_inverse_discount_rate' ) @api.depends('discount_amount', 'subtotal') def _compute_discount_rate(self): for record in self: if record.subtotal:
record.discount_rate = (record.discount_amount / record.subtotal) * 100 else:
record.discount_rate = 0.0 def _inverse_discount_rate(self): for record in self:
record.discount_amount = (record.discount_rate / 100) * record.subtotal
When the user types a new discount_rate value, _inverse_discount_rate fires and updates discount_amount accordingly. The next time _compute_discount_rate runs, it recalculates from the updated dependency, keeping everything in sync.
For a deeper look at how @api.depends and other ORM decorators work together, the Odoo 19 API Decorators guide is worth reading alongside this article.
Method 2 — Using store=True with readonly=False
If you do not need bidirectional field computation and simply want the field to be editable while also being auto-calculated on dependency changes, you can combine store=True with readonly=False in the view XML.
python
total_price = fields.Float(
string='Total Price',
compute='_compute_total_price',
store=True )Then in your view XML:
<field name="total_price" readonly="0"/>This makes the field editable in the UI. However, be aware of the trade-off: every time a dependency changes, Odoo will overwrite whatever the user entered with the freshly computed value. This approach works well when you want manual entry as a fallback, but automatic computation should take precedence. It is not suitable when the user’s input needs to persist independently of dependency changes.
Method 3 — Controlling Editability via XML View Attributes
For scenarios where the field should be editable only under certain conditions, such as when a record is in draft state or when a specific flag is set, you can use Odoo’s attrs or readonly expression in the view XML without touching the model layer.
<field name="total_price" readonly="state != 'draft'"/>This approach keeps the ORM clean and handles editability purely at the Odoo form view field behaviour level. It is the lightest option but only applies to that specific view. It will not affect API writes or other views where the field appears.
Writing a Clean inverse Method in Odoo 19
The inverse method is a hook. When a user writes a value to the computed field, Odoo calls this method and passes the updated recordset as self. Your job inside the method is to set the relevant dependency fields so that the computation remains logically consistent.
Python Code Walkthrough
Here is a complete, production-ready example using a margin field on a custom sale order line:
python
from odoo import api, fields, models
class CustomSaleOrderLine(models.Model):
_inherit = 'sale.order.line'
margin_percent = fields.Float(
string='Margin (%)',
compute='_compute_margin_percent',
inverse='_inverse_margin_percent',
store=True ) @api.depends('price_unit', 'purchase_price') def _compute_margin_percent(self): for record in self: if record.price_unit:
record.margin_percent = ( (record.price_unit - record.purchase_price) / record.price_unit
) * 100 else:
record.margin_percent = 0.0 def _inverse_margin_percent(self): for record in self: if record.price_unit:
record.purchase_price = record.price_unit * ( 1 - (record.margin_percent / 100) )Notice that store=True is used alongside the inverse parameter. This is important: a non-stored computed field with an inverse method can behave unpredictably because the ORM may not have the cached values needed for the inverse logic to resolve correctly.
Common Mistakes and How to Avoid Them
The most frequent issue developers encounter is writing an inverse method that reads from fields not yet in the ORM cache. If the inverse method accesses a field whose value is not in cache at the time the inverse fires, Odoo returns False for that field. This silently breaks your logic. Always use store=True on the computed field when writing an inverse, and keep the inverse method’s logic minimal — only set the dependencies, do not add additional business logic inside it.
A second common mistake is forgetting that the compute method will re-fire after the inverse runs. If the inverse writes a dependency, the compute re-evaluates immediately. If your compute and inverse are not true inverses of each other mathematically, you will end up in a loop or with a value that differs from what the user entered. Test both directions thoroughly before deploying to production.
Need help applying this to your business?
When to Use inverse vs store=True vs onchange
This is the question that causes the most confusion in Odoo module customization patterns, especially for teams new to Odoo 19 ORM best practices.
Use inverse when you genuinely need bidirectional computation — the user can either let Odoo calculate the field, or manually override it and have the dependencies update accordingly. This is the most powerful and semantically correct option.
Use store=True with readonly=False in the view when computed values need to be searchable and filterable at the database level, and where automatic recomputation should always take precedence over manual input.
Use onchange only for UI-side convenience — populating default values when another field changes in a form view. The Odoo 19 documentation explicitly warns against using onchange for business logic because it does not fire when records are created or updated programmatically. It is a view-level convenience, not a model-level rule.
Performance Considerations for Business Owners and Developers
Every stored computed field adds a write overhead to your database. When a dependency changes on a record, Odoo recalculates the field and writes the result back to the database column. On models with tens of thousands of records and frequently changing dependencies, this can add up. If the field is read far more often than it is written, store=True is the right call. If the field changes constantly, leaving it non-stored and using an inverse without store=True (where supported) may be more appropriate, though this requires extra care with cache handling as noted above.
For business owners evaluating customization scope: making a computed field editable is a targeted change, but it needs a developer who understands the full data lifecycle in Odoo’s ORM. A poorly written inverse method can introduce data inconsistencies that are difficult to trace in production.
If you need help designing computed fields, inverse methods, or custom Odoo module logic that performs reliably at scale, Book a Consultation to discuss your specific requirements and get a clear technical plan for your Odoo 19 project.
Conclusion
Making a computed field editable in Odoo 19 comes down to choosing the right tool for the job. The inverse parameter is the cleanest and most correct solution for true bidirectional field logic. Combining store=True with view-level readonly=False works well when computed values need database persistence and auto-computation should remain dominant. View-level attrs expressions handle conditional editability without touching the model. Each approach has its place, and understanding when to use which one is what separates maintainable Odoo customization from technical debt. Apply the right pattern from the start, test both the compute and inverse paths thoroughly, and your fields will behave exactly as the business expects.
You’re here because something matters.
If this decision impacts your operations, your team, or your growth
Let’s talk before it becomes harder to undo.
Frequently Asked Questions
1. Can I make a computed field editable without an inverse method?
Yes, in limited scenarios. If the field is defined with store=True, you can set readonly=False in the view XML to allow user input. However, the next time a dependency changes, Odoo will overwrite the user’s value with the freshly computed result. For true user-controlled editability that persists independently, an inverse method is required.
2. Does store=True automatically make a computed field editable?
No. store=True tells Odoo to write the computed value to the database, which enables searching and filtering. Editability is a separate concern controlled by the inverse parameter at the ORM level or readonly attributes at the view level. You need to explicitly enable editability alongside store=True.
3. What happens when both a compute and inverse method are defined?
By default, the summary export does not break down figures by analytic account. However, if you apply an analytic filter within the General Ledger report before exporting, Odoo 19 will reflect that filter in the output, giving you a view scoped to specific projects, cost centres, or analytic tags.
4. Is the XLSX export from the General Ledger in Odoo 19 suitable for further processing in Excel?
Yes. The XLSX file generated by Odoo 19 is a standard Excel-compatible format. You can open it in Microsoft Excel, Google Sheets, or any compatible spreadsheet tool and apply further formatting, formulas, or pivot tables as needed for management reporting ERP workflows.
5. Can I schedule automatic General Ledger exports in Odoo 19?
Odoo 19 does not natively support scheduled automatic exports of the General Ledger as a built-in feature. However, with the right configuration through Odoo’s automation tools or a custom integration, periodic exports can be triggered and delivered to a designated location or email. This is a common request during Odoo implementations and is worth raising with your consultant during setup.
Real Stories. Real Results.
See what our clients have to say — in their own words. These video testimonials share genuine experiences from business owners and teams who’ve transformed their operations with Odoo. From smoother workflows to faster decision-making, their stories reflect the real impact of getting the right system and guidance.