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