Одним из наиболее практичных инструментов, предоставляемых Odoo для упрощения выбора товаров в рамках бизнес-процессов, является каталог товаров. В Odoo 19 это будет намного проще, быстрее и гибче, что позволит пользователям легче получать продукты, фильтровать их по атрибутам и включать в форму - это могут быть заказы на продажу, запросы или любая другая пользовательская модель.
Добавление функции каталога товаров в Odoo 19 позволяет пользователям быстрее и нагляднее выбирать товары внутри документов. Вместо того, чтобы выбирать товары из длинных выпадающих списков, пользователи могут открыть специальный каталог, в котором товары отображаются в виде карточек с изображениями, ценами и возможностью быстрого контроля количества. Эта функция уже доступна в стандартных приложениях, таких как Sales, а Odoo 19 позволяет разработчикам повторно использовать те же возможности в пользовательских модулях.
Чтобы понять, как работает каталог, давайте сначала рассмотрим знакомый пример с экрана заказа на продажу.

В заказе на продажу при нажатии кнопки Каталог открывается таблица товаров в режиме Канбан. Здесь пользователи могут:
- Просматривать товары визуально
- Выполнять поиск по названию или внутренней ссылке
- Мгновенно увеличивать или уменьшать количество
- Добавлять или удалять товары, не закрывая каталог

Кнопка “Вернуться к предложению” позволяет пользователям вернуться к форме заказа на продажу после завершения выбора. Товары, добавленные из каталога, сразу же отображаются в строках заказа с выбранными количествами.
Такое же поведение каталога может быть реализовано в любой пользовательской модели в Odoo 19 с помощью соответствующих микшеров и методов, которые мы рассмотрим далее.
Шаг 1: Включите поддержку каталога с помощью product.catalog.mixin
Чтобы включить функциональность каталога в пользовательскую модель, модель должна наследовать product.catalog.mixin.
Этот микшер предоставляет всю внутреннюю логику, необходимую для работы с каталогом, такую как открытие каталога, обработка выбранных продуктов и синхронизация количества.
from odoo import models, fields, api
class WarrantyRequest(models.Model):
_name = 'warranty.request'
_description = 'Warranty Request Model'
_inherit = ['mail.thread', 'mail.activity.mixin', 'product.catalog.mixin']
Как только это соединение будет добавлено, модель станет совместимой с интерфейсом каталога Odoo.
Шаг 2: Добавьте кнопку каталога в XML-представление
Пользователям необходимо выполнить видимое действие, чтобы открыть каталог. Это делается путем размещения кнопки каталога в разделе <control> в режиме просмотра списка one2many. Расположение кнопки здесь гарантирует, что она будет отображаться рядом со стандартными линейными действиями. При нажатии на него открывается представление каталога и идентификатор активной записи передается через контекст, позволяя Odoo правильно связать выбранные продукты с документом.
<button name="action_add_from_catalog" type="object"
string="Catalog" class="px-4 btn-link"
context="{'order_id': parent.id}"/>
Окончательный XML-код выглядит следующим образом,
<notebook>
<page id="product" name="Product">
<field name="warranty_line_ids" mode="list"
widget="section_and_note_one2many"
context="{'default_display_type': False}">
<list editable='bottom'>
<control>
<create name="add_product_control" string="Add a product"/>
<create name="add_section_control"
string="Add a section"
context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control"
string="Add a note"
context="{'default_display_type': 'line_note'}"/>
<button name="action_add_from_catalog" type="object"
string="Catalog" class="px-4 btn-link"
context="{'order_id': parent.id}"/>
</control>
<field name="product_id" required="not display_type"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
<field name="quantity" required="not display_type"/>
<field name="uom_id"/>
<field name="lst_price"/>
<field name="warranty_charge" sum="Warranty Charge"/>
</list>
</field>
</page>
</notebook>
Шаг 3: Перенаправьте действие Catalog из дочерней модели
Действие catalog выполняется на родительском уровне, поэтому дочерняя модель должна правильно перенаправить запрос.В модели warranty.request.line определите метод action_add_from_catalog. Этот метод извлекает родительскую запись warranty.request, используя значение order_id из контекста, а затем запускает действие каталога из родительской model.from.
def action_add_from_catalog(self):
"""Trigger catalog action from child model."""
warranty_request = self.env['warranty.request'].browse(
self.env.context.get('order_id')
)
return warranty_request.action_add_from_catalog()
Это гарантирует, что каталог всегда взаимодействует с правильной записью, даже при доступе из строки one2many.
Шаг 4: Добавьте метод _get_product_catalog_order_data в родительскую модель
В модели warranty.request определите метод _get_product_catalog_order_data. Этот метод подготавливает информацию о продукте, которая будет отображаться в представлении каталога.
def _get_product_catalog_order_data(self, products, **kwargs):
"""Retrieve product details for the catalog."""
res = super()._get_product_catalog_order_data(products, **kwargs)
for product in products:
res[product.id] |= {
'price': product.standard_price,
}
return res
Расширяя базовую реализацию и вызывая функцию super(), вы сохраняете поведение каталога по умолчанию, позволяя при этом включать в отображение каталога дополнительные сведения о продукте, такие как цены.

Шаг 5: Отобразите уже добавленные продукты в представлении каталога
Чтобы отразить продукты, которые уже добавлены в документ, методы должны быть реализованы как в родительской, так и в дочерней моделях.
а) Сгруппируйте существующие строки в родительской модели
В модели warranty.request определите метод _get_product_catalog_record_lines. Этот метод группирует существующие строки запроса гарантии по продуктам, гарантируя, что продукты, уже присутствующие в поле one2many, будут распознаны каталогом. Он возвращает словарь, группирующий записи по идентификатору продукта.
def _get_product_catalog_record_lines(self, product_ids, child_field=False, **kwargs):
"""Group warranty request lines by product for catalog view."""
grouped_lines = defaultdict(lambda: self.env['warranty.request.line'])
for line in self.warranty_line_ids:
if line.product_id.id not in product_ids:
continue
grouped_lines[line.product_id] |= line
return grouped_lines
b) Укажите сведения о строке в дочерней модели
В запросе на гарантию.в модели строки укажите метод _get_product_catalog_lines_data. Этот метод предоставляет информацию о количестве и ценах для каждой линейки продуктов. Если продукт еще не был добавлен, количество по умолчанию равно нулю.
def _get_product_catalog_lines_data(self, **kwargs):
"""Provide details for warranty request lines in catalog view."""
self.ensure_one()
return {
'quantity': self.quantity or 0,
'price': self.product_id.standard_price,
'uomDisplayName': self.uom_id.name if self.uom_id else '',
'readOnly': False,
}
В представлении каталога теперь будут отображаться товары, уже добавленные в строку запроса на гарантийное обслуживание. Пользователи могут удалять или добавлять товары непосредственно из представления каталога.

Шаг 6: Обновите гарантийные строки в представлении каталога
В модели warranty.request определите метод _update_order_line_info. Этот метод обрабатывает обновления, поступающие из представления каталога, всякий раз, когда пользователь добавляет продукт, изменяет его количество или удаляет его, установив количество равным нулю.
def _update_order_line_info(self, product_id, quantity, **kwargs):
"""
Update warranty request line information for a given product.
:param int product_id: The product ID (`product.product`).
:param float quantity: The quantity to update.
:return: The updated warranty request line or None if removed.
"""
self.ensure_one()
warranty_line = self.warranty_line_ids.filtered(
lambda line: line.product_id.id == product_id
)
if warranty_line:
if quantity != 0:
warranty_line.quantity = quantity
else:
warranty_line.unlink()
elif quantity > 0:
product = self.env['product.product'].browse(product_id)
warranty_line = self.env['warranty.request.line'].create({
'warranty_id': self.id,
'product_id': product_id,
'name': product.display_name,
'quantity': quantity,
})
return warranty_line

Этот метод гарантирует, что строки one2many обновляются, создаются или удаляются соответствующим образом, сохраняя полную синхронизацию каталога и представления формы.

Как вы можете видеть из приведенного выше примера, товары, выбранные из каталога, отображаются в гарантийных строках с указанием их количества; все синхронизируется правильно, и если пользователь установит количество равным 0, товар будет удален из гарантийных строк.
Шаг 7: Отфильтруйте товары, отображаемые в каталоге.
В модели warranty.request определите метод _get_product_catalog_domain. Этот метод ограничивает количество продуктов, отображаемых в каталоге, в зависимости от конкретных условий, таких как продукты sale_ok или компания.
def _get_product_catalog_domain(self):
"""Define the product filter for the catalog."""
return [
('company_id', 'in', [self.company_id.id, False]),
('sale_ok', '=', True), # Show products that can be sold
]
Как вы можете видеть в примере ниже, все перечисленные здесь товары являются товарами с включенной функцией sale_ok.

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