本文目前仅提供英文版本。翻译即将推出。
You save a record and Odoo blocks it with a red toast and this server log entry:
odoo.exceptions.ValidationError: The field 'partner_invoice_id' is required.
(Model: sale.order, Record: NewId_0x7f8a2c0)
Sometimes the field is one a user can see and fill in. More often it is an internal field that should have been auto-populated by an onchange, a default, or a related computation — and something broke that chain. This guide separates the user-facing case from the silent-data-pipeline case and gives a fix for each.
Quick Fix
If the field is visible on the form, the user just needs to fill it. If it is hidden or expected to auto-fill, run this in a shell to confirm the default is firing:
order = env['sale.order'].new({'partner_id': 7})
print(order.partner_invoice_id) # should not be empty
If it is empty, the onchange chain is broken. The fastest patch is to add an explicit default:
class SaleOrder(models.Model):
_inherit = 'sale.order'
partner_invoice_id = fields.Many2one(
'res.partner',
default=lambda self: self.env.user.partner_id.id,
)
That keeps the form usable while you investigate the real cause.
Why This Happens
ValidationError for a missing required field has six common roots in 17.0/18.0/19.0:
- A required field was added to a model in a custom module without a default and without a database migration to backfill existing rows.
- An
onchangemethod that used to set the field stopped firing — common when a parent field was renamed or a custom module overrides_onchange_partner_idand forgets to callsuper(). - A related field's source is empty.
partner_invoice_idonsale.orderis computed frompartner_id. Ifpartner_idis set to a partner withoutinvoice_partner_id, the computation returns nothing. - A
computefield stopped recomputing because of a stale cache or wrong@api.depends. - A studio/customization added
required=Trueto an existing field without realizing legacy data has nulls. - A web client form bypassed the server defaults — happens when third-party modules use JSON-RPC
createdirectly with partial payloads.
Step-by-Step Diagnosis
1. Read the exception carefully. It names the model and the field. Both matter.
2. Confirm the field really is required=True. In a shell:
field = env['sale.order']._fields['partner_invoice_id']
print(field.required, field.default, field.related, field.compute)
If required is True but default and compute are both None, the field has no auto-fill and any incomplete payload will fail.
3. Test the onchange chain in isolation.
form = Form(env['sale.order'])
form.partner_id = env['res.partner'].browse(7)
print(form.partner_invoice_id) # is it set?
If the form helper shows the field empty, your onchange is broken.
4. Check for migration backlog. If a field was made required recently and existing rows still have NULL:
SELECT count(*) FROM sale_order WHERE partner_invoice_id IS NULL;
Any non-zero result is a data-migration bug that will keep biting until backfilled.
5. Check @api.depends correctness. A common Odoo 18.0 mistake is depending on a @property instead of a real field — @api.depends only works with stored fields.
Permanent Fix
For each cause:
Cause 1 — New required field, no migration. Ship a migration script in migrations/<version>/post-migration.py:
def migrate(cr, version):
if not version:
return
cr.execute("""
UPDATE sale_order
SET partner_invoice_id = partner_id
WHERE partner_invoice_id IS NULL
""")
Always backfill before promoting a field to required.
Cause 2 — Broken onchange. Audit all overrides:
@api.onchange('partner_id')
def _onchange_partner_id(self):
res = super()._onchange_partner_id() # <-- DO NOT DROP THIS
# custom logic here
return res
Forgetting super() in an onchange override is the single most common source of "required field not set" in custom modules.
Cause 3 — Empty related source. Add a defensive default that falls back to the parent:
partner_invoice_id = fields.Many2one(
'res.partner',
compute='_compute_partner_invoice_id',
store=True,
readonly=False,
)
@api.depends('partner_id')
def _compute_partner_invoice_id(self):
for rec in self:
rec.partner_invoice_id = rec.partner_id.address_get(['invoice'])['invoice'] or rec.partner_id
address_get is the canonical Odoo way to resolve invoice/delivery contacts with a fallback.
Cause 4 — Stale compute cache. If you suspect cache, force recompute:
self.invalidate_recordset()
self._compute_partner_invoice_id()
But the durable fix is to correct @api.depends to list every triggering field.
Cause 5 — required=True added by Studio. Either backfill the data with a one-off SQL update (with backup first), or relax the field to required=False and validate at the workflow boundary instead.
Cause 6 — JSON-RPC payloads. Validate at the controller layer. Never trust external callers to populate every required field — apply defaults server-side before calling create().
How to Prevent It
- Treat
required=Trueas a contract change. Addingrequired=Trueto an existing field is a breaking change. Ship it with a migration that guarantees no NULLs and a default that prevents future NULLs. - Always call
super()inonchange/compute/create/writeoverrides. Make this a code-review rule. Most missing-default bugs trace back to a forgotten super-call. - Lint for
@api.dependscorrectness. OCA'spylint-odooships a checker for this. Wire it into CI. - Server-side default fallback. For any required
Many2onethat depends on another field, always provide adefault=callable as a belt-and-braces backup, not just anonchange. - Snapshot test the create path. A simple integration test that creates each major business record with the minimum payload catches every missing-default regression before deploy.
Related Errors
- Cannot delete record because of dependency — UserError, sibling exception class.
- Recursive dependency in computed field — what happens when
@api.dependsis wrong in a different way. - Stale related field — same family of "field looks empty but should not be" bugs.
- Data not found during init — when XML data installs in the wrong order.
Frequently Asked Questions
Should I add required=True to fix data quality, or rely on workflow checks?
Workflow checks. required=True is enforced on every write at every layer, including imports and migrations, which makes data fixes painful. Validate "must have an invoice address before confirming" at action_confirm, not at the field level. You keep the data discipline without the migration nightmare.
My field is compute=... and store=True. Why does Odoo say it is empty?
Either the compute did not run (check @api.depends), or it ran but assigned an empty value. Recordsets can be assigned False or an empty recordset, both of which read as falsy. Add a unit test that creates the record and asserts the computed value is non-empty.
What is the difference between default= and _compute_default_*()?
default= runs once at record creation and never again. compute= runs whenever a dependency changes. If the field should be set once and respected forever (like a sequence-based name), use default. If it should track another field, use compute with store=True.
Why does the same record save fine via XML import but fail in the UI?
XML imports run as superuser and skip many onchange chains, populating fields directly from the XML. UI saves go through the full onchange/default pipeline. If the UI fails, the XML is not actually testing your defaults — write a Python test instead.
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、操作绑定。带有印刷徽标+页脚覆盖。