22 min read

In this article by, Daniel Reis, the author of the book Odoo 10 Development Essentials, we will create our first Odoo application and learn the steps needed make it available to Odoo and install it.

(For more resources related to this topic, see here.)

Inspired by the notable http://todomvc.com/ project, we will build a simple To-Do application. It should allow us to add new tasks, mark them as completed, and finally clear the task list from all the already completed tasks.

Understanding applications and modules

It’s common to hear about Odoo modules and applications. But what exactly is the difference between them?

Module add-ons are building blocks for Odoo applications. A module can add new features to Odoo, or modify existing ones. It is a directory containing a manifest, or descriptor file, named __manifest__.py, plus the remaining files that implement its features.

Applications are the way major features are added to Odoo. They provide the core elements for a functional area, such as Accounting or HR, based on which additional add-on modules modify or extend features. Because of this, they are highlighted in the Odoo Apps menu.

If your module is complex, and adds new or major functionality to Odoo, you might consider creating it as an application. If you module just makes changes to existing functionality in Odoo, it is likely not an application.

Whether a module is an application or not is defined in the manifest. Technically is does not have any particular effect on how the add-on module behaves. It is only used for highlight on the Apps list.

Creating the module basic skeleton

We should have the Odoo server at ~/odoo-dev/odoo/. To keep things tidy, we will create a new directory alongside it to host our custom modules, at ~/odoo-dev/custom-addons.

Odoo includes a scaffold command to automatically create a new module directory, with a basic structure already in place. You can learn more about it with:

$ ~/odoo-dev/odoo/odoo-bin scaffold --help

You might want to keep this in mind when you start working your next module, but we won’t be using it right now, since we will prefer to manually create all the structure for our module.

An Odoo add-on module is a directory containing a __manifest__.py descriptor file.

In previous versions, this descriptor file was named __openerp__.py. This name is still supported, but is deprecated.

It also needs to be Python-importable, so it must also have an __init__.py file.

The module’s directory name is its technical name. We will use todo_app for it. The technical name must be a valid Python identifier: it should begin with a letter and can only contain letters, numbers, and the underscore character.

The following commands create the module directory and create an empty __init__.py file in it, ~/odoo-dev/custom-addons/todo_app/__init__.py.

In case you would like to do that directly from the command line, this is what you would use:

$ mkdir ~/odoo-dev/custom-addons/todo_app
$ touch ~/odoo-dev/custom-addons/todo_app/__init__.py

Next, we need to create the descriptor file. It should contain only a Python dictionary with about a dozen possible attributes; of this, only the name attribute is required. A longer description attribute and the author attribute also have some visibility and are advised.

We should now add a __manifest__.py file alongside the __init__.py file with the following content:

    'name': 'To-Do Application',
    'description': 'Manage your personal To-Do tasks.',
    'author': 'Daniel Reis',
    'depends': ['base'],
    'application': True,

The depends attribute can have a list of other modules that are required. Odoo will have them automatically installed when this module is installed. It’s not a mandatory attribute, but it’s advised to always have it. If no particular dependencies are needed, we should depend on the core base module.

You should be careful to ensure all dependencies are explicitly set here; otherwise, the module may fail to install in a clean database (due to missing dependencies) or have loading errors, if by chance the other required modules are loaded afterwards.

For our application, we don’t need any specific dependencies, so we depend on the base module only.

To be concise, we chose to use very few descriptor keys, but in a real word scenario, we recommend that you also use the additional keys since they are relevant for the Odoo apps store:

  • summary: This is displayed as a subtitle for the module.
  • version: By default, is 1.0. It should follow semantic versioning rules (see http://semver.org/ for details).
  • license: By default, is LGPL-3.
  • website: This is a URL to find more information about the module. This can help people find more documentation or the issue tracker to file bugs and suggestions.
  • category: This is the functional category of the module, which defaults to Uncategorized. The list of existing categories can be found in the security groups form (Settings | User | Groups), in the Application field drop-down list.

These other descriptor keys are also available:

  • installable: It is by default True but can be set to False to disable a module.
  • auto_install: If the auto_install module is set to True, this module will be automatically installed, provided all its dependencies are already installed. It is used for the Glue modules.

Since Odoo 8.0, instead of the description key, we can use a README.rst or README.md file in the module’s top directory.

A word about licenses

Choosing a license for your work is very important, and you should consider carefully what is the best choice for you, and its implications. The most used licenses for Odoo modules are the GNU Lesser General Public License (LGLP) and the Affero General Public License (AGPL). The LGPL is more permissive and allows commercial derivate work, without the need to share the corresponding source code. The AGPL is a stronger open source license, and requires derivate work and service hosting to share their source code. Learn more about the GNU licenses at https://www.gnu.org/licenses/.

Adding to the add-ons path

Now that we have a minimalistic new module, we want to make it available to the Odoo instance. For that, we need to make sure the directory containing the module is in the add-ons path, and then update the Odoo module list.

We will position in our work directory and start the server with the appropriate add-ons path configuration:

$ cd ~/odoo-dev
$ ./odoo/odoo-bin -d todo --addons-path="custom-addons,odoo/addons" --save

The –save option saves the options you used in a config file. This spares us from repeating them every time we restart the server: just run ./odoo-bin and the last saved options will be used.

Look closely at the server log. It should have an INFO ? odoo: addons paths:[…] line. It should include our custom-addons directory.

Remember to also include any other add-ons directories you might be using. For instance, if you also have a ~/odoo-dev/extra directory containing additional modules to be used, you might want to include them also using the option:


Now we need the Odoo instance to acknowledge the new module we just added.

Installing the new module

In the Apps top menu, select the Update Apps List option. This will update the module list, adding any modules that may have been added since the last update to the list. Remember that we need the developer mode enabled for this option to be visible. That is done in the Settings dashboard, in the link at the bottom right, below the Odoo version number information .

Make sure your web client session is working with the right database. You can check that at the top right: the database name is shown in parenthesis, right after the user name. A way to enforce using the correct database is to start the server instance with the additional option –db-filter=^MYDB$.

The Apps option shows us the list of available modules. By default it shows only application modules. Since we created an application module we don’t need to remove that filter to see it. Type todo in the search and you should see our new module, ready to be installed.

Odoo 10 Development Essentials

Now click on the module’s Install button and we’re ready!

The Model layer

Now that Odoo knows about our new module, let’s start by adding a simple model to it.

Models describe business objects, such as an opportunity, sales order, or partner (customer, supplier, and so on.). A model has a list of attributes and can also define its specific business.

Models are implemented using a Python class derived from an Odoo template class. They translate directly to database objects, and Odoo automatically takes care of this when installing or upgrading the module. The mechanism responsible for this is Object Relational Model (ORM).

Our module will be a very simple application to keep to-do tasks. These tasks will have a single text field for the description and a checkbox to mark them as complete. We should later add a button to clean the to-do list from the old completed tasks.

Creating the data model

The Odoo development guidelines state that the Python files for models should be placed inside a models subdirectory. For simplicity, we won’t be following this here, so let’s create a todo_model.py file in the main directory of the todo_app module.

Add the following content to it:

# -*- coding: utf-8 -*-
from odoo import models, fields
class TodoTask(models.Model):
    _name = 'todo.task'
    _description = 'To-do Task'

    name = fields.Char('Description', required=True)
    is_done = fields.Boolean('Done?')
    active = fields.Boolean('Active?', default=True)

The first line is a special marker telling the Python interpreter that this file has UTF-8 so that it can expect and handle non-ASCII characters. We won’t be using any, but it’s a good practice to have it anyway.

The second line is a Python import statement, making available the models and fields objects from the Odoo core.

The third line declares our new model. It’s a class derived from models.Model.

The next line sets the _name attribute defining the identifier that will be used throughout Odoo to refer to this model. Note that the actual Python class name , TodoTask in this case, is meaningless to other Odoo modules. The _name value is what will be used as an identifier.

Notice that this and the following lines are indented. If you’re not familiar with Python, you should know that this is important: indentation defines a nested code block, so these four lines should all be equally indented.

Then we have the _description model attribute. It is not mandatory, but it provides a user friendly name for the model records, that can be used for better user messages.

The last three lines define the model’s fields. It’s worth noting that name and active are special field names. By default, Odoo will use the name field as the record’s title when referencing it from other models. The active field is used to inactivate records, and by default, only active records will be shown. We will use it to clear away completed tasks without actually deleting them from the database.

Right now, this file is not yet used by the module. We must tell Python to load it with the module in the __init__.py file. Let’s edit it to add the following line:

from . import todo_model

That’s it. For our Python code changes to take effect the server instance needs to be restarted (unless it was using the –dev mode).

We won’t see any menu option to access this new model, since we didn’t add them yet. Still we can inspect the newly created model using the Technical menu. In the Settings top menu, go to Technical | Database Structure | Models, search for the todo.task model on the list and then click on it to see its definition:

Odoo 10 Development Essentials

If everything goes right, it is confirmed that the model and fields were created. If you can’t see them here, try a server restart with a module upgrade, as described before.

We can also see some additional fields we didn’t declare. These are reserved fields Odoo automatically adds to every new model. They are as follows:

  • id: A unique, numeric identifier for each record in the model.
  • create_date and create_uid: These specify when the record was created and who created it, respectively.
  • write_date and write_uid: These confirm when the record was last modified and who modified it, respectively.
  • __last_update: This is a helper that is not actually stored in the database. It is used for concurrency checks.

The View layer

The View layer describes the user interface. Views are defined using XML, which is used by the web client framework to generate data-aware HTML views.

We have menu items that can activate the actions that can render views. For example, the Users menu item processes an action also called Users, that in turn renders a series of views. There are several view types available, such as the list and form views, and the filter options made available are also defined by particular type of view, the search view.

The Odoo development guidelines state that the XML files defining the user interface should be placed inside a views/ subdirectory.

Let’s start creating the user interface for our To-Do application.

Adding menu items

Now that we have a model to store our data, we should make it available on the user interface.

For that we should add a menu option to open the To-do Task model so that it can be used.

Create the views/todo_menu.xml file to define a menu item and the action performed by it:

<?xml version="1.0"?>
    <!-- Action to open To-do Task list -->
    <act_window id="action_todo_task"
      name="To-do Task"
      view_mode="tree,form" />
    <!-- Menu item to open To-do Task list -->
    <menuitem id="menu_todo_task"
      action="action_todo_task" />

The user interface, including menu options and actions, is stored in database tables. The XML file is a data file used to load those definitions into the database when the module is installed or upgraded. The preceding code is an Odoo data file, describing two records to add to Odoo:

  • The <act_window> element defines a client-side window action that will open the todo.task model with the tree and form views enabled, in that order
  • The <menuitem> defines a top menu item calling the action_todo_task action, which was defined before

Both elements include an id attribute. This id , also called an XML ID, is very important: it is used to uniquely identify each data element inside the module, and can be used by other elements to reference it. In this case, the <menuitem> element needs to reference the action to process, and needs to make use of the <act_window> id for that.

Our module does not know yet about the new XML data file. This is done by adding it to the data attribute in the __manifest__.py file. It holds the list of files to be loaded by the module. Add this attribute to the descriptor’s dictionary:

'data': ['views/todo_menu.xml'],

Now we need to upgrade the module again for these changes to take effect. Go to the Todos top menu and you should see our new menu option available:

Odoo 10 Development Essentials

Even though we haven’t defined our user interface view, clicking on the Todos menu will open an automatically generated form for our model, allowing us to add and edit records.

Odoo is nice enough to automatically generate them so that we can start working with our model right away.

Odoo supports several types of views, but the three most important ones are: tree (usually called list views), form, and search views. We’ll add an example of each to our module.

Creating the form view

All views are stored in the database, in the ir.ui.view model. To add a view to a module, we declare a <record> element describing the view in an XML file, which is to be loaded into the database when the module is installed.

Add this new views/todo_view.xml file to define our form view:

<?xml version="1.0"?>
  <record id="view_form_todo_task" model="ir.ui.view">
    <field name="name">To-do Task Form</field>
    <field name="model">todo.task</field>
    <field name="arch" type="xml">
      <form string="To-do Task">
          <field name="name"/>
          <field name="is_done"/>
          <field name="active" readonly="1"/>

Remember to add this new file to the data key in manifest file, otherwise our module won’t know about it and it won’t be loaded.

This will add a record to the ir.ui.view model with the identifier view_form_todo_task. The view is for the todo.task model and is named To-do Task Form. The name is just for information; it does not have to be unique, but it should allow one to easily identify which record it refers to. In fact the name can be entirely omitted, in that case it will be automatically generated from the model name and the view type.

The most important attribute is arch, and contains the view definition, highlighted in the XML code above. The <form> tag defines the view type, and in this case contains three fields. We also added an attribute to the active field to make it read-only.

Adding action buttons

Forms can have buttons to perform actions. These buttons are able to trigger workflow actions, run window actions—such as opening another form, or run Python functions defined in the model.

They can be placed anywhere inside a form, but for document-style forms, the recommended place for them is the <header> section.

For our application, we will add two buttons to run the methods of the todo.task model:

  <button name="do_toggle_done" type="object"
    string="Toggle Done" class="oe_highlight" />
  <button name="do_clear_done" type="object"
    string="Clear All Done" />

The basic attributes of a button comprise the following:

  • The string attribute that has the text to be displayed on the button
  • The type attribute referring to the action it performs
  • The name attribute referring to the identifier for that action
  • The class attribute, which is an optional attribute to apply CSS styles, like in regular HTML

The complete form view

At this point, our todo.task form view should look like this:

        <button name="do_toggle_done" type="object"
          string="Toggle Done" class="oe_highlight" />
        <button name="do_clear_done" type="object"
          string="Clear All Done" />
        <group name="group_top">
          <group name="group_left">
            <field name="name"/>
          <group name="group_right">
            <field name="is_done"/>
            <field name="active" readonly="1" />

Remember that for the changes to be loaded to our Odoo database, a module upgrade is needed. To see the changes in the web client, the form needs to be reloaded: either click again on the menu option that opens it or reload the browser page (F5 in most browsers).

The action buttons won’t work yet, since we still need to add their business logic.

The business logic layer

Now we will add some logic to our buttons. This is done with Python code, using the methods in the model’s Python class.

Adding business logic

We should edit the todo_model.py Python file to add to the class the methods called by the buttons. First we need to import the new API, so add it to the import statement at the top of the Python file:

from odoo import models, fields, api

The action of the Toggle Done button will be very simple: just toggle the Is Done? flag. For logic on records, use the @api.multi decorator. Here, self will represent a recordset, and we should then loop through each record.

Inside the TodoTask class, add this:

def do_toggle_done(self):
    for task in self:
        task.is_done = not task.is_done
    return True

The code loops through all the to-do task records, and for each one, modifies the is_done field, inverting its value. The method does not need to return anything, but we should have it to at least return a True value. The reason is that clients can use XML-RPC to call these methods, and this protocol does not support server functions returning just a None value.

For the Clear All Done button, we want to go a little further. It should look for all active records that are done and make them inactive. Usually, form buttons are expected to act only on the selected record, but in this case, we will want it also act on records other than the current one:

def do_clear_done(self):
    dones = self.search([('is_done', '=', True)])
    dones.write({'active': False})
    return True

On methods decorated with @api.model, the self variable represents the model with no record in particular. We will build a dones recordset containing all the tasks that are marked as done. Then, we set on the active flag to False on them.

The search method is an API method that returns the records that meet some conditions. These conditions are written in a domain, which is a list of triplets.

The write method sets the values at once on all the elements of a recordset. The values to write are described using a dictionary. Using write here is more efficient than iterating through the recordset to assign the value to each of them one by one.

Set up access security

You might have noticed that upon loading, our module is getting a warning message in the server log:

The model todo.task has no access rules, consider adding one.

The message is pretty clear: our new model has no access rules, so it can’t be used by anyone other than the admin super user. As a super user, the admin ignores data access rules, and that’s why we were able to use the form without errors. But we must fix this before other users can use our model.

Another issue yet to address is that we want the to-do tasks to be private to each user. Odoo supports row-level access rules, which we will use to implement that.

Adding access control security

To get a picture of what information is needed to add access rules to a model, use the web client and go to Settings | Technical | Security | Access Controls List:

Odoo 10 Development Essentials

Here we can see the ACL for some models. It indicates, per security group, what actions are allowed on records.

This information has to be provided by the module using a data file to load the lines into the ir.model.access model. We will add full access to the Employee group on the model. Employee is the basic access group nearly everyone belongs to.

This is done using a CSV file named security/ir.model.access.csv. Let’s add it with the following content:


The filename corresponds to the model to load the data into, and the first line of the file has the column names. These are the columns provided by the CSV file:

  • id: It is the record external identifier (also known as XML ID). It should be unique in our module.
  • name: This is a description title. It is only informative and it’s best if it’s kept unique. Official modules usually use a dot-separated string with the model name and the group. Following this convention, we used todo.task.user.
  • model_id: This is the external identifier for the model we are giving access to. Models have XML IDs automatically generated by the ORM: for todo.task, the identifier is model_todo_task.
  • group_id: This identifies the security group to give permissions to. The most important ones are provided by the base module. The Employee group is such a case and has the identifier base.group_user.

The last four perm fields flag the access to grant read, write, create, or unlink (delete) access.

We must not forget to add the reference to this new file in the __manifest__.py descriptor’s data attribute It should look like this:

'data': [

As before, upgrade the module for these additions to take effect. The warning message should be gone, and we can confirm that the permissions are OK by logging in with the user demo (password is also demo). If we run our tests now it they should only fail the test_record_rule test case.


We created a new module from the start, covering the most frequently used elements in a module: models, the three basic types of views (form, list, and search), business logic in model methods, and access security. Always remember, when adding model fields, an upgrade is needed. When changing Python code, including the manifest file, a restart is needed. When changing XML or CSV files, an upgrade is needed; also, when in doubt, do both: restart the server and upgrade the modules.

Resources for Article:

Further resources on this subject:


Please enter your comment!
Please enter your name here