本文目前仅提供英文版本。翻译即将推出。
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.
相关文章
如何将自定义按钮添加到 Odoo 表单视图 (2026)
将自定义操作按钮添加到 Odoo 19 表单视图:Python 操作方法、视图继承、条件可见性、确认对话框。经过生产测试。
如何在没有 Studio 的情况下在 Odoo 中添加自定义字段 (2026)
通过 Odoo 19 中的自定义模块添加自定义字段:模型继承、视图扩展、计算字段、存储/非存储决策。代码优先,版本控制。
如何使用外部布局在 Odoo 中添加自定义报告
使用 web.external_layout 在 Odoo 19 中构建品牌 PDF 报告:QWeb 模板、paperformat、操作绑定。带有印刷徽标+页脚覆盖。