Ce que tu vas apprendre
Les trois ingrédients
Un rapport PDF Odoo tient en trois objets : un format papier, une action et un template. Ni plus, ni moins.
Le branchement au menu
Comment binding_model_id fait apparaître ton rapport dans le menu
Imprimer du bon modèle, sans toucher aux vues.
Le squelette QWeb
L'enchaînement html_container → external_layout →
page qui donne en-tête et pied de page société gratuitement.
contacts installé,
et un module custom. On reste en pur déclaratif : aucune ligne de Python. Si tu
cherches plutôt à retoucher un rapport existant, c'est l'objet de
l'article
précédent sur l'héritage xpath.
Un rapport, trois objets
Créer un rapport de zéro intimide souvent. Pourtant, Odoo n'attend que trois pièces, et elles se déclarent toutes en XML :
le format papier → ir.actions.report
l'action + le menu → template QWeb
le contenu
Le format papier fixe les marges et l'orientation. L'action relie un modèle, un template et ce format ; c'est elle qui décide où le bouton d'impression apparaît. Le template dessine le document. On les pose dans cet ordre. Notre exemple : une fiche contact imprimable depuis n'importe quelle société ou personne.
Étape 1 — Le format papier
Un report.paperformat décrit la feuille : taille, orientation, marges en
millimètres, espacement de l'en-tête. On en crée un dédié plutôt que de réutiliser celui
d'Odoo, pour garder la main sur les marges de notre fiche.
<record id="paperformat_contact_sheet" model="report.paperformat">
<field name="name">Fiche contact A4</field>
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<field name="margin_top">40</field>
<field name="margin_bottom">25</field>
<field name="margin_left">10</field>
<field name="margin_right">10</field>
<field name="header_spacing">35</field>
<field name="dpi">90</field>
</record>
Rien d'obligatoire ici : sans format dédié, Odoo prend celui de la société. Mais dès qu'on veut des marges précises, ce petit enregistrement fait la différence. Côté configuration, le format d'impression se règle aussi sans code.
Étape 2 — L'action de rapport
Le cœur du dispositif. L'ir.actions.report relie tout : le modèle visé
(res.partner), le template à rendre, le format papier. Et surtout, son
binding_model_id est ce qui fait surgir l'entrée dans le menu d'impression
du contact.
<record id="action_report_contact_sheet" model="ir.actions.report">
<field name="name">Fiche contact</field>
<field name="model">res.partner</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">odooskills_contact_report.report_contact_sheet</field>
<field name="report_file">odooskills_contact_report.report_contact_sheet</field>
<field name="print_report_name">'Fiche - %s' % (object.name)</field>
<field name="paperformat_id" ref="paperformat_contact_sheet"/>
<field name="binding_model_id" ref="base.model_res_partner"/>
<field name="binding_type">report</field>
</record>
Trois champs méritent l'attention. report_name doit être préfixé du
nom technique du module (module.template) : c'est l'identifiant
complet du template, pas son seul id. print_report_name calcule
le nom du fichier téléchargé à partir de l'enregistrement (object).
binding_model_id, enfin, pointe vers l'identifiant externe du modèle —
base.model_res_partner — et c'est lui qui branche le rapport sur le menu.
Résultat immédiat : dans le menu d'actions ⚙ d'une fiche contact, une entrée Fiche contact est apparue. Aucun bouton à coder, aucune vue à modifier.
binding_model_id.Étape 3 — Le template QWeb
Le contenu, enfin. Un template de rapport suit toujours le même squelette :
web.html_container enveloppe le tout, on boucle sur les enregistrements
(docs), et web.external_layout apporte gratuitement l'en-tête et
le pied de page de la société. Notre contenu vit dans le <div class="page">.
Ce squelette est le même pour tous les rapports ; on le retrouve en détail dans
les fondamentaux du moteur
QWeb.
<template id="report_contact_sheet">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<div class="page">
<h2 t-field="doc.name"/>
<div t-if="doc.function" class="text-muted" t-field="doc.function"/>
<div class="row mt-3">
<div class="col-6">
<strong>Adresse</strong>
<div t-if="doc.street" t-field="doc.street"/>
<div><span t-field="doc.zip"/> <span t-field="doc.city"/></div>
<div t-if="doc.country_id" t-field="doc.country_id"/>
</div>
<div class="col-6">
<strong>Coordonnées</strong>
<div t-if="doc.phone">Tél. : <span t-field="doc.phone"/></div>
<div t-if="doc.email">Courriel : <span t-field="doc.email"/></div>
<div t-if="doc.website">Site : <span t-field="doc.website"/></div>
</div>
</div>
<div class="row mt-4">
<div class="col-6" t-if="doc.parent_id">
<strong>Société</strong>
<div t-field="doc.parent_id.name"/>
</div>
<div class="col-6" t-if="doc.vat">
<strong>N° TVA</strong>
<div t-field="doc.vat"/>
</div>
</div>
<div class="mt-4" t-if="doc.category_id">
<strong>Étiquettes : </strong>
<span t-foreach="doc.category_id" t-as="tag" class="me-1">
<span class="badge text-bg-secondary" t-field="tag.name"/>
</span>
</div>
</div>
</t>
</t>
</t>
</template>
La variable de boucle s'appelle doc (on itère sur docs, fourni
automatiquement par le moteur de rapport). Chaque champ s'affiche avec
t-field, qui gère le formatage et la traduction. Une fois le module installé,
cliquer sur Fiche contact produit ce PDF, en-tête société comprise :
external_layout, contenu de la fiche dans la page.⚠️ Quatre pièges qui font échouer le rendu
html_containerest obligatoire. Sans cette enveloppe, le PDF ne charge ni les styles ni la structure de page. C'est la première chose à vérifier devant une page blanche.report_name=module.template. Oublier le préfixe du module donne un rapport « introuvable » à l'impression.t-fieldexige un point.t-field="doc"lève une erreur : il faut toujours un champ, commedoc.name. Pour afficher l'enregistrement lui-même, on liste ses champs un à un.- Le champ
mobilea disparu deres.partneren v19. Reprendre un vieux template qui l'utilise plante le rendu ; on s'appuie désormais surphone.
À retenir
🧱 Trois objets suffisent
Format papier, action, template : un rapport complet sans une ligne de Python.
🖨️ Le menu vient du binding
binding_model_id + binding_type="report" branchent le
rapport sur le menu d'impression du modèle.
🏛️ external_layout offert
En-tête et pied de page société sont fournis ; ton contenu vit dans la
page.
🔖 Nomme bien le template
report_name doit être préfixé du module — l'erreur n°1 du débutant.
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
- Personnaliser un rapport existant (xpath) — quand il s'agit de retoucher plutôt que de créer.
- Générer des rapports PDF avec QWeb — les fondamentaux du moteur de rendu.
- Côté métier : modèles de rapports et format d'impression — régler la mise en page sans coder.