Ce que tu vas apprendre
Hériter, pas copier
Cibler un nœud précis du rapport standard avec xpath et y greffer
ton contenu — le reste du modèle officiel continue d'évoluer avec Odoo.
Les trois positions clés
before, after et attributes : trois
façons d'insérer ou de modifier, illustrées sur une vraie facture.
Un geste réutilisable
Le même schéma s'applique au devis, à la demande de prix et au bon de commande fournisseur. Un tableau de correspondance te donne les bons identifiants.
account,
sale et purchase installés, et un module custom où déposer
les vues. Aucune ligne de Python n'est nécessaire : un rapport PDF Odoo est un
template QWeb, et on le personnalise comme n'importe quelle vue — par héritage.
Un rapport PDF n'est qu'une vue QWeb
Derrière chaque bouton Imprimer se cache une action
ir.actions.report qui pointe vers un template QWeb. Pour
la facture client, ce template s'appelle account.report_invoice_document.
Odoo le rend en HTML, puis le convertit en PDF. (Quand c'est un classeur qu'il te faut
plutôt qu'un PDF, générer
un rapport Excel natif répond à l'autre moitié du besoin.)
La tentation du débutant : copier ce template entier dans son module pour le modifier. Mauvaise idée. À la moindre mise à jour d'Odoo, ta copie diverge du modèle officiel et tu rates les corrections. La bonne approche consiste à hériter le template et à n'y décrire que ta modification. À l'inverse, bâtir un rapport QWeb de toutes pièces repart, lui, d'un template vierge — c'est un autre chantier.
report_invoice_document
+
Vue héritéeinherit_id + xpath
→
Rapport PDF final
Une vue héritée déclare l'inherit_id du template à étendre, puis une ou
plusieurs balises <xpath>. Chaque xpath désigne un nœud
du template d'origine et une position qui dit où insérer : avant,
après, à l'intérieur, ou bien en remplaçant ses attributs.
Le cas pilier : la facture client
Objectif concret : faire ressortir l'échéance de paiement avant le tableau des lignes, et ajouter un encadré « coordonnées bancaires » après les totaux. On touche ici au contenu ; pour la mise en page globale (marges, format papier), le format d'impression se règle sans code. Voici la facture standard, telle qu'Odoo la produit par défaut.
Et voici la même facture, une fois le module de personnalisation installé : un bandeau d'échéance apparaît au-dessus des lignes, et un encadré de mentions se loge sous les totaux. Le corps officiel du rapport, lui, n'a pas bougé d'un pixel.
Le code : trois positions xpath
Tout tient dans une seule vue héritée. On déclare l'inherit_id, puis on
enchaîne les xpath. Remarque la variable de boucle : dans le rapport
de facture, l'enregistrement courant s'appelle o.
<template id="report_invoice_document_oski"
inherit_id="account.report_invoice_document">
<!-- 1) position="before" : un bandeau juste AVANT le tableau des lignes -->
<xpath expr="//table[@name='invoice_line_table']" position="before">
<div t-if="o.move_type == 'out_invoice' and o.invoice_date_due"
style="border-left:3px solid #d6336c;background:#fdf2f6">
<span class="fw-bold">Échéance de paiement :</span>
<span t-field="o.invoice_date_due"/>
</div>
</xpath>
<!-- 2) position="attributes" : on AJOUTE une classe au tableau existant -->
<xpath expr="//table[@name='invoice_line_table']" position="attributes">
<attribute name="class" add="oski-lines" separator=" "/>
</xpath>
<!-- 3) position="after" : un encadré APRÈS le bloc des totaux -->
<xpath expr="//div[@id='total']" position="after">
<div style="border-top:3px solid #d6336c;background:#fbfbfc">
<p class="fw-bold">Merci de votre confiance.</p>
<p t-if="o.partner_bank_id">
<span class="fw-bold">IBAN :</span>
<span t-field="o.partner_bank_id.acc_number"/>
</p>
</div>
</xpath>
</template>
Trois techniques, trois usages. before et after insèrent un
bloc relativement à un nœud existant. attributes ne crée rien : il
modifie un attribut du nœud visé — ici on ajoute une classe sans écraser
celles d'origine, grâce à add= et separator=" ".
Comment choisir le nœud cible ? On lit le template standard et on s'accroche à un
point stable : un id (comme id="total") ou un attribut
name (comme name="invoice_line_table"). Ces ancres sont
pensées pour l'héritage et changent rarement d'une version à l'autre — bien plus sûres
qu'un chemin reposant sur la position d'un <div>.
Côté manifeste, aucune dépendance exotique : on déclare les modules dont on étend les rapports, et le fichier de vues.
{
'name': 'Personnalisation rapports PDF',
'version': '19.0.1.0.0',
'depends': ['account', 'sale', 'purchase'],
'data': ['report/account_invoice_inherit.xml'],
'license': 'LGPL-3',
}
⚠️ Le piège qui fait perdre une heure : o ou doc ?
La variable qui représente l'enregistrement courant n'est pas la même selon
le rapport. Reprendre un xpath d'un rapport à l'autre sans
vérifier ce nom provoque un rendu vide ou une erreur QWeb :
- Facture, bon de commande, demande de prix → variable
o - Devis / commande de vente → variable
doc
En cas de doute, ouvre le template d'origine et cherche le
t-foreach ou le t-set qui définit la variable de boucle.
Le même geste, sur tous les documents
La méthode ne change pas : on hérite le bon template et on cible une ancre stable.
Seuls l'inherit_id et la variable de boucle varient. Voici la table de
correspondance pour les quatre rapports les plus demandés.
| Document | inherit_id | Variable |
|---|---|---|
| Facture client | account.report_invoice_document | o |
| Devis / Pro forma | sale.report_saleorder_document | doc |
| Demande de prix (RFQ) | purchase.report_purchasequotation_document | o |
| Bon de commande fournisseur | purchase.report_purchaseorder_document | o |
Détail utile : le rapport Pro forma de vente
(sale.report_saleorder_pro_forma) réutilise en interne le même
report_saleorder_document. Hériter ce dernier suffit donc à toucher le
devis et la pro forma, sans une ligne de plus.
Sur le devis, le même schéma ajoute un rappel de validité de l'offre. La greffe
s'accroche cette fois au bloc d'informations, avec la variable doc :
<template id="report_saleorder_document_oski"
inherit_id="sale.report_saleorder_document">
<xpath expr="//div[@id='informations']" position="after">
<div t-if="doc.validity_date" style="border-left:3px solid #d6336c">
Offre valable jusqu'au <span t-field="doc.validity_date"/>.
</div>
</xpath>
</template>
doc.Quatre réflexes Odoo 19
t-out, jamaist-esc. Pour afficher une valeur calculée,t-outa remplacé l'ancient-esc. Pour un champ, on gardet-field.- Cibler par
@name, pas par@string. Dans unxpath, on s'accroche à@nameou@id.@stringn'est pas un point d'ancrage fiable. - Filtrer une classe avec
hasclass(). Pour viser un nœud par sa classe,//div[hasclass('page')]est plus robuste que//div[@class='page'], qui casse dès qu'une classe s'ajoute. - Les styles inline passent toujours. Un PDF est rendu hors
navigateur ; un
style="…"directement sur l'élément s'applique sans dépendre d'un bundle d'assets.
À retenir
🎯 Hériter plutôt que copier
Un inherit_id + des xpath ciblés : ta perso
survit aux mises à jour d'Odoo.
📌 Trois positions
before et after insèrent, attributes
modifie un attribut existant.
🧭 Des ancres stables
Accroche-toi à un @name ou un @id du template
d'origine, pas à une position fragile.
🔁 Un geste, quatre documents
Facture, devis, RFQ, bon de commande : même méthode, seul
l'inherit_id change — attention à o vs doc.
Télécharge le Guide Technique Odoo
Un module Odoo 19 fonctionnel, 20+ articles techniques, environnements de dev et pipeline complet — le tout en PDF.
Télécharger le guide📘 Pour aller plus loin : nos formations Odoo 19
À lire également
- Générer un rapport PDF avec QWeb — créer son propre rapport de A à Z, la suite logique de cet article.
- Côté métier : modèles de rapports et format d'impression — régler la mise en page sans toucher au code.
- L'autre face du reporting : générer un rapport Excel natif quand le PDF ne suffit plus.