Skip to main content

Odoo ValidationError: Required Field Not Set — Fix Guide

Hit ValidationError: The field X is required in Odoo 17/18/19? Diagnose schema, defaults, and onchange chain to fix it permanently with code examples.

E
ECOSIRE Research and Development Team
|May 4, 20266 min read1.2k Words|

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

Written by

ECOSIRE Team

Technical 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

Transform Your Business with Odoo ERP

Expert Odoo implementation, customization, and support to streamline your operations.