Salta al contingut principal
Odoo 19 · Instal·lació · Desenvolupament de mòduls

Un mòdul que compila a Odoo 19 i tot i així no s'instal·la

Compilar no és instal·lar, i instal·lar en una base neta no és actualitzar una base que ja tenia la versió anterior. Són tres proves diferents i gairebé tothom fa només la primera.

Si has arribat fins aquí és perquè el build t'ha escopit alguna cosa com ValueError: Invalid field 'category_id' in 'res.groups' i en local no passava res. No és el teu entorn. El linter, el import i els tests unitaris no executen la part d'Odoo que falla: el registre que es construeix en instal·lar.

El terreny del «què canvia a Odoo 19» ja està molt treballat. Això va de l'escaló següent: el mòdul passa el linter, s'importa sense error, i tot i així el -i falla. O pitjor, s'instal·la i es comporta malament en silenci.

Què comprova el teu CI i què no

python -m compileall comprova la sintaxi de Python. xmllint --noout comprova que l'XML està ben format. Una suite de tests «purs», sense Odoo aixecat, comprova la teva lògica. Cap de les tres obre el registre d'Odoo, i és allà on viu l'error.

Instal·lar és una altra cosa: Odoo construeix el registre i cada <record> acaba en un create() o un write() de l'ORM. I write() valida els noms de camp un per un: a odoo/orm/models.py de la 19.0, un KeyError sobre self._fields[field_name] surt com ValueError: Invalid field 'category_id' in 'res.groups'. No és un error de sintaxi; és un error d'execució de l'XML.

I el parany més gran, el que converteix un gate en teatre: un -i sobre un mòdul ja instal·lat no fa res. A odoo/modules/loading.py de la 19.0, update_operation val 'install' només si l'estat és to install; si és installed, val None. I amb None, quan a la línia d'ordres hi ha un -i o un -u, ni es carreguen les dades ni s'entra al bloc de tests. Build verd, zero tests. Abans de creure't un gate, mira quants tests diu que ha executat.

res.groups.privilege no és un camp reanomenat: és un model nou

Les guies de migració porten una taula de reanomenaments i amb això arregles la meitat dels casos. category_id no hi és, perquè Odoo 19 no el va reanomenar: el va treure de res.groups. Un cercar-i-reemplaçar no té a què apuntar.

A 17 i 18, res.groups.category_id apunta a ir.module.category. A 19 hi ha un model intermedi nou, res.groups.privilege (odoo/addons/base/models/res_groups_privilege.py, catorze línies): el grup porta privilege_id i és el privilegi qui porta category_id. De passada, 19 afegeix UNIQUE (privilege_id, name): dos grups anomenats «Manager» sota el mateix privilegi no s'instal·len.

L'arranjament no és canviar un nom: és crear un registre d'un model que a la teva versió d'origen no existia. Així queda als nostres mòduls, amb el comentari que deixem al fitxer perquè no torni a passar:

<record id="module_category_mi_modulo" model="ir.module.category">
  <field name="name">Mi modulo</field>
</record>

<!-- Odoo 19 moved the grouping of access groups out of res.groups: a group
     now points at a res.groups.privilege, and the privilege is what carries
     the module category. Setting category_id on res.groups raises
     "Invalid field 'category_id' in 'res.groups'". -->
<record id="privilege_mi_modulo" model="res.groups.privilege">
  <field name="name">Mi modulo</field>
  <field name="category_id" ref="module_category_mi_modulo"/>
</record>

<record id="group_mi_modulo_user" model="res.groups">
  <field name="name">User</field>
  <field name="privilege_id" ref="privilege_mi_modulo"/>
</record>

Al voltant sí que hi ha reanomenaments de veritat, i aquests sí que els arregla un reemplaçament: res.groups.users passa a user_ids, res.users.groups_id a group_ids, i igual a ir.actions.act_window i ir.actions.server. El que no va canviar són les taules: res_groups_users_rel(gid, uid) i res_groups_implied_rel(gid, hid) són idèntiques a 17, 18 i 19. L'SQL d'una migració sobreviu a les tres sèries; el codi ORM que l'envolta, no.

base.group_system amb noupdate: s'instal·la bé i els permisos es queden com estaven

L'idioma habitual perquè l'administrador hereti el teu grup és un <record> sobre un xmlid aliè:

<record id="base.group_system" model="res.groups">
  <field name="implied_ids" eval="[(4, ref('group_mi_manager'))]"/>
</record>

Això s'aplica en una instal·lació nova. En una actualització, Odoo ho ignora en silenci. La regla és a _load_records (odoo/orm/models.py) i cap en una línia: if not (update and d_noupdate): to_update.append(data). El que decideix no és el teu fitxer, sinó la columna noupdate de la fila que ja existeix a ir_model_data per a aquest xmlid. No ho donis per fet en cap sentit: pregunta-ho a la teva base de dades.

SELECT module, name, noupdate FROM ir_model_data
 WHERE module = 'base' AND name IN ('group_system', 'group_user');

A la nostra producció d'Odoo.sh, comprovat el 3 de setembre de 2026, les dues files tornen amb noupdate = t. Amb aquest valor, l'XML de dalt no es reaplica mai en un upgrade.

Per què importa: si a més poses groups="mi_modulo.group_manager" en un camp de credencials, en una base ja instal·lada aquest grup es queda sense cap membre i ningú (tampoc l'administrador) pot llegir ni escriure aquest camp. El codi intern amb sudo() continua funcionant, així que cap test ho veu: els tests corren sobre una instal·lació neta, on el <record> sí que es va aplicar. Ens va passar exactament així.

La recepta: deixar l'XML, que cobreix instal·lacions noves, i afegir una migració migrations/<serie>.<version>/post-migrate.py que ho escrigui en SQL idempotent sobre res_groups_implied_rel i res_groups_users_rel: implied_ids només propaga als usuaris per l'ORM, així que cal inserir-los també. I un test que falli si el grup es queda buit. La clausura transitiva és all_implied_ids a 19 i trans_implied_ids a 17 i 18.

ir.cron sense numbercall: el cron corre una vegada i s'apaga sol

Aquest no es veu fins a la setmana següent. A Odoo 17, odoo/addons/base/models/ir_cron.py declara numbercall = fields.Integer(..., default=1, ...). Un <record model="ir.cron"> que no ho posi hereta aquest 1. En acabar la primera execució, call_count_left dona 0 i el UPDATE que tanca l'execució escriu active = job['active'] and bool(call_count_left): fals. Per si quedava algun dubte, la consulta que selecciona feines filtra AND numbercall != 0.

Resultat: el cron s'executa una vegada, es desactiva a si mateix i no deixa rastre. Ni excepció, ni línia de log, ni res a la interfície. Sembla exactament «el cron no salta». L'arranjament és <field name="numbercall">-1</field>, negatiu per «sense límit».

I aquí la simetria desagradable: Odoo 18 va eliminar el camp i 19 mai no el va tenir. Posar numbercall a 19 impedeix instal·lar (Invalid field 'numbercall' in 'ir.cron'); no posar-lo a 17 trenca el funcionament sense dir res. El mateix fitxer falla en dues direccions oposades. Amb doall, igual.

Aquest error es comet també a casa. Auditant el nostre catàleg vam trobar catorze registres ir.cron en quatre connectors portats a 17 sense un sol numbercall; ampliant el rastreig a la branca 17.0 del desplegament que realment s'executa, 130 registres en quinze mòduls, cap amb el camp. Comptat com el que és: ho vam trobar perquè vam anar a buscar-ho amb un grep, no perquè saltés.

L'inventari, i en quina versió mossega cada parany

Això és el que hem recollit instal·lant de debò sobre Odoo.sh a les tres sèries, amb el nostre propi catàleg. Cap d'aquests paranys no el caça un compilador ni un linter. Les files marcades «silenciós» són pitjors: el mòdul s'instal·la i el build surt verd.

Què escriusQuè passaOn mossega
category_id a res.groupsInvalid field 'category_id' in 'res.groups'No s'instal·la · 19
users o groups_idReanomenats a user_ids i group_idsNo s'instal·la · 19
numbercall o doall a ir.cronInvalid field 'numbercall' in 'ir.cron'No s'instal·la · 18 i 19
falta numbercall a ir.cronS'instal·la; el cron corre una vegada i s'apagaSilenciós · només 17
_sql_constraints en un model de 19S'instal·la; un warning al log i la restricció no es creaSilenciós · 19
models.Constraint a 18 o 17module 'odoo.models' has no attribute 'Constraint'No s'instal·la · 18 i 17
<record id="base.group_system">S'instal·la; en un upgrade no es reaplicaSilenciós · les tres
<tree> en una herènciaL'xpath no encaixa i la vista rebentaNo s'instal·la · 18 i 19
Camp citat en un modificador i no renderitzatmust be present in view but is missingNo s'instal·la · només 17

Fixa't en l'última fila: aquesta comprovació només la fa 17. El missatge continua al codi de 18 i 19, però allà només cobreix noms i accions, no un camp citat en un modificador. Si desenvolupes contra 19 i portes cap enrere, la vista que a tu t'instal·la perfectament trenca a 17. Els paranys no van tots en la mateixa direcció.

Com es detecta això de debò: instal·lar dues vegades, en dues bases diferents

Prova A: base neta, -i. Agafa tot el que impedeix instal·lar: camps que ja no existeixen, xpath que no encaixa, vistes invàlides, constraints que la base rebutja. No agafa res del que només passa en actualitzar: aquí update és fals, els noupdate s'ignoren i els scripts de migrations/ ni s'executen, perquè migrate_module (odoo/modules/migration.py) surt per la porta si el mòdul no és a to upgrade.

Prova B: base amb la versió anterior, -u. Agafa el que només apareix en actualitzar: registres que no es reaplican, columnes noves sense omplir, migracions que fallen a mig camí, dades velles que ja no encaixen. Aquí sí que corren els teus pre-migrate i post-migrate, i és l'única manera de saber si funcionen.

Cap de les dues substitueix l'altra, i si publiques per a 17, 18 i 19 són sis instal·lacions, no dues. Un detall pràctic: abans del -i, reseteja amb una desinstal·lació real per ORM. Canviar l'estat a mà amb psql deixa el graf inconsistent i el carregador se salta el mòdul en silenci: una altra vegada un build verd que no ha provat res.

La llista de comprovació abans de pujar

  • grep de category_id en qualsevol <record model="res.groups">. Si en surt algun, cal crear un res.groups.privilege.
  • grep de groups_id i de users a l'XML: a 19 són group_ids i user_ids.
  • grep de numbercall i doall: fora a 18 i 19, obligatori a -1 a 17.
  • grep de _sql_constraints a 19: es queda en un warning del log i la restricció no es crea. I de models.Constraint a 18 i 17: aquest nom no hi existeix i el mòdul ni s'importa.
  • grep de <tree a 18/19 i de <list a 17, sense oblidar el view_mode de les accions.
  • Per cada <record id="base....">: hi ha migració per a l'upgrade i un test que falli si el grup es queda buit?
  • Instal·lar en base neta. Comptar els tests que s'han executat. Si diu zero, el gate és buit.
  • Instal·lar sobre una base amb la versió anterior. Comptar els tests una altra vegada.
  • Repetir a cada sèrie que vulguis publicar.

Els cinc primers punts són un script estàtic que corre en un segon; el build real triga minuts. Fes primer el barat, però no deixis que substitueixi el car: el grep troba el que ja saps buscar, i el build troba el que no.

Si véns d'una migració, el nivell anterior és als errors comuns en migrar a Odoo 19.

La regla de fons, l'única que cal endur-se: compilar, parsejar i tenir tests verds no és el mateix que instal·lar. Els dos mòduls que més cars ens han sortit aquest any tenien tot verd en local i no s'instal·laven.

Enginyeria per a partners d'OdooProducte propi que s'instal·la a 17, 18 i 19, amb gate real a cada sèrie.Migració a Odoo 19Què revisar, en quin ordre i què encareix una migració.Errors de migració a Odoo 19El nivell anterior: el que es trenca abans d'arribar al build.Què porta Odoo 19Els canvis funcionals, per decidir si val la pena moure's.

Preguntes freqüents

Per què passen els meus tests si el mòdul no s'instal·la?

Perquè una suite de Python pur no obre el registre d'Odoo: valida la teva lògica, no els teus fitxers de dades. I un -i sobre un mòdul ja instal·lat no processa res: el bloc de tests ni s'executa i el build surt verd amb zero tests.

He de crear un res.groups.privilege també a 17 i 18?

No: aquest model no existeix abans de 19. A 17 i 18 la categoria va al propi grup, amb category_id. És una de les raons per les quals l'XML de seguretat no pot ser el mateix fitxer a les tres sèries.

Puc deixar numbercall posat perquè valgui a les tres versions?

No. A 18 i 19 el camp no existeix i la seva presència impedeix instal·lar el mòdul. I treure'l a 17 fa que el cron s'apagui sol després de la primera execució. Cal generar el fitxer per sèrie.

Si instal·lo en una base neta i va bé, no n'hi ha prou?

No, perquè en una instal·lació neta els scripts de migrations/ no arriben a executar-se i els registres marcats noupdate sí que s'apliquen. Justament els dos comportaments que fallen en actualitzar queden fora d'aquesta prova.

El teu mòdul no s'instal·la i no saps per què?

Mirem el teu XML de seguretat, els teus crons, les teves vistes heretades i els teus scripts de migració, i et diem què trenca a cada sèrie abans que ho descobreixi un client. Reserva una trucada a Calendly o truca'ns al +34 616 809 504.

Parlar amb un enginyer