Skip to content

OD206 unknown-env-model

env[...] names a model no indexed module defines.

Category Severity Series Needs the index
Correctness Warning all yes

What it reports

A literal model name in a subscript of env, such as self.env["x.y"] or request.env["x.y"], that no module on the addons path defines.

Why it matters

The environment looks the model up in the registry when the line runs and raises KeyError for a model that is not there. Nothing fails when the module installs: the error reaches whoever first triggers that code path, often a user clicking a button.

The rule stays silent where the code may know better. A name without a dot is skipped, since it is as likely a key of some other mapping called env. A model that a module missing from depends defines is not reported either, unlike OD202, because such a lookup is usually guarded by a check the rule cannot follow. A model from a module that is not on the addons path, such as an Enterprise module, is reported: add that module's directory to the addons path.

A model the module may build itself gets no finding. That is a name that fits an expression code outside tests/ builds _name from, where an expression that shows no literal start fits every name, as does a parent a class in a file that does not parse does not spell out, or the name a class may take in a module of unknown series, as Series describes.

Code that runs only once a check at runtime has shown a model, a field or a module there is skipped too, since a soft dependency is meant to be absent at times. A check asks whether the environment or the registry has the model, whether a model has the field, by name or through hasattr, or whether ir.module.module shows the module installed. Such code is one of these:

  • What the check opens: the body of if "x.y" in self.env: or of a test on self.env.get("x.y"), the else of if "x.y" not in self.env:, and the operands after such a test in an and.
  • The rest of a block that leaves when the model is absent: after if "x.y" not in self.env: return, or with self.skipTest(...) in place of return. So too after self.ensure_installed("m"), a method of AccountTestInvoicingCommon from 17.0 that skips the test unless the module is installed. The module name must be a literal, and the call covers every lookup behind it, whichever module it names.
  • What a try that looks up an env.ref(...) or an env[...], catches the failed lookup and skips the test from every handler guards: its body, its else and the rest of the block after it.

For an env.ref(), OD302 narrows a check that names a module or a model to the ids of the modules that come with what it names.

A value that holds the result of a check counts as the check: a name until it is assigned again, and an attribute of self or cls across the class.

Some tests prove nothing, so what follows them is reported. A test of a variable says nothing of a literal use: if name in self.env:. A private name is more often a method or an internal attribute than a field: hasattr(self, "_x"). A record of ir.module.module exists whether or not the module is installed, so only its state counts, and a search of modules counts when it asks for installed ones. A body that runs when the test fails is not behind it: the body of if "x.y" not in self.env:, or the rest of a block after if "x.y" in self.env: return.

A check is read where it is written, in the code around the lookup. One that a helper method or another class makes does not count, so to keep a lookup of a soft dependency quiet, test for the model, field or module in the function that needs it, or in the class that holds the result. ensure_installed is the one helper that counts, by its name. Elsewhere, use self.skipTest.

Example

models/sale_order.py
class SaleOrder(models.Model):
    _inherit = "sale.order"

    def _invoiced_lines(self):
        return self.env["account.move.lines"].search([("sale_line_ids", "in", self.order_line.ids)])

The model is account.move.line, so this method raises KeyError on its first call.

models/sale_order.py
class SaleOrder(models.Model):
    _inherit = "sale.order"

    def _invoiced_lines(self):
        return self.env["account.move.line"].search([("sale_line_ids", "in", self.order_line.ids)])

Suppressing

slots = self.env["x_delivery.slot"].search([])  # odooscan: ignore[OD206]

See suppressing a finding.