Skip to content

OD225 sql-view-missing-column

The SQL view of a model selects no column for one of its stored fields.

Category Severity Series Needs the index
Correctness Error all yes

What it reports

A stored field of a model with _auto = False whose init() creates its SQL view, when the view selects no column of the field's name, and a view that selects no id. The field is reported at its first definition, once, whichever class or module adds it. The missing id, and a field that a dependency of the module whose init() creates the view defines first, are reported at the query, in that module.

A field is checked when Odoo merges it stored in the module and its depends: not related or computed without store=True. A module that Odoo always installs with base, such as web, counts as in depends. One2many and Many2many fields, which have no column, Binary and Image fields, whose column depends on attachment, and company-dependent fields, whose column depends on the series, are not checked.

The view is read only when its whole select list is literal. init() must pass a literal CREATE [OR REPLACE] [MATERIALIZED] VIEW to execute() or SQL(), as "..." % values or "...".format() too, directly or through a name it binds once, and name the view after the model's table, with a public. schema or none, or by a placeholder such as %s. The names are those of the select list of the first branch of its query, past a WITH clause and the brackets around it: an alias after AS or bare, or the column an item reads, alone, qualified or cast, as l.order_id and l.date::date do. A column list, as in CREATE VIEW v (id, total) AS ..., names the first items in its place. A list with *, t.* or a placeholder, or with an expression the column list does not name and that has no alias, is not read.

The rule skips a model when the classes of it and of the models it inherits, in any indexed module, define init() more than once, only in a class of another model or only in a module outside the dependencies, or define _auto_init(); when a class of the model sets _table_query or _table_sql, or is abstract; when init() creates no view or several, or one under another name; when the model has a plain Python base class or an _inherit element that is not a literal; and when the module or one of its dependencies depends on a module that is not indexed.

Why it matters

A model with _auto = False reads its records from whatever relation bears its table name, and Odoo selects a column for every stored field it fetches: a read prefetches all of them, so a field the view lacks makes every read of the model fail, with an error about the missing column. A view without id fails the same way, since every record is fetched by its id. A field added by another module to a reporting model needs the view rebuilt with that column too.

Example

models/sale_line_digest.py
class SaleLineDigest(models.Model):
    _name = "x.sale.line.digest"
    _description = "Sale line digest"
    _auto = False

    order_id = fields.Many2one("sale.order", readonly=True)
    product_id = fields.Many2one("product.product", readonly=True)
    currency_id = fields.Many2one("res.currency", readonly=True)
    amount = fields.Monetary(readonly=True)

    def init(self):
        tools.drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute("""
            CREATE OR REPLACE VIEW x_sale_line_digest AS (
                SELECT l.id, l.order_id, l.product_id, l.price_subtotal AS amount
                  FROM sale_order_line l
            )
        """)

In a module that depends on sale, currency_id is reported: the view has no such column, so opening the report fails.

models/sale_line_digest.py
class SaleLineDigest(models.Model):
    _name = "x.sale.line.digest"
    _description = "Sale line digest"
    _auto = False

    order_id = fields.Many2one("sale.order", readonly=True)
    product_id = fields.Many2one("product.product", readonly=True)
    currency_id = fields.Many2one("res.currency", readonly=True)
    amount = fields.Monetary(readonly=True)

    def init(self):
        tools.drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute("""
            CREATE OR REPLACE VIEW x_sale_line_digest AS (
                SELECT l.id, l.order_id, l.product_id, l.currency_id, l.price_subtotal AS amount
                  FROM sale_order_line l
            )
        """)

Suppressing

    currency_id = fields.Many2one("res.currency", readonly=True)  # odooscan: ignore[OD225]

A field that a dependency defines first is reported at the query, as above, because the dependency's own scan does not see this module's init() and the field's file belongs to another module. Fix it in this view: select a column of the field's name, or a typed NULL such as NULL::float AS weight_total when the view has no value for it. A missing id needs a real unique column, never NULL.

To suppress it, put # odooscan: ignore[OD225] alone on the line above the one where the literal starts, or at the end of that line when the string closes on it. Either comment hides every OD225 finding at that query.

See suppressing a finding.