Se rendre au contenu

Insérer des graphiques dans un rapport Excel sous Odoo 19

Série Rapports Excel · Article 4/6 — Développement Odoo
25 juin 2026 par
Insérer des graphiques dans un rapport Excel sous Odoo 19
| Aucun commentaire pour l'instant

Série Rapports Excel · Article 4/6

Insérer des graphiques dans un rapport Excel sous Odoo 19

Les tableaux donnent les chiffres ; les graphiques donnent le sens. Place au tableau de bord visuel : un histogramme et un camembert du chiffre d'affaires par produit, une courbe par mois — des graphiques natifs Excel, éditables, générés avec add_chart et insert_chart de la seule bibliothèque xlsxwriter embarquée.

~12 minutes de lecture · niveau intermédiaire · parcours Framework & ORM

Ce que tu vas apprendre

Agréger en Python

Construire les séries à tracer — CA par produit, CA par mois — en sommant directement sur le recordset, sans SQL.

Trois types de graphiques

Histogramme, camembert et courbe avec add_chart, chacun nourri d'une plage de données par add_series.

Un tableau de bord

Poser les graphiques sur une feuille dédiée avec insert_chart — des objets vivants, pas des images figées.

Un graphique Excel n'est pas une image

Point clé avant de coder : xlsxwriter ne colle pas une image dans la feuille, il écrit un vrai graphique Excel. Il pointe vers une plage de cellules, se met à jour si les données changent, et reste éditable dans le tableur. Le principe tient en trois temps : écrire les données dans une feuille, créer un objet graphique avec workbook.add_chart, puis l'ancrer sur une feuille avec worksheet.insert_chart.

Tout passe par une méthode _build_sale_lines_xlsx_chart sur sale.order, appuyée sur un agrégateur et une petite méthode d'écriture de table. On construit bloc par bloc.

1. Agréger le chiffre d'affaires

Un graphique a besoin de séries courtes et propres, pas de la liste brute des lignes. On agrège donc le CA HT par produit et par mois dans deux dictionnaires, puis on trie : les produits par montant décroissant (top 10), les mois par ordre chronologique.

    def _aggregate_ca(self):
        """Agrège le CA HT des lignes par produit et par mois."""
        by_product = {}
        by_month = {}
        for order in self:
            month_key = order.date_order.strftime('%Y-%m') if order.date_order else _('Inconnu')
            for line in order.order_line.filtered(lambda l: not l.display_type):
                name = line.product_id.display_name or line.name
                by_product[name] = by_product.get(name, 0.0) + line.price_subtotal
                by_month[month_key] = by_month.get(month_key, 0.0) + line.price_subtotal

        products = sorted(by_product.items(), key=lambda kv: kv[1], reverse=True)[:10]
        months = sorted(by_month.items(), key=lambda kv: kv[0])
        # Libellé lisible 'MM/AAAA' pour les mois datés.
        months = [
            (('%s/%s' % (k[5:], k[:4])) if '-' in k else k, v)
            for k, v in months
        ]
        return products, months

Trier les mois sur la clé 'AAAA-MM' garantit l'ordre chronologique avant de transformer la clé en libellé 'MM/AAAA' plus lisible. Cette agrégation Python suffit pour des volumes raisonnables ; un prochain article de la série montrera comment agréger en SQL pour les gros jeux de données.

2. Les feuilles de données et le tableau de bord

Comme à l'article multi-feuilles, on crée la feuille « Tableau de bord » en premier pour qu'elle reste l'onglet de tête, puis deux feuilles de données. Les graphiques référenceront ces feuilles par leur nom — l'ordre de création des feuilles de données n'a aucune importance, les références sont résolues à la fermeture du classeur.

        # Feuille tableau de bord créée EN PREMIER → onglet en tête. Les
        # graphiques référencent des plages par leur nom de feuille ; les
        # feuilles de données peuvent être créées après.
        dashboard = workbook.add_worksheet(_('Tableau de bord'))
        dashboard.activate()

        prod_sheet_name = _('CA par produit')
        month_sheet_name = _('CA par mois')
        n_prod = self._write_data_sheet(
            workbook.add_worksheet(prod_sheet_name), prod_sheet_name,
            _('Produit'), products, title_fmt, header_fmt, text_fmt, money_fmt,
        )
        n_month = self._write_data_sheet(
            workbook.add_worksheet(month_sheet_name), month_sheet_name,
            _('Mois'), months, title_fmt, header_fmt, text_fmt, money_fmt,
        )

La méthode d'écriture est volontairement générique : un titre fusionné, un en-tête, puis les couples (libellé, montant). Les données démarrent en ligne 3 (index 2) — un détail qui fixera les coordonnées des plages de graphiques.

    def _write_data_sheet(self, sheet, sheet_title, label, rows,
                          title_fmt, header_fmt, text_fmt, money_fmt):
        """Écrit une table à deux colonnes (libellé, CA HT). Renvoie le nb de lignes."""
        sheet.merge_range(0, 0, 0, 1, sheet_title, title_fmt)
        sheet.set_row(0, 22)
        sheet.write(1, 0, label, header_fmt)
        sheet.write(1, 1, _('CA HT'), header_fmt)
        row = 2
        for name, amount in rows:
            sheet.write(row, 0, name, text_fmt)
            sheet.write_number(row, 1, amount, money_fmt)
            row += 1
        sheet.set_column(0, 0, 32)
        sheet.set_column(1, 1, 16)
        return len(rows)

3. L'histogramme du CA par produit

Un graphique se crée avec add_chart({'type': ...}), reçoit une série via add_series, puis se pose avec insert_chart. Le plus robuste pour décrire une plage est la forme liste [nom_feuille, première_ligne, première_colonne, dernière_ligne, dernière_colonne] : pas de chaîne A1 à construire à la main, donc pas d'erreur d'index.

        # --- Graphique 1 : histogramme du CA par produit ---
        if n_prod:
            col_chart = workbook.add_chart({'type': 'column'})
            col_chart.add_series({
                'name': _('CA HT par produit'),
                'categories': [prod_sheet_name, 2, 0, n_prod + 1, 0],
                'values': [prod_sheet_name, 2, 1, n_prod + 1, 1],
                'data_labels': {'value': True, 'num_format': '#,##0\\ €'},
                'fill': {'color': '#1D6F42'},
            })
            col_chart.set_title({'name': _('CA HT par produit')})
            col_chart.set_legend({'none': True})
            col_chart.set_y_axis({'num_format': '#,##0\\ €'})
            col_chart.set_size({'width': 640, 'height': 380})
            dashboard.insert_chart('B2', col_chart)

Les données commençant en ligne 3 (index 2) et la feuille comptant n_prod lignes, la plage va de l'index 2 à n_prod + 1. On masque la légende (une seule série), on formate l'axe et les étiquettes en euros, et on ancre le graphique en B2.

Histogramme Excel du chiffre d'affaires HT par produit, colonnes vertes avec étiquettes en euros au-dessus de chaque barre
L'histogramme : une colonne par produit, étiquette de valeur en euros, classé par CA décroissant.

4. Le camembert de répartition

Le camembert réutilise exactement la même plage de données — seul le type change. Ses étiquettes affichent un pourcentage plutôt qu'un montant, et la légende passe en bas pour laisser de la place aux noms de produits.

            # --- Graphique 2 : camembert de répartition par produit ---
            pie_chart = workbook.add_chart({'type': 'pie'})
            pie_chart.add_series({
                'name': _('Répartition du CA par produit'),
                'categories': [prod_sheet_name, 2, 0, n_prod + 1, 0],
                'values': [prod_sheet_name, 2, 1, n_prod + 1, 1],
                'data_labels': {'percentage': True},
            })
            pie_chart.set_title({'name': _('Répartition du CA par produit')})
            pie_chart.set_legend({'position': 'bottom'})
            pie_chart.set_size({'width': 640, 'height': 420})
            dashboard.insert_chart('B22', pie_chart)

Le data_labels à percentage est calculé par Excel à l'ouverture : inutile de pré-calculer les parts. La légende en bottom évite que les longs libellés de produits débordent à droite du graphique.

Camembert Excel de la répartition du chiffre d'affaires par produit, parts en pourcentage et légende des produits en bas
Le camembert : la même série vue en parts de marché, légende des produits placée en bas.

5. La courbe du CA par mois

La courbe trace l'évolution temporelle : elle pointe vers la seconde feuille de données (CA par mois). Une largeur de trait et des marqueurs ronds rendent la tendance lisible.

        # --- Graphique 3 : courbe du CA par mois ---
        if n_month:
            line_chart = workbook.add_chart({'type': 'line'})
            line_chart.add_series({
                'name': _('CA HT par mois'),
                'categories': [month_sheet_name, 2, 0, n_month + 1, 0],
                'values': [month_sheet_name, 2, 1, n_month + 1, 1],
                'line': {'color': '#1D6F42', 'width': 2.25},
                'marker': {'type': 'circle', 'size': 6},
            })
            line_chart.set_title({'name': _('CA HT par mois')})
            line_chart.set_legend({'none': True})
            line_chart.set_y_axis({'num_format': '#,##0\\ €'})
            line_chart.set_size({'width': 640, 'height': 380})
            dashboard.insert_chart('K2', line_chart)

Trois graphiques sur la même feuille, ancrés en B2, B22 et K2 : un véritable tableau de bord. Chacun reste un objet Excel vivant, que l'utilisateur peut retailler, recolorer ou repointer.

Courbe Excel du chiffre d'affaires HT par mois, ligne verte avec marqueurs ronds et axe vertical en euros
La courbe : l'évolution du CA mois après mois, marqueurs ronds et axe en euros.

Le controller et le bouton

Le service est identique au reste de la série : une route HTTP réservée aux utilisateurs connectés sert les octets, déclenchée par un bouton d'en-tête sur sale.order.

    @route('/odooskills/export/sale_lines/xlsx_chart', type='http', auth='user', readonly=True)
    def export_sale_lines_xlsx_chart(self, order_ids=None, **kw):
        ids = [int(x) for x in (order_ids or '').split(',') if x.strip().isdigit()]
        orders = request.env['sale.order'].browse(ids).exists()
        content = orders._build_sale_lines_xlsx_chart()
        headers = [
            ('Content-Type',
             'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
            ('Content-Disposition', content_disposition('rapport_ventes_graphiques.xlsx')),
        ]
        return request.make_response(content, headers)
En-tête d'une commande de vente Odoo 19 avec le bouton Exporter en Excel (graphiques) aux côtés des boutons des articles précédents
Le bouton « Exporter en Excel (graphiques) » rejoint ceux des articles précédents.

⚠️ Pièges à éviter

  • Un graphique pointe vers des cellules : la feuille de données doit exister et porter les valeurs. Sans données écrites, le graphique s'affiche vide.
  • Préférer la forme liste [feuille, l1, c1, l2, c2] à la chaîne A1 pour les plages : moins d'erreurs d'index, et le nom de feuille est géré pour toi.
  • Attention aux bornes : données démarrant en index 2 sur n lignes → dernière ligne à l'index n + 1.
  • Le format monétaire d'un axe ou d'une étiquette s'écrit avec un antislash d'échappement, #,##0\\ €, comme pour une cellule.
  • Sur un camembert aux libellés longs, placer la légende en bottom évite qu'elle déborde à droite.

À retenir

  • Agréger d'abord en Python des séries courtes ; un graphique se nourrit d'une plage, pas d'une liste brute de lignes.
  • add_chart + add_series + insert_chart : trois appels suffisent, et le type seul distingue histogramme, camembert et courbe.
  • Les graphiques sont des objets Excel vivants, pointés sur des cellules — éditables et recalculés à l'ouverture, pas des images figées.

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

Se connecter pour laisser un commentaire.
Un rapport Excel multi-feuilles avec sommaire dans Odoo 19
Série Rapports Excel · Article 3/6 — Développement Odoo