Хто ў 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 там – усё яшчэ адкрыты pull request, а не выпушчаны модуль.
Але важней тое, што гэтыя два модулі робяць розныя рэчы:
| 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 · Ticket: HT00219 <- Загаловак
3,000 x Гадзіна тэхніка 477,00
Прамежкавая сума 477,00
НЕТА-СУМА 4.054,50
Загаловак друкуецца на дакуменце, які атрымлівае кліент, а стандартны справаздачны шаблон рахунку Odoo сам дадае прамежкавую суму для кожнага раздзела. Так кліент бачыць таксама, колькі каштавала кожная частка.
Подпіс складаецца з таго, што сапраўды ёсць у крыніцы:
- заказы, непасрэдна з сувязей пазіцый – назвы таму дакладныя; калі сувязей з заказамі няма, выкарыстоўваецца свабодны тэкст з invoice_origin
- тыкеты, вызначаныя праз улік часу – выводзяцца як Ticket: HT00219 або Tickets: …, з абмежаваннем да пяці плюс «і яшчэ N»
- калі няма ні таго, ні другога: нумар зыходнага рахунку, потым яго рэферэнс
- калі няма наогул нічога: нейтральнае «Іншыя пазіцыі», каб радкі крыніцы ніколі не знікалі моўчкі ў блоку вышэй
Чаго няма, тое прапускаецца, а не прыдумваецца. Тыкет друкуецца са сваім уласным нумарам – нумар helpdesk_mgmt, Enterprise-ticket_ref або адлюстроўваемая назва мадэлі – ніколі з ID базы даных.
Загалоўкі пішуцца на мове кліента, так, як гэта робіць Odoo пры стварэнні рахункаў з заказаў. Chatter застаецца на мове карыстальніка.
Загаловак не мае ні прадукту, ні колькасці, ні цаны, ні падатку, ні рахунку, ні размеркавання па цэнтрах выдаткаў. Ён не можа паўплываць на сумы, падатковую базу і кошт-уліку. Хто адключае загалоўкі ў памочніку, атрымлівае дакладна той рахунак, які ўзнік бы без гэтай функцыі – тэст параўноўвае абодва вынікі да цэнта.
Што адбываецца са зыходнымі рахункамі
Памочнік прапануе тры опцыі:
- Сторнаваць (стандарт, рэкамендуецца). Сторнаванне чарнавіка не мае наступстваў: ніякага нумара дакумента, ніякай праводкі. Рахункі застаюцца ў базе даных як сторнаваныя дакументы з Chatter і спасылкай на новы рахунак. qty_invoiced у заказе на продаж прапускае сторнаваныя дакументы, нічога не лічыцца двойчы. Гэта опцыя, якая застаецца зразумелай і праз паўгода.
- Выдаліць. Незваротна. Chatter і зваротная спасылка гінуць разам са запісам; застаецца толькі паведамленне ў новым рахунку. Мае сэнс для чарнавікоў-аднадзёнак, якія ніхто больш не павінен бачыць.
- Пакінуць як чарнавік. Крыніцы застаюцца адкрытымі. Іх заказы на продаж тады лічаць і крыніцу, і новы рахунак выстаўленымі – праца выглядае двойчы выстаўленай, пакуль хтосьці не ачысціць чарнавікі ўручную. Прызначана для рэдкага выпадку, калі перад рашэннем хочацца параўнаць. Тэст фіксуе менавіта гэты двайны ўлік, каб ён нікога не здзівіў.
Межы – чаго модуль наўмысна не робіць
Калі ласка, прачытайце перад прадукцыйным выкарыстаннем:
- Толькі чарнавікі. Праведзеныя рахункі не аб’ядноўваюцца. Гэта задача для крэдытовых нот, а не працэс капіравання.
- Аднойчы праведзеныя, скінутыя назад чарнавікі адхіляюцца. Яны маюць нумар і гісторыю праводак; хаваць гэта ў іншым дакуменце – не тое, што павінна ціха рабіць групавое дзеянне.
- Ніякага аб’яднання паміж партнёрамі, валютамі, кампаніямі або журналамі. Для гэтага няма разумнага адзінага значэння, таму модуль адхіляе, а не здагадваецца.
- payment_reference не пераносіцца. Структураваныя плацёжныя рэферэнсы ідэнтыфікуюць дакумент; два злучаныя разам не падыходзяць нікуды ў банку. Новы рахунак атрымлівае ўласны пры праводцы.
- Пазіцыі авансавых плацяжоў капіруюцца, як і любая іншая пазіцыя. Калі крыніцы змяшчаюць радкі авансавых плацяжоў з sale, вынік трэба праверыць перад праводкай.
- Аўтаматычна перапрывязваецца толькі timesheet_invoice_id. Любая іншая мадэль з Many2one на account.move (уласныя цыклы выстаўлення рахункаў, спасылкі на падпіскі, перадачы EDI) па-ранейшаму паказвае на крыніцу. Для гэтага ёсць хук _merge_draft_relink.
- Аб’яднання аднолькавых пазіцый няма – наўмысна. Хто хоча кароткі, акуратны рахунак, той тут не па адрасе і заплаціў бы за гэта сувязямі з паходжаннем.
- Загалоўкі ствараюцца аднойчы пры аб’яднанні. Пасля гэтага яны – звычайныя радкі раздзелаў: рэдагуюцца, выдаляюцца і не абнаўляюцца, калі заказ або тыкет пазней пераназваць.
- Загаловак называе тыкеты ўсяго зыходнага рахунку, а не асобнай пазіцыі.
Усталёўка ў Odoo 19 Community
Усталёўка адбываецца як і для звычайнага дадатковага модуля Odoo:
- Скапіраваць тэчку модуля ў наладжаны шлях addons.
- Абнавіць спіс прыкладанняў у 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 – гэты helper часта не запускаецца на мігрыраваных прадукцыйных базах даных, а менавіта на такіх модуль і павінен працаваць.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Як і для любога пашырэння ў бухгалтарскай сферы, мы рэкамендуем спачатку праверыць устаноўку ў тэставым асяроддзі на копіі ўласнай базы даных.
Бясплатна і Open Source для супольнасці Odoo
Мы ў Biricon IT Services GmbH спачатку распрацоўваем рашэнні для канкрэтных патрабаванняў нашай уласнай працы. Калі з гэтага ўзнікае модуль, прыдатны для агульнага выкарыстання, мы не хочам хаваць яго ў адной устаноўцы.
Таму Merge Draft Invoices даступны бясплатна як Open-Source-праект. Зыходны код можна праглядаць, правяраць, адаптаваць і развіваць у рамках ліцэнзіі AGPL-3.
Нашай мэтай быў не як мага карацейшы рахунак, а рахунак, у якім і праз год кожная пазіцыя вядзе назад да свайго заказу, свайго тыкета і свайго запісу часу.
Праект, зыходны код і ўдзел
Поўны зыходны код, ўказанні па ўстаноўцы і тэхнічная дакументацыя даступныя на GitHub:
Адкрыць Odoo 19 Merge Draft Invoices на GitHub
Мы будзем рады практычнаму досведу, справаздачам пра памылкі, прапановам па паляпшэнні і ўкладу з боку супольнасці Odoo.
Бясплатна. Зразумела. Open Source. Для Odoo 19 Community.