Кој во Odoo фактурира од нарачки, Helpdesk-тикети и евиденција на време, го знае резултатот: клиентот на крајот на месецот не добива една фактура, туку пет. Секоја потекнува од друга нарачка или тикет, секоја е стручно коректна – и сепак клиентот прашува дали тоа не би можело да стане во еден документ.
Токму за тоа во Odoo 19 Community не постои чисто решение. Познатиот OCA-модул account_invoice_merge сè уште не е објавен за Odoo 19, а проблемот го решава на начин што за давателите на услуги претставува сериозен ризик: еднакви ставки се собираат и така се губи врската назад кон нарачката, задачата и евидентираното време.
За нашата сопствена инсталација на Odoo 19 Community затоа го развивме модулот Merge Draft Invoices. Бидејќи овој проблем не го засега само нас, го ставаме бесплатно на располагање на GitHub под слободната AGPL-3-лиценца.
Кон проектот на GitHub:
Зошто сопствен модул наместо OCA-решението?
OCA со години го одржува account_invoice_merge во репозиториумот OCA/account-invoicing. Во времето на овој напис, миграцијата кон 19.0 таму сè уште е отворен Pull Request, а не објавен модул.
Поважно е, сепак, што двата модула прават различни работи:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Резултат | се спојува во една од избраните фактури | секогаш нова фактура |
| Ставки | еднаквите ставки се собираат, количините се додаваат | секоја ставка се презема еден на еден |
| Потекло | invoice_origin / payment_reference се спојуваат во заглавието | полињата за потекло по ставка се копираат преку сопствениот _copy_data_extend_business_fields-Hook на Odoo |
| Евиденција на време | не се зема предвид | timesheet_invoice_id се префрла на новата фактура |
| Читливо за клиентот | ништо | наслов над ставките од секој извор со нарачка и тикет |
| Изворни фактури | се сторнираат | сторнирање (стандардно), бришење или задржување – со јасно наведени последици |
Собирањето на еднакви ставки е одлучувачката разлика. Собраната ставка може да носи само едно потекло. Штом два реда од две различни нарачки изгледаат исто, врската од фактурата назад кон нарачката, задачата и книжењето на време е изгубена. Во услужно претпријатие каде фактурирањето тече преку Тикет → Евиденција на време → Фактура, тоа значи: истите часови се фактурираат двојно или исчезнуваат.
Нашиот модул затоа во принцип ништо не собира. Споената фактура има точно толку ставки колку што имаат нејзините извори заедно.
Не е преземен код од OCA-модулот. Двата модула се под AGPL-3.
Што прави модулот
Во прегледот на листата на фактури означете два или повеќе нацрти, во менито со акции изберете Спојување на нацрт-фактури – асистент ги покажува изборот, проверките и опциите за изворните фактури. По потврдувањето директно се стигнува до новата фактура.
Пред спојувањето модулот проверува, и тоа сите точки одеднаш, за да не се гледа следната грешка по секоја корекција:
- избрани најмалку две фактури
- сите во статус Нацрт
- ниту една не била некогаш книжена (документ вратен на нацрт ја задржува својата историја на книжење и се одбива)
- ниту една веќе не била споена
- сите се фактура, фактура од добавувач, кредитна нота или документ – не чисти налози за книжење
- ист партнер, тип на документ, претпријатие, валута и журнал
Разликите во условите за плаќање, даночното пресликување, адресата за испорака, банкарската сметка или продавачот не блокираат. Асистентот предупредува и ја презема вредноста од најстарата избрана фактура.
Што се задржува
Суштината на модулот е дека секоја врска што ставката од фактурата ја има со своето потекло постои и на новата фактура:
| Врска | Постапување |
|---|---|
| Ставки од продажни нарачки (sale_line_ids) | се копираат по ставка преку _copy_data_extend_business_fields на Odoo |
| Ставки од набавни нарачки (purchase_line_id) | исто така |
| Распределба на центри на трошоци (analytic_distribution) | се копира по ставка |
| други полиња за потекло по ставка | автоматски, ако модулот што ги испорачува го имплементира документираниот Odoo-Hook |
| Потекло и референца во заглавието на фактурата | споени, дедуплицирани, уредливи во асистентот |
| Продажна нарачка: invoice_ids, invoice_status, qty_invoiced | се пресметуваат од ставките – нарачката сама ја препознава новата фактура |
| Евиденција на време (timesheet_invoice_id) | се префрла на новата фактура пред изворите да станат неважечки |
| Helpdesk-тикет | е врзан за евиденцијата на време и така автоматски покажува на новата фактура |
| Chatter | Порака на новата фактура со сите извори, порака на секој извор со врска кон новата |
| merged_into_move_id / merged_from_move_ids | зачувана врска во двете насоки |
Редовите за даноци, условите за плаќање, заокружувањето и попустот за брзо плаќање не се копираат, туку Odoo ги пресметува одново од содржината. Споената фактура така има еден ред за данок по даночна стапка, а не еден по извор. Се создава без број како нацрт; самото спојување не троши број на документ.
Наслови: клиентот гледа од каде потекнува секоја ставка
Ова е функцијата што прави разлика во секојдневието. Над ставките на секоја изворна фактура модулот поставува ред за дел – истиот елемент што Odoo самиот го користи за делот „Аванси“:
BE10181 <- Наслов
22,500 x Час на техничар 3.577,50
Меѓузбир 3.577,50
BE10182 · Тикет: HT00219 <- Наслов
3,000 x Час на техничар 477,00
Меѓузбир 477,00
НЕТО ИЗНОС 4.054,50
Насловот се печати на документот што го добива клиентот, а стандардниот извештај за фактури на Odoo сам додава меѓузбир по дел. Клиентот така гледа и колку чинел секој дел.
Ознаката се составува од она што изворот навистина го има:
- нарачките, директно од врските на ставките – имињата така се точни; ако недостасуваат врски кон нарачки, се користи слободниот текст од invoice_origin
- тикетите, утврдени преку евиденцијата на време – прикажани како Тикет: HT00219 или Тикети: …, ограничено на пет плус „и уште N“
- ако нема ниту едно од двете: бројот на изворната фактура, потоа нејзината референца
- ако нема воопшто ништо: неутрално „Дополнителни ставки“, за редовите од еден извор никогаш тивко да не исчезнат во блокот погоре
Што недостасува, се испушта, не се измислува. Тикетот се печати со својот сопствен број – број од helpdesk_mgmt, Enterprise-ticket_ref или прикажаното име на моделот – никогаш со ID од базата на податоци.
Насловите се пишуваат на јазикот на клиентот, како што тоа го прави Odoo при создавање фактури од нарачки. Chatter останува на јазикот на корисникот.
Насловот не носи производ, количина, цена, данок, сметка ниту распределба на центри на трошоци. Не може да влијае на збировите, даночната основа и пресметката на трошоците. Кој ги исклучи насловите во асистентот, ја добива точно фактурата што би настанала без оваа функција – еден тест ги споредува двата резултата до последен цент.
Што се случува со изворните фактури
Асистентот нуди три опции:
- Сторнирање (стандардно, препорачано). Сторнирањето на нацрт е без последици: без број на документ, без книжење. Фактурите остануваат во базата на податоци како сторнирани документи со Chatter и врска кон новата фактура. qty_invoiced на продажната нарачка ги прескокнува сторнираните документи, ништо не се брои двојно. Тоа е опцијата што е проследлива и по шест месеци.
- Бришење. Неповратно. Chatter и повратната референца умираат со записот; останува само пораката на новата фактура. Смислено за нацрти за фрлање што никој повеќе не треба да ги гледа.
- Оставање како нацрт. Изворите остануваат отворени. Нивните продажни нарачки тогаш го бројат изворот и новата фактура како фактурирани – работата изгледа двојно пресметана сè додека некој рачно не ги исчисти нацртите. Наменето за ретката ситуација кога пред одлуката сакате да споредите. Еден тест го фиксира токму ова двојно броење за да не изненади никого.
Ограничувања – што модулот намерно не прави
Ве молиме прочитајте пред продуктивна употреба:
- Само нацрти. Книжените фактури не се спојуваат. Тоа е задача за кредитни ноти, а не процес на копирање.
- Еднаш книжените, вратени нацрти се одбиваат. Тие имаат број и историја на книжење; тоа да се крие во друг документ не е нешто што една групна акција треба тивко да го прави.
- Не се спојува меѓу партнери, валути, претпријатија или журнали. За тоа не постои смислена единствена вредност, па модулот одбива наместо да погодува.
- payment_reference не се презема. Структурираните референци на плаќање идентификуваат еден документ; две споени не одговараат на ништо во банката. Новата фактура ја добива својата при книжењето.
- Ставките за аванс се копираат како секоја друга ставка. Ако изворите содржат редови за аванс од sale, резултатот мора да се провери пред книжењето.
- Само timesheet_invoice_id автоматски се префрла. Секој друг модел со Many2one кон account.move (сопствени циклуси на фактурирање, врски со претплати, EDI-испраќања) и натаму покажува на изворот. За тоа постои Hook-от _merge_draft_relink.
- Нема собирање на еднакви ставки – намерно. Кој сака кратка, средена фактура, е на погрешно место и за тоа би платил со врските на потеклото.
- Насловите настануваат еднаш, при спојувањето. Потоа тие се нормални редови за дел: уредливи, бришливи и не се ажурираат ако нарачката или тикетот подоцна се преименуваат.
- Насловот ги наведува тикетите на целата изворна фактура, а не на поединечната ставка.
Инсталација во Odoo 19 Community
Инсталацијата се врши како кај обичен дополнителен модул на Odoo:
- Копирајте ја папката на модулот во конфигурирана патека на Addons.
- Ажурирајте ја листата на апликации во Odoo.
- Инсталирајте го модулот account_invoice_merge_draft.
Модулот зависи само од account. Сè што знае за sale, purchase, project, hr_timesheet, sale_timesheet или некој Helpdesk-модул се утврдува при извршување – се инсталира и функционира и на чиста база на податоци за фактурирање.
Потоа: Сметководство → Клиенти → Фактури (или Добавувачи → Фактури), означете два или повеќе нацрти, мени со запчаник → Спојување на нацрт-фактури. Акцијата им е достапна на сите корисници од групата Фактурирање / Наплата.
Кој ажурира од претходна верзија на модулот, треба еднаш да ажурира со -u account_invoice_merge_draft --i18n-overwrite, за променетите називи на полињата да стигнат во германскиот превод. Ова не се однесува на нова инсталација.
За програмери: API и Hooks
Спојувањето може да се повика и од сопствен код:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" или "keep"
add_source_sections=True, # наслови; стандардно True
)
merge_draft_moves фрла UserError со сите причини за одбивање ако изборот не може да се спои. _merge_draft_get_errors() ги враќа истите причини како листа без Exception.
Кој има сопствени повратни референци кон account.move, го препокрива _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}
)
Кој има повратна референца на ставката од фактурата, воопшто не го бара овој модул: доволен е сопствениот Hook на Odoo account.move.line._copy_data_extend_business_fields, а полето се презема подеднакво при спојување, сторнирање и дуплирање. За други Helpdesk-модули или поинаква формулација на насловите постојат _merge_draft_get_source_tickets() и _merge_draft_section_name().
Тестирано пред да работи на продуктивни податоци
48 тестови покриваат мешани статуси, различни партнери, валути и типови на документи, ставки со и без продажна или набавна нарачка, даноци, делови и белешки, распределби на центри на трошоци, префрлањето на евиденцијата на време, сите три опции за изворните фактури, асистентот и насловите – дека се стандардно активни, дека нивното исклучување го репродуцира стариот резултат до цент, дека насловот не носи износи или сметки, дека ставките од еден извор остануваат заедно, дека тикетот се појавува со својот број, а никогаш со ID од базата на податоци.
Тест-податоците се градат врз постојниот сметковен план на активната фирма, а не врз AccountTestInvoicingCommon – овој Helper често не се стартува на мигрирани продуктивни бази на податоци, а токму на такви треба да работи модулот.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Како кај секое проширување во областа на сметководството, препорачуваме инсталацијата најпрво да се провери во тест-околина со копија од сопствената база на податоци.
Бесплатно и Open Source за Odoo Community
Ние во Biricon IT Services GmbH решенија најпрво развиваме за конкретни барања од сопственото работење. Кога од тоа ќе настане модул што може општо да се користи, не сакаме да го криеме во една единствена инсталација.
Merge Draft Invoices затоа е бесплатно достапен како Open-Source-проект. Изворниот код може да се прегледува, проверува, прилагодува и, во рамките на AGPL-3-лиценцата, понатаму да се развива.
Нашата цел не беше најкратка можна фактура, туку фактура кај која и по една година секоја ставка води назад до својата нарачка, својот тикет и своето книжење на време.
Проект, изворен код и соработка
Целосниот изворен код, упатствата за инсталација и техничката документација се достапни на GitHub:
Отворете го Odoo 19 Merge Draft Invoices на GitHub
Се радуваме на практични искуства, извештаи за грешки, предлози за подобрување и придонеси од Odoo Community.
Бесплатно. Проследливо. Open Source. За Odoo 19 Community.