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