Blog11 mars 2026 · Mis à jour le 13 mars 2026Par Rachid, Architecte Odoo Senior

Migration de t-esc vers t-out dans OWL 3.0 :
Guide pratique d'un architecte senior pour Odoo 19

LE PROBLÈME

Pourquoi une simple dépréciation peut casser toute votre bibliothèque de modules

Si vous maintenez des modules Odoo personnalisés, vous avez probablement vu cet avertissement dans la console depuis Odoo 18 :

[OWL] t-esc is deprecated. Use t-out instead.

Dans Odoo 18, c'était un avertissement. Dans Odoo 19, c'est une erreur bloquante. OWL 3.0 a complètement supprimé le support de t-esc, et chaque template qui l'utilise encore refusera de compiler à l'exécution.

Pour une entreprise avec 5 à 10 modules personnalisés, cela peut représenter 20 templates à mettre à jour. Pour une organisation avec plus de 80 modules construits sur trois versions d'Odoo ? Vous risquez des centaines de vues cassées le jour J de votre mise à jour — sauf si vous anticipez.

Ce n'est pas un simple rechercher-remplacer. Le passage de t-esc à t-out modifie le comportement d'échappement par défaut, avec des implications directes en matière de sécurité. Une erreur, et vous cassez soit votre interface, soit vous ouvrez votre application aux attaques Cross-Site Scripting (XSS).

LE CHANGEMENT TECHNIQUE

Comment OWL 3.0 a modifié le rendu des templates dans Odoo 19

Dans OWL 2.x (Odoo 17–18), le moteur de templates proposait deux directives pour l'affichage dynamique :

  • t-esc — Affiche une expression avec échappement HTML (sécurisé par défaut)
  • t-raw — Affiche du HTML brut sans échappement (déprécié depuis Odoo 16)

OWL 3.0 fusionne les deux en une seule directive unifiée : t-out.

La différence comportementale clé :

  • t-out avec une valeur string → échappement HTML automatique (identique à l'ancien t-esc)
  • t-out avec un objet Markup → rendu en HTML brut (remplace l'ancien t-raw)

Cela signifie que la décision d'échappement passe de la couche template à la couche données. Au lieu de choisir t-esc vs. t-raw dans votre XML, vous contrôlez maintenant la sécurité en encapsulant le HTML de confiance dans l'utilitaire markup() d'OWL au niveau JavaScript.

Insight Architectural

C'est un choix de conception délibéré, emprunté aux moteurs de templates Jinja2 et Django. En rendant le producteur de données responsable de déclarer « ceci est du HTML sûr », on élimine toute une catégorie de bugs où un développeur utilise accidentellement t-raw sur une saisie utilisateur.

XSS ET SÉCURITÉ

Pourquoi t-out est plus sécurisé que t-esc ne l'a jamais été

L'ancien modèle à double directive (t-esc + t-raw) avait un défaut fondamental : c'est l'auteur du template qui décidait de l'échappement. Dans les grandes bases de code avec plusieurs développeurs, cela menait à des pratiques comme :

<!-- Dangereux : le développeur a choisi t-raw pour le "formatage" -->
<span t-raw="record.user_description"/>

<!-- Le champ user_description contient une saisie utilisateur -->
<!-- Résultat : vulnérabilité XSS -->

Avec t-out, ce schéma devient structurellement impossible sauf si le développeur encapsule explicitement la valeur avec markup() :

<!-- Sécurisé : t-out échappe automatiquement les strings -->
<span t-out="record.user_description"/>

<!-- Si vous avez vraiment besoin de HTML brut (ex : champ rich-text assaini) : -->
// Dans votre composant JS :
import { markup } from "@odoo/owl";
get safeDescription() {
    return markup(this.record.user_description);
}

L'appel à markup() agit comme une porte de sécurité explicite. Il est visible en revue de code, recherchable par grep, et force le développeur à se demander : « Suis-je certain que ce contenu est assaini ? »

Bonne Pratique Sécurité

N'appelez jamais markup() sur une saisie utilisateur à moins qu'elle ait été assainie côté serveur. Le fields.Html d'Odoo avec sanitize=True (par défaut) est sûr. Les fields.Text bruts ou les réponses API ne le sont pas.

ANCIEN vs. NOUVEAU

t-esc vs. t-out : comparaison complète pour la migration de modules Odoo

AspectAncienne méthode (t-esc / t-raw)Méthode Odoo 19 (t-out)
Directivet-esc pour échappé, t-raw pour brutt-out pour les deux
Décision d'échappementCouche template (XML)Couche données (JS/Python)
Sortie HTML brut<span t-raw="html_content"/><span t-out="markup(html_content)"/>
Surface de risque XSSTout template utilisant t-rawUniquement le code appelant markup()
Signal en revue de codeScanner tous les templates XMLGrep markup( dans les fichiers JS
Statut Odoo 19Supprimé — erreur de compilationRequis — seule directive supportée
Contenu par défautNon supporté<t t-out="val">texte par défaut</t>
SCRIPT DE MIGRATION

Automatiser la migration t-esc vers t-out dans votre bibliothèque de modules

Voici un script Python que nous utilisons en interne chez Octura Solutions pour automatiser le remplacement dans toute une bibliothèque de modules personnalisés. Il gère les templates XML, les fichiers QWeb et génère un journal d'audit détaillé.

#!/usr/bin/env python3
"""
migrate_tesc_to_tout.py
Remplace t-esc par t-out dans tous les templates XML/QWeb d'une arborescence Odoo.
Génère un journal d'audit CSV pour la revue de code.

Usage :
    python migrate_tesc_to_tout.py /chemin/vers/custom_addons [--dry-run]
"""
import argparse
import csv
import os
import re
import sys
from datetime import datetime
from pathlib import Path

# Correspond à t-esc="..." (guillemets simples et doubles)
T_ESC_PATTERN = re.compile(
    r'\bt-esc\s*=\s*(["\'])(.*?)\1',
    re.DOTALL,
)

# Attrape aussi les t-raw restants (déprécié depuis Odoo 16)
T_RAW_PATTERN = re.compile(
    r'\bt-raw\s*=\s*(["\'])(.*?)\1',
    re.DOTALL,
)

EXTENSIONS = {'.xml', '.qweb'}


def find_template_files(root: Path):
    """Parcourt tous les fichiers XML/QWeb sous la racine."""
    for dirpath, _, filenames in os.walk(root):
        for fname in filenames:
            fpath = Path(dirpath) / fname
            if fpath.suffix.lower() in EXTENSIONS:
                yield fpath


def migrate_file(fpath: Path, dry_run: bool = False):
    """Remplace t-esc et t-raw dans un fichier. Retourne la liste des changements."""
    content = fpath.read_text(encoding='utf-8')
    changes = []

    def replace_tesc(match):
        quote = match.group(1)
        expr = match.group(2)
        changes.append({
            'file': str(fpath),
            'old': match.group(0),
            'new': f't-out={quote}{expr}{quote}',
            'type': 't-esc → t-out',
            'line': content[:match.start()].count('\n') + 1,
        })
        return f't-out={quote}{expr}{quote}'

    def replace_traw(match):
        quote = match.group(1)
        expr = match.group(2)
        changes.append({
            'file': str(fpath),
            'old': match.group(0),
            'new': f't-out={quote}{expr}{quote}',
            'type': 't-raw → t-out (⚠ VÉRIFIER : ajouter markup())',
            'line': content[:match.start()].count('\n') + 1,
        })
        return f't-out={quote}{expr}{quote}'

    new_content = T_ESC_PATTERN.sub(replace_tesc, content)
    new_content = T_RAW_PATTERN.sub(replace_traw, new_content)

    if changes and not dry_run:
        fpath.write_text(new_content, encoding='utf-8')

    return changes


def main():
    parser = argparse.ArgumentParser(
        description='Migrer t-esc/t-raw → t-out pour Odoo 19 / OWL 3.0'
    )
    parser.add_argument('root', help='Répertoire racine des addons personnalisés')
    parser.add_argument(
        '--dry-run',
        action='store_true',
        help='Aperçu des changements sans écrire les fichiers',
    )
    args = parser.parse_args()

    root = Path(args.root).resolve()
    if not root.is_dir():
        print(f"Erreur : {root} n'est pas un répertoire", file=sys.stderr)
        sys.exit(1)

    all_changes = []
    file_count = 0

    for fpath in find_template_files(root):
        changes = migrate_file(fpath, dry_run=args.dry_run)
        if changes:
            file_count += 1
            all_changes.extend(changes)

    # Générer le journal d'audit CSV
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    log_file = root / f'tesc_migration_log_{timestamp}.csv'
    with open(log_file, 'w', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(
            f, fieldnames=['file', 'line', 'type', 'old', 'new']
        )
        writer.writeheader()
        writer.writerows(all_changes)

    mode = "SIMULATION" if args.dry_run else "APPLIQUÉ"
    print(f"\n[{mode}] Migration terminée.")
    print(f"  Fichiers affectés : {file_count}")
    print(f"  Total changements : {len(all_changes)}")
    t_raw_hits = sum(
        1 for c in all_changes if 't-raw' in c['type']
    )
    if t_raw_hits:
        print(
            f"  ⚠ t-raw → t-out : {t_raw_hits} "
            f"(REVUE MANUELLE : ajouter markup() en JS)"
        )
    print(f"  Journal d'audit  : {log_file}")


if __name__ == '__main__':
    main()

Comment l'utiliser :

Étape 1

Lancez en mode simulation : python migrate_tesc_to_tout.py ./custom_addons --dry-run

Étape 2

Examinez le journal d'audit CSV généré. Portez une attention particulière aux entrées t-raw → t-out — celles-ci nécessitent l'ajout de markup() dans le code JS/Python correspondant.

Étape 3

Appliquez les changements : python migrate_tesc_to_tout.py ./custom_addons

Étape 4

Lancez votre suite de tests et vérifiez manuellement chaque remplacement t-raw dans un environnement de staging.

Pour les modules riches en JavaScript, voici une commande compagnon pour trouver tous les templates de composants OWL utilisant encore t-esc :

# Bash : trouver les t-esc restants dans les littéraux JS et XML
grep -rn --include="*.js" --include="*.xml" \
  't-esc=' ./custom_addons/ | grep -v node_modules
INSIGHTS D'EXPERT

3 pièges qui cassent les migrations Odoo 19 (et comment nous les gérons)

Après avoir migré plus de 40 bibliothèques de modules personnalisés vers Odoo 19, voici les trois problèmes qui prennent les équipes au dépourvu :

Piège #1

Les templates hérités qui surchargent t-esc dans les vues standard Odoo. Votre module peut utiliser xpath pour injecter du contenu dans une vue standard qu'Odoo a déjà migrée vers t-out. Le script regex ne les détectera pas car le t-esc se trouve dans un bloc xpath position="replace" ciblant un nœud qui n'existe plus. Notre solution : Nous exécutons une passe de validation secondaire qui charge chaque template dans une instance Odoo de test et capture les erreurs de compilation au niveau QWeb.

Piège #2

Le contenu Fields.Html rendu sans markup() après migration. Si vous remplacez aveuglément t-raw par t-out, tout contenu fields.Html s'affichera soudain comme des entités HTML échappées (&lt;p&gt;Bonjour&lt;/p&gt;) au lieu de texte formaté. Notre solution : Le script de migration signale chaque remplacement t-raw séparément. Nous traçons ensuite chaque expression jusqu'à sa définition de champ et ajoutons l'encapsulation markup() uniquement pour les champs Html assainis.

Piège #3

Les modules OCA tiers avec des dépendances OWL non verrouillées. De nombreux modules OCA référencent encore t-esc dans leurs vues. Si vous mettez à jour Odoo mais gardez une branche OCA plus ancienne, le module se charge correctement — jusqu'à ce que le template compile et plante. Notre solution : Nous maintenons une matrice de compatibilité par projet qui associe chaque module OCA installé à sa branche compatible Odoo 19 (ou notre fork corrigé). Nous ne mettons jamais à jour le core Odoo sans mettre à jour simultanément l'ensemble de l'arbre de dépendances.

ROI MÉTIER

Ce que cela signifie pour votre résultat net

La migration t-esct-out est souvent rejetée comme « juste un changement de template ». Voici pourquoi c'est une idée coûteuse :

  • Blocage de mise à jour : Les directives t-esc non résolues empêchent complètement votre mise à jour Odoo 19. Chaque semaine de retard est une semaine sans nouvelles fonctionnalités, correctifs de sécurité et améliorations de performance.
  • Responsabilité sécurité : Les directives t-raw mal migrées créent des vecteurs d'attaque XSS. Une seule vulnérabilité dans un portail client peut coûter plus de 50 000 € en réponse à incident et dommages réputationnels.
  • Temps développeur : La migration manuelle sur plus de 50 modules prend typiquement 2 à 4 semaines de temps développeur senior. Notre approche automatisée réduit cela à 2 à 3 jours incluant revue et tests.
  • Pérennité : Les modules correctement migrés vers t-out sont compatibles avec la feuille de route OWL jusqu'à Odoo 20+. Vous investissez une seule fois et évitez la dette de migration récurrente.
NOTES SEO

Métadonnées d'optimisation

Meta Description

Odoo 19 supprime t-esc dans OWL 3.0. Apprenez à migrer vers t-out, éviter les failles XSS et automatiser le changement avec notre script Python.

Mots-clés H2

1. « Comment OWL 3.0 a modifié le rendu des templates dans Odoo 19 »
2. « Automatiser la migration t-esc vers t-out dans votre bibliothèque de modules »
3. « 3 pièges qui cassent les migrations Odoo 19 (et comment nous les gérons) »

Mots-clés cibles

Odoo 19 t-esc déprécié, guide migration OWL 3.0, directive t-out Odoo, migration template Odoo 19, Odoo QWeb t-esc vers t-out

Vous migrez vers Odoo 19 ? Faisons-le correctement.

Les dépréciations de templates ne sont qu'une pièce du puzzle Odoo 19. OWL 3.0 apporte des changements au cycle de vie des composants, à la réactivité et au pipeline de bundling des assets — chacun avec ses propres exigences de migration.

Chez Octura Solutions, nous avons migré des bibliothèques de modules d'entreprise avec plus de 100 modules personnalisés vers Odoo 19 sans un seul jour d'indisponibilité. Que vous ayez besoin d'un audit de migration complet, d'un sprint de remédiation t-esc ciblé ou d'un support de mise à jour à long terme — nous l'avons déjà fait, et nous le ferons bien.

Réserver un audit de migration gratuit