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







