Se rendre au contenu

Un rapport PDF dynamique dans Odoo 19

Série Rapports PDF · Article 5/5 — Développement Odoo
28 juin 2026 par
Un rapport PDF dynamique dans Odoo 19
| Aucun commentaire pour l'instant

Série Rapports PDF · Article 5/5

Un rapport PDF dynamique dans Odoo 19

Jusqu'ici, le template lisait des champs. Ici, il affiche des données calculées : une synthèse agrégée par catégorie, un total général, et un QR code. Le contenu ne vient plus d'un enregistrement, mais d'un modèle Python qui le fabrique.

~10 minutes de lecture · niveau avancé · parcours Web & UI

Ce que tu vas apprendre

Calculer les données

Le modèle de rapport Python et sa méthode _get_report_values, qui injecte un dictionnaire sur mesure dans le template.

Agréger en SQL

_read_group pour sommer quantités et montants par groupe, sans charger chaque ligne en mémoire.

Insérer un QR code

Le contrôleur /report/barcode/QR/… qui transforme n'importe quelle valeur en code à scanner.

odoo-bin -i odooskills_report_dynamic_demo -d ta_base

Le flux des données

Un rapport statique lit directement l'enregistrement (doc.name, doc.partner_id…). Un rapport dynamique insère une étape : un modèle Python calcule les données — agrégats, totaux, valeurs dérivées — et les passe au template sous forme de dictionnaire. Le template ne fait plus qu'afficher — il reste construit sur le même squelette html_containerexternal_layout que les rapports précédents.

sale.order.line les données brutes _read_group agrégation SQL _get_report_values le dictionnaire template QWeb t-foreach + QR

1 — Le modèle de rapport

Tout commence par un AbstractModel dont le nom est report. suivi du report_name de l'action. C'est cette convention de nommage qui dit au moteur : « pour ce rapport, appelle ma méthode _get_report_values avant de rendre le template ». La méthode reçoit les docids sélectionnés et renvoie le dictionnaire qui peuplera le template.

from odoo import models


class ReportSalesSummary(models.AbstractModel):
    _name = 'report.odooskills_report_dynamic_demo.report_sales_summary'
    _description = "Synthèse des ventes par catégorie"

    def _get_report_values(self, docids, data=None):
        docs = self.env['sale.order'].browse(docids)

        # Agrégation SQL : somme des quantités et des montants par produit.
        groups = self.env['sale.order.line']._read_group(
            domain=[('order_id', 'in', docs.ids), ('display_type', '=', False)],
            groupby=['product_id'],
            aggregates=['product_uom_qty:sum', 'price_subtotal:sum'],
        )

        # Repli par catégorie (dictionnaire simple, sans import externe).
        by_category = {}
        for product, qty, subtotal in groups:
            name = product.categ_id.display_name or "Sans catégorie"
            bucket = by_category.setdefault(name, {'qty': 0.0, 'subtotal': 0.0})
            bucket['qty'] += qty
            bucket['subtotal'] += subtotal

        summary = [
            {'category': name, 'qty': vals['qty'], 'subtotal': vals['subtotal']}
            for name, vals in sorted(by_category.items())
        ]
        grand_total = sum(row['subtotal'] for row in summary)

        return {
            'doc_ids': docids,
            'doc_model': 'sale.order',
            'docs': docs,
            'summary': summary,
            'grand_total': grand_total,
        }

Les clés doc_ids, doc_model et docs sont attendues par convention ; summary et grand_total sont nos données maison. Tout ce qui est renvoyé là devient une variable disponible dans le template.

2 — Agréger avec _read_group

Le cœur du calcul. _read_group délègue l'agrégation à PostgreSQL au lieu de parcourir chaque ligne en Python — indispensable dès que le volume grimpe. Sa signature tient en trois arguments utiles :

groups = self.env['sale.order.line']._read_group(
    domain=[('order_id', 'in', docs.ids), ('display_type', '=', False)],
    groupby=['product_id'],
    aggregates=['product_uom_qty:sum', 'price_subtotal:sum'],
)
# -> liste de tuples : [(product, qty_sum, subtotal_sum), ...]

Le domain filtre les lignes (ici, celles des commandes choisies, hors lignes de section). groupby liste les axes de regroupement. aggregates énumère les calculs au format 'champ:fonction':sum, :avg, :count, etc. Le retour est une liste de tuples : le premier élément est la valeur du groupe (le recordset produit pour un champ relationnel), les suivants sont les agrégats, dans l'ordre demandé. On les déballe directement dans la boucle. La signature complète figure dans le source ORM d'Odoo 19.

3 — Le template consomme les données

Côté QWeb, plus de t-field : les valeurs ne sont pas des champs d'un enregistrement, mais des entrées de dictionnaire. On les affiche avec t-out, qui évalue une expression Python. On boucle sur summary, et on lit grand_total dans le pied du tableau :

<t t-call="web.external_layout">
    <t t-set="o" t-value="docs[:1]"/>
    <t t-set="layout_document_title">Synthèse des ventes</t>
    <div class="page">
        <p>Synthèse portant sur <span t-out="len(docs)"/> commande(s).</p>
        <table class="table table-sm">
            <tbody>
                <tr t-foreach="summary" t-as="row">
                    <td><span t-out="row['category']"/></td>
                    <td class="text-end"><span t-out="'%.2f' % row['qty']"/></td>
                    <td class="text-end"><span t-out="'%.2f' % row['subtotal']"/></td>
                </tr>
            </tbody>
            <tfoot>
                <tr><th>Total général</th><th/>
                    <th class="text-end"><span t-out="'%.2f' % grand_total"/></th></tr>
            </tfoot>
        </table>
    </div>
</t>

Extrait condensé : le module complet ajoute les en-têtes de colonnes et le bloc QR détaillé plus bas.

On pose o = docs[:1] pour que external_layout retrouve la société et son en-tête — c'est là que se branche tout le travail de mise en page et de branding. Le résultat : une page de synthèse unique, agrégée sur l'ensemble des commandes sélectionnées. Pour la tester : dans Ventes, coche plusieurs commandes dans la liste, puis Imprimer → Synthèse des ventes.

Rapport PDF Odoo 19 de synthèse des ventes : tableau agrégé par catégorie de produit avec quantités, totaux HT, total général et QR code
La synthèse rendue : cinq catégories agrégées, un total général, un QR code — toutes données calculées en Python.

4 — Le QR code

Odoo embarque un générateur de codes-barres accessible par une simple URL d'image. Pour un QR, on appelle /report/barcode/QR/<valeur> dans le src d'une balise img. La valeur peut être n'importe quoi — un numéro, un texte, ou ici une URL de vérification construite à partir de la commande :

<img t-att-src="'/report/barcode/QR/%s' % ('https://odooskills.com/verifier/%s' % docs[0].name)"
     style="width:110px;height:110px;"/>

Le contrôleur accepte aussi une forme paramétrée (/report/barcode/?barcode_type=QR&value=…&width=…&height=…) pour régler finement la taille ou le niveau de correction d'erreur. Scanné, ce QR ouvre la page de vérification — un usage courant pour relier un document papier à son équivalent en ligne.

Détail d'un rapport PDF Odoo 19 montrant un QR code généré par le contrôleur report barcode, avec une légende invitant à scanner pour vérifier la synthèse
Le QR code généré par /report/barcode/QR/…, prêt à être scanné.

⚠️ Trois pièges du rapport dynamique

  • _read_group, avec l'underscore. L'ancien read_group public est retiré ; on utilise _read_group(domain, groupby, aggregates), qui renvoie des tuples, pas des dictionnaires.
  • t-out, pas t-field. Les données calculées ne sont pas des champs d'un enregistrement ; t-field exigerait un record.champ. Pour une valeur de dictionnaire, c'est t-out.
  • Le nom du modèle fait le lien. Si _name ne vaut pas exactement report. + le report_name de l'action, _get_report_values n'est jamais appelée et le template ne reçoit que le contexte par défaut.

À retenir

🧮 _get_report_values fabrique les données

Un AbstractModel nommé report.<report_name> injecte un dictionnaire sur mesure.

⚡ _read_group agrège en SQL

Sommes et moyennes par groupe sans charger chaque ligne ; retour en tuples.

🔤 t-out affiche le calculé

Les valeurs de dictionnaire s'affichent avec t-out, pas t-field.

🔳 Un QR en une URL

/report/barcode/QR/<valeur> dans un img, rien de plus.

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

À lire également

↔ Besoin de chiffres, pas de PDF ?

Pour un export chiffré exploitable plutôt qu'un PDF, la série Excel couvre le sujet : générer un rapport Excel XLSX natif. Et côté configuration sans code, le blog fonctionnel détaille les modèles de rapports et le format d'impression.

Se connecter pour laisser un commentaire.
Mise en page et branding d'un rapport PDF dans Odoo 19
Série Rapports PDF · Article 4/5 — Développement Odoo