Customizing the web client
This guide is about creating modules for Odoo’s web client.
A Simple Module
Let’s start with a simple Odoo module holding basic web component configuration and letting us test the web framework.
The example module is available online and can be downloaded using the following command:
This will create a
petstore folder wherever you executed the command. You then need to add that folder to Odoo’s
addons path, create a new database and install the
If you browse the
petstore folder, you should see the following content:
The module already holds various server customizations. We’ll come back to these later, for now let’s focus on the web-related content, in the
Files used in the “web” side of an Odoo module must be placed in a
static folder so they are available to a web browser, files outside that folder can not be fetched by browsers. The
src/xml sub-folders are conventional and not strictly necessary.
Currently empty, will hold the CSS for pet store content
Mostly empty, will hold QWeb Templates templates
Which only prints a small message in the browser’s console.
The files in the
static folder, need to be defined within the module in order for them to be loaded correctly. Everything in
src/xml is defined in
__manifest__.py while the contents of
src/js are defined in
petstore.xml, or a similar file.
The Odoo framework uses one such pattern to define modules within web addons, in order to both namespace code and correctly order its loading.
oepetstore/static/js/petstore.js contains a module declaration:
In Odoo web, modules are declared as functions set on the global
odoo variable. The function’s name must be the same as the addon (in this case
oepetstore) so the framework can find it, and automatically initialize it.
When the web client loads your module it will call the root function and provide two parameters:
the first parameter is the current instance of the Odoo web client, it gives access to various capabilities defined by the Odoo (translations, network services) as well as objects defined by the core or by other modules.
the second parameter is your own local namespace automatically created by the web client. Objects and variables which should be accessible from outside your module (either because the Odoo web client needs to call them or because others may want to customize them) should be set inside that namespace.
New classes are defined by calling the
extend() method of
extend() method takes a dictionary describing the new class’s content (methods and static attributes). In this case, it will only have a
say_hello method which takes no parameters.
Classes are instantiated using the
And attributes of the instance can be accessed via
Classes can provide an initializer to perform the initial setup of the instance, by defining an
init() method. The initializer receives the parameters passed when using the
It is also possible to create subclasses from existing (used-defined) classes by calling
extend() on the parent class, as is done to subclass
When overriding a method using inheritance, you can use
this._super() to call the original method:
The Odoo web client bundles jQuery for easy DOM manipulation. It is useful and provides a better API than standard W3C DOM2, but insufficient to structure complex applications leading to difficult maintenance.
Much like object-oriented desktop UI toolkits (e.g. Qt, Cocoa or GTK), Odoo Web makes specific components responsible for sections of a page. In Odoo web, the base for such components is the
Widget() class, a component specialized in handling a page section and displaying information for the user.
Your First Widget
The initial demonstration module already provides a basic widget:
Widget() and overrides the standard method
start(), which — much like the previous
MyClass — does little for now.
This line at the end of the file:
registers our basic widget as a client action. Client actions will be explained later, for now this is just what allows our widget to be called and displayed when we select themenu.
Widgets have a number of methods and features, but the basics are simple:
set up a widget
format the widget’s data
display the widget
HomePage widget already has a
start() method. That method is part of the normal widget lifecycle and automatically called once the widget is inserted in the page. We can use it to display some content.
All widgets have a
$el which represents the section of page they’re in charge of (as a jQuery object). Widget content should be inserted there. By default,
$el is an empty
<div> element is usually invisible to the user if it has no content (or without specific styles giving it a size) which is why nothing is displayed on the page when
HomePage is launched.
Let’s add some content to the widget’s root element, using jQuery:
That message will now appear when you open
HomePage widget is used by Odoo Web and managed automatically. To learn how to use a widget “from scratch” let’s create a new one:
We can now add our
GreetingsWidget to the
HomePage by using the
HomePagefirst adds its own content to its DOM root
Finally it tells
GreetingsWidgetwhere to insert itself, delegating part of its
appendTo() method is called, it asks the widget to insert itself at the specified position and to display its content. The
start() method will be called during the call to
To see what happens under the displayed interface, we will use the browser’s DOM Explorer. But first let’s alter our widgets slightly so we can more easily find where they are, by
adding a class to their root elements:
If you can find the relevant section of the DOM (right-click on the text then Inspect Element), it should look like this:
Which clearly shows the two
<div> elements automatically created by
Widget(), because we added some classes on them.
We can also see the two message-holding divs we added ourselves
Finally, note the
<div class="oe_petstore_greetings"> element which represents the
GreetingsWidget instance is inside the
<div class="oe_petstore_homepage"> which represents the
HomePage instance, since we appended
Widget Parents and Children
In the previous part, we instantiated a widget using this syntax:
The first argument is
this, which in that case was a
HomePage instance. This tells the widget being created which other widget is its parent.
As we’ve seen, widgets are usually inserted in the DOM by another widget and inside that other widget’s root element. This means most widgets are “part” of another widget, and exist on behalf of it. We call the container the parent, and the contained widget the child.
Due to multiple technical and conceptual reasons, it is necessary for a widget to know who is its parent and who are its children.
can be used to get the parent of a widget:
can be used to get a list of its children:
When overriding the
init() method of a widget it is of the utmost importance to pass the parent to the
this._super() call, otherwise the relation will not be set up correctly:
Finally, if a widget does not have a parent (e.g. because it’s the root widget of the application),
null can be provided as parent:
If you can display content to your users, you should also be able to erase it. This is done via the
When a widget is destroyed it will first call
destroy() on all its children. Then it erases itself from the DOM. If you have set up permanent structures in
start() which must be explicitly cleaned up (because the garbage collector will not handle them), you can override
The QWeb Template Engine
In the previous section we added content to our widgets by directly manipulating (and adding to) their DOM:
This allows generating and displaying any type of content, but gets unwieldy when generating significant amounts of DOM (lots of duplication, quoting issues, …)
Each template file (XML files) contains multiple templates
It has special support in Odoo Web’s
Widget(), though it can be used outside of Odoo’s web client (and it’s possible to use
Widget()without relying on QWeb)
First let’s define a simple QWeb template in the almost-empty
Now we can use this template inside of the
HomePage widget. Using the
QWeb loader variable defined at the top of the page, we can call to the template defined in the XML file:
QWeb.render() looks for the specified template, renders it to a string and returns the result.
Widget() has special integration for QWeb the template can be set directly on the widget via its
Although the result looks similar, there are two differences between these usages:
with the second version, the template is rendered right before
in the first version the template’s content is added to the widget’s root element, whereas in the second version the template’s root element is directly set as the widget’s root element. Which is why the “greetings” sub-widget also gets a red background
QWeb templates can be given data and can contain basic display logic.
For explicit calls to
QWeb.render(), the template data is passed as second parameter:
with the template modified to:
will result in:
Widget()’s integration it is not possible to provide additional data to the template. The template will be given a single
widget context variable, referencing the widget being rendered right before
start() is called (the widget’s state will essentially be that set up by
We’ve seen how to render QWeb templates, let’s now see the syntax of the templates themselves.
A QWeb template is composed of regular XML mixed with QWeb directives. A QWeb directive is declared with XML attributes starting with
The most basic directive is
t-name, used to declare new templates in a template file:
t-name takes the name of the template being defined, and declares that it can be called using
QWeb.render(). It can only be used at the top-level of a template file.
t-esc directive can be used to output text:
or method calls:
To inject HTML in the page being rendered, use
QWeb can have conditional blocks using
t-if. The directive takes an arbitrary expression, if the expression is falsy (
0 or an empty string) the whole block is suppressed, otherwise it is displayed.
To iterate on a list, use
t-foreach takes an expression returning a list to iterate on
t-as takes a variable name to bind to each item during iteration.
QWeb provides two related directives to define computed attributes:
t-attf-name. In either case, name is the name of the attribute to create (e.g.
t-att-id defines the attribute
id after rendering).
}}, which will be replaced by the result of the expression. It is most useful for attributes which are partially literal and partially computed such as a class:
Calling other templates
Templates can be split into sub-templates (for simplicity, maintainability, reusability or to avoid excessive markup nesting).
This is done using the
t-call directive, which takes the name of the template to render:
A template will result in:
Sub-templates inherit the rendering context of their caller.
To Learn More About QWeb
For a QWeb reference, see QWeb Templates.
Widget’s jQuery Selector
Selecting DOM elements within a widget can be performed by calling the
find() method on the widget’s DOM root:
But because it’s a common operation,
Widget() provides an equivalent shortcut through the
Easier DOM Events Binding
We have previously bound DOM events using normal jQuery event handlers (e.g.
.change()) on widget elements:
While this works it has a few issues:
it is rather verbose
it does not support replacing the widget’s root element at runtime as the binding is only performed when
start()is run (during widget initialization)
it requires dealing with
Widgets thus provide a shortcut to DOM event binding via
events is an object (mapping) of an event to the function or method to call when the event is triggered:
the key is an event name, possibly refined with a CSS selector in which case only if the event happens on a selected sub-element will the function or method run:
clickwill handle all clicks within the widget, but
click .my_buttonwill only handle clicks in elements bearing the
the value is the action to perform when the event is triggered
It can be either a function:
or the name of a method on the object (see example above).
In either case, the
thisis the widget instance and the handler is given a single parameter, the jQuery event object for the event.
Widget Events and Properties
Widgets provide an event system (separate from the DOM/jQuery event system described above): a widget can fire events on itself, and other widgets (or itself) can bind themselves and listen for these events:
This widget acts as a facade, transforming user input (through DOM events) into a documentable internal event to which parent widgets can bind themselves.
trigger() takes the name of the event to trigger as its first (mandatory) argument, any further arguments are treated as event data and passed directly to listeners.
We can then set up a parent event instantiating our generic widget and listening to the
user_chose event using
on() binds a function to be called when the event identified by
event_name is. The
func argument is the function to call and
object is the object to which that function is related if it is a method. The bound function will be called with the additional arguments of
trigger() if it has any. Example:
Properties are very similar to normal object attributes in that they allow storing data on a widget instance, however they have the additional feature that they trigger events when set:
set()sets the value of a property and triggers
change:propname(where propname is the property name passed as first parameter to
get()retrieves the value of a property.
Modify existing widgets and classes
The class system of the Odoo web framework allows direct modification of existing classes using the
This system is similar to the inheritance mechanism, except it will alter the target class in-place instead of creating a new class.
In that case,
this._super() will call the original implementation of a method being replaced/redefined. If the class already had sub-classes, all calls to
this._super() in sub-classes will call the new implementations defined in the call to
include(). This will also work if some instances of the class (or of any of its sub-classes) were created prior to the call to
In Odoo, translations files are automatically generated by scanning the source code. All piece of code that calls a certain function are detected and their content is added to a translation file that will then be sent to the translators. In Python, the function is
_t() (and also
_t() will return the translation defined for the text it is given. If no translation is defined for that text, it will return the original text as-is.
_lt() (“lazy translate”) is similar but somewhat more complex: instead of translating its parameter immediately, it returns an object which, when converted to a string, will perform the translation.
It is used to define translatable terms before the translations system is initialized, for class attributes for instance (as modules are loaded before the user’s language is configured and translations are downloaded).
Communication with the Odoo Server
Most operations with Odoo involve communicating with models implementing business concern, these models will then (potentially) interact with some storage engine (usually PostgreSQL).
Although jQuery provides a $.ajax function for network interactions, communicating with Odoo requires additional metadata whose setup before every call would be verbose and error-prone. As a result, Odoo web provides higher-level communication primitives.
To demonstrate this, the file
petstore.py already contains a small model with a sample method:
This declares a model with two fields, and a method
my_method() which returns a literal dictionary.
Here is a sample widget that calls
my_method() and displays the result:
The class used to call Odoo models is
odoo.Model(). It is instantiated with the Odoo model’s name as first parameter (
call() can be used to call any (public) method of an Odoo model. It takes the following positional arguments:
The name of the method to call,
an array of positional arguments to provide to the method. Because the example has no positional argument to provide, the
argsparameter is not provided.
Here is an other example with positional arguments:
a mapping of keyword arguments to pass. The example provides a single named argument
call() returns a deferred resolved with the value returned by the model’s method as first argument.
The previous section used a
context argument which was not explained in the method call:
The context is like a “magic” argument that the web client will always give to the server when calling a method. The context is a dictionary containing multiple keys. One of the most important key is the language of the user, used by the server to translate all the messages of the application. Another one is the time zone of the user, used to compute correctly dates and times if Odoo is used by people in different countries.
argument is necessary in all methods, otherwise bad things could happen (such as the application not being translated correctly). That’s why, when you call a model’s method, you should always provide that argument. The solution to achieve that is to use
CompoundContext() is a class used to pass the user’s context (with language, time zone, etc…) to the server as well as adding new keys to the context (some models’ methods use arbitrary keys added to the context). It is created by giving to its constructor any number of dictionaries or other
CompoundContext() instances. It will merge all those contexts before sending them to the server.
You can see the dictionary in the argument
context contains some keys that are related to the configuration of the current user in Odoo plus the
new_key key that was added when instantiating
call() is sufficient for any interaction with Odoo models, Odoo Web provides a helper for simpler and clearer querying of models (fetching of records based on various conditions):
query() which acts as a shortcut for the common combination of
search() and :
read(). It provides a clearer syntax to search and read models:
query()takes an optional list of fields as parameter (if no field is provided, all fields of the model are fetched). It returns a
odoo.web.Query()which can be further customized before being executed
Query()represents the query being built. It is immutable, methods to customize the query actually return a modified copy, so it’s possible to use the original and the new version side-by-side. See
Query()for its customization options.
When the query is set up as desired, simply call
all() to execute it and return a deferred to its result. The result is the same as
read()’s, an array of dictionaries where each dictionary is a requested record, with each requested field a dictionary key.
Existing web components
The Action Manager
In Odoo, many operations start from an action: opening a menu item (to a view), printing a report, …
In Odoo Web, the component responsible for handling and reacting to these actions is the Action Manager.
Using the Action Manager
do_action() is a shortcut of
Widget() looking up the “current” action manager and executing the action:
The most common action
ir.actions.act_window which provides views to a model (displays a model in various manners), its most common attributes are:
The model to display in views
For form views, a preselected record in
Lists the views available through the action. A list of
view_idcan either be the database identifier of a view of the right type, or
falseto use the view by default for the specified type. View types can not be present multiple times. The action will open the first view of the list by default.
current(the default) which replaces the “content” section of the web client by the action, or
newto open the action in a dialog box.
Additional context data to use within the action.
Throughout this guide, we used a simple
HomePage widget which the web client automatically starts when we select the right menu item. But how did the Odoo web know to start this widget? Because the widget is registered as a client action.
Our widget is registered as the handler for the client action through this:
instance.web.client_actions is a
Registry() in which the action manager looks up client action handlers when it needs to execute one. The first parameter of
add() is the name (tag) of the client action, and the second parameter is the path to the widget from the Odoo web client root.
When a client action must be executed, the action manager looks up its tag in the registry, walks the specified path and displays the widget it finds at the end.
On the server side, we had simply defined an
and a menu opening the action:
Architecture of the Views
Much of Odoo web’s usefulness (and complexity) resides in views. Each view type is a way of displaying a model in the client.
The View Manager
ActionManager instance receive an action of type
ir.actions.act_window, it delegates the synchronization and handling of the views themselves to a view manager, which will then set up one or multiple views depending on the original action’s requirements:
Most Odoo views are implemented through a subclass of
odoo.web.View() which provides a bit of generic basic structure for handling events and displaying model information.
The search view is considered a view type by the main Odoo framework, but handled separately by the web client (as it’s a more permanent fixture and can interact with other views, which regular views don’t do).
A view is responsible for loading its own description XML (using
fields_view_get) and any other data source it needs. To that purpose, views are provided with an optional view identifier set as the
Views are also provided with a
DataSet() instance which holds most necessary model information (the model name and possibly various record ids).
Views may also want to handle search queries by overriding
do_search(), and updating their
DataSet() as necessary.
The Form View Fields
A common need is the extension of the web form view to add new ways of displaying fields.
All built-in fields have a default display implementation, a new form widget may be necessary to correctly interact with a new field type (e.g. a GIS field) or to provide new representations and ways to interact with existing field types (e.g. validate
Char fields which should contain email addresses and display them as email links).
To explicitly specify which form widget should be used to display a field, simply use the
widget attribute in the view’s XML description:
Fields are instantiated by the form view after it has read its XML description and constructed the corresponding HTML representing that description. After that, the form view will communicate with the field objects using some methods. These methods are defined by the
FieldInterface interface. Almost all fields inherit the
AbstractField abstract class. That class defines some default mechanisms that need to be implemented by most fields.
Here are some of the responsibilities of a field class:
The field class must display and allow the user to edit the value of the field.
It must correctly implement the 3 field attributes available in all fields of Odoo. The
AbstractFieldclass already implements an algorithm that dynamically calculates the value of these attributes (they can change at any moment because their value change according to the value of other fields). Their values are stored in Widget Properties (the widget properties were explained earlier in this guide). It is the responsibility of each field class to check these widget properties and dynamically adapt depending of their values. Here is a description of each of these attributes:
required: The field must have a value before saving. If
trueand the field doesn’t have a value, the method
is_valid()of the field must return
invisible: When this is
true, the field must be invisible. The
AbstractFieldclass already has a basic implementation of this behavior that fits most fields.
true, the field must not be editable by the user. Most fields in Odoo have a completely different behavior depending on the value of
readonly. As example, the
FieldChardisplays an HTML
<input>when it is editable and simply displays the text when it is read-only. This also means it has much more code it would need to implement only one behavior, but this is necessary to ensure a good user experience.
Fields have two methods,
get_value(), which are called by the form view to give it the value to display and get back the new value entered by the user. These methods must be able to handle the value as given by the Odoo server when a
read()is performed on a model and give back a valid value for a
read()and given to
write()is not necessarily the same in Odoo. As example, when you read a many2one, it is always a tuple whose first value is the id of the pointed record and the second one is the name get (ie:
(15, "Agrolait")). But when you write a many2one it must be a single integer, not a tuple anymore.
AbstractFieldhas a default implementation of these methods that works well for simple data type and set a widget property named
Please note that, to better understand how to implement fields, you are strongly encouraged to look at the definition of the
FieldInterface interface and the
AbstractField class directly in the code of the Odoo web client.
Creating a New Type of Field
In this part we will explain how to create a new type of field. The example here will be to re-implement the
FieldChar class and progressively explain each part.
Simple Read-Only Field
Here is a first implementation that will only display text. The user will not be able to modify the content of the field.
In this example, we declare a class named
FieldChar2 inheriting from
AbstractField. We also register this class in the registry
instance.web.form.widgets under the key
char2. That will allow us to use this new field in any form view by specifying
widget="char2" in the
<field/> tag in the XML declaration of the view.
In this example, we define a single method:
render_value(). All it does is display the widget property
value. Those are two tools defined by the
AbstractField class. As explained before, the form view will call the method
set_value() of the field to set the value to display. This method already has a default implementation in
AbstractField which simply sets the widget property
AbstractField also watch the
change:value event on itself and calls the
render_value() when it occurs. So,
render_value() is a convenience method to implement in child classes to perform some operation each time the value of the field changes.
init() method, we also define the default value of the field if none is specified by the form view (here we assume the default value of a
char field should be an empty string).
Read-only fields, which only display content and don’t allow the user to modify it can be useful, but most fields in Odoo also allow editing. This makes the field classes more complicated, mostly because fields are supposed to handle both editable and non-editable mode, those modes are often completely different (for design and usability purpose) and the fields must be able to switch between modes at any moment.
To know in which mode the current field should be, the
AbstractField class sets a widget property named
effective_readonly. The field should watch for changes in that widget property and display the correct mode accordingly. Example:
start() method (which is called immediately after a widget has been appended to the DOM), we bind on the event
change:effective_readonly. That allows us to redisplay the field each time the widget property
effective_readonly changes. This event handler will call
display_field(), which is also called directly in
display_field() was created specifically for this field, it’s not a method defined in
AbstractField or any other class. We can use this method to display the content of the field depending on the current mode.
From now on the conception of this field is typical, except there is a lot of verifications to know the state of the
In the QWeb template used to display the content of the widget, it displays an
<input type="text" />if we are in read-write mode and nothing in particular in read-only mode.
display_field()method, we have to bind on the
changeevent of the
<input type="text" />to know when the user has changed the value. When it happens, we call the
internal_set_value()method with the new value of the field. This is a convenience method provided by the
AbstractFieldclass. That method will set a new value in the
valueproperty but will not trigger a call to
render_value()(which is not necessary since the
<input type="text" />already contains the correct value).
render_value(), we use a completely different code to display the value of the field depending if we are in read-only or in read-write mode.
The Form View Custom Widgets
Form fields are used to edit a single field, and are intrinsically linked to a field. Because this may be limiting, it is also possible to create form widgets which are not so restricted and have less ties to a specific lifecycle.
Custom form widgets can be added to a form view through the
This type of widget will simply be created by the form view during the creation of the HTML according to the XML definition. They have properties in common with the fields (like the
effective_readonly property) but they are not assigned a precise field. And so they don’t have methods like
set_value(). They must inherit from the
FormWidget abstract class.
Form widgets can interact with form fields by listening for their changes and fetching or altering their values. They can access form fields through their
FormWidget is generally the
FormView() itself, but features used from it should be limited to those defined by
FieldManagerMixin(), the most useful being:
get_field_value(field_name)()which returns the value of a field.
set_values(values)()sets multiple field values, takes a mapping of
field_changed:field_nameis triggered any time the value of the field called
as a separate concept from instances. In many languages classes are full-fledged objects and themselves instance (of metaclasses) but there remains two fairly separate hierarchies between classes and instances
as well as papering over cross-browser differences, although this has become less necessary over time