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

Скидки из кода - создание, обязательные поля, перенос

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

Решение

Где живут правила

Читаем оба семейства скидок:

use Bitrix\Main\Loader;
Loader::includeModule('sale');
Loader::includeModule('catalog');
// правило корзины: считается на составе заказа целиком
$rules = \Bitrix\Sale\Internals\DiscountTable::getList([
'select' => ['ID', 'NAME', 'LID', 'ACTIVE'],
])->fetchAll();
// скидка каталога: меняет цену товара ещё на витрине
$catalog = \Bitrix\Catalog\DiscountTable::getList([
'select' => ['ID', 'NAME', 'SITE_ID', 'VALUE_TYPE', 'ACTIVE'],
])->fetchAll();

Правило корзины и скидка каталога - разные сущности в разных модулях, и путать их дорого. D7-таблицы здесь годятся только для чтения: методы add, update и delete у Catalog\DiscountTable оставлены заглушками и бросают исключение.

Создание правила корзины скриптом

Заводим скидку десять процентов от суммы:

$discountId = \CSaleDiscount::Add([
'LID' => 's1',
'NAME' => '10% для заказов от 5000',
'ACTIVE' => 'Y',
'USER_GROUPS' => [2], // обязателен и не бывает пустым
'CURRENCY' => 'RUB', // нужна и процентной скидке тоже
'CONDITIONS' => [
'CLASS_ID' => 'CondGroup',
'DATA' => ['All' => 'AND', 'True' => 'True'],
'CHILDREN' => [[
'CLASS_ID' => 'CondBsktAmtGroup',
'DATA' => ['logic' => 'EqGr', 'Value' => 5000], // EqGr это >=
'CHILDREN' => [],
]],
],
'ACTIONS' => [
'CLASS_ID' => 'CondGroup',
'DATA' => ['All' => 'AND'],
'CHILDREN' => [[
'CLASS_ID' => 'ActSaleBsktGrp',
'DATA' => ['Type' => 'Discount', 'Value' => 10, 'Unit' => 'Perc'],
'CHILDREN' => [],
]],
],
]);

Группы пользователей задаются в момент создания, и при пустом списке скидка молча не применится ни к кому. Плоские поля вроде PRICE_FROM и DISCOUNT_VALUE устарели с версии 14.0, поэтому условия описываются деревом. Правку существующего правила делает CSaleDiscount::Update с тем же набором полей.

Скидка каталога и её обязательные поля

Заводим процентную скидку на конкретный товар:

$catalogId = \CCatalogDiscount::Add([
'SITE_ID' => 's1',
'ACTIVE' => 'Y',
'NAME' => 'Скидка 10% на товар',
'VALUE_TYPE' => \CCatalogDiscount::TYPE_PERCENT,
'VALUE' => 10,
'CURRENCY' => 'RUB', // без валюты Add вернёт false
'CONDITIONS' => [
'CLASS_ID' => 'CondGroup',
'DATA' => ['All' => 'AND', 'True' => 'True'],
'CHILDREN' => [[
'CLASS_ID' => 'CondIBElement',
'DATA' => ['logic' => 'Equal', 'value' => $productId],
]],
],
]);
if (!$catalogId) {
global $APPLICATION;
throw new \RuntimeException($APPLICATION->GetException()->GetString());
}

Метод возвращает false без единого слова о причине: текст ошибки лежит в $APPLICATION->GetException(), и без этой проверки скрипт молча создаёт ноль скидок. Условие CondIBElement ссылается на товар по идентификатору, а несуществующий идентификатор в списке ломает всю запись целиком.

Включение купонов у скидки

Создаём купон и разрешаем его использование:

$coupon = \Bitrix\Catalog\DiscountCouponTable::add([
'DISCOUNT_ID' => $catalogId,
'ACTIVE' => 'Y',
'COUPON' => \CatalogGenerateCoupon(), // не длиннее 32 символов
'TYPE' => \Bitrix\Catalog\DiscountCouponTable::TYPE_ONE_ORDER,
]);
// признак работы с купонами включается отдельным вызовом, а не полем скидки
\Bitrix\Catalog\DiscountTable::setUseCoupons($catalogId, 'Y');

Поле USE_COUPONS в CCatalogDiscount::Add передавать нельзя: купоны от этого не включатся, и код будет отвергаться при проверке. Порядок обязателен - сначала купон, потом признак у скидки.

Проверка, что скидка применилась

Смотрим скидки товара и сработавшие правила заказа:

global $USER;
$active = \CCatalogDiscount::GetDiscountByProduct(
$productId, $USER->GetUserGroupArray(), 'N', [$basePriceTypeId], SITE_ID
);
print_r($active); // пустой массив - скидок нет, false - товар некорректен
$order = \Bitrix\Sale\Order::load($orderId);
print_r($order->getDiscount()->getApplyResult()); // что сработало в заказе

Первый вызов проверяет скидку каталога с учётом групп покупателя и прав на тип цены. Второй показывает правила корзины по заказу и отвечает на вопрос точнее, чем сравнение сумм глазами.

Перенос набора скидок на другой стенд

Выгружаем правила вместе с условиями:

$dump = [];
foreach (\Bitrix\Sale\Internals\DiscountTable::getList([
'select' => ['ID', 'NAME', 'LID', 'ACTIVE', 'SORT',
'CURRENCY', 'CONDITIONS', 'ACTIONS'],
])->fetchAll() as $row) {
unset($row['ID']); // ID на приёмнике будет свой
$row['CONDITIONS'] = unserialize($row['CONDITIONS']);
$row['ACTIONS'] = unserialize($row['ACTIONS']);
$dump[] = $row;
}
file_put_contents(__DIR__ . '/discounts.json', json_encode($dump));

На приёмнике файл читается и каждая запись уходит в CSaleDiscount::Add. Перед этим правятся все идентификаторы: LID берётся от нужного сайта, а товары и разделы внутри условий сопоставляются по внешнему коду. Группы пользователей в дамп не попадают: они хранятся отдельной связью и задаются полем USER_GROUPS при создании, поэтому их сопоставляют по названию руками. Купоны, счётчики их применения и статистика скидок тоже заводятся на приёмнике отдельно.

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

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

Скидка создана скриптом, но не применяется ни к одному заказу.

Поле USER_GROUPS пустое либо не передано вовсе. Группы задаются только в момент создания, и без них правило неприменимо.

CCatalogDiscount::Add вернул false без сообщения.

Чаще всего не передана CURRENCY: она обязательна даже для процентной скидки. Текст ошибки лежит в $APPLICATION->GetException().

Обновление скидки через Update не проходит.

В условии остались идентификаторы товаров, которых на этом стенде нет. Несуществующий элемент в списке отменяет запись целиком.

Выборка скидок в скрипте по крону возвращает пусто.

В консоли константа SITE_ID не определена и содержит код языка вместо кода сайта. Фильтр по LID при этом не находит ничего.

Купон создан, а скидка его не видит.

Не вызван DiscountTable::setUseCoupons после добавления купона. Признак работы с купонами включается отдельно от полей скидки.

Метод добавления купона вернул объект вместо кода.

Это Result, а не строка: успех проверяют через isSuccess, идентификатор берут через getId.

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

Можно ли скопировать скидку на другой сайт из админки?

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

Почему нельзя писать скидки через D7-таблицы?

Методы add, update и delete у Catalog\DiscountTable оставлены заглушками и бросают исключение. Запись идёт легаси-классами, а D7-таблица нужна для чтения и для setUseCoupons.

Как правильно обновлять скидку через CSaleDiscount::Update?

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

Что переносится вместе со скидкой, а что нет?

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

Как узнать, какая скидка сработала в конкретном заказе?

Методом getApplyResult у объекта скидки заказа после его загрузки. Если вызов падает на getDiscount, значит заказ по этому номеру не найден и загрузка вернула пустое значение.

Смежное

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