This article is currently available in English only. Translation coming soon.
You update a field on a parent record and the related field on a child record still shows the old value:
# In a shell
order.partner_id.email # shows new value: '[email protected]'
order.line_ids[0].partner_email # shows OLD value: '[email protected]'
# where partner_email is fields.Char(related='order_id.partner_id.email')
There is no exception, no error log — just wrong data on screen and in reports. This is one of the most insidious Odoo bugs. It bites Odoo 17.0/18.0/19.0 alike, and it almost always points at a cache invalidation gap, not a database problem.
Quick Fix
Force the cache to clear before the read:
self.invalidate_recordset(['partner_email'])
fresh_value = self.partner_email
For related fields, the durable fix is usually to switch to a stored compute with explicit @api.depends:
# WRONG — related field, cache invalidation can lag
partner_email = fields.Char(related='order_id.partner_id.email')
# RIGHT — explicit, predictable
partner_email = fields.Char(
compute='_compute_partner_email',
store=True,
)
@api.depends('order_id.partner_id.email')
def _compute_partner_email(self):
for line in self:
line.partner_email = line.order_id.partner_id.email or False
Why This Happens
Odoo keeps a per-cursor cache of field values. When you write to a field, Odoo invalidates the cache for that field and any field that depends on it via @api.depends. Cache invalidation breaks down when:
- The dependency graph is incomplete. A
relatedfield's invalidation chain isfield <- intermediate <- root. If the chain is long or crosses acomputethat has wrong@api.depends, invalidation stops short. - A direct SQL
UPDATEbypassed the ORM. The database row is current, but the in-memory cache still has the old value. - Multi-cursor isolation. A request handler's cursor sees a value committed by another worker after the request started. Until the next request, the cache shows the older value.
browse()afterwrite()in the same flow without a flush. The cache returns the value from before the write because no read has refreshed it.prefetch_fieldshas loaded a stale value into the prefetch cache, and subsequent accesses use it.
Step-by-Step Diagnosis
1. Confirm the database is correct. Run direct SQL:
SELECT email FROM res_partner WHERE id = 7;
If the database has the new value but Odoo shows the old value, it is a cache problem, not a data problem.
2. Check the dependency graph.
field = env['sale.order.line']._fields['partner_email']
print(field._depends) # the full transitive depend tree
print(field.related) # if it is a related field
Look for gaps. If partner_email is related='order_id.partner_id.email' but order_id was set after the line was created, the cache may not have linked them.
3. Force invalidation manually.
self.env.invalidate_all() # nuclear — clears entire cache
fresh = self.partner_email
If fresh shows the right value, you have confirmed it is a cache-invalidation gap.
4. Check for direct SQL writes. Search your codebase for cr.execute("UPDATE ..."). Each one is suspect — direct SQL bypasses the ORM cache.
5. Test in a fresh request. Reload the browser tab. If the value is now correct, the bug is intra-request — invalidation never fired during that request's flow.
Permanent Fix
Replace fragile related with stored compute fields:
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
partner_email = fields.Char(
compute='_compute_partner_email',
store=True,
readonly=True,
)
@api.depends('order_id.partner_id.email')
def _compute_partner_email(self):
for line in self:
line.partner_email = line.order_id.partner_id.email or False
Stored computes with explicit deps are predictable, indexable, and survive every Odoo upgrade. related fields are syntactic sugar with looser invalidation semantics — fine for trivial cases, fragile when the chain crosses module boundaries.
For places where you wrote and immediately read, explicitly flush:
order.partner_id = new_partner
self.env.flush_all() # writes any pending changes
self.env.invalidate_all() # drops cache so next read goes to DB
print(order.line_ids[0].partner_email) # fresh from DB
Use flush_all() and invalidate_all() sparingly — they cost performance — but they are the right tools when correctness matters.
For direct SQL writes, wrap them so cache invalidation always follows:
def _bulk_update_emails(self, partner_ids, new_email):
self.env.cr.execute("""
UPDATE res_partner SET email = %s WHERE id = ANY(%s)
""", (new_email, partner_ids))
# CRITICAL: invalidate the affected records' cache
self.env['res.partner'].browse(partner_ids).invalidate_recordset(['email'])
If you do not invalidate after raw SQL, every code path that already has those records cached will read stale values until the cursor closes.
How to Prevent It
- Avoid
relatedfor cross-module dependencies. Arelatedfield that crosses module boundaries is fragile. Use a stored compute with explicit@api.dependsinstead. - Never bypass the ORM unless you also invalidate. Make this a code-review rule: every
cr.execute("UPDATE")must be followed by an explicitinvalidate_recordset()orinvalidate_all(). - Lint for
@api.dependscompleteness. OCA'spylint-odoochecks that@api.dependslists match the fields actually accessed in the compute body. Wire it into CI. - Test cache invalidation explicitly. A unit test that writes a parent field and reads the related child field on the same cursor catches invalidation bugs cheaply. We ship one for every related field in ECOSIRE modules.
- Use
.flush_all()before tests assert. Vitest-style assertion right after a write can read cached pre-write values. Flushing first removes the false negatives. - Avoid long
relatedchains. Anything more than 2 hops (a_id.b_id.field) is asking for trouble. Flatten with a compute.
Related Errors
- Recursive dependency in computed field — what happens when
@api.dependsis wrong in a different way. - Required field not set — frequent symptom of cache returning empty stale values.
- KeyError: environments_cache after upgrade — process-level cache problem, sibling failure mode.
- Recordset has no attribute — when the cache returns an unexpected shape.
Frequently Asked Questions
What is the difference between invalidate_all() and invalidate_recordset()?
invalidate_all() drops the entire ORM cache for the current cursor — every record, every field. It is correct after operations that touched many models. invalidate_recordset(['field_a', 'field_b']) is surgical: only the listed fields on the current recordset get evicted. Prefer the surgical version unless you know you need the full clear.
Does flush_all() commit to the database?
No. flush_all() writes the in-memory pending changes to the cursor (so subsequent SQL queries see them), but the transaction itself only commits at the end of the request or when you call cr.commit() explicitly. Cache invalidation and transaction commit are different operations.
Can I disable the cache?
You can, with env['model'].with_context(prefetch_fields=False), but the performance cost is severe — every field access becomes a DB round-trip. The cache is correct in 99 percent of cases; chase the bug, not the architecture.
Why is my Odoo Studio related field stale?
Studio fields are stored as ordinary ir.model.fields rows but with the compute defined as a Python expression evaluated at runtime. They are particularly prone to invalidation gaps because Studio sometimes generates incomplete @api.depends. The fix is to convert the field to a hand-written compute in a custom module — Studio is fine for prototypes but should be rewritten as Python for production.
Does read_group see stale cache?
Yes, if a write happened earlier in the same transaction without a flush. read_group queries SQL directly, but Odoo writes are buffered in the cache and only flushed at commit. Call self.env.flush_all() before read_group to ensure the SQL sees the latest data.
Can I see what is in the cache for debugging?
Yes:
print(self.env.cache._data.keys()) # field tuples currently cached
Or for a specific field on a specific record:
print(self.env.cache.get(rec, rec._fields['partner_email']))
Useful when chasing "why is this value wrong" through several writes.
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
تحریر
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
Odoo ERP کے ساتھ اپنے کاروبار کو تبدیل کریں
آپ کے کاموں کو ہموار کرنے کے لیے ماہر Odoo کا نفاذ، حسب ضرورت، اور معاونت۔
متعلقہ مضامین
How to Add a Custom Button to an Odoo Form View (2026)
Add custom action buttons to Odoo 19 form views: Python action method, view inheritance, conditional visibility, confirmation dialogs. Production-tested.
How to Add a Custom Field in Odoo Without Studio (2026)
Add custom fields via custom module in Odoo 19: model inheritance, view extension, computed fields, store/non-store decisions. Code-first, version-controlled.
How to Add a Custom Report in Odoo Using External Layout
Build a branded PDF report in Odoo 19 using web.external_layout: QWeb template, paperformat, action binding. With print logo + footer overrides.