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

Корзина в Битриксе: свои правила расчёта и вывод на сайте

За десяток проектов на 1С-Битрикс я не встречал магазина, где хватило бы штатной корзины без единой доработки. Битрикс корзина по умолчанию берёт цену из прайс-группы, накладывает скидки из модуля «Скидки и промоакции» и на этом успокаивается. Как только нужен пересчёт по сумме заказа, скидка за способ оплаты или наценка за регион доставки, штатных настроек не хватает. Дальше разберу, в каких местах менять логику расчёта корзины на Битриксе и как вывести результат в собственном шаблоне на сайте.

Как Битрикс считает цену корзины по умолчанию

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

Базовая цена берётся из типа цен (прайс-группы) в каталоге: розница, опт, цена для конкретной группы пользователей. Дальше в дело вступает модуль «Скидки и промоакции» из раздела Маркетинг: правила по сумме корзины, по количеству товара, по группе покупателей или купону. Правила этого модуля лежат в таблице Bitrix\Sale\Internals\DiscountTable, но создавать их прямым ORM-вызовом DiscountTable::add() нельзя: такой вызов не выполняет всю подготовку правила и не создаёт привязки к группам пользователей, для создания есть CSaleDiscount::Add(). Применяются правила автоматически при пересчёте корзины, а результат, который применился к заказу, сохраняется в таблицах OrderDiscountTable, OrderCouponsTable, OrderRulesTable и OrderRulesDescrTable.

Каждая позиция корзины это объект \Bitrix\Sale\BasketItem с полями PRICE (текущая цена), BASE_PRICE (цена без скидок) и DISCOUNT_PRICE (сумма скидки). Штатный механизм отлично справляется с процентными и фиксированными скидками, привязанными к каталогу. Проблемы начинаются там, где правило зависит от чего-то вне каталога: способа оплаты, региона, истории заказов клиента или внешнего API.

Когда стандартных скидок в корзине не хватает

На практике за нестандартной логикой обращаются в похожих ситуациях.

  • Наценка или скидка за способ доставки: например, самовывоз дешевле, а доставка СДЭК в отдалённый регион дороже расчётной по каталогу.
  • Скидка за оплату определённым способом: при подключённом эквайринге T‑Bank часть магазинов даёт скидку за оплату картой онлайн вместо наличных курьеру.
  • Индивидуальные условия для конкретного клиента или группы клиентов, которые не укладываются в стандартные группы пользователей Битрикса.
  • Пересчёт корзины по внешним данным: курс валюты, остаток на складе поставщика, тариф транспортной компании.
  • Ограничение или комбинирование скидок, например запрет на суммирование промокода с акцией, хотя штатный механизм скидок это разрешает.

Все эти случаи объединяет одно: логика не описывается через интерфейс модуля скидок и требует кода.

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

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

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

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

Свои правила расчёта: Basket API и AJAX-пересчёт

Для кастомного расчёта я использую объектную модель D7, класс \Bitrix\Sale\Basket, а не старые процедурные функции над корзиной: она удобнее и не требует пересчитывать скидки каталога вручную.

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

ls /local/ajax/
# recalc-basket.php: обработчик пересчёта корзины

Внутри такого обработчика корзина загружается через \Bitrix\Sale\Basket::loadItemsForFUser(), для каждой позиции берётся количество через getQuantity() и текущая цена через getPrice(), а собственная цена задаётся через setFields() с полями CUSTOM_PRICE = 'Y', PRICE и CURRENCY: значение Y включает собственную цену позиции, и пересчёт не должен заменить PRICE обычной ценой каталога. Если правится цена позиции уже оформленного заказа, поле дополнительно помечают пользовательским через markFieldCustom('PRICE'). После цикла по позициям остаётся вызвать $basket->save().

Для правил, которые не зависят от действий пользователя в моменте, например ежедневной переоценки по курсу валют, я ставлю агент через CAgent::AddAgent(). Агент пересчитывает отложенные корзины по расписанию, а не при каждом визите покупателя.

Когда таких правил становится много и они начинают конфликтовать друг с другом (скидка плюс наценка плюс округление), проще вынести их в отдельный класс-калькулятор, а не размазывать по обработчикам событий. Если разбираться с этим своими силами долго и логика продолжает расти, доработка и поддержка 1С-Битрикс снимает эту задачу с разработчика на стороне клиента.

Способ Когда срабатывает Для каких правил подходит
Модуль скидок Битрикса Автоматически при пересчёте корзины Скидки по сумме, количеству, купону, без привязки к внешним условиям
AJAX-обработчик на Basket API По действию пользователя: смена количества, способа доставки Правила, зависящие от выбора в интерфейсе корзины
Агент (CAgent) По расписанию, в фоне Переоценка по курсу, синхронизация с внешним прайсом

Дополнительные поля позиции корзины: PROPS и свойства товара

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

Для этого при добавлении товара в корзину я передаю параметр PROPS в \CSaleBasket::Add(): массив с ключами NAME, CODE и VALUE для каждого свойства. Эти свойства хранятся вместе с позицией корзины и переходят в заказ при оформлении, их видно и в админке, и в шаблоне корзины на сайте.

Второй вариант, актуальный для D7-логики: работать через коллекцию свойств позиции, \Bitrix\Sale\BasketPropertyItem. Разница в основном в том, из какого места вызывается код. Старый CSaleBasket::Add() проще применить в существующем шаблоне карточки товара, где логика добавления в корзину уже завязана на старое API.

Свой шаблон вывода корзины на сайте

Штатный шаблон компонента bitrix:sale.basket.basket показывает то, что знает из полей BasketItem: название, цену, скидку, количество. Кастомные свойства и результат своих правил расчёта в нём по умолчанию не отображаются, шаблон под них не рассчитан.

Чтобы вывести свои данные, копирую шаблон компонента в /local/templates/, не трогая исходники в /bitrix/:

mkdir -p /local/templates/.default/components/bitrix/sale.basket.basket
cp -r /bitrix/components/bitrix/sale.basket.basket/templates/.default \
  /local/templates/.default/components/bitrix/sale.basket.basket/.default

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

Для мобильных клиентов и виджетов на других страницах сайта та же логика расчёта должна быть доступна не только через компонент, а как отдельный AJAX-эндпоинт. Это тот же обработчик из предыдущего раздела, но с ответом в JSON вместо HTML.

Ошибки, из-за которых своя логика ломает заказ

За практикой кастомных корзин набралось несколько повторяющихся граблей.

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

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

Разная логика для гостя и авторизованного пользователя. Корзина в Битриксе привязана к FUSER, и для неавторизованных посетителей это временный идентификатор. Если правило завязано на группу пользователей, для гостя оно просто не сработает, и это стоит явно предусмотреть на этапе проектирования, а не находить багом на тестировании.

Пересчёт тяжёлой логики на каждый показ страницы. Запрос к API транспортной компании или обращение к внешнему прайсу в обработчике, который дёргается при каждой отрисовке корзины, ощутимо тормозит страницу. Такие данные стоит кэшировать на разумный срок или считать агентом в фоне, а не при каждом заходе покупателя.

1С-Битрикс регулярно обновляется, поэтому перед внедрением я сверяюсь с актуальной документацией.

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

Можно ли поменять цену в корзине, не трогая цену товара в каталоге?

Да, через setFields() у объекта BasketItem с полями CUSTOM_PRICE = ‘Y’, PRICE и CURRENCY. Значение Y включает собственную цену позиции, и пересчёт не заменяет PRICE обычной ценой каталога. Цена в карточке товара при этом остаётся прежней.

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

Через параметр PROPS в CSaleBasket::Add() либо через BasketPropertyItem в объектной модели D7. Оба варианта дают дополнительное свойство, привязанное к позиции корзины, которое переходит в заказ при оформлении.

Нужен ли отдельный сервер для пересчёта корзины по своим правилам?

Нет, вся логика на PHP и MySQL и работает на том же хостинге, где уже стоит Битрикс. Отдельный сервер имеет смысл только если пересчёт тянет за собой тяжёлые фоновые интеграции с очередями сообщений, а для скидок и наценок в рамках сайта это не требуется.

Что делать, если после доработки корзина стала считать медленно?

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

Есть задача?

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

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

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