Skip to content

OD220 duplicate-field-label

Two fields of a model have the same label.

Category Severity Series Needs the index
Correctness Warning all yes

What it reports

A field whose label another field of the same model already has. The label is the literal string, by keyword or by position, or for a field that gives none and is not related, the one Odoo derives from its name: without a trailing _ids or _id, underscores made spaces, and each word capitalized, so partner_id and partner_ids are both Partner. The fields Odoo adds to a model count with their labels: ID and Display Name, Last Modified on (__last_update) until 16.0, and on a model that logs access, Created by, Created on, Last Updated by and Last Updated on. An abstract model has the first three until saas~14.3, so in 12.0 to 14.0, none of them from saas~14.4 to 18.0, so in 15.0 to 18.0, and ID and Display Name from saas~18.1, so from 19.0, where BaseModel defines them. A module of unknown series is checked as one of saas~14.4 to 18.0, without Last Modified on.

The finding is at the definition in the module that decides the label: the last one that sets string, or the field's first definition when the label is derived. A label is reported against one that another module decides, or one that a definition this module loads earlier decides, so each pair is reported once. When the two definitions are in different files of the module, the order of the files in the module picks the one reported, since the order of the imports is not indexed.

Why it matters

When Odoo records a model's fields in ir.model.fields, it logs a warning for each field whose label another field of the model already has. The label is what users see as the field's column, filter and export name, so two fields with one label cannot be told apart. Give one of them another string. Before saas~11.2, Odoo logs no such warning, and the rule stays silent on such a series. A module of unknown series is checked all the same, so one on such a series gets a false report: pass --odoo-version.

A label that is not a literal, such as _lt("Notes"), is unknown and never reported, nor is a related field without string, which takes the label of the field it relates to, nor a field the model gets through _inherits. The fields that log access are counted unless the model is abstract, or a class of it sets _auto or _log_access to anything but True.

What a class that redefines one of the fields Odoo adds makes of its label depends on the series. From saas~14.4, so from 15.0, Odoo adds the field to the class that introduces the model with _name, which merges after the models it inherits, so a string that a mixin sets gives way to the label Odoo gives the field, which stands unless a later class of the model sets string. Odoo does not add a field of a name the introducing class defines, so the definitions decide the label, a mixin's string included when that class sets none. Two fields are exceptions: in 15.0 to 18.0 Odoo adds id even over the introducing class's definition, and from saas~18.1, so from 19.0, BaseModel defines id and display_name before every class. The field Odoo adds as id sets no string, so for id, and for display_name from 19.0, the string of any definition stands, a mixin's included. For any other field, when a class of the model lists another model before the model's own in _inherit, that model merges after the field Odoo adds, so the label of a field that a model the model inherits defines is unknown and never reported.

Until saas~14.3, so in 12.0 to 14.0, Odoo adds no field that a class of the model or of a model it inherits defines, so the definitions decide the label, except for id, which keeps its label whatever a class sets. Until saas~12.4, so in 12.0, a definition without string also takes the label of the field Odoo added to a model the model inherits: Display Name always, and the label of a field that logs access when that model logs access; when a model the model inherits defines such a field too, its label is unknown and never reported. Last Modified on always stands: Odoo adds __last_update whatever a class defines, and Python renames the name in a class body (to _, the class name without its leading underscores, then the name), so no class redefines it.

The rule does not run for a module when the module or one of its dependencies depends on a module that is not indexed. A model is skipped when the class that introduces it, with _name or as a class Odoo names, is in a module outside the dependencies, or when a class of it, or of a model it inherits or delegates to, has a plain Python base class or an _inherit element that is not a literal, a parent whose fields the scan cannot see. A module that Odoo always installs with base, such as web, counts as in depends.

Example

models/parcel_bag.py
class ParcelBag(models.Model):
    _name = "parcel.bag"
    _description = "Parcel bag"

    partner_id = fields.Many2one("res.partner")
    partner_ids = fields.Many2many("res.partner")
models/parcel_bag.py
class ParcelBag(models.Model):
    _name = "parcel.bag"
    _description = "Parcel bag"

    partner_id = fields.Many2one("res.partner")
    partner_ids = fields.Many2many("res.partner", string="Followers")

Suppressing

partner_ids = fields.Many2many("res.partner")  # odooscan: ignore[OD220]

See suppressing a finding.