Chapter 4: Application Models

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:

  • Defining the model representation and order
  • Adding data fields to a model
  • Using a float field with configurable precision
  • Adding a monetary field to a model
  • Adding relational fields to a model
  • Adding a hierarchy to a model
  • Adding constraint validations to a model
  • Adding computed fields to a model
  • Exposing related fields stored in other models
  • Adding dynamic relations using reference fields
  • Adding features to a model using inheritance
  • Using abstract models for reusable model features
  • Using delegation inheritance to copy features to another model

Technical requirements

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.

Defining the model representation and order

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:

  • _rac_name is used to set the field that's used as a representation or title for the records.
  • The other one is _order, which is used to set the order in which the records are presented.

Getting ready

This recipe assumes that you have an instance ready with the my_library module, as described in Chapter 3, Creating Odoo Add-On Modules.

How to do it...

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:

  1. To add a user-friendly title to the model, add the following code:

    _description = 'Library Book'

  2. To sort the records first (from the newest to the oldest, and then by title), add the following code:

    _order = 'date_release desc, name'

  3. To use the short_name field as the record representation, add the following code:

    _rec_name = 'short_name'

    short_name = fields.Char('Short Title', required=True)

  4. Add the short_name field in the form view so that it can display the new field in the view:

    <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:

Figure 4.1 – Option to update the module

Figure 4.1 – Option to update the module

Alternatively, you can also use the -u my_library command in the command line.

How it works...

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.

There's more...

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).

Adding data fields to a model

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.

Getting ready

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.

How to do it...

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:

  1. Use the minimal syntax to add fields to the Library Books model:

    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,

        )

  2. We have added new fields to the model. We still need to add these fields to the form view in order to reflect these changes in the user interface. Refer to the following code to add fields in the form view:

    <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)

How it works...

Fields are added to models by defining an attribute in their Python classes. The non-relational field types that are available are as follows:

  • Char is used for string values.
  • Text is used for multiline string values.
  • Selection is used for selection lists. This has a list of values and description pairs. The value that is selected is what gets stored in the database, and it can be a string or an integer. The description is automatically translatable.

    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.

  • Html is similar to the text field, but is expected to store rich text in an HTML format.
  • Binary fields store binary files, such as images or documents.
  • Boolean stores True/False values.
  • Date stores date values. They are stored in the database as dates. The ORM handles them in the form of Python date objects. You can use fields.Date.today() to set the current date as a default value in the date field.
  • Datetime is used for datetime values. They are stored in the database in a naive datetime, in UTC time. The ORM handles them in the form of Python datetime objects. You can use fields.Date.now() to set the current time as a default value in the datetime field.
  • The Integer fields need no further explanation.
  • The Float fields store numeric values. Their precision can optionally be defined with a total number of digits and decimal digit pairs.
  • Monetary can store an amount in a certain currency. This will also be explained in the Adding a monetary field recipe in this chapter.

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:

  • string is the field's title, and is used in UI view labels. It is optional. If not set, a label will be derived from the field name by adding a title case and replacing the underscores with spaces.
  • translate, when set to True, makes the field translatable. It can hold a different value, depending on the user interface language.
  • default is the default value. It can also be a function that is used to calculate the default value; for example, default=_compute_default, where _compute_default is a method that was defined on the model before the field definition.
  • help is an explanation text that's displayed in the UI tooltips.
  • groups makes the field available only to some security groups. It is a string containing a comma-separated list of XML IDs for security groups. This is addressed in more detail in Chapter 10, Security Access.
  • states allows the user interface to dynamically set the value for the readonly, required, and invisible attributes, depending on the value of the state field. Therefore, it requires a state field to exist and be used in the form view (even if it is invisible). The name of the state attribute is hardcoded in Odoo and cannot be changed.
  • copy flags whether the field value is copied when the record is duplicated. By default, it is True for non-relational and Many2one fields, and False for One2many and computed fields.
  • index, when set to True, creates a database index for the field, which sometimes allows for faster searches. It replaces the deprecated select=1 attribute.
  • The readonly flag makes the field read-only by default in the user interface.
  • The required flag makes the field mandatory by default in the user interface.

    The various whitelists that are mentioned here are defined in odoo/tools/mail.py.

  • The company_dependent flag makes the field store different values for each company. It replaces the deprecated Property field type.
  • group_operator is an aggregate function used to display results in the group by mode. Possible values for this attribute include count, count_distinct, array_agg, bool_and, bool_or, max, min, avg, and sum. Integer, float, and monetary field types have the default value sum for this attribute.
  • The sanitize flag is used by HTML fields and strips its content from potentially insecure tags. Using this performs a global cleanup of the input.

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:

  • sanitize_tags=True, to remove tags that are not part of a whitelist (this is the default)
  • sanitize_attributes=True, to remove attributes of the tags that are not part of a whitelist
  • sanitize_style=True, to remove style properties that are not part of a whitelist
  • strip_style=True, to remove all style elements
  • strip_class=True, to remove the class attributes

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.

There's more...

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:

  • fields.Date.to_date(string_value) parses the string into a date object.
  • fields.Date.to_string(date_value) converts the python Date object as a string.
  • fields.Date.today() returns the current day in a string format. This is appropriate to use for default values.
  • fields.Date.context_today(record, timestamp) returns the day of the timestamp (or the current day, if timestamp is omitted) in a string format, according to the time zone of the record's (or record set's) context.

For Datetime, we have the following:

  • fields.Datetime.to_datetime(string_value) parses the string into a datetime object.
  • fields.Datetime.to_string(datetime_value) converts the datetime object to a string.
  • fields.Datetime.now() returns the current day and time in a string format. This is appropriate to use for default values.
  • fields.Datetime.context_timestamp(record, timestamp) converts a timestamp-naive datetime object into a time zone-aware datetime object using the time zone in the context of record. This is not suitable for default values, but can be used for instances when you're sending data to an external system.

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:

  • create_date is the record creation timestamp.
  • create_uid is the user who created the record.
  • write_date is the last recorded timestamp edit.
  • write_uid is the user who last edited the record.

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.

Using a float field with configurable precision

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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

Perform the following steps to apply dynamic decimal precision to the model's cost_price field:

  1. Activate Developer Mode from the link in the Settings menu (refer to the Activating the Odoo developer tools recipe in Chapter 1, Installing the Odoo Development Environment). This will enable the Settings | Technical menu.
  2. Access the decimal precision configurations. To do this, open the Settings top menu and select Technical | Database Structure | Decimal Accuracy. We should see a list of the currently defined settings.
  3. Add a new configuration, setting Usage to Book Price, and choosing the Digits precision:
    Figure 4.2 – Creating new decimal precision

    Figure 4.2 – Creating new decimal precision

  4. To add the model field using this decimal precision setting, edit the models/library_book.py file by adding the following code:

    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"/>.

How it works...

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')).

Adding a monetary field to a model

Odoo has special support for monetary values related to a currency. Let's see how we can use this in a model.

Getting ready

We will continue to use the my_library add-on module from the previous recipe.

How to do it...

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:

  1. Add the field to store the currency that is to be used:

    class LibraryBook(models.Model):

        # ...

        currency_id = fields.Many2one(

            'res.currency', string='Currency')

  2. Add the monetary field to store the amount:

    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:

Figure 4.3 – Currency symbol in the monetary field

Figure 4.3 – Currency symbol in the monetary field

How it works...

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).

Adding relational fields to a model

Relations between Odoo models are represented by relational fields. There are three different types of relations:

  • many-to-one, commonly abbreviated as m2o
  • one-to-many, commonly abbreviated as o2m
  • many-to-many, commonly abbreviated as m2m

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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

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:

  1. Add the many-to-one field for the book's publisher to Library Books:

    class LibraryBook(models.Model):

        # ...

        publisher_id = fields.Many2one(

            'res.partner', string='Publisher',

            # optional:

            ondelete='set null',

            context={},

            domain=[],

            )

  2. To add the one-to-many field for a publisher's books, we need to extend the partner model. For simplicity, we will add that to the same Python file:

    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.

  3. We've already created the many-to-many relation between books and authors, but let's revisit it:

    class LibraryBook(models.Model):

        # ...

        author_ids = fields.Many2many(

            'res.partner', string='Authors')

  4. The same relation, but from authors to books, should be added to the partner model:

    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.

How it works...

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:

  • context adds variables to the client context when clicking through the field to the related record's view. We can, for example, use it to set default values for new records that are created through that view.
  • domain is a search filter that's used to limit the list of related records that are available.

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.

There's more...

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:

  • comodel_name: This is the target model identifier and is mandatory for all relational fields, but it can be defined position-wise, without the keyword.
  • inverse_name: This only applies to One2many and is the field name in the target model for the inverse Many2one relation.
  • limit: This applies to One2many and Many2many, and sets an optional limit in terms of the number of records to read that are used at the user interface level.

The Many2many field attributes are as follows:

  • comodel_name: This is the same as it is for the One2many field.
  • relation: This is the name to use for the table supporting the relation, overriding the automatically defined name.
  • column1: This is the name for the Many2one field in the relational table linking to this model.
  • column2: This is the name for the Many2one field in the relational table linking to comodel.

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:

  • One is the case where we need more than one Many2many relations between the same two models. For this to be possible, we must provide ourselves with the relation table name for the second relation, which must be different from the first relation.
  • The other case is when the database names of the related tables are long enough for the automatically generated relation name to exceed the 63-character PostgreSQL limit for database object names.

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.

Adding a hierarchy to a model

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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

We will add a new Python file, models/library_book_categ.py, for the category tree, as follows:

  1. To load the new Python code file, add the following line to models/__init__.py:

    from . import library_book_categ

  2. To create the Book Category model with the parent and child relations, create the models/library_book_categ.py file with the following code:

    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')

  3. To enable the special hierarchy support, also add the following code:

    _parent_store = True

    _parent_name = "parent_id" # optional if field is 'parent_id'

    parent_path = fields.Char(index=True)

  4. To add a check preventing looping relations, add the following line to the model:

    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.')

  5. Now, we need to assign a category to a book. To do this, we will add a new many2one field to the library.book model:

    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.

How it works...

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.

There's more...

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.

Adding constraint validations to a model

Models can have validations preventing them from entering undesired conditions.

Odoo supports two different types of constraints:

  • The ones checked at the database level
  • The ones checked at the server level

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.

Getting ready

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')

How to do it...

We will edit the LibraryBook class in the models/library_book.py Python file:

  1. To create the database constraint, add a model attribute:

    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')

            ]

  2. To create the Python code constraint, add a model method:

    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.

How it works...

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:

  • A suffix to use for the constraint identifier. In our example, we used name_uniq, and the resulting constraint name is library_book_name_uniq.
  • The SQL to use in the PostgreSQL instruction to alter or create the database table.
  • A message to report to the user when the constraint is violated.

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.

There's more...

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.

Adding computed fields to a model

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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

We will edit the models/library_book.py code file to add a new field and the methods supporting its logic:

  1. Start by adding the new field to the Library Books model:

    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

        )

  2. Next, add the method with the value computation logic:

    # ...

    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

  3. To add the method and implement the logic to write on the computed field, use the following code:

    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

  4. To implement the logic that will allow you to search in the computed field, use the following code:

    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.

How it works...

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.

There's more...

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.

Exposing related fields stored in other models

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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

Edit the models/library_book.py file to add the new related field:

  1. Ensure that we have a field for the book publisher:

    class LibraryBook(models.Model):

        # ...

        publisher_id = fields.Many2one(

            'res.partner', string='Publisher')

  2. Now, add the related field for the publisher's city:

    # 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.

How it works...

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.

There's more...

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.

Adding dynamic relations using reference fields

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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

Edit the models/library_book.py file to add the new related field:

  1. First, we need to add a helper method to dynamically build a list of selectable target models:

    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]

  2. Then, we need to add the reference field and use the previous function to provide a list of selectable 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.

How it works...

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.

Adding features to a model using inheritance

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:

  • Class inheritance (extension)
  • Prototype inheritance
  • Delegation 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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

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:

  1. First, we will ensure that the authored_book_ids inverse relation is in the partner model and add the computed field:

    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' )

  2. Next, add the method that's needed to compute the book count:

    # ...

    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.

How it works...

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.

Copy model definition using inheritance

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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

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:

  1. Add new file called library_book_copy.py to the /my_library/models/ directory.
  2. Add the following content to the in library_book_copy.py file:

    from odoo import models, fields, api

    class LibraryBookCopy(models.Model):

        _name = "library.book.copy"

        _inherit = "library.book"

        _description = "Library Book's Copy"

  3. Import a new file reference into the /my_library/models/__init__.py file. Following the changes, your __init__.py file will look like this:

    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.

How it works...

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.

There's more…

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.

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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

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:

  1. Add the new model, inheriting from res.partner:

    class LibraryMember(models.Model):

        _name = 'library.member'

        _inherits = {'res.partner': 'partner_id'}

        partner_id = fields.Many2one(

            'res.partner',

            ondelete='cascade')

  2. Next, we will add the fields that are specific to library members:

    # 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.

How it works...

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:

  • A new record is created in the res_partner table.
  • A new record is created in the library_member table.
  • The partner_id field of the library_member table is set to the ID of the res_partner record that is created for it.

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's more...

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.

Using abstract models for reusable model features

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.

Getting ready

We will continue using the my_library add-on module from the previous recipe.

How to do it...

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:

  1. Add the abstract model for the archive feature. It must be defined in the Library Book model, where it will be used:

    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

  2. Now, we will edit the Library Book model to inherit the archive model:

    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.

How it works...

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.

There's more...

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.23.130.108