Skip to content

OD320 unknown-server-code-method

The code of a server action or a cron calls a method its model does not have.

Category Severity Series Needs the index
Correctness Error all yes

What it reports

A call model.x(), records.x() or record.x() in the code of an ir.actions.server or ir.cron record, where neither the classes of its model_id, those of the models it inherits, nor BaseModel define x, or where only a module missing from depends adds it. Private methods are fine: this is Python, not a button. OD312 says what defines a method.

env holds a callable Environment on every series, so calling it is fine. pool holds a callable Registry through saas~18.4, so model.pool('x') reports from 19.0, where it raises a TypeError, and not in a module of unknown series. Write model.pool['x'].

ir.cron has code only from 11.0: below it Odoo drops the field, so the code never runs, and the rule stays silent, as OD319 does. The record must run code: state is code, a cron leaves it unset, or a server action created without it on 9.0 or 10.0, where it defaults to code. A write to another module's action creates nothing, and a server action without state in a module of unknown series counts as not running code. From 11.0 through saas~18.2 the default is object_write. From saas~18.3 state is required with no default, so such a record fails to load unless a default_state in a context supplies it. The rule reads no default_state on any series. model_id reads as a ref, an eval="ref('...')", or a search on one model term. When several records of the load write the same XML id, each field takes the last value the module's scope gave it, and only the last code is checked. A demo write does not count for a data value, as a database may hold no demo data.

From saas~18.2 Odoo creates a child action on the model of its parent, whatever the child gives model_id, so the rule reads the code against the parent's model. A child linked to its parent after its creation keeps its own model, and so does every child before saas~18.2. A later write of model_id wins in either case. Give a child the model of its parent: a module update writes the child's own value back, which the rule ignores. It stays silent when it cannot tell the parent's model, and in an unknown series when the two models differ.

Why it matters

From 10.0 Odoo sets model, record and records from model_id. 9.0 sets self, model, obj and object, and no record or records. 10.0 sets all five, and obj and object end there. From 10.0 Odoo compiles the code without looking the method up, and 9.0 does not compile it on load, so the module installs. The AttributeError comes when the action runs, or from 11.0 the cron, wrapped in a ValueError by safe_eval: a button that fails for a user, or a cron that fails at night and logs it.

The rule follows model, record and records on every series, and never obj or object. On 9.0 a record or records the code does not bind raises a NameError, wrapped the same way, when the action runs, a stateless one included, whatever the method. The rule reports it only under the conditions above. It stays silent on a name the code binds anywhere, by an assignment, a loop, a parameter, a comprehension, |= or del, in any order. It also skips env[...], sudo() chains and other receivers that are not one of the three names, and the body of a try, whose handler may expect the failure. A <function> that writes the record goes unseen.

It stays silent when it cannot see every class, as OD314 does: a module with a dependency that is not indexed, a module that builds model names at runtime, a model that is unknown or outside depends, and a class of the model, or of a model it inherits, with a plain Python base or an _inherit no indexed module defines, or in a file that does not parse, which counts as one the scan cannot read. Such a class of a model reached through _inherits or delegate=True does not count, since those methods stay on that model.

Example

data/server_actions.xml
<odoo>
    <record id="action_confirm_orders" model="ir.actions.server">
        <field name="name">Confirm orders</field>
        <field name="model_id" ref="sale.model_sale_order"/>
        <field name="state">code</field>
        <field name="code">records.action_cofnirm()</field>
    </record>
</odoo>

sale.order has no action_cofnirm, so the action fails each time it runs.

data/server_actions.xml
<odoo>
    <record id="action_confirm_orders" model="ir.actions.server">
        <field name="name">Confirm orders</field>
        <field name="model_id" ref="sale.model_sale_order"/>
        <field name="state">code</field>
        <field name="code">records.action_confirm()</field>
    </record>
</odoo>

The name now matches a method sale.order has on every series. records holds the selected orders; model is an empty recordset, so model.action_confirm() would confirm nothing.

Both examples need 10.0 or later. On 9.0 records is unbound, so it fails before the method matters: call obj.action_confirm(), which acts on the active order, and which the rule does not read.

Suppressing

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

An XML comment inside the field ends the text Odoo reads, so inside the code use a Python comment, # odooscan: ignore[OD320].

See suppressing a finding.