Kto w Odoo rozlicza na podstawie zamówień, zgłoszeń helpdesku i ewidencji czasu pracy, zna ten efekt: klient na koniec miesiąca dostaje nie jedną fakturę, lecz pięć. Każda pochodzi z innego zamówienia lub zgłoszenia, każda jest merytorycznie poprawna – a mimo to klient pyta, czy nie zmieściłoby się to w jednym dokumencie.
Właśnie do tego w Odoo 19 Community nie ma czystego rozwiązania. Znany moduł OCA account_invoice_merge nie jest jeszcze udostępniony dla Odoo 19 i rozwiązuje problem w sposób, który dla usługodawców stanowi poważne ryzyko: jednakowe pozycje są łączone, przez co zostaje utracone powiązanie z zamówieniem, zadaniem i zarejestrowanym czasem.
Dlatego dla własnej instalacji Odoo 19 Community opracowaliśmy moduł Merge Draft Invoices. Ponieważ ten problem dotyczy nie tylko nas, udostępniamy go bezpłatnie na GitHubie na wolnej licencji AGPL-3.
Przejdź do projektu na GitHubie:
Dlaczego własny moduł zamiast rozwiązania OCA?
OCA od wielu lat utrzymuje account_invoice_merge w repozytorium OCA/account-invoicing. W momencie publikacji tego wpisu migracja do wersji 19.0 jest tam jeszcze otwartym Pull Requestem, a nie opublikowanym modułem.
Ważniejsze jest jednak to, że oba moduły robią różne rzeczy:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Wynik | jest łączona z jedną z wybranych faktur | zawsze nowa faktura |
| Pozycje | jednakowe pozycje są łączone, ilości sumowane | każda pozycja jest przejmowana jeden do jednego |
| Pochodzenie | invoice_origin / payment_reference zestawione razem w nagłówku | pola pochodzenia kopiowane dla każdej pozycji przez własny hook Odoo _copy_data_extend_business_fields |
| Ewidencja czasu pracy | nieuwzględniana | timesheet_invoice_id jest przepinany na nową fakturę |
| Czytelność dla klienta | nic | nagłówek nad pozycjami każdego źródła z zamówieniem i zgłoszeniem |
| Faktury źródłowe | są anulowane | anulować (domyślnie), usunąć lub zachować – z jasno nazwanymi skutkami |
Łączenie jednakowych pozycji to decydująca różnica. Połączona pozycja może mieć tylko jedno pochodzenie. Gdy tylko dwa wiersze z dwóch różnych zamówień wyglądają tak samo, powiązanie z faktury z powrotem do zamówienia, zadania i księgowania czasu przepada. W firmie usługowej, w której rozliczanie przebiega według schematu zgłoszenie → ewidencja czasu → faktura, oznacza to: te same godziny zostaną rozliczone podwójnie lub znikną.
Nasz moduł zasadniczo niczego więc nie łączy. Połączona faktura ma dokładnie tyle pozycji, ile mają razem jej źródła.
Nie przejęto żadnego kodu z modułu OCA. Oba moduły podlegają licencji AGPL-3.
Co robi moduł
Na liście faktur zaznaczyć dwie lub więcej wersji roboczych, w menu akcji wybrać Połącz robocze faktury – kreator pokazuje wybór, kontrole i opcje dotyczące faktur źródłowych. Po potwierdzeniu trafia się bezpośrednio do nowej faktury.
Przed połączeniem moduł sprawdza, i to wszystkie punkty naraz, aby po każdej poprawce nie pojawiał się kolejny błąd:
- wybrano co najmniej dwie faktury
- wszystkie w stanie Wersja robocza
- żadna nie była nigdy zaksięgowana (dokument cofnięty do wersji roboczej zachowuje historię księgowań i zostaje odrzucony)
- żadna nie została już wcześniej połączona
- wszystkie są fakturą, fakturą zakupu, notą kredytową lub dokumentem – żadnymi czystymi zapisami księgowymi
- ten sam partner, typ dokumentu, firma, waluta i dziennik
Różnice w warunkach płatności, przyporządkowaniu podatków, adresie dostawy, rachunku bankowym lub sprzedawcy nie blokują. Kreator ostrzega i przejmuje wartość z najstarszej wybranej faktury.
Co zostaje zachowane
Rdzeniem modułu jest to, że każde powiązanie, jakie pozycja faktury ma ze swoim pochodzeniem, istnieje także w nowej fakturze:
| Powiązanie | Obsługa |
|---|---|
| Pozycje zamówienia sprzedaży (sale_line_ids) | kopiowane dla każdej pozycji przez _copy_data_extend_business_fields Odoo |
| Pozycje zamówienia zakupu (purchase_line_id) | tak samo |
| Rozdział na miejsca powstawania kosztów (analytic_distribution) | kopiowany dla każdej pozycji |
| inne pola pochodzenia dla każdej pozycji | automatycznie, jeśli moduł dostarczający implementuje udokumentowany hook Odoo |
| Pochodzenie i referencja w nagłówku faktury | połączone, pozbawione duplikatów, edytowalne w kreatorze |
| Zamówienie sprzedaży: invoice_ids, invoice_status, qty_invoiced | są obliczane z pozycji – zamówienie samo rozpoznaje nową fakturę |
| Ewidencja czasu pracy (timesheet_invoice_id) | jest przepinana na nową fakturę, zanim źródła staną się nieważne |
| Zgłoszenie helpdesku | jest powiązane z ewidencją czasu pracy i w ten sposób automatycznie wskazuje na nową fakturę |
| Chatter | Wiadomość w nowej fakturze ze wszystkimi źródłami, wiadomość w każdym źródle z linkiem do nowej |
| merged_into_move_id / merged_from_move_ids | zapisane powiązanie w obu kierunkach |
Wiersze podatków, warunków płatności, zaokrągleń i skonta nie są kopiowane, lecz obliczane przez Odoo na nowo na podstawie zawartości. Połączona faktura ma więc jeden wiersz podatku na stawkę podatkową, a nie jeden na źródło. Jest tworzona bez numeru jako wersja robocza; samo łączenie nie zużywa numeru dokumentu.
Nagłówki: klient widzi, skąd pochodzi każda pozycja
To funkcja, która w codziennej pracy robi różnicę. Nad pozycjami każdej faktury źródłowej moduł umieszcza wiersz sekcji – ten sam element, którego Odoo samo używa dla sekcji „Zaliczki”:
BE10181 <- nagłówek
22,500 x Godzina technika 3.577,50
Suma częściowa 3.577,50
BE10182 · Zgłoszenie: HT00219 <- nagłówek
3,000 x Godzina technika 477,00
Suma częściowa 477,00
KWOTA NETTO 4.054,50
Nagłówek jest drukowany na dokumencie, który otrzymuje klient, a standardowy raport faktury Odoo sam dodaje sumę częściową dla każdej sekcji. Klient widzi dzięki temu również, ile kosztowała każda część.
Opis jest składany z tego, co źródło faktycznie ma:
- zamówienia, bezpośrednio z powiązań pozycji – nazwy są więc dokładne; jeśli brakuje powiązań z zamówieniami, używany jest tekst dowolny z invoice_origin
- zgłoszenia, ustalane na podstawie ewidencji czasu pracy – wyświetlane jako Zgłoszenie: HT00219 lub Zgłoszenia: …, ograniczone do pięciu plus „i N kolejnych”
- jeśli nie ma ani jednego, ani drugiego: numer faktury źródłowej, następnie jej referencja
- jeśli nie ma nic: neutralne „Dalsze pozycje”, aby wiersze jednego źródła nigdy nie znikały po cichu w bloku powyżej
To, czego brakuje, jest pomijane, a nie wymyślane. Zgłoszenie jest drukowane z własnym numerem – numerem helpdesk_mgmt, ticket_ref wersji Enterprise lub nazwą wyświetlaną modelu – nigdy z identyfikatorem bazy danych (ID).
Nagłówki są pisane w języku klienta, tak jak robi to Odoo przy tworzeniu faktur z zamówień. Chatter pozostaje w języku użytkownika.
Nagłówek nie zawiera produktu, ilości, ceny, podatku, konta ani rozdziału na miejsca powstawania kosztów. Nie może wpływać na sumy, podstawę opodatkowania ani rachunek kosztów. Kto wyłączy nagłówki w kreatorze, otrzyma dokładnie taką fakturę, jaka powstałaby bez tej funkcji – test porównuje oba wyniki co do centa.
Co dzieje się z fakturami źródłowymi
Kreator oferuje trzy opcje:
- Anulować (domyślnie, zalecane). Anulowanie wersji roboczej jest bez konsekwencji: bez numeru dokumentu, bez księgowania. Faktury pozostają w bazie danych jako anulowane dokumenty z chatterem i linkiem do nowej faktury. qty_invoiced w zamówieniu sprzedaży pomija anulowane dokumenty, nic nie jest liczone podwójnie. To opcja, która pozostaje zrozumiała także po sześciu miesiącach.
- Usunąć. Nieodwołalnie. Chatter i odsyłacz zanikają razem z rekordem; zostaje tylko wiadomość w nowej fakturze. Sensowne dla jednorazowych wersji roboczych, których nikt nie ma już widzieć.
- Pozostawić jako wersję roboczą. Źródła pozostają otwarte. Ich zamówienia sprzedaży liczą wtedy źródło i nową fakturę jako zafakturowane – praca wygląda na rozliczoną podwójnie, dopóki ktoś nie uporządkuje wersji roboczych ręcznie. Przewidziane na rzadki przypadek, gdy przed decyzją chce się porównać. Test utrwala właśnie to podwójne liczenie, aby nikogo nie zaskoczyło.
Granice – czego moduł celowo nie robi
Prosimy przeczytać przed użyciem produkcyjnym:
- Tylko wersje robocze. Zaksięgowane faktury nie są łączone. To zadanie dla not kredytowych, a nie proces kopiowania.
- Wersje robocze zaksięgowane kiedyś i cofnięte są odrzucane. Mają numer i historię księgowań; ukrywanie tego w innym dokumencie nie jest niczym, co akcja zbiorcza powinna załatwiać po cichu.
- Brak łączenia między partnerami, walutami, firmami lub dziennikami. Nie ma dla tego sensownej pojedynczej wartości, więc moduł odmawia, zamiast zgadywać.
- payment_reference nie jest przejmowane. Ustrukturyzowane referencje płatności identyfikują dokument; dwie zestawione razem nie pasują w banku do niczego. Nowa faktura otrzymuje własną podczas księgowania.
- Pozycje zaliczek są kopiowane jak każda inna pozycja. Jeśli źródła zawierają wiersze zaliczek z modułu sale, wynik należy sprawdzić przed zaksięgowaniem.
- Automatycznie przepinane jest tylko timesheet_invoice_id. Każdy inny model z polem Many2one na account.move (własne cykle rozliczeniowe, powiązania z subskrypcjami, przekazania EDI) nadal wskazuje na źródło. Służy do tego hook _merge_draft_relink.
- Brak łączenia jednakowych pozycji – celowo. Kto chce krótkiej, uporządkowanej faktury, jest tu w złym miejscu i zapłaciłby za to powiązaniami z pochodzeniem.
- Nagłówki powstają jednorazowo przy łączeniu. Potem są zwykłymi wierszami sekcji: można je edytować, usuwać i nie są aktualizowane, gdy zamówienie lub zgłoszenie zostanie później przemianowane.
- Nagłówek wymienia zgłoszenia całej faktury źródłowej, a nie pojedynczej pozycji.
Instalacja w Odoo 19 Community
Instalacja przebiega jak w przypadku zwykłego dodatkowego modułu Odoo:
- Skopiować katalog modułu do skonfigurowanej ścieżki addons.
- Zaktualizować listę aplikacji w Odoo.
- Zainstalować moduł account_invoice_merge_draft.
Moduł zależy tylko od account. Wszystko, co wie o sale, purchase, project, hr_timesheet, sale_timesheet lub module helpdesku, jest ustalane w czasie działania – instaluje się i działa także na czystej bazie rozliczeniowej.
Następnie: Księgowość → Klienci → Faktury (lub Dostawcy → Faktury), zaznaczyć dwie lub więcej wersji roboczych, menu z kołem zębatym → Połącz robocze faktury. Akcja jest dostępna dla wszystkich użytkowników z grupą Rozliczenia / Fakturowanie.
Kto aktualizuje z wcześniejszej wersji modułu, powinien jednorazowo wykonać aktualizację z -u account_invoice_merge_draft --i18n-overwrite, aby zmienione nazwy pól trafiły do polskiego tłumaczenia. Nowej instalacji to nie dotyczy.
Dla programistów: API i hooki
Łączenie można wywołać także z własnego kodu:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" lub "keep"
add_source_sections=True, # nagłówki; domyślnie True
)
merge_draft_moves zgłasza UserError ze wszystkimi powodami odmowy, jeśli wybranych faktur nie można połączyć. _merge_draft_get_errors() zwraca te same powody jako listę bez wyjątku.
Kto ma własne odsyłacze zwrotne do account.move, nadpisuje _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}
)
Kto ma odsyłacz zwrotny w pozycji faktury, w ogóle nie potrzebuje tego modułu: wystarczy własny hook Odoo account.move.line._copy_data_extend_business_fields, a pole jest przejmowane jednakowo przy łączeniu, anulowaniu i duplikowaniu. Dla innych modułów helpdesku lub innego sformułowania nagłówków istnieją _merge_draft_get_source_tickets() i _merge_draft_section_name().
Przetestowane, zanim trafi na dane produkcyjne
48 testów obejmuje mieszane stany, różnych partnerów, waluty i typy dokumentów, pozycje z zamówieniem sprzedaży lub zakupu i bez nich, podatki, sekcje i notatki, rozdziały na miejsca powstawania kosztów, przepinanie ewidencji czasu pracy, wszystkie trzy opcje dla faktur źródłowych, kreator i nagłówki – to, że są domyślnie aktywne, że ich wyłączenie odtwarza dawny wynik co do centa, że nagłówek nie zawiera kwot ani kont, że pozycje jednego źródła pozostają razem, że zgłoszenie pojawia się ze swoim numerem, a nigdy z identyfikatorem bazy danych.
Dane testowe opierają się na istniejącym planie kont aktywnej firmy zamiast na AccountTestInvoicingCommon – ten helper często nie uruchamia się na zmigrowanych produkcyjnych bazach danych, a właśnie na takich moduł ma działać.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Jak przy każdym rozszerzeniu w obszarze księgowości zalecamy, aby instalację najpierw sprawdzić w środowisku testowym na kopii własnej bazy danych.
Bezpłatnie i open source dla społeczności Odoo
My w Biricon IT Services GmbH opracowujemy rozwiązania najpierw z myślą o konkretnych wymaganiach z naszej własnej działalności. Gdy powstaje z tego moduł nadający się do ogólnego użytku, nie chcemy ukrywać go w pojedynczej instalacji.
Merge Draft Invoices jest więc dostępny bezpłatnie jako projekt open source. Kod źródłowy można przeglądać, sprawdzać, dostosowywać i rozwijać w ramach licencji AGPL-3.
Naszym celem nie była możliwie najkrótsza faktura, lecz faktura, w której także za rok każda pozycja prowadzi z powrotem do swojego zamówienia, zgłoszenia i księgowania czasu.
Projekt, kod źródłowy i współpraca
Pełny kod źródłowy, wskazówki dotyczące instalacji i dokumentacja techniczna są dostępne na GitHubie:
Otwórz Odoo 19 Merge Draft Invoices na GitHubie
Cieszymy się z doświadczeń praktycznych, zgłoszeń błędów, propozycji ulepszeń i wkładu społeczności Odoo.
Bezpłatnie. Możliwe do prześledzenia. Open source. Dla Odoo 19 Community.