Le développement modulaire, expliqué aux développeurs

L’arbre des dépendances est la seule structure qu’Odoo suit pour installer, mettre à jour et charger le code. Tout ce qui le contourne finit par casser, en général à la migration suivante.

Un module, c’est quoi ?

Un dossier qui ajoute quelque chose à Odoo : des modèles (les objets, comme une commande ou un contact), des vues (les écrans), des données, des droits d’accès et des tests. Son fichier __manifest__.py est sa carte d’identité : son nom, sa version, et surtout ses depends, la liste des modules dont il a besoin. Odoo charge les modules dans l’ordre de ces dépendances, et un module ne voit que ce que cette liste lui donne.

Les règles

  • Un besoin, un module

    Un module fait une chose, dite dans son nom (timesheet_billing plutôt que custom_stuff). Il s’installe seul sur une base vide, et ses tests passent avec ses seules dépendances. On ne découpe pas pour le plaisir : on sépare quand une moitié peut servir sans l’autre, quand une licence l’impose, ou quand une application est facultative. Sinon, on garde ensemble.

  • Des dépendances dans un seul sens

    La liste depends dit tout. Un module ne nomme jamais un module qui dépend de lui, ni un module absent de sa liste : ni dans le code, ni dans les fichiers de données, ni dans les tests, ni dans la documentation. Si le module A a besoin d’un champ du module B, c’est A qui dépend de B, jamais l’inverse.

  • Un module de liaison plutôt qu’une devinette

    Regarder dans le code si un autre module est installé, pour changer de comportement, crée une dépendance cachée : Odoo ne la voit pas, et elle casse à la migration suivante. À la place, un troisième module, dit de liaison, qui dépend des deux et porte le code qui les relie. Marqué auto_install, Odoo l’installe tout seul dès que les deux sont là. Un script du dépôt vérifie cette règle à chaque changement.

  • Étendre, pas modifier

    Pour ajouter un champ ou changer le comportement d’un module existant, on ne touche pas à ses fichiers : on hérite. En Odoo, _inherit permet d’ajouter à un modèle existant depuis son propre module, et une méthode peut être complétée en la redéfinissant. Le module d’origine reste intact, et il se met à jour sans conflit.

  • Les données propres au client, dans leur module

    La configuration d’un déploiement (catégories, numérotation, étapes de projet) vit dans un module de données qui dépend des modules génériques, jamais dans les génériques eux-mêmes. Ils restent réutilisables, et ce qui est propre au client se voit. Déplacer un champ d’un module à un autre demande un script de migration, sinon la mise à jour supprime ses données.

  • Supprimer, pas désactiver

    Un comportement non voulu se retire : son code, ses tests, sa documentation, et un script de migration pour ce qui reste en base. Pas d’option pour le taire : elle garderait du code mort, qu’il faudrait encore migrer.

Un exemple : facturer le temps passé

Trois modules, et chacun s’installe seul.

  1. timesheet_category

    Ajoute une catégorie sur chaque ligne de temps : facturable ou non. Dépend du module de feuilles de temps d’Odoo.

  2. timesheet_budget

    Un budget d’heures par client, et ce qu’il en reste. Dépend du premier.

  3. timesheet_billing

    Transforme les heures facturables en lignes de facture. Dépend des deux, et n’existe que parce qu’un client en avait besoin.

En pratique

Le manifeste d’un module de liaison : il dépend des deux modules qu’il relie, et Odoo l’installe de lui-même quand les deux sont là.

# sale_delivery_zone/__manifest__.py
{
    "name": "Ventes – zones de livraison",
    # Les deux modules reliés : sans l'un des deux, rien à faire.
    "depends": ["sale", "delivery_zone"],
    # Installé tout seul dès que les deux sont présents.
    "auto_install": True,
    "license": "AGPL-3",
}

Étendre sans modifier : le champ s’ajoute au modèle existant depuis le module de liaison, et le module d’origine ne change pas.

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


class SaleOrder(models.Model):
    # On complète la commande de vente, on ne la réécrit pas.
    _inherit = "sale.order"

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

Ce que ça coûte

Plus de modules, donc plus de fichiers à nommer, documenter et lister. Une branche de travail par module. Et un refus, parfois, d’ajouter un champ « là où c’est pratique ». C’est le prix d’une mise à jour qui ne surprend pas.

Vous faites autrement ?

Un désaccord, une meilleure façon de faire, un cas qui ne rentre pas dans ces règles : écrivez-moi, je lis tout.

Me contacter

Les autres méthodologies