Anyone who invoices from sales orders, helpdesk tickets and timesheets in Odoo knows the result: at the end of the month, a customer receives five invoices instead of one. Each comes from a different order or ticket, and each is correct – yet the customer still asks whether they could all fit on a single document.
Odoo 19 Community has no straightforward solution for this. The well-known OCA module account_invoice_merge has not yet been released for Odoo 19, and it tackles the problem in a way that poses a serious risk for service providers: identical lines are combined, losing the link back to the order, task and recorded time.
We therefore developed Merge Draft Invoices for our own Odoo 19 Community installation. Since we are not the only ones facing this problem, we are making it available free of charge on GitHub under the AGPL-3 free software licence.
View the project on GitHub:
Why build our own module rather than use the OCA solution?
The OCA has maintained account_invoice_merge in the OCA/account-invoicing repository for many years. At the time of writing, its migration to 19.0 is still an open pull request, rather than a released module.
More importantly, the two modules do different things:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Result | merged into one of the selected invoices | always a new invoice |
| Lines | identical lines are combined and quantities added together | each line is copied one for one |
| Source | invoice_origin / payment_reference concatenated in the header | Source fields copied for each line using Odoo’s own _copy_data_extend_business_fields hook |
| Timesheets | not taken into account | timesheet_invoice_id is reassigned to the new invoice |
| Clear for the customer | nothing | a heading above each source’s lines, showing the order and ticket |
| Source invoices | are cancelled | cancel (default), delete or keep – with the consequences clearly explained |
Combining identical lines is the crucial difference. A combined line can only retain one source. As soon as two lines from two different orders look the same, the link from the invoice back to the order, task and timesheet entry is lost. For a service business whose billing runs from ticket → timesheet → invoice, this means the same hours are billed twice or disappear.
Our module therefore never combines lines. The merged invoice has exactly as many lines as its sources put together.
No code has been taken from the OCA module. Both modules are licensed under AGPL-3.
What the module does
Select two or more drafts in the invoice list view, then choose Merge Draft Invoices from the Action menu. A wizard shows your selection, the checks and the options for the source invoices. Once you confirm, you are taken straight to the new invoice.
Before merging, the module checks all the following points at once, so you do not have to fix one error only to discover the next:
- at least two invoices selected
- all in Draft status
- none has ever been posted (a document reset to draft retains its posting history and is rejected)
- none has already been merged
- all are customer invoices, vendor bills, credit notes or receipts – no standalone 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 displays a warning and uses the value from the oldest selected invoice.
What is preserved
The module’s core principle is that every link between an invoice line and its source is preserved on the new invoice:
| Link | Handling |
|---|---|
| Sales order lines (sale_line_ids) | copied for each line using Odoo’s _copy_data_extend_business_fields |
| Purchase order lines (purchase_line_id) | as above |
| Analytic distribution (analytic_distribution) | copied for each line |
| other source fields on each line | automatically, if the module providing them implements the documented Odoo hook |
| Source and reference in the invoice header | combined, deduplicated and editable in the wizard |
| Sales order: invoice_ids, invoice_status, qty_invoiced | calculated from the lines – the order recognises the new invoice automatically |
| Timesheets (timesheet_invoice_id) | reassigned to the new invoice before the sources become invalid |
| Helpdesk ticket | linked to the timesheets, so it automatically points to the new invoice |
| Chatter | a message on the new invoice listing all sources, and a message on each source linking to the new invoice |
| merged_into_move_id / merged_from_move_ids | stored link in both directions |
Tax lines, payment term lines, rounding lines and cash discount lines are recalculated by Odoo from the invoice contents rather than copied. 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 does not use up a document number.
Headings: customers can see where each line comes from
This is the feature that makes a difference in everyday use. Above the lines from each source invoice, the module inserts a section line – the same element 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 automatically adds a subtotal for each section. This also lets the customer see how much each part cost.
The label is built from the information actually available on the source:
- the orders, taken directly from the line links to ensure the names are exact; if there are no order links, the free text from invoice_origin is used
- the tickets, identified through the timesheets – displayed as Ticket: HT00219 or Tickets: …, limited to five followed by “and N more”
- if neither is available: the source invoice’s number, then its reference
- if nothing is available: a neutral “Additional lines”, so the lines from one source never silently disappear into the section above
Missing information is omitted, not invented. The ticket is printed with its own number – the helpdesk_mgmt number, Enterprise ticket_ref or the model’s display name – never its database ID.
Headings are written in the customer’s language, just as Odoo does when generating invoices from orders. Chatter remains in the user’s language.
A heading has no product, quantity, price, tax, account or analytic distribution. It cannot affect totals, the tax base or cost accounting. Disabling headings in the wizard produces exactly the invoice that would be created without this feature – a test compares both results down to the penny.
What happens to the source invoices
The wizard offers three options:
- Cancel (default, recommended). Cancelling a draft has no accounting consequences: no document number, no posting. The invoices remain in the database as cancelled documents, complete with Chatter and a link to the new invoice. qty_invoiced on the sales order skips cancelled documents, so nothing is counted twice. This is the option that will still leave a clear trail six months later.
- 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.
- Keep as drafts. The sources remain open. Their sales orders then count both the source and the new invoice as invoiced – the work appears to have been billed twice until someone manually clears up the drafts. Intended for the rare case where you want to compare before deciding. A test explicitly verifies this double counting so that it takes nobody by surprise.
Limitations – what the module deliberately does not do
Please read before using in production:
- Drafts only. Posted invoices are not merged. That calls for credit notes, not a copying operation.
- Drafts that were previously posted and then reset are rejected. They have a number and a posting history; hiding that inside another document is not something a bulk action should do silently.
- No merging across partners, currencies, companies or journals. There is no sensible single value to use, so the module rejects the merge rather than guessing.
- payment_reference is not copied. Structured payment references identify a single document; joining two together produces a reference the bank cannot match to anything. The new invoice receives its own reference when posted.
- Down payment lines are copied like any other line. If the sources contain down payment lines from sale, check the result before posting.
- Only timesheet_invoice_id is reassigned automatically. Any other model with a Many2one to account.move (custom billing runs, subscription links, EDI transmissions) continues to point to the source. The _merge_draft_relink hook is provided for these cases.
- No combining of identical lines – by design. If you want a short, tidy invoice, this module is not for you: that would come at the cost of the source links.
- Headings are created once, during the merge. After that, they are ordinary section lines: they can be edited or deleted, and are not updated if an order or ticket is renamed later.
- A heading lists the tickets for the entire source invoice, rather than for the individual line.
Installation in Odoo 19 Community
Installation follows the usual process for an Odoo add-on:
- Copy the module directory to a configured addons path.
- Update the Apps list in Odoo.
- Install the account_invoice_merge_draft module.
The module depends only on account. Everything it needs to know about sale, purchase, project, hr_timesheet, sale_timesheet or a helpdesk module is determined at runtime – it also installs and works in a database used solely for invoicing.
Then go to Accounting → Customers → Invoices (or Vendors → Bills), select two or more drafts, and choose the cog menu → Merge Draft Invoices. The action is available to all users in the Invoicing / Billing group.
If you are upgrading from an earlier version of the module, run the update once with -u account_invoice_merge_draft --i18n-overwrite so that the updated field labels are applied to the German translation. Fresh installations are unaffected.
For developers: API and hooks
You can also trigger a merge 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; defaults to True
)
merge_draft_moves raises a UserError listing all reasons why the selected records cannot be merged. _merge_draft_get_errors() returns the same reasons as a list without raising an exception.
If you have custom 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 your reference is on the invoice line, you do not need this module at all: Odoo's own account.move.line._copy_data_extend_business_fields hook is sufficient, and the field is carried over when merging, reversing or duplicating invoices. For other Helpdesk modules or different heading wording, use _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 uses the active company's existing chart of accounts rather than AccountTestInvoicingCommon. That helper often fails to initialise on migrated production databases, precisely the kind of database this module is intended 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 accounting extension, we recommend testing the installation in a test environment with a copy of your own database first.
Free and open source for the Odoo Community
At Biricon IT Services GmbH, we initially develop solutions to meet specific needs in our own business. When this results in a module that others can use, we want to share it beyond a single installation.
That is why Merge Draft Invoices is available free of charge as an open-source project. The source code can be viewed, reviewed, adapted and developed further under the AGPL-3 licence.
Our aim was to produce an invoice where every line can still be traced back to its order, ticket and timesheet entry a year later, rather than simply making the invoice as short as possible.
Project, source code and contributions
The full source code, installation instructions and technical documentation are available on GitHub:
View Odoo 19 Merge Draft Invoices on GitHub
We welcome feedback from real-world use, bug reports, suggestions for improvements and contributions from the Odoo Community.
Free. Transparent. Open source. For Odoo 19 Community.