Работа с товарами и торговыми предложениями
Товар каталога состоит из связанных данных: карточки в инфоблоке, товарной записи модуля catalog, цены и, при необходимости, торговых предложений. Карточка хранит название, код, активность и свойства. Товарная запись хранит тип товара, остаток и правила учета. Цена хранится отдельно и привязана к типу цены.
Базовый жизненный цикл товара включает создание простого товара, услуги, создание товара с торговыми предложениями, изменение карточки, товарных параметров и цены, а также удаление. Для товара без вариантов цена и остаток относятся к самому товару. Для товара с торговыми предложениями цена и остаток относятся к конкретному предложению, а родительская карточка хранит общее описание.
В примерах $productId обозначает простой товар или родительскую карточку, а $offerId — конкретное торговое предложение. Для простого товара цену и остаток меняют по $productId. Для товара с торговыми предложениями цену и остаток меняют по $offerId.
Значения в примерах демонстрационные: замените их на данные собственного проекта. Перед выполнением примеров подключите модули iblock и catalog и проверьте права на изменение инфоблоков и каталога.
Создать простой товар в каталоге
Создайте простой товар, если у него нет вариантов: одна карточка, одна цена, один остаток. Например, книга без размеров и цветов. Для такой структуры хватает одного инфоблока, который зарегистрирован как торговый каталог.
Создать инфоблок товаров
Создайте инфоблок с помощью CIBlock::Add(). Передайте в метод массив с параметрами:
-
IBLOCK_TYPE_ID— код существующего типа инфоблоков, например,catalog, -
NAME— название нового инфоблока, -
CODE— символьный код для обращения в коде, -
API_CODE— код для работы через ORM инфоблоков, код должен быть уникальным в рамках типа инфоблоков, -
ACTIVE— признак активности:YилиN, -
LID— массив кодов сайтов для привязки, в примере используется сайтs1.
$iblock = new \CIBlock;
$productIblockId = $iblock->Add([
'IBLOCK_TYPE_ID' => 'catalog',
'NAME' => 'Каталог товаров',
'CODE' => 'products',
'API_CODE' => 'Products',
'ACTIVE' => 'Y',
'LID' => ['s1'],
]);
if (!$productIblockId)
{
throw new \RuntimeException($iblock->getLastError()->getMessage());
}
Метод Add() возвращает идентификатор созданного инфоблока или false при ошибке. Сохраните идентификатор $productIblockId. Он понадобится на следующих шагах для регистрации каталога и добавления товаров.
Сделать инфоблок торговым каталогом
Обычный инфоблок хранит карточки, но не работает с ценами, остатками и другими возможностями торгового каталога. Чтобы элементы инфоблока стали товарами, зарегистрируйте инфоблок в модуле catalog с помощью CCatalog::Add().
Для простого каталога без торговых предложений параметры связи можно не передавать: по умолчанию они будут равны 0. В примере они указаны явно:
-
PRODUCT_IBLOCK_ID = 0— нет отдельного инфоблока предложений, -
SKU_PROPERTY_ID = 0— нет свойства привязки.
Дополнительные параметры:
-
YANDEX_EXPORT— использовать каталог в экспорте в Яндекс.Маркет:YилиN, -
SUBSCRIPTION— разрешить подписку на поступление:YилиN, -
VAT_ID— ставка НДС по умолчанию или0.
$result = \CCatalog::Add([
'IBLOCK_ID' => $productIblockId,
'YANDEX_EXPORT' => 'N',
'SUBSCRIPTION' => 'Y',
'VAT_ID' => 0,
'PRODUCT_IBLOCK_ID' => 0,
'SKU_PROPERTY_ID' => 0,
]);
if (!$result)
{
throw new \RuntimeException('Не удалось создать торговый каталог');
}
После успешной регистрации инфоблок получает функции торгового каталога: цены, остатки, скидки.
Создать товар
Создание товара состоит из двух шагов.
-
Создайте элемент инфоблока
$productIdчерезCIBlockElement::Add(). -
Добавьте к нему товарные параметры через
\Bitrix\Catalog\Model\Product::add().
Пока не выполнен второй шаг, элемент инфоблока не считается товаром каталога.
Параметры товарной записи:
-
TYPE— тип товара:TYPE_PRODUCTдля простого товара, -
QUANTITY— общий остаток, -
QUANTITY_TRACE— вести количественный учет:YилиN, -
CAN_BUY_ZERO— разрешить покупку при нулевом остатке:YилиN, -
SUBSCRIBE— разрешить подписку на поступление:YилиN.
$element = new \CIBlockElement;
$productId = $element->Add([
'IBLOCK_ID' => $productIblockId, // идентификатор инфоблока
'NAME' => 'Настольная лампа',
'CODE' => 'desk-lamp',
'ACTIVE' => 'Y',
]);
if (!$productId)
{
throw new \RuntimeException($element->LAST_ERROR);
}
$result = \Bitrix\Catalog\Model\Product::add([
'ID' => $productId, // идентификатор элемента инфоблока
'TYPE' => \Bitrix\Catalog\ProductTable::TYPE_PRODUCT,
'QUANTITY' => 15,
'QUANTITY_TRACE' => 'Y',
'CAN_BUY_ZERO' => 'N',
'SUBSCRIBE' => 'Y',
]);
if (!$result->isSuccess())
{
throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}
Метод Product::add() возвращает объект результата. После успешного выполнения идентификатор товара совпадает с $productId — идентификатором элемента инфоблока.
Добавить цену товару
Цена хранится отдельно от товарных параметров. Добавьте цену через \Bitrix\Catalog\Model\Price::add(). В метод передайте:
-
PRODUCT_ID— идентификатор товара, -
CATALOG_GROUP_ID— идентификатор типа цены, -
PRICE— числовое значение цены, -
CURRENCY— код валюты.
Идентификатор типа цены получите заранее и сохраните в переменной. Например, можно получить базовый тип цены $basePriceTypeId.
О том, как получить идентификатор типа цены, читайте в статье Базовые настройки каталога.
$result = \Bitrix\Catalog\Model\Price::add([
'PRODUCT_ID' => $productId, // идентификатор товара
'CATALOG_GROUP_ID' => $basePriceTypeId,
'PRICE' => 1250.00,
'CURRENCY' => 'RUB',
]);
if (!$result->isSuccess())
{
throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}
Метод Price::add() возвращает объект результата. Идентификатор созданной цены можно получить через $result->getId().
В результате товар готов к продаже: у него есть карточка в инфоблоке, товарные параметры и цена.
Создать услугу
Создавайте услугу, если продаете работу или цифровой результат без складского остатка: доставку, консультацию или доступ к сервису. Услуга остается элементом инфоблока, но в товарной записи получает тип 'TYPE' => \Bitrix\Catalog\ProductTable::TYPE_SERVICE.
Сценарий создания услуги состоит из трех шагов.
-
Создайте элемент инфоблока через
CIBlockElement::Add(). -
Добавьте товарную запись через
\Bitrix\Catalog\Model\Product::add()с типом\Bitrix\Catalog\ProductTable::TYPE_SERVICE. -
Добавьте цену через
\Bitrix\Catalog\Model\Price::add().
Для услуги не нужно вести складской остаток. Если услуга продается по фиксированной цене, достаточно карточки, товарной записи с типом услуги и цены.
$element = new \CIBlockElement;
$serviceId = $element->Add([
'IBLOCK_ID' => $productIblockId, // идентификатор инфоблока
'NAME' => 'Настройка оборудования',
'CODE' => 'equipment-setup',
'ACTIVE' => 'Y',
]);
if (!$serviceId)
{
throw new \RuntimeException($element->LAST_ERROR);
}
$productResult = \Bitrix\Catalog\Model\Product::add([
'ID' => $serviceId,
'TYPE' => \Bitrix\Catalog\ProductTable::TYPE_SERVICE,
]);
if (!$productResult->isSuccess())
{
throw new \RuntimeException(implode('; ', $productResult->getErrorMessages()));
}
$priceResult = \Bitrix\Catalog\Model\Price::add([
'PRODUCT_ID' => $serviceId,
'CATALOG_GROUP_ID' => $basePriceTypeId,
'PRICE' => 3000.00,
'CURRENCY' => 'RUB',
]);
if (!$priceResult->isSuccess())
{
throw new \RuntimeException(implode('; ', $priceResult->getErrorMessages()));
}
После успешного выполнения услуга доступна как продаваемая позиция каталога. Для проверки цены используйте сценарии из статьи Доступность, цены и подписка.
Создать каталог с торговыми предложениями
Каталог с предложениями нужен, когда у товара есть варианты с разными параметрами: цвет, размер, комплектация, своя цена и свой остаток. Пример: диван в нескольких цветах.
Логика структуры каталога с предложениями:
-
основная карточка хранит родительский товар без цены и остатка,
-
каждое предложение хранит конкретный вариант, его цену и остаток.
Каталог с предложениями можно собрать за шесть шагов.
-
Создайте инфоблок товаров и инфоблок торговых предложений.
-
Создайте в инфоблоке предложений свойство привязки к товару.
-
Зарегистрируйте оба инфоблока как торговые каталоги и свяжите их через свойство привязки.
-
Создайте основной товар с типом
TYPE_SKU. -
Создайте торговое предложение с типом
TYPE_OFFER. -
Добавьте цену и остаток к торговому предложению, а не к основной карточке.
В результате будет два инфоблока:
-
инфоблок товаров — хранит основные карточки товаров,
-
инфоблок торговых предложений — хранит варианты товаров и свойство привязки к основному товару.
Создать инфоблоки
Создайте два инфоблока через CIBlock::Add(): один для товаров, второй для предложений. У каждого должны быть уникальные CODE и API_CODE.
$iblock = new \CIBlock;
$productIblockId = $iblock->Add([
'IBLOCK_TYPE_ID' => 'catalog', // код типа инфоблоков
'NAME' => 'Каталог мебели', // название инфоблока
'CODE' => 'furniture', // символьный код
'API_CODE' => 'Furniture', // код для работы через ORM
'ACTIVE' => 'Y', // признак активности
'LID' => ['s1'], // массив кодов сайтов для привязки
]);
if (!$productIblockId)
{
throw new \RuntimeException($iblock->getLastError()->getMessage());
}
$offerIblockId = $iblock->Add([
'IBLOCK_TYPE_ID' => 'catalog',
'NAME' => 'Торговые предложения мебели',
'CODE' => 'furniture_offers',
'API_CODE' => 'FurnitureOffers',
'ACTIVE' => 'Y',
'LID' => ['s1'],
]);
if (!$offerIblockId)
{
throw new \RuntimeException($iblock->getLastError()->getMessage());
}
Каждый вызов Add() возвращает идентификатор созданного инфоблока или false при ошибке. Сохраните $productIblockId и $offerIblockId — они понадобятся для связи каталогов.
Создать свойство привязки предложения к товару
В инфоблоке предложений создайте свойство-связку с товаром через CIBlockProperty::Add(). Это свойство будет хранить ссылку на родительский товар.
Ключевые параметры метода CIBlockProperty::Add():
-
IBLOCK_ID— идентификатор инфоблока предложений$offerIblockId, -
NAME— название свойства, -
CODE— символьный код, используйтеCML2_LINKдля совместимости, -
PROPERTY_TYPE— типE: привязка к элементу, -
USER_TYPE— типSKU: специальный тип для торговых предложений, -
LINK_IBLOCK_ID— идентификатор инфоблока товаров$productIblockId, -
MULTIPLE— признак множественности:Nдля одного товара на предложение.
$property = new \CIBlockProperty;
$skuPropertyId = $property->Add([
'IBLOCK_ID' => $offerIblockId,
'NAME' => 'Товар',
'CODE' => 'CML2_LINK',
'PROPERTY_TYPE' => 'E',
'USER_TYPE' => 'SKU',
'LINK_IBLOCK_ID' => $productIblockId,
'MULTIPLE' => 'N',
]);
if (!$skuPropertyId)
{
throw new \RuntimeException($property->LAST_ERROR);
}
Метод возвращает идентификатор свойства или false. Сохраните $skuPropertyId — он нужен для регистрации каталога предложений.
Связать каталоги
Зарегистрируйте оба инфоблока в модуле catalog через CCatalog::Add().
Для инфоблока товаров укажите:
-
PRODUCT_IBLOCK_ID = 0— нет родительского инфоблока, -
SKU_PROPERTY_ID = 0— нет свойства привязки.
Для инфоблока предложений укажите:
-
PRODUCT_IBLOCK_ID— идентификатор инфоблока товаров, -
SKU_PROPERTY_ID— идентификатор свойстваCML2_LINK.
$productCatalogCreated = \CCatalog::Add([
'IBLOCK_ID' => $productIblockId,
'YANDEX_EXPORT' => 'N',
'SUBSCRIPTION' => 'Y',
'VAT_ID' => 0,
'PRODUCT_IBLOCK_ID' => 0,
'SKU_PROPERTY_ID' => 0,
]);
if (!$productCatalogCreated)
{
throw new \RuntimeException('Не удалось создать каталог товаров');
}
$offerCatalogCreated = \CCatalog::Add([
'IBLOCK_ID' => $offerIblockId,
'YANDEX_EXPORT' => 'N',
'SUBSCRIPTION' => 'Y',
'VAT_ID' => 0,
'PRODUCT_IBLOCK_ID' => $productIblockId,
'SKU_PROPERTY_ID' => $skuPropertyId,
]);
if (!$offerCatalogCreated)
{
throw new \RuntimeException('Не удалось создать каталог торговых предложений');
}
В результате инфоблок предложений становится полноценным каталогом. Каждое предложение получает собственные товарные параметры, цены и остатки.
Создать основной товар
Создайте родительский товар в инфоблоке товаров через CIBlockElement::Add(). Затем добавьте товарные параметры и задайте тип TYPE_SKU. Это означает, что товар продается через предложения.
$element = new \CIBlockElement;
$productId = $element->Add([
'IBLOCK_ID' => $productIblockId, // идентификатор родительского инфоблока
'NAME' => 'Диван',
'CODE' => 'sofa',
'ACTIVE' => 'Y',
]);
if (!$productId)
{
throw new \RuntimeException($element->LAST_ERROR);
}
$result = \Bitrix\Catalog\Model\Product::add([
'ID' => $productId,
'TYPE' => \Bitrix\Catalog\ProductTable::TYPE_SKU,
]);
if (!$result->isSuccess())
{
throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}
Метод Product::add() возвращает объект результата. После успешного выполнения товар $productId зарегистрирован как родительская карточка каталога.
Цену и остаток храните на предложениях. Родительская карточка обычно не участвует в расчете цены и остатка.
Создать торговое предложение
Создайте предложение в инфоблоке предложений через CIBlockElement::Add(). Обязательно заполните CML2_LINK — это связь с родительским товаром.
После создания элемента добавьте товарные параметры:
-
TYPE— типTYPE_OFFERозначает, что это вариант товара, -
QUANTITY— остаток конкретного варианта, -
QUANTITY_TRACE,CAN_BUY_ZERO,SUBSCRIBE— правила учета и подписки.
$element = new \CIBlockElement;
$offerId = $element->Add([
'IBLOCK_ID' => $offerIblockId, // идентификатор инфоблока предложений
'NAME' => 'Диван, цвет бежевый',
'CODE' => 'sofa-beige',
'ACTIVE' => 'Y',
'PROPERTY_VALUES' => [
'CML2_LINK' => $productId, // идентификатор основного товара
],
]);
if (!$offerId)
{
throw new \RuntimeException($element->LAST_ERROR);
}
$result = \Bitrix\Catalog\Model\Product::add([
'ID' => $offerId,
'TYPE' => \Bitrix\Catalog\ProductTable::TYPE_OFFER,
'QUANTITY' => 7,
'QUANTITY_TRACE' => 'Y',
'CAN_BUY_ZERO' => 'N',
'SUBSCRIBE' => 'Y',
]);
if (!$result->isSuccess())
{
throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}
Добавить цену торговому предложению
Добавьте цену торговому предложению с помощью \Bitrix\Catalog\Model\Price::add() как для простого товара, но в PRODUCT_ID передайте идентификатор предложения $offerId.
Типичная ошибка — добавить цену к основному товару с типом TYPE_SKU, а не к предложению. В этом случае цена не попадет в расчет при выборе конкретного варианта. Всегда передавайте в PRODUCT_ID идентификатор предложения $offerId.
$result = \Bitrix\Catalog\Model\Price::add([
'PRODUCT_ID' => $offerId,
'CATALOG_GROUP_ID' => $basePriceTypeId, // идентификатор типа цены
'PRICE' => 1500.00,
'CURRENCY' => 'RUB',
]);
if (!$result->isSuccess())
{
throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}
Метод возвращает объект результата. Идентификатор созданной цены доступен через $result->getId().
Получить торговые предложения товара
Самый простой способ получить предложения — использовать метод CCatalogSKU::getOffersList().
Параметры метода:
-
productID— идентификатор товара или массив идентификаторов, -
iblockID— идентификатор инфоблока предложений или0для автоопределения, -
skuFilter— фильтр предложений, -
fields— список полей для выборки.
$offersByProduct = \CCatalogSKU::getOffersList(
$productId,
0,
['ACTIVE' => 'Y'],
['ID', 'NAME', 'IBLOCK_ID']
);
$offers = $offersByProduct[$productId] ?? [];
Метод getOffersList() возвращает массив, который сгруппирован по идентификаторам родительских товаров. В переменной $offers остаются предложения для $productId.
Для кастомного запроса сначала получите параметры связки инфоблоков через CCatalogSKU::GetInfoByProductIBlock(). Затем отфильтруйте элементы предложений по свойству привязки.
$skuInfo = \CCatalogSKU::GetInfoByProductIBlock($productIblockId);
if (!$skuInfo)
{
throw new \RuntimeException('Инфоблок не связан с торговыми предложениями');
}
$offersIterator = \CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => $skuInfo['IBLOCK_ID'],
'PROPERTY_' . $skuInfo['SKU_PROPERTY_ID'] => $productId,
'ACTIVE' => 'Y',
],
false,
false,
['ID', 'NAME']
);
while ($offer = $offersIterator->Fetch())
{
echo $offer['ID'] . ': ' . $offer['NAME'] . "\n";
}
Метод CIBlockElement::GetList() возвращает итератор CDBResult. Метод Fetch() по шагам выдает строки предложений.
Изменить товар
Товар состоит из нескольких частей. Перед обновлением определите, что именно нужно изменить: карточку инфоблока, товарные параметры, цену или настройки продажи.
|
Что изменить |
Метод |
|
Название, символьный код, активность и свойства карточки |
|
|
Тип товара, общий остаток, правила учета количества и подписки |
|
|
Цену |
|
|
НДС, единицу измерения и коэффициент продажи |
|
|
Остатки, доступность и подписку |
Методы товарных параметров. Подробные сценарии читайте в статье Доступность, цены и подписка |
|
Комплекты, наборы и скидки |
|
Изменить карточку товара
Чтобы изменить поля элемента инфоблока, используйте CIBlockElement::Update(). Метод подходит для названия, символьного кода, активности и свойств карточки. Передайте идентификатор элемента инфоблока и массив полей.
Для простого товара или родительской карточки передайте $productId. Родительскую карточку обновляйте, когда нужно изменить общее описание товара: название, код, активность или свойства, которые относятся ко всем вариантам.
Настольная лампа с регулировкой и adjustable-desk-lamp — примеры новых значений названия и символьного кода.
$element = new \CIBlockElement;
$updated = $element->Update($productId, [
'NAME' => 'Настольная лампа с регулировкой',
'CODE' => 'adjustable-desk-lamp',
'ACTIVE' => 'Y',
]);
if (!$updated)
{
throw new \RuntimeException($element->LAST_ERROR);
}
Для торгового предложения передайте в первый параметр $offerId, если нужно изменить карточку конкретного варианта.
Изменить товарные параметры
Чтобы изменить данные модуля catalog, используйте \Bitrix\Catalog\Model\Product::update(). Метод меняет товарную запись, а не поля элемента инфоблока. Передайте идентификатор товара $productId или предложения $offerId и массив товарных полей.
$result = \Bitrix\Catalog\Model\Product::update($productId, [
'QUANTITY_TRACE' => 'Y',
'CAN_BUY_ZERO' => 'N',
'SUBSCRIBE' => 'Y',
]);
if (!$result->isSuccess())
{
throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}
Про правила обновления остатков читайте в статье Доступность, цены и подписка.
Изменить цену без дублей
Цена хранится в отдельной записи. При изменении цены обновите существующую запись для пары PRODUCT_ID и CATALOG_GROUP_ID. Не создавайте новую для того же товара и типа цены.
Для простого товара в PRODUCT_ID используйте $productId. Для товара с торговыми предложениями используйте $offerId, потому что цена продаваемого варианта хранится на предложении.
Полный сценарий создания или обновления цены без дублей смотрите в статье Доступность, цены и подписка.
Удалить товар
Перед удалением проверьте наличие торговых предложений. Если предложения существуют, удалите их сначала. Затем удалите родительскую карточку. Система не удаляет предложения автоматически вместе с родителем.
В примерах удаления $productId — идентификатор простого товара или родительского товара с предложениями.
Удалить простой товар
Для простого товара без предложений используйте CIBlockElement::Delete(). В метод передайте идентификатор элемента инфоблока.
if (!\CIBlockElement::Delete($productId))
{
global $APPLICATION;
$exception = $APPLICATION->GetException();
$message = $exception ? $exception->GetString() : 'Не удалось удалить товар';
throw new \RuntimeException($message);
}
Метод CIBlockElement::Delete() возвращает true при успешном удалении и false при ошибке. Он очищает данные из инфоблока и связанные товарные параметры модуля catalog.
Удалить товар с торговыми предложениями
Чтобы удалить товар с торговыми предложениями, выполните три шага.
-
Получите список предложений через
CCatalogSKU::getOffersList(). -
Удалите каждое предложение в цикле с помощью
CIBlockElement::Delete(). -
Удалите родительскую карточку через
CIBlockElement::Delete().
$offersByProduct = \CCatalogSKU::getOffersList(
$productId,
0,
[],
['ID']
);
$offers = $offersByProduct[$productId] ?? [];
foreach ($offers as $offer)
{
if (!\CIBlockElement::Delete((int)$offer['ID']))
{
global $APPLICATION;
$exception = $APPLICATION->GetException();
$message = $exception ? $exception->GetString() : 'Не удалось удалить торговое предложение';
throw new \RuntimeException($message);
}
}
if (!\CIBlockElement::Delete($productId))
{
global $APPLICATION;
$exception = $APPLICATION->GetException();
$message = $exception ? $exception->GetString() : 'Не удалось удалить родительский товар';
throw new \RuntimeException($message);
}
Цикл обрабатывает каждое предложение по идентификатору. После завершения цикла метод удаляет родительский элемент.