Your IP : 18.190.207.82


Current Path : /home/bitrix/ext_www/dev.klimatlend.ua/bitrix/modules/catalog/lib/product/
Upload File :
Current File : /home/bitrix/ext_www/dev.klimatlend.ua/bitrix/modules/catalog/lib/product/sku.php

<?php
namespace Bitrix\Catalog\Product;

use Bitrix\Main,
	Bitrix\Catalog,
	Bitrix\Iblock;

/**
 * Class Sku
 * Provides various useful methods for sku data.
 *
 * @package Bitrix\Catalog\Product
 */
class Sku
{
	const OFFERS_ERROR = 0x0000;
	const OFFERS_NOT_EXIST = 0x0001;
	const OFFERS_NOT_AVAILABLE = 0x0002;
	const OFFERS_AVAILABLE = 0x0004;

	protected static $allowUpdateAvailable = 0;
	protected static $allowPropertyHandler = true;

	protected static $productIds = array();
	protected static $offers = array();
	private static $changeActive = array();
	private static $currentActive = array();

	private static $separateSkuMode = null;

	private static $deferredCalculation = -1;

	private static $calculateAvailable = false;
	private static $calculatePriceTypes = array();

	private static $deferredSku = array();
	private static $deferredOffers = array();
	private static $deferredUnknown = array();

	private static $skuExist = array();
	private static $skuAvailable = array();
	private static $offersIds = array();
	private static $offersMap = array();
	private static $skuPrices = array();

	/**
	 * Enable automatic update parent product available and prices.
	 *
	 * @return void
	 */
	public static function enableUpdateAvailable()
	{
		self::$allowUpdateAvailable++;
	}

	/**
	 * Disable automatic update parent product available and prices.
	 *
	 * @return void
	 */
	public static function disableUpdateAvailable()
	{
		self::$allowUpdateAvailable--;
	}

	/**
	 * Return true if allowed automatic update parent product available and prices.
	 *
	 * @return bool
	 */
	public static function allowedUpdateAvailable()
	{
		return (self::$allowUpdateAvailable >= 0);
	}

	/**
	 * Enable deferred calculation parent product available and prices.
	 *
	 * @return void
	 */
	public static function enableDeferredCalculation()
	{
		self::$deferredCalculation++;
	}

	/**
	 * Disable deferred calculation parent product available and prices.
	 *
	 * @return void
	 */
	public static function disableDeferredCalculation()
	{
		self::$deferredCalculation--;
	}

	/**
	 * Return true if allowed deferred calculation parent product available and prices.
	 *
	 * @return bool
	 */
	public static function usedDeferredCalculation()
	{
		return (self::$deferredCalculation >= 0);
	}

	/**
	 * Return default settings for product with sku.
	 *
	 * @param int $state			State flag.
	 * @param bool $productIblock   Is iblock (no catalog) with offers.
	 * @return array
	 */
	public static function getDefaultParentSettings($state, $productIblock = false)
	{
		$state = (int)$state;
		switch ($state)
		{
			case self::OFFERS_NOT_EXIST:
				$result = array(
					'TYPE' => ($productIblock ? Catalog\ProductTable::TYPE_EMPTY_SKU : Catalog\ProductTable::TYPE_PRODUCT),
					'AVAILABLE' => Catalog\ProductTable::STATUS_NO,
					'QUANTITY' => 0,
					'QUANTITY_TRACE' => Catalog\ProductTable::STATUS_YES,
					'CAN_BUY_ZERO' => Catalog\ProductTable::STATUS_NO
				);
				break;
			case self::OFFERS_NOT_AVAILABLE:
				$result = array(
					'TYPE' => Catalog\ProductTable::TYPE_SKU,
					'AVAILABLE' => Catalog\ProductTable::STATUS_NO,
					'QUANTITY' => 0,
					'QUANTITY_TRACE' => Catalog\ProductTable::STATUS_YES,
					'CAN_BUY_ZERO' => Catalog\ProductTable::STATUS_NO
				);
				break;
			case self::OFFERS_AVAILABLE:
				$result = array(
					'TYPE' => Catalog\ProductTable::TYPE_SKU,
					'AVAILABLE' => Catalog\ProductTable::STATUS_YES,
					'QUANTITY' => 0,
					'QUANTITY_TRACE' => Catalog\ProductTable::STATUS_NO,
					'CAN_BUY_ZERO' => Catalog\ProductTable::STATUS_YES,
				);
				break;
			default:
				$result = array();
				break;
		}
		return $result;
	}

	/**
	 * Update product available.
	 *
	 * @deprecated deprecated since catalog 17.6.0
	 * @see Sku::calculateComplete (for sku) and Catalog\Model\Product::update (for all product types)
	 *
	 * @param int $productId			Product Id.
	 * @param int $iblockId				Iblock Id (optional).
	 * @param array $productFields		Product fields (optional).
	 * @return bool
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Exception
	 */
	public static function updateAvailable($productId, $iblockId = 0, array $productFields = array())
	{
		if (!self::allowedUpdateAvailable())
			return true;
		static::disableUpdateAvailable();

		$result = true;
		$process = true;
		$iblockData = false;
		$fields = array();
		$parentId = 0;
		$parentIblockId = 0;

		$productId = (int)$productId;
		if ($productId <= 0)
		{
			$process = false;
			$result = false;
		}
		if ($process)
		{
			$iblockId = (int)$iblockId;
			if ($iblockId <= 0)
				$iblockId = (int)\CIBlockElement::GetIBlockByID($productId);
			if ($iblockId <= 0)
			{
				$process = false;
				$result = false;
			}
		}

		if ($process)
		{
			$iblockData = \CCatalogSku::GetInfoByIBlock($iblockId);
			if (empty($iblockData))
			{
				$process = false;
				$result = false;
			}
		}

		if ($process)
		{
			switch ($iblockData['CATALOG_TYPE'])
			{
				case \CCatalogSku::TYPE_PRODUCT:
					if (self::isSeparateSkuMode())
						$fields = static::getParentDataAsProduct($productId, $productFields);
					else
						$fields = static::getDefaultParentSettings(static::getOfferState($productId, $iblockId), true);
					break;
				case \CCatalogSku::TYPE_FULL:
					$offerState = static::getOfferState($productId, $iblockId);
					if ($offerState != self::OFFERS_ERROR)
					{
						switch ($offerState)
						{
							case self::OFFERS_AVAILABLE:
							case self::OFFERS_NOT_AVAILABLE:
								if (self::isSeparateSkuMode())
									$fields = static::getParentDataAsProduct($productId, $productFields);
								else
									$fields = static::getDefaultParentSettings($offerState, false);
								break;
							case self::OFFERS_NOT_EXIST:
								$product = Catalog\Model\Product::getCacheItem($productId, true);
								if (!empty($product))
								{
									switch ($product['TYPE'])
									{
										case Catalog\ProductTable::TYPE_SKU:
											$fields = static::getDefaultParentSettings($offerState, false);
											break;
										case Catalog\ProductTable::TYPE_PRODUCT:
										case Catalog\ProductTable::TYPE_SET:
											$fields['AVAILABLE'] = Catalog\ProductTable::calculateAvailable($product);
											break;
										default:
											break;
									}
								}
								unset($product);
								break;
						}
					}
					break;
				case \CCatalogSku::TYPE_OFFERS:
					$parent = \CCatalogSku::getProductList($productId, $iblockId);
					if (!isset($parent[$productId]))
					{
						$fields = array(
							'TYPE' => Catalog\ProductTable::TYPE_FREE_OFFER,
						);
					}
					else
					{
						$fields = array(
							'TYPE' => Catalog\ProductTable::TYPE_OFFER,
						);
						$parentId = $parent[$productId]['ID'];
						$parentIblockId = $parent[$productId]['IBLOCK_ID'];
					}
					$fields = array_merge(
						self::getProductAvailable($productId, $productFields),
						$fields
					);
					break;
				case \CCatalogSku::TYPE_CATALOG:
					$fields = self::getProductAvailable($productId, $productFields);
					break;
			}
		}

		if (
			$process
			&& !empty($fields)
		)
		{
			$updateResult = Catalog\ProductTable::update($productId, $fields);
			if (!$updateResult->isSuccess())
			{
				$process = false;
				$result = false;
			}
			unset($updateResult);
		}

		if (
			$process
			&& $parentId > 0
			&& $parentIblockId > 0
		)
		{
			$result = self::updateParentAvailable($parentId, $parentIblockId);
			if (!$result)
				$process = false;
		}
		unset($parentIblockId, $parentId, $fields, $iblockData);
		unset($process);
		static::enableUpdateAvailable();

		return $result;
	}

	/**
	 * Prepare data for update parent product available and prices. Run calculate, if disable deferred calculation.
	 *
	 * @param int $id				Product id (sku, offer).
	 * @param null|int $iblockId	Product iblock (null, if unknown).
	 * @param null|int $type		Real product type (or null, if unknown).
	 *
	 * @return void
	 */
	public static function calculateComplete($id, $iblockId = null, $type = null)
	{
		if (!self::allowedUpdateAvailable())
			return;

		$id = (int)$id;
		if ($id <= 0)
			return;

		if (
			$type == Catalog\ProductTable::TYPE_FREE_OFFER
			|| ($type == Catalog\ProductTable::TYPE_PRODUCT && !self::isSeparateSkuMode())
			|| $type == Catalog\ProductTable::TYPE_SET
		)
			return;

		if ($iblockId !== null)
		{
			$iblockId = (int)$iblockId;
			if ($iblockId <= 0)
				$iblockId = null;
		}

		switch ($type)
		{
			case Catalog\ProductTable::TYPE_SKU:
			case Catalog\ProductTable::TYPE_EMPTY_SKU:
				self::setCalculateData(self::$deferredSku, $id, $iblockId);
				break;
			case Catalog\ProductTable::TYPE_OFFER:
				self::setCalculateData(self::$deferredOffers, $id, $iblockId);
				break;
			default:
				if (isset(self::$deferredSku[$id]))
					self::setCalculateData(self::$deferredSku, $id, $iblockId);
				elseif (isset(self::$deferredOffers[$id]))
					self::setCalculateData(self::$deferredOffers, $id, $iblockId);
				else
					self::setCalculateData(self::$deferredUnknown, $id, $iblockId);
				break;
		}
		if (!self::usedDeferredCalculation())
			self::calculate();
	}

	/**
	 * Prepare data for update parent product prices. Run calculate, if disable deferred calculation.
	 *
	 * @param int $id				Product id (sku, offer).
	 * @param null|int $iblockId	Product iblock (null, if unknown).
	 * @param null|int $type		Real product type (or null, if unknown).
	 * @param array $priceTypes		Price type ids for calculation (empty, if need all price types).
	 *
	 * @return void
	 */
	public static function calculatePrice($id, $iblockId = null, $type = null, array $priceTypes = [])
	{
		if (!self::allowedUpdateAvailable())
			return;

		if (self::isSeparateSkuMode())
			return;

		$id = (int)$id;
		if ($id <= 0)
			return;

		if (
			$type == Catalog\ProductTable::TYPE_FREE_OFFER
			|| $type == Catalog\ProductTable::TYPE_PRODUCT
			|| $type == Catalog\ProductTable::TYPE_SET
		)
			return;

		if ($iblockId !== null)
		{
			$iblockId = (int)$iblockId;
			if ($iblockId <= 0)
				$iblockId = null;
		}

		switch ($type)
		{
			case Catalog\ProductTable::TYPE_SKU:
			case Catalog\ProductTable::TYPE_EMPTY_SKU:
				self::setCalculatePriceTypes(self::$deferredSku, $id, $iblockId, $priceTypes);
				break;
			case Catalog\ProductTable::TYPE_OFFER:
				self::setCalculatePriceTypes(self::$deferredOffers, $id, $iblockId, $priceTypes);
				break;
			default:
				if (isset(self::$deferredSku[$id]))
					self::setCalculatePriceTypes(self::$deferredSku, $id, $iblockId, $priceTypes);
				elseif (isset(self::$deferredOffers[$id]))
					self::setCalculatePriceTypes(self::$deferredOffers, $id, $iblockId, $priceTypes);
				else
				self::setCalculatePriceTypes(self::$deferredUnknown, $id, $iblockId, $priceTypes);
				break;
		}

		if (!self::usedDeferredCalculation())
			self::calculate();
	}

	/**
	 * Run calculate parent product available and prices. Need data must will be prepared in Sku::calculateComplete or Sku::calculatePrice.
	 *
	 * @return void
	 */
	public static function calculate()
	{
		if (!self::allowedUpdateAvailable())
			return;

		static::disableUpdateAvailable();

		self::updateDeferredSkuList();

		if (!empty(self::$deferredSku))
		{
			self::clearStepData();

			self::$calculatePriceTypes = array_keys(self::$calculatePriceTypes);
			if (!empty(self::$calculatePriceTypes))
				sort(self::$calculatePriceTypes);

			self::loadProductIblocks();

			$list = array_keys(self::$deferredSku);
			sort($list);

			foreach (array_chunk($list, 100) as $pageIds)
			{
				self::loadProductData($pageIds);
				self::updateProductData($pageIds);
				self::updateProductFacetIndex($pageIds);
			}
			unset($pageIds, $list);

			self::clearStepData();

			self::$deferredSku = array();
		}

		self::$calculateAvailable = false;
		self::$calculatePriceTypes = array();

		static::enableUpdateAvailable();
	}

	/**
	 * OnIBlockElementAdd event handler. Do not use directly.
	 *
	 * @param array $fields				Element data.
	 * @return void
	 */
	public static function handlerIblockElementAdd($fields)
	{
		static::disablePropertyHandler();
	}

	/**
	 * OnAfterIBlockElementAdd event handler. Do not use directly.
	 *
	 * @param array &$fields			Element data.
	 *
	 * @return void
	 */
	public static function handlerAfterIblockElementAdd(&$fields)
	{
		static::enablePropertyHandler();
	}

	/**
	 * OnIBlockElementUpdate event handler. Do not use directly.
	 *
	 * @param array $newFields			New element data.
	 * @param array $oldFields			Current element data.
	 *
	 * @return void
	 */
	public static function handlerIblockElementUpdate($newFields, $oldFields)
	{
		static::disablePropertyHandler();

		$iblockData = \CCatalogSku::GetInfoByOfferIBlock($newFields['IBLOCK_ID']);
		if (empty($iblockData))
			return;

		if (isset($newFields['ACTIVE']) && $newFields['ACTIVE'] != $oldFields['ACTIVE'])
			self::$changeActive[$newFields['ID']] = $newFields['ACTIVE'];
		self::$currentActive[$newFields['ID']] = $oldFields['ACTIVE'];
	}

	/**
	 * OnAfterIBlockElementUpdate event handler. Do not use directly.
	 *
	 * @param array &$fields			New element data.
	 *
	 * @return void
	 */
	public static function handlerAfterIblockElementUpdate(&$fields)
	{
		$process = true;
		$modifyActive = false;
		$modifyProperty = false;
		$iblockData = false;
		$elementId = 0;

		if (!$fields['RESULT'])
			$process = false;
		else
			$elementId = $fields['ID'];

		if ($process)
		{
			$modifyActive = isset(self::$changeActive[$elementId]);
			$modifyProperty = (
				isset(self::$offers[$elementId])
				&& self::$offers[$elementId]['CURRENT_PRODUCT'] != self::$offers[$elementId]['NEW_PRODUCT']
			);
			$process = $modifyActive || $modifyProperty;
		}

		if ($process)
		{
			$iblockData = \CCatalogSku::GetInfoByOfferIBlock($fields['IBLOCK_ID']);
			$process = !empty($iblockData);
		}

		if ($process)
		{
			if ($modifyActive && !isset(self::$offers[$elementId]))
			{
				$parent = \CCatalogSku::getProductList($elementId, $fields['IBLOCK_ID']);
				if (!empty($parent[$elementId]))
					self::$offers[$elementId] = array(
						'CURRENT_PRODUCT' => $parent[$elementId]['ID'],
						'NEW_PRODUCT' => $parent[$elementId]['ID'],
						'PRODUCT_IBLOCK_ID' => $parent[$elementId]['IBLOCK_ID']
					);
				unset($parent);
			}

			if (isset(self::$offers[$elementId]))
			{
				if (self::$offers[$elementId]['CURRENT_PRODUCT'] > 0)
				{
					if ($modifyActive || $modifyProperty)
					{
						self::calculateComplete(
							self::$offers[$elementId]['CURRENT_PRODUCT'],
							$iblockData['PRODUCT_IBLOCK_ID'],
							Catalog\ProductTable::TYPE_SKU
						);
					}
				}
				if (self::$offers[$elementId]['NEW_PRODUCT'] > 0)
				{
					$elementActive = (
						$modifyActive
						? self::$changeActive[$elementId]
						: self::$currentActive[$elementId]
					);
					if ($modifyProperty && $elementActive == 'Y')
					{
						self::calculateComplete(
							self::$offers[$elementId]['NEW_PRODUCT'],
							$iblockData['PRODUCT_IBLOCK_ID'],
							Catalog\ProductTable::TYPE_SKU
						);
					}
				}
				if (self::$offers[$elementId]['CURRENT_PRODUCT'] == 0 || self::$offers[$elementId]['NEW_PRODUCT'] == 0)
				{
					$type = (
						self::$offers[$elementId]['NEW_PRODUCT'] > 0
						? Catalog\ProductTable::TYPE_OFFER
						: Catalog\ProductTable::TYPE_FREE_OFFER
					);
					self::disableUpdateAvailable();
					$result = Catalog\Model\Product::update($elementId, array('TYPE' => $type));
					unset($result);
					self::enableUpdateAvailable();
					unset($type);
				}
			}
			else
			{
				self::disableUpdateAvailable();
				$result = Catalog\Model\Product::update($elementId, array('TYPE' => Catalog\ProductTable::TYPE_FREE_OFFER));
				unset($result);
				self::enableUpdateAvailable();
			}
		}
		if (isset(self::$offers[$elementId]))
			unset(self::$offers[$elementId]);
		if (isset(self::$currentActive[$elementId]))
			unset(self::$currentActive[$elementId]);
		if (isset(self::$changeActive[$elementId]))
			unset(self::$changeActive[$elementId]);
		static::enablePropertyHandler();
	}

	/**
	 * OnIBlockElementDelete event handler. Do not use directly.
	 *
	 * @param int $elementId			Element id.
	 * @param array $elementData		Element data.
	 *
	 * @return void
	 */
	public static function handlerIblockElementDelete($elementId, $elementData)
	{
		if ((int)$elementData['WF_PARENT_ELEMENT_ID'] > 0)
			return;

		$iblockData = \CCatalogSku::GetInfoByOfferIBlock($elementData['IBLOCK_ID']);
		if (empty($iblockData))
			return;

		$parent = \CCatalogSku::getProductList($elementId, $elementData['IBLOCK_ID']);
		if (!empty($parent[$elementId]))
			self::$offers[$elementId] = array(
				'CURRENT_PRODUCT' => $parent[$elementId]['ID'],
				'NEW_PRODUCT' => 0,
				'PRODUCT_IBLOCK_ID' => $parent[$elementId]['IBLOCK_ID']
			);
		unset($parent);
	}

	/**
	 * OnAfterIBlockElementDelete event handler. Do not use directly.
	 *
	 * @param array $elementData		Element data.
	 *
	 * @return void
	 */
	public static function handlerAfterIblockElementDelete($elementData)
	{
		$elementId = $elementData['ID'];
		if (!isset(self::$offers[$elementId]))
			return;

		self::calculateComplete(
			self::$offers[$elementId]['CURRENT_PRODUCT'],
			self::$offers[$elementId]['PRODUCT_IBLOCK_ID'],
			Catalog\ProductTable::TYPE_SKU
		);

		unset(self::$offers[$elementId]);
	}

	/**
	 * OnIBlockElementSetPropertyValues event handler. Do not use directly.
	 *
	 * @param int $elementId							Element id.
	 * @param int $iblockId								Iblock id.
	 * @param array $newValues							New properties values.
	 * @param int|string|false $propertyIdentifyer		Property identifier.
	 * @param array $propertyList						Changed property list.
	 * @param array $currentValues						Current properties values.
	 *
	 * @return void
	 */
	public static function handlerIblockElementSetPropertyValues(
		$elementId,
		$iblockId,
		$newValues,
		$propertyIdentifyer,
		$propertyList,
		$currentValues
	)
	{
		$iblockData = \CCatalogSku::GetInfoByOfferIBlock($iblockId);
		if (empty($iblockData))
			return;

		$skuPropertyId = $iblockData['SKU_PROPERTY_ID'];
		if (!isset($propertyList[$skuPropertyId]))
			return;
		$skuPropertyCode = (string)$propertyList[$skuPropertyId]['CODE'];

		$foundValue = false;
		$skuValue = null;
		if ($propertyIdentifyer)
		{
			if (is_int($propertyIdentifyer))
			{
				$propertyId = $propertyIdentifyer;
			}
			else
			{
				$propertyId = (int)$propertyIdentifyer;
				if ($propertyId.'' != $propertyIdentifyer)
					$propertyId = ($skuPropertyCode == $propertyIdentifyer ? $skuPropertyId : 0);
			}
			if ($propertyId == $skuPropertyId)
			{
				$skuValue = $newValues;
				$foundValue = true;
			}
			unset($propertyId);
		}
		else
		{
			if (array_key_exists($skuPropertyId, $newValues))
			{
				$skuValue = $newValues[$skuPropertyId];
				$foundValue = true;
			}
			elseif (array_key_exists($skuPropertyCode, $newValues))
			{
				$skuValue = $newValues[$skuPropertyCode];
				$foundValue = true;
			}
		}
		if (!$foundValue)
			return;
		unset($foundValue);

		$newSkuPropertyValue = 0;
		if (!empty($skuValue))
		{
			if (!is_array($skuValue))
			{
				$newSkuPropertyValue = (int)$skuValue;
			}
			else
			{
				$skuValue = current($skuValue);
				if (!is_array($skuValue))
					$newSkuPropertyValue = (int)$skuValue;
				elseif (!empty($skuValue['VALUE']))
					$newSkuPropertyValue = (int)$skuValue['VALUE'];
			}
		}
		unset($skuValue);
		if ($newSkuPropertyValue < 0)
			$newSkuPropertyValue = 0;

		$currentSkuPropertyValue = 0;
		if (!empty($currentValues[$skuPropertyId]) && is_array($currentValues[$skuPropertyId]))
		{
			$currentSkuProperty = current($currentValues[$skuPropertyId]);
			if (!empty($currentSkuProperty['VALUE']))
				$currentSkuPropertyValue = (int)$currentSkuProperty['VALUE'];
			unset($currentSkuProperty);
		}
		if ($currentSkuPropertyValue < 0)
			$currentSkuPropertyValue = 0;

		// no error - first condition for event OnAfterIblockElementUpdate handler
		if (!static::allowedPropertyHandler() || ($currentSkuPropertyValue != $newSkuPropertyValue))
		{
			self::$offers[$elementId] = array(
				'CURRENT_PRODUCT' => $currentSkuPropertyValue,
				'NEW_PRODUCT' => $newSkuPropertyValue,
				'PRODUCT_IBLOCK_ID' => $iblockData['PRODUCT_IBLOCK_ID']
			);
		}
	}

	/**
	 * OnAfterIBlockElementSetPropertyValues event handler. Do not use directly.
	 *
	 * @param int $elementId							Element id.
	 * @param int $iblockId								Iblock id.
	 * @param array $newValues							New properties values.
	 * @param int|string|false $propertyIdentifyer		Property identifier.
	 *
	 * @return void
	 */
	public static function handlerAfterIBlockElementSetPropertyValues(
		$elementId,
		$iblockId,
		$newValues,
		$propertyIdentifyer
	)
	{
		if (!static::allowedPropertyHandler())
			return;

		self::calculateOfferChange($elementId, $iblockId);
	}

	/**
	 * OnIBlockElementSetPropertyValuesEx event handler. Do not use directly.
	 *
	 * @param int $elementId							Element id.
	 * @param int $iblockId								Iblock id.
	 * @param array $newValues							New properties values.
	 * @param array $propertyList						Changed property list.
	 * @param array $currentValues						Current properties values.
	 *
	 * @return void
	 */
	public static function handlerIblockElementSetPropertyValuesEx(
		$elementId,
		$iblockId,
		$newValues,
		$propertyList,
		$currentValues
	)
	{
		$iblockData = \CCatalogSku::GetInfoByOfferIBlock($iblockId);
		if (empty($iblockData))
			return;

		$skuPropertyId = $iblockData['SKU_PROPERTY_ID'];
		if (!isset($propertyList[$skuPropertyId]))
			return;
		$skuPropertyCode = (string)$propertyList[$skuPropertyId]['CODE'];

		$foundValue = false;
		$skuValue = null;
		if (array_key_exists($skuPropertyId, $newValues))
		{
			$skuValue = $newValues[$skuPropertyId];
			$foundValue = true;
		}
		elseif (array_key_exists($skuPropertyCode, $newValues))
		{
			$skuValue = $newValues[$skuPropertyCode];
			$foundValue = true;
		}
		if (!$foundValue)
			return;
		unset($foundValue);

		$newSkuPropertyValue = 0;
		if (!empty($skuValue))
		{
			if (!is_array($skuValue))
			{
				$newSkuPropertyValue = (int)$skuValue;
			}
			else
			{
				if (array_key_exists('VALUE', $skuValue))
				{
					$newSkuPropertyValue = (int)$skuValue['VALUE'];
				}
				else
				{
					foreach($skuValue as $row)
					{
						if (!is_array($row))
							$newSkuPropertyValue = (int)$row;
						elseif (array_key_exists('VALUE', $row))
							$newSkuPropertyValue = (int)$row['VALUE'];
					}
					unset($row);
				}
			}
		}
		unset($skuValue);
		if ($newSkuPropertyValue < 0)
			$newSkuPropertyValue = 0;

		$currentSkuPropertyValue = 0;
		if (!empty($currentValues[$skuPropertyId]) && is_array($currentValues[$skuPropertyId]))
		{
			$currentSkuProperty = current($currentValues[$skuPropertyId]);
			if (!empty($currentSkuProperty['VALUE']))
				$currentSkuPropertyValue = (int)$currentSkuProperty['VALUE'];
			unset($currentSkuProperty);
		}
		if ($currentSkuPropertyValue < 0)
			$currentSkuPropertyValue = 0;

		if (!static::allowedPropertyHandler() || ($currentSkuPropertyValue != $newSkuPropertyValue))
		{
			self::$offers[$elementId] = [
				'CURRENT_PRODUCT' => $currentSkuPropertyValue,
				'NEW_PRODUCT' => $newSkuPropertyValue,
				'PRODUCT_IBLOCK_ID' => $iblockData['PRODUCT_IBLOCK_ID']
			];
		}
	}

	/**
	 * OnAfterIBlockElementSetPropertyValuesEx event handler. Do not use directly.
	 *
	 * @param int $elementId							Element id.
	 * @param int $iblockId								Iblock id.
	 * @param array $newValues							New properties values.
	 * @param array $flags								Flags from \CIBlockElement::SetPropertyValuesEx.
	 *
	 * @return void
	 */
	public static function handlerAfterIblockElementSetPropertyValuesEx(
		$elementId,
		$iblockId,
		$newValues,
		$flags
	)
	{
		self::calculateOfferChange($elementId, $iblockId);
	}

	/**
	 * Return available and exist product offers.
	 *
	 * @param int $productId			Product id.
	 * @param int $iblockId				Iblock id.
	 *
	 * @return int
	 *
	 * @throws \Bitrix\Main\ArgumentException
	 */
	public static function getOfferState($productId, $iblockId = 0)
	{
		$result = self::OFFERS_ERROR;
		$productId = (int)$productId;
		if ($productId <= 0)
			return $result;
		$iblockId = (int)$iblockId;
		if ($iblockId <= 0)
			$iblockId = (int)\CIBlockElement::GetIBlockByID($productId);
		if ($iblockId <= 0)
			return $result;

		$result = self::OFFERS_NOT_EXIST;
		$offerList = \CCatalogSku::getOffersList($productId, $iblockId, array(), array('ID', 'ACTIVE'));
		if (!empty($offerList[$productId]))
		{
			$result = self::OFFERS_NOT_AVAILABLE;
			$activeOffers = array_filter($offerList[$productId], '\Bitrix\Catalog\Product\Sku::filterActive');
			if (!empty($activeOffers))
			{
				$existOffers = Catalog\ProductTable::getList(array(
					'select' => array('ID', 'AVAILABLE'),
					'filter' => array('@ID' => array_keys($activeOffers), '=AVAILABLE' => Catalog\ProductTable::STATUS_YES),
					'limit' => 1
				))->fetch();
				if (!empty($existOffers))
					$result = self::OFFERS_AVAILABLE;
				unset($existOffers);
			}
			unset($activeOffers);
		}
		unset($offerList);

		return $result;
	}

	/**
	 * Update sku product available.
	 * @deprecated deprecated since catalog 17.6.0
	 * @see Sku::calculateComplete
	 *
	 * @param int $productId			Product id.
	 * @param int|null $iblockId		Iblock id.
	 *
	 * @return bool
	 */
	protected static function updateProductAvailable($productId, $iblockId)
	{
		$productId = (int)$productId;
		if ($productId <= 0)
			return false;

		self::calculateComplete($productId, $iblockId, Catalog\ProductTable::TYPE_SKU);

		return true;
	}

	/**
	 * Update offer product type.
	 * @deprecated deprecated since catalog 17.6.0
	 * @see \Bitrix\Catalog\Model\Product::update
	 *
	 * @param int $offerId				Offer id.
	 * @param int $type					Product type.
	 *
	 * @return bool
	 *
	 * @throws \Exception
	 */
	protected static function updateOfferType($offerId, $type)
	{
		$offerId = (int)$offerId;
		$type = (int)$type;
		if ($offerId <= 0 || ($type != Catalog\ProductTable::TYPE_OFFER && $type != Catalog\ProductTable::TYPE_FREE_OFFER))
			return false;
		static::disableUpdateAvailable();
		$updateResult = Catalog\Model\Product::update($offerId, array('TYPE' => $type));
		$result = $updateResult->isSuccess();
		static::enableUpdateAvailable();
		return $result;
	}

	/**
	 * Enable property handlers.
	 *
	 * @return void
	 */
	protected static function enablePropertyHandler()
	{
		self::$allowPropertyHandler++;
	}

	/**
	 * Disable property handlers.
	 *
	 * @return void
	 */
	protected static function disablePropertyHandler()
	{
		self::$allowPropertyHandler--;
	}

	/**
	 * Return is enabled property handlers.
	 *
	 * @return bool
	 */
	protected static function allowedPropertyHandler()
	{
		return (self::$allowPropertyHandler >= 0);
	}

	/**
	 * Method for array_filter.
	 *
	 * @param array $row			Product/ Offer data.
	 *
	 * @return bool
	 */
	protected static function filterActive(array $row)
	{
		return (isset($row['ACTIVE']) && $row['ACTIVE'] == 'Y');
	}

	/**
	 * Return separate sku mode (catalog option).
	 * @internal
	 *
	 * @return bool
	 *
	 * @throws Main\ArgumentNullException
	 * @throws Main\ArgumentOutOfRangeException
	 */
	private static function isSeparateSkuMode()
	{
		if (self::$separateSkuMode === null)
			self::$separateSkuMode = (string)Main\Config\Option::get('catalog', 'show_catalog_tab_with_offers') == 'Y';

		return self::$separateSkuMode;
	}

	/**
	 * Calculate available for product with sku as simple product. Compatible only.
	 * @internal
	 *
	 * @param int $productId				Product id.
	 * @param array $productFields			Product fields (optional).
	 *
	 * @return array
	 *
	 * @throws Main\ArgumentException
	 */
	private static function getParentDataAsProduct($productId, array $productFields = array())
	{
		if (!isset($productFields['QUANTITY'])
			|| !isset($productFields['QUANTITY_TRACE'])
			|| !isset($productFields['CAN_BUY_ZERO'])
		)
			$productFields = array_merge(Catalog\Model\Product::getCacheItem($productId, true), $productFields);

		return array(
			'TYPE' => Catalog\ProductTable::TYPE_SKU,
			'AVAILABLE' => Catalog\ProductTable::calculateAvailable($productFields)
		);
	}

	/**
	 * Returns the current calculated availability of the product if it is necessary to update it.
	 * @internal
	 *
	 * @param int $productId            Product id.
	 * @param array $productFields      Current product values. Can be empty.
	 *
	 * @return array
	 */
	private static function getProductAvailable($productId, array $productFields)
	{
		$fields = array();

		if (isset($productFields['AVAILABLE']))
			return $fields;

		if (
			isset($productFields['QUANTITY'])
			|| isset($productFields['QUANTITY_TRACE'])
			|| isset($productFields['CAN_BUY_ZERO'])
		)
		{
			if (
				!isset($productFields['QUANTITY'])
				|| !isset($productFields['QUANTITY_TRACE'])
				|| !isset($productFields['CAN_BUY_ZERO'])
			)
				$productFields = array_merge(Catalog\Model\Product::getCacheItem($productId, true), $productFields);
			$fields['AVAILABLE'] = Catalog\ProductTable::calculateAvailable($productFields);
		}

		return $fields;
	}

	/**
	 * Change parent available.
	 * @internal
	 *
	 * @param int $parentId             Parent id.
	 * @param int $parentIblockId       Parent iblock id.
	 * @return bool
	 */
	private static function updateParentAvailable($parentId, $parentIblockId)
	{
		$result = true;

		$parentIBlock = \CCatalogSku::GetInfoByIblock($parentIblockId);
		if (
			empty($parentIBlock)
			|| (self::isSeparateSkuMode() && $parentIBlock['CATALOG_TYPE'] == \CCatalogSku::TYPE_FULL)
		)
			return $result;

		$parentFields = static::getDefaultParentSettings(static::getOfferState(
			$parentId,
			$parentIblockId
		));

		self::disableUpdateAvailable();
		$iterator = Catalog\Model\Product::getList(array(
			'select' => array('ID'),
			'filter' => array('=ID' => $parentId)
		));
		$row = $iterator->fetch();
		if (!empty($row))
		{
			$updateResult = Catalog\Model\Product::update($parentId, $parentFields);
		}
		else
		{
			$parentFields['ID'] = $parentId;
			$updateResult = Catalog\Model\Product::add($parentFields);
		}

		if (!$updateResult->isSuccess())
			$result = false;
		unset($updateResult);

		self::enableUpdateAvailable();

		return $result;
	}

	/**
	 * Check product type for unknown id and transfer to offer list or parent product list.
	 * @internal
	 *
	 * @return void
	 *
	 * @throws Main\ArgumentException
	 */
	private static function updateDeferredSkuList()
	{
		if (!empty(self::$deferredUnknown))
		{
			$list = array_keys(self::$deferredUnknown);
			sort($list);
			foreach (array_chunk($list, 500) as $pageIds)
			{
				$iterator = Catalog\ProductTable::getList(array(
					'select' => array('ID', 'TYPE'),
					'filter' => array(
						'@ID' => $pageIds,
						'@TYPE' => array(Catalog\ProductTable::TYPE_SKU, Catalog\ProductTable::TYPE_OFFER)
					)
				));
				while ($row = $iterator->fetch())
				{
					$row['ID'] = (int)$row['ID'];
					if ($row['TYPE'] == Catalog\ProductTable::TYPE_SKU)
						self::migrateCalculateData(self::$deferredUnknown, self::$deferredSku, $row['ID']);
					else
						self::migrateCalculateData(self::$deferredUnknown, self::$deferredOffers, $row['ID']);
				}
			}
			unset($row, $iterator, $pageIds, $list);

			self::$deferredUnknown = array();
		}
		if (!empty(self::$deferredOffers))
		{
			$productList = \CCatalogSku::getProductList(array_keys(self::$deferredOffers));
			if (!empty($productList))
			{
				foreach ($productList as $id => $row)
					self::transferCalculationData(self::$deferredOffers, self::$deferredSku, $id, $row['ID'], $row['IBLOCK_ID']);
				unset($id, $row);
			}
			unset($productList);

			self::$deferredOffers = array();
		}
	}

	/**
	 * Fill entity list for calculation.
	 * @internal
	 *
	 * @param array &$list			Item storage.
	 * @param int $id				Item id.
	 * @param null|int $iblockId	Iblock id (null if unknown).
	 *
	 * @return void
	 */
	private static function setCalculateData(array &$list, $id, $iblockId)
	{
		static $priceTypes = null,
			$priceTypeKeys;

		self::$calculateAvailable = true;

		//TODO:: replace \CCatalogGroup::GetListArray after create cached d7 method
		if ($priceTypes === null)
		{
			$priceTypes = array_keys(\CCatalogGroup::GetListArray());
			$priceTypeKeys = array_fill_keys($priceTypes, true);
		}
		self::$calculatePriceTypes = $priceTypeKeys;

		if ($iblockId === null)
		{
			if (isset($list[$id]))
				$iblockId = $list[$id]['IBLOCK_ID'];
		}

		$list[$id] = array(
			'IBLOCK_ID' => $iblockId,
			'AVAILABLE' => true,
			'PRICE' => self::$calculatePriceTypes
		);
	}

	/**
	 * Fill price type list for calculation.
	 * @internal
	 *
	 * @param array &$list				Item storage.
	 * @param int $id					Product id.
	 * @param null|int $iblockId		Iblock id.
	 * @param array $priceTypes			Price types (empty if need all).
	 *
	 * @return void
	 */
	private static function setCalculatePriceTypes(array &$list, $id, $iblockId, array $priceTypes)
	{
		static $allPriceTypes = null;

		//TODO:: replace \CCatalogGroup::GetListArray after create cached d7 method
		if ($allPriceTypes === null)
			$allPriceTypes = array_keys(\CCatalogGroup::GetListArray());

		if (empty($priceTypes))
			$priceTypes = $allPriceTypes;

		foreach ($priceTypes as $typeId)
			self::$calculatePriceTypes[$typeId] = true;

		if ($iblockId === null)
		{
			if (isset($list[$id]))
				$iblockId = $list[$id]['IBLOCK_ID'];
		}

		if (!isset($list[$id]))
		{
			$list[$id] = array(
				'IBLOCK_ID' => $iblockId,
				'PRICE' => array_fill_keys($priceTypes, true)
			);
		}
		elseif (!isset($list[$id]['PRICE']))
		{
			if ($iblockId !== null)
				$list[$id]['IBLOCK_ID'] = $iblockId;
			$list[$id]['PRICE'] = array_fill_keys($priceTypes, true);
		}
		else
		{
			if ($iblockId !== null)
				$list[$id]['IBLOCK_ID'] = $iblockId;
			foreach ($priceTypes as $typeId)
				$list[$id]['PRICE'][$typeId] = true;
		}

		unset($typeId);
	}

	/**
	 * Remove data from unknown list to parent product list or offer list.
	 * @internal
	 *
	 * @param array &$source		Source storage.
	 * @param array &$destination	Destination storage
	 * @param int $id				Product id.
	 *
	 * @return void
	 */
	private static function migrateCalculateData(array &$source, array &$destination, $id)
	{
		if (!isset($source[$id]))
			return;

		if (isset($destination[$id]))
		{
			if (isset($source[$id]['AVAILABLE']))
				self::setCalculateData($destination, $id, $source[$id]['IBLOCK_ID']);
			elseif (isset($source[$id]['PRICE']))
				self::setCalculatePriceTypes($destination, $id, $source[$id]['IBLOCK_ID'], array_keys($source[$id]['PRICE']));
		}
		else
		{
			$destination[$id] = $source[$id];
		}
		unset($source[$id]);
	}

	/**
	 * Transfer data from unknown list to parent product list or offer list with change id.
	 * @internal
	 *
	 * @param array &$source		Source storage.
	 * @param array &$destination	Destination storage
	 * @param int $sourceId			Product source id.
	 * @param int $destinationId	Product destination id.
	 * @param null|int $iblockId	Iblock id (null, if unknown).
	 *
	 * @return void
	 */
	private static function transferCalculationData(array &$source, array &$destination, $sourceId, $destinationId, $iblockId)
	{
		if (!isset($source[$sourceId]))
			return;

		if (isset($destination[$destinationId]))
		{
			if (isset($source[$sourceId]['AVAILABLE']))
				self::setCalculateData($destination, $destinationId, $iblockId);
			elseif (isset($source[$sourceId]['PRICE']))
				self::setCalculatePriceTypes($destination, $destinationId, $iblockId, array_keys($source[$sourceId]['PRICE']));
		}
		else
		{
			$destination[$destinationId] = $source[$sourceId];
			$destination[$destinationId]['IBLOCK_ID'] = $iblockId;
		}
		unset($source[$sourceId]);
	}

	/**
	 * Clear internal calculate data.
	 * @internal
	 *
	 * @return void
	 */
	private static function clearStepData()
	{
		self::$skuExist = array();
		self::$skuAvailable = array();
		self::$offersIds = array();
		self::$offersMap = array();
		self::$skuPrices = array();
	}

	/**
	 * Get iblock ids for products.
	 * @internal
	 *
	 * @return void
	 */
	private static function loadProductIblocks()
	{
		$listIds = array();
		foreach (array_keys(self::$deferredSku) as $id)
		{
			if (self::$deferredSku[$id]['IBLOCK_ID'] === null)
				$listIds[] = $id;
		}
		unset($id);
		if (!empty($listIds))
		{
			$data = \CIBlockElement::GetIBlockByIDList($listIds);
			foreach ($data as $id => $iblockId)
			{
				self::$deferredSku[$id]['IBLOCK_ID'] = $iblockId;
			}
			unset($id, $iblockId);
		}
		unset($listIds);
	}

	/**
	 * Load parent product data (offers exists and state, exist in database, etc).
	 * @internal
	 *
	 * @param array $listIds	Product ids.
	 *
	 * @return void
	 */
	private static function loadProductData(array $listIds)
	{
		$iterator = Catalog\Model\Product::getList(array(
			'select' => array('ID'),
			'filter' => array('@ID' => $listIds)
		));
		while ($row = $iterator->fetch())
		{
			$row['ID'] = (int)$row['ID'];
			self::$skuExist[$row['ID']] = true;
		}
		unset($row, $iterator);
		//TODO: change CATALOG_AVAILABLE to PRODUCT.AVAILABLE
		$offers = \CCatalogSku::getOffersList(
			$listIds,
			0,
			array(),
			array('ID', 'ACTIVE', 'CATALOG_AVAILABLE')
		);
		foreach ($listIds as $id)
		{
			self::$skuAvailable[$id] = self::OFFERS_NOT_EXIST;
			if (empty($offers[$id]))
				continue;

			self::$skuAvailable[$id] = self::OFFERS_NOT_AVAILABLE;
			$allOffers = array();
			$availableOffers = array();
			foreach ($offers[$id] as $offerId => $row)
			{
				$allOffers[] = $offerId;
				//TODO: change CATALOG_AVAILABLE to PRODUCT.AVAILABLE
				if ($row['ACTIVE'] != 'Y' || $row['CATALOG_AVAILABLE'] != 'Y')
					continue;
				self::$skuAvailable[$id] = self::OFFERS_AVAILABLE;
				$availableOffers[] = $offerId;
			}
			self::$skuPrices[$id] = array();
			if (self::$skuAvailable[$id] == self::OFFERS_AVAILABLE)
			{
				foreach ($availableOffers as $offerId)
				{
					self::$offersMap[$offerId] = $id;
					self::$offersIds[] = $offerId;
				}
			}
			else
			{
				foreach ($allOffers as $offerId)
				{
					self::$offersMap[$offerId] = $id;
					self::$offersIds[] = $offerId;
				}
			}
		}
		unset($offerId, $availableOffers, $allOffers, $id);

		if (!self::isSeparateSkuMode())
			self::loadProductPrices();
	}

	/**
	 * Save available for parent products.
	 * @internal
	 *
	 * @param array $listIds	Product ids.
	 *
	 * @return void
	 */
	private static function updateProductData(array $listIds)
	{
		$separateMode = self::isSeparateSkuMode();
		if (self::$calculateAvailable)
		{
			foreach ($listIds as $id)
			{
				if (empty(self::$deferredSku[$id]['AVAILABLE']))
					continue;
				if (empty(self::$deferredSku[$id]['IBLOCK_ID']))
					continue;
				if (!isset(self::$skuAvailable[$id]))
					continue;

				$iblockData = \CCatalogSku::GetInfoByIBlock(self::$deferredSku[$id]['IBLOCK_ID']);
				if (empty($iblockData))
					continue;

				$fields = self::getDefaultParentSettings(
					self::$skuAvailable[$id],
					$iblockData['CATALOG_TYPE'] == \CCatalogSku::TYPE_PRODUCT
				);
				if (empty($fields))
					continue;

				// for separate only
				if ($separateMode)
					$fields = ['TYPE' => $fields['TYPE']];

				if (isset(self::$skuExist[$id]))
				{
					$result = Catalog\Model\Product::update($id, $fields);
					unset(self::$skuExist[$id]);
				}
				else
				{
					$fields['ID'] = $id;
					$result = Catalog\Model\Product::add($fields);
				}
				if (!$result->isSuccess())
				{

				}
			}
			unset($result, $id);
		}

		if (!$separateMode)
			self::updateProductPrices($listIds);
	}

	/**
	 * Load exist parent product prices.
	 * @internal
	 *
	 * @return void
	 *
	 * @throws Main\ArgumentException
	 */
	private static function loadProductPrices()
	{
		if (empty(self::$calculatePriceTypes) || empty(self::$offersIds))
			return;

		sort(self::$offersIds);
		foreach (array_chunk(self::$offersIds, 500) as $pageOfferIds)
		{
			$filter = Main\Entity\Query::filter();
			$filter->whereIn('PRODUCT_ID', $pageOfferIds);
			$filter->whereIn('CATALOG_GROUP_ID', self::$calculatePriceTypes);
			$filter->where(Main\Entity\Query::filter()->logic('or')->where('QUANTITY_FROM', '<=', 1)->whereNull('QUANTITY_FROM'));
			$filter->where(Main\Entity\Query::filter()->logic('or')->where('QUANTITY_TO', '>=', 1)->whereNull('QUANTITY_TO'));

			$iterator = Catalog\PriceTable::getList(array(
				'select' => array(
					'PRODUCT_ID', 'CATALOG_GROUP_ID', 'PRICE', 'CURRENCY',
					'PRICE_SCALE', 'TMP_ID'  //TODO: add MEASURE_RATIO_ID
				),
				'filter' => $filter,
				'order' => array('PRODUCT_ID' => 'ASC', 'CATALOG_GROUP_ID' => 'ASC')
			));
			while ($row = $iterator->fetch())
			{
				/*
				if ($row['MEASURE_RATIO_ID'] !== null)
					continue;
				unset($row['MEASURE_RATIO_ID']);
				*/

				$typeId = (int)$row['CATALOG_GROUP_ID'];
				$offerId = (int)$row['PRODUCT_ID'];
				$productId = self::$offersMap[$offerId];
				if (!isset(self::$deferredSku[$productId]['PRICE'][$typeId]))
					continue;
				unset($row['PRODUCT_ID']);

				if (!isset(self::$skuPrices[$productId][$typeId]))
					self::$skuPrices[$productId][$typeId] = $row;
				elseif (self::$skuPrices[$productId][$typeId]['PRICE_SCALE'] > $row['PRICE_SCALE'])
					self::$skuPrices[$productId][$typeId] = $row;
			}
			unset($row, $iterator);
			unset($filter);
		}
	}

	/**
	 * Update parent product prices.
	 * @internal
	 *
	 * @param array $listIds	Product ids.
	 *
	 * @return void
	 *
	 * @throws Main\ArgumentException
	 * @throws Main\Db\SqlQueryException
	 * @throws \Exception
	 */
	private static function updateProductPrices(array $listIds)
	{
		if (empty(self::$calculatePriceTypes))
			return;

		$process = true;

		if (!empty(self::$skuPrices))
		{
			$existIds = array();
			$existIdsByType = array();
			$iterator = Catalog\PriceTable::getList(array(
				'select' => array('ID', 'CATALOG_GROUP_ID', 'PRODUCT_ID'),
				'filter' => array('@PRODUCT_ID' => $listIds, '@CATALOG_GROUP_ID' => self::$calculatePriceTypes),
				'order' => array('ID' => 'ASC')
			));
			while ($row = $iterator->fetch())
			{
				$row['ID'] = (int)$row['ID'];
				$priceTypeId = (int)$row['CATALOG_GROUP_ID'];
				$productId = (int)$row['PRODUCT_ID'];
				$existIds[$row['ID']] = $row['ID'];
				if (!isset($existIdsByType[$productId]))
					$existIdsByType[$productId] = array();
				if (!isset($existIdsByType[$productId][$priceTypeId]))
					$existIdsByType[$productId][$priceTypeId] = array();
				$existIdsByType[$productId][$priceTypeId][] = $row['ID'];
			}
			unset($row, $iterator);
			foreach ($listIds as $productId)
			{
				if (!isset(self::$skuPrices[$productId]))
					continue;

				foreach (array_keys(self::$skuPrices[$productId]) as $resultPriceType)
				{
					$rowId = null;
					$row = self::$skuPrices[$productId][$resultPriceType];
					if (!empty($existIdsByType[$productId][$resultPriceType]))
					{
						$rowId = array_shift($existIdsByType[$productId][$resultPriceType]);
						unset($existIds[$rowId]);
					}
					if ($rowId === null)
					{
						$row['PRODUCT_ID'] = $productId;
						$row['CATALOG_GROUP_ID'] = $resultPriceType;
						$rowResult = Catalog\PriceTable::add($row);
					}
					else
					{
						$rowResult = Catalog\PriceTable::update($rowId, $row);
					}
					if (!$rowResult->isSuccess())
					{
						$process = false;
						break;
					}
				}
			}
			unset($row, $rowResult, $resultPriceType);

			unset($existIdsByType);

			if ($process)
			{
				if (!empty($existIds))
				{
					$conn = Main\Application::getConnection();
					$helper = $conn->getSqlHelper();
					$tableName = $helper->quote(Catalog\PriceTable::getTableName());
					foreach (array_chunk($existIds, 500) as $pageIds)
					{
						$conn->queryExecute(
							'delete from '.$tableName.' where '.$helper->quote('ID').' in ('.implode(',', $pageIds).')'
						);
					}
					unset($pageIds);
					unset($helper, $conn);
				}
				unset($existIds);
			}
		}
		else
		{
			$conn = Main\Application::getConnection();
			$helper = $conn->getSqlHelper();
			$conn->queryExecute(
				'delete from '.$helper->quote(Catalog\PriceTable::getTableName()).
				' where '.$helper->quote('PRODUCT_ID').' in ('.implode(',', $listIds).')'.
				' and '.$helper->quote('CATALOG_GROUP_ID').' in ('.implode(',', self::$calculatePriceTypes).')'
			);
			unset($helper, $conn);
		}
	}

	/**
	 * Update parent product facet index.
	 * @internal
	 *
	 * @param array $listIds	Product ids.
	 *
	 * @return void
	 */
	private static function updateProductFacetIndex(array $listIds)
	{
		foreach ($listIds as $id)
		{
			if (empty(self::$deferredSku[$id]['IBLOCK_ID']))
				continue;
			if (!isset(self::$skuAvailable[$id]))
				continue;
			Iblock\PropertyIndex\Manager::updateElementIndex(
				self::$deferredSku[$id]['IBLOCK_ID'],
				$id
			);
		}
	}

	/**
	 * Update parent product data from iblock event handlers.
	 *
	 * @param int $elementId	Offer id.
	 * @param int $iblockId		Offer iblock id.
	 *
	 * @return void
	 */
	private static function calculateOfferChange($elementId, $iblockId)
	{
		if (!isset(self::$offers[$elementId]))
			return;

		$iblockData = \CCatalogSku::GetInfoByOfferIBlock($iblockId);
		if (!empty($iblockData))
		{
			$existCurrentProduct = (self::$offers[$elementId]['CURRENT_PRODUCT'] > 0);
			$existNewProduct = (self::$offers[$elementId]['NEW_PRODUCT'] > 0);
			if ($existCurrentProduct > 0)
			{
				self::calculateComplete(
					self::$offers[$elementId]['CURRENT_PRODUCT'],
					$iblockData['PRODUCT_IBLOCK_ID'],
					Catalog\ProductTable::TYPE_SKU
				);
			}
			if ($existNewProduct > 0)
			{
				self::calculateComplete(
					self::$offers[$elementId]['NEW_PRODUCT'],
					$iblockData['PRODUCT_IBLOCK_ID'],
					Catalog\ProductTable::TYPE_SKU
				);
			}
			if (!$existCurrentProduct || !$existNewProduct)
			{
				self::disableUpdateAvailable();
				$type = (
					$existNewProduct
					? Catalog\ProductTable::TYPE_OFFER
					: Catalog\ProductTable::TYPE_FREE_OFFER
				);
				$result = Catalog\Model\Product::update($elementId, array('TYPE' => $type));
				unset($result);
				self::enableUpdateAvailable();
			}
			unset($existNewProduct, $existCurrentProduct);
		}
		unset(self::$offers[$elementId]);
	}
}