Se rendre au contenu

Pièges Odoo 19 CE — index des erreurs classifiées et leurs solutions

Cette page compile les pièges rencontrés et corrigés sur Odoo 19 Community Edition, chacun validé par un cycle qualité complet (PDCA) sur du code réel testé de bout en bout. Chaque entrée donne le symptôme observable, la cause identifiée et la solution vérifiée.

Les identifiants sont stables : F00x (fix Odoo 19 — changements de version), B00x (bugs rencontrés en développement) et N00x (méthode native Website Builder et multi-website). Trois familles, trois sections ci-dessous.

Index des pièges

Pour aller droit au but : chaque référence renvoie à sa fiche détaillée (symptôme, cause, solution).

RéfDomaineSynthèse
F001Cron ir.cronnumbercall retiré + active via eval="True"
F002Vues search n'accepte aucun attribut en search
F003Séquences ir.sequenceSans company_id lockée sur main_company
B003Controllers @routetype='json' deprecated → type='jsonrpc'
B004ACL ir.model.access.csvgroup_id explicite obligatoire
B005Groupes res.groupscategory_id n'existe plus
B006Utilisateurs res.usersgroups_idgroup_ids ; env.user shell = superuser
B007Table ir_ui_menu_group_relgroup_id renommé gid
B008Assets web après installPurger ir.attachment URL /web/assets/%
B009Menus invisibles après B008Apps → module → « Mettre à jour » (UI)
B010Tests — IntegrityErrorAbsent de odoo.exceptions ; mute_logger + savepoint
B011Tests — --test-enableÉchoue si service actif ; --test-tags=/
B012Contraintes SQL-u ne supprime pas ; DROP CONSTRAINT manuel ; composite multi-société
B016Syntaxe attrs={}Supprimé Odoo 17+ → invisible="condition_python"
B017Chatter
après
B019"false"ParseError ; create/delete="false"
B021Modèle _inheritsCréer le record parent d'abord
B022res.users + with_companyExige base.group_system ; société dans company_ids
B023portal.portal_layoutAncre o_portal_sidebar inexistante → ParseError
B024csrf + HttpCasecsrf=False requis sur POST publics testés
B025HttpCase.authenticate()password obligatoire au create()
B027Borne Datetime<= 'YYYY-MM-DD' exclut le jour → ' 23:59:59'
N001Formulaire — route 404Route = /website/form/, pas /website_form/
N002Formulaire — mauvais modèleAction → mail.mail, pas res.partner
N003Formulaire — email non reçuSMTP + champ email_to
N004Snippet non éditableInsérer via la barre, pas copier-coller
N005data-vcss manquantInsérer depuis l'éditeur (attributs auto)
N006Image hors médiathèqueUploader via Website Builder / Médiathèque
N007Vue d'extensionactive=False, ne jamais vider l'arch
N008Double injection PlausibleDésactiver la vue d'injection manuelle
N009Champ website inexistantTester in env['website']._fields avant write
N010Partage res.companyFooter natif ou société distincte par site
N011Site servi par website_id=1Host $host Nginx + domain sans https://
N012CSS palette par sitewebsite.custom_code_head natif
N013data-website-id sur Écrire body { ... } (custom_code_head scopé)
N014Vue d'extension non appliquéeinherit_id en champ + mode='extension'
N015Markup.replace() échappestr() d'abord
N016Override per-sitet-if website.id dans la vue partagée ; website.rewrite
N017Logo d'en-têtewebsite.logores.company.logo ; contraste fond
N018Racine /doc réservéeModule core api_doc (Odoo 19) — choisir une autre racine de pages
N019Menu parent → URL #Rechercher le menu par nom, pas par URL ; lier l'accueil dans le déroulant

Code Odoo 19 — pièges F et B (socle technique)

Ces pièges touchent le code des modules : cron, vues XML, controllers, sécurité, ORM et tests. Ils proviennent presque tous de changements introduits entre Odoo 16/17 et Odoo 19 — du code valide hier casse aujourd'hui, souvent silencieusement.

F001 — Cron ir.cron : numbercall supprimé, active via eval

Symptôme : erreur d'installation dès qu'un fichier data/*.xml référence le champ numbercall ; ou cron qui reste inactif malgré active renseigné.

Cause : le champ numbercall n'existe pas en Odoo 19. Et la forme texte True n'est plus interprétée.

Solution : ne jamais utiliser numbercall ; renseigner active via eval.

F002 — Vue search : n'accepte aucun attribut

Symptôme : erreur de chargement de vue sur une vue search contenant .

Cause : en Odoo 19, le d'une vue search n'accepte aucun attribut — ni expand, ni string.

Solution : utiliser simple ; le string est porté par chaque . Pour structurer la barre, enchaîner plusieurs consécutifs.


...


    

F003 — Séquence ir.sequence lockée sur la société principale

Symptôme : en multi-société, next_by_code() retourne None pour toute société autre que la principale.

Cause : une ir.sequence créée sans company_id explicite est lockée sur base.main_company.

Solution : déclarer dans le XML. Si la séquence est déjà lockée, un -u ne corrige pas les données noupdate="1" — corriger en SQL :

UPDATE ir_sequence SET company_id = NULL WHERE code = 'mon.module.code';

B003 — Controller @route : type='json' deprecated

Symptôme : route JSON qui ne fonctionne pas correctement en Odoo 19.

Cause : type='json' est deprecated dans le décorateur @http.route.

Solution : utiliser type='jsonrpc' pour toutes les routes JSON-RPC.

# ❌ Deprecated en Odoo 19
@http.route("/my/endpoint", type="json", auth="public", methods=["POST"], csrf=False)
# ✅ Correct
@http.route("/my/endpoint", type="jsonrpc", auth="public", methods=["POST"], csrf=False)

B004 — ACL : group_id explicite obligatoire

Symptôme : warnings bloquants à l'installation sur des lignes de ir.model.access.csv.

Cause : en Odoo 19, toute ligne ACL sans group_id:id explicite est deprecated.

Solution : renseigner un groupe sur chaque ligne. Pour donner accès « à tous », créer plusieurs lignes (une par groupe : base.group_public, base.group_portal, base.group_user…), jamais une ligne à groupe vide.

access_my_model_public,my.model public,model_my_model,base.group_public,1,0,0,0
access_my_model_user,my.model user,model_my_model,base.group_user,1,1,1,0

B005 — res.groups : category_id n'existe plus

Symptôme : ValueError: Invalid field 'category_id' in 'res.groups' à l'installation.

Cause : le champ category_id a été retiré de res.groups en Odoo 19 ; le modèle ir.module.category existe encore mais la liaison est rompue.

Solution : ne plus utiliser category_id ; préfixer le nom du groupe par le nom du module (ex. « Mon Module / Administrateur ») et gérer l'héritage via implied_ids.


    Mon Module / Administrateur
    

B006 — res.users : groups_id renommé group_ids

Symptôme : erreur à la création d'un utilisateur avec groups_id, ou erreur dans un domaine ORM filtrant sur les groupes.

Cause : le champ Many2many des groupes est renommé groups_idgroup_ids en Odoo 19 ; groups_id n'est en outre pas utilisable dans un domaine de recherche.

Solution : utiliser group_ids à la création ; pour filtrer, préférer .filtered(lambda u: u.has_group('base.group_system')).

# ❌ INCORRECT Odoo 19
self.env['res.users'].create({'login': '...', 'groups_id': [(4, group.id)]})
# ✅ CORRECT
self.env['res.users'].create({'login': '...', 'group_ids': [(4, group.id)]})

Note shell CLI : dans un odoo shell, env.user est le superuser interne (__system__), pas l'admin. Pour agir en admin : env.ref('base.user_admin').

B007 — Table ir_ui_menu_group_rel : colonne group_id renommée gid

Symptôme : échec de toute requête SQL ciblant ir_ui_menu_group_rel.group_id.

Cause : dans la table de liaison menus ↔ groupes, la colonne group_id est renommée gid en Odoo 19.

Solution : privilégier l'API ORM (menu.groups_id) plutôt que le SQL direct.

B008 — Assets web obsolètes après installation d'un module

Symptôme : après installation d'un module ajoutant menus ou assets, l'interface ne reflète pas les nouveautés malgré un redémarrage.

Cause : un simple restart ne suffit pas — les anciens bundles d'assets restent en cache dans ir.attachment.

Solution : purger les assets puis redémarrer Odoo ; ils sont recompilés au prochain chargement.

DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';

B009 — Menus toujours invisibles après la purge B008

Symptôme : menus d'un module fraîchement installé toujours absents, même après purge des assets et restart.

Cause : le registry in-worker n'est pas complètement invalidé par le restart ni par le -u en CLI.

Solution : passer par l'interface : Apps → le module → bouton « Mettre à jour » — cette action déclenche l'invalidation complète du registry.

B010 — Tests : IntegrityError absent de odoo.exceptions

Symptôme : impossible d'importer IntegrityError depuis odoo.exceptions dans un test de contrainte.

Cause : l'exception n'y est pas exposée ; de plus, une erreur SQL invalide la transaction tant qu'elle n'est pas rollbackée.

Solution : combiner mute_logger, assertRaises(Exception) et un savepoint().

from odoo.tools import mute_logger
with mute_logger('odoo.sql_db'), self.assertRaises(Exception):
    with self.cr.savepoint():
        self.env['ma_table'].create({...})  # déclenche la contrainte

B011 — --test-enable échoue si le service Odoo est actif

Symptôme : lancement des tests en CLI qui échoue (port occupé) quand le service Odoo tourne.

Cause : le processus de test entre en conflit avec le service actif sur le même port.

Solution : arrêter le service avant les tests, et toujours cibler --test-tags=/ pour un module dépendant de website ou portal — sinon les tests website natifs (échecs préexistants en Odoo 19 CE) polluent le résultat.

sudo systemctl stop odoo
sudo -u odoo /usr/bin/odoo -c /etc/odoo/odoo.conf -i mon_module \
    -d ma_base-dev --test-enable --test-tags=/mon_module --stop-after-init
sudo systemctl start odoo

B012 — Contraintes SQL : un -u ne supprime rien, et _sql_constraints a disparu

Symptôme : une contrainte UNIQUE obsolète continue de bloquer les créations (cas typique multi-société), malgré la mise à jour du module ; ou AttributeError en cascade au chargement du modèle.

Cause : une contrainte SQL déjà en base n'est ni supprimée ni modifiée par un simple -u. Par ailleurs, Odoo 19 n'utilise plus la liste _sql_constraints ; une déclaration models.Constraint placée dans une liste fait échouer la définition de classe — le modèle est absent du pool et tous les Many2one qui le référencent lèvent des AssertionError.

Solution : supprimer manuellement l'ancienne contrainte avant le -u, et déclarer les contraintes en attribut de classe direct.

-- 1. Supprimer manuellement l'ancienne contrainte AVANT le -u
ALTER TABLE ma_table DROP CONSTRAINT IF EXISTS ma_table_reference_uniq;
-- 2. Puis lancer le -u : la nouvelle contrainte composite sera créée
# ✅ Attribut de classe direct — jamais dans une liste [...]
name_unique = models.Constraint('UNIQUE(name)', 'Message')

B016 — Syntaxe attrs={} supprimée (Odoo 17+)

Symptôme : erreur de vue sur toute déclaration attrs="{'invisible': [...]}".

Cause : la syntaxe attrs n'existe plus depuis Odoo 17.

Solution : conditions en expressions Python directes, évaluées contre le record courant (pas des domaines) : invisible, readonly, required sur ,

Développement et déploiement — table de triage rapide

Ces pièges se rencontrent pendant le cycle développement / déploiement sur serveur : symptômes opérationnels (logs vides, 502, modification ignorée…) plutôt qu'erreurs de code. La table ci-dessous permet un diagnostic en quelques secondes ; les entrées liées à un piège classifié renvoient vers sa fiche.

ProblèmeCauseSolution
Mise à jour silencieuse (aucun log)Erreur XML/Python bloquante silencieuseLire le log après -u
Logs vides dans le terminallogfile configuré dans odoo.confLire /var/log/odoo/odoo-server.log
.py modifié non pris en compteCache bytecode .pycfind ... -name "*.pyc" -delete + restart
Menu invisible après -uErreur XML silencieuse / action manquanteVérifier logs + mode debug (cf. B008/B009)
attrs deprecatedOdoo 17+ supprime attrsinvisible="condition" directe (cf. B016)
t-set en back-officeDirective OWL interdite en vue formulaireTemplates QWeb website uniquement
res.partner.mobile → AttributeErrorChamp absent en Odoo 19 CEUtiliser phone et email
502 Bad GatewayOdoo planté au démarragesudo tail -50 /var/log/odoo/odoo-server.log
CSRF manquant dans form POSTToken requis dans tout
Champ inconnu dans domain filterModule dépendant non déclaréAjouter la dépendance dans __manifest__.py
/website/info expose les modulesRoute publique listant les modules — risque sécuSite Web → Pages système → désactiver /website/info
_sql_constraints → upgrade silencieuxOdoo 19 n'utilise plus _sql_constraintsMigrer vers models.Constraint attribut direct (cf. B012)
models.Constraint dans une liste → AttributeErrorSyntaxe liste interditename_unique = models.Constraint('UNIQUE(name)', 'Message')
groups_id dans domain ORM → erreurNon searchable en Odoo 19.filtered(lambda u: u.has_group('base.group_system')) (cf. B006)
StripeObject.get() → AttributeErrorSDK Stripe récent : .get() non supportépi[key] dans un helper _pi_field(pi, key, default)
'cancelled' dans Selection offre → rollback silencieuxValeur inexistante → rollback PG sans logUtiliser 'rejected' pour les offres non retenues
numbercall dans ir.cron → erreur installChamp suppriméNe pas utiliser (cf. F001)
active="True" dans ir.cron → ignoréForme texte non interprétée (cf. F001)
en search → erreur n'accepte aucun attribut simple (cf. F002)
type="json" dans @http.route → KODeprecatedtype="jsonrpc" (cf. B003)
Ligne ACL sans group_id → warning bloquantObligatoire en Odoo 19Groupe explicite (cf. B004)

Website Builder et multi-website — pièges N001 à N019

Ces pièges sont spécifiques à la méthode native de création de site (Website Builder d'Odoo 19 CE) et au multi-website : formulaires, snippets, médiathèque, vues QWeb par site et analytics. Le contexte complet est décrit dans le guide Créer un site web avec le Website Builder.

N001 — Route formulaire /website_form/ → 404

Symptôme : le formulaire de contact renvoie une erreur 404 à la soumission.

Cause : la route Odoo 19 est /website/form/ (avec préfixe website/), pas /website_form/. Le JS construit l'URL à partir de l'action + data-model_name.

Solution : vérifier l'action du /website/form/ uniquement.

N002 — Formulaire envoie vers le mauvais modèle

Symptôme : les soumissions sont créées dans res.partner au lieu de partir par email.

Cause : le modèle cible par défaut du formulaire est res.partner.

Solution : dans le Website Builder, action → « Envoyer un email » (mail.mail). Vérifier data-model_name="mail.mail" sur le .

N003 — Email non reçu après soumission du formulaire

Symptôme : le formulaire semble fonctionner mais aucun email n'arrive.

Cause : (1) SMTP sortant non configuré — les mails sont créés en base mais non livrés ; (2) champ caché email_to absent ou incorrect.

Solution : configurer le SMTP (Paramètres → Email → Serveurs sortants) et vérifier le champ caché email_to.

N004 — Snippet non reconnu après import

Symptôme : un snippet collé dans une page n'est plus éditable comme un bloc du Website Builder.

Cause : snippet collé manuellement sans attributs data-name, ou classe racine non reconnue.

Solution : toujours insérer via la barre de l'éditeur (glisser-déposer), pas par copier-coller.

N005 — data-vcss manquant : styles absents

Symptôme : s_comparisons, s_features_grid, s_quotes_carousel… s'affichent sans mise en forme.

Cause : le CSS de ces snippets est chargé via data-vcss="001" (ou 002) ; si l'attribut est absent, la feuille versionnée n'est pas chargée.

Solution : insérer le snippet depuis la barre de l'éditeur — les attributs sont portés automatiquement.

N006 — Image non reprise par la médiathèque

Symptôme : image en src="data:..." (base64), absente de la médiathèque.

Cause : image insérée en data URI au lieu d'un upload.

Solution : uploader via le Website Builder (« Remplacer » → « Uploader ») ou la Médiathèque ; utiliser l'URL Odoo (/web/image/ir.attachment/ID/raw).

N007 — Désactiver une vue d'extension : active=False, pas vider l'arch

Symptôme : 500 Internal Server Error au prochain chargement après modification d'une vue d'extension.

Cause : une vue d'extension vidée a des vues enfants (primary) dont les XPath (//a[hasclass('oe_unremovable')]) ne trouvent plus leur cible.

Solution : view.write({'active': False}) — Odoo ignore les vues inactives. Ne jamais vider l'arch d'une vue qui peut avoir des enfants.

N008 — Double injection du script Plausible

Symptôme : curl -s https://CLIENT.fr | grep -c "plausible" retourne 2 ou plus.

Cause : une vue ir.ui.view héritant de website.layout (injection manuelle) coexiste avec l'intégration native activée.

Solution : désactiver la vue d'injection manuelle — avec active=False, jamais en vidant l'arch (cf. N007).

env['ir.ui.view'].search([('key', '=', 'website.CLIENT_plausible')]).write({'active': False})

N009 — Champ inexistant dans le modèle website : rollback en cascade

Symptôme : un write() sur un champ inconnu lève une ValueError avant le commit() → rollback de tous les create() précédents de la même transaction.

Cause : champ testé sans vérification d'existence, dans la même transaction que des créations critiques.

Solution : tester l'existence du champ, et ne jamais mettre un write() douteux dans la même transaction que des create() critiques.

if 'MON_CHAMP' in env['website']._fields:
    w.write({'MON_CHAMP': valeur})

N010 — Partage de res.company : coordonnées et footer identiques

Symptôme : en multi-website, les deux sites affichent les mêmes coordonnées dans le pied de page natif.

Cause : les sites partagent la même société ; les snippets lisent env.company.

Solution : désactiver le pied de page natif (website.SITE_hide_footer_copyright) ou créer une société distincte par site.

N011 — Site servi systématiquement par website_id=1

Symptôme : le second domaine affiche le contenu du premier site.

Cause : (1) proxy_set_header Host $host; absent du vhost Nginx → Odoo reçoit Host: 127.0.0.1:8069 et sert le site par défaut ; (2) website.domain contient https://exemple.fr au lieu de exemple.fr ; (3) DNS non propagé (tester curl -H "Host: exemple2.fr" http://IP_SERVEUR).

Solution : vérifier proxy_set_header Host $host; et le champ domain (hostname nu, sans https:// ni www.).

N012 — CSS de palette par site : custom_code_head, pas une vue assets

Symptôme : le CSS de palette d'un site ne s'applique jamais.

Cause : la vue censée étendre assets_frontend n'est pas chargée (cf. N014).

Solution : injecter via le champ natif website.custom_code_head (Réglages → Code personnalisé), déjà scopé au site.

env['website'].browse(WID).write({'custom_code_head': ''})

N013 — data-website-id est sur , pas sur

Symptôme : le sélecteur body[data-website-id="2"] { ... } ne stylise rien.

Cause : Odoo place data-website-id sur (via html_data).

Solution : comme custom_code_head est déjà scopé au site, écrire simplement body { ... } (sinon html[data-website-id="2"] body).

N014 — Vue d'extension jamais appliquée (inherit_id en texte, pas en champ)

Symptôme : une vue d'extension créée par script n'a aucun effet.

Cause : inherit_id mis dans le texte XML du , mais le champ inherit_id (Many2one) resté vide + mode='primary' → Odoo ne chaîne jamais la vue.

Solution : régler le champ inherit_id (id de la vue parente) et mode='extension'. Attention : l'xpath doit cibler un nœud réel de l'arch propre du parent — website.layout n'a pas de (c'est une extension de portal.frontend_layout), donc //head n'y matche pas.

N015 — Markup.replace() échappe les motifs cherchés

Symptôme : un str.replace() sur custom_code_head / arch_db ne remplace rien.

Cause : ces champs renvoient un objet markupsafe.Markup ; .replace('a="2"', ...) échappe les guillemets ("") avant de chercher.

Solution : convertir en chaîne d'abord : css = str(view.custom_code_head).

N016 — Override per-site d'une vue partagée : t-if website.id, pas le COW

Symptôme : un footer ou lien partagé (website_id=False) doit différer par site ; le COW (view.with_context(website_id=N).write(...)) crée une copie mais le rendu sert toujours la vue globale.

Cause : le mécanisme copy-on-write ne produit pas l'effet attendu sur ce cas.

Solution : rendre le contenu conditionnel dans la vue partagée. Pour un simple lien : une redirection website.rewrite (301, url_from/url_to, website_id) scopée au site.

…site N……autres…

Cache à connaître : les changements de vues (ir.ui.view) peuvent tarder (cache de page/QWeb des workers) — patienter ou systemctl restart odoo. custom_code_head, website.logo et website.rewrite se reflètent vite.

N017 — Logo d'en-tête : website.logo ≠ res.company.logo

Symptôme : le logo modifié sur la société n'apparaît pas dans le bandeau du site, ou le logo est illisible sur le fond du header.

Cause : le logo du bandeau est website.logo (par site), distinct du logo société (res.company.logo, utilisé sur factures et devis).

Solution : modifier website.logo pour le site, et adapter la couleur au fond du header : logo clair sur header sombre, logo foncé sur header blanc. Recolorer un SVG (changer les fill) est immédiat ; un SVG rend bien dans un navigateur (contrairement à wkhtmltopdf, qui ne rend pas le SVG).

N018 — Racine /doc réservée par le module core api_doc (Odoo 19)

Symptôme : une page website créée sous /doc/ avec un seul segment (ex. /doc/ma-doc) renvoie un 303 vers la page de connexion pour un visiteur, alors qu'elle est publiée et publique ; connecté, c'est la documentation de l'API qui répond, jamais la page.

Cause : le module core api_doc (documentation de l'API REST, intégrée à Odoo 19) déclare la route ['/doc', '/doc/', '/doc/index.html'] avec auth='user' — et un contrôleur gagne toujours sur la résolution des pages website. Les URLs à deux segments (/doc/a/b) ne matchent pas ce pattern, d'où des sous-pages qui fonctionnent pendant que l'accueil est intercepté.

Solution : ne jamais créer de page website directement sous /doc/ ; choisir une autre racine (ex. /documentation/...). Avant d'adopter une racine, vérifier les collisions de routes : grep -rn "route(" addons/*/controllers/.

N019 — Menu parent transformé en déroulant : URL remplacée par #

Symptôme : un website.menu créé par ORM avec une URL la perd (remplacée par #) dès qu'on lui ajoute des sous-menus ; une recherche d'idempotence par URL ne le retrouve plus et le script en crée un doublon à la ré-exécution — et la page visée par l'ancienne URL n'est plus accessible depuis le menu.

Cause : Website transforme tout menu ayant des enfants en simple déclencheur de déroulant, sans lien propre.

Solution : rechercher le menu parent par (name, parent_id, website_id) et non par URL ; et donner à la page d'accueil de la section sa propre entrée dans le déroulant (ex. « Vue d'ensemble » en première position).

Méthode IA&TIC — © 2026 IA&TIC — Freddy Maillard & Fabrice-Loïc Barbier — CC BY-SA 4.0