Dieser Artikel ist derzeit nur auf Englisch verfügbar. Die Übersetzung folgt bald.
You add or modify a computed field, restart Odoo, and the registry blows up at module load time:
RecursionError: maximum recursion depth exceeded while calling a Python object
Or more specifically:
ValueError: Recursive dependency detected on field 'sale.order.amount_total'
This stops the server from starting cleanly on Odoo 17.0/18.0/19.0. The error is the ORM refusing to build a circular dependency graph for a @api.depends declaration that ends up watching itself. The fix is straightforward once you see the cycle, but finding the cycle in a deep field tree takes some discipline.
Quick Fix
Find the cycle by reading every @api.depends in the call chain on paper, then break it. The shortest fix is to remove the back-reference and use a one-way trigger:
# WRONG — A depends on B, B depends on A
class A(models.Model):
field_a = fields.Float(compute='_compute_a')
@api.depends('b.field_b')
def _compute_a(self):
for rec in self:
rec.field_a = rec.b.field_b * 2
class B(models.Model):
field_b = fields.Float(compute='_compute_b')
@api.depends('a.field_a') # cycle!
def _compute_b(self):
for rec in self:
rec.field_b = rec.a.field_a / 2
# RIGHT — break the cycle by storing one side
class B(models.Model):
field_b = fields.Float() # no compute; set explicitly when needed
Why This Happens
@api.depends declares which fields trigger a recompute. Odoo builds a directed graph at registry load time. If the graph has a cycle, the recompute order is undefined — Odoo aborts rather than hang. The four common causes:
- Direct cycle. Field A depends on B, B depends on A. Easy to spot, easy to fix.
- Indirect cycle. A depends on B which depends on C which depends on A. Common in modules with deep customization layers.
- Self-reference. A depends on its own field, often via a related field:
@api.depends('order_line.order_id.amount_total')whereorder_idis a back-reference toself. - Missing depends, real cycle. You declared
@api.depends('a', 'b')where the actual logic also touchesc. The cycle is hidden until you addcto depends and the real graph appears.
Step-by-Step Diagnosis
1. Read the error's field name. It tells you exactly where the cycle starts.
2. Map the dependency graph. For the named field, list every @api.depends in your codebase that references it, and every field that field depends on.
3. Use Odoo's tools. In a shell:
field = env['sale.order']._fields['amount_total']
for dep in field._depends:
print(dep)
For deeper analysis, the OCA dev_field_dependency_graph module dumps the full registry-level graph as Graphviz. Indispensable for large codebases.
4. Walk the graph from the error. Start at the named field. Each step, follow the @api.depends to the next node. If you return to the start, you have your cycle.
5. Check related fields. A related='partner_id.parent_id.partner_id' is a sneaky cycle if parent_id and partner_id reference each other.
Permanent Fix
For direct cycles, decide which side is the source of truth. The other side either becomes a stored field set explicitly, or recomputes on a different trigger:
class A(models.Model):
a_total = fields.Float(compute='_compute_a_total')
@api.depends('lines.price')
def _compute_a_total(self):
for rec in self:
rec.a_total = sum(rec.lines.mapped('price'))
class ALine(models.Model):
a_id = fields.Many2one('a')
price = fields.Float()
# No compute back to a_total — that direction is read-only
For indirect cycles, the graph traversal in the diagnosis section identifies which edge to remove. Usually the correct break is the most recent customization layer — older code is usually correct.
For self-references via related, replace the related with a compute that is explicit about the path:
# WRONG — related field cycles through order_id
salesperson_email = fields.Char(related='order_id.user_id.partner_id.email')
# RIGHT — compute with explicit deps, no implicit back-reference
salesperson_email = fields.Char(
compute='_compute_salesperson_email',
store=True,
)
@api.depends('order_id.user_id', 'order_id.user_id.partner_id.email')
def _compute_salesperson_email(self):
for line in self:
line.salesperson_email = line.order_id.user_id.partner_id.email or False
For "missing depends" cycles, audit the compute body and add every field accessed to @api.depends. If adding the missing field reveals a cycle, you have a real bug to fix — your compute should not need that field, or the architecture needs a different shape.
How to Prevent It
- Direct cycles fail in development. Always restart your server after adding a compute. The
RecursionErroron registry load is your friend — far better than a runtime infinite loop. - Audit related-field chains. Any
related='a.b.c.d'longer than three hops is a code smell. Replace with an explicit compute. - One-way dataflow. Pick a direction for derived data. Order computes from order lines, never lines computing from order. Invoice computes from invoice lines, never the reverse. Treat this as architectural and cycles disappear.
- Static analysis.
pylint-odooflags some@api.dependsissues. The OCAdev_field_dependency_graphshows the full graph visually — run it on every major release of your module. - Document the dependency direction in the field's docstring. A one-line comment ("computes from order_line; never set on lines from order") prevents the next developer from creating a back-reference.
- Test on a populated database. Cycles that fail at registry load are easy. Cycles that only fire under specific data shapes (a rare related-field traversal) need a populated test database to catch.
Related Errors
- Stale related field cache invalidation — when the dependency graph is fine but cache invalidation is wrong.
- Required field not set — when a broken compute leaves a required field empty.
- Expected singleton — common companion bug in compute methods.
Frequently Asked Questions
Why does Odoo not handle cycles automatically?
Because the recompute order matters. With A = f(B) and B = g(A), Odoo would have to pick a direction and compute one side from a stale cache, producing wrong values. Refusing the cycle forces you to choose explicitly which side is derived.
Can I use @api.depends_context instead?
@api.depends_context triggers recompute when context keys change, not when fields change. It does not break field cycles — it serves a different purpose (context-sensitive computes). Mixing them is a sign of a deeper modeling issue.
What about compute='_compute_x' with store=False?
Non-stored computes do not write to the database, but they still go through the dependency resolver. A cycle still raises. The fix is the same: break the cycle.
My cycle only appears after installing a third-party module — can I work around it?
Yes. Override the compute in your own module to remove the back-reference:
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.depends('order_line.price_subtotal') # narrowed depends
def _compute_amount(self):
return super()._compute_amount()
But the right long-term fix is to file an issue with the third-party module. ECOSIRE's Odoo support team regularly upstreams these fixes for our customers.
What if my "cycle" is intentional — like A and B mutually adjusting?
Then one direction must be triggered manually (a button or onchange), not automatically (a compute). Computes are for derived data that is always a function of inputs. Mutual adjustment is workflow logic, which belongs in explicit methods. The instinct to model bidirectional dependencies via compute is the wrong instinct in 90 percent of cases.
Can I see the full dependency graph for a model?
Yes, with the OCA dev_field_dependency_graph module. It exports the registry's compute graph as Graphviz DOT, which renders as a visual map. Indispensable when debugging large modules with many computed fields. Run it once after every major release and store the diff — sudden new edges flag risky changes.
How do I test that my fix actually broke the cycle?
Restart Odoo with -u <module>. If the registry loads cleanly, no cycle exists. Then unit-test that values still compute correctly: create a record, write to A, assert B updated; write to B, assert A did not update (since you broke that direction). Both halves matter.
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
Geschrieben von
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Transformieren Sie Ihr Unternehmen mit Odoo ERP
Kompetente Odoo-Implementierung, Anpassung und Support zur Optimierung Ihrer Abläufe.
Verwandte Artikel
So fügen Sie einer Odoo-Formularansicht eine benutzerdefinierte Schaltfläche hinzu (2026)
Fügen Sie benutzerdefinierte Aktionsschaltflächen zu Odoo 19-Formularansichten hinzu: Python-Aktionsmethode, Ansichtsvererbung, bedingte Sichtbarkeit, Bestätigungsdialoge. Produktionsgeprüft.
So fügen Sie ein benutzerdefiniertes Feld in Odoo ohne Studio hinzu (2026)
Fügen Sie benutzerdefinierte Felder über ein benutzerdefiniertes Modul in Odoo 19 hinzu: Modellvererbung, Ansichtserweiterung, berechnete Felder, Store/Non-Store-Entscheidungen. Code-First, versioniert.
So fügen Sie einen benutzerdefinierten Bericht in Odoo mithilfe eines externen Layouts hinzu
Erstellen Sie einen gebrandeten PDF-Bericht in Odoo 19 mit web.external_layout: QWeb-Vorlage, Papierformat, Aktionsbindung. Mit gedrucktem Logo + Fußzeilenüberschreibungen.