Skip to Content

Odoo 19 Community: Merging Draft Invoices – Our Open-Source Module Merge Draft Invoices

18 September 2026 by
Odoo 19 Community: Merging Draft Invoices – Our Open-Source Module Merge Draft Invoices
Alexander Biringer | Biricon IT Services GmbH

Anyone who bills from orders, helpdesk tickets and time tracking in Odoo knows the result: at the end of the month, a customer gets not one invoice but five. Each comes from a different order or ticket, each is correct in substance – and yet the customer asks whether it couldn't all fit on one document.

There is no clean solution for exactly this in Odoo 19 Community. The well-known OCA module account_invoice_merge has not yet been released for Odoo 19, and it solves the problem in a way that poses a serious risk for service providers: identical lines are combined, and with that the link back to the order, the task and the recorded time is lost.

For our own Odoo 19 Community installation, we have therefore developed the module Merge Draft Invoices. Because this problem does not affect only us, we are making it available free of charge on GitHub under the free AGPL-3 licence.

To the project on GitHub:

Odoo 19 Merge Draft Invoices

Why Our Own Module Instead of the OCA Solution?

The OCA has been maintaining account_invoice_merge for many years in the OCA/account-invoicing repository. At the time of this post, the migration to 19.0 is still an open pull request there, not a published module.

More importantly, however, the two modules do different things:


OCA account_invoice_mergeMerge Draft Invoices
Resultis merged into one of the selected invoicesalways a new invoice
Linesidentical lines are combined, quantities addedevery line is carried over one to one
Origininvoice_origin / payment_reference concatenated in the headerorigin fields copied per line via Odoo's own _copy_data_extend_business_fields hook
Time trackingnot taken into accounttimesheet_invoice_id is re-pointed to the new invoice
Readable for the customernothinga heading above the lines of each source, with order and ticket
Source invoicesare cancelledcancel (default), delete or keep – with clearly named consequences

Combining identical lines is the decisive difference. A combined line can carry only one origin. As soon as two lines from two different orders look the same, the link from the invoice back to the order, the task and the time entry is gone. In a service business where billing runs via ticket → time tracking → invoice, this means: the same hours are billed twice or disappear.

Our module therefore does not combine anything as a matter of principle. The merged invoice has exactly as many lines as its sources taken together.

No code was taken from the OCA module. Both modules are licensed under AGPL-3.

What the Module Does

In the invoice list view, select two or more drafts and choose Merge draft invoices in the action menu – a wizard shows the selection, the checks and the options for the source invoices. After confirming, you land directly on the new invoice.

Before merging, the module runs its checks, all of them at once, so that you do not see the next error after every correction:

  • at least two invoices selected
  • all in Draft status
  • none has ever been posted (a document reset to draft keeps its posting history and is rejected)
  • none has already been merged
  • all are an invoice, vendor bill, credit note or receipt – no pure journal entries
  • same partner, document type, company, currency and journal

Differences in payment terms, fiscal position, delivery address, bank account or salesperson do not block the merge. The wizard warns and takes over the value of the oldest selected invoice.

What Is Preserved

The core of the module is that every link an invoice line has to its origin also exists on the new invoice:

LinkHandling
Sales order lines (sale_line_ids)copied per line via Odoo's _copy_data_extend_business_fields
Purchase order lines (purchase_line_id)likewise
Cost centre distribution (analytic_distribution)copied per line
other origin fields per lineautomatically, if the supplying module implements the documented Odoo hook
Origin and reference in the invoice headermerged, deduplicated, editable in the wizard
Sales order: invoice_ids, invoice_status, qty_invoicedare computed from the lines – the order recognises the new invoice by itself
Time tracking (timesheet_invoice_id)is re-pointed to the new invoice before the sources become invalid
Helpdesk ticketis attached to the time tracking and therefore automatically points to the new invoice
Chattermessage on the new invoice with all sources, message on each source with a link to the new one
merged_into_move_id / merged_from_move_idsstored link in both directions

Tax lines, payment term lines, rounding and cash discount lines are not copied but recomputed by Odoo from the content. The merged invoice therefore has one tax line per tax rate, not one per source. It is created as an unnumbered draft; the merge itself consumes no document number.

Headings: The Customer Sees Where Each Line Comes From

This is the function that makes the difference in everyday use. Above the lines of each source invoice, the module inserts a section line – the same element that Odoo itself uses for the “Down Payments” section:

BE10181                                     <- Heading
  22,500 x Technician hour        3.577,50
                         Subtotal 3.577,50
BE10182 · Ticket: HT00219                   <- Heading
   3,000 x Technician hour          477,00
                         Subtotal   477,00
                      NET AMOUNT 4.054,50

The heading is printed on the document the customer receives, and Odoo's standard invoice report adds a subtotal per section by itself. The customer thus also sees what each part cost.

The label is assembled from what the source actually has:

  1. the orders, directly from the line links – the names are therefore exact; if order links are missing, the free text from invoice_origin is used
  2. the tickets, determined via time tracking – output as Ticket: HT00219 or Tickets: …, limited to five plus “and N more”
  3. if neither exists: the number of the source invoice, then its reference
  4. if there is nothing at all: a neutral “Further lines”, so that the lines of a source never silently disappear into the block above

What is missing is omitted, not invented. The ticket is printed with its own number – helpdesk_mgmt number, Enterprise ticket_ref or the display name of the model – never with the database ID.

The headings are written in the customer's language, just as Odoo does when generating invoices from orders. The chatter remains in the user's language.

A heading carries no product, quantity, price, tax, account or cost centre distribution. It cannot affect totals, tax base or cost accounting. Anyone who switches off the headings in the wizard gets exactly the invoice that would have been created without this function – a test compares both results down to the cent.

What Happens to the Source Invoices

The wizard offers three options:

  • Cancel (default, recommended). Cancelling a draft has no consequences: no document number, no posting. The invoices remain in the database as cancelled documents with chatter and a link to the new invoice. qty_invoiced on the sales order skips cancelled documents, nothing is counted twice. This is the option that is still traceable six months from now.
  • Delete. Irreversible. Chatter and back-reference die with the record; only the message on the new invoice remains. Sensible for throwaway drafts that nobody should see any more.
  • Leave as draft. The sources remain open. Their sales orders then count the source and the new invoice as invoiced – the work looks as if it has been billed twice until someone cleans up the drafts by hand. Intended for the rare case where you want to compare before deciding. A test records exactly this double counting so that it does not surprise anyone.

Limits – What the Module Deliberately Does Not Do

Please read before production use:

  • Drafts only. Posted invoices are not merged. That is a credit-note task, not a copy operation.
  • Drafts that were posted once and then reset are rejected. They have a number and a posting history; hiding that in another document is nothing a bulk action should do silently.
  • No merging across partners, currencies, companies or journals. There is no sensible single value for this, so the module refuses instead of guessing.
  • payment_reference is not carried over. Structured payment references identify a document; two concatenated together match nothing at the bank. The new invoice gets its own when it is posted.
  • Down payment lines are copied like any other line. If the sources contain down payment lines from sale, the result should be checked before posting.
  • Only timesheet_invoice_id is re-pointed automatically. Every other model with a Many2one to account.move (own billing runs, subscription links, EDI transmissions) continues to point to the source. The _merge_draft_relink hook exists for this.
  • No combining of identical lines – deliberately. Anyone who wants a short, tidy invoice is in the wrong place here and would pay for it with the origin links.
  • Headings are created once, at the time of merging. After that they are ordinary section lines: editable, deletable and not updated if an order or ticket is renamed later.
  • A heading names the tickets of the entire source invoice, not of the individual line.

Installation in Odoo 19 Community

Installation works as with any usual Odoo add-on module:

  1. Copy the module directory into a configured addons path.
  2. Update the app list in Odoo.
  3. Install the account_invoice_merge_draft module.

The module depends only on account. Everything it knows about sale, purchase, project, hr_timesheet, sale_timesheet or a helpdesk module is determined at runtime – it installs and works even on a pure accounting database.

Then: Accounting → Customers → Invoices (or Vendors → Bills), select two or more drafts, gear menu → Merge draft invoices. The action is available to all users with the Billing / Invoicing group.

Anyone upgrading from an earlier version of the module should update once with -u account_invoice_merge_draft --i18n-overwrite so that changed field labels arrive in the German translation. A fresh installation is not affected.

For Developers: API and Hooks

Merging can also be called from your own code:

python

merged = draft_moves.merge_draft_moves(
    extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
    source_handling="cancel",   # "delete" or "keep"
    add_source_sections=True,   # headings; default True
)

merge_draft_moves raises a UserError with all rejection reasons if the selection cannot be merged. _merge_draft_get_errors() returns the same reasons as a list without an exception.

If you have your own back-references to account.move, override _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}
        )

If you have a back-reference on the invoice line, you do not need this module at all: Odoo's own hook account.move.line._copy_data_extend_business_fields is enough, and the field is carried over equally when merging, cancelling and duplicating. For other helpdesk modules or a different wording of the headings, there are _merge_draft_get_source_tickets() and _merge_draft_section_name().

Tested Before It Runs on Production Data

48 tests cover mixed statuses, different partners, currencies and document types, lines with and without a sales or purchase order, taxes, sections and notes, cost centre distributions, the re-pointing of time tracking, all three options for the source invoices, the wizard and the headings – that they are active by default, that switching them off reproduces the old result down to the cent, that a heading carries no amounts or accounts, that the lines of a source stay together, that the ticket appears with its number and never with the database ID.

The test data builds on the existing chart of accounts of the active company rather than on AccountTestInvoicingCommon – that helper often fails to start on migrated production databases, and those are exactly the ones the module is meant to run on.

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

As with any extension in the accounting area, we recommend testing the installation first in a test environment with a copy of your own database.

Free and Open Source for the Odoo Community

At Biricon IT Services GmbH, we develop solutions first for specific requirements from our own operations. If this results in a generally usable module, we do not want to hide it away in a single installation.

Merge Draft Invoices is therefore available free of charge as an open-source project. The source code can be viewed, reviewed, adapted and further developed within the terms of the AGPL-3 licence.

Our aim was not the shortest possible invoice, but an invoice on which, even a year from now, every line still leads back to its order, its ticket and its time entry.

Project, Source Code and Contributing

The complete source code, the installation instructions and the technical documentation are available on GitHub:

Open Odoo 19 Merge Draft Invoices on GitHub

We welcome practical experience, bug reports, suggestions for improvement and contributions from the Odoo Community.

Free. Traceable. Open source. For Odoo 19 Community.