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







