Bitrix: скрыть товары без цены для группы пользователя
Как в 1C-Bitrix скрыть товары от пользователей группы, если для их bitrix тип цены не задана цена. Фильтрация по bitrix группы пользователя через события OnBeforeIBlockElementGetList и OnAfter. Примеры кода, учет SKU и кеша.
Как в 1C-Bitrix скрыть товары от пользователей группы, если для их типа цены не задана цена?
Описание проблемы:
- Есть товары на складе.
- 30 типов цен, привязанных к 30 группам пользователей.
- Для конкретного пользователя товары без цены в его типе цен отображаются как «нет в наличии».
- Нужно полностью скрыть такие товары от пользователя.
Стандартные настройки не работают.
Попытка решения через события (не полностью работает):
// /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 группы пользователя, bitrix тип цены)
- Реализация в OnBeforeIBlockElementGetList (bitrix тип цены)
- OnAfterIBlockElementGetList с поддержкой офферов (bitrix группы пользователя)
- Учет офферов (SKU), навигации и кеша
- Альтернативы: менять ACTIVE / свойство «Скрыть в каталоге»
- Пошаговый план внедрения и тестирования
- Источники
- Заключение
Что происходит и почему стандартные настройки не помогают
В Bitrix стандартная опция “Не показывать отсутствующие товары” и флаг HIDE_NOT_AVAILABLE часто ориентированы на остатки или на конкретный (по умолчанию) тип цены; при нестандартной связке — 30 типов цен, привязанных к 30 группам — компонент может показать товар как «нет в наличии», если для текущего типа цены цена не задана. То есть товар физически есть на складе, но для данного типа цены нет записи в b_catalog_price — и компонент считает его недоступным, не скрывая его полностью. Документация и обсуждения подтверждают, что требуется серверная фильтрация по цене для соответствующего CATALOG_GROUP_ID, либо доработка событий/компонентов ASPRO — скрыть недоступные товары, INTEC — «Товар без цены».
Общий подход: фильтрация по типам цен для группы пользователя (bitrix группы пользователя, bitrix тип цены)
Идея простая:
- Получить список групп пользователя ($USER->GetUserGroupArray()) — bitrix получить группы пользователя.
- Найти типы цен (CATALOG_GROUP_ID), которые открыты для этих групп (b_catalog_group_access / \Bitrix\Catalog\GroupTable).
- На этапе выборки элементов добавить условие: есть ли для элемента цена с CATALOG_GROUP_ID из списка и PRICE > 0. Технически это можно сделать двумя способами:
- добавить SQL‑фильтр ДО выполнения выборки (OnBeforeIBlockElementGetList) — быстро, правильна навигация;
- или отфильтровать массив результатов ПОСЛЕ выборки (OnAfterIBlockElementGetList) — нужно при офферах, но придётся корректировать NAV_RESULT и следить за кешем.
Функция для получения типов цен пользователя (пример):
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):
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):
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 — скрыть товары.
Пошаговый план внедрения и тестирования
- Найдите IBLOCK_ID каталога и определите: используются ли офферы.
- Реализуйте и проверьте функцию getUserAllowedPriceTypes(); убедитесь, что она возвращает ожидаемые CATALOG_GROUP_ID.
- Для простого каталога (без офферов) добавьте OnBeforeIBlockElementGetList — тестируйте с отключённым кешем.
- Если есть офферы — реализуйте OnAfterIBlockElementGetList, проверьте корректность фильтрации и обновление NAV_RESULT.
- Настройте кеш: включите CACHE_GROUPS = ‘Y’ или добавьте $allowedPriceTypes в ключ кеширования компонента.
- Тесты: переключайтесь между пользователями из разных групп, проверяйте страницы с пагинацией, фильтрацией и сортировкой; замеряйте время запроса в debug mode.
- Логи и мониторинг: временно логируйте SQL‑запросы/основные массивы, чтобы убедиться, что фильтр применяется корректно.
Источники
- Товар без цены — INTEC (курс по Bitrix)
- Как скрыть недоступные товары и торговые предложения — ASPRO
- Обсуждение: Деактивировать товары без цен — dev.1c-bitrix.ru
- Как скрыть недоступные товары и торговые предложения — RedSign
- Как скрыть определенные товары из публичной части — Bitrix24 Helpdesk
- Форум: Скрытие товаров без фото/цены — dev.1c-bitrix.ru
- Скрыть товары с 0 остатком — 1c.1c-bitrix.ru
- Вывод товаров без цены — dev.1c-bitrix.ru
- Скрыть отсутствующие товары и торговые предложения — dev.1c-bitrix.ru
- Обсуждение на Toster
Заключение
Решение — серверная фильтрация по типам цен, привязанным к битрикс группам пользователя: сначала получите список типов цен (bitrix получить группы пользователя → bitrix тип цены), затем либо добавьте в $arFilter условия ‘>CATALOG_PRICE_{ID} > 0’ на этапе OnBeforeIBlockElementGetList (быстро, корректная пагинация), либо, при наличии офферов, отфильтруйте результат в OnAfterIBlockElementGetList, проверяя цены у товара и его офферов. Не забудьте про кеш (CACHE_GROUPS или ключ по типам цен) и корректировку NAV_RESULT при пост‑фильтрации.