Enable Dark Mode!
how-to-write-computed-fields-inverse-and-onchange-methods-in-odoo-20.jpg
By: Ayana R

How to Write Computed Fields, Inverse and Onchange Methods in Odoo 20

Technical Odoo 20 Fields and widgets

In Odoo development, fields are the way of storing and managing business information. But there are cases when the value of the field should be computed automatically based on the values of other fields, changed upon modifying the related information, or computed dynamically while filling the form.

There are several ORM features offered by Odoo to handle these cases. Such features are Computed Fields, Inverse Methods, and Onchange Methods.

Computed fields can be used to calculate field values automatically based on other fields from the same model. Inverse methods allow making the computed field editable, allowing the user to modify the underlying fields. Onchange methods allow developing an interactive form by updating the field values when users modify the related field.

There are also some additional features provided by Odoo 20 to deal with computed fields: precompute, compute_sql, and @api.depends_context. These features are intended for improving the computation performance, searching and grouping non-stored computed fields, and handling context-based values.

In this blog, we are going to consider how to use computed fields, inverse methods, and onchange methods in Odoo 20 by means of practical examples. Dependency declarations, storing options, problems of inverse methods, and the necessity of using constraints will.

1. What Are Computed Fields in Odoo 20?

A computed field refers to a field whose values will be computed automatically through a Python function rather than having the user type in the values manually.

To illustrate this, let us take the case of a service quote in which the subtotal is dependent upon the quantity and unit price.

Rather than having the user calculate the subtotal himself, we can have a computed field to calculate this subtotal automatically for us.

Basic Syntax

from odoo import models, fields, api

class ServiceQuotation(models.Model):
    _name = 'service.quotation'
    _description = 'Service Quotation'
    quantity = fields.Float(string='Quantity')
    unit_price = fields.Float(string='Unit Price')
    subtotal = fields.Float(
        string='Subtotal',
        compute='_compute_subtotal'
    )
    @api.depends('quantity', 'unit_price')
    def _compute_subtotal(self):
        for record in self:
            record.subtotal = record.quantity * record.unit_price

In this case, the subtotal will be calculated through the quantity and unit_price fields.

The compute argument will indicate the name of the Python method that will calculate the field value. The compute method must calculate a value for the field for each item of self.

2. Understanding @api.depends in Computed Fields

The "@api.depends" decorator is used to declare the fields affecting the computed field.

Odoo calculates when the computed field should be recalculated using these dependencies.

Let us consider the following declaration:

@api.depends('quantity', 'unit_price')
def _compute_subtotal(self):
    for record in self:
        record.subtotal = record.quantity * record.unit_price

In case of a change in quantity or unit price, Odoo will schedule a recalculation of the subtotal.

Why Is It Important to List Every Dependency?

Another critical guideline for creating computed fields is that all the fields accessed within the compute function must be declared.

For example:

@api.depends('quantity', 'unit_price', 'tax_rate')
def _compute_total(self):
    for record in self:
        subtotal = record.quantity * record.unit_price
        record.total = subtotal + (
            subtotal * record.tax_rate / 100
        )

In this case, all three fields are marked as dependencies because the formula accesses all three fields.

If a field is accessed by the computation but it does not appear in @api.depends, then changes in that field might fail to result in recomputation.

Dependencies can be specified as dotted paths for relational fields.

Example:

@api.depends('partner_id.name', 'partner_id.vat')
def _compute_partner_information(self):
    for record in self:
        if record.partner_id.vat:
            record.partner_information = (
                record.partner_id.name + ' - ' +
                record.partner_id.vat
            )
        else:
            record.partner_information = record.partner_id.name

For instance, the calculation is based on fields that belong to the related partner record.

Good practice: Declare all fields that have an effect on the calculation result. This ensures that Odoo tracks dependencies properly.

3. Stored and Non-Stored Computed Fields

By default, computed fields are not stored in the database, but their value is calculated when accessed through the ORM.

Odoo offers the “store” attribute to decide whether computed fields need to be stored.

Non-Stored Computed Fields

The computed field is non-stored by default.

subtotal = fields.Float(
    string='Subtotal',
    compute='_compute_subtotal'
)

Features:

  • It is computed using the compute method.
  • It is not persisted as part of a database column.
  • It can be applied to values that are easy to compute.
  • It cannot be used directly for database searches and grouping.

Stored Computed Fields Using store=True

If the calculated value is supposed to be stored in the database, then set store=True.

subtotal = fields.Float(
    string='Subtotal',
    compute='_compute_subtotal',
    store=True
)

When the dependencies of a stored computed field are modified, Odoo will schedule the recomputation of that field along with the updating of its stored value.

Benefits of store=True:

  • Computed values are saved in database fields.
  • Fields can be filtered using regular ORM domains.
  • These fields can be used for grouping and reporting.
  • Calculations can be spared on read operations.

But then, it is not always required to save all computed fields. If a field is easy to compute and does not need database search or grouping, it can use a non-stored computed field.

4. Using precompute=True in Computed Fields

Odoo has the precompute argument to compute stored computed fields prior to the insertion of the record into the database.

The precomputation is off by default.

For instance:

from odoo import models, fields, api

class ServiceQuotationLine(models.Model):
    _name = 'service.quotation.line'
    _description = 'Service Quotation Line'
    quantity = fields.Float(string='Quantity', default=1.0)
    unit_price = fields.Float(string='Unit Price')
    subtotal = fields.Float(
        string='Subtotal',
        compute='_compute_subtotal',
        store=True,
        precompute=True
    )
    @api.depends('quantity', 'unit_price')
    def _compute_subtotal(self):
        for record in self:
            record.subtotal = (
                record.quantity * record.unit_price
            )

For instance, Odoo will perform the subtotal computation before adding the quotation line to the database if all precomputation prerequisites are met.

When Should We Use precompute=True?

Precomputation may be necessary in cases where:

  • The field can be computed based on values that are already known before adding the field.
  • Records are being added in bulk.
  • The One2many lines are being created simultaneously via the parent record.
  • There is no computation based on any search from the database that requires the existence of the record.

Some Precautions

Precomputation is not always faster.

Odoo’s ORM performs stored field computation in bulk while flushing. In the case of creating records separately, the setting of precomputation may lead to computation of each record separately, thus losing the benefits of batching.

Moreover, precomputation is done only if there is no value and no default value set for the field during creation.

So precompute=True should be used only if needed.

5. Making Computed Fields Editable Using Inverse Methods

Computed fields are readonly by default in the user interface.

But at times, there could be cases where users might want to change a computed field value.

Take the example of a quote where the total amount is computed based on quantity, unit price, and tax percentage. The user might want to enter the total amount required, and Odoo would calculate the unit price.

Here comes the concept of inverse methods.

The inverse method makes the ORM convert the changes made to a computed field into changes to its base fields.

Inverse Method Implementation

The inverse method is defined with the inverse attribute.

total = fields.Float(
    string='Total',
    compute='_compute_total',
    inverse='_inverse_total',
    store=True,
    readonly=False
)

In this case:

  • compute computes the total amount.
  • inverse deals with modifications to the total amount.
  • store=True retains the computed value.
  • readonly=False makes the field editable.

6. Understanding Shared Compute and Inverse Method Pitfalls

Odoo can have multiple computed fields that call the same compute function.

For instance,

discount_amount = fields.Float(
    compute='_compute_discount'
)
total = fields.Float(
    compute='_compute_discount'
)
@api.depends('amount', 'discount')
def _compute_discount(self):
    for record in self:
        record.discount_amount = (
            record.amount * record.discount / 100)
        record.total = (
            record.amount - record.discount_amount)

The above strategy is legitimate, as one computation strategy can compute more than one field.

However, use of a common inverse strategy for several computed fields is not advised.

Why Does Common Inverse Strategy Lead to Mistakes?

The Odoo system safeguards all fields that employ the same inverse strategy.

If the inverse strategy involves reading a protected computed field whose value is not stored in the cache, the ORM might provide the default False value instead of calculating the real value.

This leads to mistakes.

Consider two computed fields that have the same inverse strategy. If one inverse process reads the other field, the answer may be wrong.

Recommended Strategy

Have separate inverse strategies for computed fields that involve different reverse computations.

amount_with_tax = fields.Float(
    compute='_compute_amounts',
    inverse='_inverse_amount_with_tax'
)
amount_without_tax = fields.Float(
    compute='_compute_amounts',
    inverse='_inverse_amount_without_tax'
)

The two fields could still be computed using the same compute method, although their respective inverse methods have to be dealt with separately.

Best practice: Computing methods may be shared but never inverse methods if the latter requires the use of other computed fields.

7. Using compute_sql for Non-Stored Computed Fields

Odoo version 20 includes the compute_sql feature that provides the capability to create an SQL expression for a computed field.

Compute_sql is beneficial when a computed field needs to be non-stored yet capable of supporting database activities like searches, sorting, and grouping.

Normally, non-stored computed fields cannot be searched using the database, as there is no column to store the computed value.

Using compute_sql, Odoo uses an SQL expression to represent the computed value in database searches.

Example: Calculating Duration Between Two Dates

Let’s consider a project model with start date and end date.

We need to compute the duration between those dates.

from odoo import models, fields, api
from odoo.tools import SQL

class ProjectTask(models.Model):
    _inherit = 'project.task'
    duration_days = fields.Integer(
        string='Duration (Days)',
        compute='_compute_duration',
        compute_sql='_compute_sql_duration',
        compute_sudo=False
    )
    @api.depends('date_start', 'date_end')
    def _compute_duration(self):
        for record in self:
            if record.date_start and record.date_end:
                record.duration_days = (
                    record.date_end - record.date_start
                ).days
            else:
                record.duration_days = 0
    def _compute_sql_duration(self, table):
        return SQL(
            "COALESCE(%s - %s, 0)",
            table.date_end,
            table.date_start
        )

Understanding the Implementation

The compute method in Python returns the duration for normal field access through ORM.

The SQL computation method returns an SQL expression equivalent to this Python code to be used at the database level.

The table parameter refers to the SQL table expression for this model, making it easier for the programmer to use its columns.

compute_sudo = False explicitly defines the security mode in which the computed field operates.

Advantages of compute_sql

  • Ability to use database-level searches on computed values.
  • Allows for sorting without the need to store a computed column.
  • Enables grouping and reporting for suitable computed fields.
  • No need to maintain a separate stored column for the computed value.

Considerations

SQL expression should return the same value as returned by the Python compute method.

If the two computations return a different value, then the inconsistency would arise in the results based on whether the ORM accessed the field or whether it was part of a database query.

Also, use the Odoo SQL wrapper and not the user input values directly in SQL expressions.

Use compute_sql if your computation is possible to do at the database level using SQL. For any other computation not possible at the database level, try a search method.

8. Using @api.depends_context for Context-Dependent Computed Fields

Sometimes, the calculation of the computed field is influenced by both the fields of the model and the execution context.

For instance, the display value for the product can be influenced by whether the current interface asks for the internal reference.

Odoo offers @api.depends_context for that purpose.

Example

from odoo import models, fields, api

class ProductTemplate(models.Model):
    _inherit = 'product.template'
    display_label = fields.Char(
        string='Display Label',
        compute='_compute_display_label'
    )
    @api.depends('name', 'default_code')
    @api.depends_context('show_internal_code')
    def _compute_display_label(self):
        for record in self:
            if self.env.context.get('show_internal_code'):
                record.display_label = (
                    f'[{record.default_code or ""}] {record.name}'
                )
            else:
                record.display_label = record.name

The field relies on both regular model fields and the show_internal_code context key.

If the value of the context is changed, Odoo will be able to identify the relevant computed values in the cache of the system.

Special Context Dependencies

Odoo offers special support for several context keys:

  • company: Current company/ID in the context.
  • uid: Current user ID and whether it is a superuser.
  • active_test: Active records filtering context.
  • Context dependencies need to be hashable values.

Note: @api.depends_context is designed to be used for non-stored computed fields. The value that depends on the context is usually not stored as one database value, since the same record will generate different values for different users/companies/contexts.

9. Understanding Onchange Methods in Odoo 20

Onchange methods are applied in order to change form values when users make changes to specific fields.

Unlike computed fields, onchange methods are created primarily for interactive user interfaces.

For instance, whenever a user chooses a product in the quotation line, the product description and its price will be automatically filled in Odoo.

This functionality is supported by the @api.onchange decorator.

Basic Syntax

@api.onchange('product_id')
def _onchange_product_id(self):
    if self.product_id:
        self.unit_price = self.product_id.list_price
        self.description = self.product_id.display_name

When the product field in the form is changed by the user, then the function of onchange will be executed.

Practical Example

from odoo import models, fields, api

class ServiceQuotationLine(models.Model):
    _name = 'service.quotation.line'
    _description = 'Service Quotation Line'
    product_id = fields.Many2one(
        'product.product',
        string='Product'
    )
    description = fields.Char(
        string='Description'
    )
    quantity = fields.Float(
        string='Quantity',
        default=1.0
    )
    unit_price = fields.Float(
        string='Unit Price'
    )
    @api.onchange('product_id')
    def _onchange_product_id(self):
        if self.product_id:
            self.description = self.product_id.display_name
            self.unit_price = self.product_id.list_price
        else:
            self.description = False
            self.unit_price = 0.0

How Does It Work?

  1. The user clicks on the quote form.
  2. The user selects a product.
  3. Odoo invokes _onchange_product_id().
  4. The description and price per unit get automatically filled in.
  5. The user can check the values and modify them.

Important Limitations of Onchange Methods

There are several limitations to Onchange:

  • It is initiated by interactions within the form and not automatically in ORM create/write functions.
  • It is done on a pseudo record that represents the state of the form.
  • It must not do any CRUD function like create(), write() or unlink() on the pseudo record.
  • The Dotted notation is not available in the @api.onchange decorator.
  • There is no way for a One2many/Many2many field to change itself from within its own onchange function because of the web client limitation.

10. Using Constraints for Data Integrity Instead of Relying Only on Onchange

Some limitations of OnChange are as follows:

OnChange is executed based on operations carried out on the form and not on automatically executed ORM create/write methods.

It executes on the pseudo record that reflects the state of the form.

It must not execute any operation on CRUD methods like create(), write(), and unlink() on the pseudo record.

The dotted notation is not allowed in the @api.onchange method.

It is not possible to change One2many or Many2many fields through an onchange method because of web client limitations.

Using @api.constrains

from odoo import models, fields, api, _
from odoo.exceptions import ValidationError

class ServiceQuotationLine(models.Model):
    _name = 'service.quotation.line'
    _description = 'Service Quotation Line'
    quantity = fields.Float(
        string='Quantity',
        default=1.0
    )
    @api.constrains('quantity')
    def _check_quantity(self):
        for record in self:
            if record.quantity <= 0:
                raise ValidationError(
                    _("Quantity must be greater than zero.")
                )

In case the value of the quantity gets changed during an ORM create/write operation, including the specified field, Odoo performs the check and throws a validation error in case of an invalid value.

Onchange and Constraints Together

Both techniques can be applied at once for better user experience.

Onchange method:

@api.onchange('quantity')
def _onchange_quantity(self):
    if self.quantity <= 0:
        return {
            'warning': {
                'title': _('Invalid Quantity'),
                'message': _('Quantity must be greater than zero.')
            }
        }
@api.constrains('quantity')
def _check_quantity(self):
    for record in self:
        if record.quantity <= 0:
            raise ValidationError(
                _("Quantity must be greater than zero.")
            )

Constraint method: Onchange method gives instant feedback during the form completion process. The constraint makes sure that no erroneous data can be accepted using the ORM operation.

Note: The @api.constrains function runs if the defined fields are present in the create or write operation. It does not accept the dotted name of the relational fields. If the validation has to take place on the creation of a record even without that particular field, then provide an implementation accordingly.

The computed fields, inverse methods, and onchange methods are crucial parts of the Odoo ORM. They are used to perform automated calculations, make computed values editable, and enhance the user experience with dynamically updated forms.

The Odoo 20 framework allows developers to use features such as store=True, precompute=True, compute_sql, and @api.depends_context to get more flexibility in computations, storage, and access to computed fields.

It is vital to know the differences between all these approaches to develop an Odoo application successfully. The computed fields should be responsible for calculations, inverse methods for changes in the computed values, and onchange methods for assisting users in filling out the form.

The most important thing is that business logic should not depend only on onchange methods. Constraints will help to keep the data in a valid state regardless of how records were created or modified.

Thus, using these ideas appropriately, developers can create effective and reliable custom modules for Odoo 20.

To read more about Comparison Between Computed and Onchange in Odoo 19, refer to our blog, Comparison Between Computed and Onchange in Odoo 19.


Frequently Asked Questions

What is the distinction between store=True and precompute=True in Odoo 20?

The store=True attribute stores the value of the computed field in the database and makes it searchable and groupable. The precompute=True attribute precalculates a stored computed field prior to the insertion of the record in the database. These attributes have their own purpose and can be used simultaneously.

Is it possible to make a computed field editable in Odoo 20?

Yes, it is possible. To make a computed field editable, it is necessary to define an inverse method with the inverse attribute and to set readonly=False to make the field editable.

Why should constraints be used rather than onchange methods?

Onchange methods are mostly called out due to the interaction of a user with form views. These methods cannot guarantee validation of records in the backend, imports, or API calls. Constraints validate business rules during ORM operations and help maintain data integrity across different entry points.

If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



WhatsApp