Как вывести таблицу значений в макет с группировкой и итогами по подразделениям?

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

При разработке отчетов и обработок в среде 1С часто возникает задача вывода готовой таблицы значений (ТЗ) в табличный документ. Ситуация усложняется, когда данные в таблице хранятся в «плоском» виде (например, строки формата Сотрудник — Подразделение — КоличествоЧасов), а пользователю необходимо представить их в структурированном виде: сгруппированными по подразделениям, с подсчетом промежуточных итогов по каждому отделу и возможностью сворачивания и разворачивания строк.

В этой статье мы подробно разберем, как решить эту задачу. Мы изучим несколько практических способов реализации — от классического запроса к таблице значений до использования Системы компоновки данных (СКД) и ручного алгоритмического обхода. Это позволит вам выбрать наиболее оптимальное решение под архитектуру вашего программного кода.

Подготовительный этап: Проектирование областей макета

Прежде чем приступать к написанию программного кода, нам необходимо правильно настроить структуру макета (который мы будем получать через ПолучитьОбласть()). Для реализации иерархического отчета с группировками нам понадобятся следующие именованные области:

  1. Шапка — область, содержащая заголовок отчета, параметры периода (например, ДатаНачала и ДатаОкончания) и заголовки колонок таблицы.
  2. ГруппировкаПодразделение — строка, которая будет выводить название подразделения и сумму часов по этому подразделению. Обычно ее выделяют жирным шрифтом или фоновым цветом для визуального отличия.
  3. СтрокаТЧ — детальная строка для вывода информации по конкретному сотруднику и его часам.
  4. Итоги — подвал отчета, где выводится общий итог по всем подразделениям.

Теперь рассмотрим программные способы связывания этих областей с данными нашей таблицы значений.

---

Способ №1. Запрос к таблице значений с предложением ИТОГИ ПО (Рекомендуемый способ)

Этот метод является наиболее изящным и производительным в платформе 1С:Предприятие 8. Вместо того чтобы вручную группировать данные в цикле, мы передаем готовую таблицу значений в качестве параметра в новый запрос, задействуем мощный механизм итогов СУБД/платформы и выводим результат.

Важное условие: типизация колонок ТЗ
Для того чтобы использовать таблицу значений в качестве источника данных для запроса, все ее колонки должны быть строго типизированы. Если колонки ТЗ были созданы динамически без явного указания типов (например, через ТЗ.Колонки.Добавить("Часы")), то при попытке передать ее в запрос платформа выдаст ошибку времени выполнения. Выясним, как правильно типизировать ТЗ перед передачей в запрос.

Рассмотрим пошагово пример реализации функции вывода отчета через запрос к ТЗ:


Функция ЗаполнитьТабДокЧерезЗапросКТЗ(ИсходнаяТЗ, НачалоПериода, КонецПериода)

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

КонецФункции

Метод хорош тем, что платформа берет на себя всю математику расчета итогов по группировочным полям, а также автоматически распределяет уровни иерархии в табличном документе благодаря методу Выборка.Уровень().

---

Способ №2. Программное использование Системы компоновки данных (СКД)

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

Проанализируем шаги настройки схемы компоновки данных:

  1. Создайте новый макет с типом Схема компоновки данных.
  2. Добавьте новый Набор данных - объект. Назовите его, например, ВнешнийИсточникДанных.
  3. Добавьте в набор данных поля: Сотрудник (тип: СправочникСсылка.ФизическиеЛица), Подразделение (тип: СправочникСсылка.ПодразделенияОрганизаций), Часы (тип: Число).
  4. Перейдите на вкладку Ресурсы и сделайте поле Часы ресурсом с выражением Сумма(Часы).
  5. На вкладке Настройки добавьте группировку по полю Подразделение, а внутрь нее вложите детальные записи (или группировку по полю Сотрудник).

Теперь напишем программный код, который свяжет нашу ТЗ с этой схемой компоновки данных и выведет готовый отчет:


Функция СформироватьОтчетЧерезСКД(ТЗ)

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

КонецФункции

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

---

Способ №3. Классический ручной обход отсортированной таблицы значений

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

Основное правило этого метода — строгая предварительная сортировка таблицы значений по группировочному полю. Без сортировки этот метод работать не будет.

Давайте разберем по шагам алгоритм реализации этого метода:


Функция ВывестиТЗРучнымОбходом(ТЗ, НачалоПериода, КонецПериода)

    ТабДок = Новый ТабличныйДокумент;            
    Макет = ПолучитьМакет("Макет");
    
    // Шаг 1. Обязательная сортировка по группировочному полю!
    ТЗ.Сортировать("Подразделение");
    
    // Шаг 2. Вывод шапки отчета
    ОбластьШапка = Макет.ПолучитьОбласть("Шапка");       
    ОбластьШапка.Параметры.ДатаНачала = Формат(НачалоПериода, "ДФ=dd.MM.yyyy");
    ОбластьШапка.Параметры.ДатаОкончания = Формат(КонецПериода, "ДФ=dd.MM.yyyy");
    ТабДок.Вывести(ОбластьШапка);
    
    // Инициализация служебных переменных для отслеживания смены группы
    ТекущееПодразделение = Неопределено;
    ИтогПоПодразделению = 0;
    
    // Будем накапливать строки сотрудников для вывода после того, как узнаем общий итог по группе
    МассивСтрокГруппы = Новый Массив;
    
    ТабДок.НачатьАвтогруппировкуСтрок();
    
    // Шаг 3. Цикл обхода всей таблицы значений
    Для Каждого СтрокаТЗ Из ТЗ Цикл
        
        Если СтрокаТЗ.Часы = 0 Тогда
            Продолжить; // Пропускаем строки с нулевыми часами
        КонецЕсли;
        
        // Если это новая группа (сменилось подразделение)
        Если СтрокаТЗ.Подразделение <> ТекущееПодразделение Тогда
            
            // Если это не первая итерация — выводим накопленные данные по предыдущему подразделению
            Если ТекущееПодразделение <> Неопределено Тогда
                ВывестиНакопленныеДанныеГруппы(ТабДок, Макет, ТекущееПодразделение, ИтогПоПодразделению, МассивСтрокГруппы);
            КонецЕсли;
            
            // Сбрасываем накопители для нового подразделения
            ТекущееПодразделение = СтрокаТЗ.Подразделение;
            ИтогПоПодразделению = 0;
            МассивСтрокГруппы.Очистить();
            
        КонецЕсли;
        
        // Накапливаем данные текущей строки
        ИтогПоПодразделению = ИтогПоПодразделению + СтрокаТЗ.Часы;
        МассивСтрокГруппы.Добавить(СтрокаТЗ);
        
    КонецЦикла;
    
    // Шаг 4. Выводим последнее накопленное подразделение после завершения цикла
    Если ТекущееПодразделение <> Неопределено Тогда
        ВывестиНакопленныеДанныеГруппы(ТабДок, Макет, ТекущееПодразделение, ИтогПоПодразделению, МассивСтрокГруппы);
    КонецЕсли;
    
    ТабДок.ЗакончитьАвтогруппировкуСтрок();
    
    // Шаг 5. Вывод общих итогов по документу
    ОбластьИтоги = Макет.ПолучитьОбласть("Итоги");       
    ОбластьИтоги.Параметры.Итоги = ТЗ.Итог("Часы");
    ТабДок.Вывести(ОбластьИтоги);
    
    ТабДок.ФиксацияСверху = 3;
    
    Возврат ТабДок;

КонецФункции

// Вспомогательная процедура вывода одной группы в табличный документ
Процедура ВывестиНакопленныеДанныеГруппы(ТабДок, Макет, Подразделение, ИтогЧасов, МассивСтрок)

    // 1. Выводим группировочную строку (уровень 0)
    ОбластьГруппировка = Макет.ПолучитьОбласть("ГруппировкаПодразделение");
    ОбластьГруппировка.Параметры.Подразделение = Подразделение;
    ОбластьГруппировка.Параметры.Часы = ИтогЧасов;
    ТабДок.Вывести(ОбластьГруппировка, 0);
    
    // 2. Выводим детальные строки (уровень 1)
    Для Каждого СтрокаДанных Из МассивСтрок Цикл
        ОбластьСтрокаТЧ = Макет.ПолучитьОбласть("СтрокаТЧ");
        ЗаполнитьЗначенияСвойств(ОбластьСтрокаТЧ.Параметры, СтрокаДанных);
        ТабДок.Вывести(ОбластьСтрокаТЧ, 1);
    КонецЦикла;

КонецПроцедуры

Этот способ требует аккуратности при переносе строк, поскольку важно не потерять вывод последнего подразделения после выхода из цикла (за это отвечает проверка после окончания цикла Для Каждого).

---

Способ №4. Использование метода Свернуть() для двухфазного вывода

Еще один интересный способ группировки без использования тяжелых запросов заключается во временном разделении таблиц данных на итоговую и детальную. Мы копируем исходную таблицу значений, сворачиваем копию до уникальных подразделений с подсчетом суммы часов, а затем последовательно выводим свернутые данные, подтягивая оригинальные строки с помощью метода НайтиСтроки().

Рассмотрим, как это выглядит на практике:


Функция ВывестиЧерезСверткуТЗ(ИсходнаяТЗ, НачалоПериода, КонецПериода)

    ТабДок = Новый ТабличныйДокумент;            
    Макет = ПолучитьМакет("Макет");
    
    // Шаг 1. Вывод шапки отчета
    ОбластьШапка = Макет.ПолучитьОбласть("Шапка");       
    ОбластьШапка.Параметры.ДатаНачала = Формат(НачалоПериода, "ДФ=dd.MM.yyyy");
    ОбластьШапка.Параметры.ДатаОкончания = Формат(КонецПериода, "ДФ=dd.MM.yyyy");
    ТабДок.Вывести(ОбластьШапка);
    
    // Шаг 2. Создаем свернутую копию таблицы значений с уникальными подразделениями и суммами
    ТЗ_Группировки = ИсходнаяТЗ.Скопировать();
    ТЗ_Группировки.Свернуть("Подразделение", "Часы");
    
    ТабДок.НачатьАвтогруппировкуСтрок();
    
    // Шаг 3. Обходим свернутую таблицу и выводим итоги по каждой группе
    Для Каждого СтрокаГруппы Из ТЗ_Группировки Цикл
        
        // Выводим шапку группировки подразделения
        ОбластьГруппировка = Макет.ПолучитьОбласть("ГруппировкаПодразделение");
        ОбластьГруппировка.Параметры.Подразделение = СтрокаГруппы.Подразделение;
        ОбластьГруппировка.Параметры.Часы = СтрокаГруппы.Часы;
        ТабДок.Вывести(ОбластьГруппировка, 0);
        
        // Шаг 4. Находим детальные строки сотрудников по текущему подразделению в исходной таблице
        ПараметрыОтбора = Новый Структура("Подразделение", СтрокаГруппы.Подразделение);
        НайденныеДетальныеСтроки = ИсходнаяТЗ.НайтиСтроки(ПараметрыОтбора);
        
        Для Каждого СтрокаДетали Из НайденныеДетальныеСтроки Цикл
            Если СтрокаДетали.Часы <> 0 Тогда
                ОбластьСтрокаТЧ = Макет.ПолучитьОбласть("СтрокаТЧ");
                ЗаполнитьЗначенияСвойств(ОбластьСтрокаТЧ.Параметры, СтрокаДетали);
                ТабДок.Вывести(ОбластьСтрокаТЧ, 1);
            КонецЕсли;
        КонецЦикла;
        
    КонецЦикла;
    
    ТабДок.ЗакончитьАвтогруппировкуСтрок();
    
    // Шаг 5. Выводим итоговую область
    ОбластьИтоги = Макет.ПолучитьОбласть("Итоги");       
    ОбластьИтоги.Параметры.Итоги = ИсходнаяТЗ.Итог("Часы");
    ТабДок.Вывести(ОбластьИтоги);
    
    ТабДок.ФиксацияСверху = 3;
    
    Возврат ТабДок;

КонецФункции

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

---

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

Мы проанализировали четыре различных способа вывода плоской таблицы значений в древовидный иерархический отчет. Какой способ выбрать в конкретной ситуации?

← На главную