При формировании сводных печатных форм и отчетов нередко возникает задача объединить одинаковые позиции из разных документов в единые группы. Например, когда в нескольких документах плана закупок или заявок указаны одинаковые коды классификатора верхнего уровня (в шапке) и подчиненные подкоды (в табличной части), требуется вывести компактную печатную форму: сгруппировать строки по коду, затем по подкоду и рассчитать корректные суммы.
Рассмотрим, почему стандартный запрос к документу с вложенной табличной частью не позволяет выполнить сквозную группировку, и подробно разберем правильные способы решения этой задачи.
Выясним причину, по которой первоначальный вариант запроса с синтаксисом вложенных таблиц Документ.Состав.(...) не работает для сквозного агрегирования:
СГРУППИРОВАТЬ ПО) и расчет общих итогов по полям внешнего документа. Вложенная таблица жестко привязана к конкретному экземпляру документа в базе и не может объединять строки из разных документов между собой. При попытке группировки платформа выдает ошибку: «Нельзя группировать по полям внешних таблиц при наличии вложенных таблиц».СУММА(Сумма) от строк табличной части.Выбрать() без явного указания типа обхода выполняет прямой обход (ОбходРезультатаЗапроса.Прямой). При таком обходе в один цикл подряд попадают как итоговые записи групп, так и детальные строки, что приводит к некорректному выводу данных в макет.Разберем по шагам наиболее эффективный и классический подход для формирования печатных форм. Обратимся напрямую к табличной части документа Документ.э_ПланЗакупок.СоставПланаЗакупок, настроим расчет итогов по двум уровням (код и подкод) и выполним иерархический обход выборки.
Посмотрим на пример правильного текста запроса и процедуру вывода макета:
Процедура ПечатьПланаЗакупок() Экспорт
ТабДок = Новый ТабличныйДокумент;
Макет = Документы.э_ПланЗакупок.ПолучитьМакет("МакетПечатьПланаЗакупок");
// Вывод шапки отчета
ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
ТабДок.Вывести(ОбластьЗаголовок);
ОбластьШапка = Макет.ПолучитьОбласть("ШапкаТабличнойЧасти");
ТабДок.Вывести(ОбластьШапка);
// Формирование запроса к табличной части
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Состав.Ссылка.КодCPV КАК КодCPV,
| Состав.Ссылка.Описание КАК Описание,
| Состав.ПодкодCPV КАК ПодкодCPV,
| Состав.ОписаниеЗакупки КАК ОписаниеЗакупки,
| Состав.Сумма КАК СуммаЭлемента
|ИЗ
| Документ.э_ПланЗакупок.СоставПланаЗакупок КАК Состав
|ИТОГИ
| СУММА(СуммаЭлемента)
|ПО
| КодCPV,
| ПодкодCPV";
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Возврат;
КонецЕсли;
// Получение областей для строк
ОбластьГруппа = Макет.ПолучитьОбласть("СтрокаТабличнойЧасти");
ОбластьПодстрока = Макет.ПолучитьОбласть("ПодстрокаТабличнойЧасти");
// Обход группировки верхнего уровня (КодCPV)
ВыборкаКод = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "КодCPV");
НомСтроки = 0;
Пока ВыборкаКод.Следующий() Цикл
НомСтроки = НомСтроки + 1;
ОбластьГруппа.Параметры.НомерСтроки = НомСтроки;
ОбластьГруппа.Параметры.КодПоКлассификатору = ВыборкаКод.КодCPV;
ОбластьГруппа.Параметры.НаименованиеЗакупки = ВыборкаКод.Описание;
ОбластьГруппа.Параметры.СуммаЗакупки = ВыборкаКод.СуммаЭлемента; // Рассчитанный итог по группе
ТабДок.Вывести(ОбластьГруппа);
// Подчиненная выборка второго уровня (ПодкодCPV)
ВыборкаПодкод = ВыборкаКод.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "ПодкодCPV");
НомПодстроки = 0;
Пока ВыборкаПодкод.Следующий() Цикл
НомПодстроки = НомПодстроки + 1;
// Формируем строковый номер вида "1.1", "1.2"
ИерархическийНомер = Строка(НомСтроки) + "." + Строка(НомПодстроки);
ОбластьПодстрока.Параметры.НомерПодстроки = ИерархическийНомер;
ОбластьПодстрока.Параметры.ПодКодПоКлассификатору = ВыборкаПодкод.ПодкодCPV;
ОбластьПодстрока.Параметры.ПодНаименованиеЗакупки = ВыборкаПодкод.ОписаниеЗакупки;
ОбластьПодстрока.Параметры.СуммаЭлемента = ВыборкаПодкод.СуммаЭлемента;
ТабДок.Вывести(ОбластьПодстрока);
КонецЦикла;
КонецЦикла;
// Настройки отображения
ТабДок.ОтображатьСетку = Ложь;
ТабДок.Защита = Ложь;
ТабДок.ТолькоПросмотр = Ложь;
ТабДок.ОтображатьЗаголовки = Ложь;
ТабДок.Показать();
КонецПроцедуры
Проанализируем ключевые моменты в данном коде:
ИТОГИ указана агрегатная функция СУММА(СуммаЭлемента) и поля иерархии КодCPV, ПодкодCPV. Это заставляет платформу автоматически подсчитать итоги по каждому родителю и подчиненному элементу.Выбрать() константу ОбходРезультатаЗапроса.ПоГруппировкам и имя группировки "КодCPV".ВыборкаКод.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "ПодкодCPV"), получая только подчиненные записи текущего кода.Строка(НомСтроки) + "." + Строка(НомПодстроки)), что предотвращает накопление погрешностей дробных чисел с плавающей точкой.Рассмотрим альтернативный способ, который удобен при необходимости предварительной обработки данных в коде перед их отправкой в макет. Запрос объединяет строки по полям группировки с помощью конструкции СГРУППИРОВАТЬ ПО, а секция ИТОГИ позволяет получить готовую иерархическую структуру.
Посмотрим на пример выгрузки данных запроса в объект ДеревоЗначений:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Состав.Ссылка.КодCPV КАК КодCPV,
| МАКСИМУМ(Состав.Ссылка.Описание) КАК Описание,
| Состав.ПодкодCPV КАК ПодкодCPV,
| МАКСИМУМ(Состав.ОписаниеЗакупки) КАК ОписаниеЗакупки,
| СУММА(Состав.Сумма) КАК Сумма
|ИЗ
| Документ.э_ПланЗакупок.СоставПланаЗакупок КАК Состав
|СГРУППИРОВАТЬ ПО
| Состав.Ссылка.КодCPV,
| Состав.ПодкодCPV
|ИТОГИ
| СУММА(Сумма)
|ПО
| КодCPV,
| ПодкодCPV";
РезультатЗапроса = Запрос.Выполнить();
// Выгружаем данные с сохранением иерархии группировок
ДеревоДанных = РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
НомСтроки = 0;
Для Каждого СтрокаКод Из ДеревоДанных.Строки Цикл
НомСтроки = НомСтроки + 1;
// Вывод области верхнего уровня
ОбластьГруппа.Параметры.НомерСтроки = НомСтроки;
ОбластьГруппа.Параметры.КодПоКлассификатору = СтрокаКод.КодCPV;
ОбластьГруппа.Параметры.НаименованиеЗакупки = СтрокаКод.Описание;
ОбластьГруппа.Параметры.СуммаЗакупки = СтрокаКод.Сумма;
ТабДок.Вывести(ОбластьГруппа);
НомПодстроки = 0;
Для Каждого СтрокаПодкод Из СтрокаКод.Строки Цикл
НомПодстроки = НомПодстроки + 1;
ОбластьПодстрока.Параметры.НомерПодстроки = Строка(НомСтроки) + "." + Строка(НомПодстроки);
ОбластьПодстрока.Параметры.ПодКодПоКлассификатору = СтрокаПодкод.ПодкодCPV;
ОбластьПодстрока.Параметры.ПодНаименованиеЗакупки = СтрокаПодкод.ОписаниеЗакупки;
ОбластьПодстрока.Параметры.СуммаЭлемента = СтрокаПодкод.Сумма;
ТабДок.Вывести(ОбластьПодстрока);
КонецЦикла;
КонецЦикла;
Такой подход обеспечивает чистый и понятный код обхода коллекций через циклы Для Каждого, а агрегация данных полностью переносится на сторону СУБД.