Cine facturează în Odoo pe baza comenzilor, tichetelor Helpdesk și pontajului cunoaște rezultatul: un client primește la sfârșitul lunii nu o factură, ci cinci. Fiecare provine dintr-o altă comandă sau dintr-un alt tichet, fiecare este corectă din punct de vedere tehnic – și totuși clientul întreabă dacă nu ar încăpea toate pe un singur document.
Tocmai pentru aceasta nu există în Odoo 19 Community o soluție curată. Binecunoscutul modul OCA account_invoice_merge nu este încă lansat pentru Odoo 19 și rezolvă problema într-un mod care reprezintă un risc serios pentru furnizorii de servicii: pozițiile identice sunt reunite și, odată cu aceasta, se pierde legătura înapoi către comandă, sarcină și timpul înregistrat.
Pentru propria noastră instalare Odoo 19 Community am dezvoltat de aceea modulul Merge Draft Invoices. Deoarece această problemă nu ne afectează doar pe noi, îl punem gratuit la dispoziție pe GitHub, sub licența liberă AGPL-3.
Către proiectul de pe GitHub:
De ce un modul propriu în loc de soluția OCA?
OCA întreține de mulți ani account_invoice_merge în repository-ul OCA/account-invoicing. La momentul acestui articol, migrarea la 19.0 este acolo încă un pull request deschis, nu un modul publicat.
Mai important este însă că cele două module fac lucruri diferite:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Rezultat | este îmbinată într-una dintre facturile selectate | întotdeauna o factură nouă |
| Poziții | pozițiile identice sunt reunite, cantitățile sunt adunate | fiecare poziție este preluată unu la unu |
| Origine | invoice_origin / payment_reference lipite împreună în antet | câmpurile de origine pe fiecare poziție, copiate prin propriul hook al Odoo _copy_data_extend_business_fields |
| Pontaj | neluat în considerare | timesheet_invoice_id este mutat pe noua factură |
| Lizibil pentru client | nimic | un titlu deasupra pozițiilor fiecărei surse, cu comandă și tichet |
| Facturi sursă | sunt anulate | anulare (implicit), ștergere sau păstrare – cu consecințe denumite clar |
Reunirea pozițiilor identice este diferența decisivă. O poziție reunită poate avea o singură origine. De îndată ce două rânduri din două comenzi diferite arată la fel, legătura de la factură înapoi la comandă, la sarcină și la înregistrarea de timp dispare. Într-o firmă de servicii, în care facturarea se desfășoară prin tichet → pontaj → factură, aceasta înseamnă: aceleași ore sunt facturate de două ori sau dispar.
Modulul nostru nu reunește, așadar, în principiu nimic. Factura îmbinată are exact tot atâtea poziții câte au sursele ei la un loc.
Nu a fost preluat niciun cod din modulul OCA. Ambele module sunt sub AGPL-3.
Ce face modulul
În vizualizarea de listă a facturilor se marchează două sau mai multe ciorne, în meniul de acțiuni se alege Îmbinarea facturilor-ciornă – un asistent arată selecția, verificările și opțiunile pentru facturile sursă. După confirmare ajungeți direct la noua factură.
Înainte de îmbinare, modulul verifică, și anume toate punctele deodată, pentru ca după fiecare corectură să nu apară următoarea eroare:
- cel puțin două facturi selectate
- toate în starea Ciornă
- niciuna nu a fost vreodată înregistrată (un document revenit la ciornă își păstrează istoricul de înregistrare și este respins)
- niciuna nu a fost deja îmbinată
- toate sunt factură, factură de furnizor, notă de credit sau bon – nu simple note contabile
- același partener, tip de document, companie, monedă și jurnal
Diferențele la termenul de plată, încadrarea fiscală, adresa de livrare, contul bancar sau vânzător nu blochează. Asistentul avertizează și preia valoarea celei mai vechi facturi selectate.
Ce se păstrează
Esența modulului este că orice legătură pe care o poziție de factură o are cu originea ei există și pe noua factură:
| Legătură | Tratare |
|---|---|
| Poziții de comandă de vânzare (sale_line_ids) | copiate pe fiecare poziție prin _copy_data_extend_business_fields al Odoo |
| Poziții de comandă de achiziție (purchase_line_id) | la fel |
| Distribuția pe centre de cost (analytic_distribution) | copiată pe fiecare poziție |
| alte câmpuri de origine pe fiecare poziție | automat, dacă modulul furnizor implementează hook-ul documentat al Odoo |
| Origine și referință în antetul facturii | reunite, deduplicate, editabile în asistent |
| Comandă de vânzare: invoice_ids, invoice_status, qty_invoiced | sunt calculate din poziții – comanda recunoaște singură noua factură |
| Pontaj (timesheet_invoice_id) | este mutat pe noua factură înainte ca sursele să devină invalide |
| Tichet Helpdesk | este legat de pontaj și indică astfel automat noua factură |
| Chatter | mesaj pe noua factură cu toate sursele, mesaj pe fiecare sursă cu link către cea nouă |
| merged_into_move_id / merged_from_move_ids | legătură salvată în ambele sensuri |
Rândurile de taxe, rândurile termenelor de plată, rândurile de rotunjire și de scont nu sunt copiate, ci recalculate de Odoo din conținut. Factura îmbinată are, așadar, un rând de taxă pentru fiecare cotă de taxă, nu unul pentru fiecare sursă. Ea este creată ca ciornă, fără număr; îmbinarea în sine nu consumă niciun număr de document.
Titluri: clientul vede de unde provine fiecare poziție
Aceasta este funcția care face diferența în practica zilnică. Deasupra pozițiilor fiecărei facturi sursă, modulul inserează un rând de secțiune – același element pe care Odoo însuși îl folosește pentru secțiunea „Plăți în avans”:
BE10181 <- titlu
22,500 x Technikerstunde 3.577,50
Zwischensumme 3.577,50
BE10182 · Ticket: HT00219 <- titlu
3,000 x Technikerstunde 477,00
Zwischensumme 477,00
NETTOBETRAG 4.054,50
Titlul este tipărit pe documentul pe care îl primește clientul, iar raportul standard de factură al Odoo adaugă din proprie inițiativă un subtotal pentru fiecare secțiune. Astfel, clientul vede și cât a costat fiecare parte.
Eticheta este alcătuită din ceea ce are efectiv sursa:
- comenzile, direct din legăturile pozițiilor – numele sunt astfel exacte; dacă lipsesc legăturile cu comenzile, se folosește textul liber din invoice_origin
- tichetele, determinate prin pontaj – afișate ca Ticket: HT00219 sau Tichete: …, limitate la cinci plus „și încă N”
- dacă nu există niciuna dintre ele: numărul facturii sursă, apoi referința ei
- dacă nu există absolut nimic: un neutru „Alte poziții”, pentru ca rândurile unei surse să nu dispară niciodată în tăcere în blocul de deasupra
Ce lipsește este omis, nu inventat. Tichetul este tipărit cu propriul număr – numărul helpdesk_mgmt, ticket_ref din Enterprise sau numele afișat al modelului – niciodată cu ID-ul din baza de date.
Titlurile sunt scrise în limba clientului, așa cum face Odoo la generarea facturilor din comenzi. Chatter-ul rămâne în limba utilizatorului.
Un titlu nu are produs, cantitate, preț, taxă, cont sau distribuție pe centre de cost. El nu poate influența sumele, baza de impozitare și contabilitatea de costuri. Cine dezactivează titlurile în asistent obține exact factura care ar rezulta fără această funcție – un test compară ambele rezultate până la ultimul cent.
Ce se întâmplă cu facturile sursă
Asistentul oferă trei opțiuni:
- Anulare (implicit, recomandat). Anularea unei ciorne nu are consecințe: niciun număr de document, nicio înregistrare. Facturile rămân în baza de date ca documente anulate, cu chatter și link către noua factură. qty_invoiced de la comanda de vânzare omite documentele anulate, nimic nu este numărat de două ori. Aceasta este opțiunea care rămâne ușor de urmărit și peste șase luni.
- Ștergere. Irevocabil. Chatter-ul și trimiterea înapoi dispar odată cu înregistrarea; rămâne doar mesajul de pe noua factură. Util pentru ciorne de unică folosință pe care nu mai trebuie să le vadă nimeni.
- Păstrare ca ciornă. Sursele rămân deschise. Comenzile lor de vânzare numără atunci sursa și noua factură ca facturate – munca pare facturată de două ori până când cineva curăță manual ciornele. Gândit pentru cazul rar în care se dorește compararea înainte de decizie. Un test consemnează exact această dublă numărare, pentru ca ea să nu surprindă pe nimeni.
Limite – ce nu face modulul în mod deliberat
Vă rugăm să citiți înainte de utilizarea productivă:
- Doar ciorne. Facturile înregistrate nu sunt îmbinate. Aceasta este o sarcină de note de credit, nu o operațiune de copiere.
- Ciornele înregistrate cândva și apoi revenite sunt respinse. Ele au un număr și un istoric de înregistrare; ascunderea acestora într-un alt document nu este ceva ce o acțiune colectivă ar trebui să facă în tăcere.
- Nicio îmbinare între parteneri, monede, companii sau jurnale diferite. Pentru aceasta nu există o valoare unică rezonabilă, așa că modulul refuză în loc să ghicească.
- payment_reference nu este preluat. Referințele de plată structurate identifică un document; două lipite împreună nu se potrivesc cu nimic la bancă. Noua factură primește propria referință la înregistrare.
- Pozițiile de avans sunt copiate ca orice altă poziție. Dacă sursele conțin rânduri de avans din sale, rezultatul trebuie verificat înainte de înregistrare.
- Doar timesheet_invoice_id este mutat automat. Orice alt model cu un Many2one către account.move (cicluri proprii de facturare, legături de abonament, transmiteri EDI) indică în continuare sursa. Pentru aceasta există hook-ul _merge_draft_relink.
- Nicio reunire a pozițiilor identice – intenționat. Cine vrea o factură scurtă și ordonată este aici în locul greșit și ar plăti pentru aceasta cu legăturile de origine.
- Titlurile se creează o singură dată, la îmbinare. După aceea sunt rânduri de secțiune obișnuite: editabile, ștergibile și neactualizate dacă comanda sau tichetul sunt redenumite ulterior.
- Un titlu menționează tichetele întregii facturi sursă, nu ale unei singure poziții.
Instalarea în Odoo 19 Community
Instalarea se face ca în cazul unui modul suplimentar Odoo obișnuit:
- Copiați directorul modulului într-o cale de addon-uri configurată.
- Actualizați lista de aplicații din Odoo.
- Instalați modulul account_invoice_merge_draft.
Modulul depinde doar de account. Tot ceea ce știe despre sale, purchase, project, hr_timesheet, sale_timesheet sau un modul Helpdesk este determinat la rulare – se instalează și funcționează și pe o bază de date de facturare pură.
După aceea: Contabilitate → Clienți → Facturi (sau Furnizori → Facturi), marcați două sau mai multe ciorne, meniul cu roata dințată → Îmbinarea facturilor-ciornă. Acțiunea este disponibilă tuturor utilizatorilor din grupul Facturare.
Cine face actualizarea de la o versiune anterioară a modulului ar trebui să facă o singură dată actualizarea cu -u account_invoice_merge_draft --i18n-overwrite, pentru ca denumirile de câmp modificate să ajungă în traducerea germană. O instalare nouă nu este afectată de aceasta.
Pentru dezvoltatori: API și hook-uri
Îmbinarea poate fi apelată și din cod propriu:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" sau "keep"
add_source_sections=True, # titluri; implicit True
)
merge_draft_moves aruncă un UserError cu toate motivele de respingere dacă selecția nu poate fi îmbinată. _merge_draft_get_errors() returnează aceleași motive ca listă, fără excepție.
Cine are trimiteri înapoi proprii către account.move suprascrie _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}
)
Cine are o trimitere înapoi pe poziția facturii nu are deloc nevoie de acest modul: hook-ul propriu al Odoo account.move.line._copy_data_extend_business_fields este suficient, iar câmpul este preluat în mod egal la îmbinare, la anulare și la duplicare. Pentru alte module Helpdesk sau o altă formulare a titlurilor există _merge_draft_get_source_tickets() și _merge_draft_section_name().
Testat înainte de a rula pe date productive
48 de teste acoperă stări mixte, parteneri, monede și tipuri de documente diferite, poziții cu și fără comandă de vânzare sau de achiziție, taxe, secțiuni și notițe, distribuții pe centre de cost, mutarea pontajului, toate cele trei opțiuni pentru facturile sursă, asistentul și titlurile – că sunt active implicit, că dezactivarea lor reproduce vechiul rezultat până la ultimul cent, că un titlu nu poartă sume sau conturi, că pozițiile unei surse rămân împreună, că tichetul apare cu numărul său și niciodată cu ID-ul din baza de date.
Datele de test se bazează pe planul de conturi existent al companiei active, nu pe AccountTestInvoicingCommon – acest helper nu pornește adesea pe bazele de date productive migrate, iar modulul trebuie să ruleze tocmai pe astfel de baze.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Ca la orice extensie din domeniul contabilității, recomandăm ca instalarea să fie verificată mai întâi într-un mediu de test, cu o copie a propriei baze de date.
Gratuit și open source pentru comunitatea Odoo
La Biricon IT Services GmbH dezvoltăm soluții mai întâi pentru cerințe concrete din propria noastră activitate. Dacă din aceasta rezultă un modul utilizabil în general, nu dorim să îl ascundem într-o singură instalare.
Merge Draft Invoices este de aceea disponibil gratuit ca proiect open source. Codul sursă poate fi consultat, verificat, adaptat și dezvoltat mai departe în cadrul licenței AGPL-3.
Scopul nostru nu a fost cea mai scurtă factură posibilă, ci o factură în care, și peste un an, fiecare poziție duce înapoi la comanda ei, la tichetul ei și la înregistrarea ei de timp.
Proiect, cod sursă și colaborare
Codul sursă complet, indicațiile de instalare și documentația tehnică sunt disponibile pe GitHub:
Deschideți Odoo 19 Merge Draft Invoices pe GitHub
Ne bucurăm de experiențe practice, rapoarte de erori, propuneri de îmbunătățire și contribuții din partea comunității Odoo.
Gratuit. Ușor de urmărit. Open source. Pentru Odoo 19 Community.