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 the 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 of the old, completed tasks.

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 code 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 click on it to see its definition:

Creating the data model

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:

Programming best practices include having automated tests for your code. This is even more important for dynamic languages such as Python. Since there is no compilation step, you can't be sure there are no syntactic errors until the code is actually run by the interpreter. A good editor can help us spot these problems ahead of time, but can't help us ensure the code performs as intended like automated tests can.

Odoo supports two ways to describe tests: either using YAML data files or using Python code, based on the Unittest2 library. YAML tests are a legacy from older versions, and are not recommended. We will prefer using Python tests and will add a basic test case to our module.

The test code files should have a name starting with test_ and should be imported from tests/__init__.py. But the tests directory (or Python submodule) should not be imported from the module's top __init__.py, since it will be automatically discovered and loaded only when tests are executed.

Tests must be placed in a tests/ subdirectory. Add a tests/__init__.py file with the following:

from . import test_todo 

Now add the actual test code, available in the tests/test_todo.py file:

# -*- coding: utf-8 -*- 
from odoo.tests.common import TransactionCase 
 
class TestTodo(TransactionCase): 
 
    def test_create(self): 
        "Create a simple Todo" 
        Todo = self.env['todo.task'] 
        task = Todo.create({'name': 'Test Task'}) 
        self.assertEqual(task.is_done, False) 

This adds a simple test case to create a new to-do task and verifies that the Is Done? field has the correct default value.

Now we want to run our tests. This is done by adding the --test-enable option while installing the module:

$ ./odoo-bin -d todo -i todo_app --test-enable

The Odoo server will look for a tests/ subdirectory in the upgraded modules and will run them. If any of the tests fail, the server log will show that.