04.07.2019

Компонент для расчета стоимости доставки на любой странице сайта.

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

Для этого можно создать ajax форму со всей необходимой информацией. Размещаем кнопку на сайте, а при нажатии на нее выводим пользователю popup.
Так же было бы неплохо помочь пользователю заполнить почтовый индекс с помощью автоматического определения геолокации, например по IP.
Это может выглядеть как то так:
Кнопка на странице с товаром
Стоимость доставки кнопка
Форма с вводом почтового индекса
Popup форма расчета доставки
Расчет стоимости доставки
стоимости доставки в popup

Выглядит довольно не сложно. Теперь о том, как реализовать расчет стоимости доставки в popup?

Решение задачи лежит на поверхности. Открываем стандартное оформление заказа sale.order.ajax. И в его class.php ищем, как выбираются службы доставки для отображения в компоненте.
Это будет метод \SaleOrderAjax::calculateDeliveries. Ищем все что ему нужно для работы, и вытаскиваем в свой компонент.

В моей реализации в итоге class.php выглядит так:

<?php

use Bitrix\Main\Application;
use Bitrix\Main\Loader;
use Bitrix\Sale;
use Bitrix\Main\SystemException;

if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true) die();

class DeliveryPrice extends CBitrixComponent
{
    protected $arDeliveryServiceAll;
    protected $arUserResult;


    public function onPrepareComponentParams($arParams)
    {

        return $arParams;
    }

    public function executeComponent()
    {
        $request = Application::getInstance()->getContext()->getRequest();
        $this->arResult["ZIP"] = $request->get("zip");
        $this->arResult["WEIGHT"] = $request->get("weight");
        if (floatval($this->arResult["WEIGHT"]) <= 0) {
            $this->arResult["WEIGHT"] = $this->arParams["DEFAULT_WEIGHT"];
        }
        try {
            if ($this->arResult["ZIP"]) {
                Loader::includeModule("sale");
                if ($location = Bitrix\Sale\Location\Admin\LocationHelper::getLocationsByZip($this->arResult["ZIP"], array('limit' => 1))->fetch()) {
                    $order = Sale\Order::create(SITE_ID);
                    $order->setPersonTypeId($this->arParams["PERSON_TYPE"]);
                    $basket = Sale\Basket::create(SITE_ID);

                    $item = $basket->createItem('catalog', $this->arParams["DEFAULT_PRODUCT_ID"]);

                    $item->setFields(array(
                        'QUANTITY' => 1,
                        'CURRENCY' => "RUB",
                        'LID' => SITE_ID,
                        'CUSTOM_PRICE' => 'Y',
                        'PRICE' => '1000',
                        "WEIGHT" => $this->arResult["WEIGHT"] * 1000
                    ));

                    $this->arResult['$item->getWeight();'] = $item->getWeight();;
                    $order->setBasket($basket);

                    $arLocation = Sale\Location\LocationTable::getById($location["LOCATION_ID"])->fetch();

                    $this->arResult["LOCATION"] = Bitrix\Sale\Location\Admin\LocationHelper::getLocationStringById($location["LOCATION_ID"]);
                    $propertyCollection = $order->getPropertyCollection();
                    $locationProperty = $propertyCollection->getDeliveryLocation();
                    $locationProperty->setValue($arLocation["CODE"]);
                    $zipProperty = $propertyCollection->getDeliveryLocationZip();
                    $zipProperty->setValue($this->arResult["ZIP"]);

                    $this->initShipment($order);
                    $this->arDeliveryServiceAll = \Bitrix\Sale\Delivery\Services\Manager::getRestrictedObjectsList($this->getCurrentShipment($order), Sale\Services\Base\RestrictionManager::MODE_CLIENT);
                    $this->calculateDeliveries($order);
                } else {
                    $this->arResult["ERRORS"][] = "Не удалось найти индекс";
                }

            } else {
                \Bitrix\Main\Loader::includeModule("sale");
                $geoLocationResult = \Bitrix\Main\Service\GeoIp\Manager::getDataResult('', "ru");
                if ($geoLocationResult->isSuccess()) {
                    $geoLocation = Bitrix\Sale\Location\Search\Finder::find([
                        "select" => ["ID", "NAME", "CODE"],
                        "filter" => [
                            "PHRASE" => $geoLocationResult->getGeoData()->cityName
                        ],
                        "limit" => 1
                    ])->fetch();
                    if ($geoLocation) {
                        $geoLocation["NAME"] = Bitrix\Sale\Location\Admin\LocationHelper::getLocationStringById($geoLocation["ID"]);
                        $geoLocation["ZIP"] = Bitrix\Sale\Location\Admin\LocationHelper::getZipByLocation($geoLocation["CODE"], ['limit' => 1])->fetch();
                        if ($geoLocation["ZIP"]) {
                            $this->arResult["GEOLOCATION"] = $geoLocation;
                        }
                    }
                }
            }
        } catch (\Exception $e) {
            $this->arResult["ERRORS"][] = "Не удалось найти индекс";
        }


        $this->includeComponentTemplate();
    }

    function calculateDeliveries($order)
    {
        $this->arResult['DELIVERY'] = array();
        $this->arResult['arDeliveryServiceAll'] = $this->arDeliveryServiceAll;
        $problemDeliveries = array();

        if (!empty($this->arDeliveryServiceAll)) {
            /** @var Sale\Order $orderClone */
            $orderClone = null;
            $anotherDeliveryCalculated = false;
            /** @var Sale\Shipment $shipment */
            $shipment = $this->getCurrentShipment($order);

            /** @var Bitrix\Sale\Delivery\Services\Base $deliveryObj */
            foreach ($this->arDeliveryServiceAll as $deliveryId => $deliveryObj) {
                $calcResult = false;
                $calcOrder = false;
                $arDelivery = array();
                $arDelivery["NAME"] = $deliveryObj->getNameWithParent();
//                $arDelivery['$deliveryObj'] = $deliveryObj;

                if ((int)$shipment->getDeliveryId() === $deliveryId) {
                    $arDelivery['CHECKED'] = 'Y';
                    $mustBeCalculated = true;
                    $calcResult = $deliveryObj->calculate($shipment);
                    $calcOrder = $order;
                } else {
                    $mustBeCalculated = true;

                    if ($mustBeCalculated) {
                        $anotherDeliveryCalculated = true;

                        if (empty($orderClone)) {
                            $orderClone = $this->getOrderClone($order);
                        }

                        $orderClone->isStartField();

                        $clonedShipment = $this->getCurrentShipment($orderClone);
                        $clonedShipment->setField('DELIVERY_ID', $deliveryId);

                        $calculationResult = $orderClone->getShipmentCollection()->calculateDelivery();

                        if ($calculationResult->isSuccess()) {
                            $calcDeliveries = $calculationResult->get('CALCULATED_DELIVERIES');
                            $calcResult = reset($calcDeliveries);
                        }

                        if (empty($calcResult)) {
                            $calcResult = new Sale\Delivery\CalculationResult();
                            $calcResult->addError(new \Bitrix\Main\Error("Не удалось расчитать стоимость доставки"));
                        }

                        $orderClone->doFinalAction(true);

                        $calcOrder = $orderClone;
                    }
                }

                if ($mustBeCalculated) {
                    if ($calcResult->isSuccess()) {
                        $arDelivery['PRICE'] = Sale\PriceMaths::roundPrecision($calcResult->getPrice());
                        $arDelivery['PRICE_FORMATED'] = SaleFormatCurrency($arDelivery['PRICE'], $calcOrder->getCurrency());

                        $currentCalcDeliveryPrice = Sale\PriceMaths::roundPrecision($calcOrder->getDeliveryPrice());
                        if ($currentCalcDeliveryPrice >= 0 && $arDelivery['PRICE'] != $currentCalcDeliveryPrice) {
                            $arDelivery['DELIVERY_DISCOUNT_PRICE'] = $currentCalcDeliveryPrice;
                            $arDelivery['DELIVERY_DISCOUNT_PRICE_FORMATED'] = SaleFormatCurrency($arDelivery['DELIVERY_DISCOUNT_PRICE'], $calcOrder->getCurrency());
                        }

                        if (strlen($calcResult->getPeriodDescription()) > 0) {
                            $arDelivery['PERIOD_TEXT'] = $calcResult->getPeriodDescription();
                        }
                    } else {
                        if (count($calcResult->getErrorMessages()) > 0) {
                            foreach ($calcResult->getErrorMessages() as $message) {
                                $arDelivery['CALCULATE_ERRORS'] .= $message . '<br>';
                            }
                        } else {
                            $arDelivery['CALCULATE_ERRORS'] = "Ошибка вычисления стоимости доставки";
                        }


                        if ($this->arParams['SHOW_NOT_CALCULATED_DELIVERIES'] === 'N') {
                            unset($this->arDeliveryServiceAll[$deliveryId]);
                            continue;
                        } elseif ($this->arParams['SHOW_NOT_CALCULATED_DELIVERIES'] === 'L') {
                            $problemDeliveries[$deliveryId] = $arDelivery;
                            continue;
                        }
                    }

                    $arDelivery['CALCULATE_DESCRIPTION'] = $calcResult->getDescription();
                }

                $this->arResult['DELIVERY'][$deliveryId] = $arDelivery;
            }

            if ($anotherDeliveryCalculated) {
                $order->doFinalAction(true);
            }
        }

        if (!empty($problemDeliveries)) {
            $this->arResult['DELIVERY'] += $problemDeliveries;
        }
    }

    function getCurrentShipment(Sale\Order $order)
    {
        /** @var Sale\Shipment $shipment */
        foreach ($order->getShipmentCollection() as $shipment) {
            if (!$shipment->isSystem())
                return $shipment;
        }

        return null;
    }

    /**
     * @param Sale\Order $order
     *
     * @return Sale\Order
     */
    protected function getOrderClone(Sale\Order $order)
    {
        /** @var Sale\Order $orderClone */
        $orderClone = $order->createClone();

        $clonedShipment = $this->getCurrentShipment($orderClone);
        if (!empty($clonedShipment)) {
            $clonedShipment->setField('CUSTOM_PRICE_DELIVERY', 'N');
        }

        return $orderClone;
    }

    /**
     * Initialization of shipment object. Filling with basket items.
     *
     * @param Sale\Order $order
     * @return Sale\Shipment
     * @throws \Bitrix\Main\ArgumentTypeException
     * @throws \Bitrix\Main\NotSupportedException
     */
    public function initShipment(Sale\Order $order)
    {
        $shipmentCollection = $order->getShipmentCollection();
        $shipment = $shipmentCollection->createItem();
        $shipmentItemCollection = $shipment->getShipmentItemCollection();
        $shipment->setField('CURRENCY', $order->getCurrency());

        /** @var Sale\BasketItem $item */
        foreach ($order->getBasket() as $item) {
            /** @var Sale\ShipmentItem $shipmentItem */
            $shipmentItem = $shipmentItemCollection->createItem($item);
            $shipmentItem->setQuantity($item->getQuantity());
        }

        return $shipment;
    }
}

Шаблон компонента template.php выглядит так:

<? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true) die();
/** @var array $arParams */
/** @var array $arResult */
/** @global CMain $APPLICATION */
/** @global CUser $USER */
/** @global CDatabase $DB */
/** @var CBitrixComponentTemplate $this */
/** @var string $templateName */
/** @var string $templateFile */
/** @var string $templateFolder */
/** @var string $componentPath */
/** @var CBitrixComponent $component */
$this->setFrameMode(true);
$zipInputValue = $arResult["ZIP"] ? $arResult["ZIP"] : $arResult["GEOLOCATION"] ? $arResult["GEOLOCATION"]["ZIP"]["XML_ID"] : "";
?>
<div class="popup-form-wrapper">
    <div class="row">
        <? if ($arResult["LOCATION"]): ?>
            <div class="col-sm-12 delivery-calc-city">
                <h4 class="tac">
                    Стоимость доставки 1 кг. в:
                    <br>"<?= $arResult["LOCATION"] ?>"
                </h4>
            </div>
            <? if ($arResult["DELIVERY"]): ?>
                <div class="col-sm-12">
                    <? foreach ($arResult["DELIVERY"] as $delivery): ?>
                        <div class="delivery-price-row">
                            <div class="row">
                                <div class="col-xs-6"><?= $delivery["NAME"] ?></div>
                                <div class="col-xs-3"><?= $delivery["PERIOD_TEXT"] ?></div>
                                <div class="col-xs-3"><?= $delivery["PRICE_FORMATED"] ?></div>
                            </div>
                        </div>
                    <? endforeach ?>
                    <div class="delivery-price-row">
                        <div class="row">
                            <div class="col-xs-12 tac">
                                При заказе от 3000
                                рублей доставка бесплатно.
                                <a href="/actions/free-delivery/" target="_blank" rel="noreferrer">Подробнее</a>
                            </div>
                        </div>
                    </div>
                </div>
            <? else: ?>
                <div class="col-sm-12">
                    <? ShowError("Не удалось расчитать стоимость доставки") ?>
                </div>
            <? endif ?>
        <? else: ?>
            <div class="col-sm-12">
                <form action="<?= POST_FORM_ACTION_URI ?>">
                    <div class="form-group">
                        <label for="delivery-zip">Индекс</label>
                        <input type="text" id="delivery-zip" class="form-control" name="zip"
                               value="<?= $zipInputValue ?>" required>
                    </div>
                    <? if ($arResult["GEOLOCATION"] && ($arResult["GEOLOCATION"]["ZIP"]["XML_ID"] == $zipInputValue)): ?>
                        <div class="form-group">
                            Населенный пункт определен автоматически:<br>
                            <i><?= $arResult["GEOLOCATION"]["NAME"] ?></i><br>
                            При необходимости измените и нажмите кнопку <b>Посчитать</b>
                        </div>
                    <? endif ?>

                    <div class="form-group">
                        <button class="btn btn-default">Посчитать</button>
                    </div>
                </form>
            </div>
        <? endif ?>
        <? if ($arResult["ERRORS"]): ?>
            <div class="col-sm-12">
                <?
                foreach ($arResult["ERRORS"] as $error) {
                    ShowError($error);
                }
                ?>
            </div>
        <? endif ?>
    </div>
</div>


<script>
    BX = top.BX;
    $ = top.$;
    var currentPopup = BX.PopupWindowManager.getCurrentPopup();
    currentPopup.adjustPosition();
    currentPopup.resizeOverlay();
</script>

Вызов компонента в /ajax/getDeliveryPrice.php:

<?
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");
$APPLICATION->ShowAjaxHead();

$APPLICATION->IncludeComponent(
    "gricuk:delivery.price",
    "",
    Array(
        "DEFAULT_PRODUCT_ID" => 380,
        "SHOW_NOT_CALCULATED_DELIVERIES" => "N",
        "PERSON_TYPE" => 3,
        "DEFAULT_WEIGHT" => 1,
        "AJAX_MODE" => "Y",
        "AJAX_OPTION_SHADOW" => "N",
        "AJAX_OPTION_JUMP" => "N",
        "AJAX_OPTION_STYLE" => "Y",
        "CACHE_TYPE" => "N",
    )
);
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_after.php");

И наконец сама кнопка, при нажатии на которую показывается Popup:

<a class="btn btn-link js-ajax-popup" data-url="/ajax/getDeliveryPrice.php" href="javascript:void(0);"><i class="fa fa-truck"></i> <span>Стоимость доставки</span></a>

Разберем, что тут написано.
В class.php все методы кроме executeComponent и onPrepareComponentParams копипаста с небольшими изменениями из class.php компонента sale.order.ajax.
Для того чтобы рассчитать стоимость доставки необходимо знать почтовый индекс. Его мы пытаемся узнать по геолокации с помощью метода \Bitrix\Main\Service\GeoIp\Manager::getDataResult('', "ru");. В битриксе есть стандартные провайдер геоданных. Их можно настроить на странице админки Настройки > Настройки продукта > Геолокация. При необходимости список провайдеров можно расширить своими кастомными, но об этом в другой раз.

Если смогли найти местоположение по IP. Берем берем его ZIP и подставляем в форму. После того как пользователь подтвердит ZIP или введет свой начинает расчет стоимости доставки. Для этого создается корзина, и товар который в настройках компонента передается в качестве DEFAULT_PRODUCT_ID, добавляется в только что созданную корзину. После этого корзина добавляется к объекту заказа, и уже дальше объект заказа передается в методы, скопированные из sale.order.ajax.

Вывод формы в popup реализован с помощью Ajax Popup на Bitrix JS

Теперь о параметрах компонента

  • DEFAULT_PRODUCT_ID - ID товара который будет добавляться в корзину.
  • SHOW_NOT_CALCULATED_DELIVERIES - отображать доставки для которых не удалось расчитать стоимость
  • USER_GROUP_ID -ID типа плательщика для которого будет производиться расчет
  • DEFAULT_WEIGHT - Вес в килограммах, с которым будет происходить расчет
  • OFFERS_CART_PROPERTIES - свойства торговых предложений которые будут добавлены в корзину
  • AJAX_MODE, AJAX_OPTION_SHADOW, AJAX_OPTION_JUMP - стандартные параметры Bitrix для работы компонента в режиме ajax

На первый взгляд может описание может показаться запутанным (хотя, это может так и есть). Но на самом дела в этом нет ни чего сложного. Достаточно вдумчиво "почитать" код компонента.