本文目前仅提供英文版本。翻译即将推出。
After an Odoo major version upgrade, a database starts behaving strangely. Fields you do not recognize appear on models. Old fields are missing from the form. Some users see the new layout, others see the old. The server log shows a parade of:
WARNING: Field 'x_studio_old_field' on model 'sale.order' not found, ignored
ERROR: View 'sale.order.form.inherit.foo' has invalid arch (column does not exist)
This is registry-versus-database state drift after a partially-failed upgrade. The data is intact but Odoo's metadata about which models, fields, and views exist has gone out of sync. This guide walks through a controlled recovery for Odoo 17.0/18.0/19.0.
Quick Fix
The fastest sanity-restorer is -u all against the upgraded database with a clean worker pool:
sudo systemctl stop odoo
sudo find /opt/odoo -name "__pycache__" -type d -exec rm -rf {} +
sudo -u odoo /opt/odoo/odoo-bin -c /etc/odoo/odoo.conf \
-d <db> -u all --stop-after-init --no-http
sudo systemctl start odoo
If this clears the warnings, you were lucky — only the bytecode and registry caches were stale. If the warnings persist, you have real metadata drift and need the full recovery below.
Why This Happens
Odoo's metadata lives in three places: Python source code (the actual classes), the ir_model / ir_model_fields / ir_ui_view tables (registered metadata), and the running registry (in-process Python). After an upgrade, all three should agree. Drift happens when:
- The upgrade aborted partway through
-u all— half the modules upgraded, half did not, and the registry is mixed. - A custom module's manifest version was not bumped, so Odoo skipped its migration scripts even though the code changed.
- Studio fields added in the previous version are not in the new version's source code — they linger as ghost fields in
ir_model_fields. - A module was renamed (or a model was renamed) without an
openupgrademigration script. The DB has the old name, the source has the new. - Inherited views (
ir.ui.view) reference removed fields. The view loads, fails arch validation, gets disabled, and the form looks broken.
Step-by-Step Diagnosis
1. Confirm no module is in transitional state.
SELECT name, state FROM ir_module_module
WHERE state NOT IN ('installed', 'uninstalled', 'uninstallable');
Any rows here mean the upgrade did not finish. Re-run with -u all.
2. Find ghost fields. Fields that exist in the database but not in the source:
SELECT m.model, f.name
FROM ir_model_fields f
JOIN ir_model m ON m.id = f.model_id
WHERE f.state = 'manual' -- studio/manual fields
AND f.modules IS NULL; -- not declared by any module
Ghost fields are usually safe to drop, but back up first.
3. Find broken inherited views.
SELECT id, name, model, arch_db
FROM ir_ui_view
WHERE active = false AND inherit_id IS NOT NULL
ORDER BY write_date DESC LIMIT 20;
Recently-disabled inherited views are your top candidates.
4. Compare expected vs actual fields per model. In an Odoo shell:
expected = set(env['sale.order']._fields.keys())
actual = set(r['name'] for r in env['ir.model.fields']
.search_read([('model', '=', 'sale.order')], ['name']))
print('In DB but not source:', actual - expected)
print('In source but not DB:', expected - actual)
Either set being non-empty is a sign of drift.
5. Check ir_model_data for orphans.
SELECT module, name, model
FROM ir_model_data
WHERE module NOT IN (SELECT name FROM ir_module_module WHERE state = 'installed');
Orphan ir.model.data references break dependency resolution.
Permanent Fix
Step 1 — Snapshot the database. Always. pg_dump <db> > pre-fix.sql.
Step 2 — Drop ghost fields:
ghost_fields = env['ir.model.fields'].search([
('state', '=', 'manual'),
('modules', '=', False),
])
for f in ghost_fields:
_logger.warning("Dropping ghost field %s.%s", f.model, f.name)
f.unlink()
unlink() on ir.model.fields properly drops the column and all dependent metadata.
Step 3 — Re-enable broken inherited views with their views fixed. For each disabled inherit:
view = env['ir.ui.view'].browse(view_id)
# Inspect view.arch_db, fix the field reference, then:
view.write({'arch': fixed_arch, 'active': True})
If a view is no longer needed, archive it explicitly rather than letting it sit broken.
Step 4 — Run -u all once more with the cleaned metadata:
sudo -u odoo /opt/odoo/odoo-bin -c odoo.conf -d <db> -u all --stop-after-init
Step 5 — Verify with the OCA module checker:
pip install click-odoo-contrib
click-odoo-update -d <db>
It walks the registry and reports drifts that survived the upgrade.
For complex cases, the OCA openupgrade framework includes scripts that automate field renames, model renames, and module merges across major version bumps. ECOSIRE's Odoo migration team uses openupgrade for every version migration — manually replicating its checks on production is rarely worth it.
How to Prevent It
- Never run
-u allon production without a backup. And never run it when modules are in a transitional state. Snapshot, then upgrade, then verify. - Bump custom module versions on every release. A version bump triggers Odoo's migration runner. Without it, your
migrations/directory is silently ignored. - Inventory Studio fields before upgrading. Studio adds
manualfields that are not in any module's source. List them, decide which to keep as Python code, which to delete. - Archive instead of delete inheritance. Never
unlinkanir.ui.viewdirectly — Odoo may reactivate or reference it during upgrade. Archive (active=False) and review. - Use openupgrade. It exists for exactly this reason. The community has written hundreds of migration helpers; your job is to assemble them, not rewrite them.
- Practice on staging. Restore production into staging, run the upgrade end-to-end, run a smoke test, fix every warning before touching production. The "first attempt cost" goes down to almost zero on the second try.
Related Errors
- Upgrade aborted, database corrupted — the worse cousin of this bug.
- KeyError: environments_cache after upgrade — registry / cache reset.
- Module installation error: version mismatch — a frequent trigger of partial upgrades.
- Relational fields broken after upgrade — symptom that lives in the same family.
Frequently Asked Questions
Is dropping ghost fields safe?
Generally yes for state='manual' fields with no modules declaration — they are leftovers from Studio customizations or older module versions. Always snapshot the database first; once dropped, the column is gone and the data with it. If a Studio customization was important, export it before dropping.
Why does Odoo keep loading the broken view?
Odoo loads inherited views in module-dependency order at every server start. A broken view fails arch validation, gets active=False, and is logged but not deleted. On the next start the same thing happens. The loop only ends when you fix or archive the view.
My drift is in ir_model_data (XML IDs). What do I do?
Use the --xmlid flag with click-odoo-update, or write a one-off cleanup that nulls the orphan rows. Orphan XML IDs do not crash anything but they break the next upgrade — clean them as part of the post-upgrade pass.
Can I just restore from backup and try again?
Yes, and often this is the right call if you are within the maintenance window. Restore, fix the manifest version bumps and ghost-field cleanup before running -u all, then upgrade against the cleaned snapshot. Two passes on staging beats fixing live.
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、操作绑定。带有印刷徽标+页脚覆盖。