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¶
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.
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¶
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.