The recipes in this chapter will make small additions to an existing add-on module. In the previous chapter, we registered our add-on module in the Odoo instance. In this chapter, we will dive deeply into the database side of the module. We will add a new model (database table), new fields, and constraints. We will also examine the use of inheritance in Odoo. We will be using the module we created in the recipes in Chapter 3, Creating Odoo Add-On Modules.
In this chapter, we will cover the following recipes:
To follow the examples in this chapter, you should have the module that we created in Chapter 3, Creating Odoo Add-On Modules, and the module must be ready to use.
All the code used in this chapter can be downloaded from the GitHub repository at https://github.com/PacktPublishing/Odoo-14-Development-Cookbook-Fourth-Edition/tree/master/Chapter04.
Models have structural attributes for defining their behavior. These are prefixed with an underscore. The most important attribute of the model is _name, as this defines the internal global identifier. Internally, Odoo uses this _name attribute to create a database table. For example, if you provide _name="library.book", then the Odoo ORM will create the library_book table in the database. And that's why the _name attribute must be unique across Odoo.
There are two other attributes that we can use on a model:
This recipe assumes that you have an instance ready with the my_library module, as described in Chapter 3, Creating Odoo Add-On Modules.
The my_library instance should already contain a Python file called models/library_book.py, which defines a basic model. We will edit it to add a new class-level attribute after _name:
_description = 'Library Book'
_order = 'date_release desc, name'
_rec_name = 'short_name'
short_name = fields.Char('Short Title', required=True)
<field name="short_name"/>
When we're done, our library_book.py file should appear as follows:
from odoo import models, fields
class LibraryBook(models.Model):
_name = 'library.book'
_description = 'Library Book'
_order = 'date_release desc, name'
_rec_name = 'short_name'
name = fields.Char('Title', required=True)
short_name = fields.Char('Short Title', required=True)
date_release = fields.Date('Release Date')
author_ids = fields.Many2many('res.partner', string='Authors')
Your <form> view in the library_book.xml file will look as follows:
<form>
<group>
<group>
<field name="name"/>
<field name="author_ids" widget="many2many_tags"/>
</group>
<group>
<field name="short_name"/>
<field name="date_release"/>
</group>
</group>
</form>
We should then upgrade the module to activate these changes in Odoo. To update the module, you can open the Apps menu, search for the my_library module, and then update the module via a dropdown, as in the following screenshot:
Alternatively, you can also use the -u my_library command in the command line.
The first step adds a more user-friendly title to the model's definition. This is not mandatory, but can be used by some add-ons. For instance, it is used by the tracking feature in the mail add-on module for the notification text when a new record is created. For more details, refer to Chapter 23, Managing Emails in Odoo. If you don't use _description for your model, in that case, Odoo will show a warning in the logs.
By default, Odoo orders the records using the internal id value (autogenerated primary key). However, this can be changed so that we can use the fields of our choice by providing an _order attribute with a string containing a comma-separated list of field names. A field name can be followed by the desc keyword to sort it in descending order.
Important note
Only fields stored in the database can be used. Non-stored computed fields can't be used to sort records.
The syntax for the _order string is similar to SQL ORDER BY clauses, although it's stripped down. For instance, special clauses, such as NULLS FIRST, are not allowed.
Model records use a representation when they are referenced from other records. For example, a user_id field with the value 1 represents the Administrator user. When displayed in a form view, Odoo will display the username, rather than the database ID. In short, _rec_name is the display name of the record used by Odoo GUI to represent that record. By default, the name field is used. In fact, this is the default value for the _rec_name attribute, which is why it's convenient to have a name field in our models. In our example, the library.book model has a name field, so, by default, Odoo will use it as a display name. We want to change this behavior in step 3; we have used short_name as the _rec_name. After that, library.book model's display name is changed form name to short_name and Odoo GUI will use the value of short_name to represent the record.
Warning
If your model doesn't have a name field and you haven't specified _rec_name either in that case, your display name will be a combination of the model name and record ID, like this: (library.book, 1).
Since we have added a new field, short_name, to the model, the Odoo ORM will add a new column to the database table, but it won't display this field in the view. To do this, we need to add this field to the form view. In step 4, we added the short_name field to the form view.
Record representation is available in a magic display_name computed field and has been automatically added to all models since version 8.0. Its values are generated using the name_get() model method, which was already in existence in the previous versions of Odoo.
The default implementation of name_get() uses the _rec_name attribute to find which field holds the data, which is used to generate the display name. If you want your own implementation for the display name, you can override the name_get() logic to generate a custom display name. The method must return a list of tuples with two elements: the ID of the record and the Unicode string representation for the record.
For example, to have the title and its release date in the representation, such as Moby Dick (1851-10-18), we can define the following:
Take a look at the following example. This will add a release date in the record's name:
def name_get(self):
result = []
for record in self:
rec_name = "%s (%s)" % (record.name, record.date_release)
result.append((record.id, rec_name))
return result
After adding the preceding code, your display_name record will be updated. Suppose you have a record with the name Odoo Cookbook and a release date of 19-04-2019, then the preceding name_get() method will generate a name such as Odoo Cookbook (19-04-2019).
Models are meant to store data, and this data is structured in fields. Here, you will learn about the several types of data that can be stored in fields, and how to add them to a model.
This recipe assumes that you have an instance ready with the my_library add-on module available, as described in Chapter 3, Creating Odoo Add-On Modules.
The my_library add-on module should already have models/library_book.py, defining a basic model. We will edit it to add new fields:
from odoo import models, fields
class LibraryBook(models.Model):
# ...
short_name = fields.Char('Short Title')
notes = fields.Text('Internal Notes')
state = fields.Selection(
[('draft', 'Not Available'),
('available', 'Available'),
('lost', 'Lost')],
'State')
description = fields.Html('Description')
cover = fields.Binary('Book Cover')
out_of_print = fields.Boolean('Out of Print?')
date_release = fields.Date('Release Date')
date_updated = fields.Datetime('Last Updated')
pages = fields.Integer('Number of Pages')
reader_rating = fields.Float(
'Reader Average Rating',
digits=(14, 4), # Optional precision decimals,
)
<form>
<group>
<group>
<field name="name"/>
<field name="author_ids" widget="many2many_tags"/>
<field name="state"/>
<field name="pages"/>
<field name="notes"/>
</group>
<group>
<field name="short_name"/>
<field name="date_release"/>
<field name="date_updated"/>
<field name="cover" widget="image" class="oe_avatar"/>
<field name="reader_rating"/>
</group>
</group>
<group>
<field name="description"/>
</group>
</form>
Upgrading the module will make these changes effective in the Odoo model.
Take a look at the following samples of different fields. Here, we have used different attributes on various types in the fields. This will give you a better idea of field declaration:
short_name = fields.Char('Short Title',translate=True, index=True)
state = fields.Selection(
[('draft', 'Not Available'),
('available', 'Available'),
('lost', 'Lost')],
'State', default="draft")
description = fields.Html('Description', sanitize=True, strip_style=False)
pages = fields.Integer('Number of Pages',
groups='base.group_user',
states={'lost': [('readonly', True)]},
help='Total book page count', company_dependent=False)
Fields are added to models by defining an attribute in their Python classes. The non-relational field types that are available are as follows:
Important note
In fields of the Selection type, you can use integer keys, but you must be aware that Odoo interprets 0 as not having been set internally, and will not display the description if the stored value is zero. This can happen, so you will need to take this into account.
The first step of this recipe shows the minimal syntax to add to each field type. The field definitions can be expanded to add other optional attributes, as shown in step 2.
Here's an explanation for the field attributes that were used:
The various whitelists that are mentioned here are defined in odoo/tools/mail.py.
If you need finer control in HTML sanitization, there are a few more attributes that you can use, which only work if sanitize is enabled:
Finally, we updated the form view according to the newly added fields in the model. We placed <field> tags in an arbitrary manner here, but you can place them anywhere you want. Form views are explained in more detail in Chapter 9, Backend Views.
The Selection field also accepts a function reference as its selection attribute instead of a list. This allows for dynamically generated lists of options. You can find an example relating to this in the Adding dynamic relations using reference fields recipe in this chapter, where a selection attribute is also used.
The Date and Datetime field objects expose a few utility methods that can be convenient.
For Date, we have the following:
For Datetime, we have the following:
Other than the basic fields, we also have relational fields: Many2one, One2many, and Many2many. These are explained in the Adding relational fields to a model recipe in this chapter.
It's also possible to have fields with automatically computed values, defining the computation function with the compute field attribute. This is explained in the Adding computed fields to a model recipe.
A few fields are added by default in Odoo models, so we should not use these names for our fields. These are the id field, for the record's automatically generated identifier, and a few audit log fields, which are as follows:
The automatic creation of these log fields can be disabled by setting the _log_access=False model attribute.
Another special column that can be added to a model is active. It must be a Boolean field, allowing users to mark records as inactive. It is used to enable the archive/unarchive feature on the records. Its definition is as follows:
active = fields.Boolean('Active', default=True)
By default, only records with active set to True are visible. To retrieve them, we need to use a domain filter with [('active', '=', False)]. Alternatively, if the 'active_test': False value is added to the environment's context, the ORM will not filter out inactive records.
In some cases, you may not be able to modify the context to get both the active and the inactive records. In this case, you can use the ['|', ('active', '=', True), ('active', '=', False)] domain.
Caution
[('active', 'in' (True, False))] does not work as you might expect. Odoo is explicitly looking for an ('active', '=', False) clause in the domain. It will default to restricting the search to active records only.
When using float fields, we may want to let the end user configure the decimal precision that is to be used. In this recipe, we will add a Cost Price field to the Library Books model, with the user-configurable decimal precision.
We will continue using the my_library add-on module from the previous recipe.
Perform the following steps to apply dynamic decimal precision to the model's cost_price field:
class LibraryBook(models.Model):
cost_price = fields.Float(
'Book Cost', digits='Book Price')
Tip
Whenever you add new fields in models, you will need to add them into views in order to access them from the user interface. In the previous example, we added the cost_price field. To see this in the form view, you need to add it with <field name="cost_price"/>.
When you add a string value to the digits attribute of the field, Odoo looks up that string in the decimal accuracy model's Usage field and returns a tuple with 16-digit precision and the number of decimals that were defined in the configuration. Using the field definition, instead of having it hardcoded, allows the end user to configure it according to their needs.
Tip
If you are using a version older than v13, you require some extra work to use the digits attribute in float fields. In older versions, decimal precision was available in a separate module called decimal_precision. To enable custom decimal precision in your field, you have to use the get_precision() method of the decimal_precision module like this: cost_price = fields.Float( 'Book Cost', digits=dp.get_precision('Book Price')).
Odoo has special support for monetary values related to a currency. Let's see how we can use this in a model.
We will continue to use the my_library add-on module from the previous recipe.
The monetary field needs a complementary currency field to store the currency for the amounts.
my_library already has models/library_book.py, which defines a basic model. We will edit this to add the required fields:
class LibraryBook(models.Model):
# ...
currency_id = fields.Many2one(
'res.currency', string='Currency')
class LibraryBook(models.Model):
# ...
retail_price = fields.Monetary(
'Retail Price',
# optional: currency_field='currency_id',
)
Now, upgrade the add-on module, and the new fields should be available in the model. They won't be visible in views until they are added to them, but we can confirm their addition by inspecting the model fields in Settings | Technical | Database Structure | Models in developer mode.
After adding them to the form view, it will appear as follows:
Monetary fields are similar to float fields, but Odoo is able to represent them correctly in the user interface since it knows what their currency is through the second field.
This currency field is expected to be called currency_id, but we can use whatever field name we like as long as it is indicated using the optional currency_field parameter.
Tip
You can omit the currency_field attribute from the monetary field if you are storing your currency information in a field with the name currency_id.
This is very useful when you need to maintain the amounts in different currencies in the same record. For example, if we want to include the currency of the sale order and the currency of the company, you can configure the two fields as fields.Many2one(res.currency) and use the first one for the first amount and the other one for the second amount.
You might like to know that the decimal precision for the amount is taken from the currency definition (the decimal_precision field of the res.currency model).
Relations between Odoo models are represented by relational fields. There are three different types of relations:
Looking at the Library Books example, we can see that each book can only have one publisher, so we can have a many-to-one relation between books and publishers.
Each publisher, however, can have many books. So, the previous many-to-one relation implies a one-to-many reverse relation.
Finally, there are cases in which we can have a many-to-many relation. In our example, each book can have several (many) authors. Also, inversely, each author may have written many books. Looking at it from either side, this is a many-to-many relation.
We will continue using the my_library add-on module from the previous recipe.
Odoo uses the partner model, res.partner, to represent people, organizations, and addresses. We should use it for authors and publishers. We will edit the models/library_book.py file to add these fields:
class LibraryBook(models.Model):
# ...
publisher_id = fields.Many2one(
'res.partner', string='Publisher',
# optional:
ondelete='set null',
context={},
domain=[],
)
class ResPartner(models.Model):
_inherit = 'res.partner'
published_book_ids = fields.One2many(
'library.book', 'publisher_id',
string='Published Books')
The _inherit attribute we use here is for inheriting an existing model. This will be explained in the Adding features to a model using inheritance recipe later in this chapter.
class LibraryBook(models.Model):
# ...
author_ids = fields.Many2many(
'res.partner', string='Authors')
class ResPartner(models.Model):
# ...
authored_book_ids = fields.Many2many(
'library.book',
string='Authored Books',
# relation='library_book_res_partner_rel' # optional
)
Now, upgrade the add-on module, and the new fields should be available in the model. They won't be visible in the views until they are added to them, but we can confirm their addition by inspecting the model fields in Settings | Technical | Database Structure | Models in developer mode.
Many-to-one fields add a column to the database table of the model, storing the database ID of the related record. At the database level, a foreign key constraint will also be created, ensuring that the stored IDs are a valid reference to a record in the related table. No database index is created for these relation fields, but this can be done by adding the index=True attribute.
We can see that there are four more attributes that we can use for many-to-one fields. The ondelete attribute determines what happens when the related record is deleted. For example, what happens to books when their publisher record is deleted? The default is 'set null', which sets an empty value on the field. It can also be 'restrict', which prevents the related record from being deleted, or 'cascade', which causes the linked record to also be deleted.
The last two (context and domain) are also valid for the other relational fields. These are mostly meaningful on the client-side, and, at the model level, they act as default values that will be used in the client-side views:
Both context and domain are explained in more detail in Chapter 9, Backend Views.
One-to-many fields are the reverse of many-to-one relations, and although they are added to models just like other fields, they have no actual representation in the database. Instead, they are programmatic shortcuts, and they enable views to represent these lists of related records. That means that one-to-many fields need a many-to-one field in the reference model. In our example, we have added one-to-many field by inheriting a partner model. We will see model inheritance in detail in the Adding features to a model using inheritance recipe in this chapter. In our example, the one-to-many field published_book_ids has a reference to the publisher_id field of the library.book model.
Many-to-many relations don't add columns to the tables for the models, either. This type of relation is represented in the database using an intermediate relation table, with two columns to store the two related IDs. Adding a new relation between a book and an author creates a new record in the relation table with the ID of the book and the ID of the author.
Odoo automatically handles the creation of this relation table. The relation table name is, by default, built using the name of the two related models, alphabetically sorted, plus a _rel suffix. However, we can override this using the relation attribute.
A case to keep in mind is when the two table names are large enough for the automatically generated database identifiers to exceed the PostgreSQL limit of 63 characters. As a rule of thumb, if the names of the two related tables exceed 23 characters, you should use the relation attribute to set a shorter name. In the next section, we will go into more detail on this.
The Many2one fields support an additional auto_join attribute. This is a flag that allows the ORM to use SQL joins on this field. Due to this, it bypasses the usual ORM control, such as user access control and record access rules. In specific cases, it can solve performance issues, but it is advised to avoid using it.
We have covered the shortest way to define the relational fields. Let's take a look at the attributes specific to this type of field.
The One2many field attributes are as follows:
The Many2many field attributes are as follows:
For Many2many relations, in most cases, the ORM will take care of the default values for these attributes. It is even capable of detecting inverse Many2many relations, detecting the already existing relation table, and appropriately inverting the column1 and column2 values.
However, there are two cases where we need to step in and provide our own values for these attributes:
The relation table's automatic name is <model1>_<model2>_rel. However, this relation table also creates an index for its primary key with the following identifier:
<model1>_<model2>_rel_<model1>_id_<model2>_id_key
This primary key also needs to meet the 63-character limit. So, if the two table names combined exceed a total of 63 characters, you will probably have trouble meeting the limits and will need to manually set the relation attribute.
Hierarchies are represented like a model having relations with the same model. Each record has a parent record in the same model, and many child records. This can be achieved by simply using many-to-one relations between the model and itself.
However, Odoo also provides improved support for this type of field by using the nested set model (https://en.wikipedia.org/wiki/Nested_set_model). When activated, queries using the child_of operator in their domain filters will run significantly faster.
Staying with the Library Books example, we will build a hierarchical category tree that can be used to categorize books.
We will continue using the my_library add-on module from the previous recipe.
We will add a new Python file, models/library_book_categ.py, for the category tree, as follows:
from . import library_book_categ
from odoo import models, fields, api
class BookCategory(models.Model):
_name = 'library.book.category'
name = fields.Char('Category')
parent_id = fields.Many2one(
'library.book.category',
string='Parent Category',
ondelete='restrict',
index=True)
child_ids = fields.One2many(
'library.book.category', 'parent_id',
string='Child Categories')
_parent_store = True
_parent_name = "parent_id" # optional if field is 'parent_id'
parent_path = fields.Char(index=True)
from odoo.exceptions import ValidationError
...
@api.constraints('parent_id')
def _check_hierarchy(self):
if not self._check_recursion():
raise models.ValidationError(
'Error! You cannot create recursive categories.')
category_id = fields.Many2one('library.book.category')
Finally, a module upgrade will make these changes effective.
To display the librart.book.category model in the user interface, you will need to add menus, views, and security rules. For more details, refer to Chapter 3, Creating Odoo Add-On Modules. Alternatively, you can access all code at https://github.com/PacktPublishing/Odoo-13-Development-Cookbook-Fourth-Edition.
Steps 1 and 2 create the new model with hierarchical relations. The Many2one relation adds a field to reference the parent record. For faster child record discovery, this field is indexed in the database using the index=True parameter. The parent_id field must have ondelete set to either 'cascade' or 'restrict'. At this point, we have all that is required to achieve a hierarchical structure, but there are a few more additions we can make to enhance it. The One2many relation does not add any additional fields to the database, but provides a shortcut to access all the records with this record as their parent.
In step 3, we activate the special support for the hierarchies. This is useful for high-read but low-write instructions, since it brings faster data browsing at the expense of costlier write operations. This is done by adding one helper field, parent_path, and setting the model attribute to _parent_store=True. When this attribute is enabled, the helper field will be used to store data in searches in the hierarchical tree. By default, it is assumed that the field for the record's parent is called parent_id, but a different name can also be used. In this case, the correct field name should be indicated using the additional model attribute, _parent_name. The default is as follows:
_parent_name = 'parent_id'
Step 4 is advised in order to prevent cyclic dependencies in the hierarchy, which means having a record in both the ascending and descending trees. This is dangerous for programs that navigate through the tree, since they can get into an infinite loop. models.Model provides a utility method for this (_check_recursion) that we have reused here.
Step 5 is to add the category_id field with the type many2one to the libary.book book, so that we can set a category on book records. This is just for the purpose of completing our example.
The technique shown here should be used for static hierarchies, which are read and queried often but are updated less frequently. Book categories are a good example, since the library will not be continuously creating new categories; however, readers will often be restricting their searches to a category and its child categories. The reason for this lies in the implementation of the nested set model in the database, which requires an update of the parent_path column (and the related database indexes) for all records whenever a category is inserted, removed, or moved. This can be a very expensive operation, especially when multiple editions are being performed in parallel transactions.
If you are dealing with a very dynamic hierarchical structure, the standard parent_id and child_ids relations will often result in better performance by avoiding table-level locks.
Models can have validations preventing them from entering undesired conditions.
Odoo supports two different types of constraints:
Database-level constraints are limited to the constraints supported by PostgreSQL. The most commonly used ones are the UNIQUE constraints, but the CHECK and EXCLUDE constraints can also be used. If these are not enough for our needs, we can use Odoo server-level constraints written in Python code.
We will use the Library Books model that we created in Chapter 3, Creating Odoo Add-On Modules, and add a couple of constraints to it. We will add a database constraint that prevents duplicate book titles, and a Python model constraint that prevents release dates in the future.
We will continue using the my_library add-on module from the previous recipe. We expect it to contain at least the following:
from odoo import models, fields
class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title', required=True)
date_release = fields.Date('Release Date')
We will edit the LibraryBook class in the models/library_book.py Python file:
class LibraryBook(models.Model):
# ...
_sql_constraints = [
('name_uniq', 'UNIQUE (name)',
'Book title must be unique.'),
('positive_page', 'CHECK(pages>0)',
'No of pages must be positive')
]
from odoo import api, models, fields
from odoo.exceptions import ValidationError
class LibraryBook(models.Model):
# ...
@api.constrains('date_release')
def _check_release_date(self):
for record in self:
if record.date_release and
record.date_release > fields.Date.today():
raise models.ValidationError(
'Release date must be in the past')
After these changes are made to the code file, an add-on module upgrade and a server restart are needed.
The first step creates a database constraint on the model's table. It is enforced at the database level. The _sql_constraints model attribute accepts a list of constraints to create. Each constraint is defined by a three-element tuple. These are listed as follows:
In our example, we have used two SQL constraints. The first one is for a unique book name, and the second one is to check whether the book has a positive number of pages.
Warning
If you are adding SQL constraints to the existing model through model inheritance, make sure you don't have rows that violate the constraints. If you have such rows, then SQL constraints will not be added and an error will be generated in the log.
As we mentioned earlier, other database table constraints can also be used. Note that column constraints, such as NOT NULL, can't be added this way. For more information on PostgreSQL constraints in general and table constraints in particular, take a look at http://www.postgresql.org/docs/current/static/ddl-constraints.html.
In the second step, we added a method to perform Python code validation. It is decorated with @api.constrains, meaning that it should be executed to run checks when one of the fields in the argument list is changed. If the check fails, a ValidationError exception will be raised.
Normally, if you need complex validation, you can use @api.constrains, but for some simple cases, you can use _sql_constraints with the CHECK option. Take a look at the following example:
_sql_constraints = [
( 'check_credit_debit',
'CHECK(credit + debit>=0 AND credit * debit=0)',
'Wrong credit or debit value in accounting entry!'
)
]
In the preceding example, we have used the CHECK option, and we are checking multiple conditions in the same constraints with the AND operator.
Sometimes, we need to have a field that has a value calculated or derived from other fields in the same record or in related records. A typical example is the total amount, which is calculated by multiplying a unit price by a quantity. In Odoo models, this can be achieved using computed fields.
To show you how computed fields work, we will add one to the Library Books model to calculate the days since the book's release date.
It is also possible to make computed fields editable and searchable. We will implement this to our example as well.
We will continue using the my_library add-on module from the previous recipe.
We will edit the models/library_book.py code file to add a new field and the methods supporting its logic:
class LibraryBook(models.Model):
# ...
age_days = fields.Float(
string='Days Since Release',
compute='_compute_age',
inverse='_inverse_age',
search='_search_age',
store=False, # optional
compute_sudo=True # optional
)
# ...
from odoo import api # if not already imported
# ...
class LibraryBook(models.Model):
# ...
@api.depends('date_release')
def _compute_age(self):
today = fields.Date.today()
for book in self:
if book.date_release:
delta = today - book.date_release
book.age_days = delta.days
else:
book.age_days = 0
from datetime import timedelta
# ...
class LibraryBook(models.Model):
# ...
def _inverse_age(self):
today = fields.Date.today()
for book in self.filtered('date_release'):
d = today - timedelta(days=book.age_days)
book.date_release = d
from datetime import timedelta
class LibraryBook(models.Model):
# ...
def _search_age(self, operator, value):
today = fields.Date.today()
value_days = timedelta(days=value)
value_date = today - value_days
# convert the operator:
# book with age > value have a date < value_date
operator_map = {
'>': '<', '>=': '<=',
'<': '>', '<=': '>=',
}
new_op = operator_map.get(operator, operator)
return [('date_release', new_op, value_date)]
An Odoo restart, followed by a module upgrade, is needed to correctly activate these new additions.
The definition of a computed field is the same as that of a regular field, except that a compute attribute is added to specify the name of the method to use for its computation.
Their similarity can be deceptive, since computed fields are internally quite different from regular fields. Computed fields are dynamically calculated at runtime, and because of that, they are not stored in the database and so you cannot search or write on compute fields by default. You need to do some extra work in order to enable write and search support for compute fields. Let's see how to do it.
The computation function is dynamically calculated at runtime, but the ORM uses caching to avoid inefficiently recalculating it every time its value is accessed. So, it needs to know what other fields it depends on. It uses the @depends decorator to detect when its cached values should be invalidated and recalculated.
Ensure that the compute function always sets a value on the computed field. Otherwise, an error will be raised. This can happen when you have if conditions in your code that sometimes fail to set a value on the computed field. This can be tricky to debug.
Write support can be added by implementing the inverse function. This uses the value assigned to the computed field to update the origin fields. Of course, this only makes sense for simple calculations. Nevertheless, there are still cases where it can be useful. In our example, we make it possible to set the book release date by editing the Days Since Release computed field. The inverse attribute is optional; if you don't want to make the compute field editable, you can skip it.
It is also possible to make a non-stored computed field searchable by setting the search attribute to the method name (similar to compute and inverse). Like inverse, search is also optional; if you don't want to make the compute field searchable, you can skip it.
However, this method is not expected to implement the actual search. Instead, it receives the operator and value used to search on the field as parameters, and is expected to return a domain with the replacement search conditions to use. In our example, we translate a search of the Days Since Release field into an equivalent search condition on the Release Date field.
The optional store=True flag stores the field in the database. In this case, after being computed, the field values are stored in the database, and from there on, they are retrieved in the same way as regular fields, instead of being recomputed at runtime. Thanks to the @api.depends decorator, the ORM will know when these stored values need to be recomputed and updated. You can think of it as a persistent cache. It also has the advantage of making the field usable for search conditions, including sorting and grouping by operations. If you use store=True in your compute field, you no longer need to implement the search method because the field is stored in a database and you can search/sort based on the stored field.
The compute_sudo=True flag is to be used in cases in which the computations need to be done with elevated privileges. This might be the case when the computation needs to use data that may not be accessible to the end user.
Important note
The default value of compute_sudo is changed in Odoo v13. Prior to Odoo v13, the value of compute_sudo was False. But in v13, the default value of compute_sudo will be based on store attributes. If the value of the store attribute is True, then compute_sudo is True or it is False. However, you can always manually change it by explicitly putting compute_sudo in your field definition.
Odoo v13 introduced a new caching mechanism for ORM. Earlier, the cache was based on the environment, but now in Odoo v13, we have one global cache. So, if you have a computed field that depends on context values, then you may get incorrect values on occasion. To fix this issue, you need to use the @api.depends_context decorator. Refer to the following example:
@api.depends('price')
@api.depends_context('company_id')
def _compute_value(self):
company_id = self.env.context.get('company_id')
...
# other computation
You can see in the preceding example that our computation is using company_id from the context. By using company_id in the depends_context decorator, we are ensuring that the field value will be recomputed based on the value of company_id in the context.
When reading data from the server, Odoo clients can only get values for the fields that are available in the model and being queried. Client-side code, unlike server-side code, can't use dot notation to access data in the related tables.
However, these fields can be made available there by adding them as related fields. We will do this to make the publisher's city available in the Library Books model.
We will continue using the my_library add-on module from the previous recipe.
Edit the models/library_book.py file to add the new related field:
class LibraryBook(models.Model):
# ...
publisher_id = fields.Many2one(
'res.partner', string='Publisher')
# class LibraryBook(models.Model):
# ...
publisher_city = fields.Char(
'Publisher City',
related='publisher_id.city',
readonly=True)
Finally, we need to upgrade the add-on module for the new fields to be available in the model.
Related fields are just like regular fields, but they have an additional attribute, related, with a string for the separated chain of fields to traverse.
In our case, we access the publisher-related record through publisher_id, and then read its city field. We can also have longer chains, such as publisher_id.country_id.country_code.
Note that in this recipe, we set the related field as readonly. If we don't do that, the field will be writable, and the user may change its value. This will have the effect of changing the value of the city field of the related publisher. While this can be a useful side effect, caution needs to be exercised. All the books that are published by the same publisher will have their publisher_city field updated, which may not be what the user expects.
Related fields are, in fact, computed fields. They just provide a convenient shortcut syntax to read field values from related models. As a computed field, this means that the store attribute is also available. As a shortcut, they also have all the attributes from the referenced field, such as name, translatable, as required.
Additionally, they support a related_sudo flag similar to compute_sudo; when set to True, the field chain is traversed without checking the user access rights.
Using related fields in a create() method can affect performance, as the computation of these fields is delayed until the end of their creation. So, if you have a One2many relation, such as in sale.order and sale.order.line models, and you have a related field on the line model referring to a field on the order model, you should explicitly read the field on the order model during record creation, instead of using the related field shortcut, especially if there are a lot of lines.
With relational fields, we need to decide the relation's target model (or co-model) beforehand. However, sometimes, we may need to leave that decision to the user and first choose the model we want and then the record we want to link to.
With Odoo, this can be achieved using reference fields.
We will continue using the my_library add-on module from the previous recipe.
Edit the models/library_book.py file to add the new related field:
from odoo import models, fields, api
class LibraryBook(models.Model):
# ...
@api.model
def _referencable_models(self):
models = self.env['ir.model'].search([
('field_id.name', '=', 'message_ids')])
return [(x.model, x.name) for x in models]
ref_doc_id = fields.Reference(
selection='_referencable_models',
string='Reference Document')
Since we are changing the model's structure, a module upgrade is needed to activate these changes.
Reference fields are similar to many-to-one fields, except that they allow the user to select the model to link to.
The target model is selectable from a list that's provided by the selection attribute. The selection attribute must be a list of two element tuples, where the first is the model's internal identifier, and the second is a text description for it.
Here's an example:
[('res.users', 'User'), ('res.partner', 'Partner')]
However, rather than providing a fixed list, we can use most common models. For simplicity, we are using all the models that have the messaging feature. Using the _referencable_models method, we provided a model list dynamically.
Our recipe started by providing a function to browse all the model records that can be referenced to dynamically build a list that will be provided to the selection attribute. Although both forms are allowed, we declared the function name inside quotes, instead of directly referencing the function without quotes. This is more flexible, and it allows for the referenced function to be defined only later in the code, for example, which is something that is not possible when using a direct reference.
The function needs the @api.model decorator because it operates on the model level, and not on the record set level.
While this feature looks nice, it comes with a significant execution overhead. Displaying the reference fields for a large number of records (for instance, in a list view) can create heavy database loads as each value has to be looked up in a separate query. It is also unable to take advantage of database referential integrity, unlike regular relation fields.
One of the most important Odoo features is the ability of module add-ons to extend features that are defined in other module add-ons without having to edit the code of the original feature. This might be to add fields or methods, modify the existing fields, or extend the existing methods to perform additional logic.
According to the official documentation, Odoo provides three types of inheritance:
We will see each one of these in a separate recipe. In this recipe we will see Class inheritance (extension). It is used to add new fields or methods to existing models.
We will extend the built-in partner model res.partner to add it to a computed field with the authored book count. This involves adding a field and a method to an existing model.
We will continue using the my_library add-on module from the previous recipe.
We will be extending the built-in partner model. If you remembered, we have already inherited the res.parnter model in the Adding relational fields to a model recipe in this chapter. To keep the explanation as simple as possible, we will reuse the res.partner model in the models/library_book.py code file:
class ResPartner(models.Model):
_inherit = 'res.partner'
_order = 'name'
authored_book_ids = fields.Many2many(
'library.book', string='Authored Books')
count_books = fields.Integer( 'Number of Authored Books',
compute='_compute_count_books' )
# ...
from odoo import api # if not already imported
# class ResPartner(models.Model):
# ...
@api.depends('authored_book_ids')
def _compute_count_books(self):
for r in self:
r.count_books = len(r.authored_book_ids)
Finally, we need to upgrade the add-on module for the modifications to take effect.
When a model class is defined with the _inherit attribute, it adds modifications to the inherited model, rather than replacing it.
This means that fields defined in the inheriting class are added or changed on the parent model. At the database layer, the ORM is adding fields to the same database table.
Fields are also incrementally modified. This means that if the field already exists in the superclass, only the attributes declared in the inherited class are modified; the other ones are kept as they are in the parent class.
Methods defined in the inheriting class replace methods in the parent class. If you don't invoke the parent method with the super call, in that case, the parent's version of the method will not be executed and we will lose the features. So, whenever you add a new logic by inheriting existing methods, you should include a statement with super to call its version in the parent class. This is discussed in more detail in Chapter 5, Basic Server-Side Development.
This recipe will add new fields to the existing model. If you also want to add these new fields to existing views (the user interface), refer to the Changing existing views – view inheritance recipe in Chapter 9, Backend Views.
We have seen class inheritance (extension) in the previous recipe. Now we will see prototype inheritance, which is used to copy the entire definition of the existing model. In this recipe, we will make a copy of the library.book model.
We will continue using the my_library add-on module from the previous recipe.
Prototype inheritance is executed by using the _name and _inherit class attributes at the same time. Perform the following steps to generate a copy of the library.book model:
from odoo import models, fields, api
class LibraryBookCopy(models.Model):
_name = "library.book.copy"
_inherit = "library.book"
_description = "Library Book's Copy"
from . import library_book
from . import library_book_categ
from . import library_book_copy
Finally, we need to upgrade the add-on module for the modifications to take effect. To check the new model's definition, go to the Settings | Technical | Database Structure | Models menu. You will see a new entry for the library.book.copy model here.
Tip
In order to see menus and views for the new model, you need to add the XML definition of views and menus. To learn more about views and menus, refer to the Adding menu items and views recipe in Chapter 3, Creating Odoo Add-On Modules.
By using _name with the _inherit class attribute at the same time, you can copy the definition of the model. When you use both attributes in the model, Odoo will copy the model definition of _inherit and create a new model with the _name attribute.
In our example, Odoo will copy the definition of the library.book model and create a new model, library.book.copy.The new library.book.copy model has its own database table with its own data that is totally independent from the library.book parent model. Since it still inherits from the partner model, any subsequent modifications to it will also affect the new model.
Prototype inheritance copies all the properties of the parent class. It copies fields, attributes, and methods. If you want to modify them in the child class, you can simply do so by adding a new definition to the child class. For example, the library.book model has the _name_get method. If you want to use a different version of _name_get in the child, you need to redefine the method in the library.book.copy model.
Warning
Prototype inheritance does not work if you use the same model name in the _inherit and _name attributes. If you do use the same model name in the _inherit and _name attributes, it will just behave like a normal extension inheritance.
In the official documentation, this is called prototype inheritance, but in practice, it is rarely used. The reason for this is that delegation inheritance usually answers to that need in a more efficient way, without the need to duplicate data structures. For more information on this, you can refer to the next recipe, Using delegation inheritance to copy features to another model.
The third type of inheritance is Delegation inheritance. Instead of _inherit, it uses the _inherits class attribute. There are cases where, rather than modifying an existing model, we want to create a new model based on an existing one to use the features it already has. We can copy a model's definitions with prototype inheritance, but this will generate duplicate data structures. If you want to copy a model's definitions without duplicating data structures, then the answer lies in Odoo's delegation inheritance, which uses the _inherits model attribute (note the additional s).
Traditional inheritance is quite different from the concept in object-oriented programming. Delegation inheritance, in turn, is similar, in that a new model can be created to include the features from a parent model. It also supports polymorphic inheritance, where we inherit from two or more other models.
We have a library with books. It's about time our library also has members. For a library member, we need all the identification and address data that's found in the partner model, and we also want it to retain some information pertaining to membership: a start date, a termination date, and a card number.
Adding those fields to the partner model is not the best solution, since they will not be used for partners that are not members. It would be great to extend the partner model to a new model with some additional fields.
We will continue using the my_library add-on module from the previous recipe.
The new library member model should be in its own Python code file, but to keep the explanation as simple as possible, we will reuse the models/library_book.py file:
class LibraryMember(models.Model):
_name = 'library.member'
_inherits = {'res.partner': 'partner_id'}
partner_id = fields.Many2one(
'res.partner',
ondelete='cascade')
# class LibraryMember(models.Model):
# ...
date_start = fields.Date('Member Since')
date_end = fields.Date('Termination Date')
member_number = fields.Char()
date_of_birth = fields.Date('Date of birth')
Now, we should upgrade the add-on module to activate the changes.
The _inherits model attribute sets the parent models that we want to inherit from. In this case, we just have one—res.partner. Its value is a key-value dictionary, where the keys are the inherited models, and the values are the field names that were used to link to them. These are Many2one fields that we must also define in the model. In our example, partner_id is the field that will be used to link with the Partner parent model.
To better understand how this works, let's look at what happens at a database level when we create a new member:
The member record is automatically linked to a new partner record. It's just a many-to-one relation, but the delegation mechanism adds some magic so that the partner's fields are seen as if they belong to the member record, and a new partner record is also automatically created with the new member.
You may be interested in knowing that this automatically created partner record has nothing special about it. It's a regular partner, and if you browse the partner model, you will be able to find that record (without the additional member data, of course). All members are partners, but only some partners are also members.
So, what happens if you delete a partner record that is also a member? You decide by choosing the ondelete value for the relation field. For partner_id, we used cascade. This means that deleting the partner will also delete the corresponding member. We could have used the more conservative setting, restrict, to prohibit deleting the partner while it has a linked member. In this case, only deleting the member will work.
It's important to note that delegation inheritance only works for fields, and not for methods. So, if the partner model has a do_something() method, the members model will not automatically inherit it.
There is a shortcut for this inheritance delegation. Instead of creating an _inherits dictionary, you can use the delegate=True attribute in the Many2one field definition. This will work exactly like the _inherits option. The main advantage is that this is simpler. In the given example, we have performed the same inheritance delegation as in the previous one, but in this case, instead of creating an _inherits dictionary, we have used the delegate=True option in the partner_id field:
class LibraryMember(models.Model):
_name = 'library.member'
partner_id = fields.Many2one('res.partner', ondelete='cascade', delegate=True)
date_start = fields.Date('Member Since')
date_end = fields.Date('Termination Date')
member_number = fields.Char()
date_of_birth = fields.Date('Date of birth')
A noteworthy case of delegation inheritance is the users model, res.users. It inherits from partners (res.partner). This means that some of the fields that you can see on the user are actually stored in the partner model (notably, the name field). When a new user is created, we also get a new, automatically created partner.
We should also mention that traditional inheritance with _inherit can also copy features into a new model, although in a less efficient way. This was discussed in the Adding features to a model using inheritance recipe.
Sometimes, there is a particular feature that we want to be able to add to several different models. Repeating the same code in different files is a bad programming practice; it would be better to implement it once and reuse it.
Abstract models allow us to create a generic model that implements some features that can then be inherited by regular models in order to make that feature available.
As an example, we will implement a simple archive feature. It adds the active field to the model (if it doesn't exist already) and makes an archive method available to toggle the active flag. This works because active is a magic field. If present in a model by default, the records with active=False will be filtered out from the queries.
We will then add it to the Library Books model.
We will continue using the my_library add-on module from the previous recipe.
The archive feature certainly deserves its own add-on module, or at least its own Python code file. However, to keep the explanation as simple as possible, we will cram it into the models/library_book.py file:
class BaseArchive(models.AbstractModel):
_name = 'base.archive'
active = fields.Boolean(default=True)
def do_archive(self):
for record in self:
record.active = not record.active
class LibraryBook(models.Model):
_name = 'library.book'
_inherit = ['base.archive']
# ...
An upgrade of the add-on module is required in order for the changes to be activated.
An abstract model is created by a class based on models.AbstractModel, instead of the usual models.Model. It has all the attributes and capabilities of regular models; the difference is that the ORM will not create an actual representation for it in the database. This means that it can't have any data stored in it. It only serves as a template for a reusable feature that is to be added to regular models.
Our archive abstract model is quite simple. It just adds the active field and a method to toggle the value of the active flag, which we expect to be used later, via a button on the user interface.
When a model class is defined with the _inherit attribute, it inherits the attribute methods of those classes, and the attribute methods that are defined in the current class add modifications to those inherited features.
The mechanism at play here is the same as that of a regular model extension (as per the Adding features to a model using inheritance recipe). You may have noticed that _inherit uses a list of model identifiers instead of a string with one model identifier. In fact, _inherit can have both forms. Using the list form allows us to inherit from multiple (usually Abstract) classes. In this case, we are inheriting just one, so a text string would be fine. A list was used instead, for illustration purposes.
A noteworthy built-in abstract model is mail.thread, which is provided by the mail (Discuss) add-on module. On models, it enables the discussion features that power the message wall that's seen at the bottom of many forms.
Other than AbstractModel, a third model type is available: models.TransientModel. This has a database representation like models.Model, but the records that are created there are supposed to be temporary and regularly purged by a server-scheduled job. Other than that, transient models work just like regular models.
models.TransientModel is useful for more complex user interactions, known as wizards. The wizard is used to request inputs from the user. In Chapter 8, Advanced Server-Side Development Techniques, we explore how to use these for advanced user interaction.
3.23.130.108