Den som fakturerar från order, helpdesk-ärenden och tidregistrering i Odoo känner till resultatet: En kund får vid månadens slut inte en faktura, utan fem. Var och en härrör från en annan order eller ett annat ärende, var och en är sakligt korrekt – och ändå frågar kunden om inte det kunde rymmas på ett dokument.
Just för detta finns det i Odoo 19 Community ingen ren lösning. Den kända OCA-modulen account_invoice_merge är ännu inte frisläppt för Odoo 19, och den löser problemet på ett sätt som utgör en allvarlig risk för tjänsteleverantörer: Lika rader slås ihop, och därmed går kopplingen tillbaka till ordern, uppgiften och den registrerade tiden förlorad.
För vår egen installation av Odoo 19 Community har vi därför utvecklat modulen Merge Draft Invoices. Eftersom det här problemet inte bara berör oss tillhandahåller vi den kostnadsfritt på GitHub under den fria AGPL-3-licensen.
Till projektet på GitHub:
Varför en egen modul i stället för OCA-lösningen?
OCA har i många år underhållit account_invoice_merge i repositoryt OCA/account-invoicing. Vid tidpunkten för det här inlägget är migreringen till 19.0 där fortfarande en öppen pull request, inte en publicerad modul.
Viktigare är dock att de två modulerna gör olika saker:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Resultat | slås ihop till en av de valda fakturorna | alltid en ny faktura |
| Rader | lika rader slås ihop, kvantiteter adderas | varje rad förs över ett till ett |
| Ursprung | invoice_origin / payment_reference sammanfogas i huvudet | ursprungsfält per rad kopieras via Odoos egen hook _copy_data_extend_business_fields |
| Tidregistrering | beaktas inte | timesheet_invoice_id flyttas över till den nya fakturan |
| Läsbart för kunden | ingenting | en rubrik ovanför raderna från varje källa med order och ärende |
| Källfakturor | annulleras | annullera (standard), radera eller behålla – med tydligt angivna följder |
Att lika rader slås ihop är den avgörande skillnaden. En sammanslagen rad kan bara ha ett ursprung. Så snart två rader från två olika order ser likadana ut är kopplingen från fakturan tillbaka till ordern, uppgiften och tidbokningen borta. I en tjänsteverksamhet där faktureringen går via ärende → tidregistrering → faktura innebär det: Samma timmar faktureras två gånger eller försvinner.
Vår modul slår därför i princip inte ihop något. Den sammanförda fakturan har exakt lika många rader som dess källor tillsammans.
Ingen kod har tagits över från OCA-modulen. Båda modulerna står under AGPL-3.
Vad modulen gör
Markera två eller flera utkast i fakturalistvyn, välj Slå ihop fakturautkast i åtgärdsmenyn – en guide visar urvalet, kontrollerna och alternativen för källfakturorna. Efter bekräftelsen hamnar man direkt på den nya fakturan.
Före sammanslagningen kontrollerar modulen, och det alla punkter på en gång, så att man inte ser nästa fel efter varje korrigering:
- minst två fakturor valda
- alla i status Utkast
- ingen har någonsin bokförts (ett dokument som återställts till utkast behåller sin bokföringshistorik och avvisas)
- ingen har redan slagits ihop
- alla är faktura, leverantörsfaktura, kreditnota eller kvitto – inga rena bokföringsposter
- samma partner, dokumenttyp, företag, valuta och journal
Skillnader i betalningsvillkor, skattemappning, leveransadress, bankkonto eller säljare blockerar inte. Guiden varnar och tar över värdet från den äldsta valda fakturan.
Vad som bevaras
Modulens kärna är att varje koppling som en fakturarad har till sitt ursprung också finns på den nya fakturan:
| Koppling | Hantering |
|---|---|
| Försäljningsorderrader (sale_line_ids) | kopieras per rad via Odoos _copy_data_extend_business_fields |
| Inköpsorderrader (purchase_line_id) | likaså |
| Kostnadsställefördelning (analytic_distribution) | kopieras per rad |
| andra ursprungsfält per rad | automatiskt, om den levererande modulen implementerar den dokumenterade Odoo-hooken |
| Ursprung och referens i fakturahuvudet | sammanförs, dedupliceras, kan redigeras i guiden |
| Försäljningsorder: invoice_ids, invoice_status, qty_invoiced | beräknas utifrån raderna – ordern känner av den nya fakturan av sig själv |
| Tidregistrering (timesheet_invoice_id) | flyttas över till den nya fakturan innan källorna blir ogiltiga |
| Helpdesk-ärende | hänger på tidregistreringen och pekar därmed automatiskt på den nya fakturan |
| Chatter | Meddelande på den nya fakturan med alla källor, meddelande på varje källa med länk till den nya |
| merged_into_move_id / merged_from_move_ids | lagrad koppling i båda riktningarna |
Skatterader, rader för betalningsvillkor, avrundnings- och kontantrabattrader kopieras inte, utan beräknas på nytt av Odoo utifrån innehållet. Den sammanförda fakturan har alltså en skatterad per skattesats, inte en per källa. Den skapas onumrerad som utkast; själva sammanslagningen förbrukar inget dokumentnummer.
Rubriker: Kunden ser varifrån varje rad härrör
Det är funktionen som gör skillnad i vardagen. Ovanför raderna från varje källfaktura sätter modulen en avsnittsrad – samma element som Odoo själv använder för avsnittet ”Förskottsbetalningar”:
BE10181 <- Rubrik
22,500 x Teknikertimme 3.577,50
Delsumma 3.577,50
BE10182 · Ärende: HT00219 <- Rubrik
3,000 x Teknikertimme 477,00
Delsumma 477,00
NETTOBELOPP 4.054,50
Rubriken skrivs ut på dokumentet som kunden får, och Odoos standardfakturarapport lägger av sig själv till en delsumma per avsnitt. Kunden ser därmed också vad varje del kostade.
Beteckningen sätts samman av det som källan faktiskt har:
- ordrarna, direkt från radkopplingarna – namnen är därmed exakta; saknas orderkopplingar används fritexten från invoice_origin
- ärendena, fastställda via tidregistreringen – utmatade som Ärende: HT00219 eller Ärenden: …, begränsade till fem plus ”och N till”
- finns ingetdera: källfakturans nummer, därefter dess referens
- finns ingenting alls: ett neutralt ”Övriga rader”, så att raderna från en källa aldrig i tysthet försvinner i blocket ovanför
Det som saknas utelämnas, inte påhittas. Ärendet skrivs ut med sitt eget nummer – helpdesk_mgmt-nummer, Enterprise-ticket_ref eller modellens visningsnamn – aldrig med databas-ID:t.
Rubrikerna skrivs på kundens språk, så som Odoo gör när fakturor skapas från order. Chattern förblir på användarens språk.
En rubrik har ingen produkt, ingen kvantitet, inget pris, ingen skatt, inget konto och ingen kostnadsställefördelning. Den kan inte påverka summor, skattebas och kostnadsredovisning. Den som stänger av rubrikerna i guiden får exakt den faktura som skulle uppstå utan den här funktionen – ett test jämför båda resultaten ner till öret.
Vad som händer med källfakturorna
Guiden erbjuder tre alternativ:
- Annullera (standard, rekommenderas). Att annullera ett utkast är utan följder: inget dokumentnummer, ingen bokföring. Fakturorna blir kvar i databasen som annullerade dokument med chatter och länk till den nya fakturan. qty_invoiced på försäljningsordern hoppar över annullerade dokument, ingenting räknas dubbelt. Det är alternativet som även om sex månader fortfarande är spårbart.
- Radera. Oåterkalleligt. Chatter och bakåtreferens försvinner med posten; endast meddelandet på den nya fakturan finns kvar. Lämpligt för engångsutkast som ingen längre ska se.
- Lämna som utkast. Källorna förblir öppna. Deras försäljningsorder räknar då både källan och den nya fakturan som fakturerade – arbetet ser ut att vara fakturerat dubbelt tills någon rensar utkasten för hand. Avsett för det sällsynta fallet att man vill jämföra före beslutet. Ett test fastslår just denna dubbelräkning så att den inte överraskar någon.
Begränsningar – vad modulen medvetet inte gör
Läs detta före produktionsanvändning:
- Endast utkast. Bokförda fakturor slås inte ihop. Det är en uppgift för kreditnotor, inte en kopieringsprocess.
- Utkast som en gång bokförts och återställts avvisas. De har ett nummer och en bokföringshistorik; att gömma det i ett annat dokument är inget som en samlingsåtgärd tyst bör göra.
- Ingen sammanslagning över partners, valutor, företag eller journaler. Det finns inget meningsfullt enskilt värde för detta, så modulen avvisar i stället för att gissa.
- payment_reference tas inte över. Strukturerade betalningsreferenser identifierar ett dokument; två hopfogade passar inte mot något hos banken. Den nya fakturan får sin egen vid bokföringen.
- Förskottsrader kopieras som vilken annan rad som helst. Om källorna innehåller förskottsrader från sale ska resultatet kontrolleras före bokföringen.
- Endast timesheet_invoice_id flyttas över automatiskt. Varje annan modell med en Many2one till account.move (egna faktureringskörningar, abonnemangskopplingar, EDI-överföringar) pekar fortfarande på källan. För detta finns hooken _merge_draft_relink.
- Ingen sammanslagning av lika rader – avsiktligt. Den som vill ha en kort, städad faktura har kommit fel och skulle betala för det med ursprungskopplingarna.
- Rubriker skapas en enda gång vid sammanslagningen. Därefter är de vanliga avsnittsrader: redigerbara, raderbara och inte uppdaterade om order eller ärende senare byter namn.
- En rubrik anger ärendena för hela källfakturan, inte för den enskilda raden.
Installation i Odoo 19 Community
Installationen sker som för en vanlig Odoo-tilläggsmodul:
- Kopiera modulkatalogen till en konfigurerad addons-sökväg.
- Uppdatera applistan i Odoo.
- Installera modulen account_invoice_merge_draft.
Modulen är bara beroende av account. Allt den vet om sale, purchase, project, hr_timesheet, sale_timesheet eller en helpdesk-modul fastställs vid körning – den installeras och fungerar även på en ren faktureringsdatabas.
Därefter: Bokföring → Kunder → Fakturor (eller Leverantörer → Fakturor), markera två eller flera utkast, kugghjulsmenyn → Slå ihop fakturautkast. Åtgärden är tillgänglig för alla användare med gruppen Fakturering.
Den som uppdaterar från en tidigare version av modulen bör en gång uppdatera med -u account_invoice_merge_draft --i18n-overwrite, så att ändrade fältbeteckningar kommer med i den tyska översättningen. En nyinstallation påverkas inte av detta.
För utvecklare: API och hooks
Sammanslagningen kan också anropas från egen kod:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" eller "keep"
add_source_sections=True, # rubriker; standard True
)
merge_draft_moves kastar ett UserError med alla avvisningsskäl om urvalet inte går att slå ihop. _merge_draft_get_errors() returnerar samma skäl som lista utan undantag.
Den som har egna bakåtreferenser till account.move åsidosätter _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}
)
Den som har en bakåtreferens på fakturaraden behöver inte alls den här modulen: Odoos egen hook account.move.line._copy_data_extend_business_fields räcker, och fältet tas över lika väl vid sammanslagning, annullering och duplicering. För andra helpdesk-moduler eller en annan formulering av rubrikerna finns _merge_draft_get_source_tickets() och _merge_draft_section_name().
Testat innan det körs på produktionsdata
48 tester täcker blandade statusar, olika partners, valutor och dokumenttyper, rader med och utan försäljnings- eller inköpsorder, skatter, avsnitt och anteckningar, kostnadsställefördelningar, överflyttningen av tidregistreringen, alla tre alternativen för källfakturorna, guiden och rubrikerna – att de är aktiva som standard, att avstängning av dem reproducerar det gamla resultatet ner till öret, att en rubrik inte har några belopp eller konton, att raderna från en källa hålls ihop, att ärendet visas med sitt nummer och aldrig med databas-ID:t.
Testdatan bygger på den befintliga kontoplanen för det aktiva företaget i stället för på AccountTestInvoicingCommon – den hjälparen startar ofta inte på migrerade produktionsdatabaser, och just på sådana ska modulen köra.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Som för varje tillägg inom bokföringsområdet rekommenderar vi att installationen först granskas i en testmiljö med en kopia av den egna databasen.
Kostnadsfritt och med öppen källkod för Odoo Community
Vi på Biricon IT Services GmbH utvecklar lösningar först för konkreta krav från vår egen verksamhet. Om det resulterar i en allmänt användbar modul vill vi inte gömma den i en enskild installation.
Merge Draft Invoices finns därför kostnadsfritt tillgänglig som projekt med öppen källkod. Källkoden kan granskas, kontrolleras, anpassas och vidareutvecklas inom ramen för AGPL-3-licensen.
Vårt mål var inte den kortast möjliga fakturan, utan en faktura där varje rad även om ett år fortfarande leder tillbaka till sin order, sitt ärende och sin tidbokning.
Projekt, källkod och medverkan
Hela källkoden, installationsanvisningarna och den tekniska dokumentationen finns på GitHub:
Öppna Odoo 19 Merge Draft Invoices på GitHub
Vi uppskattar praktiska erfarenheter, felrapporter, förbättringsförslag och bidrag från Odoo Community.
Kostnadsfritt. Spårbart. Öppen källkod. För Odoo 19 Community.