Server Framework 101
Server Framework 101
Welcome to the Server framework 101 tutorial! If you reached this page that means you
are interested in the development of your own Odoo module. It might also mean that
you recently joined the Odoo company for a rather technical position. In any case, your
journey to the technical side of Odoo starts here.
The goal of this tutorial is for you to get an insight of the most important parts of the
Odoo development framework while developing your own Odoo module to manage
real estate assets. The chapters should be followed in their given order since they cover
the development of a new Odoo application from scratch in an incremental way. In
other words, each chapter depends on the previous one.
Important
Before going further, make sure you have prepared your development environment with
the setup guide.
Ready? Let’s get started!
The presentation tier is a combination of HTML5, JavaScript and CSS. The logic tier is
exclusively written in Python, while the data tier only supports PostgreSQL as an RDBMS.
Depending on the scope of your module, Odoo development can be done in any of these tiers.
Therefore, before going any further, it may be a good idea to refresh your memory if you don’t
have an intermediate level in these topics.
In order to go through this tutorial, you will need a very basic knowledge of HTML and an
intermediate level of Python. Advanced topics will require more knowledge in the other subjects.
There are plenty of tutorials freely accessible, so we cannot recommend one over another since it
depends on your background.
Note
Since version 15.0, Odoo is actively transitioning to using its own in-house developed OWL
framework as part of its presentation tier. The legacy JavaScript framework is still supported but
will be deprecated over time. This will be discussed further in advanced topics.
Odoo modules
Both server and client extensions are packaged as modules which are optionally loaded in
a database. A module is a collection of functions and data that target a single purpose.
Odoo modules can either add brand new business logic to an Odoo system or alter and extend
existing business logic. One module can be created to add your country’s accounting rules to
Odoo’s generic accounting support, while a different module can add support for real-time
visualisation of a bus fleet.
Terminology: developers group their business features in Odoo modules. The main user-facing
modules are flagged and exposed as Apps, but a majority of the modules aren’t
Apps. Modules may also be referred to as addons and the directories where the Odoo server finds
them form the addons_path.
Composition of a module
An Odoo module can contain a number of elements:
Business objects
A business object (e.g. an invoice) is declared as a Python class. The fields defined in
these classes are automatically mapped to database columns thanks to the ORM layer.
Object views
Define UI display
Data files
Module structure
Each module is a directory within a module directory. Module directories are specified by using
the --addons-path option.
When an Odoo module includes business objects (i.e. Python files), they are organized as
a Python package with a __init__.py file. This file contains import instructions for various
Python files in the module.
module
├── models
│ ├── *.py
│ └── __init__.py
├── data
│ └── *.xml
├── __init__.py
└── __manifest__.py
Odoo Editions
Odoo is available in two versions: Odoo Enterprise (licensed & shared sources) and Odoo
Community (open-source). In addition to services such as support or upgrades, the Enterprise
version provides extra functionalities to Odoo. From a technical point-of-view, these
functionalities are simply new modules installed on top of the modules provided by the
Community version.
The top area of the form view summarizes important information for the property, such as the
name, the property type, the postcode and so on. The first tab contains information describing the
property: bedrooms, living area, garage, garden…
The second tab lists the offers for the property. We can see here that potential buyers can make
offers above or below the expected selling price. It is up to the seller to accept an offer.
Here is a quick video showing the workflow of the module.
Note
Goal: the goal of this section is to have Odoo recognize our new module, which will be an empty
shell for now. It will be listed in the Apps:
The first step of module creation is to create its directory. In the tutorials directory, add a new
directory estate.
A module must contain at least 2 files: the __manifest__.py file and a __init__.py file.
The __init__.py file can remain empty for now and we’ll come back to it in the next chapter.
On the other hand, the __manifest__.py file must describe our module and cannot remain
empty. Its only required field is the name, but it usually contains much more information.
Take a look at the CRM file as an example. In addition to providing the description of the
module (name, category, summary, website…), it lists its dependencies (depends). A
dependency means that the Odoo framework will ensure that these modules are installed before
our module is installed. Moreover, if one of these dependencies is uninstalled, then our module
and any other that depends on it will also be uninstalled. Think about your favorite Linux
distribution package manager (apt, dnf, pacman…): Odoo works in the same way.
Exercise
Create the required addon files.
• /home/$USER/src/tutorials/estate/__init__.py
• /home/$USER/src/tutorials/estate/__manifest__.py
The __manifest__.py file should only define the name and the dependencies of our modules.
The only necessary framework module for now is base.
Restart the Odoo server and go to Apps. Click on Update Apps List, search for estate and…
tadaaa, your module appears! Did it not appear? Maybe try removing the default ‘Apps’ filter ;-)
Warning
Remember to enable the developer mode as explained in the previous chapter. You won’t see
the Update Apps List button otherwise.
Exercise
Make your module an ‘App’.
Add the appropriate key to your __manifest__.py so that the module appears when the ‘Apps’
filter is on.
You can even install the module! But obviously it’s an empty shell, so no menu will appear.
Before moving forward in the exercise, make sure the estate module is installed, i.e. it must
appear as ‘Installed’ in the Apps list.
Warning
Do not use mutable global variables.
A single Odoo instance can run several databases in parallel within the same python process.
Distinct modules might be installed on each of these databases, therefore we cannot rely on
global variables that would be updated depending on installed modules.
Object-Relational Mapping
Reference: the documentation related to this topic can be found in the Models API.
Note
Goal: at the end of this section, the table estate_property should be created:
$ psql -d rd-demo
rd-demo=# SELECT COUNT(*) FROM estate_property;
count
-------
0
(1 row)
A key component of Odoo is the ORM layer. This layer avoids having to manually write
most SQL and provides extensibility and security services2.
Business objects are declared as Python classes extending Model, which integrates them into the
automated persistence system.
Models can be configured by setting attributes in their definition. The most important attribute
is _name, which is required and defines the name for the model in the Odoo system. Here is a
minimum definition of a model:
class TestModel(models.Model):
_name = "test_model"
This definition is enough for the ORM to generate a database table named test_model. By
convention all models are located in a models directory and each model is defined in its own
Python file.
Take a look at how the crm_recurring_plan table is defined and how the corresponding Python
file is imported:
When the files are created, add a minimum definition for the estate.property model.
Any modification of the Python files requires a restart of the Odoo server. When we restart the
server, we will add the parameters -d and -u:
...
WARNING rd-demo odoo.models: The model estate.property has no _description
...
WARNING rd-demo odoo.modules.loading: The model estate.property has no access rules, consider
adding one...
...
If this is the case, then you should be good! To be sure, double check with psql as demonstrated
in the Goal.
Exercise
Add a description.
Fields are used to define what the model can store and where they are stored. Fields are defined
as attributes in the model class:
class TestModel(models.Model):
_name = "test_model"
_description = "Test Model"
name = fields.Char()
The name field is a Char which will be represented as a Python unicode str and a SQL VARCHAR.
Types
Note
Goal: at the end of this section, several basic fields should have been added to the
table estate_property:
$ psql -d rd-demo
rd-demo=# \d estate_property;
Table "public.estate_property"
Column | Type | Collation | Nullable | Default
--------------------+-----------------------------+-----------+----------+---------------------------------------------
id | integer | | not null | nextval('estate_property_id_seq'::regclass)
create_uid | integer | | |
create_date | timestamp without time zone | | |
write_uid | integer | | |
write_date | timestamp without time zone | | |
name | character varying | | |
description | text | | |
postcode | character varying | | |
date_availability | date | | |
expected_price | double precision | | |
selling_price | double precision | | |
bedrooms | integer | | |
living_area | integer | | |
facades | integer | | |
garage | boolean | | |
garden | boolean | | |
garden_area | integer | | |
garden_orientation | character varying | | |
Indexes:
"estate_property_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"estate_property_create_uid_fkey" FOREIGN KEY (create_uid) REFERENCES res_users(id) ON DELETE
SET NULL
"estate_property_write_uid_fkey" FOREIGN KEY (write_uid) REFERENCES res_users(id) ON DELETE
SET NULL
There are two broad categories of fields: ‘simple’ fields, which are atomic values stored directly
in the model’s table, and ‘relational’ fields, which link records (of the same or different models).
Simple field examples are Boolean, Float, Char, Text, Date and Selection.
Exercise
Add basic fields to the Real Estate Property table.
Field Type
name Char
description Text
postcode Char
date_availability Date
expected_price Float
selling_price Float
Field Type
bedrooms Integer
living_area Integer
facades Integer
garage Boolean
garden Boolean
garden_area Integer
garden_orientation Selection
The garden_orientation field must have 4 possible values: ‘North’, ‘South’, ‘East’ and ‘West’.
The selection list is defined as a list of tuples, see here for an example.
When the fields are added to the model, restart the server with -u estate
Common Attributes
Note
Goal: at the end of this section, the columns name and expected_price should be not nullable in
the table estate_property:
rd-demo=# \d estate_property;
Table "public.estate_property"
Column | Type | Collation | Nullable | Default
--------------------+-----------------------------+-----------+----------+---------------------------------------------
...
name | character varying | | not null |
...
expected_price | double precision | | not null |
...
Much like the model itself, fields can be configured by passing configuration attributes as
parameters:
name = fields.Char(required=True)
Some attributes are available on all fields, here are the most common ones:
If True, the field can not be empty. It must either have a default value or always be given
a value when creating a record.
help (str, default: '')
Field Attribute
name required
expected_price required
After restarting the server, both fields should be not nullable.
Automatic Fields
Reference: the documentation related to this topic can be found in Automatic fields.
You may have noticed your model has a few fields you never defined. Odoo creates a few fields
in all models1. These fields are managed by the system and can’t be written to, but they can be
read if useful or necessary:
id (Id)
writing raw SQL queries is possible, but requires caution as this bypasses all Odoo
authentication and security mechanisms.
"id","country_id:id","name","code"
state_au_1,au,"Australian Capital Territory","ACT"
state_au_2,au,"New South Wales","NSW"
state_au_3,au,"Northern Territory","NT"
state_au_4,au,"Queensland","QLD"
...
• id is an external identifier. It can be used to refer to the record (without knowing its in-
database identifier).
• country_id:id refers to the country by using its external identifier.
• name is the name of the state.
• code is the code of the state.
These three fields are defined in the res.country.state model.
By convention, a file importing data is located in the data folder of a module. When the data is
related to security, it is located in the security folder. When the data is related to views and
actions (we will cover this later), it is located in the views folder. Additionally, all of these files
must be declared in the data list within the __manifest__.py file. Our example file is defined in
the manifest of the base module.
Also note that the content of the data files is only loaded when a module is installed or updated.
Warning
The data files are sequentially loaded following their order in the __manifest__.py file. This
means that if data A refers to data B, you must make sure that B is loaded before A.
In the case of the country states, you will note that the list of countries is loaded before the list of
country states. This is because the states refer to the countries.
Why is all this important for security? Because all the security configuration of a model is loaded
through data files, as we’ll see in the next section.
Access Rights
Reference: the documentation related to this topic can be found in Access Rights.
Note
Goal: at the end of this section, the following warning should not appear anymore:
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_test_model,access_test_model,model_test_model,base.group_user,1,0,0,0
• id is an external identifier.
• name is the name of the ir.model.access.
• model_id/id refers to the model which the access right applies to. The standard way to
refer to the model is model_<model_name>, where <model_name> is the _name of the
model with the . replaced by _. Seems cumbersome? Indeed it is…
• group_id/id refers to the group which the access right applies to.
• perm_read,perm_write,perm_create,perm_unlink: read, write, create and unlink
permissions
Exercise
Add access rights.
Give the read, write, create and unlink permissions to the group base.group_user.
Tip: the warning message in the log gives you most of the solution ;-)
Restart the server and the warning message should have disappeared!
At the end of this chapter, we will have created a couple of menus in order to access a default list
and form view.
In Chapter 4: Security - A Brief Introduction, we added data through a CSV file. The CSV
format is convenient when the data to load has a simple format. When the format is more
complex (e.g. load the structure of a view or an email template), we use the XML format. For
example, this help field contains HTML tags. While it would be possible to load such data
through a CSV file, it is more convenient to use an XML file.
The XML files must be added to the same folders as the CSV files and defined similarly in
the __manifest__.py. The content of the data files is also sequentially loaded when a module is
installed or updated, therefore all remarks made for CSV files hold true for XML files. When the
data is linked to views, we add them to the views folder.
In this chapter, we will load our first action and menus through an XML file. Actions and menus
are standard records in the database.
Note
When performance is important, the CSV format is preferred over the XML format. This is the
case in Odoo where loading a CSV file is faster than loading an XML file.
In Odoo, the user interface (actions, menus and views) is largely defined by creating and
composing records defined in an XML file. A common pattern is Menu > Action > View. To
access records the user navigates through several menu levels; the deepest level is an action
which triggers the opening of a list of the records.
Actions
Reference: the documentation related to this topic can be found in Actions.
Note
Goal: at the end of this section, an action should be loaded in the system. We won’t see anything
yet in the UI, but the file should be loaded in the log:
Exercise
Add an action.
Menus
Reference: the documentation related to this topic can be found in Shortcuts.
Note
Goal: at the end of this section, three menus should be created and the default view is displayed:
To reduce the complexity in declaring a menu (ir.ui.menu) and connecting it to the
corresponding action, we can use the <menuitem> shortcut .
However, menus always follow an architecture, and in practice there are three levels of menus:
1. The root menu, which is displayed in the App switcher (the Odoo Community App
switcher is a dropdown menu)
2. The first level menu, displayed in the top bar
3. The action menus
The easiest way to define the structure is to create it in the XML file. A basic structure for
our test_model_action is:
Exercise
Add menus.
Create the three levels of menus for the estate.property action created in the previous
exercise. Refer to the Goal of this section for the expected result.
Restart the server and refresh the browser1. You should now see the menus, and you’ll even be
able to create your first real estate property advertisement!
The reserved fields active and state are added to the estate.property model.
So far we have only used the generic view for our real estate property advertisements, but in
most cases we want to fine tune the view. There are many fine-tunings possible in Odoo, but
usually the first step is to make sure that:
• The selling price should be read-only (it will be automatically filled in later)
• The availability date and the selling price should not be copied when duplicating a record
• The default number of bedrooms should be 2
• The default availability date should be in 3 months
Some New Attributes
Before moving further with the view design, let’s step back to our model definition. We saw that
some attributes, such as required=True, impact the table schema in the database. Other
attributes will impact the view or provide default values.
Exercise
Add new attributes to the fields.
Default Values
Any field can be given a default value. In the field definition, add the
option default=X where X is either a Python literal value (boolean, integer, float, string) or a
function taking a model and returning a value:
name = fields.Char(default="Unknown")
last_seen = fields.Datetime("Last Seen", default=fields.Datetime.now)
The name field will have the value ‘Unknown’ by default while the last_seen field will be set as
the current time.
Exercise
Set default values.
Reserved Fields
Reference: the documentation related to this topic can be found in Reserved Field names.
A few field names are reserved for pre-defined behaviors. They should be defined on a model
when the related behavior is desired.
Exercise
Add active field.
Set the appropriate default value for the active field so it doesn’t disappear anymore.
Note that the default active=False value was assigned to all existing records.
Exercise
Add state field.
Add a state field to the estate.property model. Five values are possible: New, Offer
Received, Offer Accepted, Sold and Canceled. It must be required, should not be copied and
should have its default value set to ‘New’.
Now that we are able to interact with the UI thanks to the default views, the next step is obvious:
we want to define our own views.
A refresh is needed since the web client keeps a cache of the various menus and views for
performance reasons.
Views are defined in XML files with actions and menus. They are instances of
the ir.ui.view model.
In our real estate module, we need to organize the fields in a logical way:
• in the list (tree) view, we want to display more than just the name.
• in the form view, the fields should be grouped.
• in the search view, we must be able to search on more than just the name. Specifically,
we want a filter for the ‘Available’ properties and a shortcut to group by postcode.
List
Reference: the documentation related to this topic can be found in List.
Note
Goal: at the end of this section, the list view should look like this:
List views, also called tree views, display records in a tabular form.
Their root element is <tree>. The most basic version of this view simply lists all the fields to
display in the table (where each field is a column):
<tree string="Tests">
<field name="name"/>
<field name="last_seen"/>
</tree>
A simple example can be found here.
Exercise
Add a custom list view.
Define a list view for the estate.property model in the appropriate XML file. Check
the Goal of this section for the fields to display.
Tips:
• do not add the editable="bottom" attribute that you can find in the example above.
We’ll come back to it later.
• some field labels may need to be adapted to match the reference.
As always, you need to restart the server (do not forget the -u option) and refresh the browser to
see the result.
Warning
You will probably use some copy-paste in this chapter, therefore always make sure that
the id remains unique for each view!
Form
Reference: the documentation related to this topic can be found in Form.
Note
Goal: at the end of this section, the form view should look like this:
Their root element is <form>. They are composed of high-level structure elements (groups and
notebooks) and interactive elements (buttons and fields):
<form string="Test">
<sheet>
<group>
<group>
<field name="name"/>
</group>
<group>
<field name="last_seen"/>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
</notebook>
</group>
</sheet>
</form>
It is possible to use regular HTML tags such as div and h1 as well as the the class attribute
(Odoo provides some built-in classes) to fine-tune the look.
Exercise
Add a custom form view.
Define a form view for the estate.property model in the appropriate XML file. Check
the Goal of this section for the expected final design of the page.
This might require some trial and error before you get to the expected result ;-) It is advised that
you add the fields and the tags one at a time to help understand how it works.
In order to avoid relaunching the server every time you do a modification to the view, it can be
convenient to use the --dev xml parameter when launching the server:
Search
Reference: the documentation related to this topic can be found in Search.
Note
Goal: at the end of this section, the search view should look like this:
Search views are slightly different from the list and form views since they don’t display content.
Although they apply to a specific model, they are used to filter other views’ content (generally
aggregated views such as List). Beyond the difference in use case, they are defined the same
way.
Their root element is <search>. The most basic version of this view simply lists all the fields for
which a shortcut is desired:
<search string="Tests">
<field name="name"/>
<field name="last_seen"/>
</search>
The default search view generated by Odoo provides a shortcut to filter by name. It is very
common to add the fields which the user is likely to filter on in a customized search view.
Exercise
Add a custom search view.
Define a search view for the estate.property model in the appropriate XML file. Check the
first image of this section’s Goal for the list of fields.
After restarting the server, it should be possible to filter on the given fields.
Search views can also contain <filter> elements, which act as toggles for predefined searches.
Filters must have one of the following attributes:
Before going further in the exercise, it is necessary to introduce the ‘domain’ concept.
Domains
Reference: the documentation related to this topic can be found in Search domains.
In Odoo, a domain encodes conditions on records: a domain is a list of criteria used to select a
subset of a model’s records. Each criterion is a triplet with a field name, an operator and a value.
A record satisfies a criterion if the specified field meets the condition of the operator applied to
the value.
For instance, when used on the Product model the following domain selects all services with a
unit price greater than 1000:
['|',
('product_type', '=', 'service'),
'!', '&',
('unit_price', '>=', 1000),
('unit_price', '<', 2000)]
Note
XML does not allow < and & to be used inside XML elements. To avoid parsing errors, entity
references should be used: < for < and & for &. Other entity references
(>, ' & ") are optional.
Example
<filter name="negative" domain="[('test_val', '<', 0)]"/>
Exercise
Add filter and Group By.
• a filter which displays available properties, i.e. the state should be ‘New’ or ‘Offer
Received’.
• the ability to group results by postcode.
Looking good? At this point we are already able to create models and design a user interface
which makes sense business-wise. However, a key component is still missing: the link between
models.
In our real estate module, we want the following information for a property:
Note
Goal: at the end of this section:
In our real estate module, we want to define the concept of property type. A property type is, for
example, a house or an apartment. It is a standard business need to categorize properties
according to their type, especially to refine filtering.
A property can have one type, but the same type can be assigned to many properties. This is
supported by the many2one concept.
A many2one is a simple link to another object. For example, in order to define a link to
the res.partner in our test model, we can write:
print(my_test_object.partner_id.name)
See also
foreign keys
In practice a many2one can be seen as a dropdown list in a form view.
Exercise
Add the Real Estate Property Type table.
Tip: do not forget to import any new Python files in __init__.py, add new data files
in __manifest.py__ or add the access rights ;-)
Once again, restart the server and refresh to see the results!
In the real estate module, there are still two missing pieces of information we want on a property:
the buyer and the salesperson. The buyer can be any individual, but on the other hand the
salesperson must be an employee of the real estate agency (i.e. an Odoo user).
Add a buyer and a salesperson to the estate.property model using the two common models
mentioned above. They should be added in a new tab of the form view, as depicted in this
section’s Goal.
The default value for the salesperson must be the current user. The buyer should not be copied.
Tip: to get the default value, check the note below or look at an example here.
Note
The object self.env gives access to request parameters and other useful things:
• self.env.cr or self._cr is the database cursor object; it is used for querying the
database
• self.env.uid or self._uid is the current user’s database id
• self.env.user is the current user’s record
• self.env.context or self._context is the context dictionary
• self.env.ref(xml_id) returns the record corresponding to an XML id
• self.env[model_name] returns an instance of the given model
Now let’s have a look at other types of links.
Many2many
Reference: the documentation related to this topic can be found in Many2many.
Note
Goal: at the end of this section:
• a new estate.property.tag model should be created with the corresponding menu and
action.
In our real estate module, we want to define the concept of property tags. A property tag is, for
example, a property which is ‘cozy’ or ‘renovated’.
A property can have many tags and a tag can be assigned to many properties. This is supported
by the many2many concept.
A many2many is a bidirectional multiple relationship: any record on one side can be related to
any number of records on the other side. For example, in order to define a link to
the account.tax model on our test model, we can write:
Exercise
Add the Real Estate Property Tag table.
Note
Goal: at the end of this section:
An offer applies to one property, but the same property can have many offers. The concept
of many2one appears once again. However, in this case we want to display the list of offers for a
given property so we will use the one2many concept.
A one2many is the inverse of a many2one. For example, we defined on our test model a link to
the res.partner model thanks to the field partner_id. We can define the inverse relation, i.e.
the list of test models linked to our partner:
By convention, one2many fields have the _ids suffix. They behave as a list of records, meaning
that accessing the data must be done in a loop:
Second, despite the fact that the property_id field is required, we did not include it in the views.
How does Odoo know which property our offer is linked to? Well that’s part of the magic of
using the Odoo framework: sometimes things are defined implicitly. When we create a record
through a one2many field, the corresponding many2one is populated automatically for
convenience.
Still alive? This chapter is definitely not the easiest one. It introduced a couple of new concepts
while relying on everything that was introduced before. The next chapter will be lighter, don’t
worry ;-)
These cases are supported by the concepts of computed fields and onchanges. Although this
chapter is not technically complex, the semantics of both concepts is very important. This is also
the first time we will write Python logic. Until now we haven’t written anything other than class
definitions and field declarations.
Computed Fields
Reference: the documentation related to this topic can be found in Computed Fields.
Note
Goal: at the end of this section:
• In the property model, the total area and the best offer should be computed:
• In the property offer model, the validity date should be computed and can be updated:
In our real estate module, we have defined the living area as well as the garden area. It is then
natural to define the total area as the sum of both fields. We will use the concept of a computed
field for this, i.e. the value of a given field will be computed from the value of other fields.
So far fields have been stored directly in and retrieved directly from the database. Fields can also
be computed. In this case, the field’s value is not retrieved from the database but computed on-
the-fly by calling a method of the model.
To create a computed field, create a field and set its attribute compute to the name of a method.
The computation method should set the value of the computed field for every record in self.
By convention, compute methods are private, meaning that they cannot be called from the
presentation tier, only from the business tier (see Chapter 1: Architecture Overview). Private
methods have a name starting with an underscore _.
Dependencies
The value of a computed field usually depends on the values of other fields in the computed
record. The ORM expects the developer to specify those dependencies on the compute method
with the decorator depends(). The given dependencies are used by the ORM to trigger the
recomputation of the field whenever some of its dependencies have been modified:
from odoo import api, fields, models
class TestComputed(models.Model):
_name = "test.computed"
total = fields.Float(compute="_compute_total")
amount = fields.Float()
@api.depends("amount")
def _compute_total(self):
for record in self:
record.total = 2.0 * record.amount
Note
self is a collection.
The object self is a recordset, i.e. an ordered collection of records. It supports the standard
Python operations on collections, e.g. len(self) and iter(self), plus extra set operations such
as recs1 | recs2.
Iterating over self gives the records one by one, where each record is itself a collection of size
1. You can access/assign fields on single records by using the dot notation, e.g. record.name.
Many examples of computed fields can be found in Odoo. Here is a simple one.
Exercise
Compute the total area.
description = fields.Char(compute="_compute_description")
partner_id = fields.Many2one("res.partner")
@api.depends("partner_id.name")
def _compute_description(self):
for record in self:
record.description = "Test for partner %s" % record.partner_id.name
The example is given with a Many2one, but it is valid for Many2many or a One2many. An example
can be found here.
Exercise
Compute the best offer.
• Add the best_price field to estate.property. It is defined as the highest (i.e.
maximum) of the offers’ price.
• Add the field to the form view as depicted in the first image of this section’s Goal.
Tip: you might want to try using the mapped() method. See here for a simple example.
Inverse Function
You might have noticed that computed fields are read-only by default. This is expected since the
user is not supposed to set a value.
In some cases, it might be useful to still be able to set a value directly. In our real estate example,
we can define a validity duration for an offer and set a validity date. We would like to be able to
set either the duration or the date with one impacting the other.
class TestComputed(models.Model):
_name = "test.computed"
@api.depends("amount")
def _compute_total(self):
for record in self:
record.total = 2.0 * record.amount
def _inverse_total(self):
for record in self:
record.amount = record.total / 2.0
An example can be found here.
A compute method sets the field while an inverse method sets the field’s dependencies.
Note that the inverse method is called when saving the record, while the compute method is
called at each change of its dependencies.
Exercise
Compute a validity date for offers.
• Add the fields in the form view and the list view as depicted on the second image of this
section’s Goal.
Additional Information
Computed fields are not stored in the database by default. Therefore it is not possible to search
on a computed field unless a search method is defined. This topic is beyond the scope of this
training, so we won’t cover it. An example can be found here.
Another solution is to store the field with the store=True attribute. While this is usually
convenient, pay attention to the potential computation load added to your model. Lets re-use our
example:
@api.depends("partner_id.name")
def _compute_description(self):
for record in self:
record.description = "Test for partner %s" % record.partner_id.name
Every time the partner name is changed, the description is automatically recomputed for all the
records referring to it! This can quickly become prohibitive to recompute when millions of
records need recomputation.
It is also worth noting that a computed field can depend on another computed field. The ORM is
smart enough to correctly recompute all the dependencies in the right order… but sometimes at
the cost of degraded performance.
In general performance must always be kept in mind when defining computed fields. The more
complex is your field to compute (e.g. with a lot of dependencies or when a computed field
depends on other computed fields), the more time it will take to compute. Always take some time
to evaluate the cost of a computed field beforehand. Most of the time it is only when your code
reaches a production server that you realize it slows down a whole process. Not cool :-(
Onchanges
Reference: the documentation related to this topic can be found in onchange():
Note
Goal: at the end of this section, enabling the garden will set a default area of 10 and an
orientation to North.
In our real estate module, we also want to help the user with data entry. When the ‘garden’ field
is set, we want to give a default value for the garden area as well as the orientation. Additionally,
when the ‘garden’ field is unset we want the garden area to reset to zero and the orientation to be
removed. In this case, the value of a given field modifies the value of other fields.
The ‘onchange’ mechanism provides a way for the client interface to update a form without
saving anything to the database whenever the user has filled in a field value. To achieve this, we
define a method where self represents the record in the form view and decorate it
with onchange() to specify which field it is triggered by. Any change you make on self will be
reflected on the form:
class TestOnchange(models.Model):
_name = "test.onchange"
name = fields.Char(string="Name")
description = fields.Char(string="Description")
partner_id = fields.Many2one("res.partner", string="Partner")
@api.onchange("partner_id")
def _onchange_partner_id(self):
self.name = "Document for %s" % (self.partner_id.name)
self.description = "Default description for %s" % (self.partner_id.name)
In this example, changing the partner will also change the name and the description values. It is
up to the user whether or not to change the name and description values afterwards. Also note
that we do not loop on self, this is because the method is only triggered in a form view,
where self is always a single record.
Exercise
Set values for garden area and orientation.
Create an onchange in the estate.property model in order to set values for the garden area (10)
and orientation (North) when garden is set to True. When unset, clear the fields.
Additional Information
Onchanges methods can also return a non-blocking warning message (example).
In many cases, both computed fields and onchanges may be used to achieve the same result.
Always prefer computed fields since they are also triggered outside of the context of a form
view. Never ever use an onchange to add business logic to your model. This is a very bad idea
since onchanges are not automatically triggered when creating a record programmatically; they
are only triggered in the form view.
The usual pitfall of computed fields and onchanges is trying to be ‘too smart’ by adding too
much logic. This can have the opposite result of what was expected: the end user is confused
from all the automation.
Computed fields tend to be easier to debug: such a field is set by a given method, so it’s easy to
track when the value is set. Onchanges, on the other hand, may be confusing: it is very difficult
to know the extent of an onchange. Since several onchange methods may set the same fields, it
easily becomes difficult to track where a value is coming from.
When using stored computed fields, pay close attention to the dependencies. When computed
fields depend on other computed fields, changing a value can trigger a large number of
recomputations. This leads to poor performance.
In the next chapter, we’ll see how we can trigger some business logic when buttons are clicked.
Object Type
Reference: the documentation related to this topic can be found in Actions and Error
management.
Note
Goal: at the end of this section:
A canceled property cannot be sold and a sold property cannot be canceled. For the sake of
clarity, the state field has been added on the view.
• Add a button in the view, for example in the header of the view:
<form>
<header>
<button name="action_do_something" type="object" string="Do Something"/>
</header>
<sheet>
<field name="name"/>
</sheet>
</form>
• and link this button to business logic:
from odoo import fields, models
class TestAction(models.Model):
_name = "test.action"
name = fields.Char()
def action_do_something(self):
for record in self:
record.name = "Something"
return True
By assigning type="object" to our button, the Odoo framework will execute a Python method
with name="action_do_something" on the given model.
The first important detail to note is that our method name isn’t prefixed with an underscore (_).
This makes our method a public method, which can be called directly from the Odoo interface
(through an RPC call). Until now, all methods we created (compute, onchange) were called
internally, so we used private methods prefixed by an underscore. You should always define
your methods as private unless they need to be called from the user interface.
Also note that we loop on self. Always assume that a method can be called on multiple records;
it’s better for reusability.
Finally, a public method should always return something so that it can be called through XML-
RPC. When in doubt, just return True.
There are hundreds of examples in the Odoo source code. One example is this button in a
view and its corresponding Python method
Exercise
Cancel and set a property as sold.
• Add the buttons ‘Cancel’ and ‘Sold’ to the estate.property model. A canceled property
cannot be set as sold, and a sold property cannot be canceled.
Refer to the first image of the Goal for the expected result.
Tip: in order to raise an error, you can use the UserError function. There are plenty of
examples in the Odoo source code ;-)
Refer to the second image of the Goal for the expected result.
• When an offer is accepted, set the buyer and the selling price for the corresponding
property.
Refer to the third image of the Goal for the expected result.
Pay attention: in real life only one offer can be accepted for a given property!
Action Type
In Chapter 5: Finally, Some UI To Play With, we created an action that was linked to a menu.
You may be wondering if it is possible to link an action to a button. Good news, it is! One way to
do it is:
In the next chapter we’ll see how we can prevent encoding incorrect data in Odoo.
SQL
Reference: the documentation related to this topic can be found in Models and in
the PostgreSQL’s documentation.
Note
Goal: at the end of this section:
SQL constraints are defined through the model attribute _sql_constraints. This attribute is
assigned a list of triples containing strings (name, sql_definition, message), where name is a
valid SQL constraint name, sql_definition is a table_constraint expression and message is the
error message.
You can find a simple example here.
Exercise
Add SQL constraints.
Python
Reference: the documentation related to this topic can be found in constrains().
Note
Goal: at the end of this section, it will not be possible to accept an offer lower than 90% of the
expected price.
SQL constraints are an efficient way of ensuring data consistency. However it may be necessary
to make more complex checks which require Python code. In this case we need a Python
constraint.
...
@api.constrains('date_end')
def _check_date_end(self):
for record in self:
if record.date_end < fields.Date.today():
raise ValidationError("The end date cannot be set in the past")
# all records passed the test, don't return anything
A simple example can be found here.
Exercise
Add Python constraints.
Add a constraint so that the selling price cannot be lower than 90% of the expected price.
Tip: the selling price is zero until an offer is validated. You will need to fine tune your check to
take this into account.
Warning
Always use the float_compare() and float_is_zero() methods
from odoo.tools.float_utils when working with floats!
Ensure the constraint is triggered every time the selling price or the expected price is changed!
SQL constraints are usually more efficient than Python constraints. When performance matters,
always prefer SQL over Python constraints.
Our real estate module is starting to look good. We added some business logic, and now we
make sure the data is consistent. However, the user interface is still a bit rough. Let’s see how we
can improve it in the next chapter.
This chapter covers a very small subset of what can be done in the views. Do not hesitate to read
the reference documentation for a more complete overview.
Reference: the documentation related to this chapter can be found in View records and View
architectures.
Inline Views
Note
Goal: at the end of this section, a specific list of properties should be added to the property type
view:
In the real estate module we added a list of offers for a property. We simply added the
field offer_ids with:
<field name="offer_ids"/>
The field uses the specific view for estate.property.offer. In some cases we want to define a
specific list view which is only used in the context of a form view. For example, we would like
to display the list of properties linked to a property type. However, we only want to display 3
fields for clarity: name, expected price and state.
To do this, we can define inline list views. An inline list view is defined directly inside a form
view. For example:
class TestModel(models.Model):
_name = "test_model"
_description = "Test Model"
description = fields.Char()
line_ids = fields.One2many("test_model_line", "model_id")
class TestModelLine(models.Model):
_name = "test_model_line"
_description = "Test Model Line"
model_id = fields.Many2one("test_model")
field_1 = fields.Char()
field_2 = fields.Char()
field_3 = fields.Char()
<form>
<field name="description"/>
<field name="line_ids">
<tree>
<field name="field_1"/>
<field name="field_2"/>
</tree>
</field>
</form>
In the form view of the test_model, we define a specific list view for test_model_line with
fields field_1 and field_2.
Exercise
Add an inline list view.
Note
Goal: at the end of this section, the state of the property should be displayed using a specific
widget:
Four states are displayed: New, Offer Received, Offer Accepted and Sold.
Whenever we’ve added fields to our models, we’ve (almost) never had to worry about how these
fields would look like in the user interface. For example, a date picker is provided for
a Date field and a One2many field is automatically displayed as a list. Odoo chooses the right
‘widget’ depending on the field type.
However, in some cases, we want a specific representation of a field which can be done thanks to
the widget attribute. We already used it for the tag_ids field when we used
the widget="many2many_tags" attribute. If we hadn’t used it, then the field would have
displayed as a list.
Each field type has a set of widgets which can be used to fine tune its display. Some widgets also
take extra options. An exhaustive list can be found in Fields.
Exercise
Use the status bar widget.
Use the statusbar widget in order to display the state of the estate.property as depicted in
the Goal of this section.
Add a field only once to a list or a form view. Adding it multiple times is not supported.
List Order
Reference: the documentation related to this section can be found in Models.
Note
Goal: at the end of this section, all lists should display by default in a deterministic order.
Property types can be ordered manually.
During the previous exercises, we created several list views. However, at no point did we specify
which order the records had to be listed in by default. This is a very important thing for many
business cases. For example, in our real estate module we would want to display the highest
offers on top of the list.
Model
Odoo provides several ways to set a default order. The most common way is to define
the _order attribute directly in the model. This way, the retrieved records will follow a
deterministic order which will be consistent in all views including when records are searched
programmatically. By default there is no order specified, therefore the records will be retrieved
in a non-deterministic order depending on PostgreSQL.
The _order attribute takes a string containing a list of fields which will be used for sorting. It
will be converted to an order_by clause in SQL. For example:
class TestModel(models.Model):
_name = "test_model"
_description = "Test Model"
_order = "id desc"
description = fields.Char()
Our records are ordered by descending id, meaning the highest comes first.
Exercise
Add model ordering.
Model Order
estate.property Descending ID
estate.property.offer Descending Price
estate.property.tag Name
estate.property.type Name
View
Ordering is possible at the model level. This has the advantage of a consistent order everywhere
a list of records is retrieved. However, it is also possible to define a specific order directly in a
view thanks to the default_order attribute (example).
Manual
Both model and view ordering allow flexibility when sorting records, but there is still one case
we need to cover: the manual ordering. A user may want to sort records depending on the
business logic. For example, in our real estate module we would like to sort the property types
manually. It is indeed useful to have the most used types appear at the top of the list. If our real
estate agency mainly sells houses, it is more convenient to have ‘House’ appear before
‘Apartment’.
To do so, a sequence field is used in combination with the handle widget. Obviously
the sequence field must be the first field in the _order attribute.
Exercise
Add manual ordering.
Form
Note
Goal: at the end of this section, the property form view will have:
In our real estate module, we want to modify the behavior of some fields. For example, we don’t
want to be able to create or edit a property type from the form view. Instead we expect the types
to be handled in their appropriate menu. We also want to give tags a color. In order to add these
behavior customizations, we can add the options attribute to several field widgets.
Exercise
Add widget options.
• Add the appropriate option to the property_type_id field to prevent the creation and the
editing of a property type from the property form view. Have a look at the Many2one
widget documentation for more info.
• Add the following field:
Model Field Type
estate.property.tag Color Integer
Then add the appropriate option to the tag_ids field to add a color picker on the tags. Have a
look at the FieldMany2ManyTags widget documentation for more info.
In Chapter 5: Finally, Some UI To Play With, we saw that reserved fields were used for specific
behaviors. For example, the active field is used to automatically filter out inactive records. We
added the state as a reserved field as well. It’s now time to use it! A state field can be used in
combination with an invisible attribute in the view to display buttons conditionally.
Exercise
Add conditional display of buttons.
Use the invisible attribute to display the header buttons conditionally as depicted in this
section’s Goal (notice how the ‘Sold’ and ‘Cancel’ buttons change when the state is modified).
Tip: do not hesitate to search for invisible= in the Odoo XML files for some examples.
More generally, it is possible to make a field invisible, readonly or required based on the
value of other fields. Note that invisible can also be applied to other elements of the view such
as button or group.
invisible, readonly and required can have any Python expression as value. The expression
gives the condition in which the property applies. For example:
<form>
<field name="description" invisible="not is_partner"/>
<field name="is_partner" invisible="True"/>
</form>
This means that the description field is invisible when is_partner is False. It is important to
note that a field used in invisible must be present in the view. If it should not be displayed to
the user, we can use the invisible attribute to hide it.
Exercise
Use invisible.
•Make the garden area and orientation invisible in the estate.property form view when
there is no garden.
• Make the ‘Accept’ and ‘Refuse’ buttons invisible once the offer state is set.
• Do not allow adding an offer when the property state is ‘Offer Accepted’, ‘Sold’ or
‘Canceled’. To do this use the readonly attribute.
Warning
Using a (conditional) readonly attribute in the view can be useful to prevent data entry errors,
but keep in mind that it doesn’t provide any level of security! There is no check done server-side,
therefore it’s always possible to write on the field through a RPC call.
List
Note
Goal: at the end of this section, the property and offer list views should have color decorations.
Additionally, offers and tags will be editable directly in the list, and the availability date will be
hidden by default.
When the model only has a few fields, it can be useful to edit records directly through the list
view and not have to open the form view. In the real estate example, there is no need to open a
form view to add an offer or create a new tag. This can be achieved thanks to
the editable attribute.
Exercise
Make list views editable.
Exercise
Make a field optional.
Make the field date_availability on the estate.property list view optional and hidden by
default.
Finally, color codes are useful to visually emphasize records. For example, in the real estate
module we would like to display refused offers in red and accepted offers in green. This can be
achieved thanks to the decoration-{$name} attribute (see Fields for a complete list):
<tree decoration-success="is_partner==True">
<field name="name"/>
<field name="is_partner" invisible="1"/>
</tree>
The records where is_partner is True will be displayed in green.
Exercise
Add some decorations.
• Keep in mind that all fields used in attributes must be in the view!
• If you want to test the color of the “Offer Received” and “Offer Accepted” states, add the
field in the form view and change it manually (we’ll implement the business logic for this
later).
Search
Reference: the documentation related to this section can be found in Search and Search defaults.
Note
Goal: at the end of this section, the available properties will be filtered by default, and searching
on the living area returns results where the area is larger than the given number.
Last but not least, there are some tweaks we would like to apply when searching. First of all, we
want to have our ‘Available’ filter applied by default when we access the properties. To make
this happen, we need to use the search_default_{$name} action context, where {$name} is the
filter name. This means that we can define which filter(s) will be activated by default at the
action level.
Exercise
Add a default filter.
Search view <field> elements can have a filter_domain that overrides the domain generated
for searching on the given field. In the given domain, self represents the value entered by the
user. In the example below, it is used to search on both name and description fields.
<search string="Test">
<field name="description" string="Name and description"
filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
</search>
Exercise
Change the living area search.
Add a filter_domain to the living area to include properties with an area equal to or greater
than the given value.
Stat Buttons
Note
Goal: at the end of this section, there will be a stat button on the property type form view which
shows the list of all offers related to properties of the given type when it is clicked on.
If you’ve already used some functional modules in Odoo, you’ve probably already encountered a
‘stat button’. These buttons are displayed on the top right of a form view and give a quick access
to linked documents. In our real estate module, we would like to have a quick link to the offers
related to a given property type as depicted in the Goal of this section.
At this point of the tutorial we have already seen most of the concepts to do this. However, there
is not a single solution and it can still be confusing if you don’t know where to start from. We’ll
describe a step-by-step solution in the exercise. It can always be useful to find some examples in
the Odoo codebase by looking for oe_stat_button.
The following exercise might be a bit more difficult than the previous ones since it assumes you
are able to search for examples in the source code on your own. If you are stuck there is probably
someone nearby who can help you ;-)
The exercise introduces the concept of Related fields. The easiest way to understand it is to
consider it as a specific case of a computed field. The following definition of
the description field:
...
@api.depends("partner_id.name")
def _compute_description(self):
for record in self:
record.description = record.partner_id.name
Every time the partner name is changed, the description is modified.
Exercise
Add a stat button to property type.
•
On the estate.property.offer action, add a domain that defines property_type_id as
equal to the active_id (= the current record, here is an example)
Looking good? If not, don’t worry, the next chapter doesn’t require stat buttons ;-)
Python Inheritance
Note
Goal: at the end of this section:
• When an offer is created, the property state should change to ‘Offer Received’
• It should not be possible to create an offer with a lower price than an existing offer
In our real estate module, we never had to develop anything specific to be able to do the standard
CRUD actions. The Odoo framework provides the necessary tools to do them. In fact, such
actions are already included in our model thanks to classical Python inheritance:
class TestModel(models.Model):
_name = "test_model"
_description = "Test Model"
...
Our class TestModel inherits from Model which
provides create(), read(), write() and unlink().
These methods (and any other method defined on Model) can be extended to add specific
business logic:
class TestModel(models.Model):
_name = "test_model"
_description = "Test Model"
...
@api.model
def create(self, vals):
# Do some business logic, modify vals...
...
# Then call super to execute the parent method
return super().create(vals)
The decorator model() is necessary for the create() method because the content of the
recordset self is not relevant in the context of creation, but it is not necessary for the other
CRUD methods.
It is also important to note that even though we can directly override the unlink() method, you
will almost always want to write a new method with the decorator ondelete() instead. Methods
marked with this decorator will be called during unlink() and avoids some issues that can occur
during uninstalling the model’s module when unlink() is directly overridden.
Danger
• It is very important to always call super() to avoid breaking the flow. There are only a
few very specific cases where you don’t want to call it.
• Make sure to always return data consistent with the parent method. For example, if the
parent method returns a dict(), your override must also return a dict().
Exercise
Add business logic to the CRUD methods.
• At offer creation, set the property state to ‘Offer Received’. Also raise an error if the user
tries to create an offer with a lower amount than an existing offer.
Tip: the property_id field is available in the vals, but it is an int. To instantiate
an estate.property object, use self.env[model_name].browse(value) (example)
Model Inheritance
Reference: the documentation related to this topic can be found in Inheritance and extension.
In our real estate module, we would like to display the list of properties linked to a salesperson
directly in the Settings / Users & Companies / Users form view. To do this, we need to add a
field to the res.users model and adapt its view to show it.
Odoo provides two inheritance mechanisms to extend an existing model in a modular way.
The first inheritance mechanism allows modules to modify the behavior of a model defined in an
another module by:
class InheritedModel(models.Model):
_inherit = "inherited.model"
By convention, each inherited model is defined in its own Python file. In our example, it would
be models/inherited_model.py.
Exercise
Add a field to Users.
View Inheritance
Reference: the documentation related to this topic can be found in Inheritance.
Note
Goal: at the end of this section, the list of available properties linked to a salesperson should be
displayed in their user form view
Instead of modifying existing views in place (by overwriting them), Odoo provides view
inheritance where children ‘extension’ views are applied on top of root views. These extension
can both add and remove content from their parent view.
An extension view references its parent using the inherit_id field. Instead of a single view,
its arch field contains a number of xpath elements that select and alter the content of their parent
view:
An XPath expression selecting a single element in the parent view. Raises an error if it
matches no element or more than one
position
inside
replaces the matched element with the xpath’s body, replacing any $0 node occurrence in
the new body with the original element
before
alters the attributes of the matched element using the special attribute elements in
the xpath’s body
When matching a single element, the position attribute can be set directly on the element to be
found. Both inheritances below have the same result.
Exercise
Add fields to the Users view.
In the next chapter, we will learn how to interact with other modules.
Link Module
The common approach for such use cases is to create a ‘link’ module. In our case, the module
would depend on estate and account and would include the invoice creation logic of the estate
property. This way the real estate and the accounting modules can be installed independently.
When both are installed, the link module provides the new feature.
Exercise
Create a link module.
Create the estate_account module, which depends on the estate and account modules. For
now, it will be an empty shell.
Tip: you already did this at the beginning of the tutorial. The process is very similar.
When the estate_account module appears in the list, go ahead and install it! You’ll notice that
the Invoicing application is installed as well. This is expected since your module depends on it. If
you uninstall the Invoicing application, your module will be uninstalled as well.
Invoice Creation
It’s now time to generate the invoice. We want to add functionality to
the estate.property model, i.e. we want to add some extra logic for when a property is sold.
Does that sound familiar? If not, it’s a good idea to go back to the previous chapter since you
might have missed something ;-)
As a first step, we need to extend the action called when pressing the ‘Sold’ button on a property.
To do so, we need to create a model inheritance in the estate_account module for
the estate.property model. For now, the overridden action will simply return the super call.
Maybe an example will make things clearer:
class InheritedModel(models.Model):
_inherit = "inherited.model"
def inherited_action(self):
return super().inherited_action()
A practical example can be found here.
Exercise
Add the first step of invoice creation.
If the override is working, we can move forward and create the invoice. Unfortunately, there is
no easy way to know how to create any given object in Odoo. Most of the time, it is necessary to
have a look at its model to find the required fields and provide appropriate values.
A good way to learn is to look at how other modules already do what you want to do. For
example, one of the basic flows of Sales is the creation of an invoice from a sales order. This
looks like a good starting point since it does exactly what we want to do. Take some time to read
and understand the _create_invoices method. When you are done crying because this simple task
looks awfully complex, we can move forward in the tutorial.
Exercise
Add the second step of invoice creation.
Obviously we don’t have any invoice lines so far. To create an invoice line, we need the
following information:
def inherited_action(self):
self.env["test_model"].create(
{
"name": "Test",
"line_ids": [
Command.create({
"field_1": "value_1",
"field_2": "value_2",
})
],
}
)
return super().inherited_action()
Exercise
Add the third step of invoice creation.
Add two invoice lines during the creation of the account.move. Each property sold will be
invoiced following these conditions:
However, if we want to give a unique look to our application, it is necessary to go a step further
and be able to design new views. Moreover, other features such as PDF reports or website pages
need another tool to be created with more flexibility: a templating engine.
You might already be familiar with existing engines such as Jinja (Python), ERB (Ruby) or Twig
(PHP). Odoo comes with its own built-in engine: QWeb Templates. QWeb is the primary
templating engine used by Odoo. It is an XML templating engine and used mostly to generate
HTML fragments and pages.
You probably already have come across the kanban board in Odoo where the records are
displayed in a card-like structure. We will build such a view for our real estate module.
Note
Goal: at the end of this section a Kanban view of the properties should be created:
In our estate application, we would like to add a Kanban view to display our properties. Kanban
views are a standard Odoo view (like the form and list views), but their structure is much more
flexible. In fact, the structure of each card is a mix of form elements (including basic HTML)
and QWeb. The definition of a Kanban view is similar to the definition of the list and form
views, except that their root element is <kanban>. In its simplest form, a Kanban view looks like:
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="name"/>
</div>
</t>
</templates>
</kanban>
Let’s break down this example:
• <templates>: defines a list of QWeb Templates templates. Kanban views must define at
least one root template kanban-box, which will be rendered once for each record.
• <t t-name="kanban-box">: <t> is a placeholder element for QWeb directives. In this
case, it is used to set the name of the template to kanban-box
• <div class="oe_kanban_global_click">: the oe_kanban_global_click makes
the <div> clickable to open the record.
• <field name="name"/>: this will add the name field to the view.
Exercise
Make a minimal kanban view.
Using the simple example provided, create a minimal Kanban view for the properties. The only
field to display is the name.
Tip: you must add kanban in the view_mode of the corresponding ir.actions.act_window.
Once the Kanban view is working, we can start improving it. If we want to display an element
conditionally, we can use the t-if directive (see Conditionals).
<kanban>
<field name="state"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="name"/>
<div t-if="record.state.raw_value == 'new'">
This is new!
</div>
</div>
</t>
</templates>
</kanban>
We added a few things:
Exercise
Improve the Kanban view.
Add the following fields to the Kanban view: expected price, best price, selling price and tags.
Pay attention: the best price is only displayed when an offer is received, while the selling price is
only displayed when an offer is accepted.
Exercise
Add default grouping.
Use the appropriate attribute to group the properties by type by default. You must also prevent
drag and drop.
Reference: you will find the Odoo coding guidelines in Coding guidelines.
Exercise
Polish your code.
Refactor your code to respect the coding guidelines. Don’t forget to run your linter and respect
the module structure, the variable names, the method name convention, the model attribute order
and the xml ids.
Test on the runbot
Odoo has its own CI server named runbot. All commits, branches and PR will be tested to avoid
regressions or breaking of the stable versions. All the runs that pass the tests are deployed on
their own server with demo data.
Exercise
Play with the runbot.
Feel free to go to the runbot website and open the last stable version of Odoo to check out all the
available applications and functionalities.