Skip to main content

Computed fields

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.

Searching and writing to computed fields

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.
Using these, our computed field declaration looks as follows:
# 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.

Storing computed fields

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.

Related fields

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

Popular posts from this blog

The message and activity features

The message and activity features Odoo has available global  messaging  and activity planning features, provided by the  Discuss  application, with technical name mail. The mail module provides the  mail.thread  abstract class that makes it simple to add the messaging features to any model, and the  mail.activity.mixin  that adds planned activity features. This was done in  Chapter 4 ,  Extending Modules , to explain how to inherit features from mixin abstract classes. To add these features, we need to add the mail dependency to the add-on module,  library_checkout , and then have the library checkout model class inherit from the abstract classes providing the following features. Edit the  'depends'  key in the  library_checkout/__manifest__.py   file, to add the mail module, shown as follows: Copy 'depends': ['library_member' , 'mail' ], And edit the  library_checkout/m...

Setting up an nginx reverse proxy

Setting up an nginx reverse proxy While Odoo itself can serve web pages, it's strongly recommended that there is a  reverse  proxy positioned in front of it. A reverse proxy acts as an intermediary that manages the traffic between clients sending requests and the Odoo servers responding to them. Using a reverse proxy has several benefits. On the security side, it can do the following: Handle (and enforce) HTTPS protocols to encrypt traffic Hide the internal network characteristics Act as an application firewall, limiting the URLs accepted for processing Also, on the performance side, it can provide the following significant improvements: Cached static content, hence reducing the load on the Odoo servers Compressed content to speed up loading time Act as a load balancer, distributing load between several servers Apache is a popular choice when considering a reverse proxy, although  nginx  is a recent alternative with good technical argumen...

The QWeb template language

The QWeb template language The  QWeb  parser looks for special directives in the templates and replaces them with dynamically generated HTML. These directives are XML element attributes and can be used in any valid tag or element, such as  <div> ,  <span> , or  <field> . Sometimes, we may want to use a QWeb directive but we don't want to place it in any of the XML elements in our template. For those cases, we have a  <t>  special element that can have QWeb directives, such as  t-if  or  t-foreach , but is silent and won't have any output on the final XML/HTML produced. The QWeb directives will frequently make use of evaluated expressions to produce different results, depending on the current record values. There are two different QWeb implementations: client-side JavaScript and server-side Python. The reports and website pages use the server-side Python implementation of QWeb. Kanban views us...