Summary
A developer reports that a computed field in Odoo (total_discount on sale.order) fails to update when its dependencies (order_line.discount and order_line.price_total) change. The root cause is typically insufficient dependency specification regarding the relationship between the parent record and the child lines, or a lack of proper view configuration to trigger the computation on the client side. The fix involves ensuring the @api.depends decorator captures changes in the related One2many field’s sub-fields and verifying the view forces a re-evaluation.
Root Cause
The immediate cause is that the @api.depends definition is incomplete or the view does not signal the dependency change correctly to the Odoo ORM.
- Missing Parent/Child Context: While
@api.depends('order_line.discount')is technically valid for field-level triggers, it often fails to trigger the parent’s computed field recalculation when modifications happen inside an editable list view unless the framework sees a direct link. The robust dependency is@api.depends('order_line.discount')(already present), but the issue often lies in how the data is saved. - View Save Behavior: In Odoo, computed fields are recalculated on the fly by the server when data is saved or read. If the “Save” button isn’t clicked or the view doesn’t enforce a reload of the parent record, the client (JavaScript) holds a stale value.
- Server-Side Logic: The formula
sum(line.discount)assumesline.discountis the raw value. Ifdiscountis a stored/computed field itself (rare on standardsale.order.line, but possible in custom modules) and not correctly saved, it might not be available.
Why This Happens in Real Systems
Odoo’s ORM is event-driven but relies on explicit dependency chains.
- Transient State: In an editable list view (inline editing), changing a value on a line doesn’t immediately trigger the parent record’s computed fields. The parent only recalculates when the record is explicitly saved (posted) to the database.
- Computed Fields on Related Records: If
order_linecontains computed fields (likeprice_totalused in the dependency), and those fields are not stored (store=True), they are calculated in memory. If the dependency chain breaks (e.g., the line is modified but the parent isn’t told to recalculate), the parent sees the old value. - Stale Cache: The browser might display the calculated value from the previous server interaction because the JSON-RPC call to update the line didn’t return the updated parent total.
Real-World Impact
- Data Integrity: The total discount displayed on the Sales Order is incorrect, leading to confusion for sales representatives and customers.
- Process Friction: Users must manually trigger a refresh (e.g., saving and reloading the whole form) to see correct totals, breaking the workflow flow.
- Reporting Errors: If
total_discountis used in financial reports, incorrect values lead to revenue recognition errors.
Example or Code
To fix the issue, we must ensure the dependency catches changes in the sub-records effectively. If the discount field is defined on the line, the dependency @api.depends('order_line.discount') is usually sufficient for the ORM, but explicit syntax @api.depends('order_line', 'order_line.discount') is safer to ensure the computation triggers when lines are added/removed and when their discounts change.
Here is the corrected implementation:
from odoo import models, fields, api
class SaleOrder(models.Model):
_inherit = 'sale.order'
total_discount = fields.Float(compute='_compute_total_discount', store=True, readonly=True)
@api.depends('order_line.discount')
def _compute_total_discount(self):
for order in self:
order.total_discount = sum(line.discount for line in order.order_line)
How Senior Engineers Fix It
Senior engineers approach this by validating the data flow from the database up to the UI.
- Verify the Decorator: They ensure the
@api.dependslists the specific fields that trigger the calculation. If the linediscountfield is modified via a button or code, the dependency must reflect that. - Check View Architecture: They inspect the XML view. The
order_linefield must be anOne2manywidget (usuallyone2manyorlines). If the view uses a specific widget that bypasses standard Odoo saving mechanisms, it must be adjusted to trigger the standardonchangeorsaveevents. - Ensure Store=True (If needed): If the field needs to be searchable or sorted,
store=Trueis required. However, for display only,store=False(default) works, but requires the client to request the value. - Debugging: They use
pdbor logs in the compute method to see if the method is being called at all when the line changes. If it’s not called, the dependency is wrong. If it’s called but returns the old value, the data hasn’t been committed to the transaction memory yet. - Clear Cache: They force a cache clear (
self.invalidate_cache()) if necessary, though Odoo usually handles this.
Why Juniors Miss It
- Misunderstanding
@api.depends: Juniors often think@api.depends('order_line.discount')is enough for any change on the line, but forget that Odoo only re-evaluates this when the parent record’s state changes or a specific onchange is triggered. - Ignoring the View: They focus entirely on the Python backend and forget that the
order_linewidget in the view needs to be configured to trigger the parent’s onchange events when a line is edited inline. - Assuming Immediate Recalculation: They expect the parent field to update instantly in the UI as they type in the child list, not realizing Odoo’s standard architecture requires a save/refresh cycle for 100% accuracy unless client-side logic (Odoo Studio/OWL) is used.
- Tag Confusion: They often include irrelevant tags (like
javascriptorcomputer-visionin this specific input) indicating they are shooting in the dark rather than understanding the ORM’s event-driven nature.