Служба доставки CDEK для Битрикса

В стандартной поставке Битрикса нет службы доставки CDEK. Чтобы исправить этот недостаток, предлагаем вашему вниманию класс обработчик CDeliveryCDEK.

Особенности расчета стоимости службы доставки CDEK являются:

  • задание в качестве параметров к запросу кодов городов, которые приводятся в документации;
  • необходимость задания весов товаров и габаритов или объема

Т.к. в документации приведена xls таблица городов, пользоваться ей из php невозможно. Поэтому мы сформируем таблицу в MySQL с именем `cdek_cities`, для удобного извлечения кодов городов. Таблица создается автоматически при первом обращении к обработчику, поэтому вам нужно дать пользователю MySQL права на создание таблиц.

Примечание. Битрикс не рекомендует пользоваться базой данных напрмямую в обход API битрикса, поэтому стандартные обработчики создают файлы городов на диске, а не в базе. Нам кажется это неудобным, поэтому мы пренебрежем этой рекомендацией битрикса. Если у вас в проекте уже существует таблица с именем cdek_cities, вам придется самим переписать имя таблицы в коде.

Для добавления обработчика CDEK в битрикс вам понадобятся три файла.

  • /bitrix/php_interface/include/sale_delivery/delivery_cdek.php — файл обработчика;
  • /bitrix/php_interface/include/sale_delivery/cdek_cities.sql — файл дампа базы данных городов, этот файл будет удален после создания базы данных. Файл можно скачать cdek_cities.rar и перед копированием разархивировать.
  • /bitrix/modules/sale/lang/ru/delivery/delivery_cdek.php — файл региональных настроек, вы можете создать файл и для других языков, заменив строки своими.

Тексты скриптов:

/bitrix/php_interface/include/sale_delivery/delivery_cdek.php

<?
/*********************************************************************************
Delivery handler for CDEK (http://www.cdek.ru/)
Files:
 - cdek_cities.sql - cdek city datebase
*********************************************************************************/

CModule::IncludeModule("sale");

IncludeModuleLangFile('/bitrix/modules/sale/delivery/delivery_cdek.php');

class CDeliveryCDEK {
	static private $profiles = array('default');
	static private $sql_create_table = '
CREATE TABLE IF NOT EXISTS `cdek_cities` (
`ID` int(10) unsigned NOT NULL DEFAULT "0",
`Name` varchar(150) COLLATE utf8_unicode_ci NOT NULL DEFAULT "",
`CityName` varchar(150) COLLATE utf8_unicode_ci NOT NULL DEFAULT "",
`OblName` varchar(150) COLLATE utf8_unicode_ci NOT NULL DEFAULT "",
`Center` char(1) COLLATE utf8_unicode_ci NOT NULL DEFAULT "",
`PostCode` text COLLATE utf8_unicode_ci NOT NULL,
`NalSumLimit` float NOT NULL,
PRIMARY KEY (`ID`),
KEY `CityName` (`CityName`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci';
	static private $access = array(
		'script' => 'http://api.edostavka.ru/calculator/calculate_price_by_json_request.php',
		'version' => '1.0',
	);
	private function getSecure($password) {
		return md5(date('Y-m-d').'&'.$password);
	}
	private function check_table(){
		global $DB;
		if(!isset($DB) || !($res = $DB->Query('DESCRIBE `cdek_cities`', true))) return false;
		while ($r = $res->Fetch()) $table = $r;
		return isset($table);
	}
	private function create_table(){
		global $DB;
		if(!isset($DB)
			|| !$DB->Query(self::$sql_create_table)
			|| !self::check_table()
			|| !is_file($filename = dirname(__FILE__).'/cdek_cities.sql')
			|| !($file = @file($filename))) return false;
		foreach($file as $sql){
			if(!$DB->Query($sql)) {
				$res = $DB->Query('DROP TABLE `cdek_cities`', true);
				return false;
			}
		}
		@unlink($filename);
		return true;
	}
	private function isAviable(){
		return(function_exists('json_encode')
			&& extension_loaded('curl')
			&& function_exists('http_build_query')
			)
			&& (self::check_table() || self::create_table());
	}
	private function GetDescription(){
		return GetMessage(self::isAviable() ? 'SALE_DH_CDEK_DESCRIPTION' : 'SALE_DH_CDEK_DESCRIPTION_ERROR');
	}
	function Init() {
		$base_currency = ($arCurrency = CCurrency::GetByID('RUR')) ? 'RUR' : 'RUB';
		$profiles = array();
		foreach(self::$profiles as $profile){
			$profiles[$profile] = array(
				'TITLE' => GetMessage('SALE_DH_CDEK_'.strtoupper($profile).'_TITLE'),
				'DESCRIPTION' => GetMessage('SALE_DH_CDEK_'.strtoupper($profile).'_DESCRIPTION'),
				'RESTRICTIONS_WEIGHT' => array(0),
				'RESTRICTIONS_SUM' => array(0),
			);
		}
		return array(
			'SID' => 'cdek',
			'NAME' => GetMessage('SALE_DH_CDEK_NAME'),
			'DESCRIPTION' => self::GetDescription(),
			'DESCRIPTION_INNER' => GetMessage('SALE_DH_CDEK_DESCRIPTION_INNER'),
			'BASE_CURRENCY' => $base_currency,
			'HANDLER' => __FILE__,
			'DBGETSETTINGS' => array(__CLASS__, 'GetSettings'),
			'DBSETSETTINGS' => array(__CLASS__, 'SetSettings'),
			'GETCONFIG' => array(__CLASS__, 'GetConfig'),
			'COMPABILITY' => array(__CLASS__, 'Compability'),
			'CALCULATOR' => array(__CLASS__, 'Calculate'),
			'PROFILES' => $profiles
		);
	}
	function GetConfig() {
		$arConfig = array(
			'CONFIG_GROUPS' => array(
				'all' => GetMessage('SALE_DH_CDEK_CONFIG_TITLE'),
			),
			'CONFIG' => array(
				'cdek_login' => array(
					'TYPE' => 'STRING',
					'DEFAULT' => '',
					'TITLE' => GetMessage('SALE_DH_CDEK_CONFIG_LOGIN'),
					'GROUP' => 'all',
					'VALUES' => '',
				),
				'cdek_password' => array(
					'TYPE' => 'PASSWORD',
					'DEFAULT' => '',
					'TITLE' => GetMessage('SALE_DH_CDEK_CONFIG_PASSWORD'),
					'GROUP' => 'all',
				),
				'cdek_tariffId' => array(
					'TYPE' => 'STRING',
					'DEFAULT' => '137',
					'TITLE' => GetMessage('SALE_DH_CDEK_CONFIG_TARIFID'),
					'GROUP' => 'all',
				),
				'cdek_modeId' => array(
					'TYPE' => 'STRING',
					'DEFAULT' => '3',
					'TITLE' => GetMessage('SALE_DH_CDEK_CONFIG_MODEID'),
					'GROUP' => 'all',
				),
				'cdek_volume' => array(
					'TYPE' => 'STRING',
					'DEFAULT' => '8000',
					'TITLE' => GetMessage('SALE_DH_CDEK_CONFIG_VOLUME'),
					'GROUP' => 'all',
				),
				'cdek_weight' => array(
					'TYPE' => 'STRING',
					'DEFAULT' => '1000',
					'TITLE' => GetMessage('SALE_DH_CDEK_CONFIG_WEIGHT'),
					'GROUP' => 'all',
				)
			)
		);
		return $arConfig; 
	}
	function GetSettings($strSettings) {
		return unserialize($strSettings);
	}
	function SetSettings($arSettings) {
		foreach ($arSettings as $key => $value){
			if(strpos($key, 'cdek_') !== false) $arSettings[$key] = $value;
			else{
				if($value != '') $arSettings[$key] = doubleval($value);
				else unset($arSettings[$key]);
			}
		}
		return serialize($arSettings);
	}
	function Calculate($profile, $arConfig, $arOrder, $STEP, $TEMP = false) {
		global $DB;
		if(!self::isAviable()) {
			return array(
				'RESULT' => 'ERROR',
				'TEXT' => GetMessage('SALE_DH_CDEK_DESCRIPTION_ERROR')
			);
		}
		if(empty($arConfig['cdek_tariffId']['VALUE'])
			|| empty($arConfig['cdek_modeId']['VALUE'])
			|| empty($arConfig['cdek_volume']['VALUE'])) {
			return array(
				'RESULT' => 'ERROR',
				'TEXT' => GetMessage('SALE_DH_CDEK_ERROR_NOSETTINGS')
			);
		}
		if($arOrder['WEIGHT'] == 0 && $arConfig['cdek_weight']['VALUE'] == 0) {
			return array(
				'RESULT' => 'ERROR',
				'TEXT' => GetMessage('SALE_DH_CDEK_ERROR_WEIGHTISZERO')
			);
		}
		if(!extension_loaded('curl')) {
			return array(
				'RESULT' => 'ERROR',
				'TEXT' => GetMessage('SALE_DH_CDEK_ERROR_NOCURL')
			);
		}
		$arLocationFrom = CSaleLocation::GetByID($arOrder["LOCATION_FROM"]);
		if(($city = self::__getCity($arLocationFrom['CITY_NAME'])) === false)
			return array(
				'RESULT' => 'ERROR', 
				'TEXT' => GetMessage('SALE_DH_CDEK_ERROR_NOCITY')
			);
		$city_from_id = $city['ID'];
		
		$arLocationTo = CSaleLocation::GetByID($arOrder["LOCATION_TO"]);
		if(($city = self::__getCity($arLocationTo['CITY_NAME'])) === false)
			return array(
				'RESULT' => 'ERROR', 
				'TEXT' => GetMessage('SALE_DH_CDEK_ERROR_NOCITY')
			);
		$city_to_id = $city['ID'];

		$data = array();
		if(!empty(self::$access['version']))
			$data['version'] = self::$access['version'];
		$data['dateExecute'] = date('Y-m-d');
		if(!empty($arConfig['cdek_login']['VALUE']))
			$data['authLogin'] = $arConfig['cdek_login']['VALUE'];
		if(!empty($arConfig['cdek_password']['VALUE']))
			$data['secure'] = self::getSecure($arConfig['cdek_password']['VALUE']);
		$data['senderCityId'] = $city_from_id;
		$data['receiverCityId'] = $city_to_id;
		$data['tariffId'] = intval($arConfig['cdek_tariffId']['VALUE']);
		$data['modeId'] = intval($arConfig['cdek_modeId']['VALUE']);
		
		$data['goods'][0] = array();
		$weight = intval($arConfig['cdek_weight']['VALUE']);
		if($weight == 0) $weight = $arOrder['WEIGHT'];
		$data['goods'][0]['weight'] = $weight / 1000;
		$data['goods'][0]['volume'] = intval($arConfig['cdek_volume']['VALUE']) / 1000000;
        $bodyData = array ('json' => json_encode($data));
        $data_string = http_build_query($bodyData);
        
		if(!($ch = @curl_init())){
			return array(
				'RESULT' => 'ERROR',
				'TEXT' => GetMessage('SALE_DH_CDEK_ERROR_NOCURL')
			);
		}
		
		curl_setopt($ch, CURLOPT_URL, self::$access['script']);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_HTTPHEADER, array(
		    'Content-Type: application/x-www-form-urlencoded',
            'Content-Length: '.strlen($data_string)
            ) 
		);
		curl_setopt($ch, CURLOPT_POST, 1);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
		
		$result = curl_exec($ch); 
		curl_close($ch); 
		$response = @json_decode($result, true);
        if(isset($response['result']) && !empty($response['result']) ) {
			$days = $response['result']['deliveryPeriodMin'] != $response['result']['deliveryPeriodMax'] ?
				$response['result']['deliveryPeriodMin'].'-'.$response['result']['deliveryPeriodMax'] :
				$response['result']['deliveryPeriodMin'];
			return array(
				"RESULT" => "OK",
				"VALUE" => $response['result']['price'],
				'TRANSIT' => $days
			);	
        }
		if(isset($response['error'], $response['error'][0], $response['error'][0]['text'])){
			return array(
				"RESULT" => "ERROR",
				"TEXT" => $response['error'][0]['text']
			);
		}
		return array(
			"RESULT" => "ERROR",
			"TEXT" => GetMessage('SALE_DH_CDEK_ERROR_SERVER'),
		);
	}
	function Compability($arOrder, $arConfig) {
		if(!isset($arConfig['cdek_tariffId'],
			$arConfig['cdek_modeId'],
			$arConfig['cdek_volume'])
			|| empty($arConfig['cdek_tariffId']['VALUE'])
			|| empty($arConfig['cdek_modeId']['VALUE'])
			|| empty($arConfig['cdek_volume']['VALUE'])
			|| ($arOrder['WEIGHT'] == 0 
				&& $arConfig['cdek_weight']['VALUE'] == 0)
			) return array();
		$arLocationFrom = CSaleLocation::GetByID($arOrder["LOCATION_FROM"]);
		if (!self::__IsRussian($arLocationFrom)) return array();
		$cityFrom = self::__getCity($arLocationFrom['CITY_NAME']);
		if($cityFrom === false) return array();

		if($arOrder["LOCATION_FROM"] != $arOrder["LOCATION_TO"]){
			$arLocationTo = CSaleLocation::GetByID($arOrder["LOCATION_TO"]);
			if (!self::__IsRussian($arLocationTo)) return array();
			$cityTo = self::__getCity($arLocationTo['CITY_NAME']);
			if($cityTo === false) return array();
			if($cityTo['NalSumLimit'] > $arOrder['PRICE']) return array();
		}
		else {
			if($cityFrom['NalSumLimit'] > $arOrder['PRICE']) return array();
		}
		return array_keys(self::$profiles);
	} 
	function __getCity($city_name) {
		global $DB;
		$res = $DB->Query('SELECT * FROM `cdek_cities` WHERE `CityName` LIKE "'.mysql_real_escape_string($city_name).'"');
		while($r = $res->fetch()) $city = $r;
		return isset($city) ? $city : false;
	}
	function __IsRussian($arLocation) {
		return (
			ToUpper($arLocation["COUNTRY_NAME_ORIG"]) == "РОССИЯ"
			|| ToUpper($arLocation["COUNTRY_SHORT_NAME"]) == "РОССИЯ" 
			|| ToUpper($arLocation["COUNTRY_NAME_LANG"]) == "РОССИЯ"
			|| ToUpper($arLocation["COUNTRY_NAME_ORIG"]) == "RUSSIA" 
			|| ToUpper($arLocation["COUNTRY_SHORT_NAME"]) == "RUSSIA" 
			|| ToUpper($arLocation["COUNTRY_NAME_LANG"]) == "RUSSIA"
			|| ToUpper($arLocation["COUNTRY_NAME_ORIG"]) == "РОССИЙСКАЯ ФЕДЕРАЦИЯ" 
			|| ToUpper($arLocation["COUNTRY_SHORT_NAME"]) == "РОССИЙСКАЯ ФЕДЕРАЦИЯ"
			|| ToUpper($arLocation["COUNTRY_NAME_LANG"]) == "РОССИЙСКАЯ ФЕДЕРАЦИЯ"
			|| ToUpper($arLocation["COUNTRY_NAME_ORIG"]) == "RUSSIAN FEDERATION" 
			|| ToUpper($arLocation["COUNTRY_SHORT_NAME"]) == "RUSSIAN FEDERATION"
			|| ToUpper($arLocation["COUNTRY_NAME_LANG"]) == "RUSSIAN FEDERATION"
		);
	}
}

AddEventHandler("sale", "onSaleDeliveryHandlersBuildList", array('CDeliveryCDEK', 'Init')); 

?>

/bitrix/modules/sale/lang/ru/delivery/delivery_cdek.php

<?php
$MESS['SALE_DH_CDEK_NAME'] = 'CDEK';
$MESS['SALE_DH_CDEK_DESCRIPTION'] = 'CDEK - логистические решения';
$MESS['SALE_DH_CDEK_DESCRIPTION_INNER'] = 'Обработчик службы доставки CDEK. Работает на основе <a href="http://cdek.ru/integrator/" target="_blank">API онлайн калькулятора службы</a>. <br />';
$MESS['SALE_DH_CDEK_DESCRIPTION_ERROR'] = 'Служба недоступна: невозможно создать служебные таблицы или не обнаружены требуемые функции';

$MESS['SALE_DH_CDEK_DEFAULT_TITLE'] = 'Доставка посылкой на дом';
$MESS['SALE_DH_CDEK_DEFAULT_DESCRIPTION'] = '';


$MESS['SALE_DH_CDEK_CONFIG_TITLE'] = 'Параметры';


$MESS['SALE_DH_CDEK_ERROR_CONNECT'] = 'Не удалось рассчитать стоимость доставки: ошибка соединения';
$MESS['SALE_DH_CDEK_ERROR_NOCITY'] = 'Не удалось найти город для расчета доставки';
$MESS['SALE_DH_CDEK_ERROR_NOSETTINGS'] = 'В настройках не указаны тариф, режим доставки или объем посылки';
$MESS['SALE_DH_CDEK_ERROR_WEIGHTISZERO'] = 'Вес посылки равен 0';
$MESS['SALE_DH_CDEK_ERROR_NOCURL'] = 'Не подключена библиотека CURL';
$MESS['SALE_DH_CDEK_ERROR_CURL'] = 'Ошибка при инициализации соединения CURL';
$MESS['SALE_DH_CDEK_ERROR_SERVER'] = 'Не удалось рассчитать стоимость доставки: неверный ответ сервера';

$MESS['SALE_DH_CDEK_CONFIG_LOGIN'] = 'Логин на сайте cdek.ru';
$MESS['SALE_DH_CDEK_CONFIG_PASSWORD'] = 'Пароль на сайте cdek.ru';
$MESS['SALE_DH_CDEK_CONFIG_TARIFID'] = 'ID тарифа на доставку';
$MESS['SALE_DH_CDEK_CONFIG_MODEID'] = 'ID режима на доставку';
$MESS['SALE_DH_CDEK_CONFIG_VOLUME'] = 'Размер поcылки, куб.см (например 20x 20 x 20 см = 8000 куб.см)';
$MESS['SALE_DH_CDEK_CONFIG_WEIGHT'] = 'Средний вес посылки, граммы';

?>

Требования для обработчика: наличие в системе функций json_encode, http_build_query и наличии расширения curl.

Особенности скрипта:

  • Т.к. сервис CDEK требует задания веса и габаритов (объема) посылки, то необходимо указывать вес товара.
  • С другой стороны габариты товара по умолчанию не передаются в обработчик доставки, поэтому мы не можем их использовать для расчета стоимости. Для указания объема посылки вам необходимо указать ее в настройках обработчика.
  • Там же можно указать вес посылки. Если в настройках указан вес = 0, то вес расcчитывается из веса товаров.
  • Скрипт рассчитан на кодировку базы данных UTF8, если у вас другая кодировку, измените ее вручную.

Служба доставки CDEK для Битрикса: 1 комментарий

  1. Большое спасибо.. немного доработал, но в принципе очень выручил скрипт

Обсуждение закрыто.