Тыя, хто ў Odoo выстаўляе рахункі на падставе заказаў, заявак у службу падтрымкі і ўліку працоўнага часу, добра ведаюць вынік: напрыканцы месяца кліент атрымлівае не адзін рахунак, а пяць. Кожны звязаны з асобным заказам ці заяўкай, кожны складзены правільна — і ўсё ж кліент пытаецца, ці нельга змясціць усё ў адзін дакумент.
Менавіта для гэтага ў 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 там існуе толькі ў выглядзе адкрытага запыту на ўнясенне змен, а не выпушчанага модуля.
Але важней тое, што гэтыя два модулі робяць розныя рэчы:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Вынік | аб’яднанне ў адзін з выбраных рахункаў | заўсёды новы рахунак |
| Пазіцыі | аднолькавыя пазіцыі аб’ядноўваюцца, колькасці складаюцца | кожная пазіцыя пераносіцца адзін у адзін |
| Паходжанне | invoice_origin / payment_reference злучаюцца ў шапцы | палі паходжання кожнай пазіцыі капіююцца праз уласны хук 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) | капіюецца для кожнай пазіцыі |
| іншыя палі паходжання па пазіцыях | аўтаматычна, калі модуль, які дадае поле, рэалізуе задакументаваны хук 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 аўтаматычна дадае прамежкавы вынік для кожнага раздзела. Так кліент бачыць і кошт кожнай часткі.
Загаловак складаецца з даных, якія сапраўды ёсць у крыніцы:
- заказы бяруцца непасрэдна са сувязяў пазіцый, таму іх назвы дакладныя; калі сувязяў з заказамі няма, выкарыстоўваецца адвольны тэкст з invoice_origin
- заяўкі вызначаюцца праз улік працоўнага часу і выводзяцца як «Заяўка: HT00219» або «Заяўкі: …»; паказваюцца не больш за пяць, затым дадаецца «і яшчэ N»
- калі няма ні таго, ні другога: нумар зыходнага рахунку, а затым яго рэферэнс
- калі няма нічога: нейтральны загаловак «Дадатковыя пазіцыі», каб радкі адной крыніцы ніколі непрыкметна не зліваліся з папярэднім блокам
Адсутныя даныя прапускаюцца, а не прыдумляюцца. Заяўка друкуецца з уласным нумарам — нумарам helpdesk_mgmt, ticket_ref у Enterprise або адлюстраваным імем мадэлі — і ніколі з ідэнтыфікатарам у базе даных.
Загалоўкі ствараюцца на мове кліента, як гэта робіць Odoo пры стварэнні рахункаў з заказаў. Chatter застаецца на мове карыстальніка.
Загаловак не змяшчае прадукту, колькасці, цаны, падатку, бухгалтарскага рахунку або размеркавання па цэнтрах выдаткаў. Ён не можа ўплываць на сумы, падатковую базу і ўлік выдаткаў. Калі адключыць загалоўкі ў майстры, атрымаецца дакладна такі самы рахунак, як без гэтай функцыі, — тэст параўноўвае абодва вынікі з дакладнасцю да цэнта.
Што адбываецца з зыходнымі рахункамі
Майстар прапануе тры варыянты:
- Скасаваць (прадвызначаны і рэкамендаваны варыянт). Скасаванне чарнавіка не мае наступстваў: ні нумара дакумента, ні бухгалтарскай праводкі. Рахункі застаюцца ў базе даных як скасаваныя дакументы з Chatter і спасылкай на новы рахунак. qty_invoiced у заказе на продаж прапускае скасаваныя дакументы, таму нічога не ўлічваецца двойчы. Гэты варыянт дазваляе зразумець, што адбылося, нават праз паўгода.
- Выдаліць. Незваротна. Chatter і зваротная спасылка гінуць разам са запісам; застаецца толькі паведамленне ў новым рахунку. Мае сэнс для чарнавікоў-аднадзёнак, якія ніхто больш не павінен бачыць.
- Пакінуць як чарнавік. Крыніцы застаюцца адкрытымі. Іх заказы на продаж тады лічаць і крыніцу, і новы рахунак выстаўленымі – праца выглядае двойчы выстаўленай, пакуль хтосьці не ачысціць чарнавікі ўручную. Прызначана для рэдкага выпадку, калі перад рашэннем хочацца параўнаць. Тэст фіксуе менавіта гэты двайны ўлік, каб ён нікога не здзівіў.
Абмежаванні — чаго модуль свядома не робіць
Прачытайце перад выкарыстаннем у рабочай сістэме:
- Толькі чарнавікі. Праведзеныя рахункі не аб’ядноўваюцца. Тут патрэбныя крэдыт-ноты, а не капіяванне.
- Рахункі, якія ўжо былі праведзеныя і затым вернутыя ў стан чарнавіка, не дапускаюцца. У іх ёсць нумар і гісторыя правядзення; хаваць гэта ў іншым дакуменце не павінна быць непрыкметнай часткай масавай аперацыі.
- Рахункі з рознымі контрагентамі, валютамі, кампаніямі або журналамі не аб’ядноўваюцца. Для іх немагчыма абгрунтавана выбраць адно агульнае значэнне, таму модуль адмаўляе ў аб’яднанні замест таго, каб гадаць.
- payment_reference не пераносіцца. Структураваны плацежны рэферэнс ідэнтыфікуе адзін дакумент; два злучаныя рэферэнсы не будуць адпавядаць нічому ў банку. Пры правядзенні новы рахунак атрымае ўласны рэферэнс.
- Авансавыя пазіцыі капіююцца, як і ўсе астатнія. Калі зыходныя рахункі змяшчаюць авансавыя радкі з sale, перад правядзеннем трэба праверыць вынік.
- Аўтаматычна перапрывязваецца толькі timesheet_invoice_id. Любая іншая мадэль з полем Many2one, якое спасылаецца на account.move (уласныя працэдуры выстаўлення рахункаў, сувязі з падпіскамі, перадачы EDI), працягвае спасылацца на крыніцу. Для гэтага прадугледжаны хук _merge_draft_relink.
- Аб’яднання аднолькавых пазіцый няма – наўмысна. Хто хоча кароткі, акуратны рахунак, той тут не па адрасе і заплаціў бы за гэта сувязямі з паходжаннем.
- Загалоўкі ствараюцца адзін раз пры аб’яднанні. Пасля гэтага яны становяцца звычайнымі радкамі раздзелаў: іх можна рэдагаваць і выдаляць, але яны не абнаўляюцца, калі пазней перайменаваць заказ або заяўку.
- У загалоўку ўказваюцца заяўкі ўсяго зыходнага рахунку, а не асобнай пазіцыі.
Усталяванне ў Odoo 19 Community
Усталяванне выконваецца гэтак жа, як для звычайнага дадатковага модуля Odoo:
- Скапіюйце каталог модуля ў адзін з наладжаных каталогаў дадатковых модуляў.
- Абнавіце спіс праграм у Odoo.
- Усталюйце модуль account_invoice_merge_draft.
Модуль залежыць толькі ад account. Усе даныя пра sale, purchase, project, hr_timesheet, sale_timesheet або модуль службы падтрымкі вызначаюцца падчас выканання — таму модуль усталёўваецца і працуе нават у базе даных, якая выкарыстоўваецца выключна для выстаўлення рахункаў.
Затым: Бухгалтэрыя → Кліенты → Рахункі (або Пастаўшчыкі → Рахункі), выберыце два або больш чарнавікоў, адкрыйце меню са значком шасцярэнькі → Аб’яднаць чарнавікі рахункаў. Дзеянне даступнае ўсім карыстальнікам групы Разлікі / Выстаўленне рахункаў.
Хто абнаўляецца з ранейшай версіі модуля, павінен аднойчы выканаць абнаўленне з -u account_invoice_merge_draft --i18n-overwrite, каб змененыя назвы палёў трапілі ў нямецкі пераклад. Новай устаноўкі гэта не тычыцца.
Для распрацоўшчыкаў: API і хукі
Аб’яднанне можна выклікаць і з уласнага кода:
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}
)
Калі зваротная спасылка знаходзіцца ў пазіцыі рахунку, гэты модуль для яе наогул не патрэбны: дастаткова ўласнага хука Odoo account.move.line._copy_data_extend_business_fields, і поле будзе пераносіцца пры аб’яднанні, скасаванні і дубляванні. Для іншых модуляў службы падтрымкі або іншага фармулявання загалоўкаў прадугледжаны _merge_draft_get_source_tickets() і _merge_draft_section_name().
Праверана да запуску на рабочых даных
48 тэстаў пакрываюць змешаныя статусы, розных партнёраў, валюты і тыпы дакументаў, пазіцыі з заказам на продаж або куплю і без яго, падаткі, раздзелы і нататкі, размеркаванні па цэнтрах выдаткаў, перапрывязку ўліку часу, усе тры опцыі для зыходных рахункаў, памочніка і загалоўкі – што яны стандартна ўключаны, што іх адключэнне ўзнаўляе стары вынік да цэнта, што загаловак не нясе сум і рахункаў, што пазіцыі адной крыніцы застаюцца разам, што тыкет з’яўляецца са сваім нумарам і ніколі з ID базы даных.
Тэставыя даныя выкарыстоўваюць наяўны план рахункаў актыўнай кампаніі замест 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.
Бясплатна. Зразумела. Open Source. Для Odoo 19 Community.