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

Наша компанія займається розробкою, підтримкою та обслуговуванням рішень на Бітрікс та Бітрікс24 будь-якої складності. Від простих односторінкових сайтів до складних інтернет-магазинів, CRM систем з інтеграцією 1С та телефонії. Досвід розробників підтверджено сертифікатами від вендора.
Пропоновані послуги
Показано 1 з 1 послугУсі 1626 послуг
Розробка кастомного калькулятора доставки 1С-Бітрікс
Середня
~1-2 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851
  • 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
    585
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Розробка на базі 1С Підприємство для компанії МИРСАНБЕЛ
    751
  • image_crm_dolbimby_434_0.webp
    Розробка сайту на CRM Бітрікс24 для компанії DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Розробка на базі Бітрікс24 для компанії ТЕХНОТОРГКОМПЛЕКС
    989

Розробка кастомного калькулятора доставки 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 тижні