跳至主要内容

Odoo ValidationError:未设置必填字段 — 修复指南

命中验证错误:Odoo 17/18/19 中需要字段 X?诊断架构、默认值和 onchange 链,以使用代码示例永久修复它。

E
ECOSIRE Research and Development Team
|2026年5月4日6 分钟阅读1.2k 字数|

本文目前仅提供英文版本。翻译即将推出。

You save a record and Odoo blocks it with a red toast and this server log entry:

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:

  1. A required field was added to a model in a custom module without a default and without a database migration to backfill existing rows.
  2. An onchange method that used to set the field stopped firing — common when a parent field was renamed or a custom module overrides _onchange_partner_id and forgets to call super().
  3. A related field's source is empty. partner_invoice_id on sale.order is computed from partner_id. If partner_id is set to a partner without invoice_partner_id, the computation returns nothing.
  4. A compute field stopped recomputing because of a stale cache or wrong @api.depends.
  5. A studio/customization added required=True to an existing field without realizing legacy data has nulls.
  6. A web client form bypassed the server defaults — happens when third-party modules use JSON-RPC create directly 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=True as a contract change. Adding required=True to 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() in onchange/compute/create/write overrides. Make this a code-review rule. Most missing-default bugs trace back to a forgotten super-call.
  • Lint for @api.depends correctness. OCA's pylint-odoo ships a checker for this. Wire it into CI.
  • Server-side default fallback. For any required Many2one that depends on another field, always provide a default= callable as a belt-and-braces backup, not just an onchange.
  • 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.

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.

分享:
E

作者

ECOSIRE Research and Development Team

技术写作

ECOSIRE 技术写作团队的内容涵盖 Odoo ERP、Shopify 电子商务、AI 智能体、Power BI 分析、GoHighLevel 自动化以及企业软件最佳实践。我们的指南帮助企业做出明智的技术决策。

ECOSIRE

使用 Odoo ERP 转变您的业务

专家 Odoo 实施、定制和支持可简化您的运营。