Cet article est actuellement disponible en anglais uniquement. Traduction à venir.
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.
Rédigé par
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.
ECOSIRE
Transformez votre entreprise avec Odoo ERP
Implémentation, personnalisation et assistance expertes d'Odoo pour rationaliser vos opérations.
Articles connexes
Comment ajouter un bouton personnalisé à une vue de formulaire Odoo (2026)
Ajoutez des boutons d'action personnalisés aux vues de formulaire Odoo 19 : méthode d'action Python, héritage des vues, visibilité conditionnelle, boîtes de dialogue de confirmation. Testé en production.
Comment ajouter un champ personnalisé dans Odoo sans Studio (2026)
Ajoutez des champs personnalisés via le module personnalisé dans Odoo 19 : héritage de modèle, extension de vue, champs calculés, décisions magasin/non-magasin. Code d'abord, contrôle de version.
Comment ajouter un rapport personnalisé dans Odoo à l'aide d'une mise en page externe
Créez un rapport PDF de marque dans Odoo 19 à l'aide de web.external_layout : modèle QWeb, format papier, liaison d'action. Avec logo imprimé + remplacements de pied de page.