Своя служба доставки с расчетом расстояния


для Битрикс D7

Кратко по пунктам

Решил написать про, наверное, самый востребованный по моему скромному опыту допил.
А именно как сделать свою службу доставки с вводом адреса доставки, расчетом расстояния, расчетом цены доставки, блэкджекэом и ...

В маркетплэйсе наверняка есть готовые решения для такой задачи. Но здесь речь скорее пойдет об индивидуальном подходе, чтобы гибко можно было вставить нужную логику работы на любом этапе работы со службой доставки.

Что понадобится сделать:

  1. Создать обработчик для службы доставки
  2. Настроить цены и свойства заказа
  3. Кастомизировать шаблон компонента sale.order.ajax
  4. Сделать автокомплит для подсказки адреса, который вводит пользователь
  5. Реализовать расчет расстояния между нужными точками
  6. Сохранить все в итоговый заказ

Для работы с адресами будет использоваться kladr-api.
А для расчета растояния между нужными точками api yandex карт.
Погнали...

Создаем службу доставки в Битрикс со своим обработчиком

Для того, чтобы задуманная нами служба доставки имела собственную логику, необходимо создать свой обработчик доставки.
Уже установленные в системе обработчики можно увидеть в разделе:
Рабочий стол - Магазин - Настройки - Службы доставки
При попытке добавить новую службу доставки выползет менюшка с обработчиками, вот здесь и должен появиться наш новый тип доставки.

Все кастомные обработчики в bitrix хранятся в /bitrix/php_interface/sale_delivery/ Если такой папки нет, то ее нужно создать.

Итак, создадим наш обработчик с именем AddressDistanceDelivery.php

  1. namespace Sale\Handlers\Delivery;
  2.  
  3. use Bitrix\Sale\Delivery\CalculationResult;
  4. use Bitrix\Sale\Delivery\Services\Base;
  5.  
  6. class AddressDistanceDelivery extends Base
  7. {
  8. public static function getClassTitle()
  9. {
  10. return 'Расчет доставки до адреса';
  11. }
  12.  
  13. public static function getClassDescription()
  14. {
  15. return 'Доставка, стоимость которой зависит от расстояния';
  16. }
  17.  
  18. protected function calculateConcrete(\Bitrix\Sale\Shipment $shipment)
  19. {
  20. // здесь будет расчет стоимости доставки
  21. }
  22.  
  23. protected function getConfigStructure()
  24. {
  25. return array(
  26. "MAIN" => array(
  27. "TITLE" => 'Настройка обработчика',
  28. "DESCRIPTION" => 'Настройка обработчика',
  29. "ITEMS" => array(
  30. "PRICE_KM" => array(
  31. "TYPE" => "STRING",
  32. "MIN" => 0,
  33. "NAME" => 'Стоимость доставки за километр'
  34. ),
  35.  
  36. )
  37. )
  38. );
  39. }
  40.  
  41. public function isCalculatePriceImmediately()
  42. {
  43. return true;
  44. }
  45.  
  46. public static function whetherAdminExtraServicesShow()
  47. {
  48. return true;
  49. }
  50. }

После того, как обработчик добавлен, он должен появиться при создании службы доставки в админке.
Пока в обработчике в методе getConfigStructure мы создали один параметр – стоимость доставки за километр пути.
При необходимости параметров можно добавить больше, но об этом позже.

Добавляем свойства заказа

Тут все просто – создадим два свойства

Рабочий стол - Магазин - Настройки - Свойства заказа Для хранения нам понадобятся два св-ва:
  1. кол-во километров, расчитанное Яндексом
  2. сам адрес, который ввел пользователь
Важно Не нужно делать св-ва с типом "Местоположение" т.к. мы напишем свой автокомплитер, который будет подсказывать адрес пользователю.

Кастомизируем sale.order.ajax

Будем допиливать компонент заказа, а точнее не сам компонент, а его шаблон, а если еще точнее, то собственную копию шаблона.
О том, как скопировать и применить собственный шаблон к стандартному компоненту, можно почитать здесь.

У меня путь к новому шаблону получился такой

/local/templates/.default/components/bitrix/sale.order.ajax/.default/
Находим в шаблоне файл order_ajax.js, его нам и нужно модифицировать.
Порядок действий будет такой
  1. Подредактируем вывод св-в заказа, наши свойства исключим из общего списка
  2. Сделаем собственный вывод для нашей службы доставки на шаге "доставка"
Начнем с вывода св-в заказа. Задача исключить два наших из общего списка, который Битрикс выводит на последнем шаге заказа. В файле order_ajax.js находим реализацию метода getPropertyRowNode. В конце метода дописываем условие, которое запретит выводить наши св-ва
  1. getPropertyRowNode: function(property, propsItemsContainer, disabled)
  2. {
  3. ...
  4. ...
  5. ...
  6. // исключаем вывод св-в
  7. if (property.getId() != 20 && property.getId() != 21) {
  8. propsItemsContainer.appendChild(propsItemNode);
  9. }
  10.  
  11. },
Здесь и дальше по коду будем использовать id св-в заказ. Не самое красивое решение, в случае если св-ва изменятся, придется подпилить заново. Но сейчас я хочу продемонстрировать сам ход кастомизации, в будущем можно вместо id использовать переменные и т.д.

Следующим шагом создадим блок ввода адреса доставки при выборе нашей службы.
В том же order_ajax.js находим реализацию метода editDeliveryInfo - он служит для отображения информации по службе доставки.
Дорабатываем его

  1. if(currentDelivery.ID == 11) // 11 - id нашей службы доставки
  2. {
  3.  
  4. var labelAddr, inputAddr, inputDistance, addrProp, distanceProp, autocompleteKladr, noteAddr, resultDistance;
  5. addrProp = '';
  6. distanceProp = '';
  7.  
  8. this.result.ORDER_PROP.properties.forEach(function(entry) {
  9. if(entry.ID == 20) addrProp = entry.VALUE[0];
  10. if(entry.ID == 21) distanceProp = entry.VALUE[0];
  11. });
  12. // создаем <label></label> для инпута ввода адреса
  13. labelAddr = BX.create('span', {
  14. props: {
  15. className: 'addrLabel',
  16. },
  17. text: 'Введите адрес доставки'
  18. });
  19. // создаем input для ввода адреса доставки
  20. inputAddr = BX.create('INPUT', {
  21. props: {
  22. type: 'text',
  23. name: 'ORDER_PROP_20',
  24. className: 'addr-input',
  25. value: addrProp
  26. },
  27. });
  28. // hidden инпут куда сохраним расчитанное кол-во км.
  29. inputDistance = BX.create('INPUT',{
  30. props: {
  31. type: 'hidden',
  32. name: 'ORDER_PROP_21',
  33. className: 'hiddistance',
  34. value: distanceProp
  35. }
  36. });
  37. // блок для подсказок адреса
  38. autocompleteKladr = BX.create('DIV', {
  39. props: {
  40. className: 'autocomplate-kladr'
  41. }
  42. });
  43. // общи блок куда засунем нашу разметку
  44. addDiv = BX.create('DIV', {
  45. props: {className: 'addAddres'}
  46. });
  47. addDiv.appendChild(labelAddr);
  48.  
  49. addDiv.appendChild(inputAddr);
  50. addDiv.appendChild(autocompleteKladr);
  51. addDiv.appendChild(inputDistance);
  52.  
  53. // невешиваем оработчик на keyup ввода адреса что бы аяксом показать подсказки
  54. BX.bind(inputAddr, 'keyup',function(e) {
  55. if (inputAddr.value.length > 3) {
  56. BX.ajax({
  57. url: '/ajax/getDistance.php?addr=' + inputAddr.value,
  58. dataType: 'html',
  59. onsuccess: function(data){
  60. $(autocompleteKladr).show();
  61. $(autocompleteKladr).html(data);
  62. },
  63. });
  64. }
  65. });
  66. // навешиваем обработчик на click по выбранному адресу
  67. $('body').on('click', '.place-kladr', function(){
  68. $('.addr-input').val($(this).html());
  69. // тут якарты как то вычисляют дистанцию. в $(this),html() - форматированная строка
  70. resultDistance = 15; // пока дистанция будет 15км всегда
  71. if (resultDistance){
  72. $('.hiddistance').val(resultDistance); // вычесленная дистанция
  73. }
  74. $(autocompleteKladr).hide();
  75. //пересчет доставки
  76. BX.Sale.OrderAjaxComponent.sendRequest();
  77. });
  78. }
  79. // добавим в стандартную запись вывод нашего appDiv
  80. deliveryInfoContainer.appendChild(
  81. BX.create('DIV', {
  82. props: {className: 'bx-soa-pp-company'},
  83. children: [subTitle, label, title, clear, addDiv, extraServicesNode, infoList]
  84. })
  85. );
Кратко что тут делается: В коде используется пример работы как с битиксовой либой BX, так и c jQuery.
Для оформления заюзаем стили. Всем новым элементам в коде выше мы дали className. Стили у меня получились такие
  1. .addAddres {
  2. position: relative;
  3. }
  4. .autocomplate-kladr {
  5. position: absolute;
  6. display:none;
  7. background-color: #fff;
  8. }
  9. .places-kladr {
  10. list-style: none;
  11. padding: 0;
  12. margin: 0;
  13. }
  14. .place-kladr {
  15. cursor: pointer;
  16. padding: 5px;
  17. font-size: 12px;
  18. border-bottom: 1px solid #dedede;
  19. margin-bottom: 2px;
  20. }
  21. .place-kladr-empty {
  22. padding: 5px;
  23. font-size: 12px;
  24. border-bottom: 1px solid #dedede;
  25. margin-bottom: 2px;
  26. color: #a94442;
  27. background-color: #f2dede;
  28. }
  29. .place-kladr:hover{
  30. background-color: #dedede;
  31. }

Поиск адреса с кладр api

В предыдущем шаге мы создали обработчик у поля адреса доставки, который при каждом нажатии клавиши делает ajax запрос на скрипт поиска адреса по строке. В примере

/ajax/getDistance.php
Создаем скрипт по этому пути.
  1. <?php
  2. require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");
  3. if ($_REQUEST['addr'] && strlen($_REQUEST['addr']) > 3 ) :
  4. // путь до класса кладра
  5. require($_SERVER["DOCUMENT_ROOT"] . "/ajax/kladr.php");
  6. $addr = $_REQUEST['addr'];
  7. $api = new Kladr\Api('51dfe5d42fb2b43e3300006e', '86a2c2a06f1b2451a87d05512cc2c3edfdf41969'); // тут ключи
  8.  
  9. $query = new Kladr\Query();
  10. $query->ContentName = $addr;
  11. $query->OneString = TRUE;
  12. $query->Limit = 5;
  13. $arResult = $api->QueryToArray($query);
  14. ?>
  15. <ul class="places-kladr">
  16. <?
  17. if (!count($arResult)) :
  18. ?><li class="place-kladr-empty">Мы не смогли найти такой адрес :(</li><?
  19. else :
  20. foreach ($arResult as $place) {?>
  21. <li class="place-kladr"><?=trim($place['fullName'])?></li>
  22. <?}
  23. endif;?>
  24. </ul>
  25. <?php endif;?>
В скрипте используется готовый класс kladr.php – для работы с api кладра

По итогу предыдущих телодвижений у меня получилось вот так

Раcчет расстояния между двум точками с помощью Yandex

Осталось рассчитать расстояние между двумя адресами.
Заюзаем yandex maps api.

Возращаемся в order_ajax.js и реализуем расчет внутри editDeliveryInfo, допилим обработчик click (вместо старых 66-77 строк).

  1. $('body').on('click', '.place-kladr', function(){
  2. $('.addr-input').val($(this).html());
  3. // тут якарты как то вычисляют дистанцию. в $(this),html() - форматированная строка
  4. var route = ymaps.route([
  5. 'Москва, Красная площадь, 1', //дарес точки от куда везем заказ
  6. $(this).html()
  7. ], {
  8. multiRoute: true
  9. })
  10. .done(function (route) {
  11. var distance = route.getActiveRoute().properties.get("distance");
  12. resultDistance = Math.round(distance.value/1000);
  13. $('.hiddistance').val(resultDistance);
  14. },
  15. function (err) {
  16. // если дистанция не нашлась
  17. $('.hiddistance').val(0);
  18. }, this);
  19.  
  20. $(autocompleteKladr).hide();
  21. //пересчет доставки
  22. BX.Sale.OrderAjaxComponent.sendRequest();
  23. });
Само собой, чтобы это заработало, нужно подключить скрипт Я.карт.
Ключ для Я.карт можно сгенировать здесь.

Пересчет доставки

В самом начале мы создали обработчик AddressDistanceDelivery.
Научим его высчитывать цену на основе переданной дистанции в километрах. Реализуем метод calculateConcrete
  1. protected function calculateConcrete(\Bitrix\Sale\Shipment $shipment)
  2. {
  3. $result = new CalculationResult();
  4. $price = floatval($this->config["MAIN"]["PRICE"]);
  5. $order = $shipment->getCollection()->getOrder();
  6. $props = $order->getPropertyCollection();
  7.  
  8. // получаем текущее значение расчитанного кол-ва километров
  9. $distance = $props->getItemByOrderPropertyId(21);
  10.  
  11. // получаем стоимость одного километра
  12. $priceKm = $this->config['MAIN']['PRICE_KM'];
  13. $price = $priceKm * $distance->getValue(); // расчет цены
  14. $result->setDeliveryPrice(roundEx($price, 2));
  15. return $result;
  16. }

Итог

Собственно ничего сложного нет. Подобным образом можно запилить расчет стоимости поднятия на этаж, доставку до двери в зависимости от кол-ва этажей и т.д

Сам код можно оптимизировать, уйти от использования жестких id прям в коде, обернуть все в модуль/компоненту или просто юзать на разных проектах.