При разработке отчетов на базе Системы компоновки данных (СКД) разработчики часто сталкиваются со стандартным поведением системы: один отчет — один период — один результирующий документ. Однако на практике бизнес-пользователи нередко выдвигают нетривиальные требования. Например, им может потребоваться вывести на экран сразу несколько вариантов одного и того же отчета за разные периоды (скажем, за полный декабрь, за период без последнего дня и отдельно за последний день месяца), чтобы визуально сопоставить данные.
В этой статье мы подробно разберем несколько профессиональных способов реализации такой задачи в 1С. Мы рассмотрим как чисто декларативные методы настройки схемы компоновки, так и гибкие программные алгоритмы вывода данных на управляемую форму или в многолистовой документ Excel.
Самый гибкий и контролируемый метод решения задачи — это перехват стандартного процесса компоновки в модуле отчета. Мы можем программно отключить стандартный вывод, самостоятельно рассчитать нужные параметры в цикле, выполнить компоновку для каждого периода и последовательно вывести результаты в один табличный документ. Для ускорения разработки в качестве основы можно использовать готовый шаблон вывода нескольких СКД в один табличный документ на управляемой форме.
Давайте разберем по шагам, как это реализовать. Откроем модуль отчета и добавим обработчик события ПриКомпоновкеРезультата. Нам потребуется программно инициализировать макет компоновки, процессор компоновки и процессор вывода для каждой итерации. Чтобы значительно упростить это программное манипулирование объектами и сократить объем шаблонного кода, можно использовать специальный модуль-обертку над объектной моделью СКД.
Рассмотрим пример кода:
Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
// Отменяем стандартный вывод платформы
СтандартнаяОбработка = Ложь;
ДокументРезультат.Очистить();
// Подготовим массив структур с описанием периодов для вывода
МассивПериодов = Новый Массив;
// 1. Полный период
Параметры1 = Новый Структура("Начало, Конец, Заголовок", '20231201000000', '20231220235959', "Отчет за период с 1 по 20 декабря");
МассивПериодов.Добавить(Параметры1);
// 2. Период на день меньше
Параметры2 = Новый Структура("Начало, Конец, Заголовок", '20231201000000', '20231219235959', "Отчет за период с 1 по 19 декабря");
МассивПериодов.Добавить(Параметры2);
// 3. Данные только за последний день
Параметры3 = Новый Структура("Начало, Конец, Заголовок", '20231220000000', '20231220235959', "Отчет за последний день (20 декабря)");
МассивПериодов.Добавить(Параметры3);
СхемаКомпоновки = ПолучитьМакет("ОсновнаяСхемаКомпоновкиДанных");
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
Для Каждого ТекущийПериод Из МассивПериодов Цикл
// Получаем текущие настройки компоновщика
Настройки = КомпоновщикНастроек.ПолучитьНастройки();
// Передаем программные параметры дат в настройки СКД
ПараметрНачало = Настройки.ПараметрыДанных.Элементы.Найти("НачалоПериода");
Если ПараметрНачало <> Неопределено Тогда
ПараметрНачало.Значение = ТекущийПериод.Начало;
ПараметрНачало.Использование = Истина;
КонецЕсли;
ПараметрКонец = Настройки.ПараметрыДанных.Элементы.Найти("КонецПериода");
Если ПараметрКонец <> Неопределено Тогда
ПараметрКонец.Значение = ТекущийПериод.Конец;
ПараметрКонец.Использование = Истина;
КонецЕсли;
// Опционально: Выводим текстовый заголовок перед таблицей отчета
ОбластьЗаголовка = ДокументРезультат.ПолучитьОбласть();
// Здесь можно кастомизировать вывод текстового баннера или шапки
// Выполняем компоновку макета для текущей итерации
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновки, Настройки, ДанныеРасшифровки);
// Инициализируем процессор компоновки данных
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, , ДанныеРасшифровки, Истина);
// Инициализируем процессор вывода в табличный документ
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
// Выводим результат итерации в общий документ
ПроцессорВывода.НачатьВывод();
Пока Истина Цикл
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
Если ЭлементРезультата = Неопределено Тогда
Прервать;
КонецЕсли;
ПроцессорВывода.ВывестиЭлемент(ЭлементРезультата);
КонецЦикла;
ПроцессорВывода.ЗакончитьВывод();
// Если это не последний блок, добавим визуальный разделитель страниц
Если МассивПериодов.Найти(ТекущийПериод) < МассивПериодов.Количество() - 1 Тогда
ДокументРезультат.ВывестиГоризонтальныйРазделительСтраниц();
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Этот способ гарантирует, что СУБД выполнит ровно те запросы, которые необходимы для каждого конкретного периода, обеспечивая оптимальную производительность и корректный расчет остатков на каждую границу времени.
При последовательном выводе нескольких разных таблиц в один и тот же объект ТабличныйДокумент разработчики часто сталкиваются с неприятной визуальной проблемой: колонки нижестоящих таблиц "подстраиваются" под ширину колонок первой таблицы. В результате таблицы выглядят неаккуратными и искаженными.
Чтобы решить эту проблему, 1С предоставляет встроенный инструмент фиксации ширины колонок для отдельных областей. Проанализируем, как правильно использовать метод СоздатьФорматСтрок().
При выводе каждой секции отчета вместо стандартного прямого вывода всей таблицы целиком мы можем использовать вывод по областям с принудительным перерасчетом форматов строк:
// Пример фиксации формата строк при выводе секций
ПроцессорВывода.НачатьВывод();
Пока Истина Цикл
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
Если ЭлементРезультата = Неопределено Тогда
Прервать;
КонецЕсли;
// Выводим элемент в табличный документ
ОбластьСтроки = ПроцессорВывода.ВывестиЭлемент(ЭлементРезультата);
// Создаем независимый формат строк для этой области
ДокументРезультат.СоздатьФорматСтрок(ОбластьСтроки);
КонецЦикла;
ПроцессорВывода.ЗакончитьВывод();
Использование метода СоздатьФорматСтрок() позволяет каждой таблице иметь собственную уникальную сетку колонок. Теперь, даже если структура отчетов или состав их полей различаются, они будут выглядеть идеально ровно друг под другом.
Если вывод друг под другом делает отчет слишком длинным и неудобным для чтения, мы можем распределить сформированные таблицы по разным вкладкам (страницам) управляемой формы 1С. Поскольку модуль отчета не имеет прямого доступа к элементам формы, эту логику необходимо перенести в модуль управляемой формы отчета.
Выясним алгоритм реализации этого сценария:
ГруппаВкладкиОтчетов).РезультатПериод1, РезультатПериод2, РезультатПериод3 соответственно).Рассмотрим серверный код процедуры формирования на форме:
&НаСервере
Процедура СформироватьОтчетыНаВкладкахНаСервере()
// Получаем схему компоновки из объекта отчета
ОтчетОбъект = РеквизитФормыВЗначение("Отчет");
СхемаКомпоновки = ОтчетОбъект.ПолучитьМакет("ОсновнаяСхемаКомпоновкиДанных");
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
// Зададим соответствие полей табличных документов на форме и параметров дат
НастройкиВкладок = Новый Массив;
НастройкиВкладок.Добавить(Новый Структура("ТабДок, Начало, Конец", Элементы.РезультатПериод1, '20231201000000', '20231220235959'));
НастройкиВкладок.Добавить(Новый Структура("TabDoc, Начало, Конец", Элементы.РезультатПериод2, '20231201000000', '20231219235959'));
НастройкиВкладок.Добавить(Новый Структура("TabDoc, Начало, Конец", Элементы.РезультатПериод3, '20231220000000', '20231220235959'));
Для Каждого Настройка Из НастройкиВкладок Цикл
ТекущийТабДок = Настройка.ТабДок;
ТекущийТабДок.Очистить();
// Копируем настройки по умолчанию
Настройки = ОтчетОбъект.КомпоновщикНастроек.ПолучитьНастройки();
// Устанавливаем параметры периода для конкретной вкладки
ПараметрНачало = Настройки.ПараметрыДанных.Элементы.Найти("НачалоПериода");
Если ПараметрНачало <> Неопределено Тогда
ПараметрНачало.Значение = Настройка.Начало;
ПараметрНачало.Использование = Истина;
КонецЕсли;
ПараметрКонец = Настройки.ПараметрыДанных.Элементы.Найти("КонецПериода");
Если ПараметрКонец <> Неопределено Тогда
ПараметрКонец.Значение = Настройка.Конец;
ПараметрКонец.Использование = Истина;
КонецЕсли;
// Компонуем результат
ДанныеРасшифровки = Новый ДанныеРасшифровкиКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновки, Настройки, ДанныеРасшифровки);
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, , ДанныеРасшифровки, Истина);
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(ТекущийТабДок);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);
КонецЦикла;
КонецПроцедуры
Благодаря этому подходу пользователь получит интуитивно понятный интерфейс, где данные за разные интервалы времени разнесены по удобным вкладкам внутри одной формы.
Зачастую пользователи требуют вкладки в интерфейсе 1С только ради одной конечной цели: выгрузить отчет в файл Excel так, чтобы каждый период находился на отдельном листе одной книги. Мы можем сделать это программно без использования внешних COM-объектов или сторонних библиотек.
Для объединения нескольких табличных документов в единый файл Excel с разбивкой по листам в платформе 1С предусмотрен специальный объект — ПакетОтображаемыхДокументов. Посмотрим на пример кода, который решает эту задачу:
&НаСервере
Процедура СохранитьПакетВExcel(ТабДок1, ТабДок2, ТабДок3, ПутьКФайлу)
Пакет = Новый ПакетОтображаемыхДокументов;
// Помещаем сформированные табличные документы во временное хранилище
Адрес1 = ПоместитьВоВременноеХранилище(ТабДок1, Новый УникальныйИдентификатор);
Адрес2 = ПоместитьВоВременноеХранилище(ТабДок2, Новый УникальныйИдентификатор);
Адрес3 = ПоместитьВоВременноеХранилище(ТабДок3, Новый УникальныйИдентификатор);
// Добавляем листы в пакет отображаемых документов
Лист1 = Пакет.Состав.Добавить();
Лист1.Данные = Адрес1;
Лист1.Наименование = "Период с 1 по 20 декабря";
Лист2 = Пакет.Состав.Добавить();
Лист2.Данные = Адрес2;
Лист2.Наименование = "Период с 1 по 19 декабря";
Лист3 = Пакет.Состав.Добавить();
Лист3.Данные = Адрес3;
Лист3.Наименование = "Данные за 20 декабря";
// Записываем пакет в один файл XLSX
Пакет.Записать(ПутьКФайлу, ТипФайлаПакетаОтображаемыхДокументов.XLSX);
КонецПроцедуры
Каждое свойство Наименование элемента пакета автоматически преобразуется платформой в название соответствующей вкладки (листа) внутри результирующего файла Microsoft Excel.
Если перед нами стоит задача минимизировать объем программного кода и решить задачу стандартными средствами декларативного описания СКД, мы можем прибегнуть к механизму вложенных отчетов.
Давайте разберем по шагам, как настроить такую схему в конструкторе СКД:
ДобавитьКДате(&КонецПериода, "День", -1).При формировании такого отчета СКД автоматически выполнит все вложенные схемы последовательно с пересчитанными параметрами и аккуратно выведет их в один результирующий табличный документ без написания сложного кода компоновки.
Иногда на форумах предлагают альтернативное "простое" решение: добавить в структуру отчета несколько группировок детальных записей на одном уровне и наложить на каждую группировку индивидуальный отбор по периоду. Однако этот способ имеет серьезные технологические недостатки, о которых необходимо знать.
Почему отборы на уровне группировок могут работать некорректно?
Таким образом, для простых оборотных отчетов или отчетов по документам способ с группировками допустим, но для сложных учетных регистров с расчетом остатков всегда следует отдавать предпочтение программному формированию (Способ 1) или вложенным отчетам (Способ 5).