Как программно обработать результат запроса СКД в процедуре ПриКомпоновкеРезультата?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Разработка отчетов на системе компоновки данных (СКД) в 1С — задача стандартная, но часто программисты сталкиваются с ситуациями, когда стандартных отборов и настроек СКД не хватает для реализации сложной бизнес-логики. Например, когда требуется исключить определенные строки на основе сложных внешних алгоритмов, проверить права доступа "на лету" (поможет анализ прав доступа к объектам метаданных) или отфильтровать пустые строки в иерархических группировках так, чтобы не нарушить структуру отчета.

Попытки решить эту проблему путем прямого удаления строк из готового объекта ТабличныйДокумент обычно приводят к плачевным результатам: ломается структура группировок, "съезжают" итоги, а детальные записи начинают хаотично перекрывать друг друга. Выясним причину этой проблемы и рассмотрим по шагам несколько профессиональных способов программной обработки результатов запроса СКД непосредственно перед выводом пользователю — для этого подойдёт инструментарий разработчика с консолью запросов и СКД.

Почему прямая обработка Табличного Документа — это тупиковый путь?

Когда мы пытаемся обойти уже сформированный ТабличныйДокумент в цикле и удалить области с помощью метода УдалитьОбласть(), платформа выполняет физическое смещение ячеек по вертикали или горизонтали. Поскольку СКД при выводе группировок строит сложные иерархические структуры с объединением ячеек, удаление даже одной строки приводит к деформации соседних группировок. В результате отчет визуально "ломается". Чтобы этого избежать, нам необходимо обрабатывать данные до того, как они будут отрисованы в табличном документе. Разберем наиболее эффективные архитектурные решения.

Способ 1. Динамическая подмена Набора Данных Запроса на Набор Данных Объект (Таблицу Значений)

Этот способ идеально подходит, если ваш отчет построен на основе одного простого набора данных (запроса). Идея заключается в том, чтобы перехватить запрос СКД, выполнить его программно, выгрузить результат в коллекцию ТаблицаЗначений, обработать её на встроенном языке 1С, а затем подставить обратно в макет компоновки как внешний набор данных.

Рассмотрим подробнее программную реализацию этого алгоритма в модуле объекта отчета внутри предопределенной процедуры ПриКомпоновкеРезультата (поможет инструмент для пошаговой отладки кода 1С):


Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
    
    // Отменяем стандартный вывод отчета системой
    СтандартнаяОбработка = Ложь;
    
    // Получаем текущие настройки компоновщика (с учетом пользовательских настроек)
    Настройки = КомпоновщикНастроек.ПолучитьНастройки();
    ДанныеРасшифровки = Новый ДанныеРасшифровкиКомпоновкиДанных;
    
    // Создаем макет компоновки
    КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
    МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки, ДанныеРасшифровки);
    
    // Предполагаем, что у нас один набор данных с именем "НаборДанных1"
    НаборДанных = МакетКомпоновки.НаборыДанных.НаборДанных1;
    
    // Создаем запрос на основе текста запроса из макета СКД
    Запрос = Новый Запрос(НаборДанных.Запрос);
    
    // Передаем параметры из компоновщика в наш запрос
    Для Каждого ПараметрЗапроса Из МакетКомпоновки.ЗначенияПараметров Цикл
        Запрос.УстановитьПараметр(ПараметрЗапроса.Имя, ПараметрЗапроса.Значение);
    КонецЦикла;
    
    // Выполняем запрос и получаем плоскую Таблицу Значений
    ТЗ = Запрос.Выполнить().Выгрузить();
    
    // Обрабатываем полученную таблицу на встроенном языке
    ОбработкаРезультатаСКД(ТЗ);
    
    // Сохраняем свойства старого набора данных для воссоздания структуры
    СвойстваНабора = Новый Структура("Имя,ИсточникДанных,Поля,Отбор");
    ЗаполнитьЗначенияСвойств(СвойстваНабора, НаборДанных);
    
    // Удаляем старый набор данных типа "Запрос" из макета
    МакетКомпоновки.НаборыДанных.Удалить(НаборДанных);
    
    // Добавляем новый набор данных типа "Объект" взамен удаленного
    НаборДанныхОбъект = МакетКомпоновки.НаборыДанных.Добавить(Тип("НаборДанныхОбъектМакетаКомпоновкиДанных"));
    ЗаполнитьЗначенияСвойств(НаборДанныхОбъект, СвойстваНабора);
    НаборДанныхОбъект.ИмяОбъекта = "ВнешнийИсточникТЗ";
    
    // Заполняем поля нового набора данных
    Для Каждого Поле Из СвойстваНабора.Поля Цикл
        НовоеПоле = НаборДанныхОбъект.Поля.Добавить(Тип("ПолеНабораДанныхСхемыКомпоновкиДанных"));
        ЗаполнитьЗначенияСвойств(НовоеПоле, Поле);
    КонецЦикла;
    
    // Инициализируем процессор компоновки данных, передав ТЗ во внешние наборы
    ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
    ВнешниеНаборыДанных = Новый Структура("ВнешнийИсточникТЗ", ТЗ);
    ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, ВнешниеНаборыДанных, ДанныеРасшифровки, Ложь);
    
    // Осуществляем вывод обработанного результата в итоговый табличный документ
    ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
    ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
    ПроцессорВывода.Вывести(ПроцессорКомпоновки);
    
КонецПроцедуры

Процедура ОбработкаРезультатаСКД(ТЗ)
    // Разберем на примере удаление пустых или некорректных строк
    Индекс = ТЗ.Количество() - 1;
    Пока Индекс >= 0 Цикл
        СтрокаТЗ = ТЗ[Индекс];
        // Пример условия фильтрации: удаляем строки без номенклатуры или с нулевой суммой
        Если Не ЗначениеЗаполнено(СтрокаТЗ.Номенклатура) Или СтрокаТЗ.Сумма = 0 Тогда
            ТЗ.Удалить(СтрокаТЗ);
        КонецЕсли;
        Индекс = Индекс - 1;
    КонецЦикла;
КонецПроцедуры

Данный подход гарантирует, что СКД сгруппирует, рассчитает итоги и оформит уже предварительно очищенные данные. Никаких пустых строк или сломанных иерархий в итоговом отчете не будет.

Способ 2. Решение для сложных отчетов: Паттерн «Две СКД»

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

Суть паттерна: Мы используем две схемы компоновки данных. Первая (исходная) схема собирает данные из всех 4-5 сложных наборов данных и выгружает итоговый результат в коллекцию значений с помощью объекта ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений — есть автоматическая выгрузка данных из запросов СКД. Затем мы обрабатываем эту коллекцию и передаем ее во вторую (выводную) схему, которая отвечает только за красивое представление и группировку данных.

Проанализируем пример кода для реализации этого паттерна:


Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
    
    СтандартнаяОбработка = Ложь;
    
    // ЭТАП 1: Вывод исходной СКД в Таблицу Значений
    Настройки = КомпоновщикНастроек.ПолучитьНастройки();
    ДанныеРасшифровки = Новый ДанныеРасшифровкиКомпоновкиДанных;
    
    КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
    МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки, ДанныеРасшифровки);
    
    ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
    ПроцессорКомпоновки.Инициализировать(МакетКомпоновки,,, Ложь);
    
    // Используем специальный процессор вывода в коллекцию
    ПроцессорВыводаКоллекции = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
    ТаблицаРезультата = Новый ТаблицаЗначений;
    ПроцессорВыводаКоллекции.УстановитьОбъект(ТаблицаРезультата);
    ПроцессорВыводаКоллекции.Вывести(ПроцессорКомпоновки);
    
    // ЭТАП 2: Произвольная очистка и модификация таблицы на встроенном языке
    ФильтрацияТаблицыЗначений(ТаблицаРезультата);
    
    // ЭТАП 3: Вывод обработанной таблицы через вторую (простую) схему вывода
    // Вторая схема содержит один НаборДанных-Объект и настроенные группировки/оформление
    ВтораяСхема = ПолучитьМакет("ВтораяСхемаВывода");
    
    КомпоновщикМакетаВторой = Новый КомпоновщикМакетаКомпоновкиДанных;
    МакетКомпоновкиВторой = КомпоновщикМакетаВторой.Выполнить(ВтораяСхема, ВтораяСхема.НастройкиПоУмолчанию, ДанныеРасшифровки);
    
    ПроцессорКомпоновкиВторой = Новый ПроцессорКомпоновкиДанных;
    ВнешниеДанные = Новый Структура("ВнешняяТЗ", ТаблицаРезультата);
    ПроцессорКомпоновкиВторой.Инициализировать(МакетКомпоновкиВторой, ВнешниеДанные, ДанныеРасшифровки, Ложь);
    
    ПроцессорВыводаВТабДок = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
    ПроцессорВыводаВТабДок.УстановитьДокумент(ДокументРезультат);
    ПроцессорВыводаВТабДок.Вывести(ПроцессорКомпоновкиВторой);
    
КонецПроцедуры

Этот способ позволяет безболезненно работать с любым количеством наборов данных любой сложности, сохраняя производительность и читаемость кода.

Способ 3. Поэлементный вывод результата компоновки данных

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

Посмотрим на пример реализации поэлементного обхода:


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

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

Способ 4. Декларативный обход через функции Общих Модулей в СКД

Прежде чем писать объемный программный код в ПриКомпоновкеРезультата, всегда стоит подумать: нельзя ли решить задачу декларативно? Если вам нужно отфильтровать строки по сложному условию (например, проверка прав (см. анализ прав доступа и ограничений RLS) или вычисление сложного внешнего тарифа), вы можете использовать функции общих модулей прямо в схеме СКД.

Выполним следующие шаги:

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

При таком подходе СКД сама выполнит фильтрацию данных на этапе компоновки результата, избавив вас от необходимости переопределять процедуру вывода и писать сложный программный код поддержки макетов.

Резюме и рекомендации

Мы проанализировали четыре принципиально разных подхода к программной обработке результатов СКД. Подведем итог, какой метод выбрать:

← На главную