Modular development, explained to developers

The dependency tree is the only structure Odoo follows to install, update and load code. Whatever goes around it ends up breaking, usually at the next migration.

What is a module?

A directory that adds something to Odoo: models (the objects, such as an order or a contact), views (the screens), data, access rights and tests. Its __manifest__.py file is its identity card: its name, its version, and above all its depends, the list of modules it needs. Odoo loads modules in the order of those dependencies, and a module only sees what that list gives it.

The rules

  • One need, one module

    A module does one thing, stated in its name (timesheet_billing rather than custom_stuff). It installs alone on an empty database, and its tests pass with its own dependencies only. Nothing is split for the sake of it: things are separated when one half can serve without the other, when a licence requires it, or when an application is optional. Otherwise, they stay together.

  • Dependencies one way only

    The depends list says it all. A module never names a module that depends on it, nor a module absent from its list: not in the code, not in the data files, not in the tests, not in the documentation. If module A needs a field of module B, then A depends on B, never the other way round.

  • A bridge module rather than a guess

    Looking in the code at whether another module is installed, to change behaviour, creates a hidden dependency: Odoo does not see it, and it breaks at the next migration. Instead, a third module, called a bridge, which depends on both and carries the code that links them. Marked auto_install, Odoo installs it by itself as soon as both are there. A script in the repository checks this rule at every change.

  • Extend, do not modify

    To add a field or change the behaviour of an existing module, its files are not touched: one inherits. In Odoo, _inherit adds to an existing model from one's own module, and a method can be completed by redefining it. The original module stays intact, and updates without conflict.

  • The customer's own data, in its module

    The configuration of one deployment (categories, numbering, project stages) lives in a data module that depends on the generic modules, never in the generic ones themselves. They stay reusable, and what belongs to the customer can be seen. Moving a field from one module to another takes a migration script, otherwise the update deletes its data.

  • Remove, do not disable

    An unwanted behaviour is removed: its code, its tests, its documentation, and a migration script for what remains in the database. No option to silence it: it would keep dead code, which would still have to be migrated.

An example: billing the time spent

Three modules, and each installs alone.

  1. timesheet_category

    Adds a category to each time entry: billable or not. Depends on Odoo's timesheet module.

  2. timesheet_budget

    A budget of hours per customer, and what is left of it. Depends on the first.

  3. timesheet_billing

    Turns billable hours into invoice lines. Depends on both, and exists only because a customer needed it.

In practice

The manifest of a bridge module: it depends on the two modules it links, and Odoo installs it by itself when both are there.

# sale_delivery_zone/__manifest__.py
{
    "name": "Sales – delivery zones",
    # The two linked modules: without either, nothing to do.
    "depends": ["sale", "delivery_zone"],
    # Installed by itself as soon as both are present.
    "auto_install": True,
    "license": "AGPL-3",
}

Extend without modifying: the field is added to the existing model from the bridge module, and the original module does not change.

# sale_delivery_zone/models/sale_order.py
from odoo import fields, models


class SaleOrder(models.Model):
    # The sales order is completed, not rewritten.
    _inherit = "sale.order"

    delivery_zone_id = fields.Many2one("delivery.zone")

What it costs

More modules, so more files to name, document and list. One working branch per module. And a refusal, sometimes, to add a field “where it is convenient”. That is the price of an update that holds no surprise.

Do you do it differently?

A disagreement, a better way, a case that does not fit these rules: write to me, I read everything.

Contact me

The other methodologies