Добавление товара в корзину с произвольной ценой (D7) - sidigi/bitrix-info GitHub Wiki

Чтобы добавить товар в корзину по своей цене, можно использовать несколько методов, рассмотрим их.

Метод 1. Использование CUSTOM_PRICE

Это самый простой метод для использования, необходимо использовать флаг CUSTOM_PRICE = 'Y', с его помощью мы как бы говорим системе поле PRICE находится под нашим контролем. Мы сами указываем цену, по которой товар попадает в корзину.

//ID товара
$productId = 111;
$quantity = 1;

$basket = \Bitrix\Sale\Basket::loadItemsForFUser(
   \Bitrix\Sale\Fuser::getId(), 
   \Bitrix\Main\Context::getCurrent()->getSite()
);

if ($item = $basket->getExistsItem('catalog', $productId)){
   $item->setField('QUANTITY', $item->getQuantity() + $quantity);
}else{
   $item = $basket->createItem('catalog', $productId);
   $item->setFields([
      'QUANTITY' => $quantity,
      'CURRENCY' => \Bitrix\Currency\CurrencyManager::getBaseCurrency(),
      'LID' => \Bitrix\Main\Context::getCurrent()->getSite(),
      'PRICE' => 123,
      'CUSTOM_PRICE' => 'Y',
   ]);
}

$basket->save();

Этот метод прост и достаточно удобен, если стоит задача добавить товар по определённой цене. Но у метода есть свои недостатки:

  • Пересчёта цены не будет никогда, ни в корзине, ни в админке. Товар положен в корзину "намертво", при смене цены товара этот товар не бует пересчитан
  • Не указан провайдер, а значит о пересчётах и вклинивании в алгоритм расчёта цены можно забыть.

При должной постановке задачи, эти минусы превращаются в плюсы.

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

'IGNORE_CALLBACK_FUNC' => 'Y',

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

   $item->setFields([
      'QUANTITY' => $quantity,
      'CURRENCY' => \Bitrix\Currency\CurrencyManager::getBaseCurrency(),
      'LID' => \Bitrix\Main\Context::getCurrent()->getSite(),
      'PRODUCT_PROVIDER_CLASS' => 'CCatalogProductProvider',
      'PRICE' => 123,
      'CUSTOM_PRICE' => 'Y',
      'IGNORE_CALLBACK_FUNC' => 'Y',
   ]);

Метод 2. Использование событий OnGetOptimalPrice и OnGetOptimalPriceResult

Этот метод предпочтительно использовать, если в системе у вас уже хранятся цены, скидки для товара, но механизм их применения немного не стандартный (то есть нет своего алгоритма высчитывания цены). Обычно этот метод хорошо подходит для региональной цены, сезонной цены, каких то необычных скидок, вообщем чего угодно, лишь бы сами цены и скидки хранились в системе битрикс. Необходимо отметить, что эти события срабатывают в корзине, в заказе и в закзае в админке. По сути это обработчики цен, они возвращают набор данных по цене, скидкам и т.д.

  • OnGetOptimalPrice применяется до всех расчётов с ценой
  • OnGetOptimalPriceResult применяется после всех расчётов (работа с результирующим массивом). Подключается только если не отработало событие OnGetOptimalPrice. Если обработчик вернет false - это значение вернет CCatalogProduct::GetOptimalPrice.

То есть возможно использование только одного из обработчиков - что логично.

OnGetOptimalPrice

Может возвращать:

  • true - обработчик ничего не сделал, будет выполнена работа метода CCatalogProduct::GetOptimalPrice;
  • false - возникла ошибка, работа метода прерывается;
  • array, описывающий наименьшую цену для товара:
'PRICE' => [
   'ID' => 5910,
   'CATALOG_GROUP_ID' => 6,
   'PRICE' => 900,
   'CURRENCY' => 'UAH',
   'ELEMENT_IBLOCK_ID' => 7,
   'VAT_RATE' => 0,
   'VAT_INCLUDED' => 'N',
],
'DISCOUNT_PRICE' => 880,
'DISCOUNT' => [
   'ID' => 3,
   'TYPE' => 0,
   'SITE_ID' => 's1',
   'ACTIVE' => 'Y',
   'ACTIVE_FROM' => '',
   'ACTIVE_TO' => '',
   'RENEWAL' => 'N',
   'NAME' => 'test',
   'SORT' => 100,
   'MAX_DISCOUNT' => 0.0000,
   'VALUE_TYPE' => 'F',
   'VALUE' => 20.0000,
   'CURRENCY' => 'UAH',
   'PRIORITY' => 1,
   'LAST_DISCOUNT' => 'Y',
   'COUPON' => 'CP-4PC6V-4IFUJR8',
   'COUPON_ONE_TIME' => 'N',
   'COUPON_ACTIVE' => 'Y',
   'DISCOUNT_CONVERT' => 20.0000,
],
'DISCOUNT_LIST' => [
   [
      'ID' => 3,
      'TYPE' => 0,
      'SITE_ID' => 's1',
      'ACTIVE' => 'Y',
      'ACTIVE_FROM' => '',
      'ACTIVE_TO' => '',
      'RENEWAL' => 'N',
      'NAME' => 'test',
      'SORT' => 100,
      'MAX_DISCOUNT' => 0.0000,
      'VALUE_TYPE' => 'F',
      'VALUE' => 20.0000,
      'CURRENCY' => 'UAH',
      'PRIORITY' => 1,
      'LAST_DISCOUNT' => 'Y',
      'COUPON' => 'CP-4PC6V-4IFUJR8',
      'COUPON_ONE_TIME' => 'N',
      'COUPON_ACTIVE' => 'Y',
      'DISCOUNT_CONVERT' => 20.0000,
   ]
],
 "RESULT_PRICE" => [
    "BASE_PRICE" => '',
    "DISCOUNT_PRICE"  => '',
    "DISCOUNT"  => '', //итоговая скидка (разница между BASE_PRICE и DISCOUNT_PRICE) 
    "PERCENT" => '', //итоговая скидка в процентах 
    "CURRENCY"  => ''
    ]
];

В качестве цены можно указывать и произвольную цену.

Пример изменения цены в зависимости от каких то условий:

$eventManager = Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler('catalog', 'OnGetOptimalPrice', function(
    $productId,
    $quantity = 1,
    $arUserGroups = [],
    $renewal = "N",
    $arPrices = [],
    $siteID = false,
    $arDiscountCoupons = false){

   $prices = \CCatalogProduct::GetByIDEx($productId);
   $regionPriceId = get_region_price_id(); //мнимая функция, которая возвращает ID цены в зависимости от региона
   
   if (!$regionPriceId)
      return true;

   $price = $prices['PRICES'][$regionPriceId]['PRICE'];

   return [
      'PRICE' => [
         "ID" => $productId,
         'CATALOG_GROUP_ID' => $regionPriceId,
         'PRICE' => $price,
         'CURRENCY' => \Bitrix\Currency\CurrencyManager::getBaseCurrency(),
         'ELEMENT_IBLOCK_ID' => $productId,
         'VAT_INCLUDED' => "Y",
      ],
      'DISCOUNT' => [
          'VALUE' => '',
          'CURRENCY' => "RUB",
       ],
   ];

});

OnGetOptimalPriceResult

Этот метод уже возвращает все посчитанные данные, он идеально подходит когда конечную цену надо изменить (например добавить в цену стоимость упаковки)

Пример изменения конечной цены товара

$eventManager = Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler('catalog', 'OnGetOptimalPriceResult', function(&$result){
   $result['PRICE']['PRICE'] += 100;
});

Метод 3. Свой провайдер цен

Этот метод наиболее оптимален, так как все манипуляиции с пересчётом цен мы можем произвести в одном месте. Для начала нужно указать класс провайдера, при добавлении в корзину:

//ID товара
$productId = 111;
$quantity = 1;

$basket = \Bitrix\Sale\Basket::loadItemsForFUser(
   \Bitrix\Sale\Fuser::getId(), 
   \Bitrix\Main\Context::getCurrent()->getSite()
);

if ($item = $basket->getExistsItem('catalog', $productId)){
   $item->setField('QUANTITY', $item->getQuantity() + $quantity);
}else{
   $item = $basket->createItem('catalog', $productId);
   $item->setFields([
      'QUANTITY' => $quantity,
      'CURRENCY' => \Bitrix\Currency\CurrencyManager::getBaseCurrency(),
      'LID' => \Bitrix\Main\Context::getCurrent()->getSite(),
      'PRODUCT_PROVIDER_CLASS' => '\Site\Sale\CatalogProductProvider',
      'CUSTOM_PRICE' => 'Y',
   ]);
}

$basket->save();

Всё, теперь для расчётов используем свой собственный класс провайдер. Для этого опишем свой провайдер, унаследовавшись от CCatalogProductProvider. Для получения цен в публичной части используется метод GetProductData.

namespace Site\Sale;

use \Bitrix\Main\Loader,
    \Bitrix\Main\Localization\Loc;

Loader::includeModule('catalog');
Loader::includeModule('sale');

class CatalogProductProvider extends \CCatalogProductProvider
{
    public static function GetProductData($params)
    {
        //Получение готового массива цен
        $result = parent::GetProductData($params);

        //Манипуляции с ценами
        $result = [
            'BASE_PRICE' => $productPrice['PRICE'],
            'PRICE' => ($productPrice['DISCOUNT_PRICE']) ? $productPrice['DISCOUNT_PRICE'] : $productPrice['PRICE'],
        ] + $result;

        if ($productPrice['DISCOUNT_VALUE']){
            $result = [
                'DISCOUNT_PRICE' => $productPrice['PRICE'] - $productPrice['DISCOUNT_PRICE'],
                'DISCOUNT_VALUE' => $productPrice['DISCOUNT_VALUE'],
            ] + $result;
        }

        //возвращаем готовый массив
        return $result;
    }
}

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

 public static function OrderProduct($params) {
 
    $result = parent::OrderProduct($params);
 
    return $result;
  }

Здесь можно посмотреть поля записи корзины Поля записи корзины

Полезные ссылки