Computed fields
Fields can have their values automatically calculated by a function, instead of simply reading a database stored value. A computed field is declared just like a regular field, but has the additional
compute argument to define the function used for its computation.
In most cases, computed fields involve writing some business logic. So, to take full advantage of this feature, we need to learn the topics explained in Chapter 8, Business Logic - Supporting Business Processes. We can still explain computed fields here, but will keep the business logic as simple as possible.
Let's work on an example. Books have a publisher. We would like to have the the publisher's country in book form.
For this, we will use a computed field, based on the
publisher_id, which will take its value from the publisher's country_id field.
We should edit the book model in the
library_app/models/library_book.py file to add the following:# class Book(models.Model):
publisher_country_id = fields.Many2one(
'res.country', string='Publisher Country',
compute='_compute_publisher_country',
)
@api.depends('publisher_id.country_id')
def _compute_publisher_country(self):
for book in self:
book.publisher_country_id = book.publisher_id.country_id
The preceding code adds the
publisher_country_id field, and the _compute_publisher_country method used to compute it. The function name was passed to the field as a string argument, but it may also be passed a callable reference (the function identifier, without the surrounding quotes). In that case, we need to make sure the function is defined in the Python file before the field is.
The
@api.depends decorator is needed when the computation depends on other fields, as it usually does. It lets the server know when to recompute stored or cached values. One or more field names are accepted as arguments and dot-notation can be used to follow field relationships. In our case, our field should be recomputed whenever the country_id of the book's publisher_id is changed.
As usual, the
self argument is the recordset object to work with. So, we need to iterate over it to act on each individual record. The computed value is set using the usual assignment (write) operation. In our case, the computation is quite simple, we assign it to the current book's publisher_id.country_id value.
The same computation method can be used for more than one field. In that case, the same method is used on several
compute field arguments, and the computation method should assign values to all the computed fields.Note
The computation function must assign a value to the field, or fields, to compute. If your computation method has
ifconditions, make sure that all run paths assign values to the computed field(s). Otherwise, the computation will error in cases where it fails to assign a value to the computed field(s).
We won't be working on the views for this module yet, but you can make a quick edit on the task form to confirm the computed field is working as expected by using the
Developer Mode, picking the Edit View option, and adding the field directly in the form XML. Don't worry, it will be replaced by the clean module view on the next upgrade.
The computed field we just created can be read, but it can't be searched or written to. By default, computed field values are written on the fly, and are not stored in the database. That's why we can't search them like we can regular fields.
We can enable these search and write operations by implementing specialized functions for them. Along with the
compute function, we can also set a search function to implement the search logic, and the inversefunction to implement the write logic.# class Book(models.Model):
publisher_country_id = fields.Many2one(
'res.country', string='Publisher Country',
compute='_compute_publisher_country',
# store = False, # Default is not to store in db
inverse='_inverse_publisher_country',
search='_search_publisher_country',
)
Writing in a computed field is the inverse logic of computation. So, the function in charge of handling the write operation is called the inverse. In our case, the inverse function is simple. The computation just copied the
book.publisher_id.country_id value to book.publisher_country_id. The inverse operation is to copy the value written in book.publisher_country_id to the book.publisher_id.country_id field: def _inverse_publisher_country(self):
for book in self:
book.publisher_id.country_id = book.publisher_country_id
Notice that this modifies data in the publisher's partner record, and so will also change the value seen in all books with the same publisher. Regular access controls apply to these write operations, so this action will only be successful if the current user also has write access to the partner model.
To enable search operations on a computed field, we need to implement its search function. For this, we need to be able to convert a search domain on the computed field to a search domain using regular stored fields. In our case, the actual search should be done on the
country_id field of the linked publisher_id Partner record: def _search_publisher_country(self, operator, value):
return [('publisher_id.country_id', operator, value)]
When we perform a search on a Model, a domain expression is used as an argument with the filter to apply. Domain expressions are explained in more detail in Chapter 8, Business Logic - Supporting Business Processes, but, for now, you should know that they are a list of
(field, operator, value) conditions.
The
search function is called whenever this computed field is found in conditions of a domain expression. It receives the operator and value for the search and is expected to translate the original search element into an alternative domain search expression. The country_id field is stored in the related partner model, so our search implementation just alters the original search expression to use the publisher_id.country_id field instead.
Computed field values can also be stored in the database, by setting
store = True in their definition. They will be recomputed when any of their dependencies change. Since the values are now stored, they can be searched just like regular fields, and a search function is not needed.
The computed field we implemented in the previous section just copies a value from a related record into a model's own field. This is a common use case that can be automatically handled by Odoo using the related field feature.
Related fields make available, directly in a model, fields that belong to a related model and are accessible using a dot notation chain. This makes them available in situations where dot notation can't be used, such as UI form views.
To create a related field, we declare a field of the required type, just like with regular computed fields, but instead of
compute we use the related attribute, setting it with the dot notation field chain to reach the desired field.
We can use a reference field to get the exact same effect as the previous example, the
publisher_country_idcomputed field:# class Book(models.Model):
publisher_country_related = fields.Many2one(
'res.country', string='Publisher Country (related)',
related='publisher_id.country_id',
)
Behind the scenes, related fields are just computed fields that conveniently implement
search and inversemethods. This means that we can search for and write to them out of the box, without having to write any additional code. By default, related fields are read only, so the inverse write operation won't be available. To enable it, set the readonly=False field attribute.Note
Changed in Odoo 12The related fields are now read only by default: readonly=True. In previous Odoo versions, they were writable by default, but it was proven to be a dangerous default, since it could allow changes to setup or master data in cases where it was not expected to be allowed.
It's also worth noting that these Related fields can also be stored in a database using
store=True, just like any other computed field.
Comments
Post a Comment