DEV Community

Aaron Jones
Aaron Jones

Posted on

Computed Fields Causing Infinite Recomputations (odoo)

Problem Statement: Poorly designed computed fields trigger unnecessary recomputations or infinite loops due to incorrect dependency declarations (@api.depends), leading to performance issues and unstable behavior

Step 1 Identify the problematic computed field

Typical symptoms:

  • Form view never finishes loading
  • CPU spikes when opening records
  • Logs show repeated recomputation
  • Server freezes after record creation/update

Enable logs:

--log-level=debug

Look for repeating:

Computing field x_field_name

Step 2 Identify common WRONG patterns (most bugs come from here)
WRONG: Computed field depends on itself

@api.depends('total_amount')
def _compute_total_amount(self):
    for rec in self:
        rec.total_amount = rec.price * rec.qty
Enter fullscreen mode Exit fullscreen mode

This causes infinite recomputation

WRONG: Writing to other fields inside compute

@api.depends('price', 'qty')
def _compute_total(self):
    for rec in self:
        rec.total = rec.price * rec.qty
        rec.subtotal = rec.total * 0.9  # BAD
Enter fullscreen mode Exit fullscreen mode

WRONG: Using write() inside compute

@api.depends('line_ids.amount')
def _compute_amount(self):
    for rec in self:
        rec.write({'total': sum(rec.line_ids.mapped('amount'))})

Enter fullscreen mode Exit fullscreen mode

Triggers recursive recompute + database writes

Step 3 Define correct dependencies ONLY
Correct dependency declaration

@api.depends('price', 'qty')
def _compute_total(self):
    for rec in self:
        rec.total = rec.price * rec.qty
Enter fullscreen mode Exit fullscreen mode

Rules:
Depend only on source fields
Never depend on computed field itself
Avoid @api.depends('*')

Step 4 Never modify other fields inside compute
Correct approach: separate computed fields

total = fields.Float(compute='_compute_total', store=True)
subtotal = fields.Float(compute='_compute_subtotal', store=True)

@api.depends('price', 'qty')
def _compute_total(self):
    for rec in self:
        rec.total = rec.price * rec.qty

@api.depends('total')
def _compute_subtotal(self):
    for rec in self:
        rec.subtotal = rec.total * 0.9
Enter fullscreen mode Exit fullscreen mode
  • Clean
  • Predictable
  • No loops

Step 5 Use store=True ONLY when required
BAD

total = fields.Float(compute='_compute_total', store=True)
Enter fullscreen mode Exit fullscreen mode

If:

  • Field is rarely searched
  • Used only in UI

GOOD

total = fields.Float(compute='_compute_total')
Enter fullscreen mode Exit fullscreen mode

store=True increases recomputation cost
Use only when:

  • Searching
  • Grouping
  • Reporting

Step 6 Avoid ORM writes in compute (use onchange instead)
WRONG (compute)

@api.depends('qty')
def _compute_price(self):
    for rec in self:
        rec.price = rec.qty * 10
Enter fullscreen mode Exit fullscreen mode

RIGHT (onchange)

@api.onchange('qty')
def _onchange_qty(self):
    self.price = self.qty * 10
Enter fullscreen mode Exit fullscreen mode

UI-only logic
No recomputation loops

Step 7 Use @api.depends_context when needed

If compute depends on company, language, or user:

@api.depends_context('company')
@api.depends('amount')
def _compute_tax(self):
    for rec in self:
        rec.tax = rec.amount * rec.company_id.tax_rate
Enter fullscreen mode Exit fullscreen mode

Prevents unnecessary recomputation.

Step 8 Fix One2many / Many2one loops
COMMON LOOP

@api.depends('line_ids.total')
def _compute_total(self):
    for rec in self:
        rec.total = sum(rec.line_ids.mapped('total'))
Enter fullscreen mode Exit fullscreen mode

If line.total depends back on parent → loop.

SAFE VERSION

@api.depends('line_ids.price', 'line_ids.qty')
def _compute_total(self):
    for rec in self:
        rec.total = sum(
            line.price * line.qty for line in rec.line_ids
        )
Enter fullscreen mode Exit fullscreen mode

Step 9 Use SQL constraints instead of compute when possible
BAD compute for validation

@api.depends('qty')
def _compute_check(self):
    if self.qty < 0:
        raise ValidationError("Invalid")
Enter fullscreen mode Exit fullscreen mode

GOOD constraint

@api.constrains('qty')
def _check_qty(self):
    for rec in self:
        if rec.qty < 0:
            raise ValidationError("Quantity cannot be negative")
Enter fullscreen mode Exit fullscreen mode

Step 10 Final Safe Computed Field Template (Best Practice)

total = fields.Float(
    compute='_compute_total',
    store=True,
)

@api.depends('price', 'qty')
def _compute_total(self):
    for rec in self:
        rec.total = rec.price * rec.qty if rec.price and rec.qty else 0.0
Enter fullscreen mode Exit fullscreen mode
  1. No writes
  2. No recursion
  3. Correct dependencies
  4. Safe for production

Conclusion

In Odoo, infinite recomputation issues are almost always caused by impure computed fields fields that write data, depend on themselves, or have poorly defined dependencies. The fix is simple but strict: keep compute methods pure, declare minimal dependencies, avoid ORM writes, and separate UI logic from stored logic. When computed fields are treated as read-only calculations instead of business logic containers, performance stabilizes, recomputation stops, and your Odoo development system becomes predictable and scalable.

Top comments (0)