Преминете към съдържание

Odoo 19 Community: обединяване на чернови на фактури — нашият модул с отворен код Merge Draft Invoices

18 септември 2026 г. от
Odoo 19 Community: обединяване на чернови на фактури — нашият модул с отворен код Merge Draft Invoices
Alexander Biringer | Biricon IT Services GmbH

Всеки, който фактурира в Odoo въз основа на поръчки, заявки до поддръжката и отчетено работно време, познава резултата: в края на месеца клиентът получава пет фактури вместо една. Всяка е от различна поръчка или заявка, всяка е коректна — и въпреки това клиентът пита дали всичко не може да се събере в един документ.

Точно за този случай Odoo 19 Community няма подходящо решение. Познатият модул на OCA account_invoice_merge все още не е публикуван за Odoo 19, а и решава проблема по начин, който крие сериозен риск за доставчиците на услуги: еднаквите редове се обединяват и така се губи връзката с поръчката, задачата и отчетеното време.

Затова разработихме модула Merge Draft Invoices за собствената си инсталация на Odoo 19 Community. Тъй като този проблем не засяга само нас, го предоставяме безплатно в GitHub под свободния лиценз AGPL-3.

Към проекта в GitHub:

Odoo 19 Merge Draft Invoices

Защо собствен модул вместо решението на OCA?

OCA поддържа account_invoice_merge в хранилището OCA/account-invoicing от много години. Към момента на публикуване на тази статия миграцията към 19.0 все още е отворена заявка за сливане на код, а не публикуван модул.

По-важното обаче е, че двата модула правят различни неща:


OCA account_invoice_mergeMerge Draft Invoices
Резултатобединяване в една от избраните фактуривинаги нова фактура
Редовееднаквите редове се обединяват, а количествата се сумиратвсеки ред се прехвърля едно към едно
Произходinvoice_origin / payment_reference се съединяват в заглавната частполетата за произход на всеки ред се копират чрез вградения в Odoo hook _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)пренасочва се към новата фактура, преди изходните фактури да станат невалидни
Заявка до поддръжкатасвързана е с отчетеното работно време и затова автоматично сочи към новата фактура
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 автоматично добавя междинна сума за всеки раздел. Така клиентът вижда и колко струва всяка част.

Текстът на заглавието се съставя от действително наличните данни в изходната фактура:

  1. поръчките, взети директно от връзките на редовете — така наименованията са точни; ако няма връзки с поръчки, се използва свободният текст от invoice_origin
  2. заявките, установени чрез отчетеното работно време — показват се като Заявка: HT00219 или Заявки: …, до пет на брой, последвани от „и още N“
  3. ако няма нито едното, нито другото: номерът на изходната фактура, след това нейната референция
  4. ако няма никакви данни: неутралното „Допълнителни редове“, така че редовете от дадена изходна фактура никога да не се слеят незабелязано с предходния раздел

Липсващите данни се пропускат, без да се измислят. Заявката се отпечатва със собствения си номер — номера от helpdesk_mgmt, ticket_ref от Enterprise или показваното име на модела — никога с идентификатора от базата данни.

Заглавията се създават на езика на клиента, както прави Odoo при генерирането на фактури от поръчки. Chatter остава на езика на потребителя.

Заглавието не съдържа продукт, количество, цена, данък, счетоводна сметка или разпределение по разходни центрове. То не може да влияе върху сумите, данъчната основа и отчитането на разходите. Ако изключите заглавията в помощника, ще получите точно същата фактура, която би се създала без тази функция — тест сравнява двата резултата до последния цент.

Какво се случва с изходните фактури

Помощникът предлага три опции:

  • Анулиране (по подразбиране, препоръчително). Анулирането на чернова няма счетоводни последици: няма номер на документ и няма осчетоводяване. Фактурите остават в базата данни като анулирани документи с Chatter и връзка към новата фактура. qty_invoiced в поръчката за продажба пропуска анулираните документи, така че нищо не се отчита два пъти. Тази опция запазва проследимостта и след шест месеца.
  • Изтриване. Необратимо. Chatter и обратната препратка се изтриват заедно със записа; остава само съобщението в новата фактура. Подходящо за чернови за еднократна употреба, които никой повече не трябва да вижда.
  • Оставяне като чернови. Изходните фактури остават отворени. Тогава свързаните с тях поръчки за продажба отчитат като фактурирани изходните фактури и новата фактура — работата изглежда фактурирана два пъти, докато някой не изчисти черновите ръчно. Предвидено е за редките случаи, когато искате да сравните, преди да решите. Тест проверява именно това двойно отчитане, за да не изненада никого.

Ограничения — какво модулът умишлено не прави

Прочетете преди използване в работна среда:

  • Само чернови. Осчетоводени фактури не се обединяват. За тях са необходими кредитни известия, а не копиране.
  • Чернови, които са били осчетоводени и след това върнати в чернова, се отхвърлят. Те имат номер и история на осчетоводяването; скриването им в друг документ не е нещо, което една групова операция трябва да прави мълчаливо.
  • Не се обединяват фактури с различни партньори, валути, фирми или дневници. За тези полета няма смислена обща стойност, затова модулът отказва обединяването, вместо да гадае.
  • payment_reference не се прехвърля. Структурираните платежни референции идентифицират конкретен документ; съединяването на две референции не съответства на нищо в банката. При осчетоводяване новата фактура получава собствена референция.
  • Редовете за авансови плащания се копират като всички останали редове. Ако изходните фактури съдържат редове за авансови плащания от sale, резултатът трябва да се провери преди осчетоводяване.
  • Само timesheet_invoice_id се пренасочва автоматично. Всеки друг модел с Many2one към account.move (собствени цикли на фактуриране, връзки с абонаменти, EDI предавания) продължава да сочи към изходната фактура. За това е предвиден hook _merge_draft_relink.
  • Еднаквите редове не се обединяват — това е умишлено. Ако търсите кратка, стегната фактура, този модул не е подходящ: това би било за сметка на връзките с източниците.
  • Заглавията се създават еднократно при обединяването. След това те са обикновени заглавни редове за раздели: могат да се редактират и изтриват, но не се актуализират, ако поръчката или заявката бъде преименувана по-късно.
  • Заглавието посочва заявките за цялата изходна фактура, а не за отделния ред.

Инсталиране в Odoo 19 Community

Инсталирането се извършва като при стандартен допълнителен модул за Odoo:

  1. Копирайте директорията на модула в някой от конфигурираните пътища за допълнителни модули.
  2. Обновете списъка с приложения в Odoo.
  3. Инсталирайте модула account_invoice_merge_draft.

Модулът зависи само от account. Всички данни за sale, purchase, project, hr_timesheet, sale_timesheet или модул за поддръжка се установяват по време на изпълнение — той се инсталира и работи и в база данни, използвана само за фактуриране.

След това: Счетоводство → Клиенти → Фактури (или Доставчици → Фактури), маркирайте две или повече чернови, меню със зъбно колело → Обединяване на чернови на фактури. Действието е достъпно за всички потребители в групата Фактуриране / Издаване на фактури.

Ако актуализирате от по-ранна версия на модула, изпълнете еднократно актуализация с -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 е достатъчен и полето се прехвърля еднакво при обединяване, анулиране и дублиране. За други модули за поддръжка или за различен текст на заглавията са предвидени _merge_draft_get_source_tickets() и _merge_draft_section_name().

Тестван, преди да заработи с реални данни

48 теста обхващат смесени статуси, различни партньори, валути и видове документи, редове със и без поръчка за продажба или покупка, данъци, раздели и бележки, разпределения по разходни центрове, пренасочването на отчетеното работно време, всичките три опции за изходните фактури, помощника и заглавията. Проверява се дали заглавията са включени по подразбиране, дали изключването им възпроизвежда предишния резултат до последния цент, дали заглавието не съдържа суми или сметки, дали редовете от една изходна фактура остават заедно и дали заявката се показва със собствения си номер, а никога с идентификатора от базата данни.

Тестовите данни използват съществуващия сметкоплан на активната фирма вместо AccountTestInvoicingCommon — този помощен клас често не се стартира в мигрирани работни бази данни, а модулът трябва да работи точно в такива.

odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
         --test-tags /account_invoice_merge_draft --stop-after-init

Както при всяко разширение в областта на счетоводството, препоръчваме първо да проверите инсталацията в тестова среда с копие на собствената си база данни.

Безплатно и с отворен код за общността на Odoo

В Biricon IT Services GmbH първоначално разработваме решения за конкретни нужди на собствената си дейност. Когато от тях се получи модул, полезен и за други, искаме той да бъде достъпен отвъд рамките на една инсталация.

Затова Merge Draft Invoices се предлага безплатно като проект с отворен код. Изходният код може да се разглежда, проверява, адаптира и доразвива съгласно лиценза AGPL-3.

Целта ни беше да създадем не възможно най-кратката фактура, а фактура, в която и след година всеки ред може да се проследи до съответната поръчка, заявка и запис за работното време.

Проект, изходен код и участие

Пълният изходен код, указанията за инсталиране и техническата документация са достъпни в GitHub:

Отворете Odoo 19 Merge Draft Invoices в GitHub

Ще се радваме на споделен практически опит, сигнали за грешки, предложения за подобрения и приноси от общността на Odoo.

Безплатно. Проследимо. С отворен код. За Odoo 19 Community.