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

Odoo 19 Community: наборы продуктов в коммерческом предложении как раздел – наш модуль с открытым исходным кодом Sale Product Set Layout

2 ноября 2026 | Odoo 19 Community | Собственная разработка
2 ноября 2026 г. от
Odoo 19 Community: наборы продуктов в коммерческом предложении как раздел – наш модуль с открытым исходным кодом Sale Product Set Layout

Модуль Odoo 19 Sale Product Set Layout: наборы продуктов как раздел коммерческого предложения с режимом печати, скрытыми строками и фиксированной ценой

Тот, кто продаёт в Odoo комплекты, например измерительный прибор с принадлежностями и монтажом, рабочее место с оборудованием и настройкой или пакет обслуживания из нескольких услуг, нуждается в коммерческом предложении сразу в двух вещах: состав должен быть указан в заказе, чтобы склад, технические специалисты и бухгалтерия знали, что именно поставляется. Однако в печатной форме для клиента не в каждом случае должна отображаться каждая отдельная позиция с каждой отдельной ценой.

Модуль OCA sale_product_set позволяет одним щелчком вставлять в коммерческое предложение наборы продуктов (спецификации для продажи). Внешний вид печатной формы он не определяет. Для этого мы разработали модуль Sale Product Set Layout (sale_product_set_layout). Он написан для Odoo 19 и бесплатно доступен на GitHub как свободное программное обеспечение под лицензией AGPL-3:

github.com/Alex0176/odoo-sale-product-set-layout

Проблема: наборы продуктов не влияют на вид печатной формы

Набор продуктов в Odoo — это заранее подготовленный список продуктов с количествами. Вместо того чтобы искать десять товаров по отдельности, в коммерческом предложении выбирают Добавить набор и сразу получают все строки. Это экономит время и исключает забытые позиции. Именно это делает модуль sale_product_set из репозитория OCA/sale-workflow ; сами наборы берутся из product_set в репозитории OCA/product-attribute.

На практике остаются три пробела:

  • Нет заметной группировки. Строки набора попадают в коммерческое предложение как отдельные позиции. После вставки уже не видно, какие строки относятся друг к другу, а общая сумма набора нигде не указана.
  • Нет влияния на печатную форму. То клиент должен видеть каждую позицию с ценой, то только цену комплекта, то всего одну строку. Это решение для каждого набора или каждого клиента, а не то, что нужно заново делать вручную в каждом коммерческом предложении.
  • Корректировки цены выглядят неудачно. Если у комплекта должна быть фиксированная цена, отличающаяся от суммы цен по прайс-листу, требуется корректирующая строка. Она относится к расчёту, но необязательно к документу, который получает клиент.

В Odoo 19 есть все необходимые элементы для чистого решения: разделы и подразделы в строках коммерческого предложения с параметрами Скрыть цены и Скрыть состав (поля collapse_prices и collapse_composition). Модуль связывает набор продуктов с этими стандартными параметрами, вместо того чтобы изобретать собственный формат печати.

Что делает модуль

  1. Вставка набора как раздела. Мастер Добавить набор создаёт строку раздела с названием набора, например 2 × Измерительный комплект Standard, и размещает под ней строки набора. Разделы внутри набора становятся подразделами.
  2. Режим печати для каждого набора. В наборе один раз задаётся, как он будет печататься. При вставке режим можно изменить в мастере для этого конкретного случая.
  3. Скрытие строк. Отдельные строки можно пометить как Не печатать (в наборе: Скрыто). Они учитываются в сумме, но не печатаются и не отображаются в клиентском портале. Признак переходит из коммерческого предложения в счёт.
  4. Фиксированная цена для строки набора. Строка набора может содержать фиксированную цену за единицу, которая заменяет цену из прайс-листа, например для скрытой корректировки цены.
Без модуля (только sale_product_set)С sale_product_set_layout
Строки набора как отдельные позицииСтрока раздела с названием набора, количество в названии, строки ниже
Разделы в наборе остаются разделами и разрушают структуруРазделы в наборе становятся подразделами (line_subsection)
Вид печатной формы всегда одинаковРежим печати для каждого набора, можно изменить при вставке
Корректировка цены видна в коммерческом предложении и счётеСтроку можно скрыть, но она учитывается в сумме
Цена только из прайс-листаДополнительно: фиксированная цена для строки набора
Оформление заканчивается на коммерческом предложенииОформление и скрытие действуют для коммерческого предложения, заказа, счёта и клиентского портала

Три режима печати подробно

Режим печати — это поле выбора print_mode в наборе продуктов. При вставке он преобразуется в два стандартных переключателя строки раздела. Для этого в модуле есть единственное, легко читаемое соответствие:

# Режим печати -> (collapse_prices, collapse_composition) строки раздела
PRINT_MODE_FLAGS = {
    "detailed":   (False, False),
    "total_only": (True,  False),
    "summary":    (False, True),
}

Три вида печатной формы одного и того же набора продуктов: каждая строка с ценой, только общая цена, одна-единственная строка

Режим (print_mode)Отображение в набореСтандартный параметр в OdooВид печатной формы
detailedКаждая строка с ценойнетРаздел, все строки с суммой
total_onlyСтроки без цен, только итог по наборуcollapse_prices (Скрыть цены)Раздел с итогом, строки только с названием и количеством
summaryТолько одна строка с итогом по наборуcollapse_composition (Скрыть состав)Только строка раздела с итогом

Поскольку модуль использует стандартные параметры Odoo 19, а не собственный формат печати, коммерческое предложение, подтверждение заказа, счёт и клиентский портал работают без доработанных шаблонов отчётов. Тот, у кого есть собственные шаблоны отчётов, учитывающие параметры разделов, получит такое же поведение.

Скрытые строки и фиксированная цена

Частый случай: у комплекта есть цена комплекта, которая ниже суммы цен отдельных позиций. Вместо того чтобы искажать отдельные цены, добавляют корректирующую строку с фиксированной ценой (например, −50,00 € за набор) и скрывают её. В бэкенде счёт остаётся прозрачным, а в печатной форме указана цена комплекта.

Корректировка цены видна и помечена в бэкенде, но не видна в печатной форме коммерческого предложения, в PDF, счёте и портале, итог идентичен

Важно для понимания: признак hide_in_report ничего не меняет в расчёте. Количество, цена, скидка, налог, суммы и проводки формируются стандартным Odoo без изменений. Строка отсутствует только в печатной форме и в портале.

  • В наборе продуктов: строка набора с признаком Скрыто и, при необходимости, Фиксированная цена вместе с Ценой за единицу.
  • В строках коммерческих предложений и счетов: столбец Не печатать (hide_in_report), в том числе для обычных строк без набора.
  • Передача: при создании счёта из заказа признак переносится в строку счёта. При копировании коммерческого предложения он сохраняется.

Техническая реализация

Модуль намеренно небольшой: он расширяет существующие модели и переопределяет только те места, где Odoo решает, какие строки печатать и как вставлять набор. Новых шаблонов отчётов, новых моделей и вмешательства в SQL нет.

Поток данных: набор продуктов, мастер «Добавить набор», строки коммерческого предложения, строки счёта, PDF и клиентский портал

Расширенные модели и поля

МодельДополнение
product.setПоля insert_as_section (Boolean, по умолчанию включено) и print_mode (выбор, обязательное поле, по умолчанию detailed)
product.set.lineПоля hide_in_report, fixed_price, price_unit; prepare_sale_order_line_values() передаёт их в строку коммерческого предложения
sale.product.set.wizardПоля insert_as_section, section_name, print_mode; _prepare_order_lines() формирует строку раздела
sale.order / sale.order.lineПоле hide_in_report (с copy=True); _get_order_lines_to_report() пропускает скрытые строки; _prepare_invoice_line() передаёт признак дальше
account.move / account.move.lineПоле hide_in_report; _get_move_lines_to_report(), _get_detailed_lines() и _get_subsection_lines() отфильтровывают скрытые строки; _get_aml_vals() переносит признак в данные отчёта

Скрытие в коммерческом предложении

Odoo 19 определяет печатаемые строки коммерческого предложения в _get_order_lines_to_report(). Модуль добавляет там фильтр. Вмешательство в шаблон отчёта не требуется: и шаблон отчёта коммерческого предложения (sale/report/ir_actions_report_templates.xml), и шаблон портала (sale/views/sale_portal_templates.xml) вызывают этот метод:

class SaleOrder(models.Model):
    _inherit = "sale.order"

    def _get_order_lines_to_report(self):
        return super()._get_order_lines_to_report().filtered(
            lambda line: not line.hide_in_report
        )

Передача в счёт

При создании строки счёта из строки коммерческого предложения (_prepare_invoice_line()) признак копируется. На стороне счёта в Odoo 19 есть несколько путей, по которым строки попадают в отчёт: основной список, строки раздела и строки подраздела. Поэтому модуль фильтрует в нескольких местах (_get_move_lines_to_report() в документе, _get_detailed_lines() и _get_subsection_lines() в строке), иначе скрытая строка могла бы снова появиться по одному из других путей:

class AccountMoveLine(models.Model):
    _inherit = "account.move.line"

    hide_in_report = fields.Boolean(string="Hidden in report")

    def _get_aml_vals(self, hide_taxes):
        res = super()._get_aml_vals(hide_taxes)
        res["hide_in_report"] = self.hide_in_report
        return res

    def _get_detailed_lines(self, children_lines, direct_children_lines, subsection_lines):
        res = super()._get_detailed_lines(children_lines, direct_children_lines, subsection_lines)
        return [vals for vals in res if not vals.get("hide_in_report")]

    def _get_subsection_lines(self, subsection_line, children_lines):
        res = super()._get_subsection_lines(subsection_line, children_lines)
        return [vals for vals in res if not vals.get("hide_in_report")]

Мастер: раздел вместо отдельных строк

Мастер sale.product.set.wizard из sale_product_set создаёт строки коммерческого предложения в _prepare_order_lines(). Модуль переопределяет этот метод. Если параметр Вставлять как раздел выключен, он просто вызывает исходный метод, и поведение остаётся таким же, как в модуле OCA. В противном случае сначала создаётся строка раздела с двумя стандартными переключателями, затем следуют строки набора. Разделы внутри набора становятся подразделами, чтобы не разрушать основную структуру:

def _prepare_order_lines(self):
    if not self.insert_as_section:
        return super()._prepare_order_lines()
    sequence = self._get_max_sequence() + 1
    collapse_prices, collapse_composition = PRINT_MODE_FLAGS[self.print_mode or "detailed"]
    order_lines = [Command.create({
        "display_type": "line_section",
        "name": self.section_name or self.product_set_id.name,
        "sequence": sequence,
        "collapse_prices": collapse_prices,
        "collapse_composition": collapse_composition,
    })]
    for set_line in self._get_lines():
        sequence += 1
        values = set_line.prepare_sale_order_line_values(self.order_id, self.quantity, sequence)
        if values.get("display_type") == "line_section":
            values["display_type"] = "line_subsection"
        order_lines.append(Command.create(values))
    return order_lines

Решения, которых не видно

  • Раздельные методы вычисления в мастере. У названия раздела, режима печати и параметра Вставлять как раздел есть по собственному методу compute (store=True, readonly=False). Если бы у всех трёх был один и тот же метод, явно переданное значение одного поля сбрасывало бы остальные. Это особенно заметно, когда мастер создаётся из кода или тестов со значениями.
  • Название раздела с количеством. При количестве, отличном от 1, оно указывается в названии (2 × Измерительный комплект Standard, с запятой в качестве десятичного разделителя), чтобы в печатной форме было видно, что это два набора, а не один набор с удвоенным количеством.
  • Фиксированная цена только в строках продуктов. prepare_sale_order_line_values() устанавливает hide_in_report и price_unit только в том случае, если строка набора не является строкой раздела или примечания.
  • Стандартный Odoo вместо самодельного решения. Режим печати и скрытие используют существующие хуки и поля Odoo 19. Это делает обновления управляемыми: если Odoo изменит шаблон отчёта, модуль останется работоспособным, пока существуют эти методы.

Установка без нежелательных побочных эффектов

Требования: Odoo 19.0, sale_product_set (OCA/sale-workflow, ветка 19.0) и product_set (OCA/product-attribute, ветка 19.0). Сам модуль зависит от sale_management, sale_product_set и account.

Ловушка auto_install в модулях OCA. Многие репозитории OCA содержат модули с auto_install. Как только их зависимости имеются, Odoo устанавливает их без запроса при следующей установке или обновлении. Поэтому тот, кто помещает весь репозиторий OCA в addons_path рабочего экземпляра, может получить модули, которые никто не хотел устанавливать. В случае OCA/product-attribute это касается, например, purchase_product_template_tags. Наш порядок действий:

  1. Клонировать оба репозитория OCA поверхностно (git clone --depth 1 --branch 19.0).
  2. В отдельном каталоге связать символьными ссылками только два необходимых модуля и включить этот каталог в addons_path, а не репозитории целиком.
  3. Установить на тестовую базу данных и проверить, какие модули при этом получили статус установлен.
  4. Только после этого устанавливать в рабочей среде, с --workers=0, чтобы вызов не конкурировал с работающей службой за HTTP-порт.
odoo-bin -c /etc/odoo/odoo.conf -d <база_данных> -i sale_product_set_layout --stop-after-init

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

Работа с модулем шаг за шагом

  1. Создание набора: Продажи → Продукты → Наборы продуктов. Назвать набор, выбрать Вставлять как раздел и Режим печати, внести продукты с количествами. Корректирующим строкам задать Скрыто и Фиксированная цена.
  2. В коммерческом предложении: нажать Добавить набор, выбрать набор и количество. Название раздела и режим печати можно изменить перед вставкой.
  3. Скрытие отдельных строк: в коммерческом предложении или в счёте включить столбец Не печатать.
  4. Проверка: открыть предварительный просмотр или PDF. Такое же отображение видят клиенты в портале.

Тесты

В комплект модуля входят шесть автоматических тестов, которые мы запускаем на тестовой базе данных, прежде чем что-либо попадёт в рабочую среду:

  • Вставка как раздела, включая режим печати и подраздел
  • Скрытая строка отсутствует среди печатаемых строк
  • Режим Одна строка (summary)
  • Вставка без раздела, исходное поведение
  • Счёт наследует признак hide_in_report, строка отсутствует в _get_move_lines_to_report(), итог раздела по-прежнему включает её
  • Счёт в режиме Только общая цена не показывает скрытую строку в строках раздела, итог раздела остаётся полным
odoo-bin -d <тестовая_база> -i sale_product_set_layout \
         --test-enable --test-tags=/sale_product_set_layout --stop-after-init

Немецкий перевод находится в i18n/de.po.

Ограничения и юридическое примечание

  • Только оформление. Модуль не меняет то, как Odoo рассчитывает цены, скидки или налоги. Он меняет лишь то, что печатается и отображается в портале.
  • Скрытые строки видны в бэкенде. Признак относится к печатной форме и порталу, а не к экранному представлению для сотрудников.
  • Проверьте реквизиты счёта. Скрытые строки учитываются в сумме, но не указываются в документе. Совместимо ли это с требованиями к счетам в вашей стране, например с обязательным описанием поставленных товаров или оказанных услуг, следует до применения в счетах выяснить с вашим налоговым консультантом. В коммерческих предложениях скрытие менее проблематично. Эта статья не является юридической консультацией.
  • Протестировано для Odoo 19. Мы разработали и протестировали модуль для Odoo 19.0 Community. Он использует параметры разделов, которые в таком виде имеются в Odoo 19. Для других версий мы его не проверяли.

Часто задаваемые вопросы

Зачем мне этот модуль, если Odoo 19 уже умеет работать с разделами?

Параметры разделов есть в самом Odoo. Не хватало связи с набором продуктов: набор должен автоматически вставляться как раздел с режимом печати, заданным в наборе. Без модуля после каждой вставки приходится вручную создавать и настраивать строку раздела.

Чем отличаются «Скрыть цены» и «Скрыть состав»?

«Скрыть цены» (collapse_prices) оставляет строки, но не показывает суммы, только итог по разделу. «Скрыть состав» (collapse_composition) полностью скрывает строки, клиент видит только раздел с итогом.

Учитывается ли скрытая строка в сумме счёта?

Да. Скрытые строки остаются в сумме, налоге и проводке. Признак относится только к отображению в печатной форме и клиентском портале.

Могу ли я изменить режим печати при вставке?

Да. Режим берётся из набора как значение по умолчанию, в мастере его можно изменить для конкретной вставки. Сам набор при этом остаётся без изменений.

Что произойдёт, если я выключу Вставлять как раздел?

Тогда мастер ведёт себя так же, как в модуле OCA sale_product_set: строки набора вставляются без раздела.

Устанавливает ли модуль неожиданно другие модули?

Сам модуль — нет. Но в репозиториях OCA могут встречаться модули с auto_install, если помещать целые репозитории в addons_path. Поэтому связывайте символьными ссылками только два необходимых модуля (см. раздел «Установка»).

Работает ли модуль с Odoo Enterprise?

Модуль зависит только от модулей Community (sale_management, account) и двух модулей OCA. Мы используем его на Odoo 19 Community. На Enterprise мы его не тестировали.

Под какой лицензией распространяется модуль?

Под AGPL-3. Вы можете использовать, изменять и распространять его при условии, что изменения остаются под той же лицензией. Сообщения об ошибках и предложения по улучшению приветствуются в виде issue в репозитории GitHub .

Поддержка от Biricon IT Services

Мы обслуживаем установки Odoo для малых и средних предприятий: от внедрения через доработки и миграции до постоянной эксплуатации. Если вы хотите адаптировать наборы продуктов, шаблоны коммерческих предложений или отчёты в Odoo 19 под ваши процессы либо проверить подобный модуль и безопасно внедрить его в рабочий экземпляр, мы охотно поддержим вас. Другие наши собственные разработки вы найдёте в нашем блоге, например Объединение черновиков счетов и Автоматическое сопоставление банковских платежей со счетами.

Связаться с нами

По состоянию на 2 ноября 2026 г. Версия модуля 19.0.1.0.0. Примеры данных на иллюстрациях выбраны произвольно, суммы без НДС.