Rules¶
Every rule has a code of the form OD and three digits, and a name. --select, --ignore, the
configuration file and suppression comments take the code, or for the first three a prefix of it
such as OD3.
The hundreds digit says what the rule reads. A code is never renumbered and never reused for
another rule: once released, OD203 means the same check in every later version, so baselines,
suppressions and links keep working. A retired rule leaves a gap.
A rule marked as needing the index checks one module against the others: the models, fields,
methods, XML ids and views of every module it depends on. It is off unless base is among the
indexed modules, and a notice says so. The other rules read one file, or one module, at a time.
The series of a rule is the range of Odoo versions it applies to. Each module is checked against the series its manifest version names, and a module whose series is unknown gets only the rules that apply to every series.
Load failures¶
Rule pages say a failure stops the install: that holds for a file of data. From saas~11.5 a
failure while loading a file of demo stops neither the install nor the update: Odoo logs a
warning and installs the module without its demo data. That covers the load alone: an update
validates a view of that file again after it, so a view that no longer validates stops the
update. Before saas~11.5 any failure in a demo file stops them like a data file.
OD0: parsing¶
A file that does not parse. Nothing else is checked in it.
| Code | Name | Reports |
|---|---|---|
| OD000 | syntax-error |
The file does not parse. |
OD1: Python, one file at a time¶
Python code checked on its own: security, deprecated APIs and ORM mistakes visible in one file.
| Code | Name | Reports |
|---|---|---|
| OD101 | sql-injection |
cr.execute() receives a query built by string formatting. |
| OD102 | translation-interpolated |
_() receives an already formatted string, so the source text cannot be translated. |
| OD103 | cursor-commit |
Business code commits the cursor, breaking the request's transaction. |
| OD104 | default-evaluated-at-import |
A field default is called once at import instead of on each record. |
| OD105 | missing-super |
An ORM override never calls super(). |
| OD106 | name-get-deprecated |
name_get() is ignored; override_compute_display_name(). |
| OD107 | api-multi-one-removed |
@api.multi and @api.one no longer exist. |
| OD108 | group-operator-renamed |
Field attribute group_operator is renamed aggregator. |
| OD109 | private-env-shortcut |
self._cr, self._uid and self._context are deprecated; use self.env. |
| OD110 | sql-constraints-unsupported |
_sql_constraints is ignored; declare models.Constraint attributes. |
| OD111 | public-route-sudo |
A public or auth='none' route escalates with sudo(). |
| OD112 | csrf-disabled |
An HTTP route disables CSRF protection. |
| OD113 | fields-view-get-removed |
fields_view_get() is removed; override _get_view() or get_views(). |
| OD114 | depends-on-id |
@api.depends names the field id, which Odoo refuses. |
| OD115 | dotted-onchange-constrains |
@api.onchange or @api.constrains names a dotted path, which Odoo ignores. |
| OD116 | field-shadows-model-member |
A field is named like a method or an attribute every model has. |
| OD117 | create-not-batched |
create() is decorated with @api.model, so Odoo calls it once per record. |
| OD118 | create-reads-dict |
create() under @api.model reads its argument as one dict, but receives a list of them. |
| OD119 | override-drops-result |
A create(), write() or copy() override calls super() but returns nothing. |
| OD120 | ormcache-unknown-name |
An ormcache key names something that is not a parameter of the method. |
| OD121 | cursor-rollback |
Business code rolls back the cursor, discarding the caller's work with its own. |
| OD122 | sql-write-without-invalidation |
An SQL write runs without invalidating the cache, so the ORM keeps stale values. |
| OD123 | patch-at-import |
Code run at import patches Odoo, for every database the server runs. |
| OD124 | module-level-cache |
A module-level dict, list or set filled at runtime is shared by every database. |
| OD125 | python-api-deprecated |
The module uses a Python API its series deprecates or ignores. |
| OD126 | python-api-removed |
The module uses a Python API its series removed. |
OD2: models and fields¶
Models, fields and methods checked against every module in the dependency graph. A model class has
Model, TransientModel, AbstractModel, their aliases osv, osv_memory and osv_abstract or,
from 20.0, CachedModel among its bases, and any other base is a plain Python base class, which the
rules whose page says so skip, since only Python knows its fields and methods. As each page says,
they also skip a model when a class of it, or of a model it inherits or, for a rule reading fields,
delegates to, has an _inherit element that is not a literal, such as a variable, since the scan
cannot tell which model it names.
| Code | Name | Reports |
|---|---|---|
| OD201 | unknown-model |
The model is not defined by any indexed module. |
| OD202 | model-not-in-depends |
The model comes from a module missing from depends. |
| OD203 | unresolved-field-path |
A field path does not resolve on its model. |
| OD204 | missing-method |
compute, inverse, search, selection or group_expand names a method the model does not have. |
| OD205 | field-type-changed |
An inherited field is redefined with another type. |
| OD206 | unknown-env-model |
env[...] names a model no indexed module defines. |
| OD207 | related-type-mismatch |
A related field has another type than the field it relates to. |
| OD208 | invalid-one2many-inverse |
A One2many's inverse field is not a Many2one. |
| OD209 | model-redefined |
A class redefines a model that a module in depends already defines. |
| OD210 | unknown-domain-field |
A literal domain or group_by names a field its model does not have. |
| OD211 | orphan-super |
super() calls a method no parent class defines. |
| OD212 | monetary-without-currency |
A Monetary field's currency field is not a field of its model. |
| OD213 | required-many2one-set-null |
A required Many2one declares ondelete='set null'. |
| OD214 | many2many-table-clash |
Two Many2many fields of a model to one comodel get the same table. |
| OD215 | many2many-table-too-long |
A Many2many's relation table name is longer than PostgreSQL allows. |
| OD216 | selection-add-without-ondelete |
selection_add leaves a value of a required Selection with ondelete 'set null'. |
| OD217 | parent-store-without-path |
A model sets _parent_store without an indexed parent_path field. |
| OD218 | delegation-field-not-required |
A field that delegates to a parent model is not required with ondelete cascade or restrict. |
| OD219 | tracking-without-mail-thread |
A field sets tracking on a model that does not inherit mail.thread, which Odoo ignores. |
| OD220 | duplicate-field-label |
Two fields of a model have the same label. |
| OD221 | self-dependency-not-recursive |
A computed field depends on itself through other records without recursive=True. |
| OD222 | check-company-without-company |
A field sets check_company to a comodel without company_id. |
| OD223 | compute-leaves-field-unassigned |
A compute method leaves a non-stored readonly field unassigned on some records. |
| OD224 | sql-unknown-column |
A literal SQL query names a column its model's table does not have. |
| OD225 | sql-view-missing-column |
The SQL view of a model selects no column for one of its stored fields. |
| OD226 | sql-view-not-created |
A model sets _auto = False, and nothing creates its SQL view. |
| OD227 | sql-definition-unknown-column |
An SQL constraint or index names a column its model's table does not have. |
| OD228 | delegate-without-ondelete |
A delegate=True Many2one has no ondelete, so Odoo fails when it sets up the registry. |
| OD229 | override-signature-mismatch |
An override refuses a call that the method it overrides takes. |
| OD230 | orphan-hook-method |
A compute, inverse, search or onchange method of a model extension is named nowhere, so Odoo never runs it. |
OD3: XML ids, views and data¶
XML ids, views and data records, within a file and across the dependency graph.
| Code | Name | Reports |
|---|---|---|
| OD301 | unresolved-xmlid |
The XML id is not defined by its module. |
| OD302 | xmlid-not-in-depends |
The XML id comes from a module missing from depends. |
| OD303 | duplicate-xmlid |
The same XML id is defined twice in one file, with another model or overwriting a field. |
| OD304 | unknown-view-field |
A view references a field its model does not have. |
| OD305 | attrs-states-removed |
attrs and states are removed; use invisible, readonly and required expressions. |
| OD306 | tree-renamed-list |
The tree view type is renamed list. |
| OD307 | t-raw-unsafe |
t-raw is deprecated and renders unescaped HTML; use t-out with Markup. |
| OD308 | xmlid-used-before-definition |
An XML id is referenced before the file that defines it is loaded. |
| OD309 | unknown-record-field |
A data record sets a field its model does not have. |
| OD310 | unmatched-view-locator |
An inherited view locates an element its parent views do not have. |
| OD311 | unknown-qweb-template |
A QWeb view or a report names a template no module in scope defines. |
| OD312 | unknown-button-method |
A button calls a method its model does not have. |
| OD313 | private-button-method |
A button calls a private method, which Odoo refuses. |
| OD314 | unknown-function-method |
A <function> of a data file calls a method its model does not have. |
| OD315 | invalid-modifier |
A view modifier is not an expression Odoo reads. |
| OD316 | unknown-modifier-name |
A view modifier names a field its model does not have. |
| OD317 | modifier-field-not-in-view |
A view modifier reads a field the view does not show. |
| OD318 | view-mode-without-view |
A window action lists a view type its model has no view for. |
| OD319 | python-field-syntax-error |
The code of a server action or a cron, or the file name of a report, is not Python that Odoo compiles. |
| OD320 | unknown-server-code-method |
The code of a server action or a cron calls a method its model does not have. |
| OD321 | wrong-model-reference |
A data record gives a relational field a record of another model. |
| OD322 | unknown-selection-value |
A data record gives a Selection field a value it does not list. |
| OD323 | duplicate-xmlid-across-files |
A data file defines an XML id another file of the module already defines, with another model, or overwriting the fields both name. |
| OD324 | missing-required-field |
A data record is created without a required field that has no default. |
| OD325 | t-esc-removed |
The server no longer renders t-esc in a QWeb view; use t-out. |
| OD326 | kanban-box-renamed-card |
The kanban-box template of a kanban view is renamed card. |
| OD327 | chatter-div-replaced |
A form's div with class oe_chatter no longer shows the chatter; use <chatter/>. |
| OD328 | bundle-template-removed |
A data file inherits or references an asset bundle, which is no longer a template. |
OD4: access¶
Access rights: models no one can reach, and rights granted too widely.
| Code | Name | Reports |
|---|---|---|
| OD401 | model-without-access |
A new model has no ir.model.access rule, or from saas~19.4 no ir.access rule with a group. |
| OD402 | public-write-access |
An access row gives public or portal users write, create or unlink, through their group or no group. |
OD5: manifest and module graph¶
The manifest and the dependency graph: listed files, unlisted files and dependencies.
| Code | Name | Reports |
|---|---|---|
| OD501 | manifest-data-missing |
A file listed in the manifest does not exist. |
| OD502 | data-file-not-loaded |
A data file is listed under no manifest key Odoo reads, so Odoo never loads it. |
| OD503 | unknown-dependency |
A module in depends is not on the addons path. |
| OD504 | circular-dependency |
A module in depends depends back on this module. |
| OD505 | qweb-key-ignored |
The manifest lists templates under qweb, which Odoo ignores. |
| OD506 | assets-qweb-unloaded |
The manifest adds files to web.assets_qweb, which Odoo no longer loads. |
OD6: JavaScript and Owl¶
Scripts under static/src that a bundle loads, checked against the scripts of every indexed
module.
| Code | Name | Reports |
|---|---|---|
| OD601 | unresolved-js-import |
A JS import names a module or an export no indexed module provides. |
| OD602 | orphan-patch-super |
A patch calls a super method the class it patches does not have. |
| OD603 | unknown-rpc-model |
An ORM call from JS names a model no indexed module defines. |
| OD604 | unknown-rpc-method |
orm.call() names a method its model does not have. |
| OD605 | unknown-template |
A component, a t-inherit or a t-call names a template no indexed bundle defines. |
| OD606 | unmatched-template-locator |
A template locates an element its parent templates do not have. |
| OD607 | private-rpc-method |
orm.call() names a private method, which Odoo refuses to call over RPC. |
OD7: data flows¶
Values a user controls, followed through one function to the calls that must not receive them.
The sources are the parameters of a controller method behind @route; the parameters of a public
model method (no leading _, no @api.private), which any user with access to the model can call
over RPC; and the request: request.params, request.get_json_data(),
request.get_http_params(), and the args, form, values, files, cookies, headers, data
and JSON of request.httprequest.
A value stays tainted through assignments, formatting, concatenation, f-strings, string methods,
items, attributes, collections, comprehensions and module functions such as json.loads(). Records
do not carry it: what a recordset method such as browse() or search() returns is not tainted.
Both branches of an if are followed, a branch that returns or raises adds nothing after it, and
a loop body is read twice.
A value passed to a method of self, of env["x"] or of a local bound to one is followed into
every definition of that method the module's dependencies hold, overrides included, up to four
calls deep; so is a value such a method returns. The finding is at the sink when the module holds
it, otherwise at the call that leaves the module. A method that runs under sudo() only because
its caller called it so is not known to write as superuser.
The flow ends at a sanitizer: int(), float(), bool(), len(), SQL.identifier(),
escape(), html_escape(), html_sanitize() and plaintext2html(); at assert x in (...) or
if x not in allowed: raise; and at an intersection with values the code chose, such as
set(vals) & allowed.
| Code | Name | Reports |
|---|---|---|
| OD701 | sql-injection-flow |
Data a user controls reaches the text of an SQL query. |
| OD702 | code-injection-flow |
Data a user controls reaches safe_eval(), eval() or exec(). |
| OD703 | markup-injection-flow |
Data a user controls is wrapped in Markup() without escaping. |
| OD704 | sudo-mass-assignment |
A mapping a user controls is written as superuser, so the user picks the fields. |