При разработке торговых и аналитических отчетов в конфигурациях 1С часто возникает задача фильтрации выводимых данных по действующим ассортиментным матрицам. Ассортиментная матрица — это утвержденный перечень товаров, разрешенных для закупки, хранения и продажи на конкретных складах или в розничных магазинах. Если товар не входит в активную матрицу или имеет статус Пассивный, его вывод в отчет (особенно в отчеты по остаткам и продажам) часто требуется заблокировать.
В данной статье мы подробно разберем, как интегрировать механизм проверки принадлежности номенклатуры к ассортиментной матрице в существующий отчет. Мы детально проанализируем ошибки в коде, из-за которых программа аварийно завершает работу или зависает, и разработаем оптимальный алгоритм пакетной проверки с кэшированием данных в оперативной памяти.
Рассмотрим проблемы, с которыми столкнулся автор исходного кода при попытке внедрить проверку в процедуру СформироватьОтчет. В коде отчета были допущены две грубые ошибки, классифицируемые стандартами разработки 1С как критические.
Ошибка первая: Передача менеджера справочника вместо ссылки.
В исходном тексте программы мы видим следующий фрагмент:
Склад = Справочники.Склады;
Отказ = Дополнительно.ПроверитьПринадлежностьАМ(Номенклатура, Склад);
В данном случае переменной Склад присваивается объект СправочникМенеджер.Склады, а не конкретный элемент (ссылка) справочника. Когда этот объект передается в функцию проверки ПроверитьПринадлежностьАМ, условие ТипЗнч(Склад) = Тип("СправочникСсылка.Склады") возвращает значение Ложь. Программа переходит в ветку обработки, где пытается обратиться к реквизиту менеджера справочника:
МатрицаСсылка = Склад.АссортиментнаяМатрица;
Поскольку у менеджера справочника нет и не может быть реквизита АссортиментнаяМатрица, платформа генерирует системную ошибку: «Поле объекта не обнаружено (АссортиментнаяМатрица)», и выполнение отчета полностью прерывается.
Ошибка вторая: Выполнение запросов к базе данных в цикле (Проблема N+1).
В процедуре формирования отчета вызов функции проверки выполняется внутри цикла обхода строк таблицы значений:
Для Каждого Строка Из ТЗ Цикл
...
Отказ = Дополнительно.ПроверитьПринадлежностьАМ(Номенклатура, Склад);
...
КонецЦикла;
Функция ПроверитьПринадлежностьАМ внутри себя создает новый объект Запрос и обращается к регистру сведений АссортиментныеМатрицы. Если таблица продаж содержит несколько тысяч строк, система выполнит аналогичное количество отдельных запросов к СУБД. Это приводит к колоссальным потерям производительности, перегрузке сервера и зависанию отчета.
Ошибка третья: Ограничения соединений при работе с внешними источниками данных.
В процедуре получения данных ПолучитьИсточникДанных выборка производится из таблиц внешнего источника ВнешнийИсточникДанных.SIS.Таблица.sms_ChequeLine. Напрямую соединить таблицы внешнего источника данных с внутренними таблицами информационной базы 1С (такими как регистр сведений матриц) в одном запросе невозможно из-за ограничений СУБД. Нам потребуется реализовать двухэтапный алгоритм.
Чтобы отчет формировался быстро и без ошибок, мы переработаем архитектуру интеграции проверки по шагам:
Соответствие, где ключом будет товар, а значением — признак его активности в матрице.Напишем оптимизированную функцию, которая принимает на вход массив товаров и конкретный склад, а возвращает Соответствие с результатами проверки принадлежности к ассортиментной матрице. Данную функцию целесообразно разместить в общем модуле (например, Дополнительно) с флагом Сервер.
Функция ПолучитьРазрешеннуюНоменклатуруПоМатрице(МассивНоменклатуры, Склад) Экспорт
РезультатПроверки = Новый Соответствие;
// 1. Определение ссылки на ассортиментную матрицу склада
МатрицаСсылка = Неопределено;
Если ТипЗнч(Склад) = Тип("СправочникСсылка.Склады") Тогда
МатрицаСсылка = Склад.Владелец.АссортиментнаяМатрица;
ИначеЕсли ТипЗнч(Склад) = Тип("СправочникСсылка.Магазины") Тогда
МатрицаСсылка = Склад.АссортиментнаяМатрица;
КонецЕсли;
// Если матрица не задана, по умолчанию разрешаем весь товар
Если МатрицаСсылка = Неопределено Или МатрицаСсылка.Пустая() Тогда
Для Каждого ТекНоменклатура Из МассивНоменклатуры Цикл
РезультатПроверки.Вставить(ТекНоменклатура, Истина);
КонецЦикла;
Возврат РезультатПроверки;
КонецЕсли;
// 2. Построение иерархии матриц (родительские связи)
мсвАссортиментныеМатрицы = Новый Массив;
Родитель = МатрицаСсылка;
Пока НЕ Родитель.Пустая() Цикл
мсвАссортиментныеМатрицы.Добавить(Родитель);
Родитель = Родитель.Родитель;
КонецЦикла;
// 3. Пакетный запрос для определения статусов товаров в матрицах
Запрос = Новый Запрос;
// Динамически строим выборку по порядку приоритета матриц
ТекстВыбораПриоритета = "ВЫБОР ";
Для сч = 1 По мсвАссортиментныеМатрицы.Количество() Цикл
Запрос.УстановитьПараметр("Р" + сч, мсвАссортиментныеМатрицы[сч - 1]);
ТекстВыбораПриоритета = ТекстВыбораПриоритета + " КОГДА АссортиментныеМатрицы.АссортиментнаяМатрица = &Р" + сч + " ТОГДА " + сч;
КонецЦикла;
ТекстВыбораПриоритета = ТекстВыбораПриоритета + " КОНЕЦ";
Запрос.Текст =
"ВЫБРАТЬ
| АссортиментныеМатрицы.Объект КАК Номенклатура,
| АссортиментныеМатрицы.Значение КАК Статус,
| " + ТекстВыбораПриоритета + " КАК Порядок
|ПОМЕСТИТЬ ВтВсеСтатусы
|ИЗ
| РегистрСведений.АссортиментныеМатрицы КАК АссортиментныеМатрицы
|ГДЕ
| АссортиментныеМатрицы.Объект В(&МассивНоменклатуры)
| И АссортиментныеМатрицы.АссортиментнаяМатрица В(&мсвАссортиментныеМатрицы)
| И АссортиментныеМатрицы.Значение <> &ПустойСтатус
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВтВсеСтатусы.Номенклатура КАК Номенклатура,
| ВтВсеСтатусы.Статус КАК Статус
|ИЗ
| ВтВсеСтатусы КАК ВтВсеСтатусы
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
| ВтВсеСтатусы.Номенклатура КАК Номенклатура,
| МИНИМУМ(ВтВсеСтатусы.Порядок) КАК МинимальныйПорядок
| ИЗ
| ВтВсеСтатусы КАК ВтВсеСтатусы
|
| СГРУППИРОВАТЬ ПО
| ВтВсеСтатусы.Номенклатура) КАК ВтМаксимальныйПриоритет
| ПО ВтВсеСтатусы.Номенклатура = ВтМаксимальныйПриоритет.Номенклатура
| И ВтВсеСтатусы.Порядок = ВтМаксимальныйПриоритет.МинимальныйПорядок";
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
Запрос.УстановитьПараметр("мсвАссортиментныеМатрицы", мсвАссортиментныеМатрицы);
Запрос.УстановитьПараметр("ПустойСтатус", Справочники.СтатусыНоменклатуры.ПустаяСсылка());
Выборка = Запрос.Выполнить().Выбрать();
// Заполняем кэш результатов
Пока Выборка.Следующий() Цикл
Активен = Истина;
Если Выборка.Статус = Справочники.СтатусыНоменклатуры.Пассивный Тогда
Активен = Ложь; // Товар исключен из ассортимента
КонецЕсли;
РезультатПроверки.Вставить(Выборка.Номенклатура, Активен);
КонецЦикла;
// Для тех товаров, по которым нет записей в матрицах, разрешаем вывод
Для Каждого ТекНоменклатура Из МассивНоменклатуры Цикл
Если РезультатПроверки.Получить(ТекНоменклатура) = Неопределено Тогда
РезультатПроверки.Вставить(ТекНоменклатура, Истина);
КонецЕсли;
КонецЦикла;
Возврат РезультатПроверки;
КонецФункции
Теперь внедрим вызов новой оптимизированной функции в процедуру отчета. Сначала мы получим массив всей номенклатуры, которая фигурирует в продажах за выбранный период времени, определим её статус в матрице за один запрос, а затем отфильтруем вывод на форму.
Приведем переработанный фрагмент кода процедуры СформироватьОтчет:
Процедура СформироватьОтчет()
Если Флажок1 = Ложь И Флажок2 = Ложь И Номенклатура = Справочники.Номенклатура.ПустаяСсылка() Тогда
Если Вопрос("Выполнение обработки может занять продолжительное время. Продолжить?", РежимДиалогаВопрос.ДаНет, 15, КодВозвратаДиалога.Да, "Внимание!!!", КодВозвратаДиалога.Да) = КодВозвратаДиалога.Нет Тогда
Возврат;
КонецЕсли;
КонецЕсли;
ЭлементыФормы.ТабДок.Очистить();
ТЗ = ПолучитьИсточникДанных(ДатаНач, ДатаКон);
ЭлементыФормы.Документ.Очистить();
// --- НАЧАЛО БЛОКА ОПТИМИЗАЦИИ И ПАКЕТНОЙ ПРОВЕРКИ МАТРИЦ ---
// 1. Получаем реальную ссылку на склад из настроек отчета на форме
ТекСклад = ЭлементыФормы.Склад.Значение; // Предполагаем, что Склад выбран на форме
Если НЕ ЗначениеЗаполнено(ТекСклад) Тогда
// Если склад не выбран, возьмем склад по умолчанию или выдадим предупреждение
Предупреждение("Необходимо выбрать склад для проверки ассортиментной матрицы!");
Возврат;
КонецЕсли;
// 2. Собираем все уникальные коды товаров из ТЗ продаж для сопоставления
МассивКодов = ТЗ.СкопироватьКолонку("КодТовара");
ОбщегоНазначения.СвернутьМассив(МассивКодов);
// 3. Находим ссылки на Номенклатуру в нашей базе по кодам из внешней системы
ЗапросПоискаСсылки = Новый Запрос;
ЗапросПоискаСсылки.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Код КАК Код
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Код В(&МассивКодов)";
ЗапросПоискаСсылки.УстановитьПараметр("МассивКодов", МассивКодов);
СоответствиеКодСсылка = Новый Соответствие;
МассивНоменклатурыСлужебный = Новый Массив;
ВыборкаНомен = ЗапросПоискаСсылки.Выполнить().Выбрать();
Пока ВыборкаНомен.Следующий() Цикл
СоответствиеКодСсылка.Вставить(СокрЛП(ВыборкаНомен.Код), ВыборкаНомен.Ссылка);
МассивНоменклатурыСлужебный.Добавить(ВыборкаНомен.Ссылка);
КонецЦикла;
// 4. Получаем кэш статусов ассортиментных матриц для найденных товаров
КэшАссортимента = Дополнительно.ПолучитьРазрешеннуюНоменклатуруПоМатрице(МассивНоменклатурыСлужебный, ТекСклад);
// --- КОНЕЦ БЛОКА ОПТИМИЗАЦИИ ---
ТЗМолочка = Новый ТаблицаЗначений;
ТЗМолочка.Колонки.Добавить("Дата1", , "Дата1");
ТЗМолочка.Колонки.Добавить("Дата", , "Дата");
ТЗМолочка.Колонки.Добавить("Время", , "Время");
ТЗМолочка.Колонки.Добавить("Наименование", , "Наименование");
ТЗМолочка.Колонки.Добавить("КодТовара", , "КодТовара");
ТЗМолочка.Колонки.Добавить("Количество", , "Количество");
ТЗМолочка.Колонки.Добавить("Касса", , "Касса");
ТЗМолочка.Колонки.Добавить("Сумма", , "Сумма");
ТЗМолочка.Колонки.Добавить("НомерЧека", , "НомерЧека");
ТЗМолочка.Колонки.Добавить("НомерСмены", , "НомерСмены");
Для Каждого СтрокаИзТЗ Из ТЗ Цикл
// Получаем ссылку на номенклатуру из нашего соответствия
ТекКодТовара = СокрЛП(СтрокаИзТЗ.КодТовара);
СсылкаНоменклатура = СоответствиеКодСсылка.Получить(ТекКодТовара);
Если СсылкаНоменклатура = Неопределено Тогда
// Товар не найден в нашей информационной базе
Продолжить;
КонецЕсли;
// Проверяем вхождение товара в разрешенную ассортиментную матрицу через оперативный кэш
РазрешенКПродаже = КэшАссортимента.Получить(СсылкаНоменклатура);
Если РазрешенКПродаже = Ложь Тогда
// Товар исключен из ассортимента на данном складе, пропускаем строку
Продолжить;
КонецЕсли;
// Фильтрация по времени
Ч = (СтрокаИзТЗ.ДатаВремяЗакрытияЧека - НачалоДня(СтрокаИзТЗ.ДатаВремяЗакрытияЧека));
Если Час(ВремяНач) < Час(ВремяКон) Тогда
Если Ч < Час(ВремяНач) * 3600 ИЛИ Ч > Час(ВремяКон) * 3900 Тогда
Продолжить;
КонецЕсли;
ИначеЕсли Час(ВремяНач) > Час(ВремяКон) Тогда
Если не Ч > Час(ВремяНач) * 3600 И не Ч < Час(ВремяКон) * 3900 Тогда
Продолжить;
КонецЕсли;
КонецЕсли;
// Заполнение результирующей таблицы
СтрокаТЗМолочка = ТЗМолочка.Добавить();
СтрокаТЗМолочка.Дата1 = Формат(СтрокаИзТЗ.ДатаВремяЗакрытияЧека, "ДЛФ=Д");
СтрокаТЗМолочка.Дата = СтрокаИзТЗ.ДатаВремяЗакрытияЧека;
СтрокаТЗМолочка.Время = ?(ВариантНастройки = 2, ПолучитьИнтервалыЧасов(Час(СтрокаИзТЗ.ДатаВремяЗакрытияЧека)), Час(СтрокаИзТЗ.ДатаВремяЗакрытияЧека));
СтрокаТЗМолочка.Наименование = СсылкаНоменклатура;
СтрокаТЗМолочка.Количество = СтрокаИзТЗ.Количество;
СтрокаТЗМолочка.КодТовара = ТекКодТовара;
СтрокаТЗМолочка.Касса = СтрокаИзТЗ.PosName;
СтрокаТЗМолочка.Сумма = СтрокаИзТЗ.Сумма;
СтрокаТЗМолочка.НомерЧека = СтрокаИзТЗ.НомерЧекаККМ;
СтрокаТЗМолочка.НомерСмены = СтрокаИзТЗ.НомерСменыККМ;
КонецЦикла;
// Дальнейший вывод данных в табличный документ согласно выбранному варианту настройки...
// [Оставшаяся часть алгоритма вывода макета отчета]
КонецПроцедуры
Мы успешно решили задачу оптимизации отчета и интеграции механизма контроля ассортиментных матриц. Давайте проанализируем, какие преимущества дало предложенное архитектурное решение:
Справочники.Склады в функцию проверки теперь передается корректная ссылка на конкретный склад, выбранный на форме отчета.Соответствие и проверяет товары в цикле со скоростью работы оперативной памяти.Данный подход к разработке является стандартным при создании высоконагруженных отчетов в системе 1С:Предприятие 8 и гарантирует стабильную работу базы данных при любых объемах продаж.