Компонент для расчета стоимости доставки на любой странице сайта.
На сайтах интернет магазинов всегда присутствует раздел с информацией о доставке. Порой, описание стоимости может быть настолько запутанным, что его навряд ли разберет и тот кто его писал. В таких случаях можно дать пользователю возможность узнать приблизительную стоимость доставки, не заставляя его путешествовать по сайту.
Для этого можно создать ajax форму со всей необходимой информацией. Размещаем кнопку на сайте, а при нажатии на нее выводим пользователю popup.
Так же было бы неплохо помочь пользователю заполнить почтовый индекс с помощью автоматического определения геолокации, например по IP.
Это может выглядеть как то так:
Кнопка на странице с товаром
Форма с вводом почтового индекса
Расчет стоимости доставки
Выглядит довольно не сложно. Теперь о том, как реализовать расчет стоимости доставки в 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
На первый взгляд может описание может показаться запутанным (хотя, это может так и есть). Но на самом дела в этом нет ни чего сложного. Достаточно вдумчиво "почитать" код компонента.