При разработке печатных форм и отчетов в среде 1С программисты часто сталкиваются с задачей группировки выводимых данных. Типичный сценарий: у нас есть набор записей (например, из регистра или таблицы значений), в котором присутствует ключевое поле (например, Дата или Период), и нам необходимо вывести информацию в табличный документ так, чтобы строки были аккуратно сгруппированы по этому полю.
В этой статье мы подробно разберем три основных подхода к решению этой задачи, начиная от самого производительного и заканчивая методами оптимизации ручной обработки таблиц значений, а также научимся делать группы строк интерактивными (сворачиваемыми).
Если печатная форма формируется программно с помощью кода, самым быстрым и правильным с точки зрения производительности решением будет перенос логики группировки на уровень СУБД. Для этого в языке запросов 1С предусмотрена конструкция ИТОГИ ПО.
Использование итогов позволяет платформе самостоятельно построить древовидную (иерархическую) структуру данных. Нам не придется вручную отбирать строки или удалять дубликаты дат — СУБД вернет уже готовую для обхода структуру.
Рассмотрим по шагам, как реализовать этот подход:
ИТОГИ ПО с перечислением полей группировки.ОбходРезультатаЗапроса.ПоГруппировкам.Посмотрим на практический пример реализации этого алгоритма:
// 1. Формируем запрос с итогами
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Период КАК Период,
| Продажи.Номенклатура КАК Номенклатура,
| Продажи.Количество КАК Количество,
| Продажи.Сумма КАК Сумма
|ИЗ
| РегистрНакопления.Продажи КАК Продажи
|УПОРЯДОЧИТЬ ПО
| Период
|ИТОГИ ПО
| Период";
РезультатЗапроса = Запрос.Выполнить();
// 2. Инициализируем табличный документ и макет
ТабДок = Новый ТабличныйДокумент;
Макет = ПолучитьМакет("МакетПечати");
ОбластьШапка = Макет.ПолучитьОбласть("Шапка");
ТабДок.Вывести(ОбластьШапка);
// Получаем выборку верхнего уровня (только уникальные Периоды)
ВыборкаПериод = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Период");
Пока ВыборкаПериод.Следующий() Цикл
// Выводим область заголовка группы (Период/Дата)
ОбластьГруппа = Макет.ПолучитьОбласть("ШапкаГруппы");
ОбластьГруппа.Параметры.Заполнить(ВыборкаПериод);
ТабДок.Вывести(ОбластьГруппа);
// Получаем вложенную выборку детальных записей для текущего Периода
ВыборкаДетали = ВыборкаПериод.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
// Выводим строки с номенклатурой и количеством
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
ОбластьСтрока.Параметры.Заполнить(ВыборкаДетали);
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
КонецЦикла;
Преимущества метода: Максимальная скорость работы. База данных оптимизирует выполнение запроса, а оперативная память сервера не перегружается лишними итерациями циклов.
Использование СКД — это наиболее современный стандарт разработки в экосистеме 1С. Система компоновки данных полностью берет на себя рутинную работу по выводу областей макета, расчету итогов, оформлению и созданию иерархических группировок.
Если ваши данные собираются простым или средней сложности запросом, достаточно создать схему компоновки данных, добавить в настройки группировку по полю Период, а внутрь нее подчинить структуру "Детальные записи". Платформа выведет все автоматически.
Однако бывают ситуации, когда данные невозможно получить одним запросом. Например, если в коде выполняются сложные расчеты, распределение или обращение к внешним API, в результате чего формируется ТаблицаЗначений (ТЗ). Даже в этом случае мы можем применить мощь СКД, передав ТЗ как внешний набор данных.
Выясним, как программно вывести ТаблицуЗначений через СКД с группировкой по шагам:
Дата, Номенклатура, Сумма.Дата, а внутрь нее вложите детальные записи.
// Предположим, что ТаблицаДанных — это наша подготовленная ТЗ с колонками Дата, Номенклатура, Сумма
СхемаКомпоновкиДанных = ПолучитьМакет("СхемаКомпоновкиДанных");
Настройки = СхемаКомпоновкиДанных.НастройкиПоУмолчанию;
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки);
// Передаем нашу таблицу значений во внешний набор данных
ВнешниеНаборыДанных = Новый Структура;
ВнешниеНаборыДанных.Вставить("МояТаблицаДанных", ТаблицаДанных);
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, ВнешниеНаборыДанных);
ТабДок = Новый ТабличныйДокумент;
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(ТабДок);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);
ТабДок.Показать();
Преимущества метода: Полное отсутствие ручной работы с ячейками макета. Легко добавлять новые уровни группировок, условное оформление и пользовательские настройки без изменения программного кода.
Иногда разработчики идут по пути наименьшего сопротивления и реализуют группировку "в лоб": выгружают результат в ТЗ, получают массив уникальных дат, а затем в двойном цикле перебирают всю таблицу для каждой даты. Рассмотрим, почему это плохо.
При объеме таблицы всего в 10 000 строк и количестве уникальных дат равным 100, вложенный цикл совершит 1 000 000 итераций (сложность алгоритма составляет $O(N \times M)$). Это приведет к сильному зависанию системы.
Проанализируем ситуацию и оптимизируем этот ручной обход. Для ускорения нам понадобятся три инструмента платформы: индексация, метод Свернуть() и быстрый поиск через метод НайтиСтроки().
Разберем оптимизированный алгоритм по шагам:
// Исходная ТаблицаДанных содержит колонки: Дата, Номенклатура, Количество
// Шаг 1. Индексируем таблицу по группировочному полю для мгновенного поиска строк
ТаблицаДанных.Индексы.Добавить("Дата");
// Шаг 2. Получаем список уникальных дат без дублей методом Свернуть()
ТаблицаДат = ТаблицаДанных.Скопировать(, "Дата");
ТаблицаДат.Свернуть("Дата");
ТаблицаДат.Сортировать("Дата Возр"); // Упорядочиваем по возрастанию
// Шаг 3. Быстрый обход без полного перебора во внутреннем цикле
ТабДок = Новый ТабличныйДокумент;
Макет = ПолучитьМакет("МакетПечати");
Для Каждого СтрокаГруппы Из ТаблицаДат Цикл
// Выводим шапку группировки
ОбластьГруппа = Макет.ПолучитьОбласть("ШапкаГруппы");
ОбластьГруппа.Параметры.Дата = СтрокаГруппы.Дата;
ТабДок.Вывести(ОбластьГруппа);
// Шаг 4. Находим строки по индексу (работает практически мгновенно)
Отбор = Новый Структура("Дата", СтрокаГруппы.Дата);
НайденныеСтроки = ТаблицаДанных.НайтиСтроки(Отбор);
Для Каждого ТекущаяСтрока Из НайденныеСтроки Цикл
// Выводим детальные данные в Табличный Документ
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
ОбластьСтрока.Параметры.Заполнить(ТекущаяСтрока);
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
КонецЦикла;
Преимущества метода: Скорость работы возрастает в десятки и сотни раз на больших объемах данных благодаря индексам СУБД-подобного поиска внутри коллекции ТаблицаЗначений в оперативной памяти.
Для придания печатной форме профессионального вида очень важно дать пользователю возможность скрывать и раскрывать детальные строки под заголовками групп (использовать стандартные значки "плюс/минус" слева от таблицы).
Для управления структурой документа в 1С используются методы автогруппировки. Основное правило: перед выводом строк необходимо вызвать НачатьАвтогруппировкуСтрок(), при выводе указывать уровень вложенности, а в конце вызвать ЗакончитьАвтогруппировкуСтрок().
Посмотрим на практический пример интеграции автогруппировки в код вывода:
ТабДок = Новый ТабличныйДокумент;
ТабДок.НачатьАвтогруппировкуСтрок();
// Уровень 1 — это родительская группировка (наша дата)
// Уровень 2 — детальные строки (номенклатура)
Для Каждого СтрокаГруппы Из ТаблицаДат Цикл
// Выводим шапку группы на Уровне 1
// Третий параметр "Группа_" + СтрокаГруппы.Дата определяет уникальное имя группы
// Четвертый параметр "Истина" указывает, что группа по умолчанию открыта
ОбластьГруппа = Макет.ПолучитьОбласть("ШапкаГруппы");
ОбластьГруппа.Параметры.Дата = СтрокаГруппы.Дата;
ТабДок.Вывести(ОбластьГруппа, 1, "Группа_" + Формат(СтрокаГруппы.Дата, "ДФ=yyyyMMdd"), Истина);
Отбор = Новый Структура("Дата", СтрокаГруппы.Дата);
НайденныеСтроки = ТаблицаДанных.НайтиСтроки(Отбор);
Для Каждого ТекущаяСтрока Из НайденныеСтроки Цикл
// Выводим подчиненную детальную строку на Уровне 2
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
ОбластьСтрока.Параметры.Заполнить(ТекущаяСтрока);
ТабДок.Вывести(ОбластьСтрока, 2);
КонецЦикла;
КонецЦикла;
ТабДок.ЗакончитьАвтогруппировкуСтрок();
// Указываем отображать кнопки сворачивания групп ("плюсы/минусы")
ТабДок.ПоказатьСетку = Ложь;
ТабДок.ПараметрыПечати.ОриентацияСтраницы = ОриентацияСтраницы.Портрет;
// Опционально: сворачиваем отчет до первого уровня при открытии
ТабДок.ПоказатьУровеньГруппировокСтрок(1);
Для того чтобы всегда писать производительный и чистый код, руководствуйтесь следующими правилами при выборе архитектуры отчета: