Odoo 19 CE — Socle technique validé terrain
Cette page rassemble les faits techniques universels sur Odoo 19 Community Edition, tels qu'ils ont été validés sur du code réel testé de bout en bout. Elle couvre les douze zones où Odoo 19 diverge le plus souvent des habitudes héritées des versions précédentes — ou de la documentation officielle elle-même.
Chaque règle présentée ici a transité par un cycle PDCA complet (hypothèse écrite, application, test avec critères de succès définis à l'avance, inscription). Les identifiants entre parenthèses (F001, B006…) renvoient aux fiches de validation correspondantes — l'index complet figure en bas de page. La méthode qui garantit la fiabilité de ces informations est décrite sur la page PDCA & Ishikawa.
1. Cron et actions planifiées (ir.cron)
Les actions planifiées ont changé de contrat en Odoo 19 : un champ historique a disparu et la façon de renseigner l'activation a évolué. Un fichier de données XML écrit « comme avant » provoque une erreur d'installation.
- Champ
numbercall: n'existe pas en Odoo 19 (F001). Toute référence dans undata/*.xmlprovoque une erreur d'installation. Ne pas l'utiliser. - Champ
active: à renseigner viaeval="True"(oueval="False"), pas via— la forme texte n'est plus interprétée (F001).True - Champ
model_id: obligatoire, sous la formeref="model_(avec souligné, pas point)." - Champ
state:"code"pour exécuter du Python via. interval_number+interval_type: périodicité ;interval_typeaccepteminutes,hours,days,weeks,months.
Déclaration correcte d'un cron en Odoo 19 :
Nom lisible du cron
code
model._my_cron_method()
1
days
2. Vues de recherche (vue search)
Les vues de recherche restent le point d'entrée du filtrage et du regroupement dans
les listes Odoo. Le piège d'Odoo 19 se cache dans l'élément
qui structure les filtres de regroupement.
dans une vue search : n'accepte aucun attribut en Odoo 19 (F002).expand="0"commestring="..."provoquent une erreur de chargement de vue. Utilisersimple, sans attribut.- À l'intérieur du
, placer lesaveccontext="{'group_by': '...'}". - Le libellé (
string) est porté par chaque, pas par leparent. - Pour structurer la barre de recherche, utiliser plusieurs
consécutifs.
3. Controllers HTTP — décorateur @route
Les controllers exposent des routes HTTP côté serveur : API, formulaires, pages publiques. En Odoo 19, le type de route JSON a changé de nom — et l'ancien nom ne fonctionne plus correctement.
type: utilisertype='jsonrpc'pour les routes JSON-RPC (B003).type='json'est deprecated et ne fonctionne pas correctement en Odoo 19.auth:"public"(visiteur non loggué),"user"(connexion requise),"none"(sans authentification, pour les webhooks).methods:["GET"],["POST"]ou["GET", "POST"].csrf:Falsepour API/AJAX,True(défaut) pour les formulaires HTML classiques.website:Truesi la route hérite du contexte website (cookies, langue).- Accès à la session via
request.env,request.session,request.httprequest.
# ❌ 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)
4. Sécurité — ACL et groupes
4.1 ACL (ir.model.access.csv)
Les listes de contrôle d'accès définissent qui peut lire, écrire, créer ou supprimer les enregistrements d'un modèle. Odoo 19 durcit une pratique autrefois tolérée : la ligne d'accès « sans groupe ».
group_id:idest obligatoire en Odoo 19 (B004). Toute ligne sansgroup_idexplicite est deprecated et génère des warnings bloquants.- Groupes natifs courants :
base.group_public(visiteur),base.group_portal(client externe loggué),base.group_user(utilisateur interne),base.group_system(super-admin). - Pour donner accès « à tous » : créer plusieurs lignes (une par groupe), jamais une ligne avec le groupe vide.
# ❌ Deprecated — warning bloquant en Odoo 19
access_my_model_all,my.model all,model_my_model,,1,1,1,1
# ✅ Correct
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
4.2 Groupes (res.groups)
La définition de groupes de sécurité custom perd un champ historique en Odoo 19 : la catégorie de module ne se rattache plus au groupe.
category_idn'existe plus surres.groups(B005). Son usage provoque uneValueError: Invalid field 'category_id' in 'res.groups'.- Le modèle
ir.module.categoryexiste encore, mais la liaison viacategory_idest rompue — ne pas en créer dans les modules custom. - Nommage : préfixer par le nom du module, par exemple
"Mon Module / Administrateur". implied_idsreste la méthode valide pour l'héritage de groupe.
Mon Module / Administrateur
5. Modèles ORM
La zone des modèles ORM (models.Model, AbstractModel,
TransientModel) est en cours d'enrichissement par cycles PDCA : champs,
méthodes ORM, décorateurs (@api.model, @api.depends,
@api.constrains…), champs calculés, champs related et héritage
(_inherit, _inherits) y seront consignés au fur et à mesure
des validations terrain. Les faits déjà validés touchant l'ORM figurent en
sections 8 et 9.
6. Vues XML — form, list, kanban, calendar, pivot, graph
C'est dans les vues métier que les migrations Odoo cassent le plus de code hérité. Quatre ruptures validées concentrent l'essentiel des erreurs rencontrées en Odoo 19.
B016 — attrs={} supprimé depuis Odoo 17
La syntaxe
Le bloc
L'attribut
Dans la table
Les record rules complètent les ACL en filtrant les enregistrements visibles par
groupe (champ
Trois faits validés concernent la manipulation avancée de l'ORM : le renommage d'un
champ clé de
Le champ Many2many reliant un utilisateur à ses groupes change de nom en Odoo 19.
Note shell CLI : dans un
Un modèle avec
Deux pièges validés touchent le cycle de vie des données de module : les séquences
en contexte multi-société, et la persistance des contraintes SQL lors des mises à
jour de module.
Une
Si la séquence est déjà verrouillée (installation précédente), un
Une contrainte SQL déjà en base n'est ni supprimée ni modifiée par un simple
Note Odoo 19 —
Les tests automatisés Odoo (
Pour tester le déclenchement d'une contrainte SQL, combiner
Le lancement des tests en CLI échoue si le service Odoo tourne déjà (port occupé).
Arrêter le service, tester, redémarrer.
Règle
Sur un formulaire public testé en
Si l'utilisateur de test est créé sans
Un
La zone frontend (bundles d'assets, composants OWL, patching JS) s'enrichit au fil
des validations. Un fait bloquant est déjà consigné : la structure du layout portail
a changé en Odoo 19.
Le template
Après l'installation d'un module, deux comportements de cache peuvent laisser
croire à un échec alors que le module est correctement installé : les bundles
d'assets périmés et le registry non invalidé.
Après l'installation d'un module ajoutant menus ou assets, un simple restart ne
suffit pas : les anciens bundles restent en cache dans
Si les menus restent invisibles après la purge des assets, passer par
Apps → le module → bouton « Mettre à jour » depuis l'interface :
cette action déclenche une invalidation complète du registry in-worker que ni le
restart ni le
Tableau récapitulatif de l'ensemble des faits validés présentés sur cette page.
Chaque référence
Pour comprendre comment ces faits sont validés — et pourquoi cette page peut
prétendre à plus de fiabilité qu'une compilation de documentation — lire la page
Méthode : PDCA & Ishikawa.
Méthode IA&TIC — © 2026 IA&TIC — Freddy Maillard & Fabrice-Loïc Barbier — CC BY-SA 4.0attrs="{'invisible': [...], 'readonly': [...], 'required': [...]}"
n'existe plus. Les conditions deviennent des expressions Python
évaluées contre l'enregistrement courant (et non plus des domaines) :
invisible="state not in ('draft', 'done')",
readonly="partner_id != False", required="parent_id == False".
La règle s'applique à , ,
, ,
B017 — Le chatter devient
message_follower_ids, activity_ids, message_ids)
est remplacé par la balise unique , à placer
après , au même niveau dans le
.
B019 —
invalideeditable n'accepte que "top" ou
"bottom" ; la valeur "false" génère une
ParseError bloquante. Pour une liste non éditable, combiner
create="false" et delete="false".
B007 — Table de liaison menu/groupe : colonne renommée
ir_ui_menu_group_rel (liaison menus ↔ groupes), la colonne
group_id est renommée gid en Odoo 19. Tout SQL ciblant
ir_ui_menu_group_rel.group_id échoue. Privilégier l'API ORM
(menu.groups_id) plutôt que le SQL direct.
7. Sécurité — record rules (
ir.rule)domain_force). Cette zone est en cours d'enrichissement
par cycles PDCA — définition XML, application par groupes et distinction ACL
(ir.model.access) vs record rules (ir.rule) y seront
consignées après validation terrain.
8. API ORM avancée — environnement, recordsets, contexte
res.users, le comportement des modèles en héritage par
délégation, et les droits requis pour créer des utilisateurs.
B006 —
groups_id renommé group_ids sur res.users# ❌ 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)]})odoo shell,
env.user est le superuser interne (__system__), pas
l'admin. Pour agir en admin : env.ref('base.user_admin').
B021 — Modèle en
_inherits : créer le record parent d'abord_inherits = {'res.partner': 'partner_id'} n'a pas de
champs d'identité propres — name, phone, email
sont portés par le parent (res.partner).
# ❌ INCORRECT — champs inexistants sur le modèle délégant
self.env['mon_module.ma_table'].create({'name': 'Dupont', 'phone': '0102030405'})
# ✅ CORRECT — créer le parent d'abord, puis le délégant
partner = self.env['res.partner'].create({'name': 'Jean Dupont', 'phone': '0102030405'})
self.env['mon_module.ma_table'].create({'partner_id': partner.id})B022 — Création de
res.users et contrainte with_company
res.users exige le droit « Access Rights », c'est-à-dire
l'appartenance à base.group_system. Un groupe applicatif custom ne
suffit pas (AccessError). Dans les tests, créer via
base.user_admin.with_company(company) exige que la société figure dans
user.company_ids, sinon AccessError ou recordset vide
silencieux.# ✅ CORRECT dans les tests
self.env['res.users'].with_user(self.env.ref('base.user_admin')).create({
'name': 'Utilisateur Test',
'login': 'user_test@example.com',
'password': 'user_test@example.com',
'company_id': self.company.id,
'company_ids': [(4, self.company.id)],
})9. Modules, manifestes et contraintes SQL
F003 — Séquences
ir.sequence et multi-société : company_id=Falseir.sequence créée sans company_id explicite est
verrouillée sur base.main_company : next_by_code() retourne
None pour toute autre société.
-u
ne corrige pas les données noupdate="1" — correction SQL directe :
UPDATE ir_sequence SET company_id = NULL WHERE code = 'mon.module.code';B012 — Un
-u ne supprime pas une contrainte SQL existante-u. Cas typique en multi-société : une UNIQUE(reference)
globale bloque deux sociétés — il faut une contrainte composite
UNIQUE(reference, company_id).
-- 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_sql_constraints : Odoo 19 n'utilise
plus la liste _sql_constraints. Migrer vers models.Constraint
en attribut de classe direct —
name_unique = models.Constraint('UNIQUE(name)', 'Message') — jamais dans
une liste [...]. Sinon la définition de classe échoue, le modèle est
absent du pool, et une AssertionError se propage en cascade sur tous les
Many2one qui le référencent.
10. Tests (
odoo.tests)TransactionCase, HttpCase)
réservent plusieurs surprises en Odoo 19 : exceptions introuvables, conflits de
port, CSRF et authentification HTTP. Cinq faits validés.
B010 —
IntegrityError absent de odoo.exceptionsmute_logger et un savepoint — le savepoint() est
nécessaire car une erreur SQL invalide la transaction tant qu'elle n'est pas
rollbackée.
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 contrainteB011 —
--test-enable avec service Odoo actif : port occupé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--test-tags : toujours utiliser
--test-tags=/ pour un module dépendant de
website ou portal — sinon les tests website
natifs (qui contiennent des échecs préexistants en Odoo 19 CE) polluent le résultat.
B024 —
csrf=True bloque les POST en HttpCaseHttpCase, passer
csrf=False (avec validation côté backend). Sur les formulaires
authentifiés (auth='user'), le token CSRF est géré automatiquement et
csrf=True peut rester.
# ✅ CORRECT — csrf=False sur formulaires publics si validation backend
@http.route('/mon_module/endpoint', auth='public', website=True,
methods=['GET', 'POST'], csrf=False)B025 —
HttpCase.authenticate() exige un mot de passe définipassword, l'authentification
HTTP échoue silencieusement : les routes auth='user' renvoient une
redirection 303 vers la page de login.
# ✅ CORRECT — password explicite (convention : login = password dans les tests)
user = self.env['res.users'].sudo().create({
'name': 'Utilisateur Test',
'login': 'user_test@example.com',
'password': 'user_test@example.com',
})
self.authenticate('user_test@example.com', 'user_test@example.com')B027 — Borne
'YYYY-MM-DD' sur champ Datetime : le jour est exclufields.Datetime stocke YYYY-MM-DD HH:MM:SS : le domaine
('date_champ', '<=', '2026-05-20') est interprété
<= '2026-05-20 00:00:00' — la journée entière est exclue. La règle ne
s'applique pas aux champs fields.Date (sans composante horaire).
# ❌ INCORRECT
domain = [('date_champ', '<=', fin.strftime('%Y-%m-%d'))]
# ✅ option 1 : suffixer ' 23:59:59'
domain = [('date_champ', '<=', fin.strftime('%Y-%m-%d') + ' 23:59:59')]
# ✅ option 2 : borne stricte sur le lendemain
domain = [('date_champ', '<', (fin + timedelta(days=1)).strftime('%Y-%m-%d'))]11. Assets frontend et templates QWeb
B023 — Structure de
portal.portal_layout modifiéeportal.portal_layout n'expose plus l'ancre
//div[hasclass('o_portal_sidebar')]. Tout héritage QWeb ciblant ce div
lève une ParseError qui bloque l'installation du module.
12. Déploiement et administration
B008 — Purge des assets après installation d'un module
ir.attachment.
Les purger, puis redémarrer Odoo — les assets sont recompilés au prochain chargement.
DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';B009 — Menus toujours invisibles malgré B008
-u en CLI ne produisent toujours.
Index des faits validés (PDCA universels)
F00X (Fix Odoo 19) ou B00X (Bug
rencontré) correspond à un cycle Plan-Do-Check-Act complet, dont le détail (dates,
fichiers, tests) est consigné dans le journal de bugs du projet.
Réf Zone Section Synthèse F001 Cron ir.cron1 numbercall retiré + active via eval="True"F002 Vues search2 n'accepte aucun attribut en searchB003 Controllers @route3 type='json' deprecated → type='jsonrpc'B004 ACL ir.model.access.csv4 group_id explicite obligatoireB005 Groupes res.groups4 category_id n'existe plusB006 Utilisateurs res.users8 groups_id → group_ids ; env.user shell = superuserB007 Table ir_ui_menu_group_rel6 group_id renommé gidB008 Assets web après install 12 Purger ir.attachment URL /web/assets/%B009 Menus invisibles après B008 12 Apps → module → « Mettre à jour » (UI) B010 Tests — IntegrityError10 Absent de odoo.exceptions ; mute_logger + savepointB011 Tests — --test-enable10 Échoue si service actif ; --test-tags=/B012 Contraintes SQL 9 -u ne supprime pas ; DROP CONSTRAINT manuel ; composite multi-sociétéB016 Syntaxe attrs={}6 Supprimé Odoo 17+ → invisible="condition_python"B017 Chatter 6 après B019 6 "false" → ParseError ; create/delete="false"B021 Modèle _inherits8 Créer le record parent d'abord B022 res.users + with_company8 Exige base.group_system ; société dans company_idsB023 portal.portal_layout11 Ancre o_portal_sidebar inexistante → ParseErrorB024 csrf + HttpCase10 csrf=False requis sur POST publics testésB025 HttpCase.authenticate()10 password obligatoire au create()B027 Borne Datetime10 <= 'YYYY-MM-DD' exclut le jour → ' 23:59:59'F003 Séquences ir.sequence9 Sans company_id lockée sur main_company