Работа с sale.order.ajax в Битрикс: кастомизация

Работа с sale.order.ajax в Битрикс: кастомизация

Практически никого из моих клиентов не устраивает внешний вид sale.order.ajax. Здесь я опишу большую часть приемов, которыми пользуюсь сам при кастомизации данного непростого компонента.

В файле template.php блоки заказа отмечены комментариями:

BASKET ITEMS BLOCK - корзина заказа - таблица товаров
REGION BLOCK - выбор типа плательщика и ввод местоположения - города покупателя
DELIVERY BLOCK - выбор службы доставки
PAY SYSTEMS BLOCK - выбор способа оплаты
BUYER PROPS BLOCK - форма с полями для ввода данных покупателя
ORDER SAVE BLOCK - итог и кнопка подтверждения заказа

Чтобы все блоки были активны, а не только первый комментируем вторую строчку в данном блоке

initFirstSection: function()
{
	var firstSection = this.orderBlockNode.querySelector('.bx-soa-section.bx-active');
	//BX.addClass(firstSection, 'bx-selected');
	this.activeSectionId = firstSection.id;
},

Если доставки выдают ошибку ошибку вычислений, то блоки с доставкой скрываются. Отключить это можно закомментировав следующий код:

/*if (this.result.DELIVERY.length > 0)
{
	BX.addClass(this.deliveryBlockNode, 'bx-active');
	this.deliveryBlockNode.removeAttribute('style');
}
else
{
	BX.removeClass(this.deliveryBlockNode, 'bx-active');
	this.deliveryBlockNode.style.display = 'none';
}*/

Убираем сокрытие блоков при авторизации (при выключенной в настройках опции «регистрировать вместе с оформлением заказа»).

if (this.result.SHOW_AUTH && section.id != this.authBlockNode.id && section.id != this.basketBlockNode.id)
	section.style.display = 'none';
else if (section.id != this.pickUpBlockNode.id)
	section.style.display = '';

Открыть все блоки и убрать лишнее

Чтобы раскрыть все скрытые блоки можно воспользоваться следующими методами (лично использовал на версиях до 20):

Ищем строку var active = section.id == this.activeSectionId и меняем ее на

var active = true

Далее отключаем реагирование на клик по заголовку блока

BX.unbindAll(titleNode);

if (this.result.SHOW_AUTH)
{
	BX.bind(titleNode, 'click', BX.delegate(function(){
		this.animateScrollTo(this.authBlockNode);
		this.addAnimationEffect(this.authBlockNode, 'bx-step-good');
	}, this));
}
else
{
	BX.bind(titleNode, 'click', BX.proxy(this.showByClick, this));
	editButton = titleNode.querySelector('.bx-soa-editstep');
	editButton && BX.bind(editButton, 'click', BX.proxy(this.showByClick, this));
}

Чтобы всегда были открыты Регион и Пользователь

if (this.activeSectionId !== this.regionBlockNode.id)
	this.editFadeRegionContent(this.regionBlockNode.querySelector('.bx-soa-section-content'));

if (this.activeSectionId != this.propsBlockNode.id)
	this.editFadePropsContent(this.propsBlockNode.querySelector('.bx-soa-section-content'));

Чтобы убрать кнопки Далее/Назад

node.appendChild(
	BX.create('DIV', {
		props: {className: 'row bx-soa-more'},
		children: [
			BX.create('DIV', {
				props: {className: 'bx-soa-more-btn col-xs-12'},
				children: buttons
			})
		]
	})
);

Чтобы убрать ссылки «изменить» у всех блоков в editOrder (~2222 стр.)

var editSteps = this.orderBlockNode.querySelectorAll('.bx-soa-editstep'), i;
 for (i in editSteps) {
	if (editSteps.hasOwnProperty(i)) {
	   BX.remove(editSteps[i]);
	}
 }

Чтобы скрыть уведомление о том, что данный заполнены автоматически добавляем в конец файла style.css

.alert.alert-warning{display:none;}

Определение местоположения пользователя в автоматическом режиме

use Bitrix\Main\EventManager; 
$eventManager = EventManager::getInstance();
$eventManager->addEventHandler("sale", "OnSaleComponentOrderProperties", Array("Example", "OnSaleComponentOrderProperties"));

class Example
{
   /**
   * У меня по условию задачи известны ID и NAME местоположения
   */
   static $curCityId = XX;  // числовое значение идентификатора местоположения
   static $curCityName = 'Название города';
   
   /**
   * ID свойств заказа   
   */
   const PROP_LOCATION = 6; 
   const PROP_ZIP = 4; 
   const PROP_LOCATION_NAME = 5;


   static function OnSaleComponentOrderProperties(&$arFields)
   {
      $rsLocaction = CSaleLocation::GetLocationZIP(self::$curCityId); 
      $arLocation = $rsLocaction->Fetch(); 
      $arFields['ORDER_PROP'][self::PROP_ZIP] = $arLocation['ZIP'];
      $arFields['ORDER_PROP'][self::PROP_LOCATION_NAME] = self::$curCityName;
      $arFields['ORDER_PROP'][self::PROP_LOCATION] = CSaleLocation::getLocationCODEbyID(self::$curCityId);
   }
}

А вот такая модификация позволяет определить местоположение только по названию города:

class Example {
   static function OnSaleComponentOrderProperties(&$arFields)
   {
      static $curCityName = 'Название города';
      const PROP_LOCATION = 6;  // - Идентификатор свойства с местоположением
      static function OnSaleComponentOrderProperties(&$arFields)
      {
         $res = Bitrix\Sale\Location\LocationTable::getList(array(
         'filter' => array('=NAME.NAME' => self::$curCityName, '=NAME.LANGUAGE_ID' => LANGUAGE_ID),
         'select' => array('CODE' => 'CODE', 'NAME_RU' => 'NAME.NAME', 'TYPE_CODE' => 'TYPE.CODE') //'*', 
         ));
         while($item = $res->fetch())
         {            
            $code = $item["CODE"];
         }
         $arFields['ORDER_PROP'][self::PROP_LOCATION] = $code;
      }
   }
}

Скрыть какое-то свойство

Если необходимо скрыть какое-то свойство, например, свойство индекс — задать значение по умолчанию и не показывать пользователям это поле, то можно внести корректировку в JS. В функции getPropertyRowNode после switch (propertyType) добавляем скрытие данного свойства:

if(property.getId()==6){// идентификатор скрываемого свойства
   var addressInput=propsItemNode.querySelector('textarea');
   propsItemNode.style='display:none;';
   addressInput.value='нужное значение';
}

Скрываем сообщение «Вы заказывали в нашем интернет-магазине, поэтому мы заполнили все данные автоматически»

Идем в функцию checkNotifications на ~750 стр и находим код

informer.appendChild(
	BX.create('DIV', {
		props: {className: 'row'},
		children: [
			BX.create('DIV', {
				props: {className: 'col-xs-12'},
				style: {position: 'relative', paddingLeft: '48px'},
				children: [
					BX.create('DIV', {props: {className: 'icon-' + className}}),
					BX.create('DIV', {html: text})
				]
			})
		]
	})
);
BX.addClass(informer, 'alert alert-' + className);

И обрамляем данный код доп. проверкой

if(this.params.MESS_SUCCESS_PRELOAD_TEXT.indexOf('Вы заказывали в нашем инте') === false) {
	//Код выше ставим сюда
}

Скрываем сообщение «Выберите свой город в списке. Если вы не нашли свой город, выберите «другое местоположение», а город впишите в поле «Город»

Идем в функцию getDeliveryLocationInput и комментируем код:

/*
if (location && location[0])
{
	node.appendChild(
		BX.create('DIV', {
			props: {className: 'bx-soa-reference'},
			html: this.params.MESS_REGION_REFERENCE
		})
	);
}
*/

Или с помощью стилей скрываем класс bx-soa-reference

#bx-soa-region .bx_soa_location .bx-soa-reference{display: none}

Исключить из показа нулевой цены за доставку

В функции getDeliveryPriceNodes: function(delivery) в блоке «else» заменяем. Вместо:

priceNodesArray = [delivery.PRICE_FORMATED];

пишем:

if(delivery.PRICE>0) priceNodesArray = [delivery.PRICE_FORMATED];

Так мы спрячем нулевую цену из свернутого блока с выбранной доставкой.

Дальше нужно скрыть нули в списке служб доставки. Для этого в функции createDeliveryItem: function(item) делаем строгую проверку на ноль. Вместо:

if (item.PRICE >= 0 || typeof item.DELIVERY_DISCOUNT_PRICE !== 'undefined')

пишем:

if (item.PRICE > 0 || typeof item.DELIVERY_DISCOUNT_PRICE !== 'undefined')

А также вместо:

else if (deliveryCached && (deliveryCached.PRICE >= 0 || typeof deliveryCached.DELIVERY_DISCOUNT_PRICE !== 'undefined'))

пишем:

else if (deliveryCached && (deliveryCached.PRICE > 0 || typeof deliveryCached.DELIVERY_DISCOUNT_PRICE !== 'undefined'))

И последним нужно скрыть нулевую доставку из итоговых сумм. Для этого в функции editTotalBlock: function() также ставим строгую проверку на ноль. Вместо

if (parseFloat(total.DELIVERY_PRICE) >= 0 && this.result.DELIVERY.length)

пишем:

if (parseFloat(total.DELIVERY_PRICE) > 0 && this.result.DELIVERY.length)

В результате нулевая доставка не будет показана пользователю.

Убираем поле «Адрес доставки» из вывода блока «Пользователь»

Описанные у моих коллег способы в моем случае не сработали. Решил пойти некрасивым, но действенным способом

Идем в функцию editPropsItemsи находим код:

if (
	this.deliveryLocationInfo.loc == property.getId()
	|| this.deliveryLocationInfo.zip == property.getId()
	|| this.deliveryLocationInfo.city == property.getId()
)

Меняем его на:

if (
	this.deliveryLocationInfo.loc == property.getId()
	|| this.deliveryLocationInfo.zip == property.getId()
	|| this.deliveryLocationInfo.city == property.getId()
	|| property.getName()=='Адрес доставки (улица, дом, квартира)' //где property.getName() приравниваем к названию поля адреса в вашей системе
)

Перенос поля «Индекс» в блок пользовательских свойств в sale.order.ajax

В базовом стандартном шаблоне поле индекс достаточно легко перенести. Давайте перенесем поле Индекс из блока местоположений в блок пользовательских свойств

Находим функцию getDeliveryLocationInput (~4451 стр.) и комментируем код this.getZipLocationInput(node);

Идем в функцию editPropsItems (~6728 стр.) и перед propsNode.appendChild(propsItemsContainer) добавляем код this.getZipLocationInput(propsItemsContainer);.

По идее этого должно хватить. Но если все равно не получается, то перейдите по адресу /bitrix/admin/sale_order_props.php?lang=ru и в поле Индекс переведите его а группу свойств Личные данные

Вывод поля «Адрес доставки» в блоке «Доставка»

Идем в функцию editDeliveryInfo и в самый конец добавляем код:

var deliveryItemsContainer = BX.create('DIV', {props: {className: 'col-sm-12 bx-soa-delivery'}}),
	group, property, groupIterator = this.propertyCollection.getGroupIterator(), propsIterator;

if (!deliveryItemsContainer)
	deliveryItemsContainer = this.propsBlockNode.querySelector('.col-sm-12.bx-soa-delivery');

while (group = groupIterator())
{
	propsIterator =  group.getIterator();
	while (property = propsIterator())
	{
		if (property.getName()=='Адрес доставки (улица, дом, квартира)') { //Если свойство совпадает с названием поля адреса в вашей системе

			this.getPropertyRowNode(property, deliveryItemsContainer, false); //вставляем свойство в подготовленный контейнер
			deliveryNode.appendChild(deliveryItemsContainer); //контейнер вместе со свойством в нём добавляем в конце блока с описанием (deliveryInfoContainer)

		}
	}
}

Переносим поле «Адрес доставки» в отдельный новый(!) блок

Данная необходимость возникает, например, в шаблонах Аспро, где поле «Адрес доставки» в блоке с выбором доставок выглядит чужеродно и некрасиво

В шаблоне template.php компонента sale.order.ajax добавляем новый блок в наиболее удобное нам место

<!--	ADRESS BLOCK	-->
<div id="bx-soa-adress-dostavki" data-visited="false" class="bx-soa-section bx-active">
	<div class="bx-soa-section-title-container">
		<h2 class="bx-soa-section-title col-sm-9">
			<span class="bx-soa-section-title-count"></span>Данные для доставки
		</h2>
	</div>
	<div class="bx-soa-section-content container-fluid"></div>
</div>

Идем в файл order_ajax.js и в функции init на ~77 стр добавляем вызов вновь добавленного блока

this.orderAdresBlockNode = BX('bx-soa-adress-dostavki');

Скрываем вывод поля «Адрес доставки» как показано выше

if (
	this.deliveryLocationInfo.loc == property.getId()
	|| this.deliveryLocationInfo.zip == property.getId()
	|| this.deliveryLocationInfo.city == property.getId()
	|| property.getName()=='Адрес доставки' //где property.getName() приравниваем к названию поля адреса в вашей системе
)

Реализуем вывод нового поля в дополнительном блоке. В стандартном случае придется учесть, что поле «Адрес доставки» генерируется отдельно для каждого из типов плательщиков, имеет разный id свойства, а сам наш блок надо скрывать, если выбраны оплаты и доставки при которых поле «Адрес доставки» не выводится. Поэтому код будет таким.

Идем в функцию editDeliveryInfo (любую аналогичную или свою) и после строки deliveryNode.appendChild(deliveryInfoContainer) или в конце функции добавляем:

var deliveryItemsContainer = BX.create('DIV', {props: {className: 'col-sm-12 bx-soa-delivery'}}),
	group, property, groupIterator = this.propertyCollection.getGroupIterator(), propsIterator;

if (!deliveryItemsContainer)
	deliveryItemsContainer = this.propsBlockNode.querySelector('.col-sm-12.bx-soa-delivery');

while (group = groupIterator())
{
	propsIterator =  group.getIterator();
	while (property = propsIterator())
	{

		var personType = this.getSelectedPersonType();

		this.orderAdresBlockNode.querySelector('.bx-soa-section-content').innerHTML = '';

		if (property.getName()=='Адрес доставки' && personType.ID === '1' && property.getId() == '7') {
			this.getPropertyRowNode(property, deliveryItemsContainer, false); //вставляем свойство в подготовленный контейнер
			this.orderAdresBlockNode.querySelector('.bx-soa-section-content').appendChild(deliveryItemsContainer); 

			this.orderAdresBlockNode.classList.remove('hidden');
		}else if (property.getName()=='Адрес доставки' && personType.ID === '2' && property.getId() == '19') { 
			this.getPropertyRowNode(property, deliveryItemsContainer, false); //вставляем свойство в подготовленный контейнер
			this.orderAdresBlockNode.querySelector('.bx-soa-section-content').appendChild(deliveryItemsContainer);

			this.orderAdresBlockNode.classList.remove('hidden');
		}else{
			this.orderAdresBlockNode.classList.add('hidden');
		}

	}
}

Код не самый красивый и правильный, но работает

Переносим поле «Комментарии к заказу» в конец формы

Идем в функцию editActivePropsBlock и комментируем строчку:

this.editPropsComment(propsNode);

Для вывода поля ищем в шаблоне функцию последнего блока. В моем случае это вывод состава заказа. Ищем функцию editBasketItems и в самый конец дописываем:

this.editPropsComment(basketItemsNode);

Переносим поле «Местоположения» в блок пользовательских свойств

По умолчанию данное поле искуственно исключено из блока пользовательских свойств. Чтобы его вернуть на место идем в функцию editPropsItems и удаляем код:

if (
	this.deliveryLocationInfo.loc == property.getId()
	|| this.deliveryLocationInfo.zip == property.getId()
	|| this.deliveryLocationInfo.city == property.getId()
)
	continue;

Запрет Битриксу выбирать доставку по умолчанию

В случае, если у пользователя есть сохраненный профиль, ему автоматически выберется последняя выбранная им доставка, но битрикс ничего не знает о том, что у нас там еще и обязательные поля. Поэтому убираем дефолтный выбор доставки в обработчике OnSaleComponentOrderJsDataHandler. Он у нас уже есть, дописываем в него:

if (isset($arResult['JS_DATA']['LAST_ORDER_DATA']['DELIVERY'])
 && $arResult['JS_DATA']['LAST_ORDER_DATA']['DELIVERY']!='') {
    $arResult['JS_DATA']['LAST_ORDER_DATA']['DELIVERY'] = '';}

В данном случае блок с доставками всегда будет открыт, и пользователь сразу обратит внимание на необходимость заполнения полей. Но! Если пользователь в кабинете удаляет профиль, поле с местоположением будет у него незаполнено, и после его заполнения блок с доставками автоматически закроется без возможности его отредактировать (пропадет кнопка «Изменить»). Это очень трудно пофиксить, чтобы не посыпалось всё остальное, поэтому мы приняли решение убрать возможность редактирования профилей в кабинете пользователя (делается снятием галочки в настройках компонента личного кабинета)

На данный момент у меня всё. Конечно, этот код был написан для конкретного проекта и с определенными допущениями. Но надеюсь, что данная заметка оказалась вам полезной и наведет вас на путь истинный при решении вашей задачи. Ибо документации по методам класса OrderAjaxComponent нет и не будет. Если Вам есть что добавить или поправить — буду рада комментариям.

order_ajax_ext.js

Создаём файл order_ajax_ext.js в папке с шаблоном компонента sale.order.ajax (там же, где лежит файл order_ajax.js) с содержимым:

(function () {
    'use strict'; 
 
    var initParent = BX.Sale.OrderAjaxComponent.init,
        getBlockFooterParent = BX.Sale.OrderAjaxComponent.getBlockFooter,
        editOrderParent = BX.Sale.OrderAjaxComponent.editOrder
        ;
 
    BX.namespace('BX.Sale.OrderAjaxComponentExt');    
 
    BX.Sale.OrderAjaxComponentExt = BX.Sale.OrderAjaxComponent;
 
	//Пример перехвата стандартной функции
    BX.Sale.OrderAjaxComponentExt.init = function (parameters) {
        initParent.apply(this, arguments);
 
        var editSteps = this.orderBlockNode.querySelectorAll('.bx-soa-editstep'), i;
        for (i in editSteps) {
            if (editSteps.hasOwnProperty(i)) {
                BX.remove(editSteps[i]);
            }
        }
 
    }; 
})();

В отдельных переменных определяем функции-методы родительского BX.Sale.OrderAjaxComponent, чтобы их можно было вызвать в дочерних функциях и не получить ошибку Maximum call stack size exceeded.

Копируем ссылку с BX.Sale.OrderAjaxComponent в BX.Sale.OrderAjaxComponentExt.

В методе BX.Sale.OrderAjaxComponentExt.init вызываем родительский init, следом прибиваем ссылки «изменить» у всех блоков. Они нам не нужны.

В методе BX.Sale.OrderAjaxComponentExt.getBlockFooter прибиваем кнопки «Назад» и «Вперед» у блоков. Они нам тоже не понадобятся — все блоки у нас развёрнуты.

В методе BX.Sale.OrderAjaxComponentExt.editOrder ненужным блокам-секциям добавляем css-класс bx-soa-section-hide. По нему мы и будем скрывать ненужные блоки. А так же в этом методе раскрываем только нужные нам блоки: «Покупатель» и «Товары в заказе».

Метод BX.Sale.OrderAjaxComponentExt.initFirstSection оставляем просто пустым. Если этого не сделать, то у анонимов при попытке оформления будет вываливаться эксепшен, по поводу отсутствия необходимых обязательных полей.

Идем дальше.

В файле template.php нашего шаблона нового оформления добавляем подключение нашего скрипта order_ajax_ext.js

После строчки:

$this->addExternalJs($templateFolder.'/order_ajax.js');

добавляем:

$this->addExternalJs($templateFolder.'/order_ajax_ext.js');

А так же в файле template.php меняем все вызовы BX.Sale.OrderAjaxComponent на BX.Sale.OrderAjaxComponentExt

Ну и не забываем добавить в файл стилей, чтобы ненужные блоки скрылись

.bx-soa-section-hide {
    display: none;
}

Краткое описание функций sale.order.ajax

showValidationResult: function(inputs, errors) — функция в которой полям с ошибкой добавляется класс hasError, который помечает ошибкой(в стандартном варианте добавляет обводку красным).

showErrorTooltip: function(tooltipId, targetNode, text) — функция в которой добавляются тултипы для полей с ошибкой.

showError: function(node, msg, border) — функция в которой выводятся ошибки в «групповой контейнер»

refreshOrder: function(result) — функция в которой происходит разбор ошибок, которые приходят от сервера. Там есть ветка result.error

Первые 3 функции отвечают за валидацию на форме без перезагрузки, а четвёртая за обработку результатов от сервера.

Выполнение кода после перезагрузки страницы

Бывает, что нужно регулярно выполнить код после перезагрузки страницы (изменения опций заказа). Например, требуется перерисовать селект. Это просто. Откройте файл order_ajax.js и в самый конец допишите:

(function ($) {
	$(function() {
		BX.addCustomEvent('onAjaxSuccess', function(){
			$(function() {
				$('select').ikSelect({
					autoWidth:false,
					ddFullWidth:false
				});
			});
		});

		$('select').ikSelect({
			autoWidth:false,
			ddFullWidth:false
		});

		window.onresize = function() {
			$('select').ikSelect('redraw');
		}
	});
})(jQuery);

Штатный ajax возвращает данные во фрейм-контейнер. Соответственно, весь js, уже загруженный на странице, находится на уровень выше. Соотв, чтобы обратиться к нему из html, загруженном через ajax, необходимо это указать (top.имя_функции или top.имя_переменной). Спасибо Евгению Жукову

top.$

Для стилизации селектов, которые по умолчанию выглядят ужасно лучше всего использовать замечательную библиотеку ikSelect

Программная смена города в sale.order.ajax на javascript

Часто, по техническому заданию, требуется программная смена города на javascript с ajax перезагрузкой страницы и перерасчетом формы. Покажу как сделать это.

Будем считать, что у вас уже есть code локации из местоположений Битрикс. Тогда в части кода где необходимо произвести смену локации пишем:

//Данный код будет работать не только внутри sale.order.ajax, но и во внешних скриптах, поскольку мы обращается к его функциям через пространство имен BX.Sale.OrderAjaxComponent
var code = '0000000001';//Ваш код локации. В данном случае это Белоруссия

$('.dropdown-field').val(code);//Обновляем код в скрытом инпуте
//$('.bx-ui-sls-fake').val(code);

BX.Sale.OrderAjaxComponent.params.newCity=code; //Записываем в параметры компонента необходимый нам город
BX.Sale.OrderAjaxComponent.sendRequest();//Обновляем форму

Здесь мы заполнили скрытый input нужным нам кодом и записали его в переменную, которой воспользуемся при перестроении формы. Идем в функцию prepareLocations (~1557 стр.).Находим код:

temporaryLocations.push({

И выше него пишем:

//Делаем двойную проверку.
if(typeof this.params!='undefined'){//В первом случае на то, что параметры вообще установлены, поскольку код выполняется первый раз до их инициализации
	if(typeof this.params.newCity!='undefined'){//Вторая проверка - установлена ли наша переменная
		locations[i].lastValue = this.params.newCity;//Если переменная установлена, то подставляем ее в локацию
		delete this.params.newCity;//Обнуляем нашу переменную
	}
}

В итоге у меня получилось так:

for (k in output)
{
	if (output.hasOwnProperty(k))
	{
		//Делаем двойную проверку.
		if(typeof this.params!='undefined'){//В первом случае на то, что параметры вообще установлены, поскольку код выполняется первый раз до их инициализации
			if(typeof this.params.newCity!='undefined'){//Вторая проверка - установлена ли наша переменная
				locations[i].lastValue = this.params.newCity;//Если переменная установлена, то подставляем ее в локацию
				delete this.params.newCity;//Обнуляем нашу переменную
			}
		}


		temporaryLocations.push({
			output: BX.processHTML(output[k], false),
			showAlt: locations[i].showAlt,
			lastValue: locations[i].lastValue,
			coordinates: locations[i].coordinates || false
		});
	}
}

Расчет стоимости доставки для всех служб доставки

Будем полагать, что компонент sale.order.ajax вынесен у вас в отдельную папку

Тогда идем в файл /local/components/YOUR_NAMESPACE/sale.order.ajax/class.php и находим функцию protected function calculateDeliveries(Order $order) (~4289 стр)

Находим условие if ((int)$shipment->getDeliveryId() === $deliveryId) и в области else сразу после кода:

$mustBeCalculated = $this->arParams['DELIVERY_NO_AJAX'] === 'Y'
						|| ($this->arParams['DELIVERY_NO_AJAX'] === 'H' && $deliveryObj->isCalculatePriceImmediately());

пишем:

$mustBeCalculated = true;
$calcResult = $deliveryObj->calculate($shipment);
$calcOrder = $order;

Теперь после обращения к серверу в наш order_ajax.js приходят службы доставки с рассчитанными стоимостями. Остается их только обработать и вывести.

В скрипте находим функциюcreateDeliveryItem: function(item) и работаем с параметром item.PRICE или item.PRICE_FORMATED и выводим его куда нужно.

Получение стоимости доставки для продукта после применения скидок, правил корзины и …

/**
 * Получение стоимости доставки для продукта после применения скидок, правил корзины и ...
 *
 * @param string|int $bitrixProductId Id битриксового продукта
 * @param string     $siteId          Id битриксового сайта, например "s1"
 * @param string|int $userId          Id битриксового пользователя
 * @param string|int $personTypeId    Id битриксового "Тип плательщика" /bitrix/admin/sale_person_type.php?lang=ru
 * @param string|int $deliveryId      Id битриксового "Службы доставки" /bitrix/admin/sale_delivery_service_list.php?lang=ru&filter_group=0
 * @param string|int $paySystemId     Id битриксового "Платежные системы" /bitrix/admin/sale_pay_system.php?lang=ru
 * @param array      $userCityId      Id битриксового города ("куда доставлять")
 *
 * @return null|float null - не удалось получить; float - стоимость (может быть 0 (после применения скидок на доставку))
 *
 * @throws \Bitrix\Main\ArgumentException
 * @throws \Bitrix\Main\ArgumentNullException
 * @throws \Bitrix\Main\ArgumentOutOfRangeException
 * @throws \Bitrix\Main\ArgumentTypeException
 * @throws \Bitrix\Main\LoaderException
 * @throws \Bitrix\Main\NotImplementedException
 * @throws \Bitrix\Main\NotSupportedException
 * @throws \Bitrix\Main\ObjectException
 * @throws \Bitrix\Main\ObjectNotFoundException
 * @throws \Bitrix\Main\SystemException
 */
function getDeliveryPriceForProduct($bitrixProductId, $siteId, $userId, $personTypeId, $deliveryId, $paySystemId, $userCityId)
{
    $result = null;

    \Bitrix\Main\Loader::includeModule('catalog');
    \Bitrix\Main\Loader::includeModule('sale');

    $products = array(
        array(
            'PRODUCT_ID' => $bitrixProductId,
            'QUANTITY'   => 1,
            // 'NAME'       => 'Товар 1', 
            // 'PRICE' => 500,
            // 'CURRENCY' => 'RUB',
        ),
    );
    /** @var \Bitrix\Sale\Basket $basket */
    $basket = \Bitrix\Sale\Basket::create($siteId);
    foreach ($products as $product) {
        $item = $basket->createItem("catalog", $product["PRODUCT_ID"]);
        unset($product["PRODUCT_ID"]);
        $item->setFields($product);
    }

    /** @var \Bitrix\Sale\Order $order */
    $order = \Bitrix\Sale\Order::create($siteId, $userId);
    $order->setPersonTypeId($personTypeId);
    $order->setBasket($basket);

    /** @var \Bitrix\Sale\PropertyValueCollection $orderProperties */
    $orderProperties = $order->getPropertyCollection();
    /** @var \Bitrix\Sale\PropertyValue $orderDeliveryLocation */
    $orderDeliveryLocation = $orderProperties->getDeliveryLocation();
    $orderDeliveryLocation->setValue($userCityId); // В какой город "доставляем" (куда доставлять).

    /** @var \Bitrix\Sale\ShipmentCollection $shipmentCollection */
    $shipmentCollection = $order->getShipmentCollection();

    $delivery = \Bitrix\Sale\Delivery\Services\Manager::getObjectById($deliveryId);
    /** @var \Bitrix\Sale\Shipment $shipment */
    $shipment = $shipmentCollection->createItem($delivery);

    /** @var \Bitrix\Sale\ShipmentItemCollection $shipmentItemCollection */
    $shipmentItemCollection = $shipment->getShipmentItemCollection();
    /** @var \Bitrix\Sale\BasketItem $basketItem */
    foreach ($basket as $basketItem) {
        $item = $shipmentItemCollection->createItem($basketItem);
        $item->setQuantity($basketItem->getQuantity());
    }

    /** @var \Bitrix\Sale\PaymentCollection $paymentCollection */
    $paymentCollection = $order->getPaymentCollection();
    /** @var \Bitrix\Sale\Payment $payment */
    $payment = $paymentCollection->createItem(
        \Bitrix\Sale\PaySystem\Manager::getObjectById($paySystemId)
    );
    $payment->setField("SUM", $order->getPrice());
    $payment->setField("CURRENCY", $order->getCurrency());

    // $result = $order->save(); // НЕ сохраняем заказ в битриксе - нам нужны только применённые "скидки" и "правила корзины" на заказ.
    // if (!$result->isSuccess()) {
    //     //$result->getErrors();
    // }

    $deliveryPrice = $order->getDeliveryPrice();
    if ($deliveryPrice === '') {
        $deliveryPrice = null;
    }
    $result = $deliveryPrice;

    return $result;
}

// Использование
$deliveryPriceForProductCourier = getDeliveryPriceForProduct(
    $bitrixProductId,
    SITE_ID,
    $USER->GetID(),
    '1', // Юридическое лицо  /bitrix/admin/sale_person_type.php?lang=ru
    '1386', // Доставка курьером до дома (в случае наличия "профиля" - указываем его id)  /bitrix/admin/sale_delivery_service_edit.php?lang=ru
    '37', // Наличными или картой при получении  /bitrix/admin/sale_pay_system.php?lang=ru
    $userCity['ID'] // Город пользователя
);

Делаем бесплатную доставку по России с условием

Допустим у нас есть задача сделать доставку по России бесплатной, если стоимость товаров в корзине превышает определенную сумму.

В моем случае в свойствах заказа есть отдельное поле «Страна», поэтому задача упрощается и весь код можно свести к паре проверок

<?php
use Bitrix\Main\EventManager;
use Bitrix\Main\Event;
use \Bitrix\Main\EventResult;

// Получить цену товаров при создании корзины
EventManager::getInstance()->addEventHandler(
    'sale',
    'OnSaleComponentOrderCreated',
    'OnSaleComponentOrderCreated'
);

function OnSaleComponentOrderCreated($order, &$arUserResult, $request, &$arParams, &$arResult, &$arDeliveryServiceAll, &$arPaySystemServiceAll)
{
	//Записываем стоимость товаров
    $_SESSION['ORDER_BASKET_PRICE'] = $order->getBasket()->getPrice();
	
	//Записываем код текущей страны из свойства заказа с id = 24
    $_SESSION['ORDER_LOCATION'] = $arUserResult['ORDER_PROP']['24'];
}

//Изменяем стоимость доставки во время каждого расчета стоимости доставки
EventManager::getInstance()->addEventHandler(
    'sale',
    'onSaleDeliveryServiceCalculate',
    'onSaleDeliveryServiceCalculate'
);

function onSaleDeliveryServiceCalculate(\Bitrix\Main\Event $event)
{

    $baseResult = $event->getParameter('RESULT');
    $shipment = $event->getParameter('SHIPMENT');

    //Если установлена цена, страна Россия и служба доставки не EMS
    if(isset($_SESSION['ORDER_BASKET_PRICE'])
        && $_SESSION['ORDER_LOCATION']=='0000073738'
        && $event->getParameter('DELIVERY_ID') != 45
        && $event->getParameter('DELIVERY_ID') != 31
        && $event->getParameter('DELIVERY_ID') != 33){

		//Смотрим, если настройки доставки мы уже в текущем сеансе получали, то пропускаем блок
        if(!$_SESSION['DELIVERY_PRICE_WHILE_RUSSIA']){
            //Получаем настройки стоимости товара при котором доставка должна быть бесплатной
            $arSelect = Array('ID','PROPERTY_VAL');
            $arFilter = Array(
                '=IBLOCK_ID' => 27, //Настройки
                '=IBLOCK_SECTION_ID' => 86, //Настройки бесплатной доставки по России
                '=ACTIVE' => 'Y',
                '=ID' => array(1776), //Доставка по России бесплатна при стоимости цены больше
            );

            $res = CIBlockElement::GetList(Array(), $arFilter, false, Array(), $arSelect);

            //Ищем настройки бесплатной доставки
            while($ob = $res->GetNextElement())
            {
                $arFields = $ob->GetFields();

                $_SESSION['DELIVERY_PRICE_WHILE_RUSSIA'] = (int)$arFields['PROPERTY_VAL_VALUE'];
            }
        }

        $basketPrice = $_SESSION['ORDER_BASKET_PRICE'];

		//Если цена товаров больше значения из настроек
        if($basketPrice > $_SESSION['DELIVERY_PRICE_WHILE_RUSSIA']) {
            //делаем доставку бесплатной
            $baseResult->setDeliveryPrice(0);
        }
    }

    //Пересохраняем результат
    $event->addResult(
        new \Bitrix\Main\EventResult(
            \Bitrix\Main\EventResult::SUCCESS, array('RESULT' => $baseResult)
        )
    );
}


/*
// Так же обработчик можно вызвать старым способом
AddEventHandler("sale", "onSaleDeliveryServiceCalculate", "onSaleDeliveryServiceCalculate");
function onSaleDeliveryServiceCalculate($result, $shipment, $deliveryID){
    // Проверка id службы доставки
    //17 - Почта России
    //20 - Доставка курьером
    //21 - Пункт выдачи СДЭК
    //24 - Пункт выдачи Boxberry
    //33 - EMS Почта России
	
    if($deliveryID == 20 ){
        if(isset($_SESSION['ORDER_BASKET_PRICE']) )
        {
            $basketPrice = $_SESSION['ORDER_BASKET_PRICE'];

            if($basketPrice > 1000){
                // Записываем новое значение цены на доставку
                $shipment->setBasePriceDelivery(0, true);
            }
        }
    }
	
    if($deliveryID == 31 || $deliveryID == 33){
        $shipment->setBasePriceDelivery(2500, true);
    }
}*/

Не забываем, что код нужно разместить в init.php

Если вы знаете более изящные способы, то буду рад если вы ими поделитесь.

Полезные переменные order_ajax.js

this.result.TOTAL //Данные о текущей цене и стоимости корзины

Массив соответствия международных двухбуквенных кодов стран

const C_COUNTRIES = [
    "RU" => 1,  //Россия
    "AZ" => 2,  //Азербайджан
    "AM" => 3,  //Армения
    "BY" => 4,  //Беларусь
    "GE" => 5,  //Грузия
    "KZ" => 6,  //Казахстан
    "KG" => 7,  //Киргизия
    "LV" => 8,  //Латвия
    "LT" => 9,  //Литва
    "MD" => 10,  //Молдавия
    "TJ" => 11,  //Таджикистан
    "TM" => 12,  //Туркменистан
    "UZ" => 13,  //Узбекистан
    "UA" => 14,  //Украина
    "EE" => 15,  //Эстония
    "AU" => 16,  //Австралия
    "AT" => 17,  //Австрия
    "AL" => 18,  //Албания
    "DZ" => 19,  //Алжир
    "AO" => 20,  //Ангола
    "AE" => 21,  //Арабские Эмираты
    "AR" => 22,  //Аргентина
    "AW" => 23,  //Аруба
    "AF" => 24,  //Афганистан
    "BS" => 25,  //Багамские острова
    "BD" => 26,  //Бангладеш
    "BB" => 27,  //Барбадос
    "BE" => 28,  //Бельгия
    "BJ" => 29,  //Бенин
    "BM" => 30,  //Бермудские острова
    "BG" => 31,  //Болгария
    "BO" => 32,  //Боливия
    "BA" => 33,  //Босния и Герцеговина
    "BR" => 34,  //Бразилия
    "BN" => 35,  //Бруней
    "GB" => 36,  //Великобритания
    "HU" => 37,  //Венгрия
    "VE" => 38,  //Венесуэлла
    "VN" => 39,  //Вьетнам
    "HT" => 40,  //Гаити
    "GM" => 41,  //Гамбия
    "HN" => 42,  //Гондурас
    "GP" => 43,  //Гваделупа
    "GT" => 44,  //Гватемала
    "GN" => 45,  //Гвинея
    "DE" => 46,  //Германия
    "GI" => 47,  //Гибралтар
    "NL" => 48,  //Нидерланды
    "HK" => 49,  //Гонконг
    "GD" => 50,  //Гренада
    "GL" => 51,  //Гренландия
    "GR" => 52,  //Греция
    "GU" => 53,  //Гуана
    "DK" => 54,  //Дания
    "DO" => 55,  //Доминиканская Республика
    "EG" => 56,  //Египет
    "CD" => 57,  //Демократическая республика Конго
    "ZM" => 58,  //Замбия
    "ZW" => 59,  //Зимбабве
    "IL" => 60,  //Израиль
    "IN" => 61,  //Индия
    "ID" => 62,  //Индонезия
    "JO" => 63,  //Иордания
    "IQ" => 64,  //Ирак
    "IR" => 65,  //Иран
    "IE" => 66,  //Ирландия
    "IS" => 67,  //Исландия
    "ES" => 68,  //Испания
    "IT" => 69,  //Италия
    "YE" => 70,  //Йемен
    "KY" => 71,  //Каймановы острова
    "CM" => 72,  //Камерун
    "CA" => 73,  //Канада
    "KE" => 74,  //Кения
    "CY" => 75,  //Кипр
    "CN" => 76,  //Китай
    "CO" => 77,  //Колумбия
    "KH" => 78,  //Камбоджа
    "CG" => 79,  //Конго
    "KR" => 80,  //Корея (Южная)
    "CR" => 81,  //Коста Рика
    "CU" => 82,  //Куба
    "KW" => 83,  //Кувейт
    "LR" => 84,  //Либерия
    "LI" => 85,  //Лихтенштейн
    "LU" => 86,  //Люксембург
    "MR" => 87,  //Мавритания
    "MG" => 88,  //Мадагаскар
    "MK" => 89,  //Македония
    "MY" => 90,  //Малайзия
    "ML" => 91,  //Мали
    "MT" => 92,  //Мальта
    "MX" => 93,  //Мексика
    "MZ" => 94,  //Мозамбик
    "MC" => 95,  //Монако
    "MN" => 96,  //Монголия
    "MA" => 97,  //Морокко
    "NA" => 98,  //Намибия
    "NP" => 99,  //Непал
    "NG" => 100,  //Нигерия
    "NI" => 102,  //Никарагуа
    "NZ" => 103,  //Новая Зеландия
    "NO" => 104,  //Норвегия
    "PK" => 105,  //Пакистан
    "PA" => 106,  //Панама
    "PG" => 107,  //Папуа Новая Гвинея
    "PY" => 108,  //Парагвай
    "PE" => 109,  //Перу
    "PL" => 110,  //Польша
    "PT" => 111,  //Португалия
    "PR" => 112,  //Пуэрто Рико
    "RO" => 113,  //Румыния
    "SA" => 114,  //Саудовская Аравия
    "SN" => 115,  //Сенегал
    "SG" => 116,  //Сингапур
    "SY" => 117,  //Сирия
    "SK" => 118,  //Словакия
    "SI" => 119,  //Словения
    "SO" => 120,  //Сомали
    "SD" => 121,  //Судан
    "US" => 122,  //США
    "TW" => 123,  //Тайвань
    "TH" => 124,  //Таиланд
    "TT" => 125,  //Тринидад и Тобаго
    "TN" => 126,  //Тунис
    "TR" => 127,  //Турция
    "UG" => 128,  //Уганда
    "UY" => 129,  //Уругвай
    "PH" => 130,  //Филиппины
    "FI" => 131,  //Финляндия
    "FR" => 132,  //Франция
    "TD" => 133,  //Чад
    "CZ" => 134,  //Чехия
    "CL" => 135,  //Чили
    "CH" => 136,  //Швейцария
    "SE" => 137,  //Швеция
    "LK" => 138,  //Шри-Ланка
    "EC" => 139,  //Эквадор
    "ET" => 140,  //Эфиопия
    "ZA" => 141,  //ЮАР
    "RS" => 142,  //Сербия
    "JM" => 143,  //Ямайка
    "JP" => 144,  //Япония
    "BH" => 145,  //Бахрейн
    "AD" => 146,  //Андорра
    "BZ" => 147,  //Белиз
    "BT" => 148,  //Бутан
    "BW" => 149,  //Ботсвана
    "BF" => 150,  //Буркина Фасо
    "BI" => 151,  //Бурунди
    "CF" => 152,  //Центральноафриканская Республика
    "KM" => 153,  //Коморос
    "CI" => 154,  //Кот-Д`ивуар
    "DJ" => 155,  //Джибути
    "TL" => 156,  //Восточный Тимор
    "SV" => 157,  //Эль Сальвадор
    "GQ" => 158,  //Экваториальная Гвинея
    "ER" => 159,  //Эритрея
    "FJ" => 160,  //Фижи
    "GA" => 161,  //Габон
    "GH" => 162,  //Гана
    "GW" => 163,  //Гвинея-Биссау
    "KP" => 164,  //Корея (Северная)
    "LB" => 165,  //Ливан
    "LS" => 166,  //Лесото
    "LY" => 167,  //Ливия
    "MV" => 168,  //Мальдивы
    "MH" => 169,  //Маршалские острова
    "NE" => 170,  //Нигер
    "OM" => 171,  //Оман
    "QA" => 172,  //Катар
    "RW" => 173,  //Руанда
    "WS" => 174,  //Самоа
    "SC" => 175,  //Сейшеллы
    "SL" => 176,  //Сьерра-Леоне
    "SR" => 177,  //Суринам
    "SZ" => 178,  //Свазиленд
    "TZ" => 179,  //Танзания
    "EH" => 180,  //Западная Сахара
    "HR" => 181,  //Хорватия
    "AI" => 182,  //Ангилья
    "AQ" => 183,  //Антарктида
    "AG" => 184,  //Антигуа и Барбуда
    "BV" => 185,  //Остров Буве
    "IO" => 186,  //Британские территории в Индийском Океане
    "VG" => 187,  //Британские Виргинские острова
    "MM" => 188,  //Мьянма
    "CV" => 189,  //Кабо-Верде
    "CX" => 190,  //Остров Рождества
    "CC" => 191,  //Кокосовые острова
    "CK" => 192,  //Острова Кука
    "DM" => 193,  //Доминика
    "FK" => 194,  //Фолклендские острова
    "FO" => 195,  //Фарерские острова
    "GF" => 196,  //Гвиана
    "PF" => 197,  //Французская Полинезия
    "TF" => 198,  //Южные Французские территории
    "HM" => 199,  //Острова Херд и Макдоналд
    "KI" => 200,  //Кирибати
    "LA" => 201,  //Лаос
    "MO" => 202,  //Макао
    "MW" => 203,  //Малави
    "MQ" => 204,  //Мартиника
    "MU" => 205,  //Маврикий
    "YT" => 206,  //Майотта
    "FM" => 207,  //Микронезия
    "MS" => 208,  //Монтсеррат
    "NR" => 209,  //Науру
//  210  Антильские острова - они же Карибы, относятся к нескольким государствам, каое имеется ввиду в Битриксе - хз
    "NC" => 211,  //Новая Каледония
    "NU" => 212,  //Ниуэ
    "NF" => 213,  //Остров Норфолк
    "PW" => 214,  //Палау
    "PS" => 215,  //Палестина
    "PN" => 216,  //Остров Питкэрн
    "RE" => 217,  //Реюньон
    "SH" => 218,  //Остров Св.Елены
    "KN" => 219,  //Острова Сент-Киттс и Невис
    "LC" => 220,  //Санта-Лючия
    "PM" => 221,  //Острова Сен-Пьер и Микелон
    "VC" => 222,  //Сент-Винсент и Гренадины
    "SM" => 223,  //Сан-Марино
    "SB" => 224,  //Соломоновы острова
    "LK" => 225,  //Южная Георгия и Южные Сандвичевы острова
    "SJ" => 226,  //Острова Шпицберген и Ян-Майен
    "TG" => 227,  //Того
    "TK" => 228,  //Токелау
    "TO" => 229,  //Тонга
    "TC" => 230,  //Острова Тёркс и Кайкос
    "TV" => 231,  //Тувалу
    "VI" => 232,  //Американские Виргинские острова
    "VU" => 233,  //Вануату
    "VA" => 234,  //Ватикан
    "WF" => 235,  //Острова Уоллис и Футуна
    "ME" => 236,  //Черногория    
];

Альтернативные (кастомные) sale.order.ajax

Интересный кастомный sale.order.ajax предложил alorian в компоненте Opensource Bitrix Order

Ошибка в Битрикс при оформлении заказа window.__jsonp_ymaps_map is not a function

Скорей всего вы используете несколько служб доставки, таких как СДЭК или Boxberry. Зайдите в настройки этих служб и в одной из них отключите использование Яндекс карт. Для работы будет достаточно одного подключения API Яндекс

Баг при автозаполнении телефона. После любого действия начальная цифра (7) дублируется, стирая актуальную цифру телефона

С проблемой столкнулся на одном из шаблонов Аспро Оптимус. Решил обходным путем.

Идем в функцию alterProperty и отключаем штатный механизм маскирования закомментировав данные строки:

//if (settings.IS_PHONE == 'Y')
	//textNode.setAttribute('autocomplete', 'tel');

Подключил библиотеку Inputmask и добавил в конец файла order_ajax.js следующие строки, где #soa-property-3 это идентификатор нашего свойства с телефоном

$(function() {
	$('#soa-property-3').inputmask({"mask": "7 (999) 999-99-99"});

	BX.addCustomEvent('onAjaxSuccess', function(){
		$('#soa-property-3').inputmask({"mask": "7 (999) 999-99-99"});
	});

});