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

Торговые предложения изнутри - хранение, типы, выборка, корзина

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

Механика

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

Связь двух инфоблоков хранит не свойство, а запись торгового каталога у инфоблока предложений. В этой записи заполнены идентификатор инфоблока товаров и идентификатор свойства привязки конкретного проекта.

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

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

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

Цена и остаток принадлежат предложению, а не родительской карточке товара. Витрина показывает минимальную цену среди вариантов, поэтому пустая цена у родителя - это норма, а не поломка обмена.

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

Выбор варианта в карточке строится не по всем свойствам предложения. Годятся только немножественные свойства типов «список», «справочник» и «привязка к элементам», остальные покупателю выбирать нечем.

Свойства предложений участвуют в умном фильтре через фасетный индекс. Фильтр по цвету находит родительскую карточку, хотя цвет заведён у варианта, и это работает только при собранном индексе.

В корзину и в заказ попадает именно предложение, а не карточка товара. Родителя приходится доставать отдельным вызовом, когда в письме или в отчёте нужно название исходного товара.

Шаги

  1. Определить тип каталога у инфоблока и понять его роль в связке.
  2. Прочитать запись торгового каталога и взять из неё свойство привязки.
  3. Посмотреть тип товарной записи у родителя и у конкретного варианта.
  4. Получить предложения товара штатным вызовом и сверить их цены с остатками.
  5. Проверить, что в корзину уехало предложение, и найти его родителя.

Код

Определяем тип каталога у инфоблока:

\Bitrix\Main\Loader::includeModule('catalog');
$info = \CCatalogSku::GetInfoByIBlock($iblockId);
printf("тип каталога: %s\n", $info['CATALOG_TYPE'] ?? 'не каталог');
// D - обычный каталог, P - товары с вариантами, O - инфоблок предложений, X - оба сразу
// пустой результат означает, что инфоблок вообще не подключён к торговому каталогу

Буква типа отвечает на главный вопрос до всякой выборки. Код, который ждёт варианты от обычного каталога, получает пустой массив и молча показывает карточку без выбора.

Читаем связку двух инфоблоков напрямую:

$link = \Bitrix\Catalog\CatalogIblockTable::getRow([
'filter' => ['=IBLOCK_ID' => $offerIblockId],
'select' => ['IBLOCK_ID', 'PRODUCT_IBLOCK_ID', 'SKU_PROPERTY_ID'],
]);
print_r($link); // PRODUCT_IBLOCK_ID - инфоблок товаров, SKU_PROPERTY_ID - свойство привязки
// оба поля заполняются только вместе: у обычного каталога здесь нули

Идентификатор свойства привязки свой на каждом стенде и меняется при пересоздании инфоблока. Поэтому его читают из связки, а не зашивают числом в код проекта.

Смотрим тип товарной записи:

$row = \Bitrix\Catalog\ProductTable::getRow(['filter' => ['=ID' => $elementId],
'select' => ['ID', 'TYPE', 'AVAILABLE', 'QUANTITY', 'QUANTITY_TRACE', 'CAN_BUY_ZERO']]);
print_r($row);
// TYPE: 3 - родитель с вариантами, 4 - предложение, 5 - предложение без родителя,
// 6 - родитель без вариантов, 1 - простой товар, 7 - услуга

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

Получаем предложения товара штатным вызовом:

$offers = \CCatalogSKU::getOffersList($productId, 0,
['ACTIVE' => 'Y'], ['ID', 'NAME', 'QUANTITY'], ['ACTIVE' => 'Y']);
print_r($offers[$productId] ?? []); // результат сгруппирован по идентификатору родителя
// второй аргумент 0 - автоопределение инфоблока предложений по товару

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

Смотрим, где на самом деле лежит цена:

$prices = \Bitrix\Catalog\PriceTable::getList(['filter' => ['=PRODUCT_ID' => $offerId],
'select' => ['CATALOG_GROUP_ID', 'PRICE', 'CURRENCY']])->fetchAll();
printf("цен у варианта: %d, у родителя: %d\n", count($prices),
\Bitrix\Catalog\PriceTable::getCount(['=PRODUCT_ID' => $productId]));
// у родителя ноль цен - это штатное состояние товара с вариантами

Проверяем, что уехало в корзину:

$item = $basket->getItemById($basketItemId);
$offerId = $item->getProductId(); // это идентификатор предложения
$parent = \CCatalogSku::GetProductInfo($offerId, $offerIblockId);
printf("вариант %d, родитель %s\n", $offerId, $parent ? $parent['ID'] : 'потерян');
// название позиции корзины хранится копией и после правки товара не меняется

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

Обновляем фасетный индекс после правки свойств варианта:

\Bitrix\Iblock\PropertyIndex\Manager::updateElementIndex($offerIblockId, $offerId);
// индекс умного фильтра сам себя не пересчитывает после записи из кода
\Bitrix\Iblock\PropertyIndex\Manager::markAsInvalid($offerIblockId); // пометить весь индекс

Ограничения

Уровень вложенности ровно один. У предложения не бывает своих предложений, а сочетание «цвет плюс размер» задают двумя свойствами одного варианта.

Отбор покупателем идёт только по немножественным свойствам списков, справочников и привязок. Множественное свойство или строка в выбор варианта не превращаются, как бы этого ни хотелось маркетологу.

Родительская карточка не покупается. В корзину платформа пускает простые товары, комплекты, услуги и предложения, а родитель с вариантами туда не попадает вовсе.

Инфоблок предложений имеет собственные права доступа. Закрытый для группы инфоблок даёт пустой выбор в карточке, хотя сами варианты в базе активны и доступны.

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

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

Типичные проблемы

Код ищет варианты, а получает пустой массив.

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

После обмена часть предложений пропала из каталога.

У этих записей тип стал «предложение без родителя»: привязка указывает на несуществующий товар. Такие варианты ищут выборкой по типу товарной записи и чинят привязку.

Фильтр по цвету не находит товары после импорта.

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

В письме менеджеру приходит название варианта вместо товара.

Позиция заказа ссылается на предложение и хранит его название копией. Название родителя получают отдельным вызовом по идентификатору варианта.

Остаток изменили запросом, а товар остался недоступным.

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

Свойство завели у товара, а выбирать его нужно у варианта.

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

Частые вопросы

Как понять, что инфоблок - это предложения?

По типу каталога: у инфоблока предложений он равен букве O, а у инфоблока товаров с вариантами - букве P. Значение отдаёт штатный вызов по идентификатору инфоблока.

Почему у родителя нет цены?

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

Что означает тип товарной записи 5?

Это предложение без родителя: свойство привязки пустое или указывает на удалённый элемент. В каталоге такие варианты не показываются и не продаются.

Какой идентификатор лежит в корзине?

Идентификатор предложения, а не карточки товара. Родителя получают отдельным вызовом, когда нужно показать название исходного товара в отчёте.

Нужно ли пересобирать фасетный индекс руками?

Да, после записи свойств из кода: сам по себе индекс не обновляется. Для одного элемента вызывают обновление индекса, для массовой правки - пометку индекса недействительным.

Смежное

Первоисточник