Skip to content

OD321 wrong-model-reference

A data record gives a relational field a record of another model.

Category Severity Series Needs the index
Correctness Error all yes

What it reports

An XML id written to a Many2one, One2many or Many2many field of a <record> or a CSV row, when every record of that id has a model other than the field's comodel. The id reads from:

  • ref="...", and a whole eval="ref('...')", with or without a second argument.
  • A list of x2many commands: (1, ref, ...) to (4, ref), (6, 0, [ref, ...]), and Command.link, unlink, update, delete and set. (0, ...), (5, ...), Command.create and Command.clear name no record and do not hide the others.
  • A CSV column field:id or field/id, each id of a cell split at commas.

The target may be any record the index knows: a <record>, a <menuitem>, a <template>, a generated model_x or field_x__y (field_<table>_y before saas~11.2), the ir.module.module record of a module, or the parent record Odoo creates for an _inherits child. It may be defined later or in another module. A field of an _inherits parent counts as a field of the child, unless the child has its own.

Why it matters

In XML, Odoo turns the id into a database id and writes that number without looking at the model, so the file may load. The number then points at whichever record of the comodel has it. With one, the file loads and links an unrelated record. With none, a write of the number stops the install: on the foreign key of a Many2one or Many2many, and on the missing line of a One2many link. An unlink command with no such record does nothing, and a ref on an x2many stops the load either way, since Odoo expects commands there. Some fields have no foreign key and can keep a number no record holds: a Many2one to ir.actions.actions or to a model with _auto = False, a company-dependent Many2one, and a Many2many to a model backed by a SQL view. Below 18.0 a company-dependent Many2one has no column either: Odoo writes the number to ir.property under the comodel's name, and a read looks it up on the comodel. From 20.0 a write checks the ids it adds to a Many2many or One2many against a domain= that is not a string, and a number the domain rejects stops the install with Cannot link inaccessible records. On a Many2many, a number no record holds stops it with MissingError as soon as the domain reads a field other than id, before any foreign key and even with none. Replacing the lines of a One2many on a record that exists goes unchecked. Below 20.0 no domain is checked, so a Many2many to a SQL view keeps the number and a record outside the domain is linked.

A CSV row stores the wrong id the same way below 14.0. From 14.0 the import checks the id against the comodel. A Many2one or Many2many column fails with Invalid external ID ... expected model when the comodel has a record of that number and No matching record found when it has none, and Module loading ... failed stops the install. A demo file differs, here and above: see load failures. A One2many column swallows that error and creates a line instead of linking the target.

Giving the _inherits child where the field takes the parent is the usual form of this mistake: base.user_admin is a res.users, and on a res.partner field it writes the user's id, not the partner's. Use the parent's id, such as base.partner_admin.

The rule stays silent where a run decides, or where the id may be right:

  • Reference, Many2oneReference and Integer fields, which are not relational.
  • A related field without a comodel, a field that is not stored, a comodel given as an expression, and a field that unpacks **kwargs. A company-dependent field counts as stored, since its value is written as a column's would be, unless it declares store=False, or a compute or related without store=True.
  • A target whose model shares the comodel's _table, or inherits the comodel by _inherit. Among _inherit descendants, only the ir.actions.* models share ids, as ir.actions.server does with ir.actions.actions, since their tables are INHERITS (ir_actions). A model with _name, _inherit and a table of its own shares none, so a wrong id there goes unreported.
  • A target's model or a comodel whose _table only a run tells, so the two may share a table: a value that is not a literal, classes of the model that set different ones, or a plain Python base of a comodel class, which may set it.
  • An id the index does not know, an id with one definition of an unknown model (_update_xmlids, a <template> or <asset> of a theme_ module), and an id with one definition that fits.
  • A search, a type, a file, a name@lang field, a bigger eval, a bare [ref(...)], obj(), and the CSV columns a/b:id and .id. Only the last <field> of a name counts.
  • The inverse field of a record nested in a one2many, which Odoo sets itself, and a record nested in any other type of field, which it never creates.
  • A value a later write replaces in the module's scope. A value only a module outside the scope replaces still reports, since a database without that module keeps it, and so does a data value only a demo write replaces, since a database may hold no demo data.
  • A file the manifest does not load.
  • A module with a dependency that is not indexed, a module that builds model names at runtime, and a record's or target's model with a parent the scan cannot see.
  • A module with an unread data file, which may replace the value. A module that depends on one still reports, as that file has loaded by then.

Example

data/res_partner.xml
<odoo>
    <record id="partner_shop" model="res.partner">
        <field name="name">Shop</field>
        <field name="parent_id" ref="base.main_company"/>
    </record>
</odoo>

base.main_company is a res.company, and parent_id takes a res.partner.

data/res_partner.xml
<odoo>
    <record id="partner_shop" model="res.partner">
        <field name="name">Shop</field>
        <field name="parent_id" ref="base.main_partner"/>
    </record>
</odoo>

base.main_partner is the partner of that company.

Suppressing

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

See suppressing a finding.