Aller au contenu principal
Développement · Odoo 19, 18 et 17

Maintenir le même module sur Odoo 19, 18 et 17 : ce qui change, ce qui casse en silence et comment le tester pour de vrai

Une branche par version, un build réel par version et un inventaire des différences. Ce qui pose problème entre 17, 18 et 19, vérifié dans le code source d'Odoo, et comment nous le testons avec 105 modules publiés.

Écran Apps d'Odoo avec des modules installés sur un backend réel

Maintenir un module Odoo sur 19, 18 et 17 en même temps, ce n'est pas copier le dossier dans trois branches et changer le numéro du manifest. C'est une branche git par version, un build d'installation réel par version et un inventaire des différences entre versions revu à chaque release. Nous maintenons 105 modules publiés sur les trois versions ; voici la liste de ce qui casse, y compris nos propres erreurs.

Une fiche, trois branches (19.0, 18.0 et 17.0 avec le même nom technique) est expliqué dans publier un module sur l'App Store d'Odoo. Ici, on traite de ce qui se passe à l'intérieur de chaque branche.

Ce qui change entre 17, 18 et 19 et n'apparaît pas dans les guides des nouveautés

Les nouveautés d'Odoo 19 parlent de fonctionnalités ; ce tableau, de ce qui échoue à l'installation ou, pire, n'échoue pas. Il est vérifié sur le code source des trois versions, pas sur un changelog.

QuoiOdoo 17Odoo 18Odoo 19Comment ça échoue
Contraintes SQL_sql_constraints_sql_constraintsmodels.ConstraintEn silence : 19 avertit et continue ; la contrainte n'existe pas
Cron sans limitenumbercall=-1 obligatoireLe champ n'existe pasLe champ n'existe pas17 : sans lui, il s'exécute une fois puis s'éteint. 18/19 : avec lui, l'installation échoue
Groupes de l'utilisateurgroups_idgroups_idgroup_idsChamp invalide à l'installation ou à l'exécution
Utilisateurs du groupeusersusersuser_idsIdentique ; il a fait planter un cron d'alertes en 17 et 18
Catégorie du groupecategory_idcategory_idprivilege_id → res.groups.privilegeInvalid field 'category_id' in 'res.groups'
Vue liste<tree><list><list>Type de vue invalide ; les xpath //tree ne correspondent pas

Le plus dangereux : _sql_constraints

Dans Odoo 19, _sql_constraints est ignoré : le chargeur de modèles écrit « no longer supported » dans le log et continue. Le module s'installe, les tests passent et la contrainte d'unicité n'est pas dans la base de données ; un connecteur qui compte sur unique(external_id) pour ne pas dupliquer des commandes perd cette garantie sans que rien ne le signale. La nouvelle forme, models.Constraint('unique(...)', 'mensaje'), n'existe que dans la 19. Odoo fournit un convertisseur, odoo/cli/upgrade_code.py --script 18.1-00-sql-constraint, avec un piège que nous avons découvert en migrant les nôtres : il renvoie le code de sortie 1 quand il a modifié des fichiers, autrement dit 1 signifie succès. Seul un test qui tente le doublon prouve que la contrainte est bien active.

Celui qui nous a piégés : numbercall

Dans Odoo 17, ir.cron.numbercall vaut 1 par défaut : un cron déclaré sans ce champ s'exécute une fois puis se désactive tout seul, sans erreur ni ligne de log. Odoo 18 a supprimé le champ et, depuis, le déclarer empêche l'installation ; la branche 17 a donc besoin de <field name="numbercall">-1</field> dans chaque cron et les deux autres ne peuvent pas l'avoir. En septembre 2026, nous avons audité nos propres portages vers la 17 et trouvé 14 crons sans ce champ dans quatre connecteurs, dont deux avec une démo en cours : personne ne teste une synchronisation périodique en attendant la deuxième exécution. La correction tient en deux lignes par cron ; le coût, c'était de ne pas l'avoir dans l'inventaire.

Ce qu'Odoo 17 valide et que 18 et 19 laissent passer

Le trafic ne va pas que vers le haut. Odoo 17 exige qu'un champ utilisé dans un modificateur de vue (invisible, readonly) figure dans cette même vue ; 18 et 19 ne le vérifient pas. Un module écrit en 19 et porté vers l'arrière échoue en 17 avec Field 'state' used in modifier 'invisible' (state == 'draft') must be present in view but is missing. C'est pourquoi un portage n'est validé qu'une fois installé sur la version la plus ancienne. Autres cas dans un module Odoo 19 compile mais ne s'installe pas et erreurs de migration vers Odoo 19.

Stratégie de branches : une par version et aucune condition

La branche principale est celle de la version la plus récente, aujourd'hui 19.0. Chaque modification y est faite et testée, puis reportée vers l'arrière par cherry-pick sur 18.0 et 17.0, en adaptant ce que le tableau oblige à adapter. Ce que nous ne faisons jamais, c'est un seul code avec if version >= 18 à l'intérieur : on obtient un module avec les trois API à la fois, dont personne ne sait laquelle des trois branches il teste. Pour les nouveaux connecteurs, nous générons les branches 18 et 17 à partir de la source 19 avec un script qui encode le tableau ci-dessus ; il ne remplace pas le test, il remplace le copier-coller.

Deux exceptions, et toutes deux interrogent une capacité, pas un numéro de version :

  • Détection d'un champ ou d'un modèle à l'exécution. Si le module écrit dans un champ qui n'existe pas dans toutes les versions, on interroge le registre : 'mobile' in partner._fields ou 'hr.expense.sheet' in self.env. On regarde ce qui existe, pas ce qui devrait exister.
  • Tests qui sont ignorés quand la capacité est absente, avec un skipTest conditionné par la présence du champ. Ce qui ne vaut pas, c'est passer avec 0 sur 0 : un de nos modules avait zéro test sur les trois branches ; en lui en écrivant 25, trois vrais défauts sont apparus, dont le cron d'alertes qui utilisait user_ids en 17 et 18.

Le test qui compte : installer pour de vrai sur les trois

Ni le linter, ni import, ni une suite verte sur votre machine ne prouvent qu'un module s'installe : un XML avec numbercall est un XML valide et un xpath sur //tree est un xpath valide. L'échec n'apparaît que lorsque Odoo charge le module sur une base de données de cette version. Notre règle : une modification n'existe pas tant qu'elle n'a pas été installée, proprement, sur un Odoo réel de chaque version et, si le module est déjà déployé chez quelqu'un, aussi en mise à jour depuis la version précédente.

La mise à jour compte pour une raison précise : les groupes de base portent noupdate="1", donc un module qui ajoute son groupe de gestionnaire à base.group_system avec implied_ids y parvient sur une nouvelle installation, et Odoo l'ignore en silence lors de la mise à jour. Nous l'avons vu sur une production Odoo.sh : le groupe s'est retrouvé sans membres et même l'administrateur ne pouvait pas lire le champ d'identifiants qui en dépendait. Il faut une migration dans migrations/ en plus du XML, et un test qui échoue si le groupe se retrouve vide.

Nous utilisons deux types de banc. Un banc de tests, un Odoo par version (en local ou en conteneurs) qui installe chaque module et exécute sa suite sur les trois : c'est là qu'ont tourné les 37 apps de notre première campagne, là que le connecteur Mirakl passe 163 tests sur chaque version et celui d'Amazon 786 en 19 et 685 en 18 et en 17. Et Odoo.sh, un build par branche, pour l'installation réelle. Un tour de validation dure une dizaine de minutes et chacune compte : les deux modules qui nous ont coûté le plus cher cette année avaient tout au vert en local et ne s'installaient pas (tests verts et le connecteur échoue en production).

Notre propre échec : nous avions considéré la première campagne de 37 apps comme terminée avec installation et tests au vert. Quatre jours plus tard, une seconde passe sur 47 apps en Odoo 17 et 18 Community, en exerçant chaque vue avec des données, a laissé 61 exécutions propres sur 94 : 28 échecs d'installation et 4 d'exercice, soit une onze de vrais défauts. Des xpath sur //list en 17, des ancrages sur des champs qui n'existent pas en 17. Du code porté vers l'arrière sans l'installer sur la version cible.

Le coût réel et quand abandonner la 17

Trois branches ne triplent pas le développement, car la logique s'écrit une seule fois. Elles triplent le test et la publication : trois installations par modification, trois suites, trois fiches à vérifier, et un inventaire à relire en entier à chaque nouvelle version, comme la 20 (septembre 2026), qui ajoute des lignes au tableau. Odoo sort une version majeure par an et maintient les trois dernières ; le jour où elle publie la 20, la 17 sort de cette fenêtre et n'est plus maintenue. D'ici là, la branche 17 reçoit chaque correction : qui est encore en 17 prépare une migration, et migrera avec votre module ou sans lui.

Ce qui réduit vraiment le coût, c'est d'avoir l'inventaire comme outil : un scanner en lecture seule qui signale quelle ligne heurte quelle différence selon la version du manifest. Le nôtre a fini par signaler 67 modules pour avoir écrit category_id sur un groupe, et les 67 étaient corrects : cette ligne se trouvait dans un <record> de res.groups.privilege, ce qui est exactement la bonne correction pour la 19. La règle regardait la ligne et non l'enregistrement qui l'entoure. Un décompte sans ouvrir le code n'est pas une donnée, et une règle qui signale la bonne correction apprend à ignorer l'outil.

Checklist avant chaque push

  1. Contraintes : models.Constraint en 19, _sql_constraints en 18 et 17, et un test qui tente le doublon.
  2. Crons : numbercall=-1 dans tous ceux de la branche 17 ; aucun en 18 et 19.
  3. Groupes : group_ids, user_ids et privilege_id (modèle res.groups.privilege) en 19 ; groups_id, users et category_id en 18 et 17.
  4. Vues : <list> en 19 et 18, <tree> en 17 ; en 17, tout champ utilisé dans un modificateur figure dans la vue.
  5. Installation propre sur les trois, et mise à jour depuis la version précédente si le module est déjà déployé.
  6. Suite complète au vert sur les trois, et non vide : 0 sur 0 n'est pas au vert.
  7. Pas de __pycache__ dans le commit. Un git add -A après les tests nous a mis 1 078 fichiers .pyc dans 35 dépôts sur 37.

C'est ce que nous faisons quand un partner d'Odoo nous confie la maintenance d'un de ses modules sur plusieurs versions, ou quand un cabinet de conseil a besoin qu'un développement qui fonctionne pour lui en 17 arrive en 19 sans perdre la 17. Le raccourci, c'est ce que découvre le premier acheteur de la fiche.

Questions fréquentes

Puis-je maintenir un seul code avec des conditions par version ?

L'App Store publie depuis les branches 17.0, 18.0 et 19.0 ; vous vous retrouvez donc de toute façon avec trois branches, simplement avec les trois API mélangées dans chacune. Une branche par version, c'est plus de code répété et beaucoup moins de défauts. Les seules conditions acceptables interrogent une capacité, jamais le numéro de version.

Pourquoi ne suffit-il pas que ça compile et que les tests passent en local ?

Parce que les échecs liés à la version apparaissent au chargement du module sur une base de données de cette version, pas à l'import du code ; et certains, comme _sql_constraints en 19, n'échouent même pas : il faut prouver que la contrainte existe.

Liens utiles chez FlexigoTech

Publier un module sur l'App Store d'OdooUne fiche, trois branches : dépôt, manifest et nouveau scanUn module Odoo 19 compile mais ne s'installe pasLes pièges d'API de la 19 que l'analyse statique ne voit pasTests verts et le connecteur échoue en productionPourquoi le test qui compte est l'installation réelleMigration vers Odoo 19Service de migration, modules propres inclus

Un module sur une version, et il vous le faut sur les trois ?

Nous l'installons sur 19, 18 et 17, nous vous disons ce qui casse dans chacune et nous le laissons avec une branche par version et son build au vert. Écrivez à comercial@flexigobe.com ou appelez le +34 616 809 504.

Parler à un ingénieur