Your IP : 18.190.207.82
<?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]);
}
}