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.
sale, et un module
custom avec du Python. On suppose acquis le squelette d'un rapport QWeb ; si ce n'est
pas le cas, commence par
créer
un rapport de zéro. Installation classique :
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_container →
external_layout que les rapports précédents.
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.
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.
/report/barcode/QR/…, prêt à être scanné.⚠️ Trois pièges du rapport dynamique
_read_group, avec l'underscore. L'ancienread_grouppublic est retiré ; on utilise_read_group(domain, groupby, aggregates), qui renvoie des tuples, pas des dictionnaires.t-out, past-field. Les données calculées ne sont pas des champs d'un enregistrement ;t-fieldexigerait unrecord.champ. Pour une valeur de dictionnaire, c'estt-out.- Le nom du modèle fait le lien. Si
_namene vaut pas exactementreport.+ lereport_namede l'action,_get_report_valuesn'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📘 Pour aller plus loin : nos formations Odoo 19
À lire également
- Mise en page et branding — habiller la synthèse qu'on vient de calculer.
- Créer son rapport PDF de A à Z — le squelette de départ.
- Générer des rapports PDF avec QWeb — les fondamentaux du moteur de rendu.
↔ 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.