Как известно сортировка по цене и доступности товара в битриксе не такая уж и тривиальная задача, особенно если для товаров существуют торговые предложения. Существуют два варианта решения данной проблемы:
- Создание свойства MINIMUM_PRICE и запись в него минимальной цены торговых предложений, создание свойства IS_AVAILABLE с записью в него 1, если количество больше 0, и 0, если количество меньше 0 либо пустое, и в дальнейшем сортировка по ним
- Кастомизация 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' ) );