403Webshell
Server IP : 80.87.202.40  /  Your IP : 216.73.216.169
Web Server : Apache
System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64
User : bitrix ( 600)
PHP Version : 8.2.27
Disable Function : NONE
MySQL : OFF |  cURL : ON |  WGET : ON |  Perl : ON |  Python : OFF |  Sudo : ON |  Pkexec : ON
Directory :  /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/catalog/lib/product/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/catalog/lib/product/catalogprovider.php
<?php
namespace Bitrix\Catalog\Product;

use Bitrix\Catalog\Config\State;
use Bitrix\Catalog\Product\Store\BatchManager;
use Bitrix\Catalog\Product\Store\CostPriceCalculator;
use Bitrix\Main;
use Bitrix\Catalog;
use Bitrix\Iblock;
use Bitrix\Sale;
use Bitrix\Sale\SaleProviderBase as Base;
use Bitrix\Currency;
use Bitrix\Catalog\Product\Store\DistributionStrategy;

if (Main\Loader::includeModule('sale'))
{
	/**
	 * Class CatalogProvider
	 *
	 * @package Bitrix\Catalog\Product
	 */
	class CatalogProvider extends Base
	{
		private static $userCache = array();

		protected static $hitCache = array();
		protected static $priceTitleCache = array();
		protected static $clearAutoCache = array();

		protected $enableCache = true;

		protected const CACHE_USER_GROUPS = 'USER_GROUPS';
		protected const CACHE_ITEM_WITHOUT_RIGHTS = 'IBLOCK_ELEMENT_PERM_N';
		protected const CACHE_ITEM_RIGHTS = 'IBLOCK_ELEMENT';
		protected const CACHE_ITEM_WITH_RIGHTS = 'IBLOCK_ELEMENT_PERM_Y';
		protected const CACHE_ELEMENT_RIGHTS_MODE = 'ELEMENT_RIGHTS_MODE';
		protected const CACHE_ELEMENT_SHORT_DATA = 'IBLOCK_ELEMENT_SHORT';
		protected const CACHE_PRODUCT = 'CATALOG_PRODUCT';
		protected const CACHE_VAT = 'VAT_INFO';
		protected const CACHE_IBLOCK_RIGHTS = 'IBLOCK_RIGHTS';
		protected const CACHE_STORE = 'CATALOG_STORE';
		protected const CACHE_STORE_PRODUCT = 'CATALOG_STORE_PRODUCT';
		protected const CACHE_PARENT_PRODUCT_ACTIVE = 'PARENT_PRODUCT_ACTIVE';
		protected const CACHE_CATALOG_IBLOCK_LIST = 'CATALOG_IBLOCK_LIST';
		protected const CACHE_PRODUCT_STORE_LIST = 'CACHE_PRODUCT_STORE_LIST';
		protected const CACHE_PRODUCT_AVAILABLE_LIST = 'CACHE_PRODUCT_AVAILABLE_LIST';

		protected const CATALOG_PROVIDER_EMPTY_STORE_ID = Base::EMPTY_STORE_ID;
		protected const BUNDLE_TYPE = 1;

		/** @deprecated */
		protected const RESULT_PRODUCT_LIST = Base::SUMMMARY_PRODUCT_LIST;
		protected const RESULT_CATALOG_LIST = 'CATALOG_DATA_LIST';

		protected const USE_GATALOG_DATA = 'CATALOG_DATA';

		protected const AMOUNT_SRC_QUANTITY = 'QUANTITY';
		protected const AMOUNT_SRC_QUANTITY_LIST = Base::FLAT_QUANTITY_LIST;
		protected const AMOUNT_SRC_PRICE_LIST = 'PRICE_LIST';
		protected const AMOUNT_SRC_STORE_QUANTITY_LIST = Base::STORE_QUANTITY_LIST;
		protected const AMOUNT_SRC_RESERVED_LIST = Base::FLAT_RESERVED_QUANTITY_LIST;
		protected const AMOUNT_SRC_STORE_RESERVED_LIST = Base::STORE_RESERVED_QUANTITY_LIST;

		private const QUANTITY_FORMAT_STORE = 1;
		private const QUANTITY_FORMAT_SHIPMENT = 2;

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function getProductData(array $products)
		{
			return $this->getData($products);
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function getCatalogData(array $products)
		{
			return $this->getData(
				$products,
				[self::USE_GATALOG_DATA]
			);
		}

		/**
		 * @param array $products
		 * @param array $options
		 *
		 * @return Sale\Result
		 */
		private function getData(array $products, array $options = array()): Sale\Result
		{
			$context = $this->getContext();

			$resultProductList = array_fill_keys(array_keys($products), false);

			$result = new Sale\Result();

			$userId = (int)($context['USER_ID'] ?? 0);
			if ($userId < 0)
			{
				$userId = 0;
			}
			$siteId = $context['SITE_ID'] ?? false;
			$currency = $context['CURRENCY'] ?? false;
			$currency = (is_string($currency) ? Currency\CurrencyManager::checkCurrencyID($context['CURRENCY']) : false);
			if ($currency === false)
			{
				$currency = Sale\Internals\SiteCurrencyTable::getSiteCurrency($siteId ?: SITE_ID);
			}
			$adminSection = (defined('ADMIN_SECTION') && ADMIN_SECTION === true);

			if (in_array('DISABLE_CACHE', $options))
			{
				$this->enableCache = false;
			}

			$catalogDataEnabled = self::isCatalogDataEnabled($options);

			$outputVariable = static::getOutputVariable($options);

			$productGetIdList = array();
			$correctProductIds = [];

			$iblockElementSelect = array('ID', 'IBLOCK_ID', 'IBLOCK_SECTION_ID', 'ACTIVE', 'ACTIVE_DATE', 'XML_ID');
			if (!$catalogDataEnabled)
			{
				$iblockElementSelect = array_merge($iblockElementSelect, array('NAME', 'DETAIL_PAGE_URL'));
			}

			$resultList = array();
			foreach ($products as $productId => $itemData)
			{
				$resultList[$productId] = false;
				if (!isset($itemData['ITEM_CODE']))
				{
					$itemData['ITEM_CODE'] = $productId;
					$products[$productId]['ITEM_CODE'] = $productId;
				}

				if (!isset($itemData['BASKET_CODE']))
				{
					$itemData['BASKET_CODE'] = $productId;
					$products[$productId]['BASKET_CODE'] = $productId;
				}

				$hash = $productId."|".$userId;
				$productCachedData = static::getHitCache(self::CACHE_ITEM_RIGHTS, $hash, $iblockElementSelect);
				if ($this->enableCache && !empty($productCachedData))
				{
					$products[$productId]['PRODUCT_DATA'] = $productCachedData;
					$correctProductIds[$productId] = true;
				}
				else
				{
					$productGetIdList[] = $productId;
				}
			}

			if (!empty($productGetIdList))
			{
				$productDataList = $this->getElements(
					$productGetIdList,
					$iblockElementSelect,
					($adminSection ? $userId : null)
				);

				foreach ($productDataList as $productId => $productData)
				{
					$products[$productId]['PRODUCT_DATA'] = $productData;
					$hash = $productId."|".$userId;
					static::setHitCache(self::CACHE_ITEM_RIGHTS, $hash, $productData);
					$correctProductIds[$productId] = true;
				}

				$products = array_intersect_key(
					$products,
					$correctProductIds
				);
				if (empty($products))
				{
					return static::getResultProvider($result, $outputVariable, $resultList);
				}
			}
			unset($correctProductIds);

			$iblockList = array();
			$iblockDataList = array();
			$iblockGetIdList = array();
			foreach ($products as $productId => $productData)
			{
				$iblockId = $productData['PRODUCT_DATA']['IBLOCK_ID'];
				$iblockList[$iblockId][] = $productId;
			}

			foreach ($iblockList as $iblockId => $iblockProductIdList)
			{
				$iblockData = static::getHitCache(self::CACHE_CATALOG_IBLOCK_LIST, $iblockId);
				if ($this->enableCache && !empty($iblockData))
				{
					$iblockDataList[$iblockId] = $iblockData;
				}
				else
				{
					$iblockGetIdList[] = $iblockId;
				}
			}

			if (!empty($iblockGetIdList))
			{
				$iblockDataList = $this->getIblockData($iblockGetIdList) + $iblockDataList;
			}

			$iblockList = array_intersect_key(
				$iblockList,
				$iblockDataList
			);

			$iblockProductMap = static::createIblockProductMap($iblockList, $iblockDataList);

			$correctProductList = static::checkSkuPermission($iblockProductMap);

			$products = array_intersect_key(
				$products,
				array_fill_keys($correctProductList, true)
			);
			if (empty($products))
			{
				return static::getResultProvider($result, $outputVariable, $resultList);
			}

			$products = static::changeSubscribeProductQuantity($products, $iblockProductMap);

			// catalog product

			$catalogSelect = array(
				'ID',
				'CAN_BUY_ZERO',
				'QUANTITY_TRACE',
				'QUANTITY',
				'QUANTITY_RESERVED',
				'MEASURE',
				'TYPE',
				'AVAILABLE',
			);
			if (!$catalogDataEnabled)
			{
				$catalogSelect = array_merge($catalogSelect, array(
					'WEIGHT',
					'WIDTH',
					'HEIGHT',
					'LENGTH',
					'BARCODE_MULTI',
				));
			}
			$catalogSelect = array_merge($catalogSelect, Catalog\Product\SystemField::getProviderSelectFields());

			$catalogProductDataList = static::getCatalogProducts(array_keys($products), $catalogSelect);

			$products = array_intersect_key($products, $catalogProductDataList);
			if (empty($products))
			{
				return static::getResultProvider($result, $outputVariable, $resultList);
			}

			// fill catalog xml id
			$products = self::fillCatalogXmlId($products, $iblockProductMap);
			// prepare offers xml id
			$products = self::fillOfferXmlId($products, $catalogProductDataList);

			// get prices and discounts
			$priceDataList = self::getPriceDataList(
				$products,
				[
					'IS_ADMIN_SECTION' => $adminSection,
					'USER_ID' => $userId,
					'SITE_ID' => $siteId,
					'CURRENCY' => $currency,
				]
			);
			$discountList = self::getDiscountList($priceDataList);

			$productQuantityList = array();
			$productPriceList = array();

			$fullQuantityMode = in_array('FULL_QUANTITY', $options);

			foreach ($products as $productId => $productData)
			{
				$catalogProductData = $catalogProductDataList[$productId];

				$quantityList = array();

				if (array_key_exists('QUANTITY', $productData))
				{
					$quantityList = array($productData['BASKET_CODE'] => $productData['QUANTITY']);
				}

				if (!empty($productData[Base::FLAT_QUANTITY_LIST]))
				{
					$quantityList = $productData[Base::FLAT_QUANTITY_LIST];
				}

				$productQuantityList[$productData['BASKET_CODE']]['QUANTITY_RESERVED'] = $catalogProductData['QUANTITY_RESERVED'];

				$baseCatalogQuantity = (float)$catalogProductData['QUANTITY'];

				$allCount = count($quantityList);
				$sumQuantity = 0;
				foreach ($quantityList as $quantity)
				{
					$sumQuantity += (float)abs($quantity);
				}

				$catalogQuantityForAvaialable = $baseCatalogQuantity;
				$checkCatalogQuantity = $baseCatalogQuantity;

				$isEnough = !($catalogProductData['CHECK_QUANTITY'] && $catalogQuantityForAvaialable < $sumQuantity);
				$setQuantity = $baseCatalogQuantity;
				foreach ($quantityList as $basketCode => $quantity)
				{
					$quantity = (float)abs($quantity);

					if (!$isEnough)
					{
						if ($catalogQuantityForAvaialable - $quantity < 0)
						{
							$quantity = $catalogQuantityForAvaialable;
						}

						$catalogQuantityForAvaialable -= $quantity;
					}

					$productQuantityList[$basketCode]['AVAILABLE_QUANTITY'] = (
						$baseCatalogQuantity >= $quantity || !$catalogProductData['CHECK_QUANTITY']
							? $quantity
							: $baseCatalogQuantity
					);

					if ($fullQuantityMode)
					{
						$checkCatalogQuantity -= $quantity;
						$setQuantity = $quantity;
						$allCount--;

						if ($allCount == 0)
						{
							$setQuantity = $checkCatalogQuantity + $quantity;
						}
					}
					else
					{
						if ($baseCatalogQuantity - $quantity > 0 || !$catalogProductData['CHECK_QUANTITY'])
						{
							$setQuantity = $quantity;
						}
					}

					$productQuantityList[$basketCode]['QUANTITY'] = $setQuantity;
				}
				unset($basketCode, $quantity);

				foreach (array_keys($quantityList) as $basketCode)
				{
					if (isset($priceDataList[$productId][$basketCode]))
					{
						$productPriceList[$basketCode] = $priceDataList[$productId][$basketCode];
					}
				}
				unset($basketCode);

				$measure = isset($catalogProductData['MEASURE']) ? (int)$catalogProductData['MEASURE'] : null;
				$measureFields = static::getMeasure($measure);
				if (!empty($measureFields))
				{
					$catalogProductDataList[$productId] = $measureFields + $catalogProductDataList[$productId];
				}
			}

			unset($fullQuantityMode);

			$resultData = static::setCatalogDataToProducts($products, $catalogProductDataList, $options);

			$priceResultList = static::createProductPriceList($products, $productPriceList, $discountList);

			$resultList = static::createProductResult($products, $resultData, $priceResultList, $productQuantityList);

			$resultList = $resultList + $resultProductList;

			return static::getResultProvider($result, $outputVariable, $resultList);
		}

		private static function getOutputVariable(array $options = array()): string
		{
			return (self::isCatalogDataEnabled($options)
				? static::RESULT_CATALOG_LIST
				: Base::SUMMMARY_PRODUCT_LIST
			);
		}

		private static function getResultProvider(Sale\Result $result, $outputVariable, array $resultList = array()): Sale\Result
		{
			$result->setData(
				array(
					$outputVariable => $resultList,
				)
			);

			return $result;
		}

		/**
		 * @param array $list
		 * @param array $select
		 * @param int|null $userId
		 *
		 * @return array
		 */
		private function getElements(array $list, array $select, ?int $userId = null): array
		{
			$filter = array(
				'ID' => $list,
				'ACTIVE_DATE' => 'Y',
				'CHECK_PERMISSIONS' => 'Y',
				'MIN_PERMISSION' => 'R',
			);
			if ($userId !== null)
			{
				$filter['PERMISSIONS_BY'] = $userId;
			}

			$resultList = array();
			$dbIBlockElement = \CIBlockElement::GetList(
				array(),
				$filter,
				false,
				false,
				$select
			);
			while ($productData = $dbIBlockElement->GetNext())
			{
				$resultList[$productData['ID']] = $productData;
			}
			unset($dbIBlockElement);

			return $resultList;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function getBundleItems(array $products)
		{
			$result = new Sale\Result();

			$resultList = array();

			$productIdList = array();
			static $proxyCatalogProductSet = array();
			static $proxyCatalogSkuData = array();
			$bundleItemList = array();

			$bundleIndex = array();

			foreach ($products as $productId => $productData)
			{
				$proxyCatalogProductSetKey = $productId."|".static::BUNDLE_TYPE;
				if (!empty($proxyCatalogProductSet[$proxyCatalogProductSetKey]) && is_array($proxyCatalogProductSet[$proxyCatalogProductSetKey]))
				{
					$childItemList = $proxyCatalogProductSet[$proxyCatalogProductSetKey];
				}
				else
				{
					$childItemList = \CCatalogProductSet::getAllSetsByProduct($productId, static::BUNDLE_TYPE);
					if (!empty($childItemList) && is_array($childItemList))
					{
						$proxyCatalogProductSet[$proxyCatalogProductSetKey] = $childItemList;
					}
				}

				if (!empty($childItemList))
				{
					$bundleItemList = $childItemList + $bundleItemList;
					$bundleItemListIds = array_keys($childItemList);
					$bundleItemId = reset($bundleItemListIds);
					unset($bundleItemListIds);
					$bundleIndex[$bundleItemId] = $productId;
				}
			}

			$childIdList = array();

			if (empty($bundleItemList))
				return $result;

			$bundleChildList = array();
			$childProducts = array();
			$productIndexList = array();
			foreach ($bundleItemList as $parentItemid => $bundleItemData)
			{
				$productId = $bundleIndex[$parentItemid];
				foreach ($bundleItemData["ITEMS"] as $childItemid => $item)
				{
					if (!isset($childIdList[$item['ITEM_ID']]))
						$childIdList[$item['ITEM_ID']] = true;

					$bundleChildList[$item['ITEM_ID']] = $item;
					$childProducts[$item['ITEM_ID']] = array(
						'ITEM_CODE' => $item['ITEM_ID'],
						'PRODUCT_ID' => $item['ITEM_ID'],
						Base::FLAT_QUANTITY_LIST => [$item['ITEM_ID'] => $item['QUANTITY']],
						'BUNDLE_CHILD' => true,
					);

					$productIndexList[$item['ITEM_ID']] = array(
						'PRODUCT_ID' => $productId,
						'PARENT_ID' => $parentItemid,
						'CHILD_ID' => $childItemid,
					);
				}
			}

			$r = $this->getProductData($childProducts);
			if ($r->isSuccess())
			{
				$resultData = $r->getData();
				if (
					!empty($resultData[Base::SUMMMARY_PRODUCT_LIST])
					&& is_array($resultData[Base::SUMMMARY_PRODUCT_LIST])
				)
				{
					$resultDataList = $resultData[Base::SUMMMARY_PRODUCT_LIST];
					foreach ($resultDataList as $itemCode => $itemData)
					{
						$item = $bundleChildList[$itemCode];
						if (array_key_exists('QUANTITY_TRACE', $itemData))
							unset($itemData['QUANTITY_TRACE']);

						$itemData["PRODUCT_ID"] = $item["ITEM_ID"];
						$itemData["MODULE"] = 'catalog';
						$itemData["PRODUCT_PROVIDER_CLASS"] = Basket::getDefaultProviderName();

						$productIdList[] = $item["ITEM_ID"];

						$itemData["PROPS"] = array();

						if (!empty($proxyCatalogSkuData[$item["ITEM_ID"]]) && is_array($proxyCatalogSkuData[$item["ITEM_ID"]]))
						{
							$parentSkuData = $proxyCatalogSkuData[$item["ITEM_ID"]];
						}
						else
						{
							$parentSkuData = \CCatalogSku::GetProductInfo($item["ITEM_ID"]);
							if ($parentSkuData)
							{
								$proxyCatalogSkuData[$item["ITEM_ID"]] = $parentSkuData;
							}
						}

						if (!empty($parentSkuData))
						{
							$childDataList = array();
							$childIdGetList = array();

							$iblockPropertyDataList = array();
							$iblockPropertyIdList = array();

							$propsSku = array();

							foreach ($childIdList as $childId => $parentValue)
							{
								$productData = static::getHitCache(self::CACHE_ELEMENT_SHORT_DATA, $item["ITEM_ID"]);
								if (!empty($productData))
								{
									$childDataList[$childId] = $productData;
									if (!isset($iblockPropertyIdList[$productData['IBLOCK_ID']]))
									{
										$iblockPropertyIdList[$productData['IBLOCK_ID']] = true;
									}
								}
								else
								{
									$childIdGetList[] = $childId;
								}
							}

							if (!empty($childIdGetList))
							{
								$iterator = Iblock\ElementTable::getList([
									'select' => [
										'ID',
										'IBLOCK_ID',
										'NAME',
										'IBLOCK_SECTION_ID',
									],
									'filter' => \CIBlockElement::getPublicElementsOrmFilter(['@ID' => $childIdGetList]),
								]);
								while ($productData = $iterator->fetch())
								{
									static::setHitCache(self::CACHE_ELEMENT_SHORT_DATA, $productData["ID"], $productData);
									$childDataList[$productData["ID"]] = $productData;

									if (!isset($iblockPropertyIdList[$productData['IBLOCK_ID']]))
									{
										$iblockPropertyIdList[$productData['IBLOCK_ID']] = true;
									}
								}
							}

							foreach ($iblockPropertyIdList as $iblockPropertyId => $iblockPropertyValue)
							{
								if ($propsSku = static::getHitCache('IBLOCK_PROPERTY', $iblockPropertyId))
								{
									$iblockPropertyDataList[$iblockPropertyId] = $propsSku;
								}
								else
								{
									$dbOfferProperties = \CIBlock::GetProperties($iblockPropertyId, array(), array("!XML_ID" => "CML2_LINK"));
									while($offerProperties = $dbOfferProperties->Fetch())
									{
										$propsSku[] = $offerProperties["CODE"];
									}
									static::setHitCache('IBLOCK_PROPERTY', $iblockPropertyId, $propsSku);
								}
							}

							$propSkuHash = (!empty($propsSku)) ? md5(join('|', $propsSku)): md5($item["ITEM_ID"]);

							$proxyProductPropertyKey = $item["ITEM_ID"]."_".$parentSkuData["IBLOCK_ID"]."_".$propSkuHash;

							$productProperties = static::getHitCache('PRODUCT_PROPERTY', $proxyProductPropertyKey);
							if (empty($productProperties))
							{
								$productProperties = \CIBlockPriceTools::GetOfferProperties(
									$item["ITEM_ID"],
									$parentSkuData["IBLOCK_ID"],
									$propsSku
								);

								static::setHitCache('PRODUCT_PROPERTY', $proxyProductPropertyKey, $productProperties);
							}

							if (!empty($productProperties))
							{
								foreach ($productProperties as $propData)
								{
									$itemData["PROPS"][] = array(
										"NAME" => $propData["NAME"],
										"CODE" => $propData["CODE"],
										"VALUE" => $propData["VALUE"],
										"SORT" => $propData["SORT"],
									);
								}
							}

						}

						$parentProductIndexData = $productIndexList[$itemCode];

						$priceData = array();
						if (!empty($itemData['PRICE_LIST']))
						{
							$priceData = reset($itemData['PRICE_LIST']);
							unset($itemData['PRICE_LIST']);
						}

						if (array_key_exists('PRODUCT', $itemData))
						{
							unset($itemData['PRODUCT']);
						}

						$bundleItemList[$parentProductIndexData['PARENT_ID']]["ITEMS"][$parentProductIndexData['CHILD_ID']] = array_merge($item,  $itemData, $priceData);
					}
				}
			}

			$elementList = static::getHitCache('IBLOCK_ELEMENT_LIST', $productId);
			if (empty($elementList))
			{
				$productRes = \CIBlockElement::GetList(
					array(),
					array('ID' => $productIdList),
					false,
					false,
					array("ID", "IBLOCK_ID", "IBLOCK_SECTION_ID", "PREVIEW_PICTURE", "DETAIL_PICTURE", "IBLOCK_TYPE_ID", "XML_ID")
				);
				while ($productData = $productRes->GetNext())
				{
					$elementList[$productData['ID']] = $productData;
				}

				if (!empty($elementList) && is_array($elementList))
				{
					static::setHitCache('IBLOCK_ELEMENT_LIST', $productId, $elementList);
				}
			}

			if (!empty($elementList) && is_array($elementList))
			{
				foreach ($bundleItemList as $bundleParentId => $bundleItemData)
				{
					foreach ($bundleItemData["ITEMS"] as $bundleChildId => $item)
					{
						if (!$elementList[$item["ITEM_ID"]])
							continue;

						$elementData = $elementList[$item["ITEM_ID"]];

						$properties = array();
						$strIBlockXmlID = (string)\CIBlock::GetArrayByID($elementData['IBLOCK_ID'], 'XML_ID');
						if ($strIBlockXmlID != "")
						{
							$properties[] = array(
								"NAME" => "Catalog XML_ID",
								"CODE" => "CATALOG.XML_ID",
								"VALUE" => $strIBlockXmlID,
							);

							$elementData['CATALOG_XML_ID'] = $strIBlockXmlID;

						}

						if (!empty($proxyCatalogSkuData[$item["ITEM_ID"]]) && strpos($elementData["XML_ID"], '#') === false)
						{
							$parentSkuData = $proxyCatalogSkuData[$item["ITEM_ID"]];
							if (!empty($proxyParentData[$parentSkuData['ID']]) && is_array($proxyParentData[$parentSkuData['ID']]))
							{
								$parentData = $proxyParentData[$parentSkuData['ID']];
							}
							else
							{
								$parentIterator = Iblock\ElementTable::getList(
									array(
										'select' => array('ID', 'XML_ID'),
										'filter' => array('ID' => $parentSkuData['ID']),
									)
								);

								$parentData = $parentIterator->fetch();
								if (!empty($parentData))
								{
									$proxyParentData[$parentSkuData['ID']] = $parentData;
								}

								unset($parentIterator);
							}

							$elementData["XML_ID"] = $parentData['XML_ID'].'#'.$elementData["XML_ID"];
							unset($parentData);
						}

						$properties[] = array(
							"NAME" => "Product XML_ID",
							"CODE" => "PRODUCT.XML_ID",
							"VALUE" => $elementData["XML_ID"],
						);

						$bundleItemData = $bundleItemList[$bundleParentId]["ITEMS"][$bundleChildId];

						$bundleItemProps = array();
						if (is_array($elementData["PROPS"]) && !empty($elementData["PROPS"]))
						{
							$bundleItemProps = $elementData["PROPS"];
						}

						if (!empty($properties))
						{
							$bundleItemProps = $bundleItemProps + $properties;
						}

						$bundleItemList[$bundleParentId]["ITEMS"][$bundleChildId] = $bundleItemData + array(
								'IBLOCK_ID' => $elementData["IBLOCK_ID"],
								'IBLOCK_SECTION_ID' => $elementData["IBLOCK_SECTION_ID"],
								'PREVIEW_PICTURE' => $elementData["PREVIEW_PICTURE"],
								'DETAIL_PICTURE' => $elementData["DETAIL_PICTURE"],

								'CATALOG_XML_ID' => $elementData["CATALOG_XML_ID"],
								'PRODUCT_XML_ID' => $elementData["XML_ID"],
							);

						$bundleItemList[$bundleParentId]["ITEMS"][$bundleChildId]['PROPS'] = $bundleItemProps;
					}
				}

			}

			foreach(GetModuleEvents("sale", "OnGetSetItems", true) as $eventData)
			{
				ExecuteModuleEventEx($eventData, array(&$bundleItemList));
			}

			if (!empty($bundleItemList))
			{
				foreach ($bundleItemList as $bundleParentId => $bundleData)
				{
					if (empty($bundleIndex[$bundleParentId]))
						continue;

					$productId = $bundleIndex[$bundleParentId];

					$resultList[$productId] = $bundleData;
				}

				$result->setData(
					array(
						'BUNDLE_LIST' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param $userId
		 *
		 * @return bool|array
		 */
		protected static function getUserGroups($userId)
		{
			$userId = (int)$userId;
			if ($userId < 0)
				return false;

			if (!isset(self::$userCache[$userId]))
				self::$userCache[$userId] = Main\UserTable::getUserGroupIds($userId);

			return self::$userCache[$userId];
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function ship(array $products)
		{
			return $this->shipProducts($products);
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function unship(array $products)
		{
			$result = new Sale\Result();

			$r = $this->tryUnship($products);
			if (!$r->isSuccess())
			{
				$result->addErrors($r->getErrors());
				return $result;
			}

			$data = $r->getData();

			if (!empty($data['PRODUCTS_LIST_SHIPPED']))
			{
				$productsList = array();
				foreach ($data['PRODUCTS_LIST_SHIPPED'] as $productId => $value)
				{
					if ($value && !empty($products[$productId]))
					{
						$productsList[$productId] = $products[$productId];
					}
				}

				if (!empty($productsList))
				{
					$this->shipProducts($productsList);
				}
			}

			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 * @throws Main\ObjectNotFoundException
		 */
		public function deliver(array $products)
		{
			$result = new Sale\Result();

			$resultList = array();

			$productOrderList = static::createOrderListFromProducts($products);

			$deliverProductList = array();
			foreach ($products as $productId => $productData)
			{
				$userId = null;
				$orderPaid = null;
				$orderId = null;

				if (isset($productData['USER_ID']))
				{
					$userId = $productData['USER_ID'];
				}

				if (isset($productData['ORDER_ID']))
				{
					$orderId = $productData['ORDER_ID'];
				}

				if (isset($productData['PAID']))
				{
					$orderPaid = $productData['PAID'];
				}

				/**
				 * @var int $orderId
				 * @var Sale\Order $order
				 */

				if (isset($productOrderList[$productId]))
				{
					foreach ($productOrderList[$productId] as $orderId => $order)
					{
						if (!isset($resultList[$productId]))
						{
							$deliverProductList[] = array(
								'PRODUCT_ID' => $productId,
								'USER_ID' => $order->getUserId(),
								'PAID' => $order->isPaid(),
								'ORDER_ID' => $orderId,
							);
						}
					}
				}
				else
				{
					if (isset($productData['USER_ID']))
					{
						$userId = $productData['USER_ID'];
					}

					if (isset($productData['ORDER_ID']))
					{
						$orderId = $productData['ORDER_ID'];
					}

					if (isset($productData['PAID']))
					{
						$orderPaid = $productData['PAID'];
					}

					$deliverProductList[] = array(
						'PRODUCT_ID' => $productId,
						'USER_ID' => $userId,
						'PAID' => $orderPaid,
						'ORDER_ID' => $orderId,
					);
				}
			}

			if (!empty($deliverProductList))
			{
				foreach ($deliverProductList as $productData)
				{
					$productId = $productData['PRODUCT_ID'];
					$resultList[$productId] = \CatalogPayOrderCallback(
						$productId,
						$productData['USER_ID'],
						$productData['PAID'],
						$productData['ORDER_ID']
					);
				}
			}

			if (!empty($resultList))
			{
				$result->setData(
					array(
						'DELIVER_PRODUCTS_LIST' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function viewProduct(array $products)
		{
			$result = new Sale\Result();

			$resultList = array();

			foreach ($products as $productId => $itemData)
			{
				if (!isset($resultList[$productId]))
				{
					$context = $this->getContext();

					$resultList[$productId] = \CatalogViewedProductCallback(
						$productId,
						$context['USER_ID'],
						$context['SITE_ID']
					);
				}

			}

			if (!empty($resultList))
			{
				$result->setData(
					array(
						'VIEW_PRODUCTS_LIST' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array $items
		 *
		 * @return Sale\Result
		 */
		public function recurring(array $items)
		{
			$result = new Sale\Result();

			$resultList = array();

			foreach ($items as $productId => $itemData)
			{
				if (!isset($resultList[$productId]))
				{
					$context = $this->getContext();

					$resultList[$productId] = \CatalogRecurringCallback(
						$productId,
						$context['USER_ID']
					);
				}

			}

			if (!empty($resultList))
			{
				$result->setData(
					array(
						'RECURRING_PRODUCTS_LIST' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array $items
		 *
		 * @return Sale\Result
		 */
		public function checkBarcode(array $items)
		{
			$result = new Sale\Result();

			$resultList = array();

			foreach ($items as $barcodeParams)
			{
				$resultList[$barcodeParams['BARCODE']] = false;
				$dbres = \CCatalogStoreBarcode::GetList(
					array(),
					$barcodeParams
				);
				$resultList[$barcodeParams['BARCODE']] = (bool)($dbres->GetNext());

			}

			if (!empty($resultList))
			{
				$result->setData(
					array(
						'BARCODE_CHECK_LIST' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		protected function shipProducts(array $products)
		{
			$result = new Sale\Result();

			$resultList = array_fill_keys(array_keys($products), false);

			$availableItems = $this->createProductsListWithCatalogData($products);

			$productStoreDataList = [];
			if (Catalog\Config\State::isUsedInventoryManagement())
			{
				$r = $this->getProductListStores($products);
				if ($r->isSuccess())
				{
					$data = $r->getData();
					if (!empty($data['PRODUCT_STORES_LIST']))
					{
						$productStoreDataList = $data['PRODUCT_STORES_LIST'];
					}
					unset($data);
				}
				unset($r);
			}

			foreach ($availableItems as $productId => $productData)
			{
				$r = static::shipProduct(
					$productData,
					(!empty($productStoreDataList[$productId])
						? $productStoreDataList[$productId]
						: []
					)
				);
				if (!$r->isSuccess())
				{
					$result->addErrors($r->getErrors());
					$result->addWarnings($r->getErrors());
				}

				$resultList[$productId] = $r->isSuccess();
			}

			$result->setData([
				'SHIPPED_PRODUCTS_LIST' => $resultList,
			]);

			return $result;
		}

		// private function

		/**
		 * @param array $quantityList
		 *
		 * @return Sale\Result
		 */
		private static function updateCatalogStoreAmount(array $quantityList): Sale\Result
		{
			$result = new Sale\Result();
			$resultList = array();

			if (empty($quantityList))
			{
				return $result;
			}

			foreach ($quantityList as $catalogStoreId => $amount)
			{
				$fields = [
					'AMOUNT' => $amount['AMOUNT'],
				];
				if (isset($amount['QUANTITY_RESERVED']))
				{
					$fields['QUANTITY_RESERVED'] = $amount['QUANTITY_RESERVED'];
				}

				$internalResult = Catalog\StoreProductTable::update($catalogStoreId, $fields);

				$resultList[$catalogStoreId] = $internalResult->isSuccess();
			}

			$result->setData(
				array(
					'AMOUNT_UPDATED_LIST' => $resultList,
				)
			);

			return $result;
		}

		/**
		 * @param array $productData
		 * @param array $productStoreDataList
		 *
		 * @return Sale\Result
		 */
		private static function shipProduct(array $productData, array $productStoreDataList = array()): Sale\Result
		{
			$result = new Sale\Result();

			$productId = $productData['PRODUCT_ID'];

			$productQuantity = self::getTotalAmountFromQuantityList($productData);

			$needShip = ($productQuantity < 0);

			if (
				//Catalog\Config\State::isUsedInventoryManagement()
				$productData['PRODUCT']['USED_STORE_INVENTORY']
			)
			{
				if (empty($productStoreDataList) && $needShip)
				{
					$result->addError(
						new Sale\ResultError(
							Main\Localization\Loc::getMessage(
								"DDCT_DEDUCTION_STORE_ERROR",
								array_merge(
									self::getProductCatalogInfo($productId),
									array("#PRODUCT_ID#" => $productId)
								)
							), "DDCT_DEDUCTION_STORE_ERROR"
						)
					);
					return $result;
				}

				$setQuantityList = array();
				$r = static::getSetableStoreQuantityProduct($productData, $productStoreDataList);
				if ($r->isSuccess())
				{
					$resultData = $r->getData();
					if (!empty($resultData[Base::FLAT_QUANTITY_LIST]))
					{
						$setQuantityList = $resultData[Base::FLAT_QUANTITY_LIST];
					}
				}
				else
				{
					return $r;
				}

				/*if (!$productData['PRODUCT']['USED_STORE_INVENTORY']) // product types without stores
				{
					$setQuantityList = [];
				} */

				$r = static::updateCatalogStoreAmount($setQuantityList);
				if ($r->isSuccess())
				{
					$resultData = $r->getData();
					if (!empty($resultData['AMOUNT_UPDATED_LIST']))
					{
						foreach($resultData['AMOUNT_UPDATED_LIST'] as $catalogStoreIsUpdated)
						{
							if ($catalogStoreIsUpdated === true)
							{
								static::clearHitCache(self::CACHE_STORE_PRODUCT);
								if ($needShip)
								{
									$r = static::deleteBarcodes($productData);
								}
								else
								{
									$r = static::addBarcodes($productData);
								}

								if (!$r->isSuccess())
								{
									$result->addErrors($r->getErrors());
								}
							}
						}
					}
				}
				else
				{
					return $r;
				}

				return static::shipQuantityWithStoreControl($productData);
			}
			elseif (isset($productData["CATALOG"]))
			{
				if ($productData["CATALOG"]["QUANTITY_TRACE"] == "N")
				{
					return $result;
				}
			}

			return static::shipQuantityWithoutStoreControl($productData);

		}

		/**
		 * @param array $productData
		 *
		 * @return Sale\Result
		 */
		private static function shipQuantityWithStoreControl(array $productData): Sale\Result
		{
			$result = new Sale\Result();

			$productId = (int)$productData['PRODUCT_ID'];

			$productQuantity = self::getTotalAmountFromQuantityList($productData);

			$catalogData = $productData['CATALOG'];

			$isExistsReserve = static::isExistsCommonStoreReserve($productData) && static::isReservationEnabled();
			$isNeedShip = ($productQuantity < 0);

			$productQuantity = abs($productQuantity);

			$fields = array();

			$catalogReservedQuantity = (float)$catalogData['QUANTITY_RESERVED'];
			$catalogQuantity = self::getTotalAmountFromPriceList($catalogData);

			$sumCatalogQuantity = $catalogReservedQuantity + $catalogQuantity;

			if ($isNeedShip)
			{
				if ($isExistsReserve)
				{
					if ($catalogReservedQuantity >= $productQuantity)
					{
						$fields["QUANTITY_RESERVED"] = $catalogReservedQuantity - $productQuantity;
					}
					elseif ($sumCatalogQuantity >= $productQuantity)
					{
						$fields["QUANTITY_RESERVED"] = 0;
						$fields["QUANTITY"] = $catalogQuantity - ($productQuantity - $catalogReservedQuantity);
					}
					else
					{
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									"DDCT_DEDUCTION_NOT_ENOUGHT_QUANTITY_PRODUCT",
									array_merge(
										self::getProductCatalogInfo($productId),
										array("#PRODUCT_ID#" => $productId)
									)
								), "DDCT_DEDUCTION_NOT_ENOUGHT_QUANTITY_PRODUCT"
							)
						);
						return $result;
					}
				}
				else
				{
					if ($productQuantity <= $catalogQuantity)
					{
						$fields["QUANTITY"] = $catalogQuantity - $productQuantity;
					}
					elseif ($productQuantity <= $sumCatalogQuantity)
					{
						$fields["QUANTITY"] = 0;
						$fields["QUANTITY_RESERVED"] = $catalogReservedQuantity - ($productQuantity - $catalogQuantity);
					}
					else
					{
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									"DDCT_DEDUCTION_NOT_ENOUGHT_QUANTITY_PRODUCT",
									array_merge(
										self::getProductCatalogInfo($productId),
										array("#PRODUCT_ID#" => $productId)
									)
								), "DDCT_DEDUCTION_NOT_ENOUGHT_QUANTITY_PRODUCT"
							)
						);
						return $result;
					}
				}
			}
			else
			{
				if ($isExistsReserve)
				{
					$fields["QUANTITY_RESERVED"] = $catalogReservedQuantity + $productQuantity;
				}
				else
				{
					$fields["QUANTITY"] = $catalogQuantity + $productQuantity;
				}
			}

			if (!$productData['PRODUCT']['USED_RESERVATION'])
			{
				if (isset($fields['QUANTITY_RESERVED']))
				{
					unset($fields['QUANTITY_RESERVED']);
				}
			}

			$isUpdated = false;
			if (!empty($fields))
			{
				$internalResult = Catalog\Model\Product::update($productId, $fields);

				if ($internalResult->isSuccess())
				{
					$isUpdated = true;
					$quantityValues = array();

					if (isset($fields['QUANTITY']))
					{
						$quantityValues[QuantityControl::QUANTITY] = $fields['QUANTITY'];
						QuantityControl::resetAvailableQuantity($productId);
					}

					if (isset($fields['QUANTITY_RESERVED']))
					{
						$quantityValues[QuantityControl::RESERVED_QUANTITY] = $fields['QUANTITY_RESERVED'];
					}

					if (!empty($quantityValues))
					{
						QuantityControl::setValues($productId, $quantityValues);
					}
				}
				else
				{
					self::convertErrors($internalResult);
				}
				unset($internalResult);
			}

			$result->setData(
				array(
					'IS_UPDATED' => $isUpdated,
				)
			);

			return $result;
		}

		/**
		 * @param array $productData
		 *
		 * @return Sale\Result
		 */
		private static function shipQuantityWithoutStoreControl(array $productData): Sale\Result
		{
			$result = new Sale\Result();
			$productId = (int)$productData['PRODUCT_ID'];

			$catalogData = $productData['CATALOG'];

			$productQuantity = self::getTotalAmountFromQuantityList($productData);

			$catalogReservedQuantity = (float)$catalogData['QUANTITY_RESERVED'];
			$catalogQuantity = self::getTotalAmountFromPriceList($catalogData);

			$fields = array();

			$isExistsReserve = static::isExistsCommonStoreReserve($productData) && static::isReservationEnabled();
			$isNeedShip = ($productQuantity < 0);

			if ($isNeedShip)
			{
				$productQuantity = abs($productQuantity);
				if (($productQuantity <= $catalogReservedQuantity + $catalogQuantity) || $catalogData["CAN_BUY_ZERO"] == "Y")
				{
					if ($isExistsReserve)
					{
						if ($productQuantity <= $catalogReservedQuantity)
						{
							$needReservedQuantity = $catalogReservedQuantity - $productQuantity;
							$fields["QUANTITY_RESERVED"] = $needReservedQuantity;
						}
						else
						{
							$fields["QUANTITY_RESERVED"] = 0;
							$fields["QUANTITY"] = $catalogQuantity - ($productQuantity - $catalogReservedQuantity);
						}
					}
					else
					{
						$fields["QUANTITY"] = $catalogQuantity - $productQuantity;
					}

				}
				else //not enough products - don't deduct anything
				{
					$result->addError(
						new Sale\ResultError(
							Main\Localization\Loc::getMessage(
								"DDCT_DEDUCTION_QUANTITY_ERROR",
								array_merge(
									self::getProductCatalogInfo($productId),
									array("#PRODUCT_ID#" => $productId)
								)
							), "DDCT_DEDUCTION_QUANTITY_ERROR"
						)
					);
				}
			}
			else
			{
				if ($isExistsReserve)
				{
					$fields["QUANTITY_RESERVED"] = $catalogReservedQuantity + $productQuantity;
				}
				else
				{
					$fields["QUANTITY"] = $catalogQuantity + $productQuantity;
				}
			}

			if (!$productData['PRODUCT']['USED_RESERVATION'])
			{
				if (isset($fields['QUANTITY_RESERVED']))
				{
					unset($fields['QUANTITY_RESERVED']);
				}
			}

			if (!empty($fields))
			{
				$internalResult = Catalog\Model\Product::update($productId, $fields);

				if ($internalResult-> isSuccess())
				{
					$quantityValues = array();

					if (isset($fields['QUANTITY']))
					{
						$quantityValues[QuantityControl::QUANTITY] = $fields['QUANTITY'];
						QuantityControl::resetAvailableQuantity($productId);
					}

					if (isset($fields['QUANTITY_RESERVED']))
					{
						$quantityValues[QuantityControl::RESERVED_QUANTITY] = $fields['QUANTITY_RESERVED'];
					}

					if (!empty($quantityValues))
					{
						QuantityControl::setValues($productId, $quantityValues);
					}
				}
				else
				{
					self::convertErrors($internalResult);
				}
				unset($internalResult);
			}

			return $result;
		}

		private static function isExistsCommonStoreReserve(array $productData): bool
		{
			if (
				empty($productData['NEED_RESERVE_BY_STORE_LIST'])
				|| !is_array($productData['NEED_RESERVE_BY_STORE_LIST'])
			)
			{
				return false;
			}

			foreach ($productData['NEED_RESERVE_BY_STORE_LIST'] as $block)
			{
				if (empty($block) || !is_array($block))
				{
					continue;
				}
				if (in_array(true, $block, true))
				{
					return true;
				}
			}

			return false;
		}

		/**
		 * @param array $productData
		 * @param array $productStoreDataList
		 *
		 * @return Sale\Result
		 */
		private static function getSetableStoreQuantityProduct(array $productData, array $productStoreDataList): Sale\Result
		{
			$result = new Sale\Result();

			$setQuantityList = array();
			$productQuantity = self::getTotalAmountFromQuantityList($productData);
			$isNeedShip = ($productQuantity < 0);

			$quantityByStore = self::getQuantityDataFromStore($productData);
			$needQuantityList = $quantityByStore['AMOUNT'];

			if (empty($needQuantityList))
			{
				$autoShipStore = static::getAutoShipStoreData($productData, $productStoreDataList);

				if (!empty($autoShipStore))
				{
					$needQuantityList[$autoShipStore['STORE_ID']] = ($productQuantity > $autoShipStore['AMOUNT'] ? $autoShipStore['AMOUNT'] : abs($productQuantity));

					$shipmentItemList = $productData['SHIPMENT_ITEM_LIST'];
					/** @var Sale\ShipmentItem $shipmentItem */
					foreach ($shipmentItemList as $index => $shipmentItem)
					{
						$shipmentItemStoreCollection = $shipmentItem->getShipmentItemStoreCollection();
						if ($shipmentItemStoreCollection && $shipmentItemStoreCollection->count() === 0)
						{
							$item = $shipmentItemStoreCollection->createItem($shipmentItem->getBasketItem());
							$item->setField('STORE_ID', $autoShipStore['STORE_ID']);
							$item->setField('QUANTITY', abs($productData['SHIPMENT_ITEM_QUANTITY_LIST'][$index]));
						}
					}
				}
			}

			if (!empty($productStoreDataList))
			{
				$isReservationEnabled = Main\Config\Option::get("sale", "product_reserve_condition") != "S";
				$compileReserve = self::getCompileReserve($productData);
				foreach ($productStoreDataList as $storeId => $productStoreData)
				{
					$productId = $productStoreData['PRODUCT_ID'];

					if ($isNeedShip && (isset($needQuantityList[$storeId]) && $productStoreData['AMOUNT'] < $needQuantityList[$storeId]))
					{
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									'DDCT_DEDUCTION_QUANTITY_STORE_ERROR_2',
									array_merge(
										self::getProductCatalogInfo($productId),
										[
											'#STORE_NAME#' => \CCatalogStoreControlUtil::getStoreName($storeId),
											'#STORE_ID#' => $storeId,
											'#PRODUCT_ID#' => $productId,
										]
									)
								), 'DDCT_DEDUCTION_QUANTITY_STORE_ERROR'
							)
						);
					}
					else
					{
						$storeConfig = self::getUpdateStoreConfig(
							$storeId,
							$needQuantityList,
							$compileReserve,
							[
								'RESERVATION_ENABLED' => $isReservationEnabled,
							]
						);
						if (!$storeConfig['AMOUNT'] && !$storeConfig['QUANTITY_RESERVED'])
						{
							continue;
						}

						$storeUpdate = [];
						if ($storeConfig['AMOUNT'])
						{
							$setQuantity = $productQuantity;

							if (isset($needQuantityList[$storeId]))
							{
								$setQuantity = ($isNeedShip ? -1 : 1) * $needQuantityList[$storeId];
							}

							$storeUpdate['AMOUNT'] = $productStoreData['AMOUNT'] + $setQuantity;
							$storeUpdate['DELTA'] = $setQuantity;
							$storeUpdate['OLD_AMOUNT'] = $productStoreData['AMOUNT'];
							unset($setQuantity);
						}
						if ($storeConfig['QUANTITY_RESERVED'])
						{
							$setReserveQuantity = 0;
							if (isset($needQuantityList[$storeId]))
							{
								$setReserveQuantity = ($isNeedShip ? -1 : 1) * $needQuantityList[$storeId];
							}
							if (isset($quantityByStore['QUANTITY_RESERVED'][$storeId]))
							{
								$setReserveQuantity = ($isNeedShip ? -1 : 1) * $quantityByStore['QUANTITY_RESERVED'][$storeId];
							}
							if ($setReserveQuantity != 0)
							{
								$storeUpdate['QUANTITY_RESERVED'] = $productStoreData['QUANTITY_RESERVED']
									+ $setReserveQuantity;
								$storeUpdate['OLD_QUANTITY_RESERVED'] = $productStoreData['QUANTITY_RESERVED'];
								$storeUpdate['QUANTITY_RESERVED_DELTA'] = $setReserveQuantity;
							}
							unset($setReserveQuantity);
						}
						if (!empty($storeUpdate))
						{
							$setQuantityList[$productStoreData['ID']] = $storeUpdate;
						}
						unset($storeUpdate, $storeConfig);
					}
				}
			}

			if (!empty($setQuantityList))
			{
				$result->addData(
					array(
						Base::FLAT_QUANTITY_LIST => $setQuantityList,
					)
				);
			}

			return $result;
		}

		private static function getUpdateStoreConfig(int $storeId, array $quantityList, array $reserveList, array $config): array
		{
			$result = [
				'AMOUNT' => isset($quantityList[$storeId]),
				'QUANTITY_RESERVED' => false,
			];

			if ($config['RESERVATION_ENABLED'])
			{
				$result['QUANTITY_RESERVED'] = isset($reserveList[$storeId]);
			}

			return $result;
		}

		private static function getCompileReserve(array $product): array
		{
			if (empty($product['NEED_RESERVE_BY_STORE_LIST']) || !is_array($product['NEED_RESERVE_BY_STORE_LIST']))
			{
				return [];
			}

			$result = [];
			foreach ($product['NEED_RESERVE_BY_STORE_LIST'] as $shipment)
			{
				if (empty($shipment) || !is_array($shipment))
				{
					continue;
				}
				foreach ($shipment as $storeId => $flag)
				{
					if ($flag === true)
					{
						$result[$storeId] = true;
					}
				}
			}

			return $result;
		}

		private static function getQuantityDataFromStore(array $product): array
		{
			$result = [
				'AMOUNT' => [],
				'QUANTITY_RESERVED' => [],
			];

			$storeDataExists = (
				!empty($product['STORE_DATA_LIST'])
				&& is_array($product['STORE_DATA_LIST'])
			);
			$reserveDataExists = (
				!empty($product[self::AMOUNT_SRC_STORE_RESERVED_LIST])
				&& is_array($product[self::AMOUNT_SRC_STORE_RESERVED_LIST])
			);
			if (!$storeDataExists && !$reserveDataExists)
			{
				return $result;
			}

			$found = false;
			if ($storeDataExists)
			{
				foreach ($product['STORE_DATA_LIST'] as $storeList)
				{
					if (!is_array($storeList))
					{
						continue;
					}
					$found = true;
					foreach ($storeList as $storeId => $store)
					{
						if (!isset($result['AMOUNT'][$storeId]))
						{
							$result['AMOUNT'][$storeId] = 0;
						}
						$result['AMOUNT'][$storeId] += (float)$store['QUANTITY'];
						if (isset($store['RESERVED_QUANTITY']))
						{
							if (!isset($result['QUANTITY_RESERVED'][$storeId]))
							{
								$result['QUANTITY_RESERVED'][$storeId] = 0;
							}
							$result['QUANTITY_RESERVED'][$storeId] += (float)$store['RESERVED_QUANTITY'];
						}
					}
				}
			}

			if (!$found && $reserveDataExists)
			{
				switch (self::getQuantityFormat($product[self::AMOUNT_SRC_STORE_RESERVED_LIST]))
				{
					case self::QUANTITY_FORMAT_STORE:
						$internalResult = self::calculateQuantityFromStores($product[self::AMOUNT_SRC_STORE_RESERVED_LIST]);
						break;
					case self::QUANTITY_FORMAT_SHIPMENT:
						$internalResult = self::calculateQuantityFromShipments($product[self::AMOUNT_SRC_STORE_RESERVED_LIST]);
						break;
					default:
						$internalResult = null;
						break;
				}
				if ($internalResult !== null)
				{
					$result['QUANTITY_RESERVED'] = $internalResult;
				}
				unset($internalResult);
			}

			return $result;
		}

		/**
		 * @param array $productData
		 *
		 * @return Sale\Result
		 */
		private static function deleteBarcodes(array $productData): Sale\Result
		{
			$result = new Sale\Result();

			$storeData = $productData['STORE_DATA_LIST'];
			if (!empty($storeData))
			{
				foreach ($storeData as $storeDataList)
				{
					foreach($storeDataList as $storeDataValue)
					{
						$r = static::deleteBarcode($storeDataValue);
						if (!$r->isSuccess())
						{
							$result->addErrors($r->getErrors());
						}
					}
				}
			}

			return $result;
		}

		/**
		 * @param array $storeData
		 *
		 * @return Sale\Result
		 */
		private static function deleteBarcode(array $storeData): Sale\Result
		{
			$result = new Sale\Result();

			$storeId = $storeData["STORE_ID"];
			$productId = $storeData["PRODUCT_ID"];
			$barcodeMulti = $storeData['IS_BARCODE_MULTI'];

			$barcodeList = $storeData['BARCODE'];

			foreach ($barcodeList as $barcodeValue)
			{
				if (trim($barcodeValue) == "" || !$barcodeMulti)
				{
					continue;
				}

				$result = new Sale\Result();
				$barcodeFields = array(
					"STORE_ID" => $storeId,
					"BARCODE" => $barcodeValue,
					"PRODUCT_ID" => $productId,
				);

				$dbres = \CCatalogStoreBarcode::GetList(
					array(),
					$barcodeFields,
					false,
					false,
					array("ID", "STORE_ID", "BARCODE", "PRODUCT_ID")
				);

				$catalogStoreBarcodeRes = $dbres->Fetch();
				if ($catalogStoreBarcodeRes)
				{
					\CCatalogStoreBarcode::Delete($catalogStoreBarcodeRes["ID"]);
				}
				else
				{
					$result->addError(
						new Sale\ResultError(
							Main\Localization\Loc::getMessage(
								"DDCT_DEDUCTION_BARCODE_ERROR",
								array_merge(
									self::getProductCatalogInfo($productId),
									array("#BARCODE#" => $barcodeValue)
								)
							), "DDCT_DEDUCTION_BARCODE_ERROR"
						)
					);
				}
			}

			return $result;
		}

		/**
		 * @param array $productData
		 *
		 * @return Sale\Result
		 */
		private static function addBarcodes(array $productData): Sale\Result
		{
			$result = new Sale\Result();
			$storeData = $productData['STORE_DATA_LIST'];
			if (!empty($storeData))
			{
				foreach ($storeData as $storeDataList)
				{
					foreach($storeDataList as $storeDataValue)
					{
						$r = static::addBarcode($storeDataValue);
						if (!$r->isSuccess())
						{
							$result->addErrors($r->getErrors());
						}
					}
				}
			}

			return $result;
		}

		/**
		 * @param array $storeData
		 *
		 * @return Sale\Result
		 */
		private static function addBarcode(array $storeData): Sale\Result
		{
			$result = new Sale\Result();

			$storeId = $storeData["STORE_ID"];
			$productId = $storeData["PRODUCT_ID"];
			$barcodeMulti = $storeData['IS_BARCODE_MULTI'];

			$barcodeList = $storeData['BARCODE'];

			foreach ($barcodeList as $barcodeValue)
			{
				if (trim($barcodeValue) == "" || !$barcodeMulti)
				{
					continue;
				}

				$result = new Sale\Result();
				$barcodeFields = array(
					"STORE_ID" => $storeId,
					"BARCODE" => $barcodeValue,
					"PRODUCT_ID" => $productId,
				);
				\CCatalogStoreBarcode::Add($barcodeFields);
			}

			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function reserve(array $products)
		{
			$result = new Sale\Result();

			$resultList = array();

			$availableItems = $this->createProductsListWithCatalogData($products);
			foreach ($availableItems as $productId => $productData)
			{
				$resultList[$productId] = false;

				$r = static::reserveProduct($productData);
				if ($r->isSuccess())
				{
					$resultData = $r->getData();
					if (!empty($resultData))
					{
						$resultList[$productId] = $resultData;
					}
				}
				else
				{
					$result->addErrors($r->getErrors());
				}

			}

			$result->setData(array(
				'RESERVED_PRODUCTS_LIST' => $resultList,
			));

			return $result;
		}

		/**
		 * @param array $productData
		 *
		 * @return Sale\Result
		 */
		private static function reserveProduct(array $productData): Sale\Result
		{
			if (
				!static::isReservationEnabled()
				|| !$productData['PRODUCT']['USED_RESERVATION']
			)
			{
				return static::reserveQuantityWithDisabledReservation($productData);
			}

			return self::reserveStoreQuantityWithEnabledReservation($productData);

		}

		/**
		 * @param array $productData
		 *
		 * @return Sale\Result
		 */
		private static function reserveStoreQuantityWithEnabledReservation(array $productData): Sale\Result
		{
			$result = new Sale\Result();

			$enableStoreControl = Catalog\Config\State::isUsedInventoryManagement();

			$resultFields = [];
			$fields = []; // fields for update products
			$storeFields = []; // rows for update reserve in store
			$needShipList = [];

			$productId = $productData['PRODUCT_ID'];
			$storeProductQuantity = self::getStoreQuantityFromQuantityList($productData); // quantity with stores
			// empty store can't updated if used inventory managment
			if ($enableStoreControl && isset($storeProductQuantity[Base::EMPTY_STORE_ID]))
			{
				return $result;
			}

			$productQuantity = self::getTotalAmountFromQuantityList($productData);

			$isNeedReserve = ($productQuantity > 0);
			$catalogData = $productData['CATALOG'];

			$catalogReservedQuantity = (float)$catalogData['QUANTITY_RESERVED'];
			$catalogQuantity = self::getTotalAmountFromPriceList($catalogData);

			$sumCatalogQuantity = $catalogQuantity + $catalogReservedQuantity;

			if (isset($productData['NEED_SHIP']))
			{
				$needShipList = $productData['NEED_SHIP'];
			}

			$setQuantityReserved = $catalogReservedQuantity;

			if (!empty($needShipList) && !empty($productData['SHIPMENT_ITEM_DATA_LIST']))
			{
				$shipmentItemList = $productData['SHIPMENT_ITEM_DATA_LIST'];
				foreach ($needShipList as $shipmentItemIndex => $isNeedShip)
				{
					if ($setQuantityReserved <= 0)
					{
						$setQuantityReserved = 0;
						break;
					}

					if ($isNeedShip === true)
					{
						$shipmentItemQuantity = $shipmentItemList[$shipmentItemIndex];

						$setQuantityReserved -= $shipmentItemQuantity;
					}
				}
			}

			if ($catalogData["QUANTITY_TRACE"] == "N")
			{
				$fields["QUANTITY_RESERVED"] = $setQuantityReserved;
				$resultFields['IS_UPDATED'] = true;
				$resultFields['QUANTITY_RESERVED'] = 0;
			}
			else
			{
				$resultFields['QUANTITY_RESERVED'] = $catalogReservedQuantity;

				if (
					$productData['PRODUCT']['TYPE'] === Catalog\ProductTable::TYPE_PRODUCT
					|| $productData['PRODUCT']['TYPE'] === Catalog\ProductTable::TYPE_OFFER
				)
				{
					$storeFields = $enableStoreControl
						? self::loadCurrentStoreReserve($productId, $storeProductQuantity)
						: [];
				}

				if ($isNeedReserve)
				{
					if ($catalogData["CAN_BUY_ZERO"] == "Y")
					{
						$fields["QUANTITY_RESERVED"] = $catalogReservedQuantity + $productQuantity;
						$fields['QUANTITY'] = $catalogQuantity - $productQuantity;
					}
					else
					{
						if ($catalogQuantity >= $productQuantity)
						{
							$fields["QUANTITY"] = $catalogQuantity - $productQuantity;
							$fields["QUANTITY_RESERVED"] = $catalogReservedQuantity + $productQuantity;
						}
						else
						{
							$resultFields["QUANTITY_NOT_RESERVED"] = $productQuantity - $catalogQuantity;

							$fields["QUANTITY"] = 0;
							$fields["QUANTITY_RESERVED"] = $sumCatalogQuantity;

							$result->addWarning(
								new Sale\ResultWarning(
									Main\Localization\Loc::getMessage(
										"RSRV_QUANTITY_NOT_ENOUGH_ERROR",
										self::getProductCatalogInfo($productId)
									), "ERROR_NOT_ENOUGH_QUANTITY"
								)
							);
						}
					}
				}
				else //undo reservation
				{
					$correctReserve = 0;
					if ($enableStoreControl)
					{
						foreach (array_keys($storeFields) as $storeId)
						{
							if ($storeFields[$storeId]['ID'] === null)
							{
								continue;
							}
							$storeProductFields = $storeFields[$storeId];
							$newReserve = $storeProductFields['QUANTITY_RESERVED'] + $storeProductFields['ADD_QUANTITY_RESERVED'];
							if ($newReserve < 0)
							{
								$correctReserve -= $newReserve;
								$storeFields[$storeId]['ADD_QUANTITY_RESERVED'] -= $newReserve;
							}
						}
					}

					$needQuantity = abs($productQuantity) - $correctReserve;

					$fields["QUANTITY"] = $catalogQuantity + $needQuantity;

					$needReservedQuantity = $catalogReservedQuantity - $needQuantity;
					if ($needQuantity > $catalogReservedQuantity)
					{
						$needReservedQuantity = $catalogReservedQuantity;
					}

					$fields["QUANTITY_RESERVED"] = $needReservedQuantity;

					if ($enableStoreControl)
					{
						foreach (array_keys($storeFields) as $storeId)
						{
							if ($storeFields[$storeId]['ID'] === null)
							{
								unset($storeFields[$storeId]);
							}
						}
					}
				}

			} //quantity trace

			if (!$productData['PRODUCT']['USED_RESERVATION'])
			{
				if (isset($fields['QUANTITY_RESERVED']))
				{
					unset($fields['QUANTITY_RESERVED']);
				}
			}

			if (!empty($fields) && is_array($fields))
			{
				$storeSuccess = true;
				if ($enableStoreControl)
				{
					foreach (array_keys($storeFields) as $index)
					{
						if ($index === Base::EMPTY_STORE_ID)
						{
							$storeSuccess = false;
						}
						else
						{
							$storeProductFields = $storeFields[$index];
							$id = $storeProductFields['ID'];
							$storeProductFields['QUANTITY_RESERVED'] += $storeProductFields['ADD_QUANTITY_RESERVED'];
							unset($storeProductFields['ID'], $storeProductFields['ADD_QUANTITY_RESERVED']);
							if ($id === null)
							{
								$storeProductFields['AMOUNT'] = 0;
								$internalResult = Catalog\StoreProductTable::add($storeProductFields);
							}
							else
							{
								unset($storeProductFields['STORE_ID'], $storeProductFields['PRODUCT_ID']);
								$internalResult = Catalog\StoreProductTable::update($id, $storeProductFields);
							}
							if ($internalResult->isSuccess())
							{
								$storeFields[$index]['ID'] = (int)$internalResult->getId();
							}
							else
							{
								$storeFields[$index]['ERROR'] = true;
								$storeFields[$index]['ERROR_MESSAGES'] = $internalResult->getErrorMessages();
								$storeSuccess = false;
							}
						}
						if (!$storeSuccess)
						{
							break;
						}
					}
				}

				if (!$storeSuccess)
				{
					return $result;
				}

				$resultFields['IS_UPDATED'] = false;
				$internalResult = Catalog\Model\Product::update($productId, $fields);
				if ($internalResult->isSuccess())
				{
					$resultFields['IS_UPDATED'] = true;
					$quantityValues = array();
					if (isset($fields['QUANTITY']))
					{
						$quantityValues[QuantityControl::QUANTITY] = $fields['QUANTITY'];
						QuantityControl::resetAvailableQuantity($productId);
					}

					if (isset($fields['QUANTITY_RESERVED']))
					{
						$quantityValues[QuantityControl::RESERVED_QUANTITY] = $fields['QUANTITY_RESERVED'];
					}

					if (!empty($quantityValues))
					{
						QuantityControl::setValues($productId, $quantityValues);
					}
				}
				else
				{
					self::convertErrors($internalResult);
				}
				unset($internalResult);
			}

			if (isset($resultFields['IS_UPDATED']))
			{
				if (isset($fields['QUANTITY_RESERVED']))
				{
					$needReserved = $fields["QUANTITY_RESERVED"] - $resultFields['QUANTITY_RESERVED'];
					if ($resultFields['QUANTITY_RESERVED'] > $fields["QUANTITY_RESERVED"])
					{
						$needReserved = $fields["QUANTITY_RESERVED"];
					}
					$resultFields["QUANTITY_RESERVED"] = $needReserved;
				}

				if (!empty($resultFields))
				{
					$result->setData($resultFields);
				}
			}

			return $result;
		}

		/**
		 * @param array $productData
		 *
		 * @return Sale\Result
		 */
		private static function reserveQuantityWithDisabledReservation(array $productData): Sale\Result
		{
			$result = new Sale\Result();

			$catalogData = $productData['CATALOG'];

			$isQuantityTrace = $catalogData["QUANTITY_TRACE"] == 'Y';

			$productQuantity = self::getTotalAmountFromQuantityList($productData);
			$catalogQuantity = self::getTotalAmountFromPriceList($catalogData);

			$isUpdated = true;

			$fields = array(
				'QUANTITY' => $catalogQuantity,
			);

			if ($isQuantityTrace)
			{
				$productId = $productData['PRODUCT_ID'];
				$fields['QUANTITY'] -= $productQuantity;
				if ($catalogData["CAN_BUY_ZERO"] != "Y" && ($catalogQuantity < $productQuantity))
				{
					$result->addWarning(
						new Sale\ResultWarning(
							Main\Localization\Loc::getMessage(
								"RESERVE_QUANTITY_NOT_ENOUGH_ERROR",
								array_merge(
									self::getProductCatalogInfo($productId),
									array("#PRODUCT_ID#" => $productId)
								)
							), "RESERVE_QUANTITY_NOT_ENOUGH_ERROR"
						)
					);

					$fields['QUANTITY'] = 0;
				}

				$internalResult = Catalog\Model\Product::update($productId, $fields);
				if (!$internalResult->isSuccess())
				{
					$isUpdated = false;
					self::convertErrors($internalResult);
				}
				unset($internalResult);
			}

			if ($isUpdated)
			{
				$result->setData($fields);
			}

			return $result;
		}

		/**
		 * Checks offers parent products existence and activity.
		 *
		 * @param array $productIds
		 * @param int $iblockId
		 *
		 * @return array
		 */
		private static function checkParentActivity(array $productIds, int $iblockId = 0): array
		{
			$resultList = array();

			$productIdsToLoad = array();

			foreach ($productIds as $productId)
			{
				$cacheKey = $productId.'|'.$iblockId;

				if (static::isExistsHitCache(self::CACHE_PARENT_PRODUCT_ACTIVE, $cacheKey))
				{
					if (static::getHitCache(self::CACHE_PARENT_PRODUCT_ACTIVE, $cacheKey) === 'Y')
					{
						$resultList[] = $productId;
					}
				}
				else
				{
					$productIdsToLoad[] = $productId;
				}
			}

			if (!empty($productIdsToLoad))
			{
				$productToOfferMap = array();
				$parentIds = array();

				$cacheResult = array_fill_keys($productIdsToLoad, 'N');

				$productList = \CCatalogSku::getProductList($productIdsToLoad);
				if (!empty($productList))
				{
					foreach ($productList as $offerId => $productInfo)
					{
						$productToOfferMap[$productInfo['ID']][] = $offerId;
						$parentIds[] = $productInfo['ID'];
					}

					$itemList = \CIBlockElement::GetList(
						array(),
						array(
							'ID' => array_unique($parentIds),
							'IBLOCK_ID' => $iblockId,
							'ACTIVE' => 'Y',
							'ACTIVE_DATE' => 'Y',
							'CHECK_PERMISSIONS' => 'N',
						),
						false,
						false,
						array('ID')
					);
					while ($item = $itemList->Fetch())
					{
						if (!empty($productToOfferMap[$item['ID']]))
						{
							foreach ($productToOfferMap[$item['ID']] as $productId)
							{
								$cacheResult[$productId] = 'Y';
								$resultList[] = $productId;
							}
						}
					}
				}

				foreach ($cacheResult as $productId => $value)
				{
					static::setHitCache(self::CACHE_PARENT_PRODUCT_ACTIVE, $productId.'|'.$iblockId, $value);
				}
			}

			return $resultList;
		}

		/**
		 * @param $priceType
		 *
		 * @return string
		 */
		protected static function getPriceTitle($priceType)
		{
			$priceType = (int)$priceType;
			if ($priceType <= 0)
				return '';
			if (!isset(self::$priceTitleCache[$priceType]))
			{
				self::$priceTitleCache[$priceType] = '';
				$priceTypeList = Catalog\GroupTable::getTypeList();
				if (isset($priceTypeList[$priceType]))
				{
					$groupName = (string)$priceTypeList[$priceType]['NAME_LANG'];
					self::$priceTitleCache[$priceType] = ($groupName != '' ? $groupName : $priceTypeList[$priceType]['NAME']);
					unset($groupName);
				}
				unset($priceTypeList);
			}
			return self::$priceTitleCache[$priceType];
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function tryShip(array $products)
		{
			$result = new Sale\Result();
			$resultList = array();

			$filteredProducts = $this->createQuantityFilteredProducts($products);

			if (empty($filteredProducts))
			{
				$result->setData(
					array(
						'TRY_SHIP_PRODUCTS_LIST' => array_fill_keys(array_keys($products), true),
					)
				);

				return $result;
			}

			$availableItems = $this->createProductsListWithCatalogData($filteredProducts);
			if (empty($availableItems))
			{
				$productIdList = array_keys($products);
				foreach($productIdList as $productId)
				{
					$result->addError(
						new Sale\ResultError(
							Main\Localization\Loc::getMessage(
								"SALE_PROVIDER_PRODUCT_NOT_AVAILABLE",
								array_merge(
									self::getProductCatalogInfo($productId),
									array("#PRODUCT_ID#" => $productId)
								)
							), "SALE_PROVIDER_PRODUCT_NOT_AVAILABLE"
						)
					);
				}

				$result->setData(
					array(
						'TRY_SHIP_PRODUCTS_LIST' => array_fill_keys($productIdList, false),
					)
				);

				return $result;
			}
			else
			{
				foreach ($availableItems as $productId => $productData)
				{
					$messageId = null;
					if (
						isset($productData['PRODUCT']['TYPE'])
						&& $productData['PRODUCT']['TYPE'] === Catalog\ProductTable::TYPE_SERVICE
					)
					{
						if (
							(!isset($productData['CATALOG']['ACTIVE']) || $productData['CATALOG']['ACTIVE'] !== 'Y')
							|| (!isset($productData['PRODUCT']['AVAILABLE']) || $productData['PRODUCT']['AVAILABLE'] !== 'Y')
						)
						{
							$messageId = 'SALE_PROVIDER_PRODUCT_SERVICE_NOT_AVAILABLE';
						}
					}
					else
					{
						if (!isset($productData['CATALOG']['ACTIVE']) || $productData['CATALOG']['ACTIVE'] !== 'Y')
						{
							$messageId = 'SALE_PROVIDER_PRODUCT_NOT_AVAILABLE';
						}
					}
					if ($messageId !== null)
					{
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									$messageId,
									array_merge(
										self::getProductCatalogInfo($productId),
										array("#PRODUCT_ID#" => $productId)
									)
								), "SALE_PROVIDER_PRODUCT_NOT_AVAILABLE"
							)
						);

						$resultList[$productId] = false;
						unset($availableItems[$productId]);
					}
				}
			}

			if (!empty($availableItems))
			{
				if (Catalog\Config\State::isUsedInventoryManagement())
				{
					$r = $this->checkProductsInStore($availableItems);
					if ($r->isSuccess())
					{
						$data = $r->getData();
						if (!empty($data['PRODUCTS_LIST_IN_STORE']))
						{
							$resultList = $resultList + $data['PRODUCTS_LIST_IN_STORE'];
						}
					}
					else
					{
						$result->addErrors($r->getErrors());
					}
				}
				else
				{
					$r = $this->checkProductsQuantity($availableItems);
					if ($r->isSuccess())
					{
						$data = $r->getData();
						if (!empty($data['PRODUCTS_LIST_REQUIRED_QUANTITY']))
						{
							$resultList = $resultList + $data['PRODUCTS_LIST_REQUIRED_QUANTITY'];
						}
					}
					else
					{
						$result->addErrors($r->getErrors());
					}

				}
			}

			if (!empty($resultList))
			{
				$result->setData(
					array(
						'TRY_SHIP_PRODUCTS_LIST' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function isNeedShip(array $products)
		{
			$result = new Sale\Result();

			$result->setData(
				array(
					'IS_NEED_SHIP' => static::isReservationEnabled(),
				)
			);
			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return array
		 */
		private function createQuantityFilteredProducts(array $products): array
		{
			$resultList = array();
			foreach ($products as $productId => $productData)
			{
				$resultList[$productId] = $productData;
				if (array_key_exists('QUANTITY', $productData))
				{
					if ($productData['QUANTITY'] > 0)
					{
						unset($resultList[$productId]);
					}
					else
					{
						$resultList[$productId] *= -1;
					}
				}
				elseif (!empty($productData[Base::FLAT_QUANTITY_LIST]))
				{
					foreach ($productData[Base::FLAT_QUANTITY_LIST] as $basketCode => $quantity)
					{
						if ($quantity > 0)
						{
							unset($resultList[$productId][Base::FLAT_QUANTITY_LIST][$basketCode]);
						}
						else
						{
							$resultList[$productId][Base::FLAT_QUANTITY_LIST][$basketCode] *= -1;
						}
					}

					if (empty($resultList[$productId][Base::FLAT_QUANTITY_LIST]))
					{
						unset($resultList[$productId]);
					}
				}
			}

			return $resultList;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function tryUnship(array $products)
		{
			$result = new Sale\Result();
			$resultList = array();
			$availableItems = $this->createProductsListWithCatalogData($products);

			if (Catalog\Config\State::isUsedInventoryManagement())
			{
				$r = $this->checkProductsInStore($availableItems);
				if ($r->isSuccess())
				{
					$data = $r->getData();
					if (!empty($data['PRODUCTS_LIST_IN_STORE']))
					{
						$resultList = $data['PRODUCTS_LIST_IN_STORE'];
					}
				}
			}
			else
			{
				$r = $this->checkProductsQuantity($availableItems);
				if ($r->isSuccess())
				{
					$data = $r->getData();
					if (!empty($data['PRODUCTS_LIST_REQUIRED_QUANTITY']))
					{
						$resultList = $data['PRODUCTS_LIST_REQUIRED_QUANTITY'];
					}
				}
				else
				{
					return $r;
				}

			}

			if (!empty($resultList))
			{
				$result->setData(array(
					'PRODUCTS_LIST_SHIPPED' => $resultList,
				));
			}

			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function checkProductsInStore(array $products)
		{
			$result = new Sale\Result();
			$resultList = array();

			$r = $this->checkProductInStores($products);
			if (!$r->isSuccess())
			{
				return $r;
			}

			$storeProductMap = $this->createStoreProductMap($products);

			$r = $this->checkExistsProductsInStore($products, $storeProductMap);
			if ($r->isSuccess())
			{
				$data = $r->getData();
				if (!empty($data['PRODUCTS_LIST_EXISTS_IN_STORE']))
				{
					$resultList = $data['PRODUCTS_LIST_EXISTS_IN_STORE'];
				}
			}
			else
			{
				return $r;
			}

			$r = $this->checkProductQuantityInStore($products);
			if ($r->isSuccess())
			{
				$data = $r->getData();
				if (!empty($data['PRODUCTS_LIST_REQUIRED_QUANTITY_IN_STORE']))
				{
					$resultList = $data['PRODUCTS_LIST_REQUIRED_QUANTITY_IN_STORE'];
				}
			}
			else
			{
				$result->addErrors($r->getErrors());
			}

			if (!empty($resultList))
			{
				$result->addData(
					array(
						'PRODUCTS_LIST_IN_STORE' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		private function checkProductsQuantity(array $products): Sale\Result
		{
			$result = new Sale\Result();

			$resultList = array();
			$availableQuantityList = array();
			$r = $this->getAvailableQuantity($products);
			if ($r->isSuccess())
			{
				$resultData = $r->getData();
				if (!empty($resultData['AVAILABLE_QUANTITY_LIST']))
				{
					$availableQuantityList = $resultData['AVAILABLE_QUANTITY_LIST'];
				}

			}
			else
			{
				return $r;
			}

			$enabledReservation = static::isReservationEnabled();

			foreach ($products as $productId => $productData)
			{
				if (empty($productData['CATALOG']))
				{
					$resultList[$productId] = false;
					$result->addError(
						new Sale\ResultError(
							Main\Localization\Loc::getMessage(
								"SALE_PROVIDER_PRODUCT_NOT_AVAILABLE",
								array_merge(
									self::getProductCatalogInfo($productId),
									array("#PRODUCT_ID#" => $productId)
								)
							), "SALE_PROVIDER_PRODUCT_NOT_AVAILABLE"
						)
					);

					continue;
				}

				$resultList[$productId] = true;
				$catalogData = $productData['CATALOG'];


				if ($catalogData["CHECK_QUANTITY"])
				{
					$productQuantity = self::getTotalAmountFromQuantityList($productData);

					$availableQuantity = 0;

					if (isset($availableQuantityList[$productId]))
					{
						$availableQuantity = $availableQuantityList[$productId];
					}

					$availableQuantity += (float)$catalogData['QUANTITY_RESERVED'];

					if ($enabledReservation && $productQuantity > $availableQuantity)
					{
						$resultList[$productId] = false;
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									"DDCT_DEDUCTION_QUANTITY_ERROR",
									array_merge(
										self::getProductCatalogInfo($productId),
										array("#PRODUCT_ID#" => $productId)
									)
								), "DDCT_DEDUCTION_QUANTITY_ERROR"
							)
						);
					}
				}
			}

			if (!empty($resultList))
			{
				$result->setData(
					array(
						'PRODUCTS_LIST_REQUIRED_QUANTITY' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return array
		 */
		private function createProductsListWithCatalogData(array $products): array
		{
			$productDataList = array();
			$productIdList = array_fill_keys(array_keys($products), true);
			$r = $this->getData($products, [self::USE_GATALOG_DATA, 'FULL_QUANTITY']);
			if ($r->isSuccess())
			{
				$data = $r->getData();
				if (!empty($data[static::RESULT_CATALOG_LIST]))
				{
					$productDataList = $data[static::RESULT_CATALOG_LIST];
				}
			}

			$resultList = array();
			$availableListId = array_intersect_key($productIdList, $productDataList);
			if (!empty($availableListId))
			{
				foreach (array_keys($availableListId) as $productId)
				{
					if (empty($productDataList[$productId]) || !is_array($productDataList[$productId]))
					{
						continue;
					}
					$resultList[$productId] = $products[$productId];
					$resultList[$productId]['PRODUCT'] = $productDataList[$productId]['PRODUCT'];
					unset($productDataList[$productId]['PRODUCT']);
					$resultList[$productId]['CATALOG'] = $productDataList[$productId];
				}
			}

			return $resultList;
		}

		/**
		 * @param array $products
		 *
		 * @return array
		 */
		protected function createStoreProductMap(array $products)
		{
			$productStoreDataList = array();
			$r = $this->getProductListStores($products);
			if ($r->isSuccess())
			{
				$data = $r->getData();
				if (!empty($data['PRODUCT_STORES_LIST']))
				{
					$productStoreDataList = $data['PRODUCT_STORES_LIST'];
				}
			}

			$canAutoShipList = array();
			$r = $this->canProductListAutoShip($products);
			if ($r->isSuccess())
			{
				$data = $r->getData();
				if (!empty($data['PRODUCT_CAN_AUTOSHIP_LIST']))
				{
					$canAutoShipList = $data['PRODUCT_CAN_AUTOSHIP_LIST'];
				}
			}

			$storeProductList = array();
			foreach ($products as $productId => $productData)
			{
				if (!empty($productData['STORE_DATA_LIST']) && static::isExistsBarcode($productData['STORE_DATA_LIST']))
				{
					$storeProductList[$productId] = $productData['STORE_DATA_LIST'];
				}
				elseif (!empty($canAutoShipList[$productId]) && !empty($productStoreDataList[$productId]))
				{
					$productQuantity = self::getTotalAmountFromQuantityList($productData);
					foreach ($productData['SHIPMENT_ITEM_DATA_LIST'] as $shipmentItemIndex => $shipmentItemQuantity)
					{
						foreach ($productStoreDataList[$productId] as $productStoreData)
						{
							$storeId = $productStoreData['STORE_ID'];
							$storeProductList[$productId][$shipmentItemIndex][$storeId] = array(
								'PRODUCT_ID' => $productId,
								'STORE_ID' => $storeId,
								'IS_BARCODE_MULTI' => false,
								'QUANTITY' => abs($productQuantity),
							);
						}
					}
				}
			}

			return $storeProductList;
		}

		private function checkProductInStores($products): Sale\Result
		{
			$result = new Sale\Result();
			$productStoreDataList = array();
			$canAutoShipList = array();
			$r = $this->canProductListAutoShip($products);
			if ($r->isSuccess())
			{
				$data = $r->getData();
				if (!empty($data['PRODUCT_CAN_AUTOSHIP_LIST']))
				{
					$canAutoShipList = $data['PRODUCT_CAN_AUTOSHIP_LIST'];
				}
			}

			$r = $this->getProductListStores($products);
			if ($r->isSuccess())
			{
				$data = $r->getData();
				if (!empty($data['PRODUCT_STORES_LIST']))
				{
					$productStoreDataList = $data['PRODUCT_STORES_LIST'];
				}
			}

			foreach ($products as $productId => $productData)
			{
				if (!empty($productData['STORE_DATA_LIST']))
				{
					if (!static::isExistsBarcode($productData['STORE_DATA_LIST']))
					{
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									"DDCT_DEDUCTION_MULTI_BARCODE_EMPTY", self::getProductCatalogInfo($productId)
								), "DDCT_DEDUCTION_MULTI_BARCODE_EMPTY"
							)
						);
					}
				}
				elseif ($canAutoShipList[$productId] === false)
				{
					if (!isset($productStoreDataList[$productId]))
					{
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									"DDCT_DEDUCTION_STORE_EMPTY_ERROR",
									self::getProductCatalogInfo($productId)
								), "DEDUCTION_STORE_ERROR1"
							)
						);
					}
					elseif (count($productStoreDataList[$productId]) > 1)
					{
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									"DDCT_DEDUCTION_STORE_ERROR",
									self::getProductCatalogInfo($productId)
								), "DEDUCTION_STORE_ERROR1"
							)
						);
					}
				}
			}

			return $result;

		}

		private static function isExistsBarcode(array $list): bool
		{
			$resultValue = false;
			foreach ($list as $storeDataList)
			{
				foreach ($storeDataList as $storeValue)
				{
					if (is_array($storeValue['BARCODE']) && $storeValue['IS_BARCODE_MULTI'] === true)
					{
						foreach ($storeValue["BARCODE"] as $barcodeValue)
						{
							if (trim($barcodeValue) == "")
							{
								return $resultValue;
							}
						}

						$resultValue = true;

					}
					else
					{
						return (!empty($storeValue['BARCODE']));
					}

				}
			}

			return $resultValue;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		protected function checkProductQuantityInStore(array $products)
		{
			$result = new Sale\Result();

			$resultList = array();
			$productQuantityList = array();
			$usedProductStoreQuantity = [];

			$productStoreDataList = self::loadCurrentProductStores(array_keys($products));

			foreach ($products as $productId => $productData)
			{
				if (empty($productData['CATALOG']))
					continue;
				if (
					isset($productData['PRODUCT'])
					&& !$productData['PRODUCT']['USED_STORE_INVENTORY'] //product types without stores
				)
				{
					continue;
				}

				$productStoreData = $productStoreDataList[$productId] ?? [];

				$storeDataList = $productData['STORE_DATA_LIST'];

				if (!empty($storeDataList))
				{
					foreach ($storeDataList as $barcodeList)
					{
						foreach($barcodeList as $storeId => $storeDataValue)
						{
							if (!empty($storeDataValue))
							{
								if (
									!isset($productStoreData[$storeId])
									|| ($productStoreData[$storeId]["AMOUNT"] < $storeDataValue["QUANTITY"])
								)
								{
									$result->addError(
										new Sale\ResultError(
											Main\Localization\Loc::getMessage(
												'DDCT_DEDUCTION_QUANTITY_STORE_ERROR_2',
												array_merge(
													self::getProductCatalogInfo($productId),
													[
														'#STORE_NAME#' => \CCatalogStoreControlUtil::getStoreName($storeId),
														'#STORE_ID#' => $storeId,
													]
												)
											), 'DDCT_DEDUCTION_QUANTITY_STORE_ERROR'
										)
									);
								}
								else
								{
									if (!isset($productQuantityList[$productId]))
									{
										$productQuantityList[$productId] = 0;
									}

									$productQuantityList[$productId] += $storeDataValue["QUANTITY"];

									if (!isset($usedProductStoreQuantity[$productId]))
									{
										$usedProductStoreQuantity[$productId] = [];
									}
									$usedProductStoreQuantity[$productId][$storeId] = true;

									$r = static::checkProductBarcodes($productData, $productStoreData[$storeId], $storeDataValue);
									if ($r->isSuccess())
									{
										if (!array_key_exists($productId, $resultList))
										{
											$resultList[$productId] = true;
										}
									}
									else
									{
										$result->addErrors($r->getErrors());
									}
								}
							}
							else
							{
								if (!array_key_exists($productId, $resultList))
								{
									$resultList[$productId] = true;
								}
							}
						}
					}
				}
				else
				{
					$resultList[$productId] = true;

					if (!isset($productQuantityList[$productId]))
					{
						$productQuantityList[$productId] = 0;
					}

					if (
						!empty($productData[Base::FLAT_QUANTITY_LIST])
						&& is_array($productData[Base::FLAT_QUANTITY_LIST])
					)
					{
						$productQuantityList[$productId] = array_sum(
							$productData[Base::FLAT_QUANTITY_LIST]
						);
					}

				}
			}

			if (!empty($productQuantityList))
			{
				foreach ($productQuantityList as $amountProductId => $amountValue)
				{
					$product = $products[$amountProductId];
					$catalogData = $product['CATALOG'];

					$catalogQuantity = self::getTotalAmountFromPriceList($catalogData);
					$catalogReservedQuantity = (float)$catalogData['QUANTITY_RESERVED'];

					if ($product[Base::FLAT_RESERVED_QUANTITY_LIST][$product['BASKET_CODE']] > 0)
					{
						$catalogQuantity += $catalogReservedQuantity;
					}
					else
					{
						$unusedReserve = 0.0;
						if (isset($usedProductStoreQuantity[$amountProductId]))
						{
							$usedProductStores = $usedProductStoreQuantity[$amountProductId];
							$productStores = $productStoreDataList[$amountProductId];
							foreach (array_keys($productStores) as $storeId)
							{
								if (isset($usedProductStores[$storeId]))
								{
									continue;
								}
								$unusedReserve += $productStores[$storeId]['QUANTITY_RESERVED'];
							}
							unset($storeId);
							unset($productStores, $usedProductStores);
						}
						$catalogQuantity += $unusedReserve;
						unset($unusedReserve);
					}

					if ($amountValue > $catalogQuantity)
					{
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									"DDCT_DEDUCTION_SHIPMENT_QUANTITY_NOT_ENOUGH",
									self::getProductCatalogInfo($amountProductId)
								), "SALE_PROVIDER_SHIPMENT_QUANTITY_NOT_ENOUGH"
							)
						);
					}
				}
			}

			if (!empty($resultList))
			{
				$result->setData(
					array(
						'PRODUCTS_LIST_REQUIRED_QUANTITY_IN_STORE' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array
		 * @param array $storeDataList
		 *
		 * @return Sale\Result
		 */
		protected function checkExistsProductItemInStore(array $productData, array $storeDataList = array())
		{
			$result = new Sale\Result();

			if (!empty($storeDataList))
			{
				foreach ($storeDataList as $storeData)
				{
					foreach ($storeData as $storeDataValue)
					{
						$storeId = $storeDataValue['STORE_ID'];

						if ((int)$storeId < -1 || (int)$storeId == 0
							|| !isset($storeDataValue["QUANTITY"]) || (int)$storeDataValue["QUANTITY"] < 0)
						{
							$result->addError(
								new Sale\ResultError(
									Main\Localization\Loc::getMessage(
										"DDCT_DEDUCTION_STORE_ERROR",
										self::getProductCatalogInfo($productData['PRODUCT_ID'])
									), "DDCT_DEDUCTION_STORE_ERROR"
								)
							);
							return $result;
						}
					}
				}
			}
			else
			{
				$result->addError( new Sale\ResultError(
					Main\Localization\Loc::getMessage("DDCT_DEDUCTION_STORE_ERROR", self::getProductCatalogInfo($productData['PRODUCT_ID'])),
					"DEDUCTION_STORE_ERROR1"
				));
			}

			return $result;
		}

		/**
		 * @param array $products
		 * @param array $storeData
		 *
		 * @return Sale\Result
		 */
		protected function checkExistsProductsInStore(array $products, array $storeData = array())
		{
			$result = new Sale\Result();

			$resultList = array();
			if (!empty($storeData))
			{
				foreach ($products as $productId => $productData)
				{
					$productStoreData = array();
					if (!empty($storeData[$productId]))
					{
						$productStoreData = $storeData[$productId];
					}

					$resultList[$productId] = true;

					if (
						(
							isset($productData['BUNDLE_PARENT'])
							&& $productData['BUNDLE_PARENT'] === true
						)
						|| (
							isset($productData['PRODUCT']['USED_STORE_INVENTORY'])
							&& !$productData['PRODUCT']['USED_STORE_INVENTORY']
						)
					)
					{
						continue;
					}

					$r = $this->checkExistsProductItemInStore($productData, $productStoreData);
					if (!$r->isSuccess())
					{
						$result->addErrors($r->getErrors());
						$resultList[$productId] = false;
					}
				}
			}

			if (!empty($resultList))
			{
				$result->setData(
					array(
						'PRODUCTS_LIST_EXISTS_IN_STORE' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array $productData
		 * @param array $productStoreData
		 * @param array $storeData
		 *
		 * @return Sale\Result
		 */
		protected static function checkProductBarcodes(array $productData, array $productStoreData, array $storeData = array())
		{
			$result = new Sale\Result();

			$productId = $productData['PRODUCT_ID'];
			$storeId = $productStoreData['STORE_ID'];

			if (isset($storeData['BARCODE']) && count($storeData['BARCODE']) > 0)
			{
				foreach ($storeData['BARCODE'] as $barcodeValue)
				{
					if (trim($barcodeValue) == "" && $storeData['IS_BARCODE_MULTI'] === true)
					{
						$result->addError(
							new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									"DDCT_DEDUCTION_MULTI_BARCODE_EMPTY",
									array_merge(
										self::getProductCatalogInfo($productId),
										array("#STORE_ID#" => $storeId)
									)
								), "DDCT_DEDUCTION_MULTI_BARCODE_EMPTY"
							)
						);
						continue;
					}
					if (!empty($barcodeValue))
					{
						$fields = [
							'=STORE_ID' => static::CATALOG_PROVIDER_EMPTY_STORE_ID,
							'=BARCODE' => $barcodeValue,
							'=PRODUCT_ID' => $productId,
						];

						if ($storeData['IS_BARCODE_MULTI'] === true)
						{
							$fields['=STORE_ID'] = $storeId;
						}
						$iterator = Catalog\StoreBarcodeTable::getList([
							'select' => ['ID'],
							'filter' => $fields,
							'limit' => 1,
						]);
						$row = $iterator->fetch();
						unset($iterator);

						if (empty($row))
						{
							$result->addError( new Sale\ResultError(
								Main\Localization\Loc::getMessage(
									"DDCT_DEDUCTION_BARCODE_ERROR",
									array_merge(self::getProductCatalogInfo($productId), array("#BARCODE#" => $barcodeValue))
								),
								"DDCT_DEDUCTION_BARCODE_ERROR"
							));
						}
					}
				}
			}
			else
			{
				$result->addError( new Sale\ResultError(
					Main\Localization\Loc::getMessage(
						"DDCT_DEDUCTION_MULTI_BARCODE_EMPTY",
						array_merge(self::getProductCatalogInfo($productId), array("#STORE_ID#" => $storeId))
					),
					"DDCT_DEDUCTION_MULTI_BARCODE_EMPTY"
				));
			}
			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 * @throws Main\ArgumentNullException
		 */
		private function canProductListAutoShip(array $products): Sale\Result
		{
			$context = $this->getContext();
			if (empty($context['SITE_ID']))
			{
				throw new Main\ArgumentNullException('SITE_ID');
			}

			static $canAutoList = array();

			$resultList = array();
			$hasNew = false;

			$countStores = 0;

			$countStoresResult = $this->getStoresCount();
			if ($countStoresResult->isSuccess())
			{
				$countStores = $countStoresResult->get('STORES_COUNT');
			}

			$countStoresResult->getData();
			$defaultDeductionStore = (int)Main\Config\Option::get("sale", "deduct_store_id", "", $context['SITE_ID']);
			$isDefaultStore = ($defaultDeductionStore > 0);
			foreach ($products as $productId => $productData)
			{
				if (isset($canAutoList[$productId]))
				{
					$resultList[$productId] = $canAutoList[$productId];
					continue;
				}

				if (!$productData['PRODUCT']['USED_STORE_INVENTORY']) // product types without stores
				{
					$canAutoList[$productId] = true;
					$resultList[$productId] = true;
					continue;
				}

				$isOneStore = ($countStores == 1 || $countStores == -1);

				$isOnlyOneStore = ($isOneStore || $isDefaultStore);
				$isMulti = false;
				if (isset($productData['STORE_DATA_LIST']))
				{
					$storeData = array();
					$shipmentItemStoreData = reset($productData['STORE_DATA_LIST']);
					if (!empty($shipmentItemStoreData))
					{
						$storeData = reset($shipmentItemStoreData);
					}

					if (!empty($storeData))
					{
						$isMulti = isset($storeData['IS_BARCODE_MULTI']) && $storeData['IS_BARCODE_MULTI'] === true;
					}
				}
				elseif (isset($productData['IS_BARCODE_MULTI']))
				{
					$isMulti = $productData['IS_BARCODE_MULTI'] === true;
				}

				$resultList[$productId] = ($isOnlyOneStore && !$isMulti);
				$hasNew = true;

				if ($isMulti)
				{
					$hasNew = false;
				}
			}

			if ($hasNew)
			{
				$productStoreList = [];
				$r = $this->getProductListStores($products);
				if ($r->isSuccess())
				{
					$productStoreData = $r->getData();
					if (
						!empty($productStoreData['PRODUCT_STORES_LIST'])
						&& is_array($productStoreData['PRODUCT_STORES_LIST'])
					)
					{
						$productStoreList = $productStoreData['PRODUCT_STORES_LIST'];
					}
				}

				if (!empty($productStoreList))
				{
					foreach ($products as $productId => $productData)
					{
						if (!empty($productStoreList[$productId]))
						{
							$countProductInStore = 0;
							foreach ($productStoreList[$productId] as $storeData)
							{
								if ((float)$storeData['AMOUNT'] > 0)
								{
									$countProductInStore++;
								}
							}
							$resultList[$productId] = ($countProductInStore == 1);
							$canAutoList[$productId] = $resultList[$productId];
						}
					}
				}
			}

			$result = new Sale\Result();
			if (!empty($resultList))
			{
				$result->setData(
					array(
						'PRODUCT_CAN_AUTOSHIP_LIST' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @param array $product
		 * @param array $productStoreDataList
		 *
		 * @return bool|array
		 */
		private static function getAutoShipStoreData(array $product, array $productStoreDataList)
		{
			$isMulti = false;
			if (isset($product['STORE_DATA_LIST']))
			{
				$storeData = [];
				$shipmentItemStoreData = reset($product['STORE_DATA_LIST']);
				if (!empty($shipmentItemStoreData))
				{
					$storeData = reset($shipmentItemStoreData);
				}

				if (!empty($storeData))
				{
					$isMulti = isset($storeData['IS_BARCODE_MULTI']) && $storeData['IS_BARCODE_MULTI'] === true;
				}
			}
			elseif (isset($product['IS_BARCODE_MULTI']))
			{
				$isMulti = $product['IS_BARCODE_MULTI'] === true;
			}

			if ($isMulti)
			{
				return false;
			}

			$outputStoreData = false;

			if (!empty($productStoreDataList))
			{
				$countProductInStore = 0;

				$storeProductData = false;
				foreach ($productStoreDataList as $storeData)
				{
					if ((float)$storeData['AMOUNT'] > 0)
					{
						$countProductInStore++;
						if (!$storeProductData)
						{
							$storeProductData = $storeData;
						}
					}
				}

				if ($countProductInStore == 1 && !empty($storeProductData))
				{
					$outputStoreData = $storeProductData;
				}
			}

			return $outputStoreData;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		protected function getCountProductsInStore(array $products)
		{
			$result = new Sale\Result();

			$productStoreList = array();
			$productStoreResult = $this->getProductListStores($products);
			if ($productStoreResult->isSuccess())
			{
				$productStoreData = $productStoreResult->getData();

				if (array_key_exists('PRODUCT_STORES_LIST', $productStoreData))
				{
					$productStoreList = $productStoreData['PRODUCT_STORES_LIST'];
				}
			}

			if (empty($productStoreList))
			{
				return $result;
			}

			$resultList = array();
			foreach ($productStoreList as $productStoreDataList)
			{
				foreach ($productStoreDataList as $storeId =>$productStoreData)
				{
					$productId = $productStoreData['PRODUCT_ID'];
					if ($productStoreData['AMOUNT'] > 0)
					{
						if (!isset($resultList[$productId]))
						{
							$resultList[$productId] = [];
						}

						$resultList[$productId][$storeId] = $productStoreData['AMOUNT'];
					}
				}
			}

			if (!empty($resultList))
			{
				$result->setData(
					array(
						'RESULT_LIST' => $resultList,
					)
				);
			}

			return $result;
		}

		/**
		 * @internal
		 * @return Sale\Result
		 */
		public function getStoresCount()
		{
			$result = new Sale\Result();

			$count = -1;

			if (Catalog\Config\State::isUsedInventoryManagement())
			{
				$count = count($this->getStoreIds());
			}

			$result->setData(
				array(
					'STORES_COUNT' => $count,
				)
			);

			return $result;
		}

		/**
		 * @return array
		 */
		private function getStoreIds(): array
		{
			$context = $this->getContext();

			$filterId = [
				'ACTIVE' => 'Y',
			];
			if (isset($context['SITE_ID']) && $context['SITE_ID'] !== '')
			{
				$filterId['+SITE_ID'] = $context['SITE_ID'];
			}

			$cacheId = md5(serialize($filterId));
			$storeIds = static::getHitCache(self::CACHE_STORE, $cacheId);
			if (empty($storeIds))
			{
				$storeIds = [];

				$filter = Main\Entity\Query::filter();
				$filter->where('ACTIVE', '=', 'Y');
				if (isset($context['SITE_ID']) && $context['SITE_ID'] != '')
				{
					$subFilter = Main\Entity\Query::filter();
					$subFilter->logic('or')->where('SITE_ID', '=', $context['SITE_ID'])->where('SITE_ID', '=', '')->whereNull('SITE_ID');
					$filter->where($subFilter);
					unset($subFilter);
				}

				$iterator = Catalog\StoreTable::getList([
					'select' => ['ID', 'SORT'],
					'filter' => $filter,
					'order' => ['SORT' => 'ASC', 'ID' => 'ASC'],
				]);
				while ($row = $iterator->fetch())
				{
					$storeIds[] = (int)$row['ID'];
				}
				unset($row, $iterator, $filter);
				if (!empty($storeIds))
				{
					static::setHitCache(self::CACHE_STORE, $cacheId, $storeIds);
				}
			}
			unset($cacheId, $filterId);

			return $storeIds;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function getProductListStores(array $products)
		{
			$result = new Sale\Result();

			//without store control stores are used for information purposes only
			if (!Catalog\Config\State::isUsedInventoryManagement())
			{
				return $result;
			}

			$storeIds = $this->getStoreIds();
			if (empty($storeIds))
			{
				return $result;
			}

			$resultList = [];
			$productGetIdList = [];
			foreach (array_keys($products) as $productId)
			{
/*				$cacheId = md5($productId);

				$storeProductDataList = static::getHitCache(self::CACHE_STORE_PRODUCT, $cacheId);
				if (!empty($storeProductDataList))
				{
					$resultList[$productId] = $storeProductDataList;
				}
				else
				{
					$productGetIdList[$productId] = $productId;
				} */
				// remove cache because need clear cache after modify stores
				if (
					isset($products[$productId]['PRODUCT']['USED_STORE_INVENTORY'])
					&& !$products[$productId]['PRODUCT']['USED_STORE_INVENTORY']
				) // product types without stores
				{
					continue;
				}
				$productGetIdList[$productId] = $productId;
			}

			if (!empty($productGetIdList))
			{
				$emptyProductStores = [];
				$iterator = Catalog\StoreTable::getList([
					'select' => [
						'ID',
						'TITLE',
					],
					'filter' => [
						'@ID' => $storeIds,
					],
					'order' => [
						'ID' => 'ASC',
					],
				]);
				while ($row = $iterator->fetch())
				{
					$id = (int)$row['ID'];
					$emptyProductStores[$id] = [
						'ID' => 0,
						'PRODUCT_ID' => 0,
						'STORE_ID' => $row['ID'],
						'AMOUNT' => 0,
						'QUANTITY_RESERVED' => 0,
						'STORE_NAME' => $row['TITLE'],
					];
				}
				unset($row, $iterator);

				foreach (array_chunk($productGetIdList, 500) as $pageIds)
				{
					foreach ($pageIds as $productId)
					{
						$rows = $emptyProductStores;
						foreach (array_keys($rows) as $storeId)
						{
							$rows[$storeId]['PRODUCT_ID'] = $productId;
						}
						$resultList[$productId] = $rows;
						unset($rows);
					}
					unset($productId);
					$iterator = Catalog\StoreProductTable::getList([
						'select' => [
							'ID',
							'PRODUCT_ID',
							'STORE_ID',
							'AMOUNT',
							'QUANTITY_RESERVED',
						],
						'filter' => [
							'=PRODUCT_ID' => $pageIds,
							'@STORE_ID' => $storeIds,
						],
						'order' => [
							'PRODUCT_ID' => 'ASC',
							'STORE_ID' => 'ASC',
						],
					]);
					while ($row = $iterator->fetch())
					{
						$row['ID'] = (int)$row['ID'];
						$row['PRODUCT_ID'] = (int)$row['PRODUCT_ID'];
						$row['STORE_ID'] = (int)$row['STORE_ID'];
						if (!isset($resultList[$row['PRODUCT_ID']]))
						{
							$resultList[$row['PRODUCT_ID']] = [];
						}
						$resultList[$row['PRODUCT_ID']][$row['STORE_ID']]['ID'] = $row['ID'];
						$resultList[$row['PRODUCT_ID']][$row['STORE_ID']]['AMOUNT'] = (float)$row['AMOUNT'];
						$resultList[$row['PRODUCT_ID']][$row['STORE_ID']]['QUANTITY_RESERVED'] = (float)$row['QUANTITY_RESERVED'];
					}
					unset($iterator, $row);
				}
				unset($pageIds);

/*				foreach ($productGetIdList as $productId)
				{
					if (!empty($resultList[$productId]))
					{
						$cacheId = md5($productId);
						static::setHitCache(self::CACHE_STORE_PRODUCT, $cacheId, $resultList[$productId]);
					}
				} */
			}

			if (!empty($resultList))
			{
				$result->setData([
					'PRODUCT_STORES_LIST' => $resultList,
				]);
			}

			return $result;
		}

		/**
		 * @param $type
		 * @param $key
		 * @param array $fields
		 *
		 * @return bool|mixed
		 */
		protected static function getHitCache($type, $key, array $fields = array())
		{
			if (!empty(self::$hitCache[$type]) && !empty(self::$hitCache[$type][$key]))
			{
				if (static::isExistsHitCache($type, $key, $fields))
				{
					return self::$hitCache[$type][$key];
				}
			}

			return false;
		}

		/**
		 * @param $type
		 * @param $key
		 * @param array $fields
		 *
		 * @return bool
		 */
		protected static function isExistsHitCache($type, $key, array $fields = []): bool
		{
			$isExists = false;
			if (!empty(self::$hitCache[$type]) && !empty(self::$hitCache[$type][$key]))
			{
				$isExists = true;
				if (!empty($fields) && is_array($fields) && is_array(self::$hitCache[$type][$key]))
				{
					foreach ($fields as $name)
					{
						if (!array_key_exists($name, self::$hitCache[$type][$key]))
						{
							$isExists = false;
							break;
						}
					}
				}
			}

			return $isExists;
		}

		/**
		 * @param string $type
		 * @param string|int $key
		 * @param mixed $value
		 */
		protected static function setHitCache(string $type, $key, $value): void
		{
			if (!isset(self::$hitCache[$type]))
			{
				self::$hitCache[$type] = [];
			}

			if (!isset(self::$hitCache[$type][$key]))
			{
				self::$hitCache[$type][$key] = [];
			}

			self::$hitCache[$type][$key] = $value;
		}

		/**
		 * @param string|null $type
		 */
		protected static function clearHitCache(?string $type = null): void
		{
			if ($type === null)
			{
				self::$hitCache = [];
			}
			elseif (isset(self::$hitCache[$type]))
			{
				unset(self::$hitCache[$type]);
			}
		}

		/**
		 * @param $fields
		 *
		 * @return array
		 */
		protected static function clearNotCacheFields($fields)
		{
			$resultFields = array();
			$clearFields = static::getNotCacheFields();
			foreach ($fields as $name => $value)
			{
				$clearName = $name;
				if (mb_substr($clearName, 0, 1) == '~')
				{
					$clearName = mb_substr($clearName, 1, mb_strlen($clearName));
				}

				if (!in_array($clearName, $clearFields))
				{
					$resultFields[$name] = $value;
				}
			}

			return $resultFields;
		}

		/**
		 * @return array
		 */
		protected static function getNotCacheFields()
		{
			return array(
				'CAN_BUY_ZERO',
				'QUANTITY_TRACE',
				'QUANTITY',
				'CAN_BUY',
			);
		}

		protected static function checkNeedFields(array $fields, array $need)
		{
			foreach ($need as $name => $value)
			{
				if (!array_key_exists($name, $fields))
				{
					return false;
				}
			}

			return true;
		}

		/**
		 * @deprecated deprecated since 21.700.0
		 *
		 * @param $currentQuantity
		 * @param $newQuantity
		 * @param $quantityTrace
		 * @param $canBuyZero
		 * @param float|int $ratio
		 * @return bool
		 * @noinspection PhpUnusedParameterInspection
		 */
		protected static function isNeedClearPublicCache($currentQuantity, $newQuantity, $quantityTrace, $canBuyZero, $ratio = 1): bool
		{
			return false;
		}

		/**
		 * @deprecated deprecated since 21.700.0
		 *
		 * @param $productID
		 * @param array|false $productInfo
		 * @return void
		 */
		protected static function clearPublicCache($productID, $productInfo = array()): void {}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function getAvailableQuantity(array $products)
		{
			$result = $this->getAvailableQuantityByStore($products);
			if (!$result->isSuccess())
			{
				return $result;
			}
			$data = $result->getData();
			if (empty($data[Base::STORE_AVAILABLE_QUANTITY_LIST]))
			{
				return $result;
			}

			$reservedList = $data[Base::STORE_AVAILABLE_QUANTITY_LIST];
			$resultList = [];
			foreach ($reservedList as $productId => $rows)
			{
				$resultList[$productId] = reset($rows);
			}
			unset($productId, $rows);
			unset($reservedList, $data);

			$result->setData([
				Base::FLAT_AVAILABLE_QUANTITY_LIST => $resultList,
			]);

			return $result;
		}

		public function getAvailableQuantityByStore(array $products): Sale\Result
		{
			$result = new Sale\Result();
			$resultList = [];

			$isGotQuantityDataList = [];

			foreach ($products as $productId => $productData)
			{
				$catalogAvailableQuantity = QuantityControl::getAvailableQuantity($productId);
				$catalogQuantity = QuantityControl::getQuantity($productId);

				if ($catalogQuantity === null || $catalogAvailableQuantity === null)
				{
					continue;
				}

				$productQuantity = self::getStoreAmountFromQuantityList($productData);
				if ($productQuantity === null)
				{
					continue;
				}
				if ($catalogAvailableQuantity < array_sum($productQuantity))
				{
					continue;
				}

				$isGotQuantityDataList[$productId] = true;

				$resultList[$productId] = $productQuantity;
			}

			if (count($resultList) != count($products))
			{
				if ($this->isExistsCatalogData($products))
				{
					$items = $products;
				}
				else
				{
					$items = $this->createProductsListWithCatalogData($products);
				}

				foreach ($items as $productId => $productData)
				{
					if (isset($isGotQuantityDataList[$productId]))
					{
						continue;
					}

					if (empty($productData['CATALOG']) || !is_array($productData['CATALOG']))
					{
						continue;
					}

					$catalogData = $productData['CATALOG'];

					$productQuantity = self::getStoreAmountFromQuantityList($productData);
					if ($productQuantity === null)
					{
						$resultList[$productId] = [];
						continue;
					}
					$resultList[$productId] = $productQuantity;

					$catalogQuantity = self::getTotalAmountFromPriceList($catalogData, false);

					QuantityControl::setQuantity($productId, $catalogQuantity);

					if ($catalogData['CHECK_QUANTITY'])
					{
						$totalReservedQuantity = 0;
						$reservedQuantity = self::getStoreReservedQuantityFromProduct($productData);
						if ($reservedQuantity !== null)
						{
							$totalReservedQuantity = array_sum($reservedQuantity);
						}

						$needQuantity = (array_sum($productQuantity) - $totalReservedQuantity);
						if ($catalogQuantity < $needQuantity)
						{
							$limitQuantity = $catalogQuantity;
							$availableList = $resultList[$productId];
							arsort($availableList, SORT_NUMERIC);
							foreach (array_keys($availableList) as $storeId)
							{
								$storeQuantity = $resultList[$productId][$storeId];
								if ($limitQuantity > $storeQuantity)
								{
									$limitQuantity -= $storeQuantity;
								}
								else
								{
									$storeQuantity = $limitQuantity;
									if ($limitQuantity > 0)
									{
										$limitQuantity = 0;
									}
								}
								$resultList[$productId][$storeId] = $storeQuantity;
							}
						}
					}
				}
			}

			if (!empty($resultList))
			{
				$result->setData([
					Base::STORE_AVAILABLE_QUANTITY_LIST => $resultList,
				]);
			}

			return $result;
		}

		/**
		 * @param array $products
		 *
		 * @return Sale\Result
		 */
		public function getAvailableQuantityAndPrice(array $products)
		{
			$result = new Sale\Result();
			$availableQuantityListResult = $this->getAvailableQuantity($products);

			if ($this->isExistsCatalogData($products))
			{
				$items = $products;
			}
			else
			{
				$items = $this->createProductsListWithCatalogData($products);
			}

			$priceDataList = array();

			foreach ($items as $productId => $productData)
			{
				$catalogData = $productData['CATALOG'];

				if (!empty($catalogData['PRICE_LIST']))
				{
					$priceDataList[$productId] = $catalogData['PRICE_LIST'];
				}

			}

			$availableQuantityData = array();

			if ($availableQuantityListResult->isSuccess())
			{
				$availableQuantityList = $availableQuantityListResult->getData();

				if (isset($availableQuantityList[Base::FLAT_AVAILABLE_QUANTITY_LIST]))
				{
					$availableQuantityData = $availableQuantityList[Base::FLAT_AVAILABLE_QUANTITY_LIST];
				}
			}

			$result->setData([
				Base::SUMMMARY_PRODUCT_LIST => [
					Base::FLAT_PRICE_LIST => $priceDataList,
					Base::FLAT_AVAILABLE_QUANTITY_LIST => $availableQuantityData,
				],
			]);

			return $result;
		}

		public function writeOffProductBatches(array $products): Sale\Result
		{
			$result = new Sale\Result();

			if (!Catalog\Config\State::isUsedInventoryManagement() || !State::isProductBatchMethodSelected())
			{
				return $result;
			}

			foreach ($products as $productId => $productData)
			{
				if (empty($productData['SHIPMENT_ITEM_LIST']) || !is_array($productData['SHIPMENT_ITEM_LIST']))
				{
					continue;
				}

				$productBatch = new BatchManager($productId);
				/** @var Sale\ShipmentItem $item */
				foreach ($productData['SHIPMENT_ITEM_LIST'] as $item)
				{
					/** @var Sale\ShipmentItemStore $storeItem */
					foreach ($item->getShipmentItemStoreCollection() as $storeItem)
					{
						$quantity = $storeItem->getQuantity();
						if ($quantity <= 0)
						{
							continue;
						}

						$distributor = new DistributionStrategy\ShipmentStore($productBatch, $storeItem);
						$r = $distributor->writeOff($quantity);
						if (!$r->isSuccess())
						{
							$result->addErrors($r->getErrors());
						}
					}
				}
			}

			return $result;
		}

		public function returnProductBatches(array $products): Sale\Result
		{
			$result = new Sale\Result();

			if (!Catalog\Config\State::isUsedInventoryManagement() || !State::isProductBatchMethodSelected())
			{
				return $result;
			}

			foreach ($products as $productId => $productData)
			{
				if (empty($productData['SHIPMENT_ITEM_LIST']) || !is_array($productData['SHIPMENT_ITEM_LIST']))
				{
					continue;
				}

				$productBatch = new BatchManager($productId);
				/** @var Sale\ShipmentItem $item */
				foreach ($productData['SHIPMENT_ITEM_LIST'] as $item)
				{
					foreach ($item->getShipmentItemStoreCollection() as $storeItem)
					{
						$distributor = new DistributionStrategy\ShipmentStore($productBatch, $storeItem);
						$r = $distributor->return();
						if (!$r->isSuccess())
						{
							$result->addErrors($r->getErrors());
						}
					}
				}
			}

			return $result;
		}

		/**
		 * @param $products
		 *
		 * @return bool
		 */
		private function isExistsCatalogData($products): bool
		{
			foreach ($products as $productData)
			{
				if (empty($productData['CATALOG']))
				{
					return false;
				}
			}
			return true;
		}

		/**
		 * @param array $list
		 *
		 * @return array
		 */
		private function getIblockData(array $list): array
		{
			$resultList = [];
			$res = Catalog\CatalogIblockTable::getList([
				'select' => [
					'IBLOCK_ID',
					'SUBSCRIPTION',
					'PRODUCT_IBLOCK_ID',
					'CATALOG_XML_ID' => 'IBLOCK.XML_ID',
				],
				'filter' => ['@IBLOCK_ID' => $list],
			]);
			while($iblockData = $res->fetch())
			{
				$resultList[$iblockData['IBLOCK_ID']] = $iblockData;
				if ($this->enableCache)
				{
					static::setHitCache(self::CACHE_CATALOG_IBLOCK_LIST, $iblockData['IBLOCK_ID'], $iblockData);
				}
			}
			unset($res, $iblockData);

			return $resultList;
		}

		/**
		 * @param array $iblockProductMap
		 *
		 * @return array
		 */
		private static function checkSkuPermission(array $iblockProductMap): array
		{
			$resultList = array();

			foreach ($iblockProductMap as $iblockData)
			{
				if ($iblockData['PRODUCT_IBLOCK_ID'] > 0 && !empty($iblockData['PRODUCT_LIST']))
				{
					$resultList = array_merge(
						$resultList,
						static::checkParentActivity($iblockData['PRODUCT_LIST'], (int)$iblockData['PRODUCT_IBLOCK_ID'])
					);
				}
				else
				{
					foreach ($iblockData['PRODUCT_LIST'] as $productId)
					{
						$resultList[] = $productId;
					}
				}
			}

			return $resultList;
		}

		/**
		 * @param array $iblockList
		 * @param array $iblockDataList
		 *
		 * @return array
		 */
		private static function createIblockProductMap(array $iblockList, array $iblockDataList): array
		{
			$resultList = $iblockDataList;
			foreach ($iblockList as $iblockId => $iblockProductList)
			{
				if (isset($iblockDataList[$iblockId]))
				{
					$resultList[$iblockId]['PRODUCT_LIST'] = $iblockProductList;
				}
			}

			return $resultList;
		}

		/**
		 * @param array $products
		 * @param array $iblockProductMap
		 *
		 * @return array
		 */
		private static function changeSubscribeProductQuantity(array $products, array $iblockProductMap): array
		{
			$resultList = $products;

			foreach ($iblockProductMap as $iblockData)
			{
				if ($iblockData['SUBSCRIPTION'] != 'Y')
					continue;

				if (empty($iblockData['PRODUCT_LIST']))
					continue;

				foreach($iblockData['PRODUCT_LIST'] as $productId)
				{
					if (isset($resultList[$productId]))
					{
						if (
							!empty($resultList[$productId][Base::FLAT_QUANTITY_LIST])
							&& is_array($resultList[$productId][Base::FLAT_QUANTITY_LIST])
						)
						{
							foreach (array_keys($resultList[$productId][Base::FLAT_QUANTITY_LIST]) as $index)
							{
								$resultList[$productId][Base::FLAT_QUANTITY_LIST][$index] = 1;
							}
						}
					}
				}
			}

			return $resultList;
		}

		/**
		 * @param array $list
		 * @param array $select
		 *
		 * @return array
		 */
		private static function getCatalogProducts(array $list, array $select): array
		{
			$usedStoreInventory = Catalog\Config\State::isUsedInventoryManagement();

			$typesWithoutStores = [
				Catalog\ProductTable::TYPE_SET => true,
				Catalog\ProductTable::TYPE_SKU => true,
				Catalog\ProductTable::TYPE_SERVICE => true,
			];
			$typesWithoutReservation = [
				Catalog\ProductTable::TYPE_SET => true,
				Catalog\ProductTable::TYPE_SKU => true,
				Catalog\ProductTable::TYPE_SERVICE => true,
			];

			if (empty($select))
			{
				$select = ['*'];
			}
			else
			{
				$select[] = 'ID';
				$select[] = 'TYPE';
				$select[] = 'AVAILABLE';

				$select = array_unique($select);
			}
			Main\Type\Collection::normalizeArrayValuesByInt($list, true);
			if (empty($list))
			{
				return [];
			}
			$resultList = [];
			foreach (array_chunk($list, 500) as $pageIds)
			{
				$iterator = Catalog\Model\Product::getList([
					'select' => $select,
					'filter' => [
						'@ID' => $pageIds,
					],
				]);
				while ($row = $iterator->fetch())
				{
					$row['ID'] = (int)$row['ID'];
					$row['TYPE'] = (int)$row['TYPE'];
					$row['QUANTITY'] = (float)$row['QUANTITY'];
					$row['QUANTITY_RESERVED'] = (float)$row['QUANTITY_RESERVED'];
					$row['CHECK_QUANTITY'] = (
						$row['TYPE'] !== Catalog\ProductTable::TYPE_SERVICE
						&& $row['QUANTITY_TRACE'] === Catalog\ProductTable::STATUS_YES
						&& $row['CAN_BUY_ZERO'] === Catalog\ProductTable::STATUS_NO
					);
					Catalog\Product\SystemField::prepareRow($row, Catalog\Product\SystemField::OPERATION_PROVIDER);

					if (isset($typesWithoutStores[$row['TYPE']]))
					{
						$row['USED_STORE_INVENTORY'] = false;
					}
					else
					{
						$row['USED_STORE_INVENTORY'] = $usedStoreInventory;
					}
					if (isset($typesWithoutReservation[$row['TYPE']]))
					{
						$row['USED_RESERVATION'] = false;
						$row['QUANTITY_RESERVED'] = 0;
					}
					else
					{
						$row['USED_RESERVATION'] = true;
					}

					$resultList[$row['ID']] = $row;
				}
				unset($row, $iterator);
			}
			unset($pageIds);

			return $resultList;
		}

		/**
		 * @param null $id
		 *
		 * @return array
		 */
		private static function getMeasure($id = null): array
		{
			static $measureList = array();

			if (!empty($measureList[$id]))
			{
				return $measureList[$id];
			}

			$fields = array(
				'MEASURE' => $id,
				'MEASURE_NAME' => $id,
				'MEASURE_CODE' => 0,
			);

			if ((int)$id <= 0)
			{
				$measure = \CCatalogMeasure::getDefaultMeasure(true, true);
				$fields['MEASURE_NAME'] = $measure['~SYMBOL_RUS'];
				$fields['MEASURE_CODE'] = $measure['CODE'];
			}
			else
			{
				$resMeasures = \CCatalogMeasure::getList(
					array(),
					array('ID' => $id),
					false,
					false,
					array('ID', 'SYMBOL_RUS', 'CODE')
				);
				$measure = $resMeasures->fetch();

				if (!empty($measure))
				{
					$fields['MEASURE_NAME'] = $measure['SYMBOL_RUS'];
					$fields['MEASURE_CODE'] = $measure['CODE'];
				}
			}

			$measureList[$id] = $fields;

			return $fields;
		}

		/**
		 * @param array $products
		 * @param array $productPriceList
		 * @param array $discountList
		 *
		 * @return array
		 */
		private static function createProductPriceList(array $products, array $productPriceList, array $discountList = array()): array
		{
			$priceResultList = array();

			foreach ($productPriceList as $basketCode => $priceData)
			{
				if (!$priceData)
					continue;
				$priceResultList[$basketCode]['PRODUCT_PRICE_ID'] = $priceData['RESULT_PRICE']['ID'];
				$priceResultList[$basketCode]['NOTES'] = $priceData['PRICE']['CATALOG_GROUP_NAME'];
				$priceResultList[$basketCode]['DISCOUNT_NAME'] = null;
				$priceResultList[$basketCode]['DISCOUNT_COUPON'] = null;
				$priceResultList[$basketCode]['DISCOUNT_VALUE'] = null;
				$priceResultList[$basketCode]['DISCOUNT_LIST'] = array();

				$discount = array();
				if (!empty($discountList[$priceData['PRODUCT_ID']][$basketCode]))
				{
					$discount = $discountList[$priceData['PRODUCT_ID']][$basketCode];
				}

				$priceResultList[$basketCode]['PRICE_TYPE_ID'] = $priceData['RESULT_PRICE']['PRICE_TYPE_ID'];
				$priceResultList[$basketCode]['BASE_PRICE'] = $priceData['RESULT_PRICE']['BASE_PRICE'];
				$priceResultList[$basketCode]['PRICE'] = $priceData['RESULT_PRICE']['DISCOUNT_PRICE'];
				$priceResultList[$basketCode]['CURRENCY'] = $priceData['RESULT_PRICE']['CURRENCY'];
				$priceResultList[$basketCode]['DISCOUNT_PRICE'] = $priceData['RESULT_PRICE']['DISCOUNT'];
				if (isset($priceData['RESULT_PRICE']['PERCENT']))
				{
					$priceResultList[$basketCode]['DISCOUNT_VALUE'] = ($priceData['RESULT_PRICE']['PERCENT'] > 0
						? $priceData['RESULT_PRICE']['PERCENT'] . '%' : null);
				}
				$priceResultList[$basketCode]['VAT_RATE'] = $priceData['RESULT_PRICE']['VAT_RATE'];
				$priceResultList[$basketCode]['VAT_INCLUDED'] = $priceData['RESULT_PRICE']['VAT_INCLUDED'];

				if (!empty($discount))
				{
					$priceResultList[$basketCode]['DISCOUNT_LIST'] = $discount;
				}

				if (!empty($priceData['DISCOUNT']))
				{
					$priceResultList[$basketCode]['DISCOUNT_NAME'] = '[' .
						$priceData['DISCOUNT']['ID'] .
						'] ' .
						$priceData['DISCOUNT']['NAME'];
					if (!empty($priceData['DISCOUNT']['COUPON']))
					{
						$priceResultList[$basketCode]['DISCOUNT_COUPON'] = $priceData['DISCOUNT']['COUPON'];
					}

					if (empty($priceResultList[$basketCode]['DISCOUNT_LIST']))
					{
						$priceResultList[$basketCode]['DISCOUNT_LIST'] = array($priceData['DISCOUNT']);
					}
				}
			}

			$resultList = array();
			if (!empty($priceResultList))
			{
				foreach ($products as $productId => $productData)
				{
					if (!empty($products[$productId]))
					{
						$productData = $products[$productId];

						$quantityList = array();

						if (array_key_exists('QUANTITY', $productData))
						{
							$quantityList = array(
								$productData['BASKET_CODE'] => $productData['QUANTITY'],
							);
						}
						if (!empty($productData[Base::FLAT_QUANTITY_LIST]))
						{
							$quantityList = $productData[Base::FLAT_QUANTITY_LIST];
						}

						foreach($quantityList as $basketCode => $quantity)
						{
							$resultList[$basketCode] = $priceResultList[$basketCode];
						}
					}
				}
			}

			return $resultList;
		}

		/**
		 * @param array $products
		 * @param array $items
		 * @param array $priceList
		 * @param array $productQuantityList
		 *
		 * @return array
		 */
		private static function createProductResult(array $products, array $items, array $priceList, array $productQuantityList): array
		{
			$resultList = array();
			foreach ($products as $productId => $productData)
			{
				$itemCode = $productData['ITEM_CODE'];
				$basketCode = $productData['BASKET_CODE'];
				$resultList[$productId] = $items[$productId];

				if (isset($productData['PRODUCT_DATA']['ACTIVE']))
				{
					$resultList[$productId]['ACTIVE'] = $productData['PRODUCT_DATA']['ACTIVE'];
				}

				$resultList[$productId]['ITEM_CODE'] = $itemCode;

				QuantityControl::resetAllQuantity($productId);
				QuantityControl::setReservedQuantity($productId, $productQuantityList[$basketCode]['QUANTITY_RESERVED']);

				if (!isset($priceList[$basketCode]))
				{
					$priceList[$basketCode] = array();
				}

				if (!empty($productData[Base::FLAT_QUANTITY_LIST]))
				{
					foreach($productData[Base::FLAT_QUANTITY_LIST] as $basketCode => $quantity)
					{
						QuantityControl::addQuantity($productId, $productQuantityList[$basketCode]['QUANTITY']);
						QuantityControl::addAvailableQuantity($productId, $productQuantityList[$basketCode]['AVAILABLE_QUANTITY']);

						if (empty($priceList[$basketCode]))
						{
							continue;
						}

						$resultList[$productId]['PRICE_LIST'][$basketCode] = array_merge(
							array(
								'QUANTITY' => $productQuantityList[$basketCode]['QUANTITY'],
								'AVAILABLE_QUANTITY' => $productQuantityList[$basketCode]['AVAILABLE_QUANTITY'],
								"ITEM_CODE" => $itemCode,
								"BASKET_CODE" => $basketCode,
							),
							$priceList[$basketCode]
						);
					}
				}
				else
				{
					$resultList[$productId]['QUANTITY'] = $productQuantityList[$basketCode]['QUANTITY'];

					QuantityControl::addQuantity($productId, $productQuantityList[$basketCode]['QUANTITY']);
					QuantityControl::addAvailableQuantity($productId, $productQuantityList[$basketCode]['AVAILABLE_QUANTITY']);
					if (!empty($resultList[$productId]))
					{
						if (empty($priceList[$basketCode]))
						{
							continue;
						}

						$resultList[$productId] = $priceList[$basketCode] + $resultList[$productId];
					}
				}
			}

			return $resultList;
		}

		/**
		 * @param array $products
		 * @param array $catalogDataList
		 * @param array $options
		 *
		 * @return array
		 */
		private static function setCatalogDataToProducts(array $products, array $catalogDataList, array $options = array()): array
		{
			$catalogDataEnabled = self::isCatalogDataEnabled($options);
			$specialFields = [];
			foreach (Catalog\Product\SystemField::getProviderSelectFields() as $index => $value)
			{
				$specialFields[] = is_string($index) ? $index : $value;
			}

			$result = [];
			foreach ($products as $productId => $productData)
			{
				if (!isset($catalogDataList[$productId]))
				{
					continue;
				}

				$row = $catalogDataList[$productId];

				$result[$productId] = [
					'CAN_BUY' => (
						$productData['PRODUCT_DATA']['ACTIVE'] === 'Y'
						&& $row['AVAILABLE'] === 'Y'
						? 'Y'
						: 'N'
					),
					'CAN_BUY_ZERO' => $row['CAN_BUY_ZERO'],
					'QUANTITY_TRACE' => $row['QUANTITY_TRACE'],
					'CHECK_QUANTITY' => $row['CHECK_QUANTITY'],
					'QUANTITY_RESERVED' => (float)$row['QUANTITY_RESERVED'],
					'CATALOG_XML_ID' => $productData['PRODUCT_DATA']['CATALOG_XML_ID'],
					'PRODUCT_XML_ID' => $productData['PRODUCT_DATA']['~XML_ID'],
					'PRODUCT' => $row,
				];

				if (!$catalogDataEnabled)
				{
					$basketRow = [
						'NAME' => $productData['PRODUCT_DATA']['~NAME'],
						'DETAIL_PAGE_URL' => $productData['PRODUCT_DATA']['~DETAIL_PAGE_URL'],
						'MEASURE_ID' => $row['MEASURE'],
						'MEASURE_NAME' => $row['MEASURE_NAME'],
						'MEASURE_CODE' => $row['MEASURE_CODE'],
						'BARCODE_MULTI' => $row['BARCODE_MULTI'],
						'WEIGHT' => (float)$row['WEIGHT'],
						'DIMENSIONS' => serialize(
							[
								'WIDTH' => $row['WIDTH'],
								'HEIGHT' => $row['HEIGHT'],
								'LENGTH' => $row['LENGTH'],
							]
						),
					];
					switch ($row['TYPE'])
					{
						case Catalog\ProductTable::TYPE_SET:
							$basketRow['TYPE'] = Sale\BasketItem::TYPE_SET;
							break;
						case Catalog\ProductTable::TYPE_SERVICE:
							$basketRow['TYPE'] = Sale\BasketItem::TYPE_SERVICE;
							break;
						default:
							$basketRow['TYPE'] = null;
							break;
					}
					foreach ($specialFields as $index)
					{
						$basketRow[$index] = $row[$index];
					}

					$result[$productId] = array_merge(
						$result[$productId],
						$basketRow
					);
				}

				$result[$productId]["VAT_INCLUDED"] = "Y";
			}

			return $result;
		}

		/**
		 * @return bool
		 */
		protected static function isReservationEnabled()
		{
			return !(Main\Config\Option::get("catalog", "enable_reservation") == "N"
				&& Main\Config\Option::get("sale", "product_reserve_condition") != "S"
				&& !Catalog\Config\State::isUsedInventoryManagement());
		}

		/**
		 * @param array $products
		 *
		 * @return array
		 * @throws Main\ObjectNotFoundException
		 */
		public static function createOrderListFromProducts(array $products)
		{
			$productOrderList = array();
			foreach ($products as $productId => $productData)
			{
				if (!empty($productData['SHIPMENT_ITEM_LIST']))
				{
					/**
					 * @var $shipmentItemIndex
					 * @var Sale\ShipmentItem $shipmentItem
					 */
					foreach ($productData['SHIPMENT_ITEM_LIST'] as $shipmentItem)
					{
						$shipmentItemCollection = $shipmentItem->getCollection();
						if (!$shipmentItemCollection)
						{
							throw new Main\ObjectNotFoundException('Entity "ShipmentItemCollection" not found');
						}

						$shipment = $shipmentItemCollection->getShipment();
						if (!$shipment)
						{
							throw new Main\ObjectNotFoundException('Entity "Shipment" not found');
						}

						/** @var Sale\ShipmentCollection $shipmentCollection */
						$shipmentCollection = $shipment->getCollection();
						if (!$shipmentCollection)
						{
							throw new Main\ObjectNotFoundException('Entity "ShipmentCollection" not found');
						}

						$order = $shipmentCollection->getOrder();
						if (!$order)
						{
							throw new Main\ObjectNotFoundException('Entity "Order" not found');
						}

						if (empty($productOrderList[$productId][$order->getId()]))
						{
							$productOrderList[$productId][$order->getId()] = $order;
						}
					}
				}
			}

			return $productOrderList;
		}

		/**
		 * @param $productId
		 *
		 * @return array
		 */
		private static function getProductCatalogInfo($productId): array
		{
			$productId = (int)$productId;
			if ($productId <= 0)
			{
				return [];
			}

			$product = static::getHitCache(self::CACHE_ELEMENT_SHORT_DATA, $productId);
			if (empty($product))
			{
				$iterator = Iblock\ElementTable::getList([
					'select' => [
						'ID',
						'IBLOCK_ID',
						'NAME',
						'IBLOCK_SECTION_ID',
					],
					'filter' => \CIBlockElement::getPublicElementsOrmFilter(['=ID' => $productId]),
				]);
				$product = $iterator->fetch();
				if ($product)
				{
					static::setHitCache(self::CACHE_ELEMENT_SHORT_DATA, $productId, $product);
				}
			}

			return (empty($product)
				? []
				: [
					"#PRODUCT_ID#" => $product['ID'],
					"#PRODUCT_NAME#" => $product['NAME'],
				]
			);
		}

		private static function getTotalAmountFromQuantityList(array $data): float
		{
			return self::getAmountFromSource(
				$data,
				[
					self::AMOUNT_SRC_STORE_QUANTITY_LIST,
					self::AMOUNT_SRC_QUANTITY,
					self::AMOUNT_SRC_QUANTITY_LIST,
				]
			);
		}

		private static function getTotalAmountFromPriceList(array $product, bool $direction = true): float
		{
			if ($direction)
			{
				$list = [
					self::AMOUNT_SRC_QUANTITY,
					self::AMOUNT_SRC_PRICE_LIST,
				];
			}
			else
			{
				$list = [
					self::AMOUNT_SRC_PRICE_LIST,
					self::AMOUNT_SRC_QUANTITY,
				];
			}

			return self::getAmountFromSource($product, $list);
		}

		private static function isCatalogDataEnabled(array $options): bool
		{
			return in_array(self::USE_GATALOG_DATA, $options);
		}

		private static function fillCatalogXmlId(array $products, array $iblockProductMap): array
		{
			foreach ($iblockProductMap as $entityData)
			{
				if (empty($entityData['PRODUCT_LIST']) || !is_array($entityData['PRODUCT_LIST']))
				{
					continue;
				}
				foreach ($entityData['PRODUCT_LIST'] as $index)
				{
					if (!isset($products[$index]))
					{
						continue;
					}
					$products[$index]['PRODUCT_DATA']['CATALOG_XML_ID'] = $entityData['CATALOG_XML_ID'];
				}
				unset($index);
			}
			unset($entityData);

			return $products;
		}

		private static function fillOfferXmlId(array $products, array $catalogProductDataList): array
		{
			$offerList = [];
			foreach ($catalogProductDataList as $entityData)
			{
				if ($entityData['TYPE'] != Catalog\ProductTable::TYPE_OFFER)
				{
					continue;
				}
				if (strpos($products[$entityData['ID']]['PRODUCT_DATA']['~XML_ID'], '#') !== false)
				{
					continue;
				}
				$offerList[] = $entityData['ID'];
			}
			unset($entityData);
			if (!empty($offerList))
			{
				$parentMap = [];
				$parentIdList = [];
				$parentList = \CCatalogSku::getProductList($offerList, 0);
				foreach ($parentList as $offerId => $offerData)
				{
					$parentId = (int)$offerData['ID'];
					if (!isset($parentMap[$parentId]))
					{
						$parentMap[$parentId] = [];
					}
					$parentMap[$parentId][] = $offerId;
					$parentIdList[$parentId] = $parentId;
				}
				unset($offerId, $offerData, $parentList);
				if (!empty($parentMap))
				{
					sort($parentIdList);
					foreach (array_chunk($parentIdList, 500) as $pageIds)
					{
						$iterator = Iblock\ElementTable::getList([
							'select' => [
								'ID',
								'XML_ID',
							],
							'filter' => ['@ID' => $pageIds],
						]);
						while ($row = $iterator->fetch())
						{
							$parentId = (int)$row['ID'];
							if (empty($parentMap[$parentId]))
							{
								continue;
							}
							foreach ($parentMap[$parentId] as $index)
							{
								$products[$index]['PRODUCT_DATA']['~XML_ID'] = $row['XML_ID'] . '#'
									. $products[$index]['PRODUCT_DATA']['~XML_ID']
								;
							}
						}
						unset($parentId, $index);
						unset($row, $iterator);
					}
					unset($pageIds);
				}
				unset($parentIdList, $parentMap);
			}
			unset($offerList);

			return $products;
		}

		private static function getPriceDataList(array $products, array $config): array
		{
			/*
				'IS_ADMIN_SECTION' => $adminSection,
				'USER_ID' => $userId,
				'SITE_ID' => $siteId,
				'CURRENCY' => $currency,
			*/
			$userGroups = self::getUserGroups($config['USER_ID']);

			\CCatalogProduct::GetVATDataByIDList(array_keys($products));

			if ($config['IS_ADMIN_SECTION'])
			{
				if ($config['USER_ID'] > 0)
				{
					\CCatalogDiscountSave::SetDiscountUserID($config['USER_ID']);
				}
				else
				{
					\CCatalogDiscountSave::Disable();
				}
			}

			Price\Calculation::pushConfig();
			Price\Calculation::setConfig([
				'CURRENCY' => $config['CURRENCY'],
				'PRECISION' => (int)Main\Config\Option::get('sale', 'value_precision'),
				'RESULT_WITH_VAT' => true,
				'RESULT_MODE' => Catalog\Product\Price\Calculation::RESULT_MODE_RAW,
			]);

			$priceDataList = \CCatalogProduct::GetOptimalPriceList(
				$products,
				$userGroups,
				'N',
				[],
				($config['IS_ADMIN_SECTION'] ? $config['SITE_ID'] : false)
			);

			if (empty($priceDataList))
			{
				$productsQuantityList = $products;
				$quantityCorrected = false;

				foreach ($productsQuantityList as $productId => $productData)
				{
					$quantityList = array($productData['BASKET_CODE'] => $productData['QUANTITY']);

					if (empty($productData[Base::FLAT_QUANTITY_LIST]))
					{
						$quantityList = $productData[Base::FLAT_QUANTITY_LIST];
					}

					if (empty($quantityList))
					{
						continue;
					}

					foreach ($quantityList as $basketCode => $quantity)
					{
						$nearestQuantity = \CCatalogProduct::GetNearestQuantityPrice($productId, $quantity, $userGroups);
						if (!empty($nearestQuantity))
						{
							if (!empty($productData[Base::FLAT_QUANTITY_LIST]))
							{
								$productsQuantityList[$productId][Base::FLAT_QUANTITY_LIST][$basketCode]['QUANTITY'] = $nearestQuantity;
							}
							else
							{
								$productsQuantityList[$productId]['QUANTITY'] = $nearestQuantity;
							}

							$quantityCorrected = true;
						}
					}
				}

				if ($quantityCorrected)
				{
					$priceDataList = \CCatalogProduct::GetOptimalPriceList(
						$productsQuantityList,
						$userGroups,
						'N',
						[],
						($config['IS_ADMIN_SECTION'] ? $config['SITE_ID'] : false)
					);
				}

			}

			Price\Calculation::popConfig();

			if ($config['IS_ADMIN_SECTION'])
			{
				if ($config['USER_ID'] > 0)
				{
					\CCatalogDiscountSave::ClearDiscountUserID();
				}
				else
				{
					\CCatalogDiscountSave::Enable();
				}
			}

			if (!empty($priceDataList))
			{
				foreach ($priceDataList as $productId => $priceBasketDataList)
				{
					foreach ($priceBasketDataList as $basketCode => $priceData)
					{
						if ($priceData === false)
						{
							continue;
						}

						if (empty($priceData['DISCOUNT_LIST']) && !empty($priceData['DISCOUNT']) && is_array($priceData['DISCOUNT']))
						{
							$priceDataList[$productId][$basketCode]['DISCOUNT_LIST'] = [$priceData['DISCOUNT']];
						}

						if (empty($priceData['PRICE']['CATALOG_GROUP_NAME']))
						{
							if (!empty($priceData['PRICE']['CATALOG_GROUP_ID']))
							{
								$priceName = self::getPriceTitle($priceData['PRICE']['CATALOG_GROUP_ID']);
								if ($priceName !== '')
								{
									$priceDataList[$productId][$basketCode]['PRICE']['CATALOG_GROUP_NAME'] = $priceName;
								}
								unset($priceName);
							}
						}
					}
				}
			}

			return $priceDataList;
		}

		private static function getDiscountList(array $priceDataList): array
		{
			$discountList = array();
			if (!empty($priceDataList))
			{
				foreach ($priceDataList as $productId => $priceBasketDataList)
				{
					foreach ($priceBasketDataList as $basketCode => $priceData)
					{
						if ($priceData === false)
						{
							continue;
						}

						if (empty($priceData['DISCOUNT_LIST']) && !empty($priceData['DISCOUNT']) && is_array($priceData['DISCOUNT']))
						{
							$priceDataList[$productId][$basketCode]['DISCOUNT_LIST'] = [$priceData['DISCOUNT']];
						}

						if (!empty($priceData['DISCOUNT_LIST']))
						{
							if (!isset($discountList[$productId]))
							{
								$discountList[$productId] = [];
							}
							if (!isset($discountList[$productId][$basketCode]))
							{
								$discountList[$productId][$basketCode] = [];
							}
							foreach ($priceData['DISCOUNT_LIST'] as $discountItem)
							{
								$discountList[$productId][$basketCode][] = \CCatalogDiscount::getDiscountDescription($discountItem);
							}
							unset($discountItem);
						}

						if (empty($priceData['PRICE']['CATALOG_GROUP_NAME']))
						{
							if (!empty($priceData['PRICE']['CATALOG_GROUP_ID']))
							{
								$priceName = self::getPriceTitle($priceData['PRICE']['CATALOG_GROUP_ID']);
								if ($priceName != '')
								{
									$priceDataList[$productId][$basketCode]['PRICE']['CATALOG_GROUP_NAME'] = $priceName;
								}
								unset($priceName);
							}
						}
					}
				}
			}

			return $discountList;
		}

		public static function getDefaultStoreId(): int
		{
			$result = parent::getDefaultStoreId();
			if (Catalog\Config\State::isUsedInventoryManagement())
			{
				$storeId = Catalog\StoreTable::getDefaultStoreId();
				if ($storeId !== null)
				{
					$result = $storeId;
				}
			}

			return $result;
		}

		private static function getAmountFromSource(array $product, array $sourceList): float
		{
			if (empty($product) || empty($sourceList))
			{
				return 0;
			}

			$result = 0;
			$found = false;
			foreach ($sourceList as $source)
			{
				switch ($source)
				{
					case self::AMOUNT_SRC_QUANTITY:
						if (array_key_exists($source, $product))
						{
							$result = $product[$source];
							$found = true;
						}
						break;
					case self::AMOUNT_SRC_QUANTITY_LIST:
					case self::AMOUNT_SRC_RESERVED_LIST:
						if (
							!empty($product[$source])
							&& is_array($product[$source])
						)
						{
							$result = array_sum($product[$source]);
							$found = true;
						}
						break;
					case self::AMOUNT_SRC_PRICE_LIST:
						if (
							!empty($product[$source])
							&& is_array($product[$source])
						)
						{
							foreach ($product[$source] as $row)
							{
								if (!is_array($row) || !isset($row['QUANTITY']))
								{
									continue;
								}
								$result += (float)$row['QUANTITY'];
							}
							unset($row);
							$found = true;
						}
						break;
					case self::AMOUNT_SRC_STORE_QUANTITY_LIST:
					case self::AMOUNT_SRC_STORE_RESERVED_LIST:
						if (
							!empty($product[$source])
							&& is_array($product[$source])
						)
						{
							switch (self::getQuantityFormat($product[$source]))
							{
								case self::QUANTITY_FORMAT_STORE:
									$internalResult = self::calculateQuantityFromStores($product[$source]);
									break;
								case self::QUANTITY_FORMAT_SHIPMENT:
									$internalResult = self::calculateQuantityFromShipments($product[$source]);
									break;
								default:
									$internalResult = null;
									break;
							}
							if ($internalResult !== null)
							{
								$result += array_sum($internalResult);
								$found = true;
							}
							unset($internalResult);
						}
						break;
				}
				if ($found)
				{
					break;
				}
			}

			return (float)$result;
		}

		private static function getStoreAmountFromQuantityList(array $data): ?array
		{
			return self::getStoreAmountFromSource(
				$data,
				[
					self::AMOUNT_SRC_STORE_QUANTITY_LIST,
					self::AMOUNT_SRC_QUANTITY_LIST,
					self::AMOUNT_SRC_QUANTITY,
				]
			);
		}

		private static function getStoreReservedQuantityFromProduct(array $product): ?array
		{
			return self::getStoreAmountFromSource(
				$product,
				[
					self::AMOUNT_SRC_STORE_RESERVED_LIST,
					self::AMOUNT_SRC_RESERVED_LIST,
				]
			);
		}

		private static function getStoreAmountFromPriceList(array $product, bool $direction = true): ?array
		{
			if ($direction)
			{
				$list = [
					self::AMOUNT_SRC_QUANTITY,
					self::AMOUNT_SRC_PRICE_LIST,
				];
			}
			else
			{
				$list = [
					self::AMOUNT_SRC_PRICE_LIST,
					self::AMOUNT_SRC_QUANTITY,
				];
			}

			return self::getStoreAmountFromSource($product, $list);
		}

		private static function getStoreAmountFromSource(array $product, array $sourceList): ?array
		{
			if (empty($product) || empty($sourceList))
			{
				return null;
			}

			$result = [];
			$found = false;
			foreach ($sourceList as $source)
			{
				switch ($source)
				{
					case self::AMOUNT_SRC_STORE_QUANTITY_LIST:
					case self::AMOUNT_SRC_STORE_RESERVED_LIST:
						if (
							!empty($product[$source])
							&& is_array($product[$source])
						)
						{
							switch (self::getQuantityFormat($product[$source]))
							{
								case self::QUANTITY_FORMAT_STORE:
									$internalResult = self::calculateQuantityFromStores($product[$source]);
									break;
								case self::QUANTITY_FORMAT_SHIPMENT:
									$internalResult = self::calculateQuantityFromShipments($product[$source]);
									break;
								default:
									$internalResult = null;
									break;
							}
							if ($internalResult !== null)
							{
								$result = $internalResult;
								$found = true;
							}
							unset($internalResult);
						}
						break;
					case self::AMOUNT_SRC_QUANTITY_LIST:
					case self::AMOUNT_SRC_RESERVED_LIST:
						/*
						'QUANTITY_LIST' =>
							array (
								289 => 1.0,
								290 => 3.0,
								291 => 4.0,
							),
						 */
						if (
							!empty($product[$source])
							&& is_array($product[$source])
						)
						{
							$result[static::getDefaultStoreId()] = array_sum($product[$source]);
							$found = true;
						}
						break;
					case self::AMOUNT_SRC_QUANTITY:
						if (array_key_exists($source, $product))
						{
							$result[static::getDefaultStoreId()] = (float)$product[$source];
							$found = true;
						}
						break;
				}
				if ($found)
				{
					break;
				}
			}

			return (!empty($result) ? $result : null);
		}

		private static function getQuantityFormat(array $list): ?int
		{
			/*
			first variant
			'RESERVED_QUANTITY_LIST_BY_STORE' =>
			array (
				20 => basket code
					array (
						'0_0' => shipment index
							array (
								3 => 10.0, store id -> quantity
							),
					),
			),

			second variant
			'RESERVED_QUANTITY_LIST_BY_STORE' =>
			array (
				20 => basket code
					array (
						3 => 10.0, store id -> quantity
					),
			),

			'QUANTITY_LIST_BY_STORE' =>
			array (
				289 => basket code
					array (
						5 => 1.0,  store id => quantity
					),
				290 =>
					array (
						5 => 3.0,
					),
				291 =>
					array (
						5 => 4.0,
					),
				),
			),
			*/

			$basketRow = reset($list);
			if (
				empty($basketRow)
				|| !is_array($basketRow)
			)
			{
				return null;
			}

			$row = reset($basketRow);
			if (is_array($row))
			{
				return self::QUANTITY_FORMAT_SHIPMENT;
			}

			return self::QUANTITY_FORMAT_STORE;
		}

		private static function calculateQuantityFromStores(array $list): ?array
		{
			$result = [];
			$found = false;
			foreach ($list as $basketItemStores)
			{
				if (
					empty($basketItemStores)
					|| !is_array($basketItemStores)
				)
				{
					continue;
				}
				foreach ($basketItemStores as $storeId => $quantity)
				{
					if (!isset($result[$storeId]))
					{
						$result[$storeId] = 0.0;
					}
					$result[$storeId] += (float)$quantity;
					$found = true;
				}
				unset($storeId, $quantity);
			}
			unset($basketItemStores);

			return ($found ? $result : null);
		}

		private static function calculateQuantityFromShipments(array $list): ?array
		{
			$result = [];
			$found = false;
			foreach ($list as $basketItemShipments)
			{
				if (
					empty($basketItemShipments)
					|| !is_array($basketItemShipments)
				)
				{
					continue;
				}
				foreach ($basketItemShipments as $basketItemStores)
				{
					foreach ($basketItemStores as $storeId => $quantity)
					{
						if (!isset($result[$storeId]))
						{
							$result[$storeId] = 0;
						}
						$result[$storeId] += (float)$quantity;
						$found = true;
					}
				}
			}

			return ($found ? $result : null);
		}

		private static function getStoreQuantityFromQuantityList(array $product): array
		{
			return self::getStoreQuantityFromSource(
				$product,
				[
					self::AMOUNT_SRC_STORE_QUANTITY_LIST,
					self::AMOUNT_SRC_QUANTITY_LIST,
				]
			);
		}

		private static function getStoreQuantityFromSource(array $product, array $sourceList): array
		{
			if (empty($product) || empty($sourceList))
			{
				return [static::getDefaultStoreId() => 0.0];
			}

			$result = [];
			$found = false;
			foreach ($sourceList as $source)
			{
				switch ($source)
				{
					case self::AMOUNT_SRC_QUANTITY_LIST:
					case self::AMOUNT_SRC_RESERVED_LIST:
						if (
							!empty($product[$source])
							&& is_array($product[$source])
						)
						{
							$result = [
								static::getDefaultStoreId() => (float)array_sum($product[$source]),
							];
							$found = true;
						}
						break;
					/*case self::AMOUNT_SRC_PRICE_LIST:
						if (
							!empty($product[$source])
							&& is_array($product[$source])
						)
						{
							foreach ($product[$source] as $row)
							{
								if (!is_array($row) || !isset($row['QUANTITY']))
								{
									continue;
								}
								$result += (float)$row['QUANTITY'];
							}
							unset($row);
							$found = true;
						}
						break; */
					case self::AMOUNT_SRC_STORE_QUANTITY_LIST:
					case self::AMOUNT_SRC_STORE_RESERVED_LIST:
						if (
							!empty($product[$source])
							&& is_array($product[$source])
						)
						{
							switch (self::getQuantityFormat($product[$source]))
							{
								case self::QUANTITY_FORMAT_STORE:
									$internalResult = self::calculateQuantityFromStores($product[$source]);
									break;
								case self::QUANTITY_FORMAT_SHIPMENT:
									$internalResult = self::calculateQuantityFromShipments($product[$source]);
									break;
								default:
									$internalResult = null;
									break;
							}
							if ($internalResult !== null)
							{
								$result = $internalResult;
								$found = true;
							}
						}
						break;
				}
				if ($found)
				{
					break;
				}
			}

			return (!empty($result)
				? $result
				: [static::getDefaultStoreId() => 0.0]
			);
		}

		private static function loadCurrentStoreReserve(int $productId, array $reserve): array
		{
			$result = [];
			foreach ($reserve as $storeId => $quantity)
			{
				$result[$storeId] = [
					'ID' => null,
					'PRODUCT_ID' => $productId,
					'STORE_ID' => $storeId,
					'ADD_QUANTITY_RESERVED' => $quantity,
					'QUANTITY_RESERVED' => 0.0,
				];
			}

			$iterator = Catalog\StoreProductTable::getList([
				'select' => [
					'ID',
					'STORE_ID',
					'QUANTITY_RESERVED',
				],
				'filter' => [
					'=PRODUCT_ID' => $productId,
					'@STORE_ID' => array_keys($reserve),
				],
			]);
			while ($row = $iterator->fetch())
			{
				$storeId = (int)$row['STORE_ID'];
				$result[$storeId]['ID'] = (int)$row['ID'];
				$result[$storeId]['QUANTITY_RESERVED'] = (float)$row['QUANTITY_RESERVED'];
			}
			unset($row, $iterator);

			return $result;
		}

		private static function loadCurrentProductStores(array $list): array
		{
			Main\Type\Collection::normalizeArrayValuesByInt($list, true);
			if (empty($list))
			{
				return [];
			}

			$result = [];
			foreach (array_chunk($list, 500) as $pageIds)
			{
				$iterator = Catalog\StoreProductTable::getList([
					'select' => [
						'ID',
						'STORE_ID',
						'PRODUCT_ID',
						'AMOUNT',
						'QUANTITY_RESERVED',
					],
					'filter' => [
						'@PRODUCT_ID' => $pageIds,
						'=STORE.ACTIVE' => 'Y',
					],
					'order' => [
						'PRODUCT_ID' => 'ASC',
						'STORE_ID' => 'ASC',
					],
				]);
				while ($row = $iterator->fetch())
				{
					$row['ID'] = (int)$row['ID'];
					$row['PRODUCT_ID'] = (int)$row['PRODUCT_ID'];
					$row['STORE_ID'] = (int)$row['STORE_ID'];
					$row['AMOUNT'] = (float)$row['AMOUNT'];
					$row['QUANTITY_RESERVED'] = (float)$row['QUANTITY_RESERVED'];

					$productId = $row['PRODUCT_ID'];
					$storeId = $row['STORE_ID'];
					if (!isset($result[$productId]))
					{
						$result[$productId] = [];
					}
					$result[$productId][$storeId] = $row;
				}
				unset($productId, $storeId);
				unset($row, $iterator);
			}
			unset($pageIds);

			return $result;
		}

		private static function convertErrors(Main\Entity\Result $result): void
		{
			global $APPLICATION;

			$oldMessages = [];
			foreach ($result->getErrorMessages() as $errorText)
			{
				$oldMessages[] = [
					'text' => $errorText,
				];
			}
			unset($errorText);

			if (!empty($oldMessages))
			{
				$error = new \CAdminException($oldMessages);
				$APPLICATION->ThrowException($error);
				unset($error);
			}
			unset($oldMessages);
		}
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit