Skip to Content

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

# Druckmodus -> (collapse_prices, collapse_composition) der Abschnittszeile
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 <datenbank> -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 <testdatenbank> -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. Приклади даних на ілюстраціях обрано довільно, суми без податку.