OD223 compute-leaves-field-unassigned¶
A compute method leaves a non-stored readonly field unassigned on some records.
| Category | Severity | Series | Needs the index |
|---|---|---|---|
| Correctness | Error | saas~13.3 and later | yes |
What it reports¶
A compute method with a path through its loop over self that ends an iteration without assigning a
field it computes, or that leaves the loop before the last record, when Odoo neither stores the
field nor lets users write it. Odoo merges every definition of the field in the module and its
depends, so the merged one decides: store is absent or false, readonly is true, or absent with
no inverse, and the field is not related. A module that Odoo always installs with base, such
as web, counts as in depends. An attribute that definitions set differently, or that **kwargs
may set, leaves the field unchecked. The finding is at the method's name, and names each such field.
The method must be in the class that defines the field and hold every assignment of the field in
its one for record in self: loop, each on that record. A method that calls super(), reads the
field, may assign it by a name built at runtime (setattr, a key that is not a literal, or
update() or write() given something other than a dict of literal keys), or calls a method of
self or passes self on outside the loop is not checked, nor is a method that a class of the
same model in the same module overrides by a method that calls it through super(), in any file,
since the override may assign the rest; an override in another module does not count. Through the
loop body:
- an assignment,
update()orwrite()of the field, araise, and a call of a method of the record or ofself, or a call given the record, which may assign the field in a helper, count as assigning it; - a
continueorbreakbefore that is reported, and so is anifwithoutelsewhose test is false, but a chain with anelifand noelse, like amatch, is taken to cover every case; - an
ifwith noelifassigns the field when its test holds if its body assigns it, or when the test does not hold if its loneelsedoes, and aforloop whose body assigns it does when its iterable is not empty; two such statements of the same block cover every path only when one assigns exactly when the other does not:if x:andif not x:,if x:and anotherif x:that assigns in itselse,for line in lines:andif not lines:, or one comparison of the same operands under opposite operators, such as==and!=, with the operands of==,!=,isandis notin either order; twoifstatements that assign under the same test are reported, and so are== "a"and== "b", and two loops that each assign the field; - such a statement in the body of a
withpairs with a sibling statement of the enclosing block, unless thewithruns undercontextlib.suppress, and so does one in the body orelseof atrywhen every handler assigns the field or raises, since an exception may skip the assignment; in the body of anif x:, a statement that assigns whenxholds, such asfor line in x:, covers every path, and so in a loneelsedoes one that assigns whenxdoes not; - a
continueof a nested loop before the assignment means that loop may assign nothing, so afterfor line in lines: if not line.x: continuethenrecord.f = line.x, that loop does not pair withif not lines:; - a
returnin the loop body is reported, before or after the assignment, since the records after it stay unassigned, and so is abreakafter the assignment that leaves the loop overself; neither is when it is in a nested function, or follows, as a statement of its block or of a block around it, a call of a method ofself, which may assign the field on every record, araise, or a statement that makes one of them on every path: awithwhose body does, unless it runs undercontextlib.suppress, anifwith aneliforelseor amatcheach of whose branches does, awhile True:loop whose body does before everybreakof it, or atrywhose body orelsedoes and so does each handler, or whosefinallydoes; areturnorbreakin afinallyis still reported when acontinueorraiseafter the assignment skips that call; - a
tryassigns when its body andelsedo and so does each handler, or when itsfinallydoes, and areturnin any of them, or abreakthat leaves the loop overself, is reported, even before afinallythat assigns, unless thatfinallycalls a method ofself; a handler finds the field assigned when thetrybody, after nothing butpassand constants bound to names, first assigns the field alone (record.f = ..., not chained or unpacked)FalseorNone, another falsy constant unless it is aOne2manyorMany2manyfield, or a number, negative too, to anInteger,Float,MonetaryorMany2onefield, since Odoo refuses any other value of some type, such as aSelectionvalue outside its list; - a nested loop may run no iteration, so its
elsecounts, and only when everybreakin its body assigns first; awhile True:loop counts when its body assigns before everybreak, and acontinuein it only starts the next iteration, so the paths to itsbreakdecide; when abreakmay come before the first statement that calls a method ofselfor raises, on every path, a body that maycontinuestill counts as assigning but not as that call, so a laterreturnorbreakafter an assignment is reported.
Why it matters¶
When code reads a computed field, Odoo calls its method on the records to compute, then looks for
those it left without a value. Odoo raises ValueError: Compute method failed to assign for a
field it neither stores nor lets users write, so the form, list or report that reads the field
fails for the records the method skipped, often only for some data. Other computed fields get
False silently. Assign the field on every path of the loop, with False when there is no value.
Odoo raises that error from saas~13.3: it came to saas~13.5 and 14.0 with 126ebbc905af, and was
backported to saas~13.3 with a555b71261f6 and saas~13.4 with ec12ff214ddf. From 13.0 to saas~13.2,
a field the method leaves unassigned raises CacheMiss, a KeyError, when Odoo does not store it
or the record is new, whether users can write it or not. 12.0 sets every field the method computes
to False before calling it, so nothing raises.
The rule does not run for a module when the module or one of its dependencies depends on a module
that is not indexed. A model is skipped when the class that introduces it, with _name or as a
class Odoo names, is in a module outside the dependencies, or when a class of it, or of a model it
inherits or delegates to, has a plain Python base class or an _inherit element that is not a
literal, a parent whose fields the scan cannot see.
Example¶
class ResPartner(models.Model):
_inherit = "res.partner"
initials = fields.Char(compute="_compute_initials")
@api.depends("name")
def _compute_initials(self):
for partner in self:
if partner.name:
partner.initials = "".join(word[0] for word in partner.name.split())
A partner without a name, such as a new address in a form, makes reading initials raise.
class ResPartner(models.Model):
_inherit = "res.partner"
initials = fields.Char(compute="_compute_initials")
@api.depends("name")
def _compute_initials(self):
for partner in self:
if partner.name:
partner.initials = "".join(word[0] for word in partner.name.split())
else:
partner.initials = False