This article is currently available in English only. Translation coming soon.
You upgrade Odoo. A user calls minutes later: their customized mail template, scheduled action, or sequence prefix is back to defaults. You did not change those records, but the upgrade quietly overwrote them. There is rarely an explicit error in the log — just:
INFO: loading sale_extension/data/mail_template.xml
INFO: created/updated 3 mail.template records
That terse line means a customer-edited record was reset. This is the noupdate flag working backwards from what most teams want. It applies to Odoo 17.0/18.0/19.0 and is one of the most expensive silent bugs you can ship.
Quick Fix
If the wipe just happened, restore the affected records from yesterday's backup using dblink or a side-by-side database, scoping the restore to only the affected ir.model.data rows:
-- Restore mail template body from yesterday's snapshot
UPDATE mail_template t
SET body_html = old.body_html,
subject = old.subject
FROM dblink('dbname=production_yesterday',
'SELECT id, body_html, subject FROM mail_template')
AS old(id integer, body_html text, subject text)
WHERE t.id = old.id
AND t.id IN (SELECT res_id FROM ir_model_data
WHERE module = 'mail' AND name LIKE 'mail_template_%');
For long-term prevention, mark all customer-edited template-style records with noupdate=true:
<data noupdate="1">
<record id="mail_template_sale_confirmation" model="mail.template">
...
</record>
</data>
Why This Happens
XML data files in Odoo modules can declare records inside <data> or <data noupdate="1">. The flag controls upgrade behaviour:
<data>(or<data noupdate="0">): on every--update, Odoo overwrites the record with the XML contents. Customer changes are lost.<data noupdate="1">: on every--update, Odoo creates the record if it does not exist but does not overwrite an existing one. Customer changes survive.
The four common scenarios:
- Module ships default mail templates inside
<data>(no noupdate). The customer edits the template through the UI. The next-u <module>overwrites the customizations. - Default sequences without
noupdateget reset on every update — customers who renumbered their sales orders see them flip back toSO/. - Cron schedules in
<data>get reset to the module default cadence, breaking customer-tuned schedules. base.partnerdemo records that were edited get overwritten on-u base.
The inverse problem also exists: noupdate="1" for records you actually do want to update. Bug fixes shipped in the XML are silently ignored. We covered that case in the related article on data not found during init.
Step-by-Step Diagnosis
1. Identify the wiped record. Ask the user what looked different. Log onto Odoo, find the record in the UI, click the bug-icon (Developer Mode) to see its XML ID and source module.
2. Find the source XML.
grep -rn "id=\"mail_template_sale_confirmation\"" /opt/odoo/addons/sale/
3. Check the surrounding <data> tag.
<data> <!-- BAD: noupdate not set, defaults to 0 -->
<record id="mail_template_sale_confirmation" ...>
If noupdate is missing or 0, this record will be overwritten on every update.
4. Check ir_model_data history:
SELECT id, module, name, model, res_id, noupdate, write_date
FROM ir_model_data
WHERE name = 'mail_template_sale_confirmation';
noupdate here is the database flag. Odoo respects this on subsequent updates. If false, the next update will overwrite again.
5. Confirm the wipe happened in the upgrade. Check the mail.template's write history if mail.thread is enabled, or check audit_log if installed. The write_date should match the upgrade time.
Permanent Fix
For customer-customizable records, mark them noupdate="1" in the source XML:
<data noupdate="1">
<record id="mail_template_sale_confirmation" model="mail.template">
<field name="name">Sales: Order Confirmation</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="subject">Your order {{ object.name }} is confirmed</field>
<field name="body_html">...</field>
</record>
</data>
Then patch the database flag for existing rows:
# migrations/<version>/post-migration.py
def migrate(cr, version):
cr.execute("""
UPDATE ir_model_data
SET noupdate = true
WHERE module = 'sale_extension'
AND name IN (
'mail_template_sale_confirmation',
'sequence_priority_quote',
'cron_priority_followup'
)
""")
After this migration, the flag is set on disk and in the database, and future updates will not overwrite.
For records that genuinely should follow the source (you ship a fix for a security-relevant template, for example), invert the noupdate flag in pre-migration once:
def migrate(cr, version):
cr.execute("""
UPDATE ir_model_data SET noupdate = false
WHERE module = 'mail' AND name = 'mail_template_password_reset'
""")
Run the update; the record refreshes from XML; then re-set noupdate to true so customer customizations to the new content are preserved.
For records that have BOTH module and customer ownership (the customer adds a footer to a default template), the cleanest pattern is a separate template override stored as a customer-specific record, not by editing the module-shipped one. Train customers and consultants on this pattern.
How to Prevent It
- Default to
noupdate="1"for all template-class data. Mail templates, email layouts, sequences, default cron schedules, default report headers — anything a customer might reasonably edit through the UI. The cost of the discipline is zero; the cost of skipping it is data loss. - Have a release checklist for
<data>files. Every XML file gets reviewed on every release to ensure the rightnoupdatevalue. Add it to your PR template. - Snapshot before every
-u. A pre-update backup gives you a clean restore target if you discover a wipe later. ECOSIRE's deploy pipeline does this automatically (Step 3,pre-deploy-backup.cjs). - Audit log on production. With audit_log installed, every overwrite is recorded. You can detect "the upgrade wiped customer template" from the log immediately, not three days later when a customer notices.
- Test the upgrade on a copy with customer customizations. Restore production into staging, run the upgrade, then check that the templates the customer cared about still look right. This catches noupdate issues before they hit users.
- Never edit module-shipped records as a customer. Train your team to create a new template that copies the default, then customize the new one. The default template stays untouched and upgrade-safe.
Related Errors
- Data not found during init — what happens when noupdate is wrong in the other direction.
- Model state corruption after upgrade — broader registry drift.
- Upgrade aborted, database corrupted — worst-case outcome.
- Cannot delete record because of dependency — adjacent data-management headache.
Frequently Asked Questions
Why does the noupdate flag default to false?
Historical reasons. Early Odoo modules expected XML to be authoritative for everything. Real customer deployments showed that 80 percent of <data> records should be customer-overridable, but the default was never flipped. So every module author has to remember.
Can I tell at a glance which records will be overwritten?
Run this query to find every customer-customizable-looking record without noupdate:
SELECT module, name, model
FROM ir_model_data
WHERE noupdate = false
AND model IN ('mail.template', 'ir.sequence', 'ir.cron',
'ir.actions.server', 'mail.activity.type')
ORDER BY module, model, name;
These are the rows at risk. Decide for each whether to flip the flag.
Does flipping noupdate to true freeze the record forever?
No. noupdate=true only stops XML imports from overwriting. The record can still be edited via UI, API, or migration scripts. It just is not overwritten on --update.
What if my customer wants the latest content from the module's XML?
They can manually delete the customized record (which removes ir.model.data for it as well), then -u <module> recreates it from XML. Or, write a one-off migration that bumps noupdate=false, runs the update, sets noupdate=true again. Used carefully, this gives you both worlds: customer protection by default, opt-in upgrades when needed.
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.