Kas Odoo atsiskaito pagal užsakymus, Helpdesk bilietus ir laiko apskaitą, žino rezultatą: klientas mėnesio pabaigoje gauna ne vieną sąskaitą faktūrą, o penkias. Kiekviena kilusi iš kito užsakymo ar bilieto, kiekviena turinio požiūriu teisinga – ir vis dėlto klientas klausia, ar viskas negalėtų tilpti į vieną dokumentą.
Būtent tam Odoo 19 Community nėra tvarkingo sprendimo. Žinomas OCA modulis account_invoice_merge Odoo 19 dar neišleistas, o problemą jis sprendžia būdu, kuris paslaugų teikėjams kelia rimtą riziką: vienodos pozicijos sujungiamos ir taip prarandamas ryšys su užsakymu, užduotimi ir užfiksuotu laiku.
Todėl savo Odoo 19 Community instaliacijai sukūrėme modulį Merge Draft Invoices. Kadangi ši problema aktuali ne tik mums, jį nemokamai teikiame GitHub pagal laisvąją AGPL-3 licenciją.
Į projektą GitHub:
Kodėl atskiras modulis, o ne OCA sprendimas?
OCA daugelį metų prižiūri account_invoice_merge saugykloje OCA/account-invoicing. Šio įrašo rašymo metu migracija į 19.0 ten vis dar yra atviras Pull Request, o ne išleistas modulis.
Svarbiau, kad abu moduliai atlieka skirtingus dalykus:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Rezultatas | sujungiama į vieną iš pasirinktų sąskaitų faktūrų | visada nauja sąskaita faktūra |
| Pozicijos | vienodos pozicijos sujungiamos, kiekiai sumuojami | kiekviena pozicija perkeliama vienas prie vieno |
| Kilmė | invoice_origin / payment_reference antraštėje sujungiami į vieną | kilmės laukai kiekvienai pozicijai nukopijuojami per paties Odoo _copy_data_extend_business_fields hook |
| Laiko apskaita | neįtraukiama | timesheet_invoice_id perkeliamas į naują sąskaitą faktūrą |
| Skaitoma klientui | nieko | antraštė virš kiekvieno šaltinio pozicijų su užsakymu ir bilietu |
| Šaltinių sąskaitos faktūros | anuliuojamos | anuliuoti (numatyta), ištrinti arba palikti – su aiškiai įvardytomis pasekmėmis |
Vienodų pozicijų sujungimas – lemiamas skirtumas. Sujungta pozicija gali turėti tik vieną kilmę. Kai tik dvi eilutės iš dviejų skirtingų užsakymų atrodo vienodai, ryšys nuo sąskaitos faktūros atgal į užsakymą, užduotį ir laiko įrašą dingsta. Paslaugų įmonėje, kurioje atsiskaitymas vyksta pagal grandinę Bilietas → Laiko apskaita → Sąskaita faktūra, tai reiškia: tos pačios valandos arba sąskaitoje išrašomos dukart, arba dingsta.
Todėl mūsų modulis iš esmės nieko nesujungia. Sujungta sąskaita faktūra turi lygiai tiek pozicijų, kiek jų turi visi šaltiniai kartu.
Iš OCA modulio kodas nebuvo perimtas. Abu moduliai licencijuoti pagal AGPL-3.
Ką modulis daro
Sąskaitų faktūrų sąrašo vaizde pažymėkite du ar daugiau juodraščių, veiksmų meniu pasirinkite Sujungti juodraštines sąskaitas faktūras – asistentas parodo pasirinkimą, patikras ir šaltinių sąskaitų parinktis. Patvirtinus patenkama tiesiai į naują sąskaitą faktūrą.
Prieš sujungdamas modulis patikrina, ir visus punktus iš karto, kad po kiekvieno pataisymo nereikėtų matyti kitos klaidos:
- pasirinktos mažiausiai dvi sąskaitos faktūros
- visos būsenos Juodraštis
- nė viena niekada nebuvo užregistruota (į juodraštį grąžintas dokumentas išsaugo registravimo istoriją ir yra atmetamas)
- nė viena jau nebuvo sujungta
- visos yra sąskaita faktūra, tiekėjo sąskaita faktūra, kreditinė sąskaita arba kvitas – ne vien žurnalo įrašai
- tas pats partneris, dokumento tipas, įmonė, valiuta ir žurnalas
Skirtumai apmokėjimo sąlygose, mokesčių priskyrime, pristatymo adrese, banko sąskaitoje ar pardavėjuje neblokuoja. Asistentas įspėja ir perima seniausios pasirinktos sąskaitos faktūros reikšmę.
Kas išlieka
Modulio esmė ta, kad kiekvienas ryšys, kurį sąskaitos faktūros pozicija turi su savo kilme, egzistuoja ir naujoje sąskaitoje faktūroje:
| Ryšys | Tvarkymas |
|---|---|
| Pardavimo užsakymo pozicijos (sale_line_ids) | kiekvienai pozicijai nukopijuojama per Odoo _copy_data_extend_business_fields |
| Pirkimo užsakymo pozicijos (purchase_line_id) | taip pat |
| Sąnaudų centrų paskirstymas (analytic_distribution) | nukopijuojama kiekvienai pozicijai |
| kiti kilmės laukai pagal poziciją | automatiškai, jei teikiantis modulis įgyvendina dokumentuotą Odoo hook |
| Kilmė ir nuoroda sąskaitos faktūros antraštėje | sujungiama, pašalinami dublikatai, redaguojama asistente |
| Pardavimo užsakymas: invoice_ids, invoice_status, qty_invoiced | apskaičiuojami iš pozicijų – užsakymas naują sąskaitą faktūrą atpažįsta pats |
| Laiko apskaita (timesheet_invoice_id) | perkeliama į naują sąskaitą faktūrą, kol šaltiniai dar neprarado galiojimo |
| Helpdesk bilietas | susijęs su laiko apskaita, todėl automatiškai nurodo naują sąskaitą faktūrą |
| Chatter | pranešimas naujoje sąskaitoje faktūroje su visais šaltiniais, pranešimas kiekviename šaltinyje su nuoroda į naują |
| merged_into_move_id / merged_from_move_ids | išsaugotas ryšys abiem kryptimis |
Mokesčių eilutės, apmokėjimo sąlygų eilutės, apvalinimo ir nuolaidos už greitą apmokėjimą eilutės nekopijuojamos, o Odoo iš naujo apskaičiuoja iš turinio. Taigi sujungtoje sąskaitoje faktūroje yra viena mokesčių eilutė pagal kiekvieną mokesčio tarifą, o ne viena kiekvienam šaltiniui. Ji sukuriama be numerio kaip juodraštis; pats sujungimas dokumento numerio nenaudoja.
Antraštės: klientas mato, iš kur atsirado kiekviena pozicija
Tai funkcija, kuri kasdienybėje daro skirtumą. Virš kiekvienos šaltinio sąskaitos faktūros pozicijų modulis įterpia skyriaus eilutę – tą patį elementą, kurį pats Odoo naudoja skyriui „Išankstiniai mokėjimai“:
BE10181 <- Antraštė
22,500 x Technikos specialisto valanda 3.577,50
Tarpinė suma 3.577,50
BE10182 · Bilietas: HT00219 <- Antraštė
3,000 x Technikos specialisto valanda 477,00
Tarpinė suma 477,00
GRYNOJI SUMA 4.054,50
Antraštė spausdinama dokumente, kurį gauna klientas, o standartinė Odoo sąskaitos faktūros ataskaita pati prideda kiekvieno skyriaus tarpinę sumą. Taip klientas mato ir tai, kiek kainavo kiekviena dalis.
Užrašas sudaromas iš to, ką šaltinis iš tikrųjų turi:
- užsakymai, tiesiai iš pozicijų ryšių – pavadinimai todėl tikslūs; jei trūksta ryšių su užsakymais, naudojamas laisvas tekstas iš invoice_origin
- bilietai, nustatomi per laiko apskaitą – pateikiami kaip Bilietas: HT00219 arba Bilietai: …, ribojama iki penkių plius „ir dar N“
- jei nėra nei vieno, nei kito: šaltinio sąskaitos faktūros numeris, paskui jos nuoroda
- jei nėra nieko: neutralus „Kitos pozicijos“, kad vieno šaltinio eilutės niekada tyliai nedingtų virš jų esančiame bloke
Ko trūksta, praleidžiama, o ne išgalvojama. Bilietas spausdinamas su savo numeriu – helpdesk_mgmt numeris, Enterprise ticket_ref arba modelio rodomas pavadinimas – niekada su duomenų bazės ID.
Antraštės rašomos kliento kalba, kaip tai daro Odoo kurdamas sąskaitas faktūras iš užsakymų. Chatter lieka naudotojo kalba.
Antraštė neturi nei produkto, nei kiekio, nei kainos, nei mokesčio, nei sąskaitos, nei sąnaudų centrų paskirstymo. Ji negali paveikti sumų, mokesčių bazės ir sąnaudų apskaitos. Kas asistente antraštes išjungia, gauna būtent tokią sąskaitą faktūrą, kokia susidarytų be šios funkcijos – testas abu rezultatus palygina iki cento.
Kas nutinka šaltinių sąskaitoms faktūroms
Asistentas siūlo tris parinktis:
- Anuliuoti (numatyta, rekomenduojama). Juodraščio anuliavimas pasekmių neturi: jokio dokumento numerio, jokio įrašo. Sąskaitos faktūros duomenų bazėje lieka kaip anuliuoti dokumentai su Chatter ir nuoroda į naują sąskaitą faktūrą. qty_invoiced pardavimo užsakyme anuliuotus dokumentus praleidžia, niekas neskaičiuojama dukart. Tai parinktis, kuri suprantama ir po šešių mėnesių.
- Ištrinti. Negrįžtamai. Chatter ir atgalinė nuoroda dingsta kartu su įrašu; lieka tik pranešimas naujoje sąskaitoje faktūroje. Tinka vienkartiniams juodraščiams, kurių niekas nebenori matyti.
- Palikti kaip juodraštį. Šaltiniai lieka atviri. Jų pardavimo užsakymuose tada šaltinis ir nauja sąskaita faktūra skaičiuojami kaip išrašyti – atrodo, kad darbas išrašytas dukart, kol kas nors rankiniu būdu sutvarko juodraščius. Skirta retam atvejui, kai prieš apsisprendžiant norisi palyginti. Testas fiksuoja būtent šį dvigubą skaičiavimą, kad jis nieko nustebintų.
Ribos – ko modulis sąmoningai nedaro
Prašome perskaityti prieš naudojant produkcinėje aplinkoje:
- Tik juodraščiai. Užregistruotos sąskaitos faktūros nesujungiamos. Tai kreditinių sąskaitų užduotis, o ne kopijavimo veiksmas.
- Kartą užregistruoti ir atstatyti juodraščiai atmetami. Jie turi numerį ir registravimo istoriją; slėpti tai kitame dokumente nėra tai, ką turėtų tyliai atlikti masinis veiksmas.
- Jokio sujungimo tarp skirtingų partnerių, valiutų, įmonių ar žurnalų. Tam nėra prasmingos vienos reikšmės, todėl modulis atmeta, užuot spėliojęs.
- payment_reference neperimama. Struktūrizuotos mokėjimo nuorodos identifikuoja vieną dokumentą; dvi sujungtos banke niekam netinka. Nauja sąskaita faktūra registruojant gauna savo.
- Avansinių mokėjimų pozicijos kopijuojamos kaip ir bet kuri kita pozicija. Jei šaltiniuose yra avansinių mokėjimų eilučių iš sale, rezultatą prieš registruojant reikia patikrinti.
- Automatiškai perkeliamas tik timesheet_invoice_id. Kiekvienas kitas modelis su Many2one į account.move (savi atsiskaitymo vykdymai, prenumeratų ryšiai, EDI perdavimai) ir toliau rodo į šaltinį. Tam skirtas hook _merge_draft_relink.
- Vienodų pozicijų nesujungiama – tyčia. Kas nori trumpos, tvarkingos sąskaitos faktūros, čia ne vietoje ir už tai sumokėtų kilmės ryšiais.
- Antraštės sukuriamos vieną kartą sujungiant. Po to jos yra paprastos skyrių eilutės: redaguojamos, ištrinamos ir neatnaujinamos, jei užsakymas ar bilietas vėliau pervadinamas.
- Antraštėje nurodomi viso šaltinio sąskaitos faktūros bilietai, o ne atskiros pozicijos.
Diegimas Odoo 19 Community
Diegimas vyksta kaip ir įprasto Odoo papildomo modulio:
- Modulio katalogą nukopijuoti į sukonfigūruotą addons kelią.
- Atnaujinti programų sąrašą Odoo.
- Įdiegti modulį account_invoice_merge_draft.
Modulis priklauso tik nuo account. Viskas, ką jis žino apie sale, purchase, project, hr_timesheet, sale_timesheet ar Helpdesk modulį, nustatoma vykdymo metu – jis įsidiegia ir veikia net grynoje atsiskaitymo duomenų bazėje.
Po to: Apskaita → Klientai → Sąskaitos faktūros (arba Tiekėjai → Sąskaitos faktūros), pažymėti du ar daugiau juodraščių, krumpliaračio meniu → Sujungti juodraštines sąskaitas faktūras. Veiksmas prieinamas visiems naudotojams, turintiems grupę Atsiskaitymas / Sąskaitų faktūrų rašymas.
Kas atnaujina iš ankstesnės modulio versijos, turėtų vieną kartą atnaujinti su -u account_invoice_merge_draft --i18n-overwrite, kad pakeisti laukų pavadinimai pasiektų vertimą į vokiečių kalbą. Naujo diegimo tai neliečia.
Kūrėjams: API ir hooks
Sujungimą galima iškviesti ir iš savo kodo:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" arba "keep"
add_source_sections=True, # Antraštės; numatyta True
)
merge_draft_moves išmeta UserError su visomis atmetimo priežastimis, jei pasirinkimo sujungti negalima. _merge_draft_get_errors() grąžina tas pačias priežastis kaip sąrašą be išimties.
Kas turi savo atgalines nuorodas į account.move, perrašo _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}
)
Kas turi atgalinę nuorodą sąskaitos faktūros pozicijoje, šio modulio visai nereikia: pakanka paties Odoo hook account.move.line._copy_data_extend_business_fields, o laukas perimamas ir sujungiant, ir anuliuojant, ir dubliuojant. Kitiems Helpdesk moduliams arba kitai antraščių formuluotei yra _merge_draft_get_source_tickets() ir _merge_draft_section_name().
Išbandyta, kol dar neveikia su produkciniais duomenimis
48 testai apima mišrias būsenas, skirtingus partnerius, valiutas ir dokumentų tipus, pozicijas su pardavimo ar pirkimo užsakymu ir be jų, mokesčius, skyrius ir pastabas, sąnaudų centrų paskirstymus, laiko apskaitos perkėlimą, visas tris šaltinių sąskaitų parinktis, asistentą ir antraštes – kad jos pagal numatytuosius nustatymus įjungtos, kad jas išjungus senas rezultatas atkuriamas iki cento, kad antraštė neturi sumų ar sąskaitų, kad vieno šaltinio pozicijos lieka kartu, kad bilietas rodomas su savo numeriu ir niekada su duomenų bazės ID.
Testiniai duomenys remiasi aktyvios įmonės esamu sąskaitų planu, o ne AccountTestInvoicingCommon – šis pagalbinis įrankis dažnai nepasileidžia migruotose produkcinėse duomenų bazėse, o modulis turi veikti būtent tokiose.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Kaip ir su kiekvienu apskaitos srities plėtiniu, rekomenduojame diegimą pirmiausia išbandyti bandomojoje aplinkoje su savo duomenų bazės kopija.
Nemokamai ir kaip atvirasis kodas Odoo bendruomenei
Mes, Biricon IT Services GmbH, sprendimus pirmiausia kuriame konkretiems savo veiklos poreikiams. Kai iš to atsiranda visiems naudojamas modulis, nenorime jo slėpti vienoje instaliacijoje.
Todėl Merge Draft Invoices teikiamas nemokamai kaip atvirojo kodo projektas. Šaltinio kodą galima peržiūrėti, tikrinti, pritaikyti ir pagal AGPL-3 licenciją toliau plėtoti.
Mūsų tikslas buvo ne kuo trumpesnė sąskaita faktūra, o tokia, kurioje ir po metų kiekviena pozicija veda atgal į savo užsakymą, bilietą ir laiko įrašą.
Projektas, šaltinio kodas ir dalyvavimas
Visas šaltinio kodas, diegimo nurodymai ir techninė dokumentacija prieinami GitHub:
Atidaryti Odoo 19 Merge Draft Invoices GitHub
Džiaugiamės praktine patirtimi, pranešimais apie klaidas, pasiūlymais dėl patobulinimų ir Odoo bendruomenės indėliu.
Nemokamai. Suprantamai. Atvirasis kodas. Odoo 19 Community.