Как проверить принадлежность номенклатуры ассортиментным матрицам в отчетах 1С без потери производительности?

Программист 1С v8.3 (Обычные формы) Управленческий учет Торговля и дистрибуция
← На главную

При разработке торговых и аналитических отчетов в конфигурациях 1С часто возникает задача фильтрации выводимых данных по действующим ассортиментным матрицам. Ассортиментная матрица — это утвержденный перечень товаров, разрешенных для закупки, хранения и продажи на конкретных складах или в розничных магазинах. Если товар не входит в активную матрицу или имеет статус Пассивный, его вывод в отчет (особенно в отчеты по остаткам и продажам) часто требуется заблокировать.

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

Выявление и анализ критических ошибок в исходном коде

Рассмотрим проблемы, с которыми столкнулся автор исходного кода при попытке внедрить проверку в процедуру СформироватьОтчет. В коде отчета были допущены две грубые ошибки, классифицируемые стандартами разработки 1С как критические.

Ошибка первая: Передача менеджера справочника вместо ссылки.

В исходном тексте программы мы видим следующий фрагмент:


Склад = Справочники.Склады;
Отказ = Дополнительно.ПроверитьПринадлежностьАМ(Номенклатура, Склад);

В данном случае переменной Склад присваивается объект СправочникМенеджер.Склады, а не конкретный элемент (ссылка) справочника. Когда этот объект передается в функцию проверки ПроверитьПринадлежностьАМ, условие ТипЗнч(Склад) = Тип("СправочникСсылка.Склады") возвращает значение Ложь. Программа переходит в ветку обработки, где пытается обратиться к реквизиту менеджера справочника:

МатрицаСсылка = Склад.АссортиментнаяМатрица;

Поскольку у менеджера справочника нет и не может быть реквизита АссортиментнаяМатрица, платформа генерирует системную ошибку: «Поле объекта не обнаружено (АссортиментнаяМатрица)», и выполнение отчета полностью прерывается.

Ошибка вторая: Выполнение запросов к базе данных в цикле (Проблема N+1).

В процедуре формирования отчета вызов функции проверки выполняется внутри цикла обхода строк таблицы значений:


Для Каждого Строка Из ТЗ Цикл
    ...
    Отказ = Дополнительно.ПроверитьПринадлежностьАМ(Номенклатура, Склад);
    ...
КонецЦикла;

Функция ПроверитьПринадлежностьАМ внутри себя создает новый объект Запрос и обращается к регистру сведений АссортиментныеМатрицы. Если таблица продаж содержит несколько тысяч строк, система выполнит аналогичное количество отдельных запросов к СУБД. Это приводит к колоссальным потерям производительности, перегрузке сервера и зависанию отчета.

Ошибка третья: Ограничения соединений при работе с внешними источниками данных.

В процедуре получения данных ПолучитьИсточникДанных выборка производится из таблиц внешнего источника ВнешнийИсточникДанных.SIS.Таблица.sms_ChequeLine. Напрямую соединить таблицы внешнего источника данных с внутренними таблицами информационной базы 1С (такими как регистр сведений матриц) в одном запросе невозможно из-за ограничений СУБД. Нам потребуется реализовать двухэтапный алгоритм.

Разработка оптимального алгоритма интеграции проверки

Чтобы отчет формировался быстро и без ошибок, мы переработаем архитектуру интеграции проверки по шагам:

  1. Сбор исходных данных: Получаем таблицу продаж из внешнего источника данных SIS.
  2. Извлечение уникальных идентификаторов номенклатуры: Формируем массив уникальных кодов или ссылок товаров, присутствующих в выборке.
  3. Пакетный запрос к матрицам: Одним запросом выбираем состояние ассортиментных матриц для всего массива товаров.
  4. Создание кэша (Соответствия): Записываем результаты проверки в объект Соответствие, где ключом будет товар, а значением — признак его активности в матрице.
  5. Быстрая фильтрация в цикле: При выводе строк отчета мгновенно проверяем признак из оперативной памяти без повторных обращений к базе данных.

Шаг 1. Пакетный метод проверки ассортиментных матриц

Напишем оптимизированную функцию, которая принимает на вход массив товаров и конкретный склад, а возвращает Соответствие с результатами проверки принадлежности к ассортиментной матрице. Данную функцию целесообразно разместить в общем модуле (например, Дополнительно) с флагом Сервер.


Функция ПолучитьРазрешеннуюНоменклатуруПоМатрице(МассивНоменклатуры, Склад) Экспорт

    РезультатПроверки = Новый Соответствие;
    
    // 1. Определение ссылки на ассортиментную матрицу склада
    МатрицаСсылка = Неопределено;
    Если ТипЗнч(Склад) = Тип("СправочникСсылка.Склады") Тогда
        МатрицаСсылка = Склад.Владелец.АссортиментнаяМатрица;
    ИначеЕсли ТипЗнч(Склад) = Тип("СправочникСсылка.Магазины") Тогда
        МатрицаСсылка = Склад.АссортиментнаяМатрица;
    КонецЕсли;
    
    // Если матрица не задана, по умолчанию разрешаем весь товар
    Если МатрицаСсылка = Неопределено Или МатрицаСсылка.Пустая() Тогда
        Для Каждого ТекНоменклатура Из МассивНоменклатуры Цикл
            РезультатПроверки.Вставить(ТекНоменклатура, Истина);
        КонецЦикла;
        Возврат РезультатПроверки;
    КонецЕсли;
    
    // 2. Построение иерархии матриц (родительские связи)
    мсвАссортиментныеМатрицы = Новый Массив;
    Родитель = МатрицаСсылка;
    Пока НЕ Родитель.Пустая() Цикл
        мсвАссортиментныеМатрицы.Добавить(Родитель);
        Родитель = Родитель.Родитель;
    КонецЦикла;
    
    // 3. Пакетный запрос для определения статусов товаров в матрицах
    Запрос = Новый Запрос;
    
    // Динамически строим выборку по порядку приоритета матриц
    ТекстВыбораПриоритета = "ВЫБОР ";
    Для сч = 1 По мсвАссортиментныеМатрицы.Количество() Цикл
        Запрос.УстановитьПараметр("Р" + сч, мсвАссортиментныеМатрицы[сч - 1]);
        ТекстВыбораПриоритета = ТекстВыбораПриоритета + " КОГДА АссортиментныеМатрицы.АссортиментнаяМатрица = &Р" + сч + " ТОГДА " + сч;
    КонецЦикла;
    ТекстВыбораПриоритета = ТекстВыбораПриоритета + " КОНЕЦ";
    
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    АссортиментныеМатрицы.Объект КАК Номенклатура,
    |    АссортиментныеМатрицы.Значение КАК Статус,
    |    " + ТекстВыбораПриоритета + " КАК Порядок
    |ПОМЕСТИТЬ ВтВсеСтатусы
    |ИЗ
    |    РегистрСведений.АссортиментныеМатрицы КАК АссортиментныеМатрицы
    |ГДЕ
    |    АссортиментныеМатрицы.Объект В(&МассивНоменклатуры)
    |    И АссортиментныеМатрицы.АссортиментнаяМатрица В(&мсвАссортиментныеМатрицы)
    |    И АссортиментныеМатрицы.Значение <> &ПустойСтатус
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    ВтВсеСтатусы.Номенклатура КАК Номенклатура,
    |    ВтВсеСтатусы.Статус КАК Статус
    |ИЗ
    |    ВтВсеСтатусы КАК ВтВсеСтатусы
    |        ВНУТРЕННЕЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
    |            ВтВсеСтатусы.Номенклатура КАК Номенклатура,
    |            МИНИМУМ(ВтВсеСтатусы.Порядок) КАК МинимальныйПорядок
    |        ИЗ
    |            ВтВсеСтатусы КАК ВтВсеСтатусы
    |        
    |        СГРУППИРОВАТЬ ПО
    |            ВтВсеСтатусы.Номенклатура) КАК ВтМаксимальныйПриоритет
    |        ПО ВтВсеСтатусы.Номенклатура = ВтМаксимальныйПриоритет.Номенклатура
    |            И ВтВсеСтатусы.Порядок = ВтМаксимальныйПриоритет.МинимальныйПорядок";
            
    Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
    Запрос.УстановитьПараметр("мсвАссортиментныеМатрицы", мсвАссортиментныеМатрицы);
    Запрос.УстановитьПараметр("ПустойСтатус", Справочники.СтатусыНоменклатуры.ПустаяСсылка());
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    // Заполняем кэш результатов
    Пока Выборка.Следующий() Цикл
        Активен = Истина;
        Если Выборка.Статус = Справочники.СтатусыНоменклатуры.Пассивный Тогда
            Активен = Ложь; // Товар исключен из ассортимента
        КонецЕсли;
        РезультатПроверки.Вставить(Выборка.Номенклатура, Активен);
    КонецЦикла;
    
    // Для тех товаров, по которым нет записей в матрицах, разрешаем вывод
    Для Каждого ТекНоменклатура Из МассивНоменклатуры Цикл
        Если РезультатПроверки.Получить(ТекНоменклатура) = Неопределено Тогда
            РезультатПроверки.Вставить(ТекНоменклатура, Истина);
        КонецЕсли;
    КонецЦикла;
    
    Возврат РезультатПроверки;

КонецФункции

Шаг 2. Оптимизация процедуры СформироватьОтчет

Теперь внедрим вызов новой оптимизированной функции в процедуру отчета. Сначала мы получим массив всей номенклатуры, которая фигурирует в продажах за выбранный период времени, определим её статус в матрице за один запрос, а затем отфильтруем вывод на форму.

Приведем переработанный фрагмент кода процедуры СформироватьОтчет:


Процедура СформироватьОтчет()
    
    Если Флажок1 = Ложь И Флажок2 = Ложь И Номенклатура = Справочники.Номенклатура.ПустаяСсылка() Тогда
        Если Вопрос("Выполнение обработки может занять продолжительное время. Продолжить?", РежимДиалогаВопрос.ДаНет, 15, КодВозвратаДиалога.Да, "Внимание!!!", КодВозвратаДиалога.Да) = КодВозвратаДиалога.Нет Тогда
            Возврат;
        КонецЕсли;
    КонецЕсли;    
    
    ЭлементыФормы.ТабДок.Очистить();    
    ТЗ = ПолучитьИсточникДанных(ДатаНач, ДатаКон);
    ЭлементыФормы.Документ.Очистить();
    
    // --- НАЧАЛО БЛОКА ОПТИМИЗАЦИИ И ПАКЕТНОЙ ПРОВЕРКИ МАТРИЦ ---
    
    // 1. Получаем реальную ссылку на склад из настроек отчета на форме
    ТекСклад = ЭлементыФормы.Склад.Значение; // Предполагаем, что Склад выбран на форме
    Если НЕ ЗначениеЗаполнено(ТекСклад) Тогда
        // Если склад не выбран, возьмем склад по умолчанию или выдадим предупреждение
        Предупреждение("Необходимо выбрать склад для проверки ассортиментной матрицы!");
        Возврат;
    КонецЕсли;

    // 2. Собираем все уникальные коды товаров из ТЗ продаж для сопоставления
    МассивКодов = ТЗ.СкопироватьКолонку("КодТовара");
    ОбщегоНазначения.СвернутьМассив(МассивКодов);
    
    // 3. Находим ссылки на Номенклатуру в нашей базе по кодам из внешней системы
    ЗапросПоискаСсылки = Новый Запрос;
    ЗапросПоискаСсылки.Текст = 
    "ВЫБРАТЬ
    |    Номенклатура.Ссылка КАК Ссылка,
    |    Номенклатура.Код КАК Код
    |ИЗ
    |    Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |    Номенклатура.Код В(&МассивКодов)";
    ЗапросПоискаСсылки.УстановитьПараметр("МассивКодов", МассивКодов);
    
    СоответствиеКодСсылка = Новый Соответствие;
    МассивНоменклатурыСлужебный = Новый Массив;
    
    ВыборкаНомен = ЗапросПоискаСсылки.Выполнить().Выбрать();
    Пока ВыборкаНомен.Следующий() Цикл
        СоответствиеКодСсылка.Вставить(СокрЛП(ВыборкаНомен.Код), ВыборкаНомен.Ссылка);
        МассивНоменклатурыСлужебный.Добавить(ВыборкаНомен.Ссылка);
    КонецЦикла;
    
    // 4. Получаем кэш статусов ассортиментных матриц для найденных товаров
    КэшАссортимента = Дополнительно.ПолучитьРазрешеннуюНоменклатуруПоМатрице(МассивНоменклатурыСлужебный, ТекСклад);
    
    // --- КОНЕЦ БЛОКА ОПТИМИЗАЦИИ ---

    ТЗМолочка = Новый ТаблицаЗначений;
    ТЗМолочка.Колонки.Добавить("Дата1", , "Дата1");
    ТЗМолочка.Колонки.Добавить("Дата", , "Дата");
    ТЗМолочка.Колонки.Добавить("Время", , "Время");
    ТЗМолочка.Колонки.Добавить("Наименование", , "Наименование");
    ТЗМолочка.Колонки.Добавить("КодТовара", , "КодТовара");
    ТЗМолочка.Колонки.Добавить("Количество", , "Количество");
    ТЗМолочка.Колонки.Добавить("Касса", , "Касса");
    ТЗМолочка.Колонки.Добавить("Сумма", , "Сумма");
    ТЗМолочка.Колонки.Добавить("НомерЧека", , "НомерЧека");
    ТЗМолочка.Колонки.Добавить("НомерСмены", , "НомерСмены");
    
    Для Каждого СтрокаИзТЗ Из ТЗ Цикл
        
        // Получаем ссылку на номенклатуру из нашего соответствия
        ТекКодТовара = СокрЛП(СтрокаИзТЗ.КодТовара);
        СсылкаНоменклатура = СоответствиеКодСсылка.Получить(ТекКодТовара);
        
        Если СсылкаНоменклатура = Неопределено Тогда
            // Товар не найден в нашей информационной базе
            Продолжить;
        КонецЕсли;
        
        // Проверяем вхождение товара в разрешенную ассортиментную матрицу через оперативный кэш
        РазрешенКПродаже = КэшАссортимента.Получить(СсылкаНоменклатура);
        Если РазрешенКПродаже = Ложь Тогда
            // Товар исключен из ассортимента на данном складе, пропускаем строку
            Продолжить;
        КонецЕсли;
        
        // Фильтрация по времени
        Ч = (СтрокаИзТЗ.ДатаВремяЗакрытияЧека - НачалоДня(СтрокаИзТЗ.ДатаВремяЗакрытияЧека));
        Если Час(ВремяНач) < Час(ВремяКон) Тогда                                            
            Если Ч < Час(ВремяНач) * 3600 ИЛИ Ч > Час(ВремяКон) * 3900 Тогда                              
                Продолжить;
            КонецЕсли;    
        ИначеЕсли Час(ВремяНач) > Час(ВремяКон) Тогда
            Если не Ч > Час(ВремяНач) * 3600 И не Ч < Час(ВремяКон) * 3900 Тогда
                Продолжить;
            КонецЕсли;    
        КонецЕсли;    
        
        // Заполнение результирующей таблицы
        СтрокаТЗМолочка = ТЗМолочка.Добавить();
        СтрокаТЗМолочка.Дата1 = Формат(СтрокаИзТЗ.ДатаВремяЗакрытияЧека, "ДЛФ=Д");
        СтрокаТЗМолочка.Дата = СтрокаИзТЗ.ДатаВремяЗакрытияЧека;
        СтрокаТЗМолочка.Время = ?(ВариантНастройки = 2, ПолучитьИнтервалыЧасов(Час(СтрокаИзТЗ.ДатаВремяЗакрытияЧека)), Час(СтрокаИзТЗ.ДатаВремяЗакрытияЧека));   
        СтрокаТЗМолочка.Наименование = СсылкаНоменклатура;
        СтрокаТЗМолочка.Количество = СтрокаИзТЗ.Количество;
        СтрокаТЗМолочка.КодТовара = ТекКодТовара;
        СтрокаТЗМолочка.Касса = СтрокаИзТЗ.PosName;
        СтрокаТЗМолочка.Сумма = СтрокаИзТЗ.Сумма;
        СтрокаТЗМолочка.НомерЧека = СтрокаИзТЗ.НомерЧекаККМ;
        СтрокаТЗМолочка.НомерСмены = СтрокаИзТЗ.НомерСменыККМ;
        
    КонецЦикла;
    
    // Дальнейший вывод данных в табличный документ согласно выбранному варианту настройки...
    // [Оставшаяся часть алгоритма вывода макета отчета]
    
КонецПроцедуры

Подведем итоги проделанной работы

Мы успешно решили задачу оптимизации отчета и интеграции механизма контроля ассортиментных матриц. Давайте проанализируем, какие преимущества дало предложенное архитектурное решение:

  1. Устранение падения программы: Вместо передачи менеджера справочника Справочники.Склады в функцию проверки теперь передается корректная ссылка на конкретный склад, выбранный на форме отчета.
  2. Высокое быстродействие отчета: Мы полностью отказались от выполнения запросов в цикле. Вместо этого программа делает один пакетный запрос по всей выборке товаров, кэширует результаты проверки в структуру типа Соответствие и проверяет товары в цикле со скоростью работы оперативной памяти.
  3. Корректная обработка иерархии матриц: Реализованный алгоритм учитывает цепочки наследования родительских ассортиментных матриц, корректно распределяя приоритеты статусов номенклатуры от подчиненных к вышестоящим уровням.

Данный подход к разработке является стандартным при создании высоконагруженных отчетов в системе 1С:Предприятие 8 и гарантирует стабильную работу базы данных при любых объемах продаж.

← На главную