Skip to content

OD324 missing-required-field

A data record is created without a required field that has no default.

Category Severity Series Needs the index
Correctness Warning all yes

What it reports

A <record> or a CSV row that Odoo creates, leaving out a stored field with required=True and no default in any class of the model, its _inherit parents and mixins included. A record creates when it has no id, or when its id is the module's own and nothing before it defines that id.

When a record leaves the field of an _inherits parent empty, Odoo creates that parent, so a required field of the parent counts too, unless the record names the parent through the delegating field.

The rule reads a demo file like any other. A failure there costs the module its demo data from saas~11.5, and stops the install before: see load failures. Writes in later files fill nothing: Odoo creates the record at its first definition.

Why it matters

The value reaches the INSERT as NULL, and on a NOT NULL column PostgreSQL stops the load of the file with null value in column ... violates not-null constraint. The field is usually one added to the model after the data was written.

The severity is a Warning because this is a medium confidence finding: a pre_init_hook, an init() method or an SQL file can fill the column in ways the scan does not read. When an _inherit adds such a field to a table holding rows, they stay NULL, SET NOT NULL fails, Odoo only logs it, and the INSERT stores NULL.

The rule stays silent when something may give the value:

  • A default on any definition of the field, a compute or a related, whether stored or not.
  • A default on an _inherits child's own definition of a parent field, with inherited=True. A child field without inherited only shadows the parent's and gives it nothing.
  • A default_<field> key in the context of the record or of an <odoo> or <data> around it.
  • A literal <function model="ir.default" name="set"> in a module in scope. One in a demo file only counts for records in demo files, since it loads after the data.
  • default_get, create, load, _add_missing_default_values, _prepare_create_values, _load_records or _load_records_create on the model, or on a parent class whose model has the field. Which keys they set is not read.
  • The inverse field of a record nested in a one2many.
  • sequence and xmlid, which the loader may fill itself.
  • _defaults through 9.0. A method called _default_x means nothing without default=.

It stays silent in these cases, where the record creates nothing or the column has no constraint:

  • An id prefixed with another module, a later definition, an id a module in scope wrote first, and ids Odoo generates: model_*, field_* and the ids of _inherits parents of records Odoo loads. A demo write counts as written first only for a record in a demo file, as a database may hold no demo data.
  • Any id of base, which base_data.sql creates before the data, and a CSV row with a .id.
  • An id'd record after a <function> of the module, which may have created it.
  • A record in a module with an unread data file, or that depends on one, since that file may have created it.
  • forcecreate false, and a record nested in a field other than a one2many.
  • A One2many, a Many2many, a Binary or an Image field, a company-dependent field, a field that is not stored, and a model with _auto = False.
  • A delegating field left empty, since Odoo fills it with the parent it creates.
  • A file the manifest does not load.
  • A field only a module outside the scope adds, a module with a dependency that is not indexed, a module that builds model names at runtime, and a model with a parent the scan cannot see.

Example

models/tier.py
class Tier(models.Model):
    _name = "x.tier"
    _description = "Tier"

    name = fields.Char(required=True)
    code = fields.Char(required=True)
data/tiers.xml
<odoo>
    <record id="tier_gold" model="x.tier">
        <field name="name">Gold</field>
    </record>
</odoo>

Nothing gives code, so the install stops on the INSERT.

data/tiers.xml
<odoo>
    <record id="tier_gold" model="x.tier">
        <field name="name">Gold</field>
        <field name="code">gold</field>
    </record>
</odoo>

Suppressing

<!-- odooscan: ignore[OD324] -->

See suppressing a finding.