Your IP : 216.73.216.86


Current Path : /var/www/homesaver/www/bitrix/components/bitrix/catalog.store.document.product.list/
Upload File :
Current File : /var/www/homesaver/www/bitrix/components/bitrix/catalog.store.document.product.list/class.php

<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
	die();
}

use Bitrix\Catalog\Access\AccessController;
use Bitrix\Catalog\Access\ActionDictionary;
use Bitrix\Catalog\Access\Permission\PermissionDictionary;
use Bitrix\Catalog\Component\ImageInput;
use Bitrix\Catalog\Config\Feature;
use Bitrix\Catalog\Config\State;
use Bitrix\Catalog\GroupTable;
use Bitrix\Catalog\StoreBarcodeTable;
use Bitrix\Catalog\StoreDocumentBarcodeTable;
use Bitrix\Catalog\StoreDocumentElementTable;
use Bitrix\Catalog\StoreDocumentTable;
use Bitrix\Catalog\StoreProductTable;
use Bitrix\Catalog\StoreTable;
use Bitrix\Catalog\Url\InventoryBuilder;
use Bitrix\Catalog\v2\Sku\BaseSku;
use Bitrix\Crm\ProductRowTable;
use Bitrix\Main;
use Bitrix\Main\Grid\Editor\Types;
use Bitrix\Main\Grid\Panel\Snippet;
use Bitrix\Main\Loader;
use Bitrix\Main\Localization\Loc;
use Bitrix\Currency\CurrencyManager;
use Bitrix\Catalog\v2\IoC\ServiceContainer;
use Bitrix\Main\Text\HtmlFilter;
use Bitrix\Main\Web\Json;
use Bitrix\Catalog\ProductTable;
use Bitrix\Catalog\Store\EnableWizard;
use Bitrix\Sale\PriceMaths;
use Bitrix\Sale\Tax\VatCalculator;

if (!Loader::includeModule('catalog'))
{
	return;
}

final class CatalogStoreDocumentProductListComponent
	extends \Bitrix\Catalog\Component\ProductList
	implements Main\Engine\Contract\Controllerable, Main\Errorable
{
	use Main\ErrorableImplementation;

	private const NEW_ROW_ID_PREFIX = 'n';
	private const PRODUCT_ID_MASK = '#PRODUCT_ID_MASK#';

	private const VIEW_MODE_GRID_ID_POSTFIX = 'V';
	private const EDIT_MODE_GRID_ID_POSTFIX = 'E';

	/** @var Main\Grid\Options $gridConfig */
	protected $gridConfig;
	protected $storage = [];
	protected $defaultSettings = [];
	protected $rows = [];

	/** @var Main\UI\PageNavigation $navigation */
	protected $navigation;

	protected $currency = [
		'ID' => '',
		'TEMPLATE' => '',
		'TEXT' => '',
		'FORMAT' => [],
	];
	protected $stores = [];
	protected $newRowCounter = 0;

	protected $externalDocument = [];
	protected AccessController $accessController;
	/**
	 * @var int[]
	 */
	protected array $accessibleStoresIds;

	/**
	 * Base constructor.
	 *
	 * @param \CBitrixComponent|null $component Component object if exists.
	 */
	public function __construct($component = null)
	{
		parent::__construct($component);

		$this->errorCollection = new Main\ErrorCollection();
		$this->accessController = AccessController::getCurrent();
	}

	/**
	 * @return array
	 */
	public function configureActions()
	{
		return [];
	}

	/**
	 * @param $params
	 * @return array
	 */
	public function onPrepareComponentParams($params): array
	{
		/**
		 * GRID_ID - string - custom grid id
		 * NAVIGATION_ID - string - custom navigation id (maybe created from GRID_ID)
		 * FORM_ID - string - custom form identifier (maybe created from GRID_ID), default empty
		 * TAB_ID - string - custom product tab identifier, default empty
		 *
		 * AJAX_ID - string - ajax component identifier
		 * AJAX_MODE - string - is ajax enabled (Y/N), default Y
		 * AJAX_OPTION_JUMP - string - ajax option (Y/N), default N
		 * AJAX_OPTION_HISTORY - string - ajax option (Y/N), default N
		 * AJAX_LOADER - mixed|null - not used in titleflex template, default null
		 *
		 * SHOW_PAGINATION - bool or Y/N - show pagination block, default false
		 * SHOW_TOTAL_COUNTER - bool or Y/N - show count of rows, default false
		 * SHOW_PAGESIZE - bool or Y/N - show page size select, default false
		 * PAGINATION - array - pagination info (pages size, offset, etc.), default - empty array
		 *
		 * PRODUCTS - array|null - product list
		 * TOTAL_PRODUCTS_COUNT - int - full product rows quantity
		 *
		 * CUSTOM_SITE_ID - string - entity site identifier, default SITE_ID
		 * CUSTOM_LANGUAGE_ID - string - current lang identifier, default LANGUAGE_ID
		 * SET_ITEMS - bool - set rows (Y/N), default N
		 * ALLOW_EDIT - bool - allow to modify data (Y/N), default N
		 * ALLOW_ADD_PRODUCT - bool - add product to entity button (Y/N), default N
		 * ALLOW_CREATE_NEW_PRODUCT - bool - create fake products button (Y/N), default N
		 * if ALLOW_EDIT off - ALLOW_ADD_PRODUCT and ALLOW_CREATE_NEW_PRODUCT already off
		 *
		 * DOCUMENT_ID - string|int - parent entity id
		 *
		 * EXTERNAL_DOCUMENT - array|null - custom external documents
		 *
		 * PRESELECTED_PRODUCT_ID - int - preselected product (can be absent)
		 */

		$this->prepareEntityIds($params);
		$this->prepareAjaxOptions($params);
		$this->preparePaginationOptions($params);
		$this->prepareProducts($params);
		$this->prepareSettings($params);
		$this->prepareEntitySettings($params);

		$params['PRESELECTED_PRODUCT_ID'] = (int)($params['PRESELECTED_PRODUCT_ID'] ?? 0);
		if ($params['PRESELECTED_PRODUCT_ID'] < 0)
		{
			$params['PRESELECTED_PRODUCT_ID'] = 0;
		}

		return $params;
	}

	/**
	 * @return void
	 */
	public function executeComponent(): void
	{
		$this->fillSettings();
		if ($this->isExistErrors())
		{
			$this->showErrors();

			return;
		}

		$this->loadData();

		$this->rows = $this->prepareRowsForAccessRights($this->rows);
		$this->prepareResult();

		$this->includeComponentTemplate();
	}

	/**
	 * @return array
	 */
	protected function listKeysSignedParameters()
	{
		return [
			// prepareEntityIds
			'GRID_ID',
			'NAVIGATION_ID',
			'FORM_ID',
			'TAB_ID',
			'AJAX_ID',
			'CATALOG_ID',
			// prepareAjaxOptions
			'AJAX_MODE',
			'AJAX_OPTION_JUMP',
			'AJAX_OPTION_HISTORY',
			'AJAX_LOADER',
			// preparePaginationOptions
			'SHOW_PAGINATION',
			'SHOW_TOTAL_COUNTER',
			'SHOW_PAGESIZE',
			// prepareSettings
			'CUSTOM_SITE_ID',
			'CUSTOM_LANGUAGE_ID',
			'CURRENCY',
			'SET_ITEMS',
			'ALLOW_EDIT',
			'ALLOW_ADD_PRODUCT',
			'ALLOW_CREATE_NEW_PRODUCT',
			'PREFIX',
			'ID',
			'PRODUCT_DATA_FIELD_NAME',
			// prepareEntitySettings
			'DOCUMENT_TYPE',
			'DOCUMENT_ID',
			'EXTERNAL_DOCUMENT',
		];
	}

	/**
	 * @return bool
	 */
	protected function isExistErrors(): bool
	{
		return !$this->errorCollection->isEmpty();
	}

	/**
	 * @return void
	 */
	protected function showErrors(): void
	{
		foreach ($this->getErrors() as $error)
		{
			\ShowError($error);
		}
	}

	/**
	 * @param string $message
	 * @return void
	 */
	protected function addErrorMessage(string $message): void
	{
		$this->errorCollection->setError(new Main\Error($message));
	}

	/**
	 * @param array &$params
	 * @return void
	 */
	protected function prepareEntityIds(array &$params): void
	{
		static::validateListParameters(
			$params,
			[
				'GRID_ID',
				'NAVIGATION_ID',
				'FORM_ID',
				'TAB_ID',
				'AJAX_ID',
			]
		);

		if (!empty($params['GRID_ID']))
		{
			if (empty($params['NAVIGATION_ID']))
			{
				$params['NAVIGATION_ID'] = static::createNavigationId($params['GRID_ID']);
			}
			if (!isset($params['FORM_ID']))
			{
				$params['FORM_ID'] = static::createFormId($params['GRID_ID']);
			}
		}
	}

	private function isAcceptableDocumentType($type): bool
	{
		$acceptableDocumentTypes = [
			StoreDocumentTable::TYPE_ARRIVAL,
			StoreDocumentTable::TYPE_STORE_ADJUSTMENT,
			StoreDocumentTable::TYPE_DEDUCT,
			StoreDocumentTable::TYPE_MOVING,
		];

		if (!empty($this->externalDocument['TYPE']))
		{
			$acceptableDocumentTypes[] = $this->externalDocument['TYPE'];
		}

		return in_array($type, $acceptableDocumentTypes, true);
	}

	/**
	 * @param array &$params
	 * @return void
	 */
	protected function prepareAjaxOptions(array &$params): void
	{
		$params['AJAX_MODE'] = isset($params['AJAX_MODE']) && $params['AJAX_MODE'] === 'N' ? 'N' : 'Y';
		$params['AJAX_OPTION_JUMP'] = isset($params['AJAX_OPTION_JUMP']) && $params['AJAX_OPTION_JUMP'] === 'Y' ? 'Y' : 'N';
		$params['AJAX_OPTION_HISTORY'] = isset($params['AJAX_OPTION_HISTORY']) && $params['AJAX_OPTION_HISTORY'] === 'Y' ? 'Y' : 'N';
		$params['AJAX_LOADER'] = $params['AJAX_LOADER'] ?? null;
	}

	/**
	 * @param array &$params
	 * @return void
	 */
	protected function preparePaginationOptions(array &$params): void
	{
		static::validateBoolList(
			$params,
			[
				'SHOW_PAGINATION',
				'SHOW_TOTAL_COUNTER',
				'SHOW_PAGESIZE',
			]
		);

		if (empty($params['PAGINATION']) || !is_array($params['PAGINATION']))
		{
			$params['PAGINATION'] = [];
		}
	}

	/**
	 * @param array &$params
	 * @return void
	 */
	protected function prepareProducts(array &$params): void
	{
		if (isset($params['PRODUCTS']) && !is_array($params['PRODUCTS']))
		{
			$params['PRODUCTS'] = null;
		}
	}

	/**
	 * @param array &$params
	 * @return void
	 */
	protected function prepareSettings(array &$params): void
	{
		$params['CURRENCY'] = isset($params['CURRENCY']) && is_string($params['CURRENCY'])
			? $params['CURRENCY']
			: ''
		;

		$params['SET_ITEMS'] = isset($params['SET_ITEMS']) && $params['SET_ITEMS'] === 'Y';
		$params['ALLOW_EDIT'] = isset($params['ALLOW_EDIT']) && $params['ALLOW_EDIT'] === 'Y';
		$params['ALLOW_ADD_PRODUCT'] = isset($params['ALLOW_ADD_PRODUCT']) && $params['ALLOW_ADD_PRODUCT'] === 'Y';
		$params['ALLOW_CREATE_NEW_PRODUCT'] = isset($params['ALLOW_CREATE_NEW_PRODUCT']) && $params['ALLOW_CREATE_NEW_PRODUCT'] === 'Y';
		$params['CALCULATE_STORE_PURCHASING_PRICE'] = ($params['CALCULATE_STORE_PURCHASING_PRICE'] ?? 'N') === 'Y';

		if (!$params['ALLOW_EDIT'])
		{
			$params['ALLOW_ADD_PRODUCT'] = false;
			$params['ALLOW_CREATE_NEW_PRODUCT'] = false;
		}

		$params['BUILDER_CONTEXT'] =
			isset($params['BUILDER_CONTEXT']) && is_string($params['BUILDER_CONTEXT'])
				? trim($params['BUILDER_CONTEXT'])
				: InventoryBuilder::TYPE_ID
		;
		$params['PREFIX'] = (isset($params['PREFIX']) && is_string($params['PREFIX']) ? trim($params['PREFIX']) : '');
		$params['ID'] = (isset($params['ID']) && is_string($params['ID']) ? trim($params['ID']) : '');
		$params['PRODUCT_DATA_FIELD_NAME'] = isset($params['PRODUCT_DATA_FIELD_NAME']) ? $params['PRODUCT_DATA_FIELD_NAME'] : 'PRODUCT_ROW_DATA';

		$params['EXTERNAL_DOCUMENT'] = $params['EXTERNAL_DOCUMENT'] ?? [];
		$this->externalDocument = $params['EXTERNAL_DOCUMENT'];
	}

	/**
	 * @param array &$params
	 * @return void
	 */
	protected function prepareEntitySettings(array &$params): void
	{
		if (empty($params['DOCUMENT_TYPE']) || !$this->isAcceptableDocumentType($params['DOCUMENT_TYPE']))
		{
			$this->addErrorMessage(Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_WRONG_DOCUMENT_TYPE'));
			return;
		}

		$params['DOCUMENT_ID'] = (isset($params['DOCUMENT_ID']) ? (int)$params['DOCUMENT_ID'] : 0);
		if ($params['DOCUMENT_ID'] < 0)
		{
			$this->addErrorMessage(Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_WRONG_DOCUMENT_ID'));
		}

		$params['CATALOG_ID'] = (isset($params['CATALOG_ID']) ? (int)$params['CATALOG_ID'] : 0);
		if ($params['CATALOG_ID'] <= 0)
		{
			$this->addErrorMessage(Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_WRONG_CATALOG_ID'));
		}
	}

	/**
	 * @return void
	 */
	protected function fillSettings(): void
	{
		$this->checkModules();
		$this->initDefaultSettings();
		$this->loadReferences();
		$this->initSettings();
	}

	/**
	 * @return void
	 */
	protected function initDefaultSettings(): void
	{
		$this->defaultSettings = [
			'GRID_ID' => $this->getDefaultGridId(),
		];
		$this->defaultSettings['NAVIGATION_ID'] = static::createNavigationId($this->defaultSettings['GRID_ID']);
		$this->defaultSettings['FORM_ID'] = static::createFormId($this->defaultSettings['GRID_ID']);
		$this->defaultSettings['TAB_ID'] = '';
		$this->defaultSettings['AJAX_ID'] = '';
		$this->defaultSettings['PAGE_SIZES'] = [5, 10, 20, 50, 100];
		$this->defaultSettings['PRICE_PRECISION'] = 2;
		$this->defaultSettings['AMOUNT_PRECISION'] = 4;
		$this->defaultSettings['COMMON_PRECISION'] = 2;
		$this->defaultSettings['CREATE_PRODUCT_PATH'] = $this->getElementDetailUrl($this->arParams['CATALOG_ID']);
		$this->defaultSettings['NEW_ROW_POSITION'] = CUserOptions::GetOption(
			'catalog.store.document.product.list',
			'new.row.position',
			'top'
		);

		$this->defaultSettings['BASE_PRICE_ID'] = GroupTable::getBasePriceTypeId();
	}

	protected function getDefaultSetting($name)
	{
		return $this->defaultSettings[$name] ?? null;
	}

	/**
	 * @return string
	 */
	public function getDefaultGridId(): string
	{
		$modePostfix = $this->isReadOnly() ? self::VIEW_MODE_GRID_ID_POSTFIX : self::EDIT_MODE_GRID_ID_POSTFIX;
		return self::clearStringValue(self::class) . '_' . $this->getDocumentType() . '_' . $modePostfix;
	}

	protected function getDocumentId(): int
	{
		return (int)$this->arParams['DOCUMENT_ID'];
	}

	protected function getDocumentType(): ?string
	{
		return $this->arParams['DOCUMENT_TYPE'];
	}

	/**
	 * @param string $gridId
	 * @return string
	 */
	protected static function createNavigationId(string $gridId): string
	{
		return $gridId . '_NAVIGATION';
	}

	/**
	 * @param string $gridId
	 * @return string
	 */
	protected static function createFormId(string $gridId): string
	{
		return 'form_' . $gridId;
	}

	/**
	 * @return void
	 */
	protected function initSettings(): void
	{
		$paramsList = [
			'GRID_ID',
			'NAVIGATION_ID',
			'PAGE_SIZES',
			'FORM_ID',
			'TAB_ID',
			'AJAX_ID',
			'NEW_ROW_POSITION',
			'PRICE_PRECISION',
			'AMOUNT_PRECISION',
			'COMMON_PRECISION',
			'CREATE_PRODUCT_PATH',
		];
		foreach ($paramsList as $param)
		{
			$value = !empty($this->arParams[$param]) ? $this->arParams[$param] : $this->defaultSettings[$param];
			$this->setStorageItem($param, $value);
		}

		$this->initGrid();
	}

	/**
	 * @return void
	 */
	protected function loadReferences(): void
	{
		$this->loadCurrency();
		$this->loadMeasures();
		$this->loadStores();
	}

	/**
	 * @return void
	 */
	protected function loadCurrency(): void
	{
		$this->currency['ID'] =
			!empty($this->arParams['CURRENCY'])
				? $this->arParams['CURRENCY']
				: CurrencyManager::getBaseCurrency()
		;

		$format = \CCurrencyLang::GetFormatDescription($this->currency['ID']);
		$this->currency['TEMPLATE'] = $format['FORMAT_STRING'] ?? '';
		$this->currency['TEXT'] =
			isset($this->currency['TEMPLATE'])
				? trim(\CCurrencyLang::applyTemplate('', $this->currency['TEMPLATE']))
				: ''
		;

		$this->currency['FORMAT'] = $format;
	}

	/**
	 * @return array
	 */
	protected function getCurrency(): array
	{
		return $this->currency;
	}

	/**
	 * @return string
	 */
	protected function getCurrencyId(): string
	{
		return $this->currency['ID'];
	}

	/**
	 * @return string
	 */
	protected function getDefaultTotalCalculationField(): string
	{
		if (!empty($this->externalDocument['TOTAL_CALCULATION_FIELD']))
		{
			return (string)$this->externalDocument['TOTAL_CALCULATION_FIELD'];
		}

		return 'PURCHASING_PRICE';
	}

	/**
	 * @return string
	 */
	protected function getCurrencyTemplate(): string
	{
		return $this->currency['TEMPLATE'];
	}

	/**
	 * @return string
	 */
	protected function getCurrencyText(): string
	{
		return $this->currency['TEXT'];
	}

	/**
	 * @return array
	 */
	protected function getCurrencyFormat(): array
	{
		return $this->currency['FORMAT'];
	}

	protected function loadStores(): void
	{
		$this->stores = [];
		$productStoreRaw = StoreTable::getList([
			'select' => ['ID', 'TITLE', 'IS_DEFAULT', 'ADDRESS']
		]);

		while ($store = $productStoreRaw->fetch())
		{
			if ($store['TITLE'] === '')
			{
				$store['TITLE'] = $store['ADDRESS'];
			}

			$this->stores[$store['ID']] = $store;
		}
	}

	protected function checkModules(): bool
	{
		if (!Loader::includeModule('catalog'))
		{
			$this->addErrorMessage('Module "catalog" is not installed.');

			return false;
		}

		if (!Loader::includeModule('iblock'))
		{
			$this->addErrorMessage('Module "iblock" is not installed.');

			return false;
		}

		if (!Loader::includeModule('currency'))
		{
			$this->addErrorMessage('Module "currency" is not installed.');

			return false;
		}

		if (!Loader::includeModule('sale'))
		{
			$this->addErrorMessage('Module "sale" is not installed.');

			return false;
		}

		return true;
	}

	/**
	 * @return void
	 */
	protected function initUiScope(): void
	{
		global $APPLICATION;

		Main\UI\Extension::load($this->getUiExtensions());

		foreach ($this->getUiStyles() as $styleList)
		{
			$APPLICATION->SetAdditionalCSS($styleList);
		}

		$scripts = $this->getUiScripts();
		if (!empty($scripts))
		{
			$asset = Main\Page\Asset::getInstance();
			foreach ($scripts as $row)
			{
				$asset->addJs($row);
			}
			unset($row, $asset);
		}
		unset($scripts);
	}

	/**
	 * @return void
	 */
	protected function loadData(): void
	{
		$this->rows = [];

		if (
			isset($this->arParams['REQUEST'], $this->arParams['~PRODUCTS'])
			&& is_array($this->arParams['~PRODUCTS'])
		)
		{
			$this->rows = $this->getProductRowsFromRequest();

			return;
		}

		if (!empty($this->arParams['PRODUCTS']))
		{
			$documentProducts = $this->arParams['PRODUCTS'];
		}
		elseif (!$this->externalDocument && $this->getDocumentId() > 0)
		{
			$documentProducts = $this->getDocumentProducts();
		}
		elseif ($this->arParams['PRESELECTED_PRODUCT_ID'])
		{
			$documentProducts = $this->getPreselectDocumentProducts();
		}
		elseif (!empty($this->arParams['REQUEST']) && !empty($this->externalDocument['INITIAL_PRODUCTS']))
		{
			$documentProducts = $this->externalDocument['INITIAL_PRODUCTS'];
		}

		if (empty($documentProducts))
		{
			return;
		}

		$productIds = array_column($documentProducts, 'ELEMENT_ID');
		$productIds = array_unique($productIds);

		if (empty($productIds))
		{
			return;
		}

		$productInfo = $this->loadCatalog($productIds);

		$restrictedProductTypes = ProductTable::getStoreDocumentRestrictedProductTypes();
		$productIdsWithoutRestrictedTypes = array_keys(array_filter(
			$productInfo,
			static fn($product): bool => !in_array($product['FIELDS']['TYPE'], $restrictedProductTypes, true)
		));

		$productStoreInfo = $this->getProductStoreInfo($productIdsWithoutRestrictedTypes);
		$barcodes = $this->getBarcodes($productIdsWithoutRestrictedTypes);

		foreach ($documentProducts as $id => $document)
		{
			$productId = (int)($document['ELEMENT_ID'] ?? null);

			if (isset($productInfo[$productId]))
			{
				$product = $productInfo[$productId]['FIELDS'];
				if ($productInfo[$productId]['SKU'] instanceof \Bitrix\Catalog\v2\Sku\BaseSku)
				{
					$skuImageField = new ImageInput($productInfo[$productId]['SKU']);
					$product['IMAGE_INFO'] = $skuImageField->getFormattedField();
				}
			}

			$productName = $product['NAME'] ?? '';
			$rowId = (int)($product['ID'] ?? 0);
			if (
				isset($product)
				&& $productName === ''
				&& is_numeric($product['ID'])
				&& $rowId > 0
			)
			{
				$productName = "[{$rowId}]";
			}


			if (!empty($document['BARCODE']))
			{
				$barcode = $document['BARCODE'];
			}
			else
			{
				$barcode = $barcodes[$productId] ?? '';
			}

			$existsStoreTo = isset($document['STORE_TO']) && (int)$document['STORE_TO'] > 0;
			$existsStoreFrom = isset($document['STORE_FROM']) && (int)$document['STORE_FROM'] > 0;

			$availableAmountTo = 0;
			if ($productId && $existsStoreTo)
			{
				$availableAmountTo = $this->getAvailableProductAmountOnStore($productStoreInfo, $productId, $document['STORE_TO']);
			}

			$availableAmountFrom = 0;
			if ($productId && $existsStoreFrom)
			{
				$availableAmountFrom = $this->getAvailableProductAmountOnStore($productStoreInfo, $productId, $document['STORE_FROM']);
			}

			$amount = (float)$document['AMOUNT'];
			$basePrice = $document['BASE_PRICE'] ?? null;
			$taxRate = $document['TAX_RATE'] ?? null;
			$taxIncluded = $document['TAX_INCLUDED'] ?? 'N';
			$taxSum = 0;

			if ($taxRate && $basePrice)
			{
				$calculator = new VatCalculator($taxRate / 100);
				$tax = $calculator->calc(
					$basePrice,
					$taxIncluded === 'Y',
					false
				);

				$taxSum = PriceMaths::roundPrecision($tax * $amount);
			}

			$calculatedPrice = (float)($document[$this->getDefaultTotalCalculationField()] ?? 0.0);
			$totalPrice = $amount * $calculatedPrice;

			$additionalData = [
				'ROW_ID' => $this->getRowIdPrefix($document['ID']),
				'BARCODE' => $barcode,
				'DOC_BARCODE' => $barcode,
				'STORE_TO_AVAILABLE_AMOUNT' => $availableAmountTo,
				'STORE_FROM_AVAILABLE_AMOUNT' => $availableAmountFrom,
				'STORE_AMOUNT_MAP' => $productStoreInfo[$productId] ?? null,
				'IBLOCK_ID' => $product['IBLOCK_ID'] ?? $this->arParams['IBLOCK_ID'],
				'BASE_PRICE_ID' => $product['BASE_PRICE_ID'] ?? $this->getStorageItem('BASE_PRICE_ID'),
				'PARENT_PRODUCT_ID' => $product['PARENT_PRODUCT_ID'] ?? null,
				'OFFERS_IBLOCK_ID' => $product['OFFERS_IBLOCK_ID'] ?? null,
				'SKU_ID' => $product['SKU_ID'] ?? null,
				'PRODUCT_ID' => $product['PRODUCT_ID'] ?? null,
				'SKU_TREE' => !empty($product['SKU_TREE']) ? Json::encode($product['SKU_TREE']) : null,
				'DETAIL_URL' => $product['DETAIL_URL'] ?? null,
				'IMAGE_INFO' => $product['IMAGE_INFO'] ?? null,
				'MEASURE_NAME' => $product['MEASURE_NAME'] ?? null,
				'MEASURE_CODE' => $product['MEASURE_CODE'] ?? null,
				'NAME' => $productName,
				'BASE_PRICE' => $basePrice,
				'PURCHASING_PRICE' => $document['PURCHASING_PRICE'] ?? 0,
				'TOTAL_PRICE' => $totalPrice,
				'BASKET_ID' => $document['BASKET_ID'] ?? 0,
				'TYPE' => $product['TYPE'] ?? null,
				'TAX_SUM' => $taxSum,
				'TAX_RATE' => $taxRate,
				'TAX_INCLUDED' => $taxIncluded,
			];

			if ($existsStoreTo)
			{
				$additionalData['STORE_TO_TITLE'] = $this->stores[$document['STORE_TO']]['TITLE'] ?? '';
				$additionalData['STORE_TO_AMOUNT'] = $productStoreInfo[$productId][$document['STORE_TO']]['AMOUNT'] ?? '';
				$additionalData['STORE_TO_RESERVED'] = $productStoreInfo[$productId][$document['STORE_TO']]['QUANTITY_RESERVED'] ?? '';
			}
			else
			{
				$additionalData['STORE_TO_TITLE'] = '';
				$additionalData['STORE_TO_AMOUNT'] = '';
				$additionalData['STORE_TO_RESERVED'] = '';
			}

			if ($existsStoreFrom)
			{
				$additionalData['STORE_FROM_TITLE'] = $this->stores[$document['STORE_FROM']]['TITLE'] ?? '';
				$additionalData['STORE_FROM_AMOUNT'] = $productStoreInfo[$productId][$document['STORE_FROM']]['AMOUNT'] ?? '';
				$additionalData['STORE_FROM_RESERVED'] = $productStoreInfo[$productId][$document['STORE_FROM']]['QUANTITY_RESERVED'] ?? '';
			}
			else
			{
				$additionalData['STORE_FROM_TITLE'] = '';
				$additionalData['STORE_FROM_AMOUNT'] = '';
				$additionalData['STORE_FROM_RESERVED'] = '';
			}

			$documentProducts[$id] = array_merge($document, $additionalData);
		}

		$this->rows = $documentProducts;
	}

	/**
	 * Updating rows based on access rights.
	 *
	 * @return void
	 */
	private function prepareRowsForAccessRights(array $rows): array
	{
		$accessibleStoresIds = $this->getAccessibleStoresIds();
		$hiddenFields = $this->getHiddenFieldsWithoutAccess();

		$notHasAccessToPurchasingPrice = !AccessController::getCurrent()->check(ActionDictionary::ACTION_PRODUCT_PURCHASE_INFO_VIEW);

		foreach ($rows as &$row)
		{
			$hasAccess = true;

			$storeTo = (int)($row['STORE_TO'] ?? 0);
			$storeFrom = (int)($row['STORE_FROM'] ?? 0);

			if ($storeTo && $storeFrom)
			{
				$hasAccess =
					in_array($storeTo, $accessibleStoresIds, true)
					&& in_array($storeFrom, $accessibleStoresIds, true)
				;
			}
			elseif ($storeTo)
			{
				$hasAccess = in_array($storeTo, $accessibleStoresIds, true);
			}
			elseif ($storeFrom)
			{
				$hasAccess = in_array($storeFrom, $accessibleStoresIds, true);
			}

			$realValues = null;
			if (!$hasAccess)
			{
				$realValues = [];
				foreach ($hiddenFields as $fieldName)
				{
					if (isset($row[$fieldName]))
					{
						$realValues[$fieldName] = $row[$fieldName];
						$row[$fieldName] = null;
					}
				}

				$row['REAL_VALUES'] = base64_encode(Json::encode($realValues));
			}

			$row['ACCESS_DENIED'] = !$hasAccess;
			$row['ACCESS_DENIED_TO_PURCHASING_PRICE'] = $notHasAccessToPurchasingPrice;
		}

		return $rows;
	}

	/**
	 * Fields that are hidden when there is no access.
	 *
	 * @return array
	 */
	private function getHiddenFieldsWithoutAccess(): array
	{
		return [
			'STORE_TO',
			'STORE_TO_INFO',
			'STORE_TO_TITLE',
			'STORE_TO_AMOUNT',
			'STORE_TO_RESERVED',
			'STORE_TO_AVAILABLE_AMOUNT',
			'STORE_FROM',
			'STORE_FROM_INFO',
			'STORE_FROM_TITLE',
			'STORE_FROM_AMOUNT',
			'STORE_FROM_RESERVED',
			'STORE_FROM_AVAILABLE_AMOUNT',
			'PURCHASING_PRICE',
			'BASE_PRICE',
			'TOTAL_PRICE',
			'AMOUNT',
		];
	}

	protected function getProductRowsFromRequest(): array
	{
		$rows = $this->arParams['~PRODUCTS'];
		$rows = array_filter($rows);

		$actionButton = $this->arParams['REQUEST']['action_button_' . $this->getGridId()] ?? null;
		$actionAllRows = $this->arParams['REQUEST']['action_all_rows_' . $this->getGridId()] ?? null;

		if (
			$actionButton === 'delete'
			&& $actionAllRows === 'Y'
		)
		{
			return [];
		}

		$skuTreeItems = $this->getSkuTreeItems($rows);

		foreach ($rows as $index => $row)
		{
			if (
				$actionButton === 'delete'
				&& is_array($this->arParams['REQUEST']['ID'])
				&& in_array($row['ID'], $this->arParams['REQUEST']['ID'], true)
			)
			{
				unset($rows[$index]);
				continue;
			}

			if (!isset($row['ID']))
			{
				$rows[$index]['ID'] = $this->getNewRowId();
			}

			$intFields = [
				'IBLOCK_ID',
				'DOCUMENT_ID',
				'PARENT_PRODUCT_ID',
				'PRODUCT_ID',
				'OFFERS_IBLOCK_ID',
				'SKU_ID',
				'DISCOUNT_TYPE_ID',
				'SORT',
			];
			foreach ($intFields as $name)
			{
				if (isset($rows[$index][$name]))
				{
					$rows[$index][$name] = (int)$rows[$index][$name];
				}
			}

			$floatFields = [
				'AMOUNT',
				'PURCHASING_PRICE',
				'TOTAL_PRICE',
			];

			if ($rows[$index]['BASE_PRICE'] === '' || $rows[$index]['BASE_PRICE'] === null)
			{
				$rows[$index]['BASE_PRICE'] = null;
			}
			else
			{
				$floatFields[] = 'BASE_PRICE';
			}

			foreach ($floatFields as $name)
			{
				if (isset($rows[$index][$name]))
				{
					$rows[$index][$name] = (float)$rows[$index][$name];
				}
			}

			$nullFloatFields = [
				'TAX_RATE',
			];
			foreach ($nullFloatFields as $name)
			{
				if (isset($rows[$index][$name]))
				{
					$value = trim((string)$rows[$index][$name]);
					$rows[$index][$name] =
						$value === ''
							? null
							: (float)$value
					;
				}
			}

			if ($row["SKU_ID"] > 0)
			{
				$sku = $this->getSkuByProductId($row["SKU_ID"]);
				if ($sku)
				{
					$skuImageField = new ImageInput($sku);
					$rows[$index]['IMAGE_INFO'] = $skuImageField->getFormattedField();

					$skuTree = $skuTreeItems[$row['IBLOCK_ID']][$row['PRODUCT_ID']][$row['SKU_ID']] ?? null;
					$rows[$index]['SKU_TREE'] = $skuTree ? Json::encode($skuTree) : null;
				}
			}
		}

		return $rows;
	}

	private function getSkuTreeItems(array $rows): array
	{
		$iblockProductOfferIds = [];
		foreach ($rows as $row)
		{
			if (empty($row['SKU_ID']))
			{
				continue;
			}

			$iblockProductOfferIds[$row['IBLOCK_ID']][$row['PRODUCT_ID']][] = (int)$row['SKU_ID'];
		}
		$skuTreeItems = [];
		foreach ($iblockProductOfferIds as $iblockId => $productOfferIds)
		{
			$skuTree = \Bitrix\Catalog\v2\IoC\ServiceContainer::make('sku.tree', ['iblockId' => $iblockId]);
			if (!$skuTree)
			{
				continue;
			}

			$skuTreeItems[$iblockId] = $skuTree->loadJsonOffers($productOfferIds);
		}

		return $skuTreeItems;
	}

	protected function getDocumentProducts(): array
	{
		$products = [];

		$documentProductRaw = StoreDocumentElementTable::getList([
			'filter' => [
				'=DOC_ID' => $this->getDocumentId(),
			],
		]);

		while($documentProduct = $documentProductRaw->fetch())
		{
			$documentProduct['BARCODE'] = '';
			$products[$documentProduct['ID']] = $documentProduct;
		}

		$rowBarcodesRaw = StoreDocumentBarcodeTable::getList([
			'select' => ['DOC_ELEMENT_ID', 'BARCODE'],
			'filter' => ['=DOC_ID' => $this->getDocumentId()]
		]);
		while ($barcode = $rowBarcodesRaw->fetch())
		{
			$rowId = $barcode['DOC_ELEMENT_ID'];
			$products[$rowId]['BARCODE'] = $barcode['BARCODE'];
		}

		return $products;
	}

	protected function getProductStoreInfo(array $productIds): array
	{
		$productStoreInfo = [];
		$productStoreRaw = StoreProductTable::getList([
			'filter' => ['=PRODUCT_ID' => $productIds],
			'select' => [
				'STORE_ID',
				'PRODUCT_ID',
				'AMOUNT',
				'QUANTITY_RESERVED',
				'STORE_TITLE' => 'STORE.TITLE'
			]
		]);

		while ($productStore = $productStoreRaw->Fetch())
		{
			$productStoreInfo[$productStore['PRODUCT_ID']] = $productStoreInfo[$productStore['PRODUCT_ID']] ?? [];
			$productStoreInfo[$productStore['PRODUCT_ID']][$productStore['STORE_ID']] = $productStore;
		}

		return $productStoreInfo;
	}

	protected function getBarcodes(array $productIds): array
	{
		$barcodes = [];
		$barcodeRaw = StoreBarcodeTable::getList([
			'filter' => [
				'PRODUCT_ID' => $productIds,
			]
		]);

		while ($barcode = $barcodeRaw->fetch())
		{
			$barcodes[$barcode['PRODUCT_ID']] = $barcode['BARCODE'];
		}

		return $barcodes;
	}

	/**
	 * @return void
	 */
	protected function prepareResult(): void
	{
		$this->initUiScope();

		$this->arResult['ID'] = $this->getGridId();
		$gridRows = $this->getGridRows();
		$this->arResult['GRID'] = $this->getGridParams($gridRows);
		$this->arResult['GRID_EDITOR_CONFIG'] = $this->getGridEditorConfig($gridRows);
		$this->arResult['SETTINGS'] = $this->getSettings();
		$this->arResult['HIDDEN_FIELDS'] = $this->getHiddenFieldsWithoutAccess();
		$this->arResult['IS_EXTERNAL_CATALOG'] = State::isExternalCatalog();
		$this->arResult += $this->getTotalSumDetails($gridRows);
	}

	protected function getGridParams(array $gridRows): array
	{
		return [
			'GRID_ID' => $this->getGridId(),
			'HEADERS' => array_values($this->getColumns()),
			'SORT' => $this->getStorageItem('GRID_ORDER'),
			'SORT_VARS' => $this->getStorageItem('GRID_ORDER_VARS'),
			'ROWS' => $gridRows,

			'SHOW_ROW_ACTIONS_MENU' => true,
			'ALLOW_SORT' => false,
			'ALLOW_ROWS_SORT' => false,
			'ALLOW_ROWS_SORT_IN_EDIT_MODE' => false,
			'ALLOW_ROWS_SORT_INSTANT_SAVE' => false,
			'ENABLE_ROW_COUNT_LOADER' => false,
			'HIDE_FILTER' => true,
			'ENABLE_COLLAPSIBLE_ROWS' => false,
			'ADVANCED_EDIT_MODE' => true,
			'ALLOW_EDIT_SELECTION' => true,
			'NAME_TEMPLATE' => (string)($arParams['~NAME_TEMPLATE'] ?? ''),
			'SHOW_ACTION_PANEL' => true,
			// 'SETTINGS_WINDOW_TITLE' => $arResult['ENTITY']['TITLE'],

			'SHOW_NAVIGATION_PANEL' => false,
			'SHOW_PAGINATION' => false,
			'SHOW_TOTAL_COUNTER' => false,
			'SHOW_PAGESIZE' => false,
			'PAGINATION' => [],
			'NAV_OBJECT' => $this->navigation,
			'~NAV_PARAMS' => ['SHOW_ALWAYS' => false],
			'SHOW_ROW_CHECKBOXES' => true,
			'USE_CHECKBOX_LIST_FOR_SETTINGS_POPUP' => \Bitrix\Main\ModuleManager::isModuleInstalled('ui'),
			'ENABLE_FIELDS_SEARCH' => 'Y',
			'CONFIG' => [
				'popupWidth' => 800,
			],

			'SHOW_SELECTED_COUNTER' => true,
			'ACTION_PANEL' => $this->getGridActionPanel(),

			// checked
			'VISIBLE_COLUMNS' => array_values($this->getVisibleColumns()),
			'AJAX_ID' => $this->getStorageItem( 'AJAX_ID'),
			'AJAX_MODE' => $this->arParams['~AJAX_MODE'],
			'AJAX_OPTION_JUMP' => $this->arParams['~AJAX_OPTION_JUMP'],
			'AJAX_OPTION_HISTORY' => $this->arParams['~AJAX_OPTION_HISTORY'],
			'AJAX_LOADER' => $this->arParams['~AJAX_LOADER'],
			'FORM_ID' => $this->getStorageItem('FORM_ID'),
			'TAB_ID' => $this->getStorageItem('TAB_ID'),

			'TOTAL_ROWS_COUNT' => $this->arParams['~TOTAL_PRODUCTS_COUNT'] ?? count($gridRows),
		];
	}

	protected function getGridActionPanel(): array
	{
		if ($this->isReadOnly())
		{
			return [];
		}

		$snippet = new Snippet();

		$dropdownStores = [];
		foreach ($this->getAccessibleStores() as $store)
		{
			$dropdownStores[] = ['NAME' => $store['TITLE'], 'VALUE' => (int)$store['ID']];
		}

		$actionList = [];
		if ($dropdownStores)
		{
			$items = [
				[
					'NAME' => Loc::getMessage('CATALOG_DOCUMENT_ACTION_DEFAULT'),
					'VALUE' => 'default',
					'ACTION' => Main\Grid\Panel\Actions::RESET_CONTROLS,
				],
			];

			$isExternalDocument = (bool)($this->externalDocument['TYPE'] ?? false);
			if (
				$isExternalDocument
				|| $this->getDocumentType() === StoreDocumentTable::TYPE_MOVING
				|| $this->getDocumentType() === StoreDocumentTable::TYPE_DEDUCT
				|| $this->getDocumentType() === StoreDocumentTable::TYPE_SALES_ORDERS
			)
			{
				$storeFromActionTitle =
					$this->getDocumentType() === StoreDocumentTable::TYPE_MOVING
						? Loc::getMessage('CATALOG_DOCUMENT_ACTION_SELECT_STORE_FROM')
						: Loc::getMessage('CATALOG_DOCUMENT_ACTION_SELECT_STORE')
				;

				$items[] = $this->getDropdownActionField(
					$snippet,
					'STORE_FROM_INFO',
					$dropdownStores,
					$storeFromActionTitle
				);
			}

			if (
				!$isExternalDocument
				&& $this->getDocumentType() !== StoreDocumentTable::TYPE_DEDUCT
				&& $this->getDocumentType() !== StoreDocumentTable::TYPE_SALES_ORDERS
			)
			{
				$storeToActionTitle =
					$this->getDocumentType() === StoreDocumentTable::TYPE_MOVING
						? Loc::getMessage('CATALOG_DOCUMENT_ACTION_SELECT_STORE_TO')
						: Loc::getMessage('CATALOG_DOCUMENT_ACTION_SELECT_STORE')
				;

				$items[] = $this->getDropdownActionField(
					$snippet,
					'STORE_TO_INFO',
					$dropdownStores,
					$storeToActionTitle
				);
			}

			$actionList = [
				'TYPE' => Main\Grid\Panel\Types::DROPDOWN,
				'ID' => 'actionListId',
				'NAME' => 'actionList',
				'ITEMS' => $items
			];
		}

		return [
			'GROUPS' => [
				[
					'ITEMS' => [
						$snippet->getRemoveButton(),
						$actionList,
						$snippet->getForAllCheckbox(),
					],
				],
			],
		];
	}

	/**
	 * @param Snippet $snippet
	 * @param string $fieldId
	 * @param array $list
	 * @param string $title
	 * @return array
	 */
	private function getDropdownActionField(Snippet $snippet, string $fieldId, array $list, string $title): array
	{
		$action = [
			'ACTION' => Main\Grid\Panel\Actions::CREATE,
			'DATA' => [
				[
					'TYPE' => Main\Grid\Panel\Types::DROPDOWN,
					'ID' => $fieldId,
					'NAME' => $fieldId,
					'ITEMS' => $list,
				],
				$snippet->getApplyButton([
					'ONCHANGE' => [
						[
							'ACTION' => \Bitrix\Main\Grid\Panel\Actions::CALLBACK,
							'DATA' => [
								[
									"JS" => "BX.Catalog.Store.ProductList.Instance.processApplyActionButtonClick('{$fieldId}')",
								]
							]
						]
					]
				]),
			]
		];

		return [
			'NAME' => $title,
			'VALUE' => $fieldId,
			'ONCHANGE' => [$action]
		];
	}

	/**
	 * @return array
	 */
	protected function getUiExtensions(): array
	{
		return [
			'core',
			'ajax',
			'tooltip',
			'ui.fonts.ruble',
			'ui.notification',
			'catalog.product-calculator',
			'catalog.product-selector',
			'catalog.store-selector',
			'catalog.tool-availability-manager',
			'currency',
		];
	}

	/**
	 * @return array
	 */
	protected function getUiStyles(): array
	{
		return [];
	}

	/**
	 * @return array
	 */
	protected function getUiScripts(): array
	{
		return [];
	}

	/**
	 * @return array
	 */
	protected function getSettings(): array
	{
		return [
			'SITE_ID' => $this->getSiteId(),
			'LANGUAGE_ID' => $this->getLanguageId(),
			'SET_ITEMS' => $this->arParams['SET_ITEMS'],
			'ALLOW_EDIT' => $this->arParams['ALLOW_EDIT'],
			'IS_READ_ONLY' => $this->isReadOnly(),
			'IS_DISPLAY_TOTAL_SUM_DETAILS' => $this->arParams['IS_DISPLAY_TOTAL_SUM_DETAILS'] ?? false,
			'CURRENCY' => $this->getCurrency(),
			'NEW_ROW_ID_PREFIX' => self::NEW_ROW_ID_PREFIX,
			'NEW_ROW_ID_COUNTER' => $this->getNewRowCounter(),
			'NEW_ROW_POSITION' => $this->getStorageItem( 'NEW_ROW_POSITION'),
			'CREATE_PRODUCT_PATH' => $this->getStorageItem( 'CREATE_PRODUCT_PATH'),
			'TOTAL_SUM_CONTAINER_ID' => $this->getPrefix() . '_product_sum_total_container',
		];
	}

	/* Storage tools */

	/**
	 * @param array $nodeValues
	 * @return void
	 */
	protected function fillStorageNode(array $nodeValues): void
	{
		if (empty($nodeValues))
		{
			return;
		}

		$this->storage = array_merge($this->storage, $nodeValues);
	}

	/**
	 * @param string $item
	 * @param mixed $value
	 * @return void
	 */
	protected function setStorageItem(string $item, $value): void
	{
		$this->storage[$item] = $value;
	}

	/**
	 * @param string $item
	 * @return mixed|null
	 */
	protected function getStorageItem(string $item)
	{
		return $this->storage[$item] ?? null;
	}

	/**
	 * @return string
	 */
	protected function getGridId(): ?string
	{
		return $this->getStorageItem('GRID_ID');
	}

	/**
	 * @return string
	 */
	protected function getFormId(): ?string
	{
		return $this->getStorageItem('FORM_ID');
	}

	/**
	 * @return string
	 */
	protected function getNavigationId(): string
	{
		return $this->getStorageItem('NAVIGATION_ID');
	}

	/**
	 * @return array
	 */
	protected function getPageSizes(): array
	{
		return $this->getStorageItem('PAGE_SIZES');
	}

	/* Storage tools finish */

	/**
	 * @return void
	 */
	protected function initGrid(): void
	{
		$this->initGridConfig();
		$this->initGridColumns();
		$this->initGridPageNavigation();
		$this->initGridOrder();
	}

	/**
	 * @return void
	 */
	protected function initGridConfig(): void
	{
		$this->gridConfig = new Main\Grid\Options($this->getGridId());
	}

	/**
	 * @return void
	 */
	protected function initGridColumns(): void
	{
		$visibleColumns = [];
		$visibleColumnsMap = [];

		$defaultList = true;
		$userColumnsOrder = [];
		$userColumns = $this->getUserGridColumnIds();
		if (!empty($userColumns))
		{
			$defaultList = false;
			$userColumnsOrder = array_fill_keys($userColumns, true);
		}

		$defaultColumnsOrder = $this->getDefaultColumns();

		$columnDescriptions = $this->getGridColumnsDescription();
		if ($defaultList)
		{
			$userColumnsOrder = array_filter(
				$defaultColumnsOrder,
				static function($columnName) use ($columnDescriptions)
				{
					return
						isset($columnDescriptions[$columnName]['default'])
						&& $columnDescriptions[$columnName]['default'] === true
					;
				}
			);
		}

		foreach ($userColumnsOrder as $key => $index)
		{
			if (!isset($columnDescriptions[$key]))
			{
				continue;
			}
			$visibleColumnsMap[$key] = true;
			$visibleColumns[$key] = $columnDescriptions[$key];
		}

		$columns = [];
		foreach ($defaultColumnsOrder as $columnCode)
		{
			if (isset($columnDescriptions[$columnCode]))
			{
				$columns[] = $columnDescriptions[$columnCode];
			}
		}

		$this->fillStorageNode( [
			'COLUMNS' => $columns,
			'VISIBLE_COLUMNS' => $visibleColumns,
			'VISIBLE_COLUMNS_MAP' => $visibleColumnsMap,
		]);
	}

	/**
	 * @return void
	 */
	protected function initGridPageNavigation(): void
	{
		$naviParams = $this->getGridNavigationParams();
		$this->navigation = new Main\UI\PageNavigation($this->getNavigationId());
		$this->navigation->setPageSizes($this->getPageSizes());
		$this->navigation->allowAllRecords(false);
		$this->navigation->setPageSize($naviParams['nPageSize']);

		//		if (!$this->isUsedImplicitPageNavigation())
		//		{
		$this->navigation->initFromUri();
		//		}
	}

	/**
	 * @return array
	 */
	protected function getGridNavigationParams(): array
	{
		return $this->gridConfig->getNavParams(['nPageSize' => 20]);
	}

	/**
	 * @return void
	 */
	protected function initGridOrder(): void
	{
		$result = ['ID' => 'DESC'];

		$sorting = $this->gridConfig->getSorting(['sort' => $result]);

		$order = strtolower(reset($sorting['sort']));
		if ($order !== 'asc')
		{
			$order = 'desc';
		}

		$field = key($sorting['sort']);
		$found = false;

		foreach ($this->getVisibleColumns() as $column)
		{
			if (!isset($column['sort']))
				continue;
			if ($column['sort'] == $field)
			{
				$found = true;
				break;
			}
		}
		unset($column);

		if ($found)
			$result = [$field => $order];

		$this->fillStorageNode(
			[
				'GRID_ORDER' => $this->modifyGridOrder($result),
				'GRID_ORDER_VARS' => $sorting['vars'],
			]
		);

		unset($found, $field, $order, $sorting, $result);
	}

	/**
	 * @param array $order
	 * @return array
	 */
	protected function modifyGridOrder(array $order): array
	{
		return $order;
	}

	protected function getCurrencyListForMoneyField(): array
	{
		return [
			$this->getCurrencyId() => $this->getCurrencyText(),
		];
	}

	protected function getDefaultColumns(): array
	{
		if (!empty($this->externalDocument['DEFAULT_COLUMNS']))
		{
			return $this->externalDocument['DEFAULT_COLUMNS'];
		}

		switch ($this->getDocumentType())
		{
			case StoreDocumentTable::TYPE_STORE_ADJUSTMENT:
			case StoreDocumentTable::TYPE_ARRIVAL:
				if ($this->isReadOnly())
				{
					return [
						'MAIN_INFO','PURCHASING_PRICE', 'BASE_PRICE',
						'AMOUNT', 'STORE_TO_INFO', 'STORE_TO_AMOUNT', 'BARCODE_INFO',
						'TOTAL_PRICE', 'COMMENT',
					];
				}

				return [
					'MAIN_INFO', 'BARCODE_INFO', 'PURCHASING_PRICE', 'BASE_PRICE',
					'AMOUNT', 'STORE_TO_INFO', 'STORE_TO_AMOUNT',
					'TOTAL_PRICE', 'COMMENT',
				];
			case StoreDocumentTable::TYPE_DEDUCT:
				if ($this->isReadOnly())
				{
					return [
						'MAIN_INFO',
						'STORE_FROM_INFO', 'STORE_FROM_AMOUNT', 'AMOUNT',
						'PURCHASING_PRICE', 'BASE_PRICE', 'BARCODE_INFO',
						'TOTAL_PRICE', 'COMMENT',
					];
				}

				return [
					'MAIN_INFO', 'BARCODE_INFO', 'AMOUNT',
					'STORE_FROM_INFO', 'STORE_FROM_AMOUNT',
					'PURCHASING_PRICE', 'BASE_PRICE',
					'TOTAL_PRICE', 'COMMENT',
				];
			case StoreDocumentTable::TYPE_MOVING:
				if ($this->isReadOnly())
				{
					return [
						'MAIN_INFO',
						'STORE_FROM_INFO', 'STORE_FROM_AVAILABLE_AMOUNT', 'STORE_FROM_AMOUNT',
						'STORE_TO_INFO', 'STORE_TO_AVAILABLE_AMOUNT', 'STORE_TO_AMOUNT', 'AMOUNT',
						'PURCHASING_PRICE', 'BASE_PRICE', 'BARCODE_INFO',
						'TOTAL_PRICE', 'COMMENT',
					];
				}

				return [
					'MAIN_INFO', 'BARCODE_INFO', 'AMOUNT',
					'STORE_FROM_INFO', 'STORE_FROM_AVAILABLE_AMOUNT', 'STORE_FROM_AMOUNT',
					'STORE_TO_INFO', 'STORE_TO_AVAILABLE_AMOUNT', 'STORE_TO_AMOUNT',
					'PURCHASING_PRICE', 'BASE_PRICE',
					'TOTAL_PRICE', 'COMMENT',
				];
		}

		return [];
	}

	/**
	 * @return array
	 */
	protected function getGridColumnsDescription(): array
	{
		$result = [];
		$columnDefaultWidth = 150;

		$result['MAIN_INFO'] = [
			'id' => 'MAIN_INFO',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_MAIN_INFO'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_MAIN_INFO'),
			'sort' => 'NAME',
			'default' => true,
		];

		if (State::isExternalCatalog() !== true)
		{
			$result['BARCODE_INFO'] = [
				'id' => 'BARCODE_INFO',
				'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_BARCODE'),
				'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_BARCODE'),
				'default' => true,
				'width' => 300,
			];
		}

		$priceEditable = [
			'TYPE' => Types::MONEY,
			'CURRENCY_LIST' => $this->getCurrencyListForMoneyField(),
			'PLACEHOLDER' => '0',
			'HTML_ENTITY' => true,
		];

		$purchasingPriceName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_PURCHASING_PRICE');
		$purchasingPriceName = $this->externalDocument['CUSTOM_COLUMN_NAMES']['PURCHASING_PRICE'] ?? $purchasingPriceName;
		$purchasingPriceEditable =
			$this->accessController->check(ActionDictionary::ACTION_PRODUCT_PURCHASE_INFO_VIEW)
			&& !(
				$this->getDocumentType() === StoreDocumentTable::TYPE_MOVING
				|| $this->getDocumentType() === StoreDocumentTable::TYPE_DEDUCT
				|| $this->getDocumentType() === StoreDocumentTable::TYPE_SALES_ORDERS
			)
		;

		if (
			$this->getDocumentType() !== StoreDocumentTable::TYPE_MOVING
			&& (
				$this->getDocumentType() !== StoreDocumentTable::TYPE_SALES_ORDERS
				|| (Feature::isStoreBatchEnabled() && State::isProductBatchMethodSelected())
			)
		)
		{
			$result['PURCHASING_PRICE'] = [
				'id' => 'PURCHASING_PRICE',
				'name' => $purchasingPriceName,
				'title' => $purchasingPriceName,
				'sort' => 'PURCHASING_PRICE',
				'default' => true,
				'editable' => $purchasingPriceEditable ? $priceEditable : false,
				'width' => $columnDefaultWidth,
			];
		}

		$result['BASE_PRICE'] = [
			'id' => 'BASE_PRICE',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_PRICE'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_PRICE'),
			'sort' => 'BASE_PRICE',
			'default' => true,
			'editable' => $this->isEditableBasePrice() ? $priceEditable : false,
			'width' => $columnDefaultWidth,
		];

		$result['TAX_RATE'] = [
			'id' => 'TAX_RATE',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_TAX_RATE'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_TAX_RATE'),
			'default' => true,
		];

		$result['TAX_INCLUDED'] = [
			'id' => 'TAX_INCLUDED',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_TAX_INCLUDED'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_TAX_INCLUDED'),
			'default' => true,
			'width' => 180,
		];

		$storeFromName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_FROM_INFO');
		$storeFromAmountName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_FROM_AMOUNT');
		if ($this->getDocumentType() === StoreDocumentTable::TYPE_MOVING)
		{
			$storeFromName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_FROM_INFO_MOVING');
			$storeFromAmountName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_FROM_AMOUNT_MOVING');
		}
		elseif ($this->getDocumentType() === StoreDocumentTable::TYPE_DEDUCT)
		{
			$storeFromName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_INFO');
			$storeFromAmountName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_AMOUNT');
		}

		$storeFromName = $this->externalDocument['CUSTOM_COLUMN_NAMES']['STORE_FROM_INFO'] ?? $storeFromName;
		$result['STORE_FROM_INFO'] = [
			'id' => 'STORE_FROM_INFO',
			'name' => $storeFromName,
			'title' => $storeFromName,
			'sort' => 'STORE_FROM',
			'default' => true,
		];

		$storeFromAmountName = $this->externalDocument['CUSTOM_COLUMN_NAMES']['STORE_FROM_AMOUNT'] ?? $storeFromAmountName;
		$result['STORE_FROM_AMOUNT'] = [
			'id' => 'STORE_FROM_AMOUNT',
			'name' => $storeFromAmountName,
			'title' => $storeFromAmountName,
			'sort' => 'STORE_FROM_AMOUNT',
			'default' => !$this->isReadOnly(),
			'editable' => false,
			'width' => $columnDefaultWidth,
		];

		$result['STORE_FROM_RESERVED'] = [
			'id' => 'STORE_FROM_RESERVED',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_FROM_AMOUNT_RESERVED'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_FROM_AMOUNT_RESERVED'),
			'sort' => 'STORE_FROM_RESERVED',
			'default' => true,
			'editable' => false,
			'width' => $columnDefaultWidth,
		];

		$storeFromCommonAmountName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_FROM_AMOUNT_AVAILABLE');
		$storeFromCommonAmountName = $this->externalDocument['CUSTOM_COLUMN_NAMES']['STORE_FROM_AVAILABLE_AMOUNT'] ?? $storeFromCommonAmountName;

		$result['STORE_FROM_AVAILABLE_AMOUNT'] = [
			'id' => 'STORE_FROM_AVAILABLE_AMOUNT',
			'name' => $storeFromCommonAmountName,
			'title' => $storeFromCommonAmountName,
			'sort' => 'STORE_FROM_AVAILABLE_AMOUNT',
			'default' => !$this->isReadOnly(),
			'editable' => false,
			'width' => $columnDefaultWidth,
		];

		$storeToInfoName = $this->getDocumentType() === StoreDocumentTable::TYPE_MOVING
			? Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_TO_INFO')
			: Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_INFO')
		;

		$result['STORE_TO_INFO'] = [
			'id' => 'STORE_TO_INFO',
			'name' =>$storeToInfoName,
			'title' =>$storeToInfoName,
			'sort' => 'STORE_TO',
			'default' => true,
		];

		$result['STORE_TO_AMOUNT'] = [
			'id' => 'STORE_TO_AMOUNT',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_TO_AMOUNT'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_TO_AMOUNT'),
			'sort' => 'STORE_TO_AMOUNT',
			'default' => !$this->isReadOnly(),
			'editable' => false,
			'width' => $columnDefaultWidth,
		];

		$result['STORE_TO_RESERVED'] = [
			'id' => 'STORE_TO_RESERVED',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_AMOUNT_RESERVED'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_AMOUNT_RESERVED'),
			'sort' => 'STORE_TO_RESERVED',
			'default' => true,
			'editable' => false,
			'width' => $columnDefaultWidth,
		];

		$result['STORE_TO_AVAILABLE_AMOUNT'] = [
			'id' => 'STORE_TO_AVAILABLE_AMOUNT',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_TO_AMOUNT_AVAILABLE'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_STORE_TO_AMOUNT_AVAILABLE'),
			'sort' => 'STORE_TO_AVAILABLE_AMOUNT',
			'default' => !$this->isReadOnly(),
			'editable' => false,
			'width' => $columnDefaultWidth,
		];

		$amountColumnName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_AMOUNT');
		if (
			$this->getDocumentType() === StoreDocumentTable::TYPE_ARRIVAL
			|| $this->getDocumentType() === StoreDocumentTable::TYPE_STORE_ADJUSTMENT
		)
		{
			$amountColumnName = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_AMOUNT_ARRIVAL');
		}

		$result['AMOUNT'] = [
			'id' => 'AMOUNT',
			'name' => $amountColumnName,
			'title' => $amountColumnName,
			'sort' => 'AMOUNT',
			'default' => true,
			'editable' => [
				'TYPE' => Types::MONEY,
				'CURRENCY_LIST' => $this->getMeasureListForMoneyField(),
				'PLACEHOLDER' => '0',
			],
			'width' => $columnDefaultWidth,
		];

		$result['TOTAL_PRICE'] = [
			'id' => 'TOTAL_PRICE',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_TOTAL_PRICE'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_TOTAL_PRICE'),
			'sort' => null,
			'default' => true,
			'editable' => false,
			'width' => $columnDefaultWidth,
		];

		$result['COMMENT'] = [
			'id' => 'COMMENT',
			'name' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_COMMENT'),
			'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_COMMENT'),
			'sort' => null,
			'default' => false,
			'editable' => true,
			'width' => $columnDefaultWidth,
		];
		if ($this->getDocumentType() === StoreDocumentTable::TYPE_DEDUCT)
		{
			$result['COMMENT']['name'] = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_COMMENT_DEDUCT');
			$result['COMMENT']['title'] = Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_COLUMN_COMMENT_DEDUCT');
			$result['COMMENT']['default'] = true;
		}

		foreach ($result as &$item)
		{
			if (empty($item['editable']))
			{
				$item['editable'] = [
					'TYPE' => Types::CUSTOM,
				];
			}
		}

		unset($item);

		return $result;
	}

	protected function getMeasureListForMoneyField(): array
	{
		return array_column($this->measures, 'SYMBOL', 'CODE');
	}

	protected function getUserGridColumnIds(): array
	{
		$result = $this->gridConfig->GetVisibleColumns();

		if (!empty($result) && !in_array('ID', $result, true))
		{
			array_unshift($result, 'ID');
		}

		return $result;
	}

	/**
	 * @return array
	 */
	protected function getColumns()
	{
		return $this->getStorageItem('COLUMNS');
	}

	/**
	 * @return array
	 */
	protected function getVisibleColumns()
	{
		return $this->getStorageItem('VISIBLE_COLUMNS');
	}

	protected function getGridEditorConfig(array $gridRows): array
	{
		$componentId = $this->randString();
		$defaultRow = $this->getDefaultRow();
		$editData = [
			'template_0' => $this->prepareEditorRow($defaultRow),
		];
		$taxIncluded = $gridRows[0]['raw_data']['TAX_INCLUDED'] ?? null;
		$taxIncludedFormatted = $gridRows[0]['data']['TAX_INCLUDED'] ?? null;
		$taxIncludedFromFirstItem = $this->getTaxIncludedFromFirstItem();
		$taxIncludedFromFirstItemFormatted = ($taxIncludedFromFirstItem === 'Y')
			? Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_TAX_INCLUDED')
			: Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_TAX_NOT_INCLUDED');

		foreach ($gridRows as $row)
		{
			if ($row['editable'] === false)
			{
				continue;
			}

			$editData[$row['id']] = $row['data'];
		}

		return [
			'componentName' => $this->getName(),
			'documentType' => $this->getDocumentType(),
			'signedParameters' => $this->getSignedParameters(),
			'reloadUrl' => $this->getPath() . '/list.ajax.php',

			'containerId' => $this->getPrefix() . '_catalog_document_product_list_container',
			'totalBlockContainerId' => $this->getPrefix() . '_product_sum_total_container',
			'gridId' => $this->getGridId(),
			'formId' => $this->getFormId(),

			'allowEdit' => !$this->isReadOnly(),
			'dataFieldName' => $this->arParams['PRODUCT_DATA_FIELD_NAME'],

			'rowIdPrefix' => $this->getRowIdPrefix(),

			'pricePrecision' => $this->getStorageItem('PRICE_PRECISION'),
			'quantityPrecision' => $this->getStorageItem('AMOUNT_PRECISION'),
			'commonPrecision' => $this->getStorageItem('COMMON_PRECISION'),

			'newRowPosition' => $this->getStorageItem('NEW_ROW_POSITION'),
			'createProductPath' => $this->getStorageItem('CREATE_PRODUCT_PATH'),

			'measures' => array_values($this->measures),
			'stores' => $this->getAccessibleStores(),
			'defaultMeasure' => $this->getDefaultMeasure(),

			'currencyId' => $this->getCurrencyId(),
			'totalCalculationSumField' => $this->getDefaultTotalCalculationField(),
			'totalCalculationSumTaxField' => 'TAX_SUM',
			'taxIncludedFormatted' => $taxIncludedFormatted,
			'taxIncluded' => $taxIncluded,
			'taxIncludedFromFirstItem' => $taxIncludedFromFirstItem,
			'taxIncludedFromFirstItemFormatted' => $taxIncludedFromFirstItemFormatted,

			'popupSettings' => $this->getPopupSettings(),
			'languageId' => $this->getLanguageId(),
			'siteId' => $this->getSiteId(),
			'catalogId' => $componentId,
			'componentId' => $this->randString(),
			'jsEventsManagerId' => "PageEventsManager_{$componentId}",

			'readOnly' => $this->isReadOnly(),
			'items' => $this->getEditorItems(),
			'rowSettings' => $this->getEditorRowSettings(),
			'templateItemFields' => $defaultRow,
			'templateIdMask' => self::PRODUCT_ID_MASK,
			'paintedColumns' => ['AMOUNT'],
			'templateGridEditData' => $editData,
			'enabledCreateProductButton' => $this->isAllowedProductCreation(),

			'productUrlBuilderContext' => htmlspecialcharsbx($this->arParams['BUILDER_CONTEXT']),

			'restrictedProductTypes' => $this->getRestrictedProductTypesForSelector(),
			'isCalculableStorePurchasingPrice' => $this->arParams['CALCULATE_STORE_PURCHASING_PRICE'],
			'isOnecInventoryManagementRestricted' => (
				EnableWizard\Manager::isOnecMode()
				&& EnableWizard\TariffChecker::isOnecInventoryManagementRestricted()
			),
		];
	}

	private function getEditorItems(): array
	{
		$items = [];
		foreach ($this->rows as $row)
		{
			$items[] = [
				'rowId' => $row['ROW_ID'],
				'fields' => $row,
			];
		}

		return $items;
	}

	private function getEditorRowSettings(): array
	{
		$columns = $this->getDefaultColumns();
		$storeHeaders = [];
		foreach ($columns as $column)
		{
			if ($column === 'STORE_TO_INFO')
			{
				$storeHeaders[$column] = 'STORE_TO';
			}
			elseif ($column === 'STORE_FROM_INFO')
			{
				$storeHeaders[$column] = 'STORE_FROM';
			}
		}

		return [
			'storeHeaderMap' => $storeHeaders,
			'isAllowedCreationProduct' => true,
			'documentType' => $this->getDocumentType(),
		];
	}

	/**
	 * @return array
	 */
	protected function getGridRows(): array
	{
		global $APPLICATION;
		$rows = [];
		foreach ($this->rows as $row)
		{
			$item = $this->prepareEditorRow($row);
			$editable = !($row['ACCESS_DENIED'] ?? false);

			$skuTree = '';
			if (isset($row['SKU_TREE']) && $row['SKU_TREE'])
			{
				$skuTree = Json::decode($row['SKU_TREE']);
			}

			ob_start();
			$APPLICATION->IncludeComponent(
				'bitrix:catalog.grid.product.field',
				'',
				[
					'BUILDER_CONTEXT' => $this->arParams['BUILDER_CONTEXT'],
					'GRID_ID' => $this->getGridId(),
					'ROW_ID' => $row['ID'],
					'GUID' => 'catalog_document_grid_'.$row['ID'],
					'PRODUCT_FIELDS' => [
						'ID' => $row['PRODUCT_ID'],
						'NAME' => $row['NAME'],
						'IBLOCK_ID' => $row['IBLOCK_ID'] ?? null,
						'SKU_IBLOCK_ID' => $row['OFFERS_IBLOCK_ID'] ?? null,
						'SKU_ID' => $row['SKU_ID'] ?? null,
						'BASE_PRICE_ID' => $row['BASE_PRICE_ID'] ?? null,
					],
					'SKU_TREE' => $skuTree,
					'MODE' => 'view',
					'VIEW_FORMAT' => 'short',
					'ENABLE_SEARCH' => false,
					'ENABLE_IMAGE_CHANGE_SAVING' => false,
					'ENABLE_INPUT_DETAIL_LINK' => true,
					'ENABLE_EMPTY_PRODUCT_ERROR' => false,
					'ENABLE_SKU_SELECTION' => false,
					'HIDE_UNSELECTED_ITEMS' => true,
					'IS_NEW' => $row['IS_NEW'] ?? 'N',
				]
			);

			$mainInfo = '<div class="main-grid-row-number"></div>' . ob_get_clean();

			$rows[] = [
				'id' => ($row['ID'] === self::PRODUCT_ID_MASK) ? 'template_0' : $row['ID'],
				'raw_data' => $row,
				'data' => $item,
				'columns' => [
					'MAIN_INFO' => $mainInfo,
					'STORE_FROM_INFO' => HtmlFilter::encode($item['STORE_FROM_TITLE']),
					'STORE_TO_INFO' => HtmlFilter::encode($item['STORE_TO_TITLE']),
					'BARCODE_INFO' => HtmlFilter::encode($item['BARCODE']),
					'BASE_PRICE' =>
						$item['BASE_PRICE'] !== null
							? \CCurrencyLang::formatValue($item['BASE_PRICE_FORMATTED'], $this->currency['FORMAT'])
							: null
					,
					'PURCHASING_PRICE' => \CCurrencyLang::formatValue($item['PURCHASING_PRICE_FORMATTED'], $this->currency['FORMAT']),
					'TOTAL_PRICE' => \CCurrencyLang::formatValue($item['TOTAL_PRICE_FORMATTED'], $this->currency['FORMAT']),
					'AMOUNT' => (float)$row['AMOUNT'].' '.htmlspecialcharsbx($row['MEASURE_NAME']),
					'STORE_FROM_AMOUNT' => $this->formatRowStoreAmount($row, 'STORE_FROM_AMOUNT'),
					'STORE_TO_AMOUNT' => $this->formatRowStoreAmount($row, 'STORE_TO_AMOUNT'),
					'STORE_FROM_RESERVED' => $this->formatRowStoreAmount($row, 'STORE_FROM_RESERVED'),
					'STORE_TO_RESERVED' => $this->formatRowStoreAmount($row, 'STORE_TO_RESERVED'),
					'STORE_FROM_AVAILABLE_AMOUNT' => $this->formatRowStoreAmount($row, 'STORE_FROM_AVAILABLE_AMOUNT'),
					'STORE_TO_AVAILABLE_AMOUNT' => $this->formatRowStoreAmount($row, 'STORE_TO_AVAILABLE_AMOUNT'),
				],
				'editable' => !$this->isReadOnly() && $editable,
			];
		}

		return $rows;
	}

	private function formatPrices($price)
	{
		return number_format(
			$price,
			$this->getStorageItem('PRICE_PRECISION'),
			'.',
			''
		);
	}

	private static function formatTaxRate(null|int|float|string $rate): string
	{
		if ($rate === null || $rate === '')
		{
			return Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_NOT_TAX');
		}

		return $rate . ' %';
	}

	private function formatRowStoreAmount(array $row, string $amountFieldName): ?string
	{
		$restrictedProductTypes = $this->getRestrictedProductTypes();

		if (
			!isset($row[$amountFieldName])
			|| !$row['PRODUCT_ID']
			|| in_array((int)$row['TYPE'], $restrictedProductTypes, true))
		{
			return null;
		}

		$formattedValue = (float)$row[$amountFieldName] . ' ' . htmlspecialcharsbx($row['MEASURE_NAME']);

		$isNegativeOrZeroStoreFromAvailableAmount =
			$amountFieldName === 'STORE_FROM_AVAILABLE_AMOUNT' && $row['STORE_FROM_AVAILABLE_AMOUNT'] <= 0
		;
		$isNegativeOrZeroStoreToAvailableAmount =
			$amountFieldName === 'STORE_TO_AVAILABLE_AMOUNT' && $row['STORE_TO_AVAILABLE_AMOUNT'] <= 0
		;
		if ($isNegativeOrZeroStoreFromAvailableAmount || $isNegativeOrZeroStoreToAvailableAmount)
		{
			$formattedValue = '<span class="text--danger">' . $formattedValue . '</span>';
		}

		return $formattedValue;
	}

	private function prepareEditorRow(array $row): array
	{
		$rowId = $row['ROW_ID'];

		$priceFormatted = null;
		if ($row['BASE_PRICE'] !== null)
		{
			$priceFormatted = $this->formatPrices($row['BASE_PRICE']);
		}

		$purchasingPriceFormatted = null;
		if ($row['PURCHASING_PRICE'] !== null)
		{
			$purchasingPriceFormatted = $this->formatPrices($row['PURCHASING_PRICE']);
		}

		$row['TOTAL_PRICE'] ??= 0;
		$totalPriceFormatted = $this->formatPrices($row['TOTAL_PRICE']);

		$row['TAX_RATE'] ??= null;
		$taxRateFormatted = self::formatTaxRate($row['TAX_RATE']);

		$editorFields = [
			'AMOUNT' => [
				'PRICE' => [
					'NAME' => $rowId .'_AMOUNT',
					'VALUE' => $row['AMOUNT'],
				],
				'CURRENCY' => [
					'NAME' => $rowId .'_MEASURE_CODE',
					'VALUE' => $row['MEASURE_CODE'],
					'DISABLED' => !$this->isCanChangeProductMeasure(),
				],
			],
			'STORE_AMOUNT_MAP' => $row['STORE_AMOUNT_MAP'] ?? null,
			'SKU_TREE' => $row['SKU_TREE'] ?? null,
			'BASE_PRICE_EXTRA' => $row['BASE_PRICE_EXTRA'] ?? null,
			'BASE_PRICE_EXTRA_RATE' => $row['BASE_PRICE_EXTRA_RATE'] ?? null,
			'BASE_PRICE_FORMATTED' => $priceFormatted,
			'TOTAL_PRICE_FORMATTED' => $totalPriceFormatted,
			'PURCHASING_PRICE_FORMATTED' => $purchasingPriceFormatted,
			'TAX_RATE' => $taxRateFormatted,
			'TAX_INCLUDED' => isset($row['TAX_INCLUDED']) && $row['TAX_INCLUDED'] === 'Y'
				? Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_TAX_INCLUDED')
				: Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_TAX_NOT_INCLUDED'),
		];
		foreach($this->getColumns() as $column)
		{
			$columnId = $column['id'];
			switch ($columnId)
			{
				case 'BASE_PRICE':
				case 'TOTAL_PRICE':
				case 'PURCHASING_PRICE':
					if ($column['editable']['TYPE'] === Types::MONEY)
					{
						$editorFields[$columnId] = [
							'PRICE' => [
								'NAME' => $rowId . '_' . $columnId,
								'VALUE' =>
									$columnId === 'BASE_PRICE'
										? $priceFormatted
										: $purchasingPriceFormatted
								,
							],
							'CURRENCY' => [
								'NAME' => $rowId . '_' . $columnId . '_CURRENCY',
								'VALUE' => $this->getCurrencyId(),
							],
						];
					}
					elseif ($row[$columnId] !== null)
					{
						$editorFields[$columnId] = \CCurrencyLang::CurrencyFormat($row[$columnId], $this->getCurrencyId());
					}
					break;

				case 'STORE_TO_INFO':
					$editorFields['STORE_TO'] = $row['STORE_TO'];
					$editorFields['STORE_TO_AMOUNT'] = $this->formatRowStoreAmount($row, 'STORE_TO_AMOUNT');
					$editorFields['STORE_TO_RESERVED'] = $this->formatRowStoreAmount($row, 'STORE_TO_RESERVED');
					$editorFields['STORE_TO_AVAILABLE_AMOUNT'] = $this->formatRowStoreAmount($row, 'STORE_TO_AVAILABLE_AMOUNT');
					break;

				case 'STORE_FROM_INFO':
					$editorFields['STORE_FROM'] = $row['STORE_FROM'];
					$editorFields['STORE_FROM_AMOUNT'] = $this->formatRowStoreAmount($row, 'STORE_FROM_AMOUNT');
					$editorFields['STORE_FROM_RESERVED'] = $this->formatRowStoreAmount($row, 'STORE_FROM_RESERVED');
					$editorFields['STORE_FROM_AVAILABLE_AMOUNT'] = $this->formatRowStoreAmount($row, 'STORE_FROM_AVAILABLE_AMOUNT');
					break;
			}
		}

		return array_merge($row, $editorFields);
	}

	private function getDefaultRow(): array
	{
		$defaultStore = $this->getDefaultStore();
		$defaultStoreId = $defaultStore['ID'] ?? null;
		$defaultStoreTitle = $defaultStore['TITLE'] ?? null;

		$defaultMeasure = $this->getDefaultMeasure();
		$row = [
			'ROW_ID' => $this->getRowIdPrefix(self::PRODUCT_ID_MASK),
			'ID' => self::PRODUCT_ID_MASK,
			'IBLOCK_ID' => $this->arParams['CATALOG_ID'],
			'OFFERS_IBLOCK_ID' => 0,
			'SKU_ID' => null,
			'BASE_PRICE_ID' => $this->getDefaultSetting('BASE_PRICE_ID'),
			'PRODUCT_ID' => null,
			'NAME' => '',
			'BARCODE' => '',
			'DOC_BARCODE' => '',
			'BASE_PRICE' => null,
			'TOTAL_PRICE' => null,
			'PURCHASING_PRICE' => null,
			'CURRENCY' => $this->getCurrency(),
			'MEASURE_NAME' => $defaultMeasure['SYMBOL'] ?? '',
			'MEASURE_CODE' => $defaultMeasure['CODE'] ?? '',
			'AMOUNT' => 0,
			'STORE_TO' => $defaultStoreId ?? null,
			'STORE_TO_TITLE' => $defaultStoreTitle ?? null,
			'STORE_TO_AMOUNT' => 0,
			'STORE_TO_RESERVED' => 0,
			'STORE_TO_AVAILABLE_AMOUNT' => 0,
			'STORE_FROM' => $defaultStoreId ?? null,
			'STORE_FROM_TITLE' => $defaultStoreTitle ?? null,
			'STORE_FROM_AMOUNT' => 0,
			'STORE_AMOUNT_MAP' => null,
			'STORE_FROM_RESERVED' => 0,
			'STORE_FROM_AVAILABLE_AMOUNT' => 0,
			'IS_NEW' => 'N',
			'BASE_PRICE_EXTRA' => '',
			'BASE_PRICE_EXTRA_RATE' => StoreDocumentElementTable::EXTRA_RATE_PERCENTAGE,
			'TYPE' => 0,
			'TAX_INCLUDED' => 'N',
		];

		return $this->prepareRowsForAccessRights([ $row ])[0];
	}

	protected function isReadOnly(): bool
	{
		return !$this->arParams['ALLOW_EDIT'];
	}

	/**
	 * @return string
	 */
	protected function getNewRowId(): string
	{
		$result = self::NEW_ROW_ID_PREFIX . $this->getNewRowCounter();
		$this->newRowCounter++;

		return $result;
	}

	/**
	 * @return int
	 */
	protected function getNewRowCounter(): int
	{
		return $this->newRowCounter;
	}

	/* Access rights tools */

	protected function getPrefix(): string
	{
		return $this->arParams['PREFIX'] !== '' ? $this->arParams['PREFIX'] : $this->getDefaultPrefix();
	}

	protected function getRowIdPrefix(string $code = null): string
	{
		return $this->getPrefix() . '_product_row_' . $code;
	}

	/**
	 * @return string
	 */
	protected function getDefaultPrefix(): string
	{
		$suffix =
			$this->getDocumentId() > 0
				? strtolower($this->getDocumentType()) . '_' . $this->getDocumentId()
				: 'new_' . strtolower($this->getDocumentType())
		;

		return "document_{$suffix}_product_editor";
	}

	/**
	 * @param string $value
	 * @return string
	 */
	private static function clearStringValue(string $value): string
	{
		return preg_replace('/[^a-zA-Z0-9_:\\[\\]]/', '', $value);
	}

	/**
	 * @param array &$params
	 * @param string $field
	 * @return void
	 */
	private static function validateSingleParameter(array &$params, string $field): void
	{
		$value = '';

		if (isset($params[$field]) && is_string($params[$field]))
		{
			$value = static::clearStringValue($params[$field]);
		}

		$params[$field] = $value;
	}

	/**
	 * @param array &$params
	 * @param array $list
	 * @return void
	 */
	private static function validateListParameters(array &$params, array $list): void
	{
		foreach ($list as $field)
		{
			static::validateSingleParameter($params, $field);
		}
	}

	/**
	 * @param array $params
	 * @param string $field
	 * @return void
	 */
	private static function validateBoolParameter(array &$params, string $field): void
	{
		if (!isset($params[$field]))
		{
			$params[$field] = false;
		}
		if (is_string($params[$field]))
		{
			$params[$field] = ($params[$field] === 'Y');
		}
		$params[$field] = (is_bool($params[$field]) && $params[$field]);
	}

	/**
	 * @param array $params
	 * @param array $list
	 * @return void
	 */
	private static function validateBoolList(array &$params, array $list): void
	{
		foreach ($list as $field)
		{
			static::validateBoolParameter($params, $field);
		}
		unset($field);
	}

	private function getPreselectDocumentProducts(): array
	{
		if ($this->arParams['PRESELECTED_PRODUCT_ID'] === 0)
		{
			return [];
		}

		$preselectedSku = $this->getSkuByProductId($this->arParams['PRESELECTED_PRODUCT_ID']);
		if ($preselectedSku)
		{
			$basePriceEntity = $preselectedSku->getPriceCollection()->findBasePrice();
			$defaultStore = $this->getDefaultStore();
			$defaultStoreId = $defaultStore['ID'] ?? null;

			$convertedPurchasingPrice = \CCurrencyRates::ConvertCurrency(
				(float)$preselectedSku->getField('PURCHASING_PRICE'),
				(string)$preselectedSku->getField('PURCHASING_CURRENCY'),
				$this->getCurrencyId()
			);

			$basePrice = $basePriceEntity ? $basePriceEntity->getPrice() : null;
			$basePriceCurrency = $basePriceEntity ? $basePriceEntity->getCurrency() : null;
			$convertedBasePrice = \CCurrencyRates::ConvertCurrency(
				(float)$basePrice,
				(string)$basePriceCurrency,
				$this->getCurrencyId()
			);

			$vatId = (int)$preselectedSku->getField('VAT_ID');
			$tax = $vatId ? \Bitrix\Catalog\VatTable::getRowById($vatId) : null;

			return [
				[
					'ID' => Main\Security\Random::getString(8, false),
					'DOC_ID' => null,
					'STORE_FROM' => $defaultStoreId,
					'STORE_TO' => $defaultStoreId,
					'ELEMENT_ID' => $preselectedSku->getId(),
					'AMOUNT' => null,
					'PURCHASING_PRICE' => $convertedPurchasingPrice,
					'BASE_PRICE' => $convertedBasePrice,
					'BASE_PRICE_EXTRA' => null,
					'BASE_PRICE_EXTRA_RATE' => StoreDocumentElementTable::EXTRA_RATE_PERCENTAGE,
					'TAX_RATE' => ($tax['RATE'] ?? null),
					'TAX_INCLUDED' => $preselectedSku->getField('VAT_INCLUDED'),
				],
			];
		}

		return [];
	}

	private function getSkuByProductId(int $productId): ?BaseSku
	{
		$repositoryFacade = ServiceContainer::getRepositoryFacade();

		return $repositoryFacade->loadVariation($productId);
	}

	private function getStores(): array
	{
		if (empty($this->stores))
		{
			$this->loadStores();
		}

		return $this->stores;
	}

	private function getAccessibleStores(): array
	{
		return array_intersect_key($this->getStores(), array_flip($this->getAccessibleStoresIds()));
	}

	/**
	 * Warehouses to which the user has access.
	 *
	 * @return array
	 */
	private function getAccessibleStoresIds(): array
	{
		if (isset($this->accessibleStoresIds))
		{
			return $this->accessibleStoresIds;
		}

		$storeIds = (array)$this->accessController->getPermissionValue(ActionDictionary::ACTION_STORE_VIEW);
		if (in_array(PermissionDictionary::VALUE_VARIATION_ALL, $storeIds, true))
		{
			$storeIds = array_column($this->getStores(), 'ID');
		}

		$this->accessibleStoresIds = array_map('intval', $storeIds);

		return $this->accessibleStoresIds;
	}

	private function getDefaultStore(): ?array
	{
		static $defaultStore;

		if (isset($defaultStore))
		{
			return $defaultStore;
		}

		$accessibleStoresIds = $this->getAccessibleStoresIds();
		if (empty($accessibleStoresIds))
		{
			return null;
		}

		$accessibleStores = array_filter(
			$this->getStores(),
			static function($store) use($accessibleStoresIds)
			{
				return in_array((int)$store['ID'], $accessibleStoresIds, true);
			}
		);

		$filteredStores = array_filter(
			$accessibleStores,
			static function($store)
			{
				return $store['IS_DEFAULT'] === 'Y';
			}
		);

		$defaultStore = reset($filteredStores) ?: reset($accessibleStores);

		return $defaultStore;
	}

	/**
	 * Returns available amount on store (amount - quantity in reserve)
	 *
	 * @param array $productStoreInfo
	 * @param int $productId
	 * @param int $storeId
	 * @return float
	 */
	private function getAvailableProductAmountOnStore(array $productStoreInfo, int $productId, int $storeId): float
	{
		$amount = 0.0;

		if (
			isset(
				$productStoreInfo[$productId][$storeId]['AMOUNT'],
				$productStoreInfo[$productId][$storeId]['QUANTITY_RESERVED']
			)
		)
		{
			$amount =
				$productStoreInfo[$productId][$storeId]['AMOUNT']
				- $productStoreInfo[$productId][$storeId]['QUANTITY_RESERVED']
			;
		}

		return $amount;
	}

	public function getPopupSettings(): array
	{
		return [
			[
				'id' => 'ADD_NEW_ROW_TOP',
				'checked' => ($this->defaultSettings['NEW_ROW_POSITION'] !== 'bottom'),
				'title' => Loc::getMessage('CATALOG_DOCUMENT_PRODUCT_LIST_SETTING_NEW_ROW_POSITION_TITLE'),
				'desc' => '',
				'action' => 'grid',
			]
		];
	}

	public function setGridSettingAction(string $settingId, $selected): Bitrix\Main\Engine\Response\AjaxJson
	{
		if (!$this->checkModules())
		{
			return Bitrix\Main\Engine\Response\AjaxJson::createError($this->errorCollection);
		}

		if ($settingId === 'ADD_NEW_ROW_TOP')
		{
			$direction = ($selected === 'true') ? 'top' : 'bottom';
			\CUserOptions::SetOption('catalog.store.document.product.list', 'new.row.position', $direction);
		}

		return Bitrix\Main\Engine\Response\AjaxJson::createSuccess();
	}

	/**
	 * Returns converted base and purchasing prices for product list.
	 *
	 * @param array $products Products information.
	 * @param string $currencyId Currency identifier for result.
	 * @param string $oldCurrencyId Old currency identifier.
	 * @return null|array
	 */
	public function calculateProductPricesAction(array $products, string $currencyId, string $oldCurrencyId): ?array
	{
		$this->fillSettings();
		if ($this->isExistErrors())
		{
			return null;
		}

		$response = [];

		foreach ($products as $product)
		{
			$fields = $product['fields'] ?? [];

			\CCurrencyRates::ConvertCurrency(
				(float)$fields['BASE_PRICE'],
				$oldCurrencyId,
				$currencyId
			);

			$basePrice = null;
			if ($fields['BASE_PRICE'] !== null)
			{
				$basePrice = $this->formatPrices(
					\CCurrencyRates::ConvertCurrency(
						(float)$fields['BASE_PRICE'],
						$oldCurrencyId,
						$currencyId
					)
				);
			}

			$response[$product['id']] = [
				'BASE_PRICE' => $basePrice,
				'PURCHASING_PRICE' => $this->formatPrices(
					\CCurrencyRates::ConvertCurrency(
						(float)$fields['PURCHASING_PRICE'],
						$oldCurrencyId,
						$currencyId
					)
				),
			];
		}

		return $response;
	}

	/**
	 * Returns cost price for product.
	 *
	 * @param int $productId Product indentifier.
	 * @param float $quantity Product quantity.
	 * @param string $currency Currency identifier.
	 * @param int $storeId Store identifier.
	 * @return null|array
	 */
	public function calculateStoreCostPriceAction(int $productId, float $quantity, string $currency, int $storeId): ?float
	{
		if (!$this->checkModules())
		{
			return null;
		}

		return (new \Bitrix\Catalog\Product\Store\BatchManager($productId))->calculateCostPrice($quantity, $storeId, $currency);
	}

	private function getRestrictedProductTypesForSelector(): array
	{
		$restrictedProductTypes = $this->getRestrictedProductTypes();

		if (!empty($this->externalDocument['RESTRICTED_PRODUCT_TYPES']))
		{
			$restrictedProductTypes = $this->externalDocument['RESTRICTED_PRODUCT_TYPES'];
		}

		return $restrictedProductTypes;
	}

	private function getRestrictedProductTypes(): array
	{
		return ProductTable::getStoreDocumentRestrictedProductTypes();
	}

	private function isEditableBasePrice(): bool
	{
		if (!$this->accessController->check(ActionDictionary::ACTION_PRICE_EDIT))
		{
			return false;
		}

		return ! in_array($this->getDocumentType(), [
			StoreDocumentTable::TYPE_MOVING,
			StoreDocumentTable::TYPE_DEDUCT,
		], true);
	}

	private function isCanChangeProductMeasure(): bool
	{
		return $this->accessController->check(ActionDictionary::ACTION_PRODUCT_EDIT);
	}

	public function isAllowedProductCreation(): bool
	{
		return $this->accessController->check(ActionDictionary::ACTION_PRODUCT_ADD);
	}

	/**
	 * Calculate and return detailed information about the total amount
	 *
	 * @param array $gridRows
	 * @return float[] ('TOTAL_TAX', 'TOTAL_SUM', 'TOTAL_SUM_BEFORE_TAX')
	 */
	private function getTotalSumDetails(array $gridRows): array
	{
		$totalSumDetails = [
			'TOTAL_TAX' => 0,
			'TOTAL_SUM' => 0,
		];

		foreach ($gridRows as $row) {
			$totalSumDetails['TOTAL_TAX'] += $row['raw_data']['TAX_SUM'] ?? 0;
			$totalSumDetails['TOTAL_SUM'] += $row['raw_data']['TOTAL_PRICE'] ?? 0;
		}

		$totalSumDetails['TOTAL_SUM_BEFORE_TAX'] = $totalSumDetails['TOTAL_SUM'] - $totalSumDetails['TOTAL_TAX'];

		return $totalSumDetails;
	}

	/**
	 * @return string|null
	 * @throws Main\ArgumentException
	 * @throws Main\ObjectPropertyException
	 * @throws Main\SystemException
	 */
	private function getTaxIncludedFromFirstItem(): ?string
	{
		$context = \Bitrix\Main\Application::getInstance()->getContext()->getRequest()->get('context');
		$ownerId = (int)($context['OWNER_ID'] ?? null);
		$ownerTypeId = (int)($context['OWNER_TYPE_ID'] ?? null);
		$taxIncludedFromFirstItem = null;

		if ($ownerId && $ownerTypeId === CCrmOwnerType::Deal)
		{
			$productRow = ProductRowTable::getRow(
				[
					'select' => ['TAX_INCLUDED'],
					'filter' => [
						'=OWNER_ID' => $ownerId,
						'=OWNER_TYPE' => CCrmOwnerTypeAbbr::ResolveByTypeID($ownerTypeId)
					],
				]
			);
			$taxIncludedFromFirstItem = $productRow['TAX_INCLUDED'] ?? null;
		}

		return $taxIncludedFromFirstItem;
	}
}