Веб

Bitrix: скрыть товары без цены для группы пользователя

Как в 1C-Bitrix скрыть товары от пользователей группы, если для их bitrix тип цены не задана цена. Фильтрация по bitrix группы пользователя через события OnBeforeIBlockElementGetList и OnAfter. Примеры кода, учет SKU и кеша.

Как в 1C-Bitrix скрыть товары от пользователей группы, если для их типа цены не задана цена?

Описание проблемы:

  • Есть товары на складе.
  • 30 типов цен, привязанных к 30 группам пользователей.
  • Для конкретного пользователя товары без цены в его типе цен отображаются как «нет в наличии».
  • Нужно полностью скрыть такие товары от пользователя.

Стандартные настройки не работают.

Попытка решения через события (не полностью работает):

php
// /local/php_interface/init.php
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;
use Bitrix\Main\Application;

$eventManager = EventManager::getInstance();

// Отключаем все стандартные фильтры по наличию и ценам
$eventManager->addEventHandler('iblock', 'OnBeforeIBlockElementGetList', function(&$arFilter) {
 // Удаляем все фильтры, связанные с каталогом
 $keysToRemove = [
 'CATALOG_AVAILABLE', 'AVAILABLE', '=CATALOG_AVAILABLE',
 'CATALOG_PRICE_', '>CATALOG_PRICE_', '!=CATALOG_PRICE_'
 ];
 
 foreach ($keysToRemove as $key) {
 foreach (array_keys($arFilter) as $filterKey) {
 if (strpos($filterKey, $key) === 0) {
 unset($arFilter[$filterKey]);
 }
 }
 }
});

// Отключаем проверку доступности через модуль каталога
$eventManager->addEventHandler('catalog', 'OnGetAvailableItems', function() {
 return false;
});

$eventManager->addEventHandler('iblock', 'OnAfterIBlockElementGetList', function(&$arResult) {
 if (empty($arResult) || !Loader::includeModule('catalog') || !Loader::includeModule('sale')) {
 return;
 }
 
 global $USER;
 $userId = $USER->GetID();
 $userGroups = $USER->GetUserGroupArray();
 
 $allowedPriceTypes = [];
 
 $priceTypeRes = \Bitrix\Catalog\GroupTable::getList([
 'select' => ['ID', 'NAME'],
 'filter' => ['=GROUP_ACCESS.GROUP_ID' => $userGroups]
 ]);
 
 while ($priceType = $priceTypeRes->fetch()) {
 $allowedPriceTypes[] = (int)$priceType['ID'];
 }
 
 if (empty($allowedPriceTypes)) {
 $priceTypeRes = \Bitrix\Catalog\GroupTable::getList(['select' => ['ID']]);
 while ($priceType = $priceTypeRes->fetch()) {
 $allowedPriceTypes[] = (int)$priceType['ID'];
 }
 }
 
 if (empty($allowedPriceTypes)) {
 return;
 }
 
 $elementIds = [];
 foreach ($arResult as $item) {
 if (isset($item['ID'])) {
 $elementIds[] = (int)$item['ID'];
 }
 }
 
 if (empty($elementIds)) {
 return;
 }
 
 $connection = Application::getConnection();
 $sql = "
 SELECT DISTINCT cp.PRODUCT_ID 
 FROM b_catalog_price cp
 WHERE cp.PRODUCT_ID IN (" . implode(',', $elementIds) . ")
 AND cp.CATALOG_GROUP_ID IN (" . implode(',', $allowedPriceTypes) . ")
 AND cp.PRICE > 0
 AND (cp.QUANTITY_FROM IS NULL OR cp.QUANTITY_FROM = 0)
 AND (cp.QUANTITY_TO IS NULL OR cp.QUANTITY_TO = 0)
 ";
 
 $result = $connection->query($sql);
 $productsWithPrices = [];
 
 while ($row = $result->fetch()) {
 $productsWithPrices[(int)$row['PRODUCT_ID']] = true;
 }
 
 $filteredResult = [];
 foreach ($arResult as $item) {
 if (isset($item['ID']) && isset($productsWithPrices[(int)$item['ID']])) {
 $filteredResult[] = $item;
 }
 }
 
 $arResult = $filteredResult;
 
 if (isset($GLOBALS['NAV_RESULT']) && is_object($GLOBALS['NAV_RESULT'])) {
 $GLOBALS['NAV_RESULT']->NavRecordCount = count($filteredResult);
 $GLOBALS['NAV_RESULT']->SelectedRows = count($filteredResult);
 }
});

Как правильно реализовать фильтрацию товаров по наличию цены для группы пользователя в Bitrix?

Скрыть товары по отсутствию цены для конкретного типа цены можно серверной фильтрацией: сначала получить для текущего пользователя список типов цен, доступных его группам (bitrix получить группы пользователя), затем встраивать условие вида ‘>CATALOG_PRICE_{ID} > 0’ в фильтр запросов или, если у товара есть офферы (SKU), отфильтровывать результат в OnAfterIBlockElementGetList, проверяя цены у самого товара и у его офферов. Ниже — рабочие варианты (OnBefore — быстрый SQL‑фильтр; OnAfter — точный, с поддержкой офферов), готовый код и рекомендации по кешированию и пагинации.


Содержание


Что происходит и почему стандартные настройки не помогают

В Bitrix стандартная опция “Не показывать отсутствующие товары” и флаг HIDE_NOT_AVAILABLE часто ориентированы на остатки или на конкретный (по умолчанию) тип цены; при нестандартной связке — 30 типов цен, привязанных к 30 группам — компонент может показать товар как «нет в наличии», если для текущего типа цены цена не задана. То есть товар физически есть на складе, но для данного типа цены нет записи в b_catalog_price — и компонент считает его недоступным, не скрывая его полностью. Документация и обсуждения подтверждают, что требуется серверная фильтрация по цене для соответствующего CATALOG_GROUP_ID, либо доработка событий/компонентов ASPRO — скрыть недоступные товары, INTEC — «Товар без цены».


Общий подход: фильтрация по типам цен для группы пользователя (bitrix группы пользователя, bitrix тип цены)

Идея простая:

  1. Получить список групп пользователя ($USER->GetUserGroupArray()) — bitrix получить группы пользователя.
  2. Найти типы цен (CATALOG_GROUP_ID), которые открыты для этих групп (b_catalog_group_access / \Bitrix\Catalog\GroupTable).
  3. На этапе выборки элементов добавить условие: есть ли для элемента цена с CATALOG_GROUP_ID из списка и PRICE > 0. Технически это можно сделать двумя способами:
  • добавить SQL‑фильтр ДО выполнения выборки (OnBeforeIBlockElementGetList) — быстро, правильна навигация;
  • или отфильтровать массив результатов ПОСЛЕ выборки (OnAfterIBlockElementGetList) — нужно при офферах, но придётся корректировать NAV_RESULT и следить за кешем.

Функция для получения типов цен пользователя (пример):

php
use Bitrix\Catalog\GroupTable;

function getUserAllowedPriceTypes(array $userGroups = null): array
{
 global $USER;
 if ($userGroups === null) {
 $userGroups = $USER->GetUserGroupArray(); // bitrix получить группы пользователя
 }
 $allowed = [];
 $res = GroupTable::getList([
 'select' => ['ID'],
 'filter' => ['=GROUP_ACCESS.GROUP_ID' => $userGroups]
 ]);
 while ($row = $res->fetch()) {
 $allowed[] = (int)$row['ID'];
 }
 return array_unique($allowed);
}

Реализация в OnBeforeIBlockElementGetList (bitrix тип цены)

Когда у вас нет офферов или вы уверены, что компонент запрашивает именно те элементы (а не родительские с офферами), самый простой и быстрый способ — добавить в $arFilter условие вида OR(>CATALOG_PRICE_12 > 0, >CATALOG_PRICE_15 > 0, …). Bitrix умеет автоматически джойнить таблицу цен для таких ключей.

Пример обработчика (в /local/php_interface/init.php):

php
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;

EventManager::getInstance()->addEventHandler('iblock', 'OnBeforeIBlockElementGetList',
 function (&$arFilter) {
 if (!Loader::includeModule('catalog')) return;
 global $USER;
 // не трогаем административную часть
 if (defined('ADMIN_SECTION') && ADMIN_SECTION === true) return;

 $allowedPriceTypes = getUserAllowedPriceTypes();
 if (empty($allowedPriceTypes)) return;

 // строим условие OR: >CATALOG_PRICE_{ID} => 0
 $priceCond = ['LOGIC' => 'OR'];
 foreach ($allowedPriceTypes as $pid) {
 $priceCond['>CATALOG_PRICE_'.$pid] = 0;
 }

 // аккуратно объединяем с существующим фильтром
 $current = $arFilter;
 $arFilter = ['LOGIC' => 'AND', $current, $priceCond];
 }
);

Плюсы:

  • фильтрация на уровне SQL — правильная пагинация, меньше PHP‑накладных расходов.
    Минусы:
  • если цена задана только для офферов, а компонент выводит родителя, условие не найдёт цену (см. раздел про офферы).

Подробно о ключах вида >CATALOG_PRICE_N и применении фильтров — в инструкциях и обсуждениях по фильтрации цен redsign и на форумах dev.1c-bitrix.


OnAfterIBlockElementGetList с поддержкой офферов (bitrix группы пользователя)

Если в каталоге используются SKU/офферы, нужно проверить не только цену родительского товара, но и цены его офферов. OnAfterIBlockElementGetList даёт массив результатов, который можно пробежать и удалить элементы без подходящей цены. Важно: при таком подходе нужно обновить навигацию ($GLOBALS[‘NAV_RESULT’]) и учесть кеш.

Упрощённый пример (учитывает офферы через CCatalogSKU и проверяет b_catalog_price через PriceTable):

php
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;
use Bitrix\Catalog\PriceTable;

EventManager::getInstance()->addEventHandler('iblock', 'OnAfterIBlockElementGetList',
 function (&$arResult) {
 if (empty($arResult) || !Loader::includeModule('catalog')) return;
 global $USER;

 $elementIds = [];
 foreach ($arResult as $it) {
 if (!empty($it['ID'])) $elementIds[] = (int)$it['ID'];
 }
 if (empty($elementIds)) return;

 $allowedPriceTypes = getUserAllowedPriceTypes();
 if (empty($allowedPriceTypes)) return;

 // получаем офферы для этих товаров (если есть)
 $offers = [];
 if (class_exists('\CCatalogSKU')) {
 $offers = \CCatalogSKU::getOffersList($elementIds, 0, ['ACTIVE' => 'Y'], ['ID']);
 }
 $offerIds = [];
 foreach ($offers as $parentId => $offArr) {
 foreach ($offArr as $o) { $offerIds[] = (int)$o['ID']; }
 }

 $allIds = array_merge($elementIds, $offerIds);

 // ищем продукты/офферы с ценой в нужных типах
 $priceRes = PriceTable::getList([
 'select' => ['PRODUCT_ID'],
 'filter' => ['=PRODUCT_ID' => $allIds, '=CATALOG_GROUP_ID' => $allowedPriceTypes, '>PRICE' => 0],
 'group' => ['PRODUCT_ID']
 ]);
 $withPrice = [];
 while ($row = $priceRes->fetch()) {
 $withPrice[(int)$row['PRODUCT_ID']] = true;
 }

 // формируем список родительских ID, которые имеют цену (либо сами, либо через оффер)
 $keep = [];
 foreach ($elementIds as $pid) {
 if (!empty($withPrice[$pid])) { $keep[$pid] = true; continue; }
 if (!empty($offers[$pid])) {
 foreach ($offers[$pid] as $o) {
 if (!empty($withPrice[(int)$o['ID']])) { $keep[$pid] = true; break; }
 }
 }
 }

 // фильтруем arResult
 $filtered = [];
 foreach ($arResult as $item) {
 if (!empty($item['ID']) && !empty($keep[(int)$item['ID']])) $filtered[] = $item;
 }
 $arResult = array_values($filtered);

 // корректируем навигацию (важно для пагинации)
 if (isset($GLOBALS['NAV_RESULT']) && is_object($GLOBALS['NAV_RESULT'])) {
 $GLOBALS['NAV_RESULT']->NavRecordCount = count($arResult);
 }
 }
);

Замечание: этот вариант точнее для SKU, но может быть медленнее при больших выборках — тестируйте и оптимизируйте запросы.


Учет офферов (SKU), навигации и кеша

  • Офферы: если цены хранятся только у офферов, OnBefore фильтр по ID родителя не поможет. Нужно либо фильтровать по офферным ID в запросе, либо применять OnAfter с проверкой офферов, как выше.
  • Пагинация: OnBefore корректно считает NavRecordCount на уровне БД; OnAfter требует ручной корректировки $GLOBALS[‘NAV_RESULT’], иначе страницы будут пустовать или дублировать.
  • Кеш: компоненты по умолчанию кешируют результаты; убедитесь, что кеш учитывает группы пользователя (CACHE_GROUPS = ‘Y’) или добавьте в ключ кеша идентификатор типов цен ($allowedPriceTypes) — иначе пользователь увидит закешированный результат другого группы. Если используете компонент с собственным кешем, переделайте старт кеша с дополнительным ключом (или инвалидируйте кеш при смене типов цен).
  • Нагрузка: фильтр вида OR для 30 типов цен генерирует JOIN’ы с таблицей цен; это нормально, но при миллионных выборках стоит продумать индексацию и ограничение поиска по IBLOCK_ID/SECTION_ID.

Альтернативы: менять ACTIVE / свойство «Скрыть в каталоге»

Если нужна простая серверная «постоянная» логика — можно автоматически ставить элементу ACTIVE = ‘N’ или присваивать свойство “HIDE_IN_CATALOG” через события OnBeforeIBlockElementAdd / OnAfterIBlockElementUpdate при отсутствии цены для всех типов цен пользователя. Минусы: вы меняете данные в БД (и скрываете товар глобально), это сложнее откатить и это влияет на админку. Документация и кейсы обсуждают такой путь как рабочий, но аккуратно: INTEC, Helpdesk — скрыть товары.


Пошаговый план внедрения и тестирования

  1. Найдите IBLOCK_ID каталога и определите: используются ли офферы.
  2. Реализуйте и проверьте функцию getUserAllowedPriceTypes(); убедитесь, что она возвращает ожидаемые CATALOG_GROUP_ID.
  3. Для простого каталога (без офферов) добавьте OnBeforeIBlockElementGetList — тестируйте с отключённым кешем.
  4. Если есть офферы — реализуйте OnAfterIBlockElementGetList, проверьте корректность фильтрации и обновление NAV_RESULT.
  5. Настройте кеш: включите CACHE_GROUPS = ‘Y’ или добавьте $allowedPriceTypes в ключ кеширования компонента.
  6. Тесты: переключайтесь между пользователями из разных групп, проверяйте страницы с пагинацией, фильтрацией и сортировкой; замеряйте время запроса в debug mode.
  7. Логи и мониторинг: временно логируйте SQL‑запросы/основные массивы, чтобы убедиться, что фильтр применяется корректно.

Источники

  1. Товар без цены — INTEC (курс по Bitrix)
  2. Как скрыть недоступные товары и торговые предложения — ASPRO
  3. Обсуждение: Деактивировать товары без цен — dev.1c-bitrix.ru
  4. Как скрыть недоступные товары и торговые предложения — RedSign
  5. Как скрыть определенные товары из публичной части — Bitrix24 Helpdesk
  6. Форум: Скрытие товаров без фото/цены — dev.1c-bitrix.ru
  7. Скрыть товары с 0 остатком — 1c.1c-bitrix.ru
  8. Вывод товаров без цены — dev.1c-bitrix.ru
  9. Скрыть отсутствующие товары и торговые предложения — dev.1c-bitrix.ru
  10. Обсуждение на Toster

Заключение

Решение — серверная фильтрация по типам цен, привязанным к битрикс группам пользователя: сначала получите список типов цен (bitrix получить группы пользователя → bitrix тип цены), затем либо добавьте в $arFilter условия ‘>CATALOG_PRICE_{ID} > 0’ на этапе OnBeforeIBlockElementGetList (быстро, корректная пагинация), либо, при наличии офферов, отфильтруйте результат в OnAfterIBlockElementGetList, проверяя цены у товара и его офферов. Не забудьте про кеш (CACHE_GROUPS или ключ по типам цен) и корректировку NAV_RESULT при пост‑фильтрации.

Авторы
Проверено модерацией
Модерация
Bitrix: скрыть товары без цены для группы пользователя