1С Битрикс · 6 мин чтения

crm.deal.productrows.set в Битрикс24: товары в сделке на практике

Метод crm.deal.productrows.set - рабочая лошадка при интеграции интернет-магазина или бота с Битрикс24: через него я пробрасываю в сделку список товаров с ценами, количеством и скидками почти в каждом проекте, где CRM синхронизируется с сайтом или ботом. За несколько лет работы с REST API Битрикс24 набил на этом методе достаточно шишек, чтобы рассказать не только сигнатуру из документации, но и то, что в ней явно не написано.

Что делает crm.deal.productrows.set и чем он отличается от add

Метод принимает ID сделки и массив товарных строк, а на выходе полностью пересобирает список товаров в этой сделке. Это не add в смысле «добавить ещё одну позицию» - это именно set, то есть замена всего содержимого разом. Если в сделке уже было пять товаров, а вы отправили массив из трёх других строк, останется ровно три - старые пять исчезнут независимо от того, упоминали вы их или нет.

Логика такая же, как у PUT в REST: вы не патчите отдельное поле, а присылаете полное новое состояние. У Битрикс24 нет отдельного метода вроде productrows.add для точечного добавления одной позиции в существующий список - если нужно дописать товар, придётся сначала прочитать текущий список через crm.deal.productrows.get, дополнить массив в коде и уже потом вызвать set с полным набором.

На практике это первое, что ломает интеграции у новичков: бот или скрипт синхронизации вызывает set при каждом добавлении товара в корзину, передавая только новый товар - и стирает всё, что было добавлено раньше.

Формат запроса и обязательные поля товарной строки

Каждый элемент массива rows - это объект с набором полей. На практике достаточно такого минимума:

  • PRODUCT_ID - ID товара из каталога Битрикс24; можно передать 0, если товара нет в каталоге
  • PRODUCT_NAME - обязателен, если PRODUCT_ID равен 0, иначе название подтянется из карточки товара
  • PRICE - цена за единицу с учётом валюты сделки
  • QUANTITY - количество, число, а не строка
  • TAX_RATE и TAX_INCLUDED - ставка налога и признак, включён ли он в цену
  • DISCOUNT_TYPE_ID и DISCOUNT_SUM или DISCOUNT_RATE - тип и размер скидки
  • MEASURE_CODE - код единицы измерения (796 для «шт.» по классификатору ОКЕИ)

Вызов через вебхук выглядит так:

curl -X POST 
  "https://your-domain.bitrix24.ru/rest/1/webhook_code/crm.deal.productrows.set" 
  -H "Content-Type: application/json" 
  -d '{
    "id": 4521,
    "rows": [
      {
        "PRODUCT_ID": 118,
        "PRICE": 3200,
        "QUANTITY": 2,
        "TAX_RATE": 20,
        "TAX_INCLUDED": "Y"
      },
      {
        "PRODUCT_ID": 0,
        "PRODUCT_NAME": "Доставка СДЭК",
        "PRICE": 450,
        "QUANTITY": 1,
        "TAX_RATE": 0
      }
    ]
  }'

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

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Почему set затирает старые строки и как обновить только одну позицию

Поскольку метод переписывает список целиком, любое частичное обновление сводится к трём шагам: получить текущие строки через crm.deal.productrows.get, найти или изменить нужный элемент массива в коде, отправить обратно весь массив через set. Пропускать первый шаг нельзя - даже если вы хотите поменять количество только у одной позиции.

В n8n это обычно оформляется как HTTP Request на get, дальше Function-нода с правкой JSON, и снова HTTP Request на set. Экономить на шаге чтения не стоит: я видел проект, где интеграция с сайтом на Tilda писала товары в сделку напрямую при каждом изменении корзины, без предварительного get - в результате при повторном оформлении заказа тем же клиентом старые позиции из первой сделки просто исчезали, потому что второй вызов их не учитывал.

Скидки, налоги и валюта в товарных строках

Поля скидки путают чаще всего. DISCOUNT_TYPE_ID принимает два значения: 1 - скидка в процентах (тогда используется DISCOUNT_RATE), 2 - скидка в валюте сделки (тогда используется DISCOUNT_SUM). Если передать оба поля одновременно без указания типа, Битрикс24 применит то, что соответствует DISCOUNT_TYPE_ID, а второе поле молча проигнорирует - итоговая сумма разойдётся с ожидаемой, и разбираться придётся через logs интеграции.

Поле Что задаёт Частая ошибка
DISCOUNT_TYPE_ID = 1 Скидка в процентах через DISCOUNT_RATE Указывают DISCOUNT_SUM вместо DISCOUNT_RATE
DISCOUNT_TYPE_ID = 2 Скидка суммой через DISCOUNT_SUM Забывают пересчитать PRICE, если скидка уже включена в цену от поставщика
TAX_INCLUDED Включён ли налог в PRICE Ставят Y, но передают цену без налога - итог по сделке занижается
Валюта строки Берётся из валюты сделки, отдельного поля нет Пытаются передать цену в другой валюте без конвертации

Отдельно стоит помнить про PRICE_EXCLUSIVE - цену без учёта скидки, которая используется для отображения зачёркнутой цены в некоторых представлениях каталога. Если её не передать, Битрикс24 подставит значение PRICE, и зачёркнутая цена в интерфейсе сделки просто не появится.

Типичные ошибки при вызове productrows.set

За практику встречались одни и те же грабли:

  • PRODUCT_ID = 0 без PRODUCT_NAME - строка сохраняется с именем «Без названия», и менеджер не понимает, что за товар в сделке
  • QUANTITY передан строкой вместо числа - часть SDK и обёрток над REST API это глотают, но при вызове напрямую через batch запрос может вернуть ошибку валидации по конкретной строке массива
  • Смешение старого каталога (crm_product) и Универсальных элементов CRM - если магазин работает через новый каталог второй версии, для позиций нужен ownerType/ownerId и соответствующие методы, а productrows.set остаётся привязан именно к сделкам
  • Превышение лимита запросов - облачный Битрикс24 ограничивает REST API примерно двумя запросами в секунду на приложение, и синхронизация заказов из WooCommerce или Tilda пачками легко в это упирается; спасает batch-запрос, где productrows.set и update сделки уходят одним HTTP-вызовом
  • Права пользователя, от имени которого выполнен вебхук - если у него нет доступа на редактирование сделки конкретной воронки, метод вернёт ошибку ACCESS_DENIED, а не тихо проигнорирует товары

Как я собираю строки товаров в реальных интеграциях

В проектах с ботом на aiogram цепочка обычно такая: пользователь набирает корзину в Telegram, бот на вебхуке создаёт сделку через crm.deal.add, затем формирует массив rows из содержимого корзины и вызывает productrows.set вторым запросом - сразу двумя вызовами в batch, чтобы не терять время на round-trip.

При синхронизации заказов из WooCommerce в Bitrix24 логика немного другая: заказ уже содержит товарные позиции с ID из внешнего магазина, и перед вызовом set приходится сопоставлять внешние ID с ID товаров в каталоге Битрикс24 - обычно через таблицу соответствий, которую храню в самой CRM в виде пользовательского поля товара.

В n8n такие сценарии собираются без единой строчки бэкенд-кода: вебхук от Tilda или CDEK-калькулятора триггерит workflow, Function-нода превращает данные заказа в массив rows нужного формата, а HTTP Request-нода отправляет его в productrows.set. Формат ноды на JavaScript для сборки строк выглядит так:

const rows = items.map(item => ({
  PRODUCT_ID: item.json.bitrixProductId || 0,
  PRODUCT_NAME: item.json.title,
  PRICE: item.json.price,
  QUANTITY: item.json.qty,
  TAX_RATE: 20,
  TAX_INCLUDED: 'Y'
}));

return [{ json: { id: dealId, rows } }];

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

Чтобы сайт работал без сбоев

Техподдержка

от 15 000 ₽/мес

Подробнее →

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

Можно ли обновить количество одного товара, не трогая остальные позиции сделки?

Напрямую нет: метод всегда заменяет весь список строк. Сначала читаете текущие позиции через crm.deal.productrows.get, меняете нужное поле в массиве на стороне вашего кода, потом отправляете весь массив обратно через productrows.set.

Как добавить в сделку позицию, которой нет в каталоге товаров?

Передайте PRODUCT_ID равным 0 и обязательно укажите PRODUCT_NAME с ценой и количеством. Так удобно добавлять доставку, сборку или разовую услугу, которую заводить в каталог нет смысла.

Что вернёт метод, если в одной из строк массива ошибка?

Запрос отклоняется целиком - частичного сохранения нет. Если из десяти строк в одной неверный тип поля или недопустимое значение DISCOUNT_TYPE_ID, не сохранится ни одна позиция, и в сделке останется прежний список товаров.

Почему сумма сделки не совпадает с суммой по товарным строкам?

Обычно расхождение из-за налогов: TAX_INCLUDED влияет на то, входит ли TAX_RATE в PRICE, и если это поле проставлено неверно, Битрикс24 считает итог иначе, чем ожидает интеграция. Второй частый источник - скидка, добавленная одновременно и на уровне сделки, и на уровне отдельной строки товара, из-за чего она применяется дважды.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно