Se rendre au contenu

Odoo 20 CE sur Ubuntu 26.04 — ce qui change depuis Odoo 19

Cette page recense les écarts constatés entre le runbook Odoo 19 / Ubuntu 24.04 et une installation neuve d'Odoo 20 CE sur Ubuntu 26.04 LTS « Resolute Raccoon ». Tout ce qui suit provient d'une installation menée à la main, chaque écart étant vérifié sur la machine et non déduit des notes de version.

Elle existe séparément parce que les deux socles cohabitent : le parc tourne encore en Odoo 19, et la chaîne de rendu PDF change entièrement d'une version à l'autre. Mélanger les deux runbooks est le meilleur moyen de perdre une après-midi.

Le réflexe qui ne marche plus. http://IP:8069 ne répond plus sur une installation Odoo 20 neuve. Ce n'est pas une panne : voir écart 10.

Index des écarts

RéfDomaineSynthèse
1Rendu PDFwkhtmltopdf et libssl1.1 disparaissent — moteur maison Paper Muncher
2Rendu PDFPaper Muncher est livré mais dormant — le défaut reste wkhtmltopdf
3Websocketworkers ≥ 1 ne suffit plus : gevent_workers est une option distincte
4Ligne de commandeToute commande odoo tente de lier 8069 si workers ≥ 1
5ORMir.config_parameter : get_param/set_param → get_str/set_str
6odoo.confaddons_path et admin_passwd ne sont plus commentés : ils sont absents
7odoo.confLes db_host = False hérités déclenchent un WARNING
8JournalisationLa directive logfile est ignorée — systemd impose --logfile
9RoutageRacine backend : / → /odoo → /web/login
10Réseauhttp_interface passe à 127.0.0.1 — IP:8069 ne répond plus
11Modulesir.model.access et ir.rule fusionnent dans ir.access
12Modulesupgrade_code convertit le code, mais exige un arbre de sources maîtrisé
13Tests--no-http est ignoré dès que --test-enable est posé
14ModulesCe qui ne change pas : ORM, OWL, vues, cycle de vie
15InterfaceOdoo 20 embarque OWL 3 : les static props ne sont plus lues
16InterfaceFont Awesome n'est plus livré — fonte maison oi par ligature

Les écarts 1 à 10 touchent l'exploitation ; les écarts 11 à 16 touchent le code des modules — voir Porter un module de 19 vers 20.

Une version jeune se vit aussi de l'autre côté : défauts à signaler, traductions à compléter. Les canaux ne sont pas ceux qu'on croit — voir Contribuer à Odoo en fin de page. Et le passage en HTTPS réserve un piège qui ne se voit pas en IPv4.

Le socle Ubuntu 26.04

Versions constatées sur un VPS OVH vierge, et ce qu'Odoo 20 en exige :

ComposantFourni par 26.04Exigence Odoo 20
Python3.14.3MIN_PY_VERSION (3,12) · MAX_PY_VERSION (3,14)
PostgreSQL18.6MIN_PG_VERSION 16
nginx1.28—
certbot4.0—
Python 3.14 est pile sur le plafond. Odoo 20 déclare MAX_PY_VERSION = (3, 14) et Ubuntu 26.04 livre 3.14.3. Il n'y a aucune marge : une distribution plus récente livrant Python 3.15 sortira de la fenêtre supportée. C'est un point à surveiller avant toute montée de version du système d'exploitation.

Les quelque quarante dépendances python3-* du paquet sont toutes présentes dans les dépôts. Une fausse alerte mérite d'être signalée : python3-renderpm n'existe plus en paquet propre, mais python3-reportlab le déclare en Provides: — la dépendance se résout normalement.

Le swap n'est pas optionnel. OVH livre ses VPS sans swap. Sur une machine de 4 Go, l'installation des modules peut déclencher un arrêt pour mémoire insuffisante. Deux gigaoctets suffisent à l'éviter.

Le rendu PDF — le changement structurant

1 — wkhtmltopdf et libssl1.1 disparaissent du runbook

Ce qui change : le maillon faible historique de l'installation Odoo n'existe plus. Ubuntu 26.04 ne package plus du tout wkhtmltopdf, et libssl1.1 a disparu depuis longtemps — le système est en OpenSSL 3.5. La recette du runbook Odoo 19 (paquet bionic 0.12.6-1 plus libssl1.1 récupéré à la main, avec son numéro de révision qui change à chaque correctif de sécurité) n'a plus d'objet.

Ce qui le remplace : Odoo 20 rend ses PDF avec Paper Muncher, son moteur maison. Le module base_report_paper_muncher est livré avec le paquet, mais il exige un binaire externe en version 0.6 ou supérieure, que le .deb d'Odoo ne contient pas.

Le binaire se récupère dans les releases du projet, qui propose un paquet construit pour Ubuntu 26.04 :

curl -fsSL -o pm.deb https://github.com/odoo/paper-muncher/releases/download/v0.8.0/paper-muncher_v0.8.0_resolute_amd64.deb
sudo apt-get install -y ./pm.deb
/opt/paper-muncher/bin/paper-muncher --version

Le paquet s'installe dans /opt/paper-muncher/bin/paper-muncher, qui est exactement le chemin de repli codé en dur dans Odoo (FALLBACK_BIN_PATH). Il n'y a donc rien à ajouter au PATH : le paquet et le module sont faits pour s'emboîter. Un command -v paper-muncher qui ne renvoie rien n'est pas un échec d'installation.

Paper Muncher n'est pas dans les dépôts Ubuntu. Le .deb vient des releases GitHub d'Odoo et n'est suivi par aucun dépôt apt — apt-cache madison paper-muncher ne renvoie rien, seul /var/lib/dpkg/status le connaît. Il ne recevra aucune mise à jour de sécurité automatique. La montée de version est un geste manuel à inscrire au suivi d'exploitation. C'est le prix du moteur maison — cela reste nettement préférable à wkhtmltopdf, dont le projet amont est arrêté depuis 2023, mais apt ne s'en occupera pas à votre place.

2 — Paper Muncher est livré, mais dormant

Symptôme : installer Odoo 20 puis demander un PDF, et voir l'instance réclamer wkhtmltopdf alors qu'on la croyait passée au nouveau moteur.

Cause : deux modules de rendu cohabitent. base_report_wkhtmltox est déclaré auto_install: True et son initialisation pose report.pdf_engine_default = 'wkhtmltopdf'. base_report_paper_muncher, lui, est bien présent dans l'arborescence mais n'est ni installé ni sélectionné.

Solution : installer le module et basculer le paramètre. Les deux gestes sont nécessaires.

sudo -u odoo odoo --config /etc/odoo/odoo.conf -d LA_BASE \
     -i base_report_paper_muncher --stop-after-init --workers=0 --no-http

# puis, dans un shell Odoo :
env['ir.config_parameter'].sudo().set_str('report.pdf_engine_default', 'paper-muncher')
env.cr.commit()

Une fois la bascule faite, wkhtmltopdf peut être retiré de la machine : le rendu continue de fonctionner sans lui.

Configuration et exploitation

3 — workers ≥ 1 ne suffit plus à ouvrir le port 8072

Symptôme : workers = 2 est bien posé dans odoo.conf, les processus de travail démarrent, mais le port 8072 n'écoute pas — et /websocket n'a donc aucune cible. La dégradation est silencieuse : seuls le chat et les notifications en temps réel cessent de fonctionner.

Cause : Odoo 20 introduit une option distincte, gevent_workers, qui pilote le nombre de processus evented. En Odoo 19, workers suffisait à faire naître le service websocket.

Solution : poser les deux options.

workers = 2
gevent_workers = 1

Contrôle : le journal doit afficher Evented/WebSocket service running on 127.0.0.1:8072, et ss -tln montrer le port à l'écoute.

4 — Toute commande odoo tente de lier le port 8069

Symptôme : OSError: [Errno 98] Address already in use (while attempting to bind on address ('127.0.0.1', 8069)) au lancement d'une commande en ligne, alors que le service tourne normalement.

Cause : la commande lit odoo.conf, y trouve workers ≥ 1 et démarre le serveur en mode pré-fork avant d'exécuter ce qu'on lui demande. Ce n'est pas propre à odoo shell : un -i module, un -u, un --load-language sont touchés de la même façon.

Solution : arrêter le service, ou — plus simple et sans interruption — neutraliser le serveur pour la durée de la commande.

sudo -u odoo odoo shell --config /etc/odoo/odoo.conf -d LA_BASE \
     --workers=0 --no-http --logfile=/dev/null

5 — ir.config_parameter : accesseurs typés

Symptôme : AttributeError sur un get_param ou set_param qui fonctionnait en Odoo 19.

Cause : le modèle n'expose plus que des accesseurs typés.

Solution : utiliser get_str, set_str, set_int, set_bool ou set_float selon la nature de la valeur.

6 — Le piège du sed qui décommente n'a plus lieu d'être

Ce qui change : le runbook Odoo 19 insistait sur un point — le odoo.conf livré par le paquet nightly contenait addons_path et admin_passwd commentés, et tout script de configuration devait les décommenter sous peine de laisser le gestionnaire de bases ouvert.

En Odoo 20, ces lignes ne sont plus commentées : elles sont purement absentes. Le fichier livré ne contient que les paramètres db_* et default_productivity_apps. On écrit donc le fichier au lieu de le modifier par substitution.

Le contrôle d'idempotence reste pertinent, lui : grep '^admin_passwd' /etc/odoo/odoo.conf doit renvoyer une ligne active. Sans mot de passe maître, le gestionnaire de bases est accessible.

7 — Les db_host = False hérités déclenchent un avertissement

Symptôme : au démarrage, trois lignes WARNING ... option db_host reads 'False' in the config file ... but isn't a boolean option, skip.

Cause : le paquet livre db_host = False, db_port = False et db_password = False, qu'Odoo 20 ne considère plus comme des booléens valides.

Solution : retirer ces trois lignes. La connexion se fait par socket Unix local sous l'utilisateur système odoo, aucune n'est nécessaire.

8 — La directive logfile d'odoo.conf est ignorée

Symptôme : logfile = /var/log/odoo/odoo.log est posé dans la configuration, et ce fichier n'existe jamais.

Cause : l'unité systemd livrée par le paquet passe --logfile /var/log/odoo/odoo-server.log en ligne de commande. L'argument de ligne de commande l'emporte sur le fichier de configuration.

Solution : ne pas mettre de directive logfile dans odoo.conf — elle ne ferait qu'induire en erreur — et lire /var/log/odoo/odoo-server.log. Pour changer de destination, c'est l'unité systemd qu'il faut modifier.

9 — La racine du backend est /odoo

L'enchaînement des redirections sur une instance neuve : / renvoie en 303 vers /odoo, qui renvoie lui-même vers /web/login avec le paramètre redirect. Sans surprise à l'usage, mais utile à connaître pour écrire des contrôles automatisés : tester / ne dit rien de la santé réelle de l'application.

10 — http_interface passe à 127.0.0.1

Symptôme : http://IP:8069 ne répond pas sur une installation neuve. C'est le réflexe de test le plus répandu, et il échoue.

Cause : le défaut de http_interface passe de 0.0.0.0 (Odoo 19) à 127.0.0.1. Odoo n'écoute plus que sur la boucle locale ; l'accès passe obligatoirement par le reverse proxy. C'est un durcissement bienvenu — une instance fraîche n'est plus exposée par accident sur son port applicatif — mais il déroute.

Conséquence combinée avec dbfilter : avec la convention dbfilter = ^%h$, l'en-tête Host doit en plus porter le nom de la base. Tant que le DNS n'est pas propagé, deux façons de tester :

# depuis le poste client : forcer la résolution
echo "IP_DU_VPS mondomaine.fr" | sudo tee -a /etc/hosts

# ou, en ligne de commande, forcer l'en-tête
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: mondomaine.fr" http://IP_DU_VPS/web/login

Installer l'instance directement en français

Odoo est servi en anglais par défaut. Il n'est pas nécessaire d'activer la langue après coup : un seul drapeau à la création de la base suffit.

sudo -u odoo odoo --config /etc/odoo/odoo.conf -d LA_BASE \
     -i base --load-language=fr_FR \
     --stop-after-init --workers=0 --no-http

L'option charge les fichiers de traduction de chaque module installé, active la langue, et place les comptes dessus. L'instance est en français dès la première connexion. Les modules installés par la suite sont traduits automatiquement tant que la langue reste active.

La différence avec une activation après coup n'est pas anodine : le drapeau posé à l'initialisation laisse fr_FR comme seule langue active, là où une activation par l'interface sur une base créée en anglais laisse en_US et fr_FR.

Sécurité — trois points à ne pas remettre à demain

Ces trois constats proviennent d'une installation réelle, et aucun n'est spécifique à Odoo 20 — mais tous trois se présentent au moment de l'installation.

Le compte admin est en admin / admin

Une base créée par odoo -d LA_BASE -i base reçoit un compte admin dont le mot de passe est admin, sans que rien ne le signale. Sur une machine jointe depuis Internet, c'est une porte ouverte. Le changer est le premier geste après l'installation — et renommer le login au passage supprime aussi le nom de compte par défaut.

L'exposition ne commence pas avec le DNS

Tant que le domaine ne résout pas, on croit volontiers l'instance encore privée. C'est faux : il suffit d'envoyer l'en-tête Host attendu sur l'adresse IP pour atteindre la page de connexion. Le dbfilter protège le choix de la base, pas le fait d'être joignable.

Fermer le gestionnaire de bases et l'accès par IP

Deux gestes complémentaires. Dans odoo.conf, list_db = False masque la liste des bases — la page répond toujours, mais avec un message de désactivation et sans rien divulguer. Côté nginx, un vhost par défaut coupe toute requête dont l'en-tête Host ne correspond à aucun domaine servi :

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

Et la règle qui ne change pas d'une version à l'autre : bloquer /website/info dans chaque vhost, cette page publiant à tout visiteur la liste des modules installés.

location = /website/info { deny all; return 404; }

HTTPS — et le piège qui ne se voit pas en IPv4

Le passage en TLS se fait par certbot --nginx, qui réécrit le vhost pour y insérer le bloc 443 et un bloc 80 de redirection. Rien de particulier à Odoo 20 dans la commande elle-même :

sudo certbot --nginx -d mondomaine.fr --redirect

Vérifier ce que certbot a réécrit, pas son message de succès

certbot modifie le fichier de vhost. Les règles ajoutées à la main peuvent en principe disparaître — il faut donc contrôler après coup plutôt que se fier au « Congratulations » :

grep -c "location = /website/info" /etc/nginx/sites-available/mondomaine.fr.cfg   # N022
grep -c "location /websocket"     /etc/nginx/sites-available/mondomaine.fr.cfg

Constaté le 05/10/2026 : les deux survivent. certbot n'ajoute que ses propres blocs et ne touche pas aux location existants. Le contrôle reste utile — c'est une vérification, pas une méfiance.

Le piège qui compte : l'enregistrement AAAA du modèle de zone.

Chez OVH, la zone DNS créée automatiquement avec un domaine fraîchement enregistré contient un enregistrement AAAA pointant vers le cluster d'hébergement mutualisé (2001:41d0:301::29), pas vers le VPS. L'enregistrement A, lui, est correct.

Conséquence : le site fonctionne parfaitement en IPv4 et sert la page de parking OVH en IPv6. Les navigateurs modernes préférant l'IPv6, une bonne partie des visiteurs ne voit jamais l'application — alors que tous les contrôles en ligne de commande, qui sortent souvent en IPv4, affichent du vert.

Et le certificat finira par ne plus se renouveler : Let's Encrypt privilégie l'IPv6 quand un AAAA existe. La première émission peut passer en IPv4, puis le renouvellement échouer silencieusement deux mois plus tard, en tapant sur le parking. La panne arrive alors longtemps après la cause.

Le diagnostic tient en deux commandes — c'est leur différence qui révèle le problème :

echo | openssl s_client -4 -connect mondomaine.fr:443 -servername mondomaine.fr 2>/dev/null | openssl x509 -noout -subject
echo | openssl s_client -6 -connect mondomaine.fr:443 -servername mondomaine.fr 2>/dev/null | openssl x509 -noout -subject

Si la première renvoie CN=mondomaine.fr et la seconde CN=cluster###.hosting.ovh.net, le AAAA est en cause. Correctif : dans la zone DNS, supprimer l'enregistrement AAAA, ou le pointer vers l'IPv6 réelle du VPS (ip -6 addr show scope global). Vérifier au passage que nginx écoute bien en IPv6 — listen [::]:443 — avant de choisir la seconde option.

Contexte sécurisé : ce que HTTPS débloque dans Odoo

Plusieurs API navigateur n'existent que dans un contexte sécurisé (HTTPS ou localhost). En HTTP simple, Odoo les appelle sans garde et l'interface lève une erreur cliente. La plus visible :

TypeError: can't access property "writeText", browser.navigator.clipboard is undefined
    onClickClipboard@…/web.assets_web.min.js

Ce n'est pas un défaut d'Odoo 20 à corriger côté serveur : c'est le navigateur qui refuse l'API. Le passage en TLS la fait disparaître. Contrôle, depuis la console du navigateur :

window.isSecureContext        // true attendu
typeof navigator.clipboard    // "object" attendu
Un piège de méthode, à propos des contrôles après rechargement. Après un systemctl reload nginx, l'ancien processus de travail continue de servir les connexions en cours avec l'ancienne configuration — un contrôle lancé dans la seconde renvoie donc un résultat faux. Mais attendre leur disparition sans borne est pire : un worker portant une connexion websocket Odoo peut y rester indéfiniment, aucun worker_shutdown_timeout n'étant défini par défaut. Une boucle d'attente non bornée se bloque alors à coup sûr. Borner l'attente à une quinzaine de secondes, puis contrôler.

Porter un module de 19 vers 20

Les écarts qui précèdent concernent l'exploitation d'une instance. Ceux-ci touchent le code des modules, et le premier est structurant : il change la façon d'écrire la sécurité. Tous ont été vérifiés en portant un module réel — un coffre-fort de secrets, dont toute la raison d'être est le contrôle d'accès — dont les dix-sept tests passent sur Odoo 20 sans que la logique métier ait bougé. L'écart 15, lui, n'est apparu qu'à l'usage : il n'est visible ni des tests ni de l'installation.

11 — ir.model.access et ir.rule fusionnent dans ir.access

Ce qui change : Odoo 20 supprime les deux modèles de sécurité historiques et les remplace par un seul, ir.access. Le fichier security/ir.model.access.csv et les enregistrements n'ont plus de destinataire : un module qui les conserve ne s'installe pas.

Le nouveau format : un seul fichier, security/ir.access.csv, déclaré comme tel dans le manifeste.

id,name,model_id,group_id/id,operation,domain
acces_utilisateur,Ce que je possède ou qu'on me partage,mon.modele,mon_module.group_user,cru,"[('owner_id', '=', user.id)]"
acces_gestionnaire,Gestionnaire,mon.modele,mon_module.group_manager,crud,

Trois différences de forme avec l'ancien CSV : model_id prend le nom technique du modèle (mon.modele) et non son identifiant externe ; les quatre colonnes perm_* deviennent une chaîne operation, sous-ensemble de crud (r, cru, crud…) ; une colonne domain porte ce qui était la règle d'enregistrement.

La sémantique est le vrai sujet. Une ligne avec groupe est une permission ; une ligne sans groupe est une restriction, qui s'applique à tout le monde. Les permissions d'un même utilisateur s'additionnent : il atteint un enregistrement dès qu'une ligne de l'un de ses groupes l'autorise. C'est le comportement qu'avaient les règles d'enregistrement non globales. Une conversion fidèle consiste donc à écrire une ligne par couple groupe et modèle, en y fusionnant l'ACL et la règle qui le concernaient.

12 — upgrade_code, l'outil officiel, et ce qu'il exige

Odoo 20 livre un convertisseur de code source, odoo upgrade_code, avec un script par écart — dont 19.4-00-ir-access.py, qui fait la conversion précédente.

odoo upgrade_code --script 19.4-00-ir-access --addons-path=/chemin/de/mes/modules

Trois choses à savoir avant de compter dessus, constatées sur une installation par paquet :

  • Le motif --glob est relatif à la racine des modules, jamais au chemin absolu. Un motif qui reprend le chemin du dossier ne correspond à rien, et l'échec est trompeur : AttributeError: 'NoneType' object has no attribute 'encode', parce que le script relit un fichier dont le contenu n'a jamais été chargé.
  • La commande recharge l'arborescence complète des modules installés, et pas seulement celle qu'on lui désigne. Sur une instance installée par paquet, un seul module tiers dont le manifeste cite un fichier absent fait tomber toute la conversion. L'outil est pensé pour un arbre de sources maîtrisé, pas pour une machine de production.
  • Le script de sécurité traite comme fichier de sécurité tout fichier du manifeste dont le nom contient « security » ou « access ». Une vue nommée views/access_log_views.xml part ainsi au même endroit que les droits d'accès.

Pour un module de taille modeste, écrire le ir.access.csv à la main et le faire valider par la suite de tests revient moins cher que de plier l'outil. Pour un gros module, l'inverse est vrai — à condition de disposer de l'arbre de sources.

13 — --no-http est ignoré quand les tests sont activés

Symptôme : une installation avec --test-enable affiche CRITICAL … Thread odoo.service.httpd … OSError: [Errno 98] Address already in use alors que --no-http est bien passé. Les tests se déroulent malgré tout.

Cause : les tests HTTP ont besoin d'un serveur ; --test-enable le démarre, et --no-http ne s'y oppose pas. Il tente alors de lier le port de la configuration, déjà pris par le service.

Solution : donner un port libre à la commande de test, --http-port=8169 --gevent-port=8172, plutôt que d'arrêter le service. Complète l'écart 4.

Corollaire de l'écart 8 : --logfile=/dev/null, commode pour garder un odoo shell lisible, fait aussi disparaître le compte rendu des tests. Pour une exécution dont on veut lire le résultat, ne pas poser l'option et rediriger la sortie.

14 — Ce qui ne change pas

Un portage se mesure aussi à ce qu'il n'y a pas à toucher. Vérifié dans la source d'Odoo 20, puis à l'exécution :

  • Modèles et champs : models.Constraint, l'attribut exportable, les champs calculés avec inverse, _read_group et ses agrégats, check_access et has_access, activity_schedule (un paramètre nommé s'ajoute, sans casser les appels existants).
  • Groupes : res.groups.privilege et le champ privilege_id — le sélecteur de droits de la fiche utilisateur s'écrit comme en 19, son libellé passant seulement de « Privilege » à « Scope ». res.users.all_group_ids reste disponible dans les domaines.
  • Vues : , , le gabarit kanban , l'attribut groups=.
  • Points d'ancrage de l'interface : standardFieldProps, useInputField, registry.category("fields"), Record.load() et Record.isDirty() existent toujours aux mêmes chemins — mais le corps d'un composant, lui, change : voir l'écart 15.
  • Cycle de vie : post_init_hook(env), les migrations migrations//post-migrate.py, les ir.cron livrés en données.

Sur le module ayant servi de banc d'essai, le portage complet tient en sept fichiers touchés : le CSV de sécurité, le XML qui le précédait, deux manifestes, un appel à ir.config_parameter et le journal des versions. Le reste du module est identique d'une série à l'autre, ce qui rend tenable d'entretenir les deux en parallèle sur deux branches.

15 — Odoo 20 embarque OWL 3 : les props se déclarent autrement

Symptôme : le module s'installe, les tests Python passent, et c'est à l'ouverture d'une fiche que le client web s'arrête net :

Component "MonChamp" defines a static "props" or "defaultProps",
which Owl 3 ignores. Declare the props schema through "useProps" instead.

Cause : Odoo 20 passe à OWL 3. Les deux formes historiques de déclaration — static props et static defaultProps — ne sont plus lues, et le framework le signale par une erreur fatale plutôt que par un oubli silencieux.

Solution : suivre la forme des champs natifs (web/static/src/views/fields/char/char_field.js est la référence la plus lisible). Quatre gestes :

  • le schéma des props devient une constante typée, hors de la classe, et se branche par une propriété d'instance : props = useProps(monSchema). Les types viennent du constructeur t : t.string().optional(), t.boolean().optional(false) — la valeur passée à optional remplace l'ancien defaultProps ;
  • useState n'existe plus : l'état réactif se crée avec proxy ;
  • useRef n'existe plus : une référence se tient par signal.ref(), déclarée elle aussi en propriété d'instance, et useInputField prend désormais ref au lieu de refName ;
  • dans le gabarit, le contexte est l'instance du composant : tout s'écrit avec le préfixe this. — this.props.multiline, this.state.revealed, this.monAccesseur, t-on-click="this.onClick", t-ref="this.input" — et t-esc cède la place à t-out.

Ce dernier point se manifeste séparément, et après coup : une fois les props corrigées, le composant se construit et c'est le rendu qui échoue, sur can't access property "…", ctx.props is undefined. Deux erreurs successives pour un seul portage, parce que l'on corrige la ligne que le message désigne au lieu de relire en entier le composant natif équivalent. char_field.js et char_field.xml contiennent toutes les réponses, y compris celles qu'on ne cherche pas encore.

Pour ce qui n'a pas d'équivalent immédiat, Odoo livre une couche de compatibilité : @web/owl2/utils réexporte notamment useExternalListener, useEnv et useSubEnv. Le script owl3-migration.py de upgrade_code automatise une partie de ces changements d'import — mais pas la déclaration des props, qui reste à faire à la main.

16 — Font Awesome n'est plus livré

Symptôme : aucune erreur, aucun message — simplement des icônes qui ne s'affichent plus : boutons statistiques, cartes kanban, pictogrammes des vues personnalisées.

Cause : Odoo 20 ne distribue plus Font Awesome. Les classes fa fa-quelque-chose n'ont plus de fonte derrière elles. Le remplacement est la fonte maison oi (odoo_ui_icons), qui ne fonctionne pas par classe mais par ligature, nommée dans un attribut.

            
            

   
 

Quelques correspondances relevées en portant un module :

Odoo 19Odoo 20Usage
fa-eye / fa-eye-slashvisibility / visibility_offrévéler, masquer
fa-clipboardcontent_copycopier
fa-magicwand_starsgénérer, compléter
fa-lock, fa-user-secretlockélément protégé
fa-keykeyidentifiants
fa-clock-oscheduleéchéance
fa-exclamation-circlewarningalerte
fa-historyhistoryjournal

Le nom d'une ligature ne se devine pas et la feuille de style ne les énumère pas. La façon fiable de vérifier qu'un nom existe est de le chercher dans le source livré, qui sert de dictionnaire :

grep -rho 'data-icon="[a-z_]*"' /usr/lib/python3/dist-packages/odoo/addons/*/static/src \
  | sort | uniq -c | sort -rn | head -40

Les record.champ.raw_value des gabarits kanban, eux, sont inchangés : seule l'icône est à reprendre.

Contribuer à Odoo

Une version jeune se traverse avec des aspérités : défauts non encore signalés, traductions en retard. Les deux se remontent en amont, mais pas par le même canal, et c'est la source d'erreur la plus fréquente.

Signaler un défaut

Les signalements passent par les issues du dépôt odoo/odoo sur GitHub. La politique du projet est explicite et contre-intuitive : « Issues are handled with a much lower priority than pull requests ». Une issue seule pèse peu ; une pull request qui décrit le problème dans sa description vaut infiniment plus — et il ne faut alors pas créer d'issue en plus.

Avant de signaler quoi que ce soit :

  • Vérifier que le défaut existe encore sur la dernière version, testable sur runbot.odoo.com ;
  • Chercher les doublons parmi les issues existantes ;
  • Utiliser le gabarit officiel : version d'Odoo, étapes de reproduction, comportement attendu contre observé ;
  • Pour tout ce qui touche à l'interface web, préciser le système d'exploitation et le navigateur, et joindre une capture.

Une faille de sécurité ne se signale jamais par une issue publique : elle relève de la page Responsible Disclosure d'Odoo.

Traduire — surtout pas par une pull request

Odoo n'utilise plus Transifex. Les traductions sont gérées sur leur propre instance Weblate : translate.odoo.com. Le wiki du projet est catégorique — « Do not create pull requests to change the translations ». Une PR de traduction est refusée.

À l'inscription, on rejoint l'équipe Suggesters : on peut proposer des traductions pour toutes les langues. L'accès en écriture directe se demande à translations@odoo.com.

Le français a un traducteur interne chez Odoo. Pour les langues concernées — dont fr — les contributeurs externes ne peuvent soumettre que des suggestions, revues puis répercutées par le traducteur maison sur l'ensemble des modules. C'est une limite réelle, mais c'est ce qui garantit la cohérence terminologique d'une version à l'autre. Les suggestions restent utiles, en particulier sur une version récente dont les traductions accusent du retard.

Quand une traduction arrive-t-elle réellement dans l'instance ?

Le cycle complet, souvent sous-estimé :

  1. Weblate est synchronisé vers GitHub chaque week-end ;
  2. Les nightly builds en bénéficient le lundi — c'est le cas d'une installation par paquet ;
  3. Après mise à jour du paquet, un administrateur doit recharger la langue : Paramètres → Traductions → Langues, bouton Mettre à jour.
La case qui décide de tout. Lors du rechargement, il faut cocher « Écraser les termes existants ». Sans elle, les traductions déjà présentes en base ne sont pas remplacées et la mise à jour paraît sans effet.
Un piège de méthode, pour finir. Après un systemctl reload nginx, l'ancien processus de travail continue de servir les connexions en cours avec l'ancienne configuration. Un contrôle lancé dans la seconde qui suit interroge donc la configuration précédente et renvoie un résultat faux. Avant de conclure qu'un changement n'a pas pris, attendre que les processus sortants aient disparu — ou simplement rejouer le test quelques secondes plus tard.