Разработка кастомного калькулятора доставки 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Разработка кастомного калькулятора доставки 1С-Битрикс
Средняя
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Разработка на базе Битрикс, Битрикс24, 1С для компании Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    745
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Разработка кастомного калькулятора доставки 1С-Битрикс

Стандартные службы доставки в 1С-Битрикс покрывают 80% типичных сценариев: фиксированная цена, процент от суммы, интеграция с транспортной компанией по API. Но как только появляется нестандартная логика — расчёт по весу и объёму одновременно, зонирование по координатам, тарифные сетки с матрицами условий, наценки за подъём на этаж — стандартные инструменты заканчиваются. Выходов два: скрутить логику в ограничениях существующей службы (хаотично, нечитаемо, ломается при обновлении) или разработать собственный обработчик доставки.

Архитектура обработчика доставки в D7

Начиная с Битрикс 14.0 все пользовательские службы доставки реализуются через класс, наследующий \Bitrix\Sale\Delivery\Services\Base. Файл обработчика размещается в /local/php_interface/include/sale_delivery/:

namespace MyProject\Delivery;

use Bitrix\Sale\Delivery\Services\Base;
use Bitrix\Sale\Delivery\Requests\RequestAbstract;
use Bitrix\Sale\Shipment;

class CustomCalculator extends Base
{
    protected static $isCalculatePriceImmediately = true;
    protected static $canHasProfiles = true;

    public static function getClassTitle(): string
    {
        return 'Кастомный калькулятор доставки';
    }

    public static function getClassDescription(): string
    {
        return 'Расчёт стоимости по весу, зоне и типу товара';
    }

    protected function calculateConcrete(Shipment $shipment): \Bitrix\Sale\Delivery\CalculationResult
    {
        $result = new \Bitrix\Sale\Delivery\CalculationResult();
        $price = $this->computePrice($shipment);

        if ($price === null) {
            $result->addError(new \Bitrix\Main\Error('Невозможно рассчитать доставку'));
            return $result;
        }

        $result->setDeliveryPrice($price);
        $result->setPeriodDescription($this->getPeriodText($shipment));
        return $result;
    }
}

Метод calculateConcrete — ключевой. Здесь вся логика расчёта. Метод получает объект Shipment с полным доступом к заказу, товарам, адресу, весу, объёму.

Получение параметров отгрузки

Внутри calculateConcrete доступны все данные:

private function computePrice(Shipment $shipment): ?float
{
    $basket = $shipment->getOrder()->getBasket();
    $totalWeight = 0;
    $totalVolume = 0;

    foreach ($basket as $item) {
        $props = $item->getPropertyCollection();
        $weight = (float)$item->getField('WEIGHT') * $item->getQuantity();
        $totalWeight += $weight;

        // Объём из свойств товара (Д × Ш × В в мм)
        $l = (float)$props->getItemByFieldValue('CODE', 'LENGTH')?->getValue() / 1000;
        $w = (float)$props->getItemByFieldValue('CODE', 'WIDTH')?->getValue() / 1000;
        $h = (float)$props->getItemByFieldValue('CODE', 'HEIGHT')?->getValue() / 1000;
        $totalVolume += $l * $w * $h * $item->getQuantity();
    }

    // Весогабаритный коэффициент: объёмный вес = объём(м³) × 250
    $volumeWeight = $totalVolume * 250;
    $billableWeight = max($totalWeight / 1000, $volumeWeight); // в кг

    return $this->getPriceByZoneAndWeight(
        $this->getDeliveryZone($shipment),
        $billableWeight
    );
}

Весогабаритный коэффициент 250 — стандарт большинства транспортных компаний (1 м³ = 250 кг расчётного веса). Если у клиента другой коэффициент — параметр выносится в настройки службы доставки.

Зонирование: от простого к сложному

Простое зонирование — по городу/региону из свойства заказа:

private function getDeliveryZone(Shipment $shipment): string
{
    $order = $shipment->getOrder();
    $props = $order->getPropertyCollection();
    $city = $props->getItemByOrderPropertyCode('CITY')?->getValue();

    return match(true) {
        in_array($city, ['Москва', 'Санкт-Петербург']) => 'zone1',
        in_array($city, ['Екатеринбург', 'Новосибирск', 'Казань']) => 'zone2',
        default => 'zone3',
    };
}

Зонирование по координатам — актуально для курьерской доставки внутри города (расчёт по расстоянию от склада):

private function getZoneByCoordinates(float $lat, float $lng): string
{
    $warehouseLat = 55.7558;
    $warehouseLng = 37.6173;

    // Формула гаверсинуса
    $earthRadius = 6371;
    $dLat = deg2rad($lat - $warehouseLat);
    $dLng = deg2rad($lng - $warehouseLng);
    $a = sin($dLat/2) ** 2 + cos(deg2rad($warehouseLat)) * cos(deg2rad($lat)) * sin($dLng/2) ** 2;
    $distance = $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a));

    return match(true) {
        $distance <= 10 => 'mkad',
        $distance <= 30 => 'mkad_plus30',
        $distance <= 50 => 'mkad_plus50',
        default => 'region',
    };
}

Координаты адреса берутся из Яндекс Геокодера или DaData при оформлении заказа и сохраняются в свойство заказа.

Тарифная матрица

Тарифы хранятся не в коде, а в таблице — иначе каждое изменение тарифа требует деплоя:

private function getPriceByZoneAndWeight(string $zone, float $weight): float
{
    // Кешируем тарифы — не дёргаем БД при каждом расчёте
    $cacheKey = "delivery_tariffs_{$zone}";
    if (!isset($this->tariffCache[$cacheKey])) {
        $this->tariffCache[$cacheKey] = $this->loadTariffs($zone);
    }

    $tariffs = $this->tariffCache[$cacheKey];
    // Тариф: ступенчатый — находим нужный диапазон веса
    foreach ($tariffs as $tier) {
        if ($weight <= $tier['max_weight']) {
            return $tier['base_price'] + ($weight - $tier['min_weight']) * $tier['per_kg'];
        }
    }

    // Сверхгабаритный — базовая цена + превышение
    $lastTier = end($tariffs);
    return $lastTier['base_price'] + ($weight - $lastTier['max_weight']) * $lastTier['oversize_per_kg'];
}

Таблица тарифов в БД позволяет менеджеру обновлять ставки через простой интерфейс без участия разработчика.

Дополнительные наценки

Реальные калькуляторы включают несколько слоёв наценок:

private function applyAdditionalCharges(float $basePrice, Shipment $shipment): float
{
    $price = $basePrice;

    // Наценка за хрупкие товары
    if ($this->hasFragileItems($shipment)) {
        $price += $price * 0.15; // +15%
    }

    // Наценка за подъём на этаж
    $floor = (int)$this->getOrderProperty($shipment, 'FLOOR');
    if ($floor > 1) {
        $price += ($floor - 1) * $this->getOption('FLOOR_SURCHARGE', 150);
    }

    // Наценка за наложенный платёж
    if ($this->isCashOnDelivery($shipment)) {
        $codPercent = (float)$this->getOption('COD_PERCENT', 3);
        $price += $shipment->getOrder()->getPrice() * ($codPercent / 100);
    }

    // Скидка для крупных клиентов (юрлица, группа B2B)
    if ($this->isB2BClient($shipment->getOrder())) {
        $price *= (1 - (float)$this->getOption('B2B_DISCOUNT', 0.1));
    }

    return round($price, 2);
}

Настройки службы в административном интерфейсе

Параметры, которые должен менять менеджер без доступа к коду, объявляются через getHandlerParams():

public static function getHandlerParams(): array
{
    return [
        'FLOOR_SURCHARGE' => [
            'TYPE' => 'NUMBER',
            'DEFAULT' => 150,
            'TITLE' => 'Наценка за этаж (руб)',
        ],
        'COD_PERCENT' => [
            'TYPE' => 'NUMBER',
            'DEFAULT' => 3,
            'TITLE' => 'Наценка за наложенный платёж (%)',
        ],
        'B2B_DISCOUNT' => [
            'TYPE' => 'NUMBER',
            'DEFAULT' => 0.1,
            'TITLE' => 'Скидка для B2B-клиентов (доля, 0.1 = 10%)',
        ],
        'VOLUME_WEIGHT_COEF' => [
            'TYPE' => 'NUMBER',
            'DEFAULT' => 250,
            'TITLE' => 'Коэффициент объёмного веса (кг/м³)',
        ],
    ];
}

Значения доступны через $this->getOption('PARAM_NAME') — читаются из настроек конкретного экземпляра службы.

Профили службы доставки

Один обработчик может представлять несколько тарифных планов через профили. Например, «Стандарт (5–7 дней)» и «Экспресс (1–2 дня)» — один класс, два профиля с разными коэффициентами:

protected static $canHasProfiles = true;

public static function getClassTitle(): string
{
    return 'Кастомный калькулятор';
}

// Базовый класс профиля
class ExpressProfile extends \Bitrix\Sale\Delivery\Services\BaseProfile
{
    public function calculateConcrete(Shipment $shipment): CalculationResult
    {
        $result = parent::calculateConcrete($shipment);
        // Умножаем на коэффициент экспресс-доставки
        $result->setDeliveryPrice($result->getDeliveryPrice() * 2.5);
        $result->setPeriodFrom(1);
        $result->setPeriodTo(2);
        return $result;
    }
}

Кеширование расчётов

Расчёт доставки вызывается при каждом изменении корзины — потенциально десятки раз за сессию. Кешируем результат по ключу из параметров отгрузки:

protected function calculateConcrete(Shipment $shipment): CalculationResult
{
    $cacheKey = $this->getCacheKey($shipment);
    $cache = \Bitrix\Main\Data\Cache::createInstance();

    if ($cache->initCache(600, $cacheKey, '/delivery/calc/')) {
        $cachedData = $cache->getVars();
        $result = new CalculationResult();
        $result->setDeliveryPrice($cachedData['price']);
        return $result;
    }

    $result = $this->doCalculate($shipment);

    $cache->startDataCache();
    $cache->endDataCache(['price' => $result->getDeliveryPrice()]);

    return $result;
}

private function getCacheKey(Shipment $shipment): string
{
    return md5(serialize([
        'zone' => $this->getDeliveryZone($shipment),
        'weight' => $this->getTotalWeight($shipment),
        'volume' => $this->getTotalVolume($shipment),
        'has_fragile' => $this->hasFragileItems($shipment),
        'floor' => $this->getOrderProperty($shipment, 'FLOOR'),
    ]));
}

TTL кеша — 600 секунд: тарифы не меняются чаще, а корзина с теми же параметрами получит мгновенный ответ.

Регистрация обработчика

После создания класса — регистрация в системе через init.php или отдельный модуль:

\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'sale',
    'onSaleDeliveryHandlersClassNames',
    function(\Bitrix\Main\Event $event) {
        $result = $event->getParameters();
        $result[] = '\MyProject\Delivery\CustomCalculator';
        return new \Bitrix\Main\EventResult(
            \Bitrix\Main\EventResult::SUCCESS,
            $result
        );
    }
);

После регистрации класс появляется в Магазин → Настройки → Службы доставки → Добавить — создаётся экземпляр с нужными настройками.

Сроки разработки

Сложность калькулятора Срок
Зонирование + ступенчатые тарифы по весу 2–3 дня
+ объёмный вес + дополнительные наценки 3–5 дней
+ зонирование по координатам + интерфейс управления тарифами 1–1.5 недели
+ несколько профилей + кеширование + тесты 1.5–2 недели