Skip to Content

Odoo 19 Community: об'єднання чернеток рахунків – наш модуль з відкритим кодом Merge Draft Invoices

18 вересня 2026 р. від
Odoo 19 Community: об'єднання чернеток рахунків – наш модуль з відкритим кодом Merge Draft Invoices
Alexander Biringer | Biricon IT Services GmbH

Хто виставляє рахунки в Odoo на підставі замовлень, тікетів helpdesk та обліку часу, знає результат: наприкінці місяця клієнт отримує не один рахунок, а п'ять. Кожен походить з іншого замовлення чи тікета, кожен по суті правильний – і все ж клієнт питає, чи не вмістилося б усе в одному документі.

Саме для цього в Odoo 19 Community немає чистого рішення. Відомий модуль OCA account_invoice_merge для Odoo 19 ще не випущений, і він вирішує проблему способом, який для надавачів послуг становить серйозний ризик: однакові позиції об'єднуються, і разом із цим втрачається зв'язок із замовленням, завданням і зафіксованим часом.

Тому для нашої власної інсталяції Odoo 19 Community ми розробили модуль Merge Draft Invoices. Оскільки ця проблема стосується не лише нас, ми безкоштовно надаємо його на GitHub під вільною ліцензією AGPL-3.

До проєкту на GitHub:

Odoo 19 Merge Draft Invoices

Чому власний модуль, а не рішення OCA?

OCA вже багато років підтримує account_invoice_merge у репозиторії OCA/account-invoicing. На момент написання цієї публікації міграція на 19.0 там усе ще є відкритим pull request, а не випущеним модулем.

Важливіше, однак, те, що ці два модулі роблять різні речі:


OCA account_invoice_mergeMerge 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)переключається на новий рахунок, перш ніж джерела стануть недійсними
Тікет helpdeskприв'язаний до обліку часу і тому автоматично вказує на новий рахунок
Chatterповідомлення на новому рахунку з усіма джерелами, повідомлення на кожному джерелі з посиланням на новий
merged_into_move_id / merged_from_move_idsзбережений зв'язок в обох напрямках

Рядки податків, рядки умов оплати, рядки округлення та знижок за дострокову оплату не копіюються, а Odoo перераховує їх заново на основі вмісту. Тому об'єднаний рахунок має один рядок податку на ставку, а не по одному на джерело. Він створюється як чернетка без номера; саме об'єднання не витрачає номера документа.

Заголовки: клієнт бачить, звідки походить кожна позиція

Це функція, яка в повсякденній роботі дає різницю. Над позиціями кожного вихідного рахунку модуль вставляє рядок розділу – той самий елемент, який Odoo сам використовує для розділу «Передоплати»:

BE10181                                     <- заголовок
  22,500 x Technikerstunde        3.577,50
                    Zwischensumme 3.577,50
BE10182 · Ticket: HT00219                   <- заголовок
   3,000 x Technikerstunde          477,00
                    Zwischensumme   477,00
                      NETTOBETRAG 4.054,50

Заголовок друкується на документі, який отримує клієнт, а стандартний звіт рахунку Odoo самостійно додає проміжну суму для кожного розділу. Так клієнт бачить також, скільки коштувала кожна частина.

Підпис складається з того, що справді є в джерелі:

  1. замовлення, безпосередньо із зв'язків позицій – назви тож точні; якщо зв'язків із замовленнями немає, використовується вільний текст з invoice_origin
  2. тікети, визначені через облік часу – виводяться як Ticket: HT00219 або Tickets: …, обмежено п'ятьма плюс «і ще N»
  3. якщо немає ні того, ні іншого: номер вихідного рахунку, потім його референція
  4. якщо немає взагалі нічого: нейтральне «Інші позиції», щоб рядки одного джерела ніколи не зникали непомітно в блоці вище

Те, чого бракує, пропускається, а не вигадується. Тікет друкується з власним номером – номер 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:

  1. Скопіювати каталог модуля в налаштований шлях аддонів.
  2. Оновити список застосунків в Odoo.
  3. Встановити модуль account_invoice_merge_draft.

Модуль залежить лише від account. Усе, що він знає про sale, purchase, project, hr_timesheet, sale_timesheet чи модуль helpdesk, визначається під час виконання – він встановлюється і працює навіть на суто бухгалтерській базі даних.

Після цього: Бухгалтерія → Клієнти → Рахунки (або Постачальники → Рахунки), позначити дві або більше чернеток, меню із зубчастим колесом → Об'єднати чернетки рахунків. Дія доступна всім користувачам із групою Виставлення рахунків / Фактурування.

Хто оновлюється з попередньої версії модуля, має один раз виконати оновлення з -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, і поле однаково переноситься під час об'єднання, скасування та дублювання. Для інших модулів helpdesk або іншого формулювання заголовків існують _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 Community

У Biricon IT Services GmbH ми спочатку розробляємо рішення для конкретних вимог нашого власного бізнесу. Якщо з цього виходить модуль загального призначення, ми не хочемо ховати його в одній окремій інсталяції.

Тому Merge Draft Invoices доступний безкоштовно як проєкт із відкритим кодом. Вихідний код можна переглядати, перевіряти, адаптувати й розвивати в межах ліцензії AGPL-3.

Наша мета – не найкоротший можливий рахунок, а рахунок, у якому й через рік кожна позиція веде назад до свого замовлення, свого тікета та свого запису часу.

Проєкт, вихідний код і участь

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

Відкрити Odoo 19 Merge Draft Invoices на GitHub

Ми будемо раді практичному досвіду, повідомленням про помилки, пропозиціям щодо покращень і внескам від спільноти Odoo Community.

Безкоштовно. Зрозуміло. Відкритий код. Для Odoo 19 Community.