Който фактурира в 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 се слепват в заглавната част | полетата за произход по редове се копират чрез собствения hook на Odoo _copy_data_extend_business_fields |
| Отчитане на времето | не се взема предвид | timesheet_invoice_id се прехвърля към новата фактура |
| Четливо за клиента | нищо | заглавие над редовете на всеки източник с поръчка и тикет |
| Изходни фактури | се анулират | анулиране (по подразбиране), изтриване или запазване – с ясно назовани последствия |
Обединяването на еднакви редове е решаващата разлика. Един обединен ред може да има само един произход. Щом два реда от две различни поръчки изглеждат еднакво, връзката от фактурата обратно към поръчката, задачата и записа на времето изчезва. В сервизна фирма, където фактурирането минава през Тикет → Отчитане на времето → Фактура, това означава: едни и същи часове се фактурират два пъти или изчезват.
Затова нашият модул по принцип нищо не обединява. Обединената фактура има точно толкова реда, колкото имат изходните ѝ фактури заедно.
Не е взет код от модула на OCA. И двата модула са под AGPL-3.
Какво прави модулът
В списъчния изглед на фактурите се маркират две или повече чернови, в менюто с действия се избира Обединяване на чернови фактури – помощник показва избора, проверките и опциите за изходните фактури. След потвърждаване директно се попада в новата фактура.
Преди обединяването модулът проверява, и то всички точки наведнъж, за да не се вижда следващата грешка след всяка корекция:
- избрани са поне две фактури
- всички са в статус Чернова
- нито една не е била осчетоводявана (документ, върнат в чернова, запазва историята на осчетоводяванията си и се отхвърля)
- нито една не е била вече обединена
- всички са фактура, фактура от доставчик, кредитно известие или документ – не чисти счетоводни записи
- същият партньор, тип документ, фирма, валута и журнал
Разлики в условията за плащане, данъчното съпоставяне, адреса за доставка, банковата сметка или продавача не блокират. Помощникът предупреждава и взема стойността на най-старата избрана фактура.
Какво се запазва
Ядрото на модула е, че всяка връзка, която един ред на фактура има към своя произход, съществува и в новата фактура:
| Връзка | Третиране |
|---|---|
| Редове на поръчки за продажба (sale_line_ids) | копират се по редове чрез _copy_data_extend_business_fields на Odoo |
| Редове на поръчки за покупка (purchase_line_id) | също така |
| Разпределение по разходни центрове (analytic_distribution) | копира се по редове |
| други полета за произход по редове | автоматично, ако предоставящият модул имплементира документирания hook на Odoo |
| Произход и референция в заглавната част на фактурата | обединени, без дублиране, редактируеми в помощника |
| Поръчка за продажба: invoice_ids, invoice_status, qty_invoiced | се изчисляват от редовете – поръчката сама разпознава новата фактура |
| Отчитане на времето (timesheet_invoice_id) | прехвърля се към новата фактура, преди източниците да станат невалидни |
| Тикет на Helpdesk | е свързан с отчитането на времето и така автоматично сочи към новата фактура |
| Chatter | съобщение в новата фактура с всички източници, съобщение във всеки източник с връзка към новата |
| merged_into_move_id / merged_from_move_ids | записана връзка в двете посоки |
Данъчните редове, редовете за условия на плащане, редовете за закръгляване и за отстъпка при плащане (Skonto) не се копират, а се изчисляват наново от 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, ticket_ref от Enterprise или показваното име на модела – никога с 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() връща същите причини като списък без изключение.
Който има собствени обратни препратки към 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
Както при всяко разширение в областта на счетоводството, препоръчваме инсталацията първо да се провери в тестова среда с копие на собствената база данни.
Безплатно и с отворен код за Odoo Community
Ние от Biricon IT Services GmbH първо разработваме решения за конкретни изисквания от собствената си дейност. Когато от това се получи модул с общо приложение, не искаме да го крием в една-единствена инсталация.
Затова Merge Draft Invoices е достъпен безплатно като проект с отворен код. Изходният код може да се разглежда, проверява, адаптира и развива в рамките на лиценза AGPL-3.
Целта ни не беше възможно най-късата фактура, а фактура, при която и след една година всеки ред все още води обратно към своята поръчка, своя тикет и своя запис на време.
Проект, изходен код и съдействие
Пълният изходен код, указанията за инсталиране и техническата документация са достъпни в GitHub:
Отваряне на Odoo 19 Merge Draft Invoices в GitHub
Радваме се на практически опит, съобщения за грешки, предложения за подобрения и приноси от общността на Odoo.
Безплатно. Проследимо. Отворен код. За Odoo 19 Community.