Перейти к содержимому

Odoo 19 Community: объединение черновиков счетов – наш Open-Source-модуль Merge Draft Invoices

18 сентября 2026 г. от
Odoo 19 Community: объединение черновиков счетов – наш Open-Source-модуль 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 Час техника            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 или отображаемое имя модели — никогда с 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

Как и для любого расширения в области бухгалтерии, мы рекомендуем сначала проверить установку в тестовой среде на копии собственной базы данных.

Бесплатно и Open Source для сообщества Odoo

Мы в Biricon IT Services GmbH сначала разрабатываем решения для конкретных потребностей нашей собственной работы. Если в результате получается универсально применимый модуль, мы не хотим прятать его в одной отдельной установке.

Поэтому Merge Draft Invoices доступен бесплатно как проект с открытым исходным кодом. Исходный код можно просматривать, проверять, адаптировать и развивать в рамках лицензии AGPL-3.

Нашей целью был не как можно более короткий счёт, а счёт, в котором и через год каждая позиция ведёт обратно к своему заказу, своему тикету и своей записи времени.

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

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

Открыть Odoo 19 Merge Draft Invoices на GitHub

Мы будем рады практическому опыту, сообщениям об ошибках, предложениям по улучшению и вкладу сообщества Odoo.

Бесплатно. Прозрачно. Open Source. Для Odoo 19 Community.