Mantenir un mòdul d'Odoo a la 19, la 18 i la 17 alhora no és copiar la carpeta a tres branques i canviar el número del manifest. És una branca de git per versió, un build d'instal·lació real per versió i un inventari de diferències entre versions que es revisa a cada release. Nosaltres mantenim 105 mòduls publicats a les tres versions; aquesta és la llista del que es trenca, incloent-hi els nostres propis errors.
Una fitxa, tres branques (19.0, 18.0 i 17.0 amb el mateix nom tècnic) està explicat a publicar un mòdul a l'App Store d'Odoo. Aquí va el que passa dins de cada branca.
El que canvia entre la 17, la 18 i la 19 i no surt a les guies de novetats
Les novetats d'Odoo 19 parlen de funcionalitat; aquesta taula, del que falla en instal·lar o, pitjor, no falla. Està comprovada contra el codi font de les tres versions, no contra un changelog.
| Què | Odoo 17 | Odoo 18 | Odoo 19 | Com falla |
|---|---|---|---|---|
| Restriccions SQL | _sql_constraints | _sql_constraints | models.Constraint | En silenci: la 19 avisa i continua; la restricció no existeix |
| Cron sense límit | numbercall=-1 obligatori | No existeix el camp | No existeix el camp | 17: sense ell s'executa una vegada i s'apaga. 18/19: amb ell no s'instal·la |
| Grups de l'usuari | groups_id | groups_id | group_ids | Camp invàlid en instal·lar o executar |
| Usuaris del grup | users | users | user_ids | Igual; ens va rebentar un cron d'avisos a la 17 i la 18 |
| Categoria del grup | category_id | category_id | privilege_id → res.groups.privilege | Invalid field 'category_id' in 'res.groups' |
| Vista de llista | <tree> | <list> | <list> | Tipus de vista invàlid; els xpath //tree no quadren |
El més perillós: _sql_constraints
A Odoo 19 _sql_constraints s'ignora: el carregador de models escriu «no longer supported» al log i continua. El mòdul s'instal·la, els tests passen i la restricció d'unicitat no és a la base de dades; un connector que confia en unique(external_id) per no duplicar comandes perd aquesta garantia sense que res ho digui. La forma nova, models.Constraint('unique(...)', 'mensaje'), només existeix a la 19. Odoo porta un conversor, odoo/cli/upgrade_code.py --script 18.1-00-sql-constraint, amb una trampa que vam aprendre migrant les nostres: retorna codi de sortida 1 quan ha canviat fitxers, o sigui, 1 és èxit. L'única cosa que demostra que la restricció és viva és un test que intenti el duplicat.
El que ens va enxampar a nosaltres: numbercall
A Odoo 17, ir.cron.numbercall val 1 per defecte: un cron declarat sense aquest camp s'executa una vegada i es desactiva sol, sense error ni línia de log. Odoo 18 va eliminar el camp i des d'aleshores declarar-lo impedeix instal·lar, així que la branca 17 necessita <field name="numbercall">-1</field> a cada cron i les altres dues no el poden dur. Al setembre de 2026 vam auditar els nostres propis ports a la 17 i vam trobar 14 crons sense aquest camp en quatre connectors, dos amb demo en marxa: ningú prova una sincronització periòdica esperant la segona execució. La correcció són dues línies per cron; el cost va ser no tenir-ho a l'inventari.
El que Odoo 17 valida i la 18 i la 19 deixen passar
El trànsit no va només cap amunt. Odoo 17 exigeix que un camp usat en un modificador de vista (invisible, readonly) sigui a aquesta mateixa vista; la 18 i la 19 no ho comproven. Un mòdul escrit a la 19 i portat cap enrere falla a la 17 amb Field 'state' used in modifier 'invisible' (state == 'draft') must be present in view but is missing. Per això un port no es dona per bo fins que s'instal·la a la versió més antiga. Més casos a un mòdul d'Odoo 19 compila però no s'instal·la i errors de migració a Odoo 19.
Estratègia de branques: una per versió i cap condicional
La branca principal és la de la versió més nova, avui 19.0. Cada canvi es fa i es prova allà i després es porta cap enrere amb cherry-pick a 18.0 i 17.0, adaptant el que la taula obliga a adaptar. El que no fem mai és un sol codi amb if version >= 18 a dins: en surt un mòdul amb les tres API alhora, del qual ningú sap quina de les tres branques està provant. Per als connectors nous generem les branques 18 i 17 des del font de la 19 amb un script que codifica la taula de dalt; no substitueix la prova, substitueix el copiar i enganxar.
Dues excepcions, i totes dues pregunten per una capacitat, no per un número de versió:
- Detecció de camp o model en temps d'execució. Si el mòdul escriu en un camp que no existeix a totes les versions, es pregunta al registre:
'mobile' in partner._fieldso'hr.expense.sheet' in self.env. Mira el que hi ha, no el que hi hauria de ser. - Tests que se salten quan la capacitat no hi és, amb un
skipTestcondicionat a la presència del camp. El que no val és passar amb 0 de 0: un mòdul nostre tenia zero tests a les tres branques; en escriure-li'n 25 van sortir tres errors reals, un el cron d'avisos que feia serviruser_idsa la 17 i la 18.
La prova que val: instal·lar de veritat a les tres
Ni el linter, ni el import, ni una suite verda a la teva màquina demostren que un mòdul s'instal·la: un XML amb numbercall és XML vàlid i un xpath sobre //tree és un xpath vàlid. L'error només apareix quan Odoo carrega el mòdul contra una base de dades d'aquella versió. La nostra regla: un canvi no existeix fins que s'ha instal·lat, net, en un Odoo real de cada versió i, si el mòdul ja és desplegat a algú, també com a actualització des de la versió anterior.
L'upgrade importa per una raó concreta: els grups de base porten noupdate="1", així que un mòdul que afegeix el seu grup de gestor a base.group_system amb implied_ids ho aconsegueix en una instal·lació nova i Odoo ho ignora en silenci en actualitzar. Ho vam veure en una producció d'Odoo.sh: el grup va quedar sense membres i ni l'administrador podia llegir el camp de credencials que en depenia. Cal una migració a migrations/ a més de l'XML, i un test que falli si el grup queda buit.
Fem servir dues classes de banc. Un de proves, un Odoo per versió (en local o en contenidors) que instal·la cada mòdul i executa la seva suite contra les tres: allà van córrer les 37 apps de la nostra primera campanya, allà el connector de Mirakl passa 163 tests a cada versió i el d'Amazon 786 a la 19 i 685 a la 18 i a la 17. I Odoo.sh, un build per branca, per a la instal·lació real. Una volta de gate són uns deu minuts i val cadascun: els dos mòduls que més car ens han costat aquest any tenien tot verd en local i no s'instal·laven (tests verds i el connector falla en producció).
Error propi: la primera campanya de 37 apps la vam donar per acabada amb instal·lació i tests en verd. Quatre dies després, una segona passada sobre 47 apps a Odoo 17 i 18 Community, exercint cada vista amb dades, va deixar 61 de 94 execucions netes: 28 errors d'instal·lació i 4 d'exercici, uns onze errors reals. Xpath sobre //list a la 17, ancoratges sobre camps que no existeixen a la 17. Codi portat cap enrere sense instal·lar-lo a la versió de destinació.
El cost real i quan s'abandona la 17
Tres branques no tripliquen el desenvolupament, perquè la lògica s'escriu una vegada. Tripliquen la prova i la publicació: tres instal·lacions per canvi, tres suites, tres fitxes per revisar, i un inventari que cal rrellegir sencer a cada versió nova, com la 20 (setembre de 2026), que afegeix files a la taula. Odoo treu una versió major l'any i en manté les tres últimes; el dia que publiqui la 20, la 17 surt d'aquesta finestra i es deixa de mantenir. Fins aleshores la branca 17 rep cada correcció: qui continua a la 17 està planificant una migració, i migrarà amb el teu mòdul o sense.
El que sí que redueix el cost és tenir l'inventari com a eina: un escàner de només lectura que assenyala quina línia trepitja quina diferència segons la versió del manifest. El nostre va arribar a marcar 67 mòduls per escriure category_id en un grup, i els 67 estaven bé: aquella línia vivia dins d'un <record> de res.groups.privilege, que és exactament l'arranjament correcte de la 19. La regla mirava la línia i no el registre que l'envolta. Un recompte sense obrir el codi no és una dada, i una regla que assenyala l'arranjament correcte t'ensenya a ignorar l'eina.
Checklist abans de cada push
- Restriccions:
models.Constrainta la 19,_sql_constraintsa la 18 i la 17, i un test que intenta el duplicat. - Crons:
numbercall=-1a tots els de la branca 17; cap a la 18 i la 19. - Grups:
group_ids,user_idsiprivilege_id(modelres.groups.privilege) a la 19;groups_id,usersicategory_ida la 18 i la 17. - Vistes:
<list>a la 19 i la 18,<tree>a la 17; a la 17, tot camp usat en un modificador és a la vista. - Instal·lació neta a les tres, i actualització des de la versió anterior si ja és desplegat.
- Suite completa en verd a les tres, i no buida: 0 de 0 no és verd.
- Res de
__pycache__al commit. Ungit add -Adesprés dels tests ens va posar 1.078 fitxers.pyca 35 de 37 repositoris.
És el que fem quan un partner d'Odoo ens encarrega mantenir un mòdul seu en diverses versions, o quan una consultoria necessita que un desenvolupament que li funciona a la 17 arribi a la 19 sense perdre la 17. El dreturer és el que descobreix el primer comprador de la fitxa.
Preguntes freqüents
Puc mantenir un sol codi amb condicionals per versió?
L'App Store publica des de branques 17.0, 18.0 i 19.0, així que acabes amb tres branques igualment, només que amb les tres API barrejades a cadascuna. Una branca per versió és més codi repetit i molts menys errors. Les úniques condicions acceptables pregunten per una capacitat, mai pel número de versió.
Per què no n'hi ha prou que compili i que els tests passin en local?
Perquè els errors de versió apareixen en carregar el mòdul contra una base de dades d'aquella versió, no en importar el codi; i alguns, com _sql_constraints a la 19, ni tan sols fallen: cal provar que la restricció existeix.
Enllaços útils dins de FlexigoTech
Tens un mòdul en una versió i el necessites a les tres?
El instal·lem a la 19, la 18 i la 17, et diem què es trenca a cadascuna i el deixem amb una branca per versió i el seu build en verd. Escriu a comercial@flexigobe.com o truca al +34 616 809 504.

