Se rendre au contenu

Odoo 19 Community : fusionner les factures brouillon – notre module open source Merge Draft Invoices

18 septembre 2026 par
Odoo 19 Community : fusionner les factures brouillon – notre module open source Merge Draft Invoices
Alexander Biringer | Biricon IT Services GmbH

Quiconque facture dans Odoo à partir de commandes, de tickets helpdesk et de saisies de temps en connaît le résultat : en fin de mois, un client ne reçoit pas une facture, mais cinq. Chacune provient d'une autre commande ou d'un autre ticket, chacune est correcte sur le fond – et pourtant le client demande si tout cela ne tiendrait pas sur un seul document.

C'est précisément pour cela qu'il n'existe pas de solution propre dans Odoo 19 Community. Le module OCA bien connu account_invoice_merge n'est pas encore publié pour Odoo 19, et il résout le problème d'une manière qui représente un risque sérieux pour les prestataires de services : les lignes identiques sont regroupées, et le lien vers la commande, la tâche et le temps saisi est ainsi perdu.

Pour notre propre installation Odoo 19 Community, nous avons donc développé le module Merge Draft Invoices. Comme ce problème ne concerne pas que nous, nous le mettons gratuitement à disposition sur GitHub sous la licence libre AGPL-3.

Accéder au projet sur GitHub :

Odoo 19 Merge Draft Invoices

Pourquoi un module dédié plutôt que la solution OCA ?

L'OCA maintient account_invoice_merge depuis de nombreuses années dans le dépôt OCA/account-invoicing. Au moment de la rédaction de cet article, la migration vers la 19.0 y est encore une pull request ouverte, et non un module publié.

Plus important encore, les deux modules font des choses différentes :


OCA account_invoice_mergeMerge Draft Invoices
Résultatest fusionnée dans l'une des factures sélectionnéestoujours une nouvelle facture
Lignesles lignes identiques sont regroupées, les quantités additionnéeschaque ligne est reprise à l'identique
Origineinvoice_origin / payment_reference concaténés dans l'en-têtechamps d'origine copiés par ligne via le hook _copy_data_extend_business_fields propre à Odoo
Saisie de tempsnon prise en comptetimesheet_invoice_id est rattaché à la nouvelle facture
Lisible pour le clientrienun titre au-dessus des lignes de chaque source, avec commande et ticket
Factures sourcessont annuléesannuler (par défaut), supprimer ou conserver – avec des conséquences clairement indiquées

Le regroupement des lignes identiques est la différence décisive. Une ligne regroupée ne peut porter qu'une seule origine. Dès que deux lignes issues de deux commandes différentes se ressemblent, le lien entre la facture et la commande, la tâche et la saisie de temps disparaît. Dans une entreprise de services, où la facturation passe par ticket → saisie de temps → facture, cela signifie : les mêmes heures sont facturées deux fois ou disparaissent.

Notre module ne regroupe donc fondamentalement rien. La facture fusionnée comporte exactement autant de lignes que ses sources réunies.

Aucun code du module OCA n'a été repris. Les deux modules sont sous AGPL-3.

Ce que fait le module

Dans la vue liste des factures, sélectionner deux brouillons ou plus, choisir Fusionner les factures brouillon dans le menu d'actions – un assistant affiche la sélection, les vérifications et les options relatives aux factures sources. Après confirmation, on arrive directement sur la nouvelle facture.

Avant la fusion, le module effectue des vérifications, et ce tous les points à la fois, afin de ne pas découvrir l'erreur suivante après chaque correction :

  • au moins deux factures sélectionnées
  • toutes à l'état Brouillon
  • aucune n'a jamais été comptabilisée (un document remis en brouillon conserve son historique de comptabilisation et est refusé)
  • aucune n'a déjà été fusionnée
  • toutes sont des factures, factures fournisseurs, avoirs ou justificatifs – pas de simples pièces comptables
  • même partenaire, même type de document, même société, même devise et même journal

Les différences de conditions de paiement, de position fiscale, d'adresse de livraison, de compte bancaire ou de vendeur ne bloquent pas. L'assistant avertit et reprend la valeur de la plus ancienne des factures sélectionnées.

Ce qui est conservé

Le cœur du module est que chaque lien qu'une ligne de facture entretient avec son origine existe également sur la nouvelle facture :

LienTraitement
Lignes de commande client (sale_line_ids)copiées par ligne via _copy_data_extend_business_fields d'Odoo
Lignes de commande d'achat (purchase_line_id)de même
Répartition analytique (analytic_distribution)copiée par ligne
autres champs d'origine par ligneautomatiquement, si le module concerné implémente le hook Odoo documenté
Origine et référence dans l'en-tête de la facturefusionnées, dédupliquées, modifiables dans l'assistant
Commande client : invoice_ids, invoice_status, qty_invoicedsont calculés à partir des lignes – la commande reconnaît d'elle-même la nouvelle facture
Saisie de temps (timesheet_invoice_id)est rattachée à la nouvelle facture avant que les sources ne deviennent invalides
Ticket helpdeskest lié à la saisie de temps et pointe ainsi automatiquement vers la nouvelle facture
Chattermessage sur la nouvelle facture avec toutes les sources, message sur chaque source avec un lien vers la nouvelle
merged_into_move_id / merged_from_move_idslien enregistré dans les deux sens

Les lignes de taxe, de conditions de paiement, d'arrondi et d'escompte ne sont pas copiées, mais recalculées par Odoo à partir du contenu. La facture fusionnée comporte donc une ligne de taxe par taux de taxe, et non une par source. Elle est créée sans numéro, à l'état de brouillon ; la fusion elle-même ne consomme aucun numéro de document.

Titres : le client voit d'où provient chaque ligne

C'est la fonction qui fait la différence au quotidien. Au-dessus des lignes de chaque facture source, le module insère une ligne de section – le même élément qu'Odoo utilise lui-même pour la section « Acomptes » :

BE10181                                     <- Titre
  22,500 x Heure de technicien    3.577,50
                      Sous-total 3.577,50
BE10182 · Ticket: HT00219                   <- Titre
   3,000 x Heure de technicien      477,00
                      Sous-total   477,00
                     MONTANT NET 4.054,50

Le titre est imprimé sur le document que reçoit le client, et le rapport de facture standard d'Odoo ajoute de lui-même un sous-total par section. Le client voit ainsi aussi ce que chaque partie a coûté.

Le libellé est composé à partir de ce que la source possède réellement :

  1. les commandes, directement à partir des liens des lignes – les noms sont donc exacts ; si les liens vers les commandes manquent, le texte libre de invoice_origin est utilisé
  2. les tickets, déterminés via la saisie de temps – affichés sous la forme Ticket : HT00219 ou Tickets : …, limités à cinq plus « et N autres »
  3. si aucun des deux n'existe : le numéro de la facture source, puis sa référence
  4. s'il n'existe rien du tout : un « Autres lignes » neutre, afin que les lignes d'une source ne disparaissent jamais silencieusement dans le bloc situé au-dessus

Ce qui manque est omis, pas inventé. Le ticket est imprimé avec son propre numéro – numéro helpdesk_mgmt, ticket_ref Enterprise ou nom d'affichage du modèle – jamais avec l'ID de la base de données.

Les titres sont rédigés dans la langue du client, comme Odoo le fait lors de la création de factures à partir de commandes. Le chatter reste dans la langue de l'utilisateur.

Un titre ne porte ni produit, ni quantité, ni prix, ni taxe, ni compte, ni répartition analytique. Il ne peut influencer ni les totaux, ni la base fiscale, ni la comptabilité analytique. Qui désactive les titres dans l'assistant obtient exactement la facture qui aurait été créée sans cette fonction – un test compare les deux résultats au centime près.

Ce qu'il advient des factures sources

L'assistant propose trois options :

  • Annuler (par défaut, recommandé). L'annulation d'un brouillon est sans conséquence : pas de numéro de document, pas d'écriture. Les factures restent dans la base de données en tant que documents annulés avec chatter et lien vers la nouvelle facture. qty_invoiced sur la commande client ignore les documents annulés, rien n'est compté en double. C'est l'option qui reste compréhensible même dans six mois.
  • Supprimer. Irréversible. Le chatter et le renvoi disparaissent avec l'enregistrement ; seul le message sur la nouvelle facture subsiste. Pertinent pour les brouillons jetables que plus personne ne doit voir.
  • Laisser en brouillon. Les sources restent ouvertes. Leurs commandes client comptent alors la source et la nouvelle facture comme facturées – le travail semble facturé deux fois jusqu'à ce que quelqu'un nettoie les brouillons à la main. Prévu pour le cas rare où l'on souhaite comparer avant de décider. Un test fixe précisément ce double comptage afin qu'il ne surprenne personne.

Limites – ce que le module ne fait volontairement pas

À lire avant l'utilisation en production :

  • Uniquement les brouillons. Les factures comptabilisées ne sont pas fusionnées. C'est une tâche d'avoir, pas une opération de copie.
  • Les brouillons déjà comptabilisés une fois puis remis en brouillon sont refusés. Ils possèdent un numéro et un historique de comptabilisation ; cacher cela dans un autre document n'est pas quelque chose qu'une action groupée devrait faire en silence.
  • Aucune fusion entre partenaires, devises, sociétés ou journaux différents. Il n'existe pour cela aucune valeur unique pertinente, le module refuse donc au lieu de deviner.
  • payment_reference n'est pas reprise. Les références de paiement structurées identifient un document ; deux références concaténées ne correspondent à rien auprès de la banque. La nouvelle facture reçoit la sienne lors de la comptabilisation.
  • Les lignes d'acompte sont copiées comme n'importe quelle autre ligne. Si les sources contiennent des lignes d'acompte issues de sale, il faut vérifier le résultat avant la comptabilisation.
  • Seul timesheet_invoice_id est rattaché automatiquement. Tout autre modèle possédant un Many2one vers account.move (cycles de facturation propres, liens d'abonnement, transmissions EDI) continue de pointer vers la source. Le hook _merge_draft_relink existe pour cela.
  • Pas de regroupement des lignes identiques – volontairement. Qui veut une facture courte et épurée se trompe de module et le paierait par la perte des liens d'origine.
  • Les titres sont créés une seule fois lors de la fusion. Ensuite, ce sont des lignes de section normales : modifiables, supprimables, et non mises à jour si la commande ou le ticket sont renommés plus tard.
  • Un titre mentionne les tickets de l'ensemble de la facture source, et non ceux de la ligne individuelle.

Installation dans Odoo 19 Community

L'installation se déroule comme pour un module complémentaire Odoo ordinaire :

  1. Copier le répertoire du module dans un chemin d'addons configuré.
  2. Mettre à jour la liste des applications dans Odoo.
  3. Installer le module account_invoice_merge_draft.

Le module ne dépend que de account. Tout ce qu'il sait de sale, purchase, project, hr_timesheet, sale_timesheet ou d'un module helpdesk est déterminé à l'exécution – il s'installe et fonctionne aussi sur une base de données purement de facturation.

Ensuite : Comptabilité → Clients → Factures (ou Fournisseurs → Factures), sélectionner deux brouillons ou plus, menu roue dentée → Fusionner les factures brouillon. L'action est disponible pour tous les utilisateurs du groupe Facturation.

Qui effectue une mise à jour depuis une version antérieure du module devrait exécuter une fois -u account_invoice_merge_draft --i18n-overwrite, afin que les libellés de champs modifiés arrivent dans la traduction allemande. Une nouvelle installation n'est pas concernée.

Pour les développeurs : API et hooks

La fusion peut aussi être appelée depuis son propre code :

python

merged = draft_moves.merge_draft_moves(
    extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
    source_handling="cancel",   # "delete" ou "keep"
    add_source_sections=True,   # titres ; par défaut True
)

merge_draft_moves lève une UserError avec tous les motifs de refus lorsque la sélection ne peut pas être fusionnée. _merge_draft_get_errors() fournit les mêmes motifs sous forme de liste, sans exception.

Qui possède ses propres renvois vers account.move surcharge _merge_draft_relink :

python

class AccountMove(models.Model):
    _inherit = "account.move"

    def _merge_draft_relink(self, new_move):
        super()._merge_draft_relink(new_move)
        if "my_move_id" not in self.env["my.model"]._fields:
            return
        self.env["my.model"].search([("my_move_id", "in", self.ids)]).write(
            {"my_move_id": new_move.id}
        )

Qui possède un renvoi sur la ligne de facture n'a pas du tout besoin de ce module : le hook propre à Odoo account.move.line._copy_data_extend_business_fields suffit, et le champ est repris de la même manière lors de la fusion, de l'annulation et de la duplication. Pour d'autres modules helpdesk ou une autre formulation des titres, il existe _merge_draft_get_source_tickets() et _merge_draft_section_name().

Testé avant de tourner sur des données de production

48 tests couvrent les états mixtes, les partenaires, devises et types de documents différents, les lignes avec et sans commande client ou commande d'achat, les taxes, les sections et les notes, les répartitions analytiques, le rattachement de la saisie de temps, les trois options pour les factures sources, l'assistant et les titres – qu'ils sont actifs par défaut, que leur désactivation reproduit l'ancien résultat au centime près, qu'un titre ne porte ni montants ni comptes, que les lignes d'une source restent ensemble, que le ticket apparaît avec son numéro et jamais avec l'ID de la base de données.

Les données de test s'appuient sur le plan comptable existant de la société active plutôt que sur AccountTestInvoicingCommon – cet assistant ne démarre souvent pas sur des bases de production migrées, et c'est précisément sur de telles bases que le module doit tourner.

odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
         --test-tags /account_invoice_merge_draft --stop-after-init

Comme pour toute extension dans le domaine comptable, nous recommandons de vérifier d'abord l'installation dans un environnement de test avec une copie de sa propre base de données.

Gratuit et open source pour la communauté Odoo

Chez Biricon IT Services GmbH, nous développons d'abord des solutions pour des besoins concrets de notre propre exploitation. Lorsqu'il en résulte un module d'usage général, nous ne souhaitons pas le cacher dans une seule installation.

Merge Draft Invoices est donc disponible gratuitement en tant que projet open source. Le code source peut être consulté, vérifié, adapté et développé dans le cadre de la licence AGPL-3.

Notre objectif n'était pas la facture la plus courte possible, mais une facture dont chaque ligne ramène, même dans un an, à sa commande, à son ticket et à sa saisie de temps.

Projet, code source et contribution

Le code source complet, les instructions d'installation et la documentation technique sont disponibles sur GitHub :

Ouvrir Odoo 19 Merge Draft Invoices sur GitHub

Nous apprécions les retours d'expérience, les rapports d'erreurs, les suggestions d'amélioration et les contributions de la communauté Odoo.

Gratuit. Traçable. Open source. Pour Odoo 19 Community.