Предупреждение: код работает на версиях модуля sale 16.0 и выше.

В 16 версии магазина Битрикса были переработаны обработчики служб доставки. Обработчиками доставки являются классы-наследники \Bitrix\Sale\Delivery\Services\Base. Встроенные обработчики лежат в папке /bitrix/modules/sale/handlers/delivery/. Там можно ознакомиться с примерами новых обработчиков.

Чтобы подключить свой собственный обработчик, вроде бы нужно положить папку со своим классом в /bitrix/php_interface/include/sale_delivery/ или /local/php_interface/include/sale_delivery/, но на текущей версии(16.0.5) это не сработало. Но есть рабочий альтернативный метод подключения любого класса как обработчика, в init.php добавляем:

$eventManager = \Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler('sale', 'onSaleDeliveryHandlersClassNamesBuildList', 'addCustomDeliveryServices');

function addCustomDeliveryServices(\Bitrix\Main\Event $event)
{
    $result = new \Bitrix\Main\EventResult(
        \Bitrix\Main\EventResult::SUCCESS, 
        array(
            '\YourNamespace\YetAnotherDeliveryHandler' => '/local/classes/YetAnotherDeliveryHandler.php'
        )
    );

    return $result;
}

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

По указанному пути создаём класс:

namespace YourNamespace;

class YetAnotherDeliveryHandler extends \Bitrix\Sale\Delivery\Services\Base
{
    protected static $isCalculatePriceImmediately = true;
    protected static $whetherAdminExtraServicesShow = false;
}

Добавляем несколько методов, смысл которых вполне понятен из названия:

public function __construct(array $initParams)
{
    parent::__construct($initParams);
}

public static function getClassTitle()
{
    return 'Yet Another Delivery';
}

public static function getClassDescription()
{
    return 'My custom handler for Yet Another Delivery Service';
}

public function isCalculatePriceImmediately()
{
    return self::$isCalculatePriceImmediately;
}

public static function whetherAdminExtraServicesShow()
{
    return self::$whetherAdminExtraServicesShow;
}

Следующий метод отвечает за настройки службы доставки:

Timeweb

protected function getConfigStructure()
{
    $result = array(
        'MAIN' => array(
            'TITLE' => 'Основные',
            'DESCRIPTION' => 'Основные настройки',
            'ITEMS' => array(
                'API_KEY' => array(
                    'TYPE' => 'STRING',
                    'NAME' => 'Ключ API',
                ),
                'TEST_MODE' => array(
                    'TYPE' => 'Y/N',
                    'NAME' => 'Тестовый режим',
                    'DEFAULT' => 'N'
                ),
                'PACKAGING_TYPE' => array(
                    'TYPE' => 'ENUM',
                    'NAME' => 'Тип упаковки',
                    'DEFAULT' => 'BOX',
                    'OPTIONS' => array(
                            'BOX' => 'Коробка',
                            'ENV' => 'Конверт',
                    )
                ),
            )
        )
    );
    return $result;
}

И, наконец, за расчёт стоимости отвечает метод calculateConcrete (аналог Calculate из старых обработчиков доставки):

protected function calculateConcrete(\Bitrix\Sale\Shipment $shipment = null)
{
    // Какие-то действия по получению стоимости и срока...

    $result = new \Bitrix\Sale\Delivery\CalculationResult();
    $result->setDeliveryPrice(
        roundEx(
            500,
            SALE_VALUE_PRECISION
        )
    );
    $result->setPeriodDescription('4-7 days');

    return $result;
}

Обязательно кешируйте все данные, полученные от внешних API с ключом, содержащим все нужные параметры для расчёта. Как кешировать данные в D7 читайте в этой статье. Как сделать запрос читайте в статье HTTP-запросы в Битрикс D7.

Аргументом является объект класса \Bitrix\Sale\Shipment, из которого можно получить объект заказа и нужные параметры расчёта:

$weight = $shipment->getWeight(); // вес отгрузки

$order = $shipment->getCollection()->getOrder(); // заказ
$props = $order->getPropertyCollection(); 
$locationCode = $props->getDeliveryLocation()->getValue(); // местоположение

Примечание: По информации ведущего разработчика Битрикс на данный момент (sale 16.0.5), пока не вышел обновлённый sale.order.ajax, в компоненте для совместимости в качестве $locationCode выступает ID местоположения, поэтому нужен такой временный костыль для получения кода местоположения:

if ($loc = \Bitrix\Sale\Location\LocationTable::getRowById($locationCode)) {
    $locationCode = $loc['CODE'];
}

Также доступен массив настроек службы доставки:

$isTestMode = ($this->config['MAIN']['TEST_MODE'] == "Y");

Метод calculateConcrete должен возвращать объект класса \Bitrix\Sale\Delivery\CalculationResult с доступными методами:

/** @param float $price */
public function setDeliveryPrice($price) {}
/** @param float $price */
public function setExtraServicesPrice($price) {}
/** @param string $description */
public function setPeriodDescription($description) {}
/** @param Bitrix\Main\Error $error */
public function addError($error) {}

Если при расчёте возникла ошибка, в CalculationResult можно добавить ошибки расчёта (эта ошибка будет выведена при оформлении заказа):

$result->addError(new Bitrix\Main\Error("Данный сервис недоступен для выбранного местоположения"));

Для проверки совместимости службы доставки и переданных параметров можно перегрузить метод isCompatible, например, самый простой вариант - проверить успешность расчёта, при наличии ошибок служба доставки просто не будет показываться при оформлении заказа:

public function isCompatible(\Bitrix\Sale\Shipment $shipment)
{
    $calcResult = self::calculateConcrete($shipment);
    return $calcResult->isSuccess();
}

Timeweb

Профили доставки

Для того, чтобы класс сервиса доставки мог поддерживать профили, добавляем в него поле и методы:

protected static $canHasProfiles = true;

public static function canHasProfiles()
{
    return self::$canHasProfiles;
}

public static function getChildrenClassNames()
{
    return array(
        '\YourNamespace\YetAnotherDeliveryProfile'
    );
}

public function getProfilesList()
{
    return array("Новый профиль");
}

В методе getChildrenClassNames указали, что профили могут быть объектами класса \YourNamespace\YetAnotherDeliveryProfile, создаём файл с классом и указываем его в обработчике, добавленном в начале статьи:

function addCustomDeliveryServices(\Bitrix\Main\Event $event)
{
    $result = new \Bitrix\Main\EventResult(
        \Bitrix\Main\EventResult::SUCCESS, 
        array(
            '\YourNamespace\YetAnotherDeliveryHandler' => '/local/classes/YetAnotherDeliveryHandler.php',
            '\YourNamespace\YetAnotherDeliveryProfile' => '/local/classes/YetAnotherDeliveryProfile.php',
        )
    );

    return $result;
}

По указанному пути размещаем класс:

namespace YourNamespace;

class YetAnotherDeliveryProfile extends \Bitrix\Sale\Delivery\Services\Base
{
    protected static $isProfile = true;
    protected static $parent = null;

    public function __construct(array $initParams)
    {
        parent::__construct($initParams);
        $this->parent = \Bitrix\Sale\Delivery\Services\Manager::getObjectById($this->parentId);
    }

    public static function getClassTitle()
    {
        return 'Yet Another Delivery profile';
    }

    public static function getClassDescription()
    {
        return 'My custom handler for Yet Another Delivery Service profile';
    }

    public function getParentService()
    {
        return $this->parent;
    }

    public function isCalculatePriceImmediately()
    {
        return $this->getParentService()->isCalculatePriceImmediately();
    }

    public static function isProfile()
    {
        return self::$isProfile;
    }
}

В остальном класс профиля является аналогом класса родительской службы.

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

protected function getConfigStructure()
{
    $result = array(
        "MAIN" => array(
            'TITLE' => 'Основные',
            'DESCRIPTION' => 'Основные настройки',
            'ITEMS' => array(
                'TARIFF_ID' => array(
                    "TYPE" => 'STRING',
                    "NAME" => 'ID тарифа службы доставки',
                ),
            )
        )
    );
    return $result;
}

За расчёт стоимости также отвечает метод calculateConcrete:

protected function calculateConcrete(\Bitrix\Sale\Shipment $shipment = null)
{
    // Какие-то действия по получению стоимости и срока...

    $result = new \Bitrix\Sale\Delivery\CalculationResult();
    $result->setDeliveryPrice(
        roundEx(
            500,
            SALE_VALUE_PRECISION
        )
    );
    $result->setPeriodDescription('2-3 days');

    return $result;
}

В методах профиля можно получить не только настройки профиля, но и настройки родительской службы:

$tariff = $this->config['MAIN']['TARIFF_ID'];
$login = $this->getParentService()->config['MAIN']['API_KEY'];

И также здесь можно проверять совместимость профилей (аналог метода Compability в старых службах доставки):

public function isCompatible(\Bitrix\Sale\Shipment $shipment)
{
    $calcResult = self::calculateConcrete($shipment);
    return $calcResult->isSuccess();
}

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

protected function calculateConcrete(\Bitrix\Sale\Shipment $shipment = null)
{
    throw new \Bitrix\Main\SystemException('Only profiles can calculate concrete');
}

Timeweb

События расчёта доставки

После расчёта доставки вызывается событие onSaleDeliveryServiceCalculate модуля sale с параметрами RESULT - результат расчёта (\Bitrix\Sale\Delivery\CalculationResult) и SHIPMENT - отгрузка (\Bitrix\Sale\Shipment). Можно использовать это событие для изменения результатов расчёта без вмешательства в код обработчика и без копирования стандартных обработчиков в свои пространства имён.

\Bitrix\Main\EventManager::getInstance()->addEventHandler('sale', 'onSaleDeliveryServiceCalculate', 'yourHandler');

function yourHandler(\Bitrix\Main\Event $event)
{
    $calcResult = $event->getParameter('RESULT');
    $shipment = $event->getParameter('SHIPMENT');

    // например, прибавим 200 рублей к стоимости доставки
    $newPrice = $calcResult->getDeliveryPrice() + 200;
    $calcResult->setDeliveryPrice($newPrice);

    return new \Bitrix\Main\EventResult(
        \Bitrix\Main\EventResult::SUCCESS,
        array(
            "RESULT" => $calcResult,
        )
    );
}

Читайте также: