diff --git a/editorial/agent-rewrites/363.json b/editorial/agent-rewrites/363.json index 169afb8..f420276 100644 --- a/editorial/agent-rewrites/363.json +++ b/editorial/agent-rewrites/363.json @@ -2,6 +2,6 @@ "index": 363, "slug": "bitrix-api-создание-добавление-торгового-пре", "title": "Bitrix API: как создать торговое предложение, связать его с товаром и проверить каталог", - "excerpt": "Торговое предложение в Bitrix состоит не только из элемента инфоблока. Разбираем связь SKU с товаром, товарные параметры, цену, типичные симптомы ошибки и проверку через тот же путь, которым читает витрина.", - "contentHtml": "
Элемент торгового предложения появился в админке, метод CIBlockElement::Add вернул ID, но в карточке товара нет варианта. Цена ошибки — менеджер видит созданную запись, а покупатель не видит размер, цвет, цену или доступный остаток. Повторный запуск импорта может добавить ещё один такой же элемент.
Причина обычно не в одном вызове API. Bitrix хранит предложение в инфоблоке SKU, связь с родительским товаром — в свойстве предложения, а цену и остаток — в данных каталога. Успешная запись одного слоя не подтверждает готовность остальных.
\nСоздавайте торговое предложение как последовательность проверяемых слоёв: элемент инфоблока, связь с товаром, товарные параметры, цена и чтение через публичный запрос. Останавливайте процесс на первом отрицательном результате. Такой порядок показывает, где именно данные потерялись.
\nСначала определите два инфоблока: инфоблок товаров и инфоблок торговых предложений. Их связь задаёт настройка торгового каталога. Не подставляйте свойство CML2_LINK вслепую: в конкретном проекте его код или ID может отличаться.
У предложения должен быть родительский товар. Значение свойства связи — это ID товара, а не ID самого предложения, его символьный код или артикул. Артикул можно сохранить отдельным свойством, но он не заменяет связь SKU.
\nПосле создания элемента каталогу нужны товарные параметры. Для старого API это запись через CCatalogProduct::Add. В ней можно указать количество, НДС и правила доступности. Цена хранится отдельно. Поэтому вызов CPrice::SetBasePrice — ещё один шаг, а не часть CIBlockElement::Add.
Названия методов зависят от версии Bitrix. Документация помечает CCatalogProduct::Add и CPrice::SetBasePrice устаревшими в новых версиях. Для нового кода сверяйте актуальные модели каталога и цены. Пример ниже нужен для понимания границ старого API и для поддержки проектов, где эти классы ещё используются.
Код ниже намеренно использует условные ID и свойства. Он не заявляет, что в вашем каталоге связь называется CML2_LINK. Перед запуском получите реальные значения из настройки инфоблока SKU и из схемы свойств.
<?php\n\nuse Bitrix\\Main\\Loader;\n\nif (!Loader::includeModule('iblock') || !Loader::includeModule('catalog')) {\n throw new RuntimeException('Не удалось подключить iblock или catalog');\n}\n\n$offerIblockId = 12; // учебное значение\n$productId = 345; // ID существующего товара\n$offerName = 'Кофе, упаковка 250 г';\n$offerPrice = 990.00;\n$currency = 'RUB';\n\n$element = new CIBlockElement();\n$offerId = $element->Add([\n 'IBLOCK_ID' => $offerIblockId,\n 'NAME' => $offerName,\n 'ACTIVE' => 'Y',\n 'PROPERTY_VALUES' => [\n 'CML2_LINK' => $productId, // проверьте код свойства в проекте\n 'ARTNUMBER' => 'COFFEE-250',\n ],\n]);\n\nif (!$offerId) {\n throw new RuntimeException(\n 'Не удалось создать предложение: ' . $element->LAST_ERROR\n );\n}\n\nif (!CCatalogProduct::Add([\n 'ID' => $offerId,\n 'VAT_INCLUDED' => 'Y',\n 'QUANTITY' => 10,\n])) {\n throw new RuntimeException('Не удалось записать параметры товара');\n}\n\nif (!CPrice::SetBasePrice($offerId, $offerPrice, $currency)) {\n throw new RuntimeException('Не удалось записать базовую цену');\n}\n\n// В production здесь нужна повторная выборка и проверка витрины.\necho 'Создано предложение: ' . $offerId;\nВ старом фрагменте статьи встречается переменная $offersId, хотя ID сохраняется в $offerId. Такая опечатка ломает запись товарных параметров. Вторая опасность — перепутанная логика if: код может выбросить исключение после успешной операции. Проверяйте каждый результат отдельным условием, как в примере.
Не создавайте новый товар автоматически, если задача требует только предложения. Сначала найдите родительский товар по устойчивому ключу проекта и проверьте, что он принадлежит ожидаемому инфоблоку. Иначе импорт создаст «сиротское» SKU или свяжет вариант с похожим товаром.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В админке есть ID, в карточке нет предложения | Неверный инфоблок, неактивный элемент или неправильная связь SKU | Выбрать элемент по ID и прочитать свойство связи тем же фильтром, что использует компонент | Исправить IBLOCK_ID или значение свойства; затем повторить чтение |
| Вариант виден, но цена пустая | Цена не записана, неверен тип цены или валюта | Получить цены по PRODUCT_ID, равному ID предложения | Записать нужный тип цены и проверить результат метода |
| Цена есть, но купить нельзя | Нет записи товарных параметров, нулевой остаток или запрещена покупка без остатка | Прочитать товарные параметры и правила доступности | Создать или обновить параметры, затем проверить итоговый флаг доступности |
| Каждый импорт добавляет новый вариант | Нет идемпотентного ключа и поиска существующего SKU | Найти элемент по XML_ID или уникальному артикулу до вызова Add | Разделить ветки create и update; не использовать имя как единственный ключ |
| Запись правильная, витрина показывает старое состояние | Кеш, индекс или отдельный слой чтения не обновился | Сравнить прямую выборку и ответ публичного компонента | Выполнить штатное обновление кеша или индекса после успешной проверки данных |
CIBlockElement::Add с активностью, названием и обязательными свойствами.false запишите LAST_ERROR и остановите цепочку. Не пытайтесь ставить цену для неизвестного ID.Если не загрузился модуль, не продолжайте выполнение. Если не найден родительский товар, верните понятную ошибку импорта. Если найдено несколько товаров, остановитесь: автоматический выбор создаёт труднообратимую ошибку данных.
\nЕсли элемент создался, а цена не записалась, повторный запуск не должен создавать новый элемент. Сохраните ID предложения, исправьте причину и выполните операцию обновления. Для этого нужен внешний ключ: XML_ID, артикул в пределах каталога или ключ, заданный интеграцией.
\nДва параллельных импорта могут одновременно не найти SKU и оба пройти к созданию. Проверки в PHP недостаточно. Защитите уникальность на уровне модели данных или очереди, а после конфликта повторите поиск существующей записи.
\nОперация готова, если повторный запуск не создаёт дубль, а один и тот же публичный запрос возвращает предложение с нужным родительским товаром, активностью, ожидаемой ценой, валютой и остатком. В журнале есть успешный результат каждого шага. При ошибке цепочка останавливается на конкретном слое и сохраняет диагностическое сообщение.
\nЭлемент торгового предложения появился в админке, метод CIBlockElement::Add вернул ID, но в карточке товара нет варианта. Цена ошибки — менеджер видит созданную запись, а покупатель не видит размер, цвет, цену или доступный остаток. Повторный запуск импорта может добавить ещё один такой же элемент.
Причина обычно не в одном вызове API. Bitrix хранит предложение в инфоблоке SKU, связь с родительским товаром — в свойстве предложения, а цену и остаток — в данных каталога. Успешная запись одного слоя не подтверждает готовность остальных.
\nСоздавайте торговое предложение как последовательность проверяемых слоёв: элемент инфоблока, связь с товаром, товарные параметры, цена и чтение через публичный запрос. Останавливайте процесс на первом отрицательном результате. Такой порядок показывает, где именно данные потерялись.
\nСначала определите инфоблок торговых предложений и его настройку в торговом каталоге. Для связки Bitrix хранит ID инфоблока товаров и ID свойства, которое соединяет предложение с товаром. Эти значения можно получить из настройки каталога через CCatalog::GetByID; не подставляйте CML2_LINK вслепую.
У предложения должен быть родительский товар. Значение свойства связи — это ID товара, а не ID самого предложения, его символьный код или артикул. Артикул можно сохранить отдельным свойством, но он не заменяет связь SKU.
\nПосле создания элемента каталогу нужны товарные параметры. Для старого API это запись через CCatalogProduct::Add. В ней можно указать количество, НДС и правила доступности. Цена хранится отдельно. Поэтому вызов CPrice::SetBasePrice — ещё один шаг, а не часть CIBlockElement::Add.
CCatalogProduct::Add устарел с версии 17.6.0, а CPrice::SetBasePrice — с версии 17.6.0. Для legacy-проекта эти вызовы нужно проверять по версии модулей. Для нового D7-кода используйте модели каталога: у цены должен быть существующий тип, а результат операции нужно проверить через isSuccess(). Пример ниже показывает границы старого API, а не рекомендуемый путь для новой интеграции.
Код ниже намеренно использует условные ID. CML2_LINK в нём — только имя-заглушка: в рабочем проекте подставьте реальный числовой ID свойства из CCatalog::GetByID и проверьте, что $productId принадлежит указанному инфоблоку товаров. Перед запуском сверяйте версию модулей, права и внешний ключ операции.
<?php\n\nuse Bitrix\\Main\\Loader;\n\nif (!Loader::includeModule('iblock') || !Loader::includeModule('catalog')) {\n throw new RuntimeException('Не удалось подключить iblock или catalog');\n}\n\n$offerIblockId = 12; // учебное значение\n$productId = 345; // ID существующего товара\n$offerName = 'Кофе, упаковка 250 г';\n$offerPrice = 990.00;\n$currency = 'RUB';\n\n$element = new CIBlockElement();\n$offerId = $element->Add([\n 'IBLOCK_ID' => $offerIblockId,\n 'NAME' => $offerName,\n 'ACTIVE' => 'Y',\n 'PROPERTY_VALUES' => [\n 'CML2_LINK' => $productId, // проверьте код свойства в проекте\n 'ARTNUMBER' => 'COFFEE-250',\n ],\n]);\n\nif (!$offerId) {\n throw new RuntimeException(\n 'Не удалось создать предложение: ' . $element->LAST_ERROR\n );\n}\n\nif (!CCatalogProduct::Add([\n 'ID' => $offerId,\n 'VAT_INCLUDED' => 'Y',\n 'QUANTITY' => 10,\n])) {\n throw new RuntimeException('Не удалось записать параметры товара');\n}\n\nif (!CPrice::SetBasePrice($offerId, $offerPrice, $currency)) {\n throw new RuntimeException('Не удалось записать базовую цену');\n}\n\n// В production здесь нужна повторная выборка и проверка витрины.\necho 'Создано предложение: ' . $offerId;\nВ старом фрагменте статьи встречается переменная $offersId, хотя ID сохраняется в $offerId. Такая опечатка ломает запись товарных параметров. Вторая опасность — перепутанная логика if: код может выбросить исключение после успешной операции. Проверяйте каждый результат отдельным условием, как в примере.
Не создавайте новый товар автоматически, если задача требует только предложения. Сначала найдите родительский товар по устойчивому ключу проекта и проверьте, что он принадлежит ожидаемому инфоблоку. Иначе импорт создаст «сиротское» SKU или свяжет вариант с похожим товаром.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В админке есть ID, в карточке нет предложения | Неверный инфоблок, неактивный элемент или неправильная связь SKU | Выбрать элемент по ID и прочитать свойство связи тем же фильтром, что использует компонент | Исправить IBLOCK_ID или значение свойства; затем повторить чтение |
| Вариант виден, но цена пустая | Цена не записана, неверен тип цены или валюта | Получить запись цены по ID предложения и нужному типу цены | Записать нужный тип цены и проверить результат метода |
| Цена есть, но купить нельзя | Нет записи товарных параметров, нулевой остаток или запрещена покупка без остатка | Прочитать товарные параметры и правила доступности | Создать или обновить параметры, затем проверить итоговый флаг доступности |
| Каждый импорт добавляет новый вариант | Нет идемпотентного ключа и поиска существующего SKU | Найти элемент по XML_ID или уникальному артикулу до вызова Add | Разделить ветки create и update; не использовать имя как единственный ключ |
| Запись правильная, витрина показывает старое состояние | Кеш, индекс или отдельный слой чтения не обновился | Сравнить прямую выборку и ответ публичного компонента | Выполнить штатное обновление кеша или индекса после успешной проверки данных |
CIBlockElement::Add с активностью, названием и обязательными свойствами.false запишите LAST_ERROR и остановите цепочку. Не пытайтесь ставить цену для неизвестного ID.Если не загрузился модуль, не продолжайте выполнение. Если не найден родительский товар, верните понятную ошибку импорта. Если найдено несколько товаров, остановитесь: автоматический выбор создаёт труднообратимую ошибку данных.
\nЕсли элемент создался, а цена не записалась, повторный запуск не должен создавать новый элемент. Сохраните ID предложения, исправьте причину и выполните операцию обновления. Для этого нужен внешний ключ: XML_ID, артикул в пределах каталога или ключ, заданный интеграцией.
\nДва параллельных импорта могут одновременно не найти SKU и оба пройти к созданию. Проверки в PHP недостаточно. Защитите уникальность на уровне модели данных или очереди, а после конфликта повторите поиск существующей записи.
\nCCatalogProduct::Add и CPrice::SetBasePrice остаются legacy-вызовами; для нового кода сверяйте D7-модели и версию модуля.Операция готова, если повторный запуск не создаёт дубль, а один и тот же публичный запрос возвращает предложение с нужным родительским товаром, активностью, ожидаемой ценой, валютой и остатком. В журнале есть успешный результат каждого шага. При ошибке цепочка останавливается на конкретном слое и сохраняет диагностическое сообщение.
\n