Как создать объект CRM с товарами, скидками и налогами
Кто может выполнять методы:
- catalog.product.list — пользователь с правом на просмотр каталога товаров и правом на чтение инфоблока торгового каталога
- catalog.price.list — пользователь с правом на просмотр каталога товаров или правом на изменение цен
- crm.item.add — пользователь с правом на добавление объекта выбранного типа
- crm.item.productrow.set — пользователь с правом на изменение созданного объекта CRM
- crm.item.productrow.list — пользователь с правом на чтение созданного объекта CRM
Выберите инструмент для разработки с AI-агентом:
- используйте Битрикс24 Вайбкод, чтобы создать приложение для Битрикс24 по описанию задачи без знания языков программирования. Агент напишет код и разместит приложение на сервере без ручной настройки хостинга
- используйте MCP-сервер, чтобы разрабатывать интеграцию через REST API в своем проекте. Агент будет обращаться к официальной REST-документации
Товарные позиции можно привязать к лиду, сделке, счету или коммерческому предложению. В примере создаем объект CRM, находим товар в каталоге, получаем его цену и сохраняем несколько товарных позиций с разными вариантами налога и скидки.
Сценарий состоит из четырех шагов.
- Найти товар методом catalog.product.list
- Получить цену товара методом catalog.price.list
- Создать объект CRM методом crm.item.add
- Сохранить товарные позиции методом crm.item.productrow.set
Подготовьте данные
Для выполнения примера нужны:
- входящий вебхук со scope
crmиcatalog - идентификатор торгового каталога
iblockId. Его можно получить методом catalog.catalog.list - тип объекта CRM, к которому нужно привязать товары
|
Объект CRM |
entityTypeId для crm.item.add |
ownerType для crm.item.productrow.set |
|
Лид |
|
|
|
Сделка |
|
|
|
Счет |
|
|
|
Коммерческое предложение |
|
|
Для новых интеграций создавайте счета как «Счет (новый)» с entityTypeId = 31 и ownerType = SI. Старый тип счета INVOICE оставлен для совместимости и не рекомендуется для новых сценариев.
Проверьте, какие обязательные поля настроены для выбранного типа объекта в вашем Битрикс24. Все обязательные поля нужно передать в fields метода crm.item.add.
Для серверных JS-примеров с B24Hook нужен Node.js 20 либо 22 и выше. B24JsSDK — ES module: сохраните код в файле .mjs или добавьте "type": "module" в package.json.
Для примеров с b24pysdk нужен Python 3.9 или новее.
1. Найдите товар в каталоге
Вызовите catalog.product.list с фильтром по iblockId. В select передайте обязательные поля id и iblockId, а также name, чтобы использовать название товара в диагностике.
Как использовать примеры в документации
// npm install @bitrix24/b24jssdk
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
async function call(method, params) {
const response = await $b24.actions.v2.call.make({ method, params })
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
return response.getData().result
}
async function getProducts(iblockId) {
const result = await call('catalog.product.list', {
select: ['id', 'iblockId', 'name'],
filter: {
iblockId: iblockId,
active: 'Y',
},
order: {
id: 'ASC',
},
start: 0,
})
return result.products
}
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
$webhookUrl = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/';
$b24 = ServiceBuilderFactory::createServiceBuilderFromWebhook($webhookUrl);
function callMethod($b24, string $method, array $params): array
{
return $b24->core
->call($method, $params)
->getResponseData()
->getResult();
}
function getProducts($b24, int $iblockId): array
{
$result = callMethod($b24, 'catalog.product.list', [
'select' => ['id', 'iblockId', 'name'],
'filter' => [
'iblockId' => $iblockId,
'active' => 'Y',
],
'order' => [
'id' => 'ASC',
],
'start' => 0,
]);
return $result['products'];
}
# pip install b24pysdk
from b24pysdk import BitrixWebhook
token = BitrixWebhook(
domain="your-domain.bitrix24.ru",
webhook_token="USER_ID/TOKEN",
)
def call_method(method: str, params: dict):
return token.call_method(method, params)["result"]
def get_products(iblock_id: int):
result = call_method("catalog.product.list", {
"select": ["id", "iblockId", "name"],
"filter": {
"iblockId": iblock_id,
"active": "Y",
},
"order": {
"id": "ASC",
},
"start": 0,
})
return result["products"]
Метод возвращает товары постранично. В примере используется первая страница, до 50 товаров. Если в вашем каталоге больше товаров, переберите страницы через параметр start.
Сокращенный ответ:
{
"result": {
"products": [
{
"id": 1243,
"iblockId": 23,
"name": "Монитор"
}
]
},
"total": 1
}
Сохраните result.products[].id. Идентификатор товара понадобится для получения цены и для параметра productId товарной позиции.
2. Получите цену товара
Цена товара хранится отдельно от карточки товара. Для каждого найденного товара вызовите catalog.price.list с фильтром по productId и выберите первую цену больше нуля.
async function getFirstPrice(productId) {
const result = await call('catalog.price.list', {
select: ['id', 'productId', 'price', 'currency'],
filter: {
productId: productId,
'>price': 0,
},
order: {
id: 'ASC',
},
start: 0,
})
return result.prices[0] ?? null
}
async function findProductWithPrice(iblockId) {
const products = await getProducts(iblockId)
for (const product of products) {
const price = await getFirstPrice(product.id)
if (price) {
return { product, price }
}
}
throw new Error('В каталоге нет активного товара с ценой больше нуля')
}
function getFirstPrice($b24, int $productId): ?array
{
$result = callMethod($b24, 'catalog.price.list', [
'select' => ['id', 'productId', 'price', 'currency'],
'filter' => [
'productId' => $productId,
'>price' => 0,
],
'order' => [
'id' => 'ASC',
],
'start' => 0,
]);
return $result['prices'][0] ?? null;
}
function findProductWithPrice($b24, int $iblockId): array
{
foreach (getProducts($b24, $iblockId) as $product) {
$price = getFirstPrice($b24, (int)$product['id']);
if ($price !== null) {
return [
'product' => $product,
'price' => $price,
];
}
}
throw new RuntimeException('В каталоге нет активного товара с ценой больше нуля');
}
def get_first_price(product_id: int):
result = call_method("catalog.price.list", {
"select": ["id", "productId", "price", "currency"],
"filter": {
"productId": product_id,
">price": 0,
},
"order": {
"id": "ASC",
},
"start": 0,
})
return result["prices"][0] if result["prices"] else None
def find_product_with_price(iblock_id: int):
for product in get_products(iblock_id):
price = get_first_price(int(product["id"]))
if price:
return {
"product": product,
"price": price,
}
raise RuntimeError("В каталоге нет активного товара с ценой больше нуля")
Сокращенный ответ:
{
"result": {
"prices": [
{
"id": 381,
"productId": 1243,
"price": 1000,
"currency": "RUB"
}
]
},
"total": 1
}
Сохраните result.prices[].price и result.prices[].currency. Цена понадобится для расчета товарных позиций, валюта — для поля currencyId создаваемого объекта CRM.
3. Создайте объект CRM
Вызовите crm.item.add. Передайте:
entityTypeId— числовой идентификатор типа объекта CRMfields.title— название объектаfields.currencyId— валюту цены из шага 2
async function createCrmItem(entityTypeId, title, currency) {
const result = await call('crm.item.add', {
entityTypeId: entityTypeId,
fields: {
title: title,
currencyId: currency,
},
})
return result.item.id
}
function createCrmItem($b24, int $entityTypeId, string $title, string $currency): int
{
$result = callMethod($b24, 'crm.item.add', [
'entityTypeId' => $entityTypeId,
'fields' => [
'title' => $title,
'currencyId' => $currency,
],
]);
return (int)$result['item']['id'];
}
def create_crm_item(entity_type_id: int, title: str, currency: str) -> int:
result = call_method("crm.item.add", {
"entityTypeId": entity_type_id,
"fields": {
"title": title,
"currencyId": currency,
},
})
return int(result["item"]["id"])
Сокращенный ответ:
{
"result": {
"item": {
"id": 342,
"title": "Сделка с товарами"
}
}
}
Сохраните result.item.id. Идентификатор понадобится для параметра ownerId метода crm.item.productrow.set.
4. Сохраните товарные позиции
Вызовите crm.item.productrow.set. Передайте:
ownerType— краткий символьный код типа объекта CRMownerId— идентификатор объекта из шага 3productRows— массив товарных позиций
В примере сохраняются четыре варианта:
- товар с налогом 20%, налог не включен в цену
- товар с налогом 20%, налог включен в цену
- товар с фиксированной скидкой в валюте цены
- товар со скидкой 10%
Для фиксированной скидки пример берет меньшее значение: 100 единиц валюты или половину цены товара. Так итоговая цена товарной позиции не станет отрицательной.
Метод crm.item.productrow.set перезаписывает все товарные позиции объекта CRM. Позиции, которые не переданы в productRows, будут удалены из объекта.
function buildProductRows(productId, basePrice) {
const price = Number(basePrice)
const fixedDiscount = Math.min(100, price / 2)
return [
{
productId: productId,
price: price,
taxRate: 20,
taxIncluded: 'N',
quantity: 1,
sort: 10,
},
{
productId: productId,
price: price * 1.2,
taxRate: 20,
taxIncluded: 'Y',
quantity: 1,
sort: 20,
},
{
productId: productId,
price: price - fixedDiscount,
discountTypeId: 1,
discountSum: fixedDiscount,
quantity: 1,
sort: 30,
},
{
productId: productId,
price: price * 0.9,
discountTypeId: 2,
discountRate: 10,
quantity: 1,
sort: 40,
},
]
}
async function setProductRows(ownerType, ownerId, productRows) {
const result = await call('crm.item.productrow.set', {
ownerType: ownerType,
ownerId: ownerId,
productRows: productRows,
})
return result.productRows
}
function buildProductRows(int $productId, float $basePrice): array
{
$fixedDiscount = min(100, $basePrice / 2);
return [
[
'productId' => $productId,
'price' => $basePrice,
'taxRate' => 20,
'taxIncluded' => 'N',
'quantity' => 1,
'sort' => 10,
],
[
'productId' => $productId,
'price' => $basePrice * 1.2,
'taxRate' => 20,
'taxIncluded' => 'Y',
'quantity' => 1,
'sort' => 20,
],
[
'productId' => $productId,
'price' => $basePrice - $fixedDiscount,
'discountTypeId' => 1,
'discountSum' => $fixedDiscount,
'quantity' => 1,
'sort' => 30,
],
[
'productId' => $productId,
'price' => $basePrice * 0.9,
'discountTypeId' => 2,
'discountRate' => 10,
'quantity' => 1,
'sort' => 40,
],
];
}
function setProductRows($b24, string $ownerType, int $ownerId, array $productRows): array
{
$result = callMethod($b24, 'crm.item.productrow.set', [
'ownerType' => $ownerType,
'ownerId' => $ownerId,
'productRows' => $productRows,
]);
return $result['productRows'];
}
def build_product_rows(product_id: int, base_price: float):
fixed_discount = min(100, base_price / 2)
return [
{
"productId": product_id,
"price": base_price,
"taxRate": 20,
"taxIncluded": "N",
"quantity": 1,
"sort": 10,
},
{
"productId": product_id,
"price": base_price * 1.2,
"taxRate": 20,
"taxIncluded": "Y",
"quantity": 1,
"sort": 20,
},
{
"productId": product_id,
"price": base_price - fixed_discount,
"discountTypeId": 1,
"discountSum": fixed_discount,
"quantity": 1,
"sort": 30,
},
{
"productId": product_id,
"price": base_price * 0.9,
"discountTypeId": 2,
"discountRate": 10,
"quantity": 1,
"sort": 40,
},
]
def set_product_rows(owner_type: str, owner_id: int, product_rows: list):
result = call_method("crm.item.productrow.set", {
"ownerType": owner_type,
"ownerId": owner_id,
"productRows": product_rows,
})
return result["productRows"]
Сокращенный ответ:
{
"result": {
"productRows": [
{
"id": 17654,
"ownerId": 342,
"ownerType": "D",
"productId": 1243,
"price": 1000,
"quantity": 1,
"taxRate": 20,
"taxIncluded": "N"
}
]
}
}
Запустите сценарий
После добавления функций из предыдущих шагов выберите нужный тип объекта в настройках crmEntity. Для лида укажите entityTypeId = 1 и ownerType = L, для сделки — 2 и D, для счета — 31 и SI, для коммерческого предложения — 7 и Q.
const crmEntity = {
entityTypeId: 2,
ownerType: 'D',
title: 'Сделка с товарами',
}
const iblockId = 23
const { product, price } = await findProductWithPrice(iblockId)
const itemId = await createCrmItem(
crmEntity.entityTypeId,
crmEntity.title,
price.currency,
)
const productRows = buildProductRows(product.id, price.price)
const savedRows = await setProductRows(crmEntity.ownerType, itemId, productRows)
console.log(`Создан объект CRM #${itemId}`)
console.log(`Товар: ${product.name}`)
console.log(savedRows)
$crmEntity = [
'entityTypeId' => 2,
'ownerType' => 'D',
'title' => 'Сделка с товарами',
];
$iblockId = 23;
$productWithPrice = findProductWithPrice($b24, $iblockId);
$product = $productWithPrice['product'];
$price = $productWithPrice['price'];
$itemId = createCrmItem(
$b24,
$crmEntity['entityTypeId'],
$crmEntity['title'],
$price['currency']
);
$productRows = buildProductRows((int)$product['id'], (float)$price['price']);
$savedRows = setProductRows($b24, $crmEntity['ownerType'], $itemId, $productRows);
print('Создан объект CRM #' . $itemId . PHP_EOL);
print('Товар: ' . $product['name'] . PHP_EOL);
print_r($savedRows);
crm_entity = {
"entityTypeId": 2,
"ownerType": "D",
"title": "Сделка с товарами",
}
iblock_id = 23
product_with_price = find_product_with_price(iblock_id)
product = product_with_price["product"]
price = product_with_price["price"]
item_id = create_crm_item(
crm_entity["entityTypeId"],
crm_entity["title"],
price["currency"],
)
product_rows = build_product_rows(int(product["id"]), float(price["price"]))
saved_rows = set_product_rows(crm_entity["ownerType"], item_id, product_rows)
print("Создан объект CRM #%s" % item_id)
print("Товар: %s" % product["name"])
print(saved_rows)
Проверим результат
Откройте созданный объект CRM в интерфейсе и проверьте вкладку с товарами. В списке должны появиться четыре товарные позиции с одним товаром и разными расчетами:
- налог не включен в цену
- налог включен в цену
- фиксированная скидка
- процентная скидка
Проверить результат через REST можно методом crm.item.productrow.list. Передайте фильтр:
=ownerType— краткий символьный код типа объекта CRM=ownerId— идентификатор созданного объекта CRM
Ошибки и диагностика
Если метод вернул ошибку, проверьте данные запроса.
|
Код |
Причина и действие |
|
|
Недостаточно прав для чтения каталога или цен. Проверьте права пользователя и scope |
|
|
Нет права на создание или изменение объекта CRM. Проверьте права пользователя в CRM |
|
|
В |
|
|
В |
|
|
Не переданы обязательные параметры. Проверьте |
Что важно учитывать
- crm.item.productrow.set заменяет все товарные позиции объекта CRM
- catalog.product.list возвращает товары, но не возвращает цены. Цены нужно получать методом catalog.price.list
- Для товаров с вариациями используйте идентификатор конкретной вариации товара
- Повторный запуск примера создает новый объект CRM и новые товарные позиции
- Если сумма объекта должна рассчитываться по товарным позициям, не передавайте ручную сумму в
opportunity