Модуль catalog надстраивает над элементами инфоблока товарный слой: цены разных типов, остатки, единицы измерения, торговые предложения и статусы доступности. Правильная работа с ним идёт не через прямые запросы в таблицы, а через D7-модели и старое API — иначе легко порушить кэш и индексы.
Как устроен товарный слой
Товар в Битрикс — это элемент инфоблока (b_iblock_element), к которому модуль catalog добавляет запись в b_catalog_product. Именно поэтому нельзя создать товар одним вызовом CIBlockElement::Add() — после добавления элемента нужно зарегистрировать его как товар.
- Товар —
\Bitrix\Catalog\Model\Product(таблицаb_catalog_product): количество, тип, вес, длина/ширина/высота, флаги учёта. - Цена —
\Bitrix\Catalog\PriceTable(b_catalog_price): по одной строке на тип цены. - Тип цены —
\Bitrix\Catalog\GroupTable(b_catalog_group): «Розница», «Опт» и т. д. - Остатки по складам —
\Bitrix\Catalog\StoreProductTable.
Product::TYPE_PRODUCT (простой), TYPE_SKU (товар с предложениями), TYPE_OFFER (торговое предложение), TYPE_SET (комплект).Создание товара из кода
Сценарий из двух шагов: сначала элемент инфоблока, затем товарная запись. Не забудьте подключить модуль.
\Bitrix\Main\Loader::includeModule('catalog');
$el = new CIBlockElement();
$id = $el->Add([
'IBLOCK_ID' => 5,
'NAME' => 'Насос WILO',
'ACTIVE' => 'Y',
]);
$res = \Bitrix\Catalog\Model\Product::add([
'ID' => $id,
'QUANTITY' => 12,
'TYPE' => \Bitrix\Catalog\ProductTable::TYPE_PRODUCT,
'AVAILABLE' => 'Y',
'WEIGHT' => 4500,
]);
if (!$res->isSuccess()) {
$errors = $res->getErrorMessages();
}
Ключ массива — ID элемента инфоблока. Метод Product::add() возвращает \Bitrix\Main\ORM\Data\AddResult: всегда проверяйте isSuccess(). Для обновления количества и характеристик используйте Product::update($id, [...]).
catalog_store), итоговое количество пересчитывается модулем из остатков по складам, и прямая запись будет затёрта.Цены и типы цен
Каждая цена привязана к типу цены (price type / catalog group). Базовый тип создаётся при установке модуля, его ID удобно получать через \Bitrix\Catalog\GroupTable::getBasePrice() или по коду BASE.
Добавление и обновление цены через D7-модель PriceTable:
$price = \Bitrix\Catalog\Model\Price::add([
'PRODUCT_ID' => $id,
'CATALOG_GROUP_ID' => 1, // базовый тип
'PRICE' => 15900,
'CURRENCY' => 'RUB',
]);
Старое API тоже актуально и часто удобнее для upsert по типу цены:
CPrice::SetBasePrice($id, 15900, 'RUB');
// или произвольный тип:
CPrice::Add(['PRODUCT_ID' => $id, 'CATALOG_GROUP_ID' => 3, 'PRICE' => 13500, 'CURRENCY' => 'RUB']);
| Задача | Метод |
|---|---|
| Задать базовую цену | CPrice::SetBasePrice() |
| Добавить цену типа | CPrice::Add() / Price::add() |
| Получить список типов | GroupTable::getList() |
| Итоговая цена со скидками | \Bitrix\Catalog\Product\CatalogProvider / CCatalogProduct::GetOptimalPrice() |
Расчёт итоговой цены со скидками
Прайс в базе — это «сырая» цена без скидок и округлений. Чтобы получить цену, которую увидит покупатель, используйте CCatalogProduct::GetOptimalPrice(): метод учитывает тип цены группы пользователя, каталожные скидки и правила округления.
$optimal = CCatalogProduct::GetOptimalPrice(
$productId, // ID товара
1, // количество
$USER->GetUserGroupArray(),
'N',
[], [], $siteId
);
$final = $optimal['RESULT_PRICE']['DISCOUNT_PRICE'];
В D7-компонентах итоговые цены отдаёт провайдер \Bitrix\Catalog\Product\CatalogProvider, но для точечных расчётов на бэкенде GetOptimalPrice() остаётся рабочим инструментом.
RESULT_PRICE.BASE_PRICE— цена до скидки;RESULT_PRICE.DISCOUNT_PRICE— цена после скидок;DISCOUNT_LIST— какие скидки применились.
Остатки и складской учёт
При включённом складском учёте (catalog → настройки → «Использовать складской учёт») количество товара — сумма остатков по складам. Пишите не в товар, а в StoreProductTable:
\Bitrix\Catalog\StoreProductTable::add([
'PRODUCT_ID' => $id,
'STORE_ID' => 2,
'AMOUNT' => 30,
]);
// пересчёт итогового наличия:
CCatalogProduct::recalculateAvailableByStores($id);
Если складской учёт выключен, наличие определяется полем QUANTITY и порогом QUANTITY_TRACE. Флаг доступности AVAILABLE модуль пересчитывает сам — не выставляйте его вручную наугад.
Торговые предложения (SKU)
Товары с вариациями (цвет, размер) — это связка «мастер-товар» + «торговые предложения». Предложения лежат в отдельном инфоблоке, связанном с основным через привязку в настройках модуля.
- ID инфоблока предложений и поле связи получают через
CCatalogSKU::GetInfoByProductIBlock($iblockId); - по конкретному предложению можно найти мастер-товар:
CCatalogSKU::GetProductInfo($offerId, $offerIblockId); - тип мастер-товара —
Product::TYPE_SKU, предложения —TYPE_OFFER.
Цены и остатки у предложений — точно такие же записи в b_catalog_price и b_catalog_product, только PRODUCT_ID указывает на элемент инфоблока предложений. Это часто путают при интеграции с учётными системами.
События модуля catalog
Модуль генерирует события, на которые удобно вешать интеграционную логику — например, синхронизацию цен с внешней системой.
| Событие | Когда |
|---|---|
OnPriceAdd / OnPriceUpdate | изменение цены |
OnProductAdd / OnProductUpdate | изменение товарной записи |
OnBeforeCatalogStoreBarcodeAdd | перед добавлением штрихкода |
Регистрируется обработчик стандартно через EventManager в init.php. При этом помните: массовые импорты часто идут в обход событий (прямые ORM-операции), поэтому не стройте критичную логику только на них.
Итог
Работа с товарами и ценами в Битрикс — это всегда два слоя: элемент инфоблока и надстройка модуля catalog. Создание товара — CIBlockElement::Add() + Product::add(); цены — CPrice/Price с привязкой к типу; итоговая цена — GetOptimalPrice(); остатки — через склады с пересчётом наличия. Прямые запросы в таблицы ломают кэш и индексы, поэтому всё делаем через API.
Когда каталог связан с 1С, маркетплейсами или B2B-порталом, цена ошибки в этой логике измеряется деньгами. Мы в B2Bsite проектируем такие обмены с транзакционностью, логированием и корректным пересчётом остатков — от разовой доработки до отдельного модуля обмена.
Частые вопросы
Почему нельзя создать товар одним CIBlockElement::Add()?
Этот вызов создаёт только элемент инфоблока. Чтобы он стал товаром, нужна запись в b_catalog_product — её добавляет Product::add(). Без неё у элемента не будет цены, остатка и доступности.
Как задать базовую цену товара из кода?
Проще всего CPrice::SetBasePrice($productId, $value, 'RUB') — метод сам найдёт нужный тип цены и обновит или создаст запись. Для произвольного типа используйте CPrice::Add() с указанием CATALOG_GROUP_ID.
Чем отличается PRICE в базе от цены на витрине?
В b_catalog_price лежит цена без скидок и округления. Цену, которую видит покупатель, возвращает CCatalogProduct::GetOptimalPrice() с учётом группы пользователя, каталожных скидок и правил округления.
Куда писать остатки при складском учёте?
В StoreProductTable по каждому складу, а затем вызывать recalculateAvailableByStores(). Прямая запись в QUANTITY товара будет затёрта пересчётом из остатков складов.
Как найти торговые предложения товара?
Через CCatalogSKU::GetInfoByProductIBlock() получаете ID инфоблока предложений и поле связи, затем выбираете предложения обычным CIBlockElement::GetList по этому полю. Обратная связь — CCatalogSKU::GetProductInfo().
Стоит ли использовать D7-модели вместо старых CCatalogProduct/CPrice?
Для новых проектов предпочтительны D7-модели Product и Price: у них единый ORM-интерфейс и Result-объекты. Но старое API полностью поддерживается и в ряде задач (SetBasePrice, GetOptimalPrice) удобнее.
Как отловить изменение цены для синхронизации?
Подпишитесь на события OnPriceUpdate/OnPriceAdd модуля catalog через EventManager. Учтите, что массовые импорты нередко пишут цены напрямую через ORM в обход событий.
Нужно ли выставлять поле AVAILABLE вручную?
Нет. Флаг доступности модуль пересчитывает сам исходя из количества, порога и складов. Ручное значение может разойтись с фактическим наличием и сбить фильтрацию по наличию.
Поможем с настройкой и поддержкой Для разработчиков
Поможем с настройкой, доработкой и поддержкой Для разработчиков.