Working with sale.order.ajax in Bitrix
Rus
Eng
Работа с sale.order.ajax в Битрикс

All English-language materials have been translated fully automatically using the Google service

Section navigation:

& nbsp;

This material is aggregate and written more for yourself. As I work and gain experience, I update it.

You need to clearly understand that not all the methods and methods described in the material will work for you. There are many Bitrix versions and the code changes quite significantly. Once worked, the solution will not necessarily work in future updates. For example, some of the methods that worked for me in version 18 stopped working in version 20.

During the work, I noticed that the use of the above methods can cause execution errors in cases when the order of blocks is changed, the quick edit mode is enabled, or the options "Autocomplete payment and delivery for the previous order" are set in the component parameters, or "Compatibility mode for previous template"

Customization & nbsp; sale.order.ajax

Almost none of my clients are satisfied with the appearance of sale.order.ajax. Here I will describe most of the techniques that I use myself when customizing this complex component.

In the template.php file, order blocks are marked with comments:

BASKET ITEMS BLOCK - order basket - product table
REGION BLOCK - select the payer type and enter the location - the buyer's city
DELIVERY BLOCK - the choice of a delivery service
PAY SYSTEMS BLOCK - choosing a payment method
BUYER PROPS BLOCK - a form with fields for entering customer data
ORDER SAVE BLOCK - total and order confirmation button

To make all blocks active, and not just the first one, comment on the second line in this block

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

If deliveries give a computation error, then the delivery blocks are hidden. You can disable this by commenting out the following code:

/*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';
}*/

We remove the hiding of blocks during authorization (when the option `` register with the checkout '' is disabled in the settings).

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 = '';

Open all blocks and remove unnecessary ones

To reveal all hidden blocks, you can use the following methods (personally used on versions up to 20):

Look for the line var active = section.id == this.activeSectionId and change it to

var active = true

Next, turn off the response to a click on the block header

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));
}

So that Region and User are always open

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
			})
		]
	})
);

To remove "edit" links from all blocks in editOrder (~ 2222 pages)

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

To hide the notification that this is filled in automatically add to the end of the file style.css

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

Determining the user's location in automatic mode

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

class Example
{
   /**
   * I know the ID and NAME of the location by the condition of the task
   */
   static $curCityId = XX;  //the numeric value of the location id
   static $curCityName = 'City name';
   
   /**
   * Order properties 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);
   }
}

But this modification allows you to determine the location only by the name of the city:

class Example {
   static function OnSaleComponentOrderProperties(&$arFields)
   {
      static $curCityName = 'City name';
      const PROP_LOCATION = 6;  // - Property ID with location
      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;
      }
   }
}

Hide some property

If you need to hide some property, for example, the index property - set the default value and not show this field to users, then you can make an adjustment in JS. In the getPropertyRowNode function, after switch (propertyType), add the hiding of this property:

if(property.getId()==6){// identifier of the property to hide
   var addressInput=propsItemNode.querySelector('textarea');
   propsItemNode.style='display:none;';
   addressInput.value='нужное значение';
}

We hide the message "You ordered in our online store, so we filled in all the data automatically"

Go to the checkNotifications function on ~ 750 pages and find the code

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);

And framing this additional code. check

if(this.params.MESS_SUCCESS_PRELOAD_TEXT.indexOf('You ordered on our website') === false) {
	//We put the code above here
}

Hide the message "Select your city in the list. If you did not find your city, select another location" and enter the city in the "City" field

Go to the getDeliveryLocationInput function and comment out the code:

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

Or, using styles to hide the class bx-soa-reference

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

Exclude from displaying zero shipping charges

In function & nbsp; getDeliveryPriceNodes: function (delivery) & nbsp; in block & quot; else & quot; replace. Instead of:

priceNodesArray = [delivery.PRICE_FORMATED];

write:

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

This will hide the zero price from the collapsed block with the selected delivery.

Next, you need to hide the zeros in the list of delivery services. To do this, in the createDeliveryItem: function (item) function, we do a strict zero check. Instead of:

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

write:

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

And also instead of:

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

write:

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

And the last one is to hide the zero delivery from the totals. To do this, in the editTotalBlock: function () function, we also set the strict check to zero. Instead of

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

write:

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

As a result, zero delivery will not be shown to the user.

Migration of Delivery Address, Comments, Locations fields in sale.order.ajax

Remove the field Delivery address from the output of the User block

The methods described by my colleagues did not work in my case. I decided to go in an ugly but effective way

Go to the editPropsItems function and find the code:

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

Change it to:

if (
	this.deliveryLocationInfo.loc == property.getId()
	|| this.deliveryLocationInfo.zip == property.getId()
	|| this.deliveryLocationInfo.city == property.getId()
	|| property.getName()=='Адрес доставки (улица, дом, квартира)' //где property.getName() equate to the name of the address field in your system
)

Moving the "Index" field to the custom properties block in sale.order.ajax

In a basic standard template, the index field is easy to move. Let's move the Index field from the Locations block to the Custom Properties block

Find the getDeliveryLocationInput function (~ 4451 pages) and comment out the code this.getZipLocationInput(node);

Go to the editPropsItems function (~ 6728 pages) and before propsNode.appendChild (propsItemsContainer) add the code this.getZipLocationInput (propsItemsContainer); .

In theory, this should be enough. But if it still does not work, then go to the address /bitrix/admin/sale_order_props.php?lang=en and in the Index field translate it to the Personal property group data

Output of the "Delivery address" field in the "Delivery" block

Go to the editDeliveryInfo function and add the code at the very end:

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()=='Адрес доставки (улица, дом, квартира)') { //If the property is the same as the name of the address field on your system

			this.getPropertyRowNode(property, deliveryItemsContainer, false); //insert the property into the prepared container
			deliveryNode.appendChild(deliveryItemsContainer); //add the container together with the property in it at the end of the block with the description (deliveryInfoContainer)

		}
	}
}

Move the "Delivery address" field into a separate new (!) block

This need arises, for example, in Aspro templates, where the "Delivery address" field in the block with the choice of deliveries looks alien and ugly

In the template.php component of the sale.order.ajax component, add a new block in the most convenient place for us


<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>

We go to the file order_ajax.js and in the init function on ~ 77 pages add a call to the newly added block

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

Hide the output of the Delivery Address field as shown above

if (
	this.deliveryLocationInfo.loc == property.getId()
	|| this.deliveryLocationInfo.zip == property.getId()
	|| this.deliveryLocationInfo.city == property.getId()
	|| property.getName()=='Адрес доставки' //where property.getName() equates to the name of the address field on your system
)

We implement the output of the new field in an additional block. In the standard case, you will have to take into account that the "Delivery address" field is generated separately for each of the payer types, has a different property id, and our block itself must be hidden if payments and delivery are selected in which the "Delivery address" field is not displayed. Therefore, the code will be like this.

Go to the editDeliveryInfo function (any similar or your own) and after the line deliveryNode.appendChild (deliveryInfoContainer) or at the end of the function add:

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');
		}

	}
}

The code is not the most beautiful and correct, but it works

Move the "Order Comments" field to the end of the form

Go to the editActivePropsBlock function and comment on the line:

this.editPropsComment(propsNode);

To display the field, look for the function of the last block in the template. In my case, this is the output of the order. We are looking for the function editBasketItems and add to the very end:

this.editPropsComment(basketItemsNode);

Moving the "Locations" field to the custom properties block

By default, this field is artificially excluded from the custom properties block. To put it back in place, go to the editPropsItems function and delete the code:

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

Banning Bitrix from choosing delivery by default

In case the user has a saved profile, the last delivery chosen by him will be automatically selected, but Bitrix does not know anything about the fact that we also have required fields there. Therefore, we remove the default delivery selection in the OnSaleComponentOrderJsDataHandler handler. We already have it, we add to it:

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

In this case, the block with deliveries will always be open, and the user will immediately pay attention to the need to fill in the fields. But! If a user deletes a profile in the account, the field with the location will be blank, and after filling it out, the delivery block will automatically close without the possibility of editing it (the `` Change '' button will disappear). This is very difficult to fix so that everything else does not fall apart, so we decided to remove the ability to edit profiles in the user's account (done by unchecking the box in the settings of the personal account component)

At the moment, I have everything. Of course, this code was written for a specific project and with certain assumptions. But I hope that this note turned out to be useful to you and will lead you on the right path in solving your problem. Because there is no documentation on the methods of the OrderAjaxComponent class and will not be. If you have anything to add or correct, I will be glad to comment.

order_ajax_ext.js

Create a file order_ajax_ext.js in the folder with the sale.order.ajax component template (in the same place as the order_ajax.js file) with the content:

(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]);
            }
        }
 
    }; 
})();

In separate variables we define the function-methods of the parent BX.Sale.OrderAjaxComponent so that they can be called in child functions and do not get the Maximum call stack size exceeded error.

Copy link from BX.Sale.OrderAjaxComponent to BX.Sale.OrderAjaxComponentExt .

In the BX.Sale.OrderAjaxComponentExt.init method, we call the parent init, then nail down the `` edit '' links all blocks. We don't need them.

In the BX.Sale.OrderAjaxComponentExt.getBlockFooter method we nail the Back buttons and `` Forward '' at the blocks. We don't need them either & mdash; all blocks are expanded.

In the BX.Sale.OrderAjaxComponentExt.editOrder method, add the bx-soa-section-hide css class to unnecessary blocks-sections. We will use it to hide unnecessary blocks. And also in this method we reveal only the blocks we need: `` Buyer '' and 'Products in order'.

Leave the BX.Sale.OrderAjaxComponentExt.initFirstSection method just empty. If this is not done, then anonymous users will get an error when they try to register because of the lack of required required fields.

Let's move on.

In the template.php file of our new design template, add the connection of our order_ajax_ext.js script

After the line:

$this-&gt;addExternalJs($templateFolder.&#39;/order_ajax.js&#39;);

add:

$this-&gt;addExternalJs($templateFolder.&#39;/order_ajax_ext.js&#39;);

And also in the template.php file, change all calls to BX.Sale.OrderAjaxComponent to BX.Sale.OrderAjaxComponentExt

Well, do not forget to add to the stylesheet so that unnecessary blocks are hidden

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

Summary of sale.order.ajax functions

showValidationResult: function (inputs, errors) - a function in which the hasError class is added to fields with an error, which is flagged with an error (in the standard version, it adds a red outline).

showErrorTooltip: function (tooltipId, targetNode, text) - a function that adds tooltips for fields with errors.

showError: function (node, msg, border) - a function that displays errors in the "group container"

refreshOrder: function (result) - a function that parses errors that come from the server. There is a branch there result.error

The first 3 functions are responsible for validation on the form without reloading, and the fourth for processing the results from the server.

Code execution after page reload

It happens that you need to regularly execute the code after reloading the page (changing order options). For example, you need to redraw the select. It's simple. Open the file order_ajax.js and add at the very end:

(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);

Standard ajax returns data to a frame container. Accordingly, all js already loaded on the page is one level higher. Correspondingly, to access it from html loaded via ajax, you must specify this (top.function_name or top.variable_name). Thanks Evgeny Zhukov

top.$

For styling selects that look awful by default, it's best to use the awesome library ikSelect

Programmatic change of city in sale.order.ajax to javascript

Often, according to the terms of reference, a programmatic change of the city to javascript with ajax page reload and form recalculation is required. I'll show you how to do it.

Let's assume that you already have code locations from Bitrix locations. Then in the part of the code where it is necessary to change the location, write:

//This code will work not only inside sale.order.ajax, but also in external scripts, since we access its functions through the BX.Sale.OrderAjaxComponent namespace
var code = '0000000001';//Your location code. In this case, it is Belarus

$('.dropdown-field').val(code);//Updating the code in the hidden input
//$('.bx-ui-sls-fake').val(code);

BX.Sale.OrderAjaxComponent.params.newCity=code; //We write the city we need in the parameters of the component
BX.Sale.OrderAjaxComponent.sendRequest();//Updating the form

Here we have filled in the hidden input with the code we need and written it into a variable that we will use when rebuilding the form. Go to the prepareLocations function (~ 1557 pages). Find the code:

temporaryLocations.push({

And above it we write:

//We do a double check.
if(typeof this.params!='undefined'){//In the first case, the fact that the parameters are generally set, since the code is executed for the first time before their initialization
	if(typeof this.params.newCity!='undefined'){//The second check is whether our variable is set
		locations[i].lastValue = this.params.newCity;//If the variable is set, then we substitute it into the location
		delete this.params.newCity;//We set our variable to zero
	}
}

In the end, I got it like this:

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
		});
	}
}

Calculation of shipping costs for all delivery services

Let's assume that the sale.order.ajax component is located in a separate folder

Then go to the file /local/components/YOUR_NAMESPACE/sale.order.ajax/class.php and find the function protected functionDeliveries (Order $ calculate order) (~ 4289 pages)

Find the condition if ((int) $ shipment-> getDeliveryId () === $ deliveryId) and in the else area just after the code:

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

write:

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

Now, after contacting the server, delivery services with calculated costs come to our order_ajax.js . All that remains is to process and display them.

In the script we find the function createDeliveryItem: function (item) and work with the parameter item.PRICE or item.PRICE_FORMATED and display it where needed .

Retrieving shipping costs for a product after discounts and cart rules have been applied

Thanks for the script Denis Myagkov

/**
 *Retrieving shipping costs for a product after applying discounts, cart rules */

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'       => 'Item 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); // In which city we are "delivering" (where to deliver).

    /** @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(); // We DO NOT save the order in bitrix - we only need the applied "discounts" and "cart rules" for the order.
    // if (!$result->isSuccess()) {
    //     //$result->getErrors();
    // }

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

    return $result;
}

// Usage
$deliveryPriceForProductCourier = getDeliveryPriceForProduct(
    $bitrixProductId,
    SITE_ID,
    $USER->GetID(),
    '1', // Entity  /bitrix/admin/sale_person_type.php?lang=ru
    '1386', // Courier delivery to your home (if you have a "profile" - indicate its id)  /bitrix/admin/sale_delivery_service_edit.php?lang=ru
    '37', // Cash or card upon receipt  /bitrix/admin/sale_pay_system.php?lang=ru
    $userCity['ID'] // User city
);

We make free delivery in Russia on condition

Suppose we have a task to make delivery in Russia free if the cost of goods in the basket exceeds a certain amount.

In my case, there is a separate "Country" field in the order properties, so the task is simplified and all the code can be reduced to a couple of checks

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

// Get the price of goods when creating a cart
EventManager::getInstance()->addEventHandler(
    'sale',
    'OnSaleComponentOrderCreated',
    'OnSaleComponentOrderCreated'
);

function OnSaleComponentOrderCreated($order, &$arUserResult, $request, &$arParams, &$arResult, &$arDeliveryServiceAll, &$arPaySystemServiceAll)
{
	// Write down the cost of goods
    $_SESSION['ORDER_BASKET_PRICE'] = $order->getBasket()->getPrice();
	
	// Write the code of the current country from the order property with id = 24
    $_SESSION['ORDER_LOCATION'] = $arUserResult['ORDER_PROP']['24'];
}

// Change the shipping cost during each shipping cost calculation
EventManager::getInstance()->addEventHandler(
    'sale',
    'onSaleDeliveryServiceCalculate',
    'onSaleDeliveryServiceCalculate'
);

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

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

    //If the price is set, the country is Russia and the delivery service is not 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){

		//We look, if we have already received the delivery settings in the current session, then we skip the block
        if(!$_SESSION['DELIVERY_PRICE_WHILE_RUSSIA']){
            //We get the settings for the cost of the goods at which the delivery should be free
            $arSelect = Array('ID','PROPERTY_VAL');
            $arFilter = Array(
                '=IBLOCK_ID' => 27, //Settings
                '=IBLOCK_SECTION_ID' => 86, //Free shipping settings in Russia
                '=ACTIVE' => 'Y',
                '=ID' => array(1776), //Delivery within Russia is free if the price is higher
            );

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

            //Looking for free shipping settings
            while($ob = $res->GetNextElement())
            {
                $arFields = $ob->GetFields();

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

        $basketPrice = $_SESSION['ORDER_BASKET_PRICE'];

		// If the price of goods is higher than the value from the settings
        if($basketPrice > $_SESSION['DELIVERY_PRICE_WHILE_RUSSIA']) {
            //we make delivery free
            $baseResult->setDeliveryPrice(0);
        }
    }

    //Re-save the result
    $event->addResult(
        new \Bitrix\Main\EventResult(
            \Bitrix\Main\EventResult::SUCCESS, array('RESULT' => $baseResult)
        )
    );
}


/*
// The handler can also be called in the old way.
AddEventHandler("sale", "onSaleDeliveryServiceCalculate", "onSaleDeliveryServiceCalculate");
function onSaleDeliveryServiceCalculate($result, $shipment, $deliveryID){
// Check the id of the delivery service
     // 17 - Russian Post
     // 20 - Delivery by courier
     // 21 - CDEK pick-up point
     // 24 - Boxberry pick-up point
     // 33 - EMS Russian Post
	
    if($deliveryID == 20 ){
        if(isset($_SESSION['ORDER_BASKET_PRICE']) )
        {
            $basketPrice = $_SESSION['ORDER_BASKET_PRICE'];

            if($basketPrice > 1000){
                // We write down the new value of the shipping price
                $shipment->setBasePriceDelivery(0, true);
            }
        }
    }
	
    if($deliveryID == 31 || $deliveryID == 33){
        $shipment->setBasePriceDelivery(2500, true);
    }
}*/

Don't forget that the code should be placed in init.php

If you know more elegant ways, I will be glad if you share them.

Making our own field validator using the example of custom validation of the "Phone" field

There are two ways to validate any field in sale.order.ajax .

- Use regular expressions

- Subscribe to OnSaleOrderBeforeSaved and implement your own validation logic

I will describe both methods

1. Go to Store-> Settings-> Order Properties-> Property List and open the desired property. In our case, this is the phone.

We are looking for the field Regular expression to check: and enter it:

/^(\s*)?(\+)?([- _():=+]?\d[- _():=+]?){10,14}(\s*)?$/

This regular pattern is suitable for checking such variants of writing a phone:

+7(903)888-88-88
8(999)99-999-99
+380(67)777-7-777
001-541-754-3010
+1-541-754-3010
19-49-89-636-48018
+233 205599853

2. In most cases, this is sufficient, but we can implement our own validation rule.

$eventManager = \Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler('sale', 'OnSaleOrderBeforeSaved', ['Class\MyEvent', 'OnSaleOrderBeforeSavedHandler']);

...
class MyEvent {
	public static function OnSaleOrderBeforeSavedHandler(\Bitrix\Main\Event $event)
    {
        /** @var \Bitrix\Sale\Order $order */
        $order = $event->getParameter("ENTITY");

        if ($order->isNew()) {//Если проверка требуется только для нового заказа, а не при его редактировании
            $propertyCollection = $order->getPropertyCollection();
            $phoneProperty = $propertyCollection->getPhone();
            $isPhoneValid = MyEvent::checkPhoneNumber($phoneProperty->getValue());

            if ($isPhoneValid != 1) {
                $event->addResult(new \Bitrix\Main\EventResult(
                    \Bitrix\Main\EventResult::ERROR,
                    new \Bitrix\Sale\ResultError('Введите корректный номер телефона', 'PHONE_PROPERTY_INVALID')
                ));
            }
        }
    }
	
    public static function checkPhoneNumber($phone)    {
        if(preg_match("#^([0-9\+]{0,3})\s?\(([0-9]{1,6})\)\s?([0-9\-]{1,9})$#", $phone))
            return 1;
        else
            return 0;
    }
}

Useful Variables order_ajax.js

this.result.TOTAL //Current price and cart value data

Array of matching international two-letter country codes

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,  //Черногория    
];

Alternative (custom) sale.order.ajax

Interesting custom sale.order.ajax suggested by alorian in Opensource Bitrix Order

Error in Bitrix when placing an order window .__ jsonp_ymaps_map is not a function

Most likely you use several delivery services, such as CDEK or Boxberry. Go to the settings of these services and disable the use of Yandex maps in one of them. One Yandex API connection will be enough for work

Bug with phone autocomplete. After any action, the initial digit (7) is duplicated, erasing the current phone number

I ran into a problem on one of the Aspro Optimus templates. I solved it in a workaround.

Go to the alterProperty function and disable the standard masking mechanisms by commenting out these lines:

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

I connected the library Inputmask and added the following lines to the end of the file order_ajax.js, where < code> # soa-property-3 this is the identifier of our property with phone

$(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"});
	});

});

 

 

Based on materials:

Comments

There are no comments yet, you can be the first to leave it

Leave a comment

The site uses a comment pre-moderation system, so your message will be published only after approval by the moderator

You are replying to a user's comment

Send

FEEDBACK

Email me

Are you developing a new service, making improvements to the existing one and want to be better than your competitors? You have come to the right place. I offer you a comprehensive studio-level website development. From me you can order design, layout, programming, development of non-traditional functionality, implementation of communication between CMS, CRM and Data Analitics, as well as everything else related to sites, except for promotion.

Contact, I will always advise on all questions and help you find the most effective solution for your business. I am engaged in the creation of sites in Novosibirsk and in other regions of Russia, I also work with the CIS countries. You will be satisfied with our cooperation

An error occurred while sending, please try again after a while
Message sent successfully

Phones

+7(993) 007-18-96

Email

info@tichiy.ru

Address

Россия, г. Москва

By submitting the form, you automatically confirm that you have read and accept the Privacy Policy site

Contact with me
Send message
By submitting the form, you automatically confirm that you have read and accept Privacy policy of site
Sending successful!
Thank you for contacting :) I will contact you as soon as possible
Sending failed
An error occurred while sending the request. Please wait and try again after a while or call my phone number