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