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