Блог о программировании на PHP, Yii2, 1C-Bitrix

Сортировка по цене и количеству в bitrix’e или новые костыли.

Как известно сортировка по цене и доступности товара в битриксе не такая уж и тривиальная задача, особенно если для товаров существуют торговые предложения. Существуют два варианта решения данной проблемы:

  1. Создание свойства MINIMUM_PRICE и запись в него минимальной цены торговых предложений, создание свойства IS_AVAILABLE с записью в него 1, если количество больше 0, и 0, если количество меньше 0 либо пустое, и в дальнейшем сортировка по ним
  2. Кастомизация GetList у CIBlockElement с добавлением выборки по цене и количеству.

Разберем оба варианта.

Первый вариант хорош, когда необходимо быстро и безболезненно сделать сортировку на маленьком проекте, где каталог насчитывает до 20к товаров. Код событий обновления свойства MINIMUM_PRICE предоставлен ниже, а так же он поставлялся в далеком 2012 году в файле init.php при первоначальной установке битрикса.

<? /*Version 0.3 2011-04-25*/
AddEventHandler( "iblock", "OnAfterIBlockElementUpdate", "DoIBlockAfterSave" );
AddEventHandler( "iblock", "OnAfterIBlockElementAdd", "DoIBlockAfterSave" );
AddEventHandler( "catalog", "OnPriceAdd", "DoIBlockAfterSave" );
AddEventHandler( "catalog", "OnPriceUpdate", "DoIBlockAfterSave" );
function DoIBlockAfterSave( $arg1, $arg2 = false ){
    $ELEMENT_ID = false;
    $IBLOCK_ID = false;
    $OFFERS_IBLOCK_ID = false;
    $OFFERS_PROPERTY_ID = false;
    if( CModule::IncludeModule( 'currency' ) )
        $strDefaultCurrency = CCurrency::GetBaseCurrency();  //Check for catalog event  if(is_array($arg2) && $arg2["PRODUCT_ID"] > 0)
	{
		//Get iblock element
		$rsPriceElement = CIBlockElement::GetList( array(), array(
				"ID" => $arg2["PRODUCT_ID"],
			), false, false, array( "ID", "IBLOCK_ID" ) );
		if( $arPriceElement = $rsPriceElement->Fetch() ){
			$arCatalog = CCatalog::GetByID( $arPriceElement["IBLOCK_ID"] );
			if( is_array( $arCatalog ) ){
				//Check if it is offers iblock
				if( $arCatalog["OFFERS"] == "Y" ){
					//Find product element
					$rsElement = CIBlockElement::GetProperty( $arPriceElement["IBLOCK_ID"], $arPriceElement["ID"], "sort", "asc", array( "ID" => $arCatalog["SKU_PROPERTY_ID"] ) );
					$arElement = $rsElement->Fetch();
					if( $arElement && $arElement["VALUE"] > 0 ){
						$ELEMENT_ID = $arElement["VALUE"];
						$IBLOCK_ID = $arCatalog["PRODUCT_IBLOCK_ID"];
						$OFFERS_IBLOCK_ID = $arCatalog["IBLOCK_ID"];
						$OFFERS_PROPERTY_ID = $arCatalog["SKU_PROPERTY_ID"];
					}
				}//or iblock which has offers
				elseif( $arCatalog["OFFERS_IBLOCK_ID"] > 0 ){
					$ELEMENT_ID = $arPriceElement["ID"];
					$IBLOCK_ID = $arPriceElement["IBLOCK_ID"];
					$OFFERS_IBLOCK_ID = $arCatalog["OFFERS_IBLOCK_ID"];
					$OFFERS_PROPERTY_ID = $arCatalog["OFFERS_PROPERTY_ID"];
				}//or it's regular catalog
				else{
					$ELEMENT_ID = $arPriceElement["ID"];
					$IBLOCK_ID = $arPriceElement["IBLOCK_ID"];
					$OFFERS_IBLOCK_ID = false;
					$OFFERS_PROPERTY_ID = false;
				}
			}
		}
	}
	//Check for iblock event
elseif
	( is_array( $arg1 ) && $arg1["ID"] > 0 && $arg1["IBLOCK_ID"] > 0 )
 {
	 //Check if iblock has offers
	 $arOffers = CIBlockPriceTools::GetOffersIBlock( $arg1["IBLOCK_ID"] );
	 if( is_array( $arOffers ) ){
		 $ELEMENT_ID = $arg1["ID"];
		 $IBLOCK_ID = $arg1["IBLOCK_ID"];
		 $OFFERS_IBLOCK_ID = $arOffers["OFFERS_IBLOCK_ID"];
		 $OFFERS_PROPERTY_ID = $arOffers["OFFERS_PROPERTY_ID"];
	 }
 }

 if( $ELEMENT_ID ){
	 static $arPropCache = array();
	 if( !array_key_exists( $IBLOCK_ID, $arPropCache ) ){
		 //Check for MINIMAL_PRICE property
		 $rsProperty = CIBlockProperty::GetByID( "MINIMUM_PRICE", $IBLOCK_ID );
		 $arProperty = $rsProperty->Fetch();
		 if( $arProperty )
			 $arPropCache[$IBLOCK_ID] = $arProperty["ID"];else
			 $arPropCache[$IBLOCK_ID] = false;
	 }

	 if( $arPropCache[$IBLOCK_ID] ){
		 //Compose elements filter
		 if( $OFFERS_IBLOCK_ID ){
			 $rsOffers = CIBlockElement::GetList( array(), array(
					 "IBLOCK_ID" => $OFFERS_IBLOCK_ID, "PROPERTY_".$OFFERS_PROPERTY_ID => $ELEMENT_ID,
				 ), false, false, array( "ID" ) );
			 while( $arOffer = $rsOffers->Fetch() )
				 $arProductID[] = $arOffer["ID"];

			 if( !is_array( $arProductID ) )
				 $arProductID = array( $ELEMENT_ID );
		 }else
			 $arProductID = array( $ELEMENT_ID );

		 $minPrice = false;
		 $maxPrice = false;
		 //Get prices
		 $rsPrices = CPrice::GetList( array(), array(
				 "PRODUCT_ID" => $arProductID,
			 ) );
		 while( $arPrice = $rsPrices->Fetch() ){
			 if( CModule::IncludeModule( 'currency' ) && $strDefaultCurrency != $arPrice['CURRENCY'] )
				 $arPrice["PRICE"] = CCurrencyRates::ConvertCurrency( $arPrice["PRICE"], $arPrice["CURRENCY"], $strDefaultCurrency );

			 $PRICE = $arPrice["PRICE"];

			 if( $minPrice === false || $minPrice > $PRICE )
				 $minPrice = $PRICE;

			 if( $maxPrice === false || $maxPrice < $PRICE ) 				 $maxPrice = $PRICE; 		 }  //Save found minimal price into property  if($minPrice !== false)  {  CIBlockElement::SetPropertyValuesEx(  $ELEMENT_ID,  $IBLOCK_ID,  array(  "MINIMUM_PRICE" => $minPrice,
		 "MAXIMUM_PRICE" => $maxPrice,
 )
 );
 }
 }
 }
}
?>

Второй вариант интереснее и элегантнее, будет лучше для проектов с наличием торговых предложений овер 40к, ну или если можно отслеживать обновления ядра и без проблем править его файлы.

Идем в файлик /bitrix/modules/iblock/classes/mysql/iblockelement.php, ищем фукнцию GetList и правим:

Во-первых, убираем сортировку по HAS_QUANTITY из arOrder, иначе будут глюки:

$has_catalog_quantity = false;
$has_catalog_quantity_order = '';
if( array_key_exists( 'HAS_CATALOG_QUANTITY', $arOrder ) ){
	$has_catalog_quantity = true;
	$has_catalog_quantity_order = $arOrder['HAS_CATALOG_QUANTITY'];
	unset( $arOrder['HAS_CATALOG_QUANTITY'] );
}

Во-вторых, добавляем дополнительные джойны торговых предложений, цен и продуктов:

$arSKU = [];

if( array_key_exists( 'PRICE_EXT', $arOrder ) || $has_catalog_quantity ){
	$arSKU = \CCatalogSKU::GetInfoByProductIBlock( $arFilter['IBLOCK_ID'] );
	
	if( $arSKU ){
		$sFrom .= "\t\t\t\tLEFT JOIN b_iblock_element_property AS obp
				ON (
					BE.ID = obp.VALUE
						AND
					obp.IBLOCK_PROPERTY_ID = '".$arSKU['SKU_PROPERTY_ID']."'
				)
			LEFT JOIN b_iblock_element AS obe
				ON (
					obe.ID = obp.IBLOCK_ELEMENT_ID
						AND
					obe.ACTIVE = 'Y'
						AND
					obe.IBLOCK_ID = '".$arSKU['IBLOCK_ID']."'
				)\n";
	}
}

if( array_key_exists( 'PRICE_EXT', $arOrder ) ){
	$sFrom .= "\t\t\t\tLEFT JOIN b_catalog_price AS bcprice
		ON (
			bcprice.CATALOG_GROUP_ID = '32'
				AND
			(
				".( $arSKU ?
						"obe.ID = bcprice.PRODUCT_ID
							OR
						BE.ID = bcprice.PRODUCT_ID"
					:
						"BE.ID = bcprice.PRODUCT_ID"
					
				)."
			)
		)\n";
}

if( $has_catalog_quantity ){
	$sFrom .= "\t\t\t
		LEFT JOIN b_catalog_product AS bcproduct";
	
	if( $arSKU ){
		$sFrom .= " ON
		(
			obe.ID = bcproduct.ID
				OR
			BE.ID = bcproduct.ID
		)";
	}else{
		$sFrom .= " ON BE.ID = bcproduct.ID";
	}
	
	$sFrom .= "\n";
}

перед закрытием области sFrom

//******************END OF FROM PART********************************************

И перед формированием селекта делаем запрос на сортировку:

$sOrderBy = '';
if( array_key_exists( 'PRICE_EXT', $arOrder ) || $has_catalog_quantity ){
	$sOrderBy .= " GROUP BY ID ";
}

if( $has_catalog_quantity ){
	$sOrderBy .= "\n\t\t\tORDER BY "."
			CASE
				WHEN
					max( bcproduct.QUANTITY ) = 0 OR max( bcproduct.QUANTITY ) is NULL OR max( bcproduct.QUANTITY ) = ''
				THEN
					1
				ELSE
					0
				END
			".$has_catalog_quantity_order;
}

if( array_key_exists( 'PRICE_EXT', $arOrder ) ){
	if( $sOrderBy == '' ){
		$sOrderBy .= "\n\t\t\tORDER BY ";
	}else{
		$sOrderBy .= ", ";
	}
	
	$sOrderBy .= "max( bcprice.PRICE ) ".$arOrder['PRICE_EXT'];
}else{
	foreach( $arSqlOrder as $i => $val ){
		if( strlen( $val ) ){
			if( $sOrderBy == '' ){
				$sOrderBy .= "\n\t\t\tORDER BY ";
			}else{
				$sOrderBy .= ", ";
			}
			
			$sOrderBy .= $val.' ';
		}
	}
}

Ну и собственно вызов осуществляется вот так:

CIBlockElement::getList( array( 'HAS_QUANTITY' => 'ASC', 'PRICE_EXT' => 'ASC' ) );