При разработке отчетов в среде 1С:Предприятие 7.7 программисты часто сталкиваются с задачей, когда для определенного аналитического разреза (например, контрагента, договора или объекта строительства) необходимо получить только один, самый актуальный по дате документ. Типичный пример — вывод последнего дополнительного соглашения по объекту при наличии целой истории изменений.
В этой статье мы подробно проанализируем, почему стандартные попытки решить эту задачу «в лоб» через обход группировок запроса часто приводят к ошибкам в логике или к аварийному завершению работы программы. Мы рассмотрим две критические ошибки в кодовой архитектуре и разберем два надежных способа решения проблемы: через правильное проектирование иерархии группировок и с помощью выгрузки результатов в ТаблицуЗначений.
Рассмотрим ситуацию, описанную разработчиком. В его регистре хранятся данные по объектам строительства, договорам и дополнительным соглашениям. Ему необходимо построить отчет, где для каждого объекта выводится только последнее дополнительное соглашение (изменение стоимости, сроков и т.д.). Однако отчет «выпрямляет» всю историю и выводит абсолютно все документы.
Выясним причину такого поведения и разберем две грубейшие ошибки в логике построения кода.
Ошибка 1. Нарушение иерархии группировок в запросе
Обратите внимание на структуру группировок в исходном запросе автора:
Группировка Отдел Упорядочить По Отдел.Код;
Группировка Документы Упорядочить По Документы.ДатаДок;
Группировка Вотчет Упорядочить По Вотчет.Код;
Группировка Объект Упорядочить По Объект.Код;
Группировка ТекДок;
Здесь группировка Документы (в которой находятся договоры и доп. соглашения) расположена на втором уровне, а группировка Объект — на четвертом. Это означает, что выборка сначала группирует данные по конкретному документу, а уже внутри него перебирает связанные объекты. При таком подходе один и тот же объект строительства неизбежно встретится в цикле несколько раз — в составе разных документов. Определить, какой из этих документов является для объекта «последним», внутри такой структуры циклов математически невозможно, так как иерархия вывернута наизнанку.
Ошибка 2. Смертельная опасность оператора Прервать в выборке запроса
На форумах часто советуют обходной маневр с использованием принудительного выхода из цикла. Например, развернуть группировку документов по убыванию даты и принудительно прервать выполнение цикла после обработки первого (то есть самого свежего) документа:
Пока Запрос.Группировка(5, -1) = 1 Цикл
Если Запрос.Документы.Вид() = "ДопСоглашение" Тогда
// Обработка данных
Прервать;
КонецЕсли;
КонецЦикла;
Важное предупреждение: Использование оператора Прервать внутри цикла Запрос.Группировка() в 1С 7.7 является грубейшей технологической ошибкой! Платформа v7.7 использует строгие последовательные указатели (курсоры) для навигации по временным таблицам результатов запроса. Когда вы принудительно прерываете цикл, указатель выборки сбивается. При переходе к следующему шагу внешнего цикла программа гарантированно пропустит часть данных, выведет некорректные итоги или аварийно завершится с системной ошибкой: «Не было обращения к старшим уровням выборки».
Проанализируем, как можно решить задачу штатными средствами запроса, не ломая логику работы платформы. Для этого нам необходимо поставить аналитический разрез (объект) выше самого документа.
Разберем по шагам правильный алгоритм построения запроса:
Объект выше группировки Документы.Прервать.Посмотрим на пример реализации данного подхода в кодовой части:
// Измененный текст запроса
ТекстЗапроса =
"Период С НачПер По ОконПер;
|Отдел = Регистр.Обороты.Объект.Отдел;
|Объект = Регистр.Обороты.Объект;
|Документы = Регистр.Обороты.Документ;
|СуммаДопСогл = Регистр.Обороты.СуммаДопСогл;
|// Другие переменные...
|Группировка Отдел Упорядочить По Отдел.Код;
|Группировка Объект Упорядочить По Объект.Код;
|Группировка Документы Упорядочить По Документы.ДатаДок Убыв;" // Сортируем документы по убыванию даты
;
Теперь напишем безопасный и корректный цикл обхода результатов запроса. Вместо ломающего выборку оператора Прервать мы применим логический флаг ДокументОбработан, который позволит нам мирно «докрутить» цикл до конца, не выполняя лишних действий:
Пока Запрос.Группировка("Отдел") = 1 Цикл
// Выводим секцию отдела
Таб.ВывестиСекцию("Отдел");
Пока Запрос.Группировка("Объект") = 1 Цикл
ДокументОбработан = 0; // Сбрасываем флаг для каждого нового объекта
Пока Запрос.Группировка("Документы") = 1 Цикл
// Если мы уже нашли и обработали самый последний документ,
// то все последующие (более старые) мы просто пропускаем
Если ДокументОбработан = 1 Тогда
Продолжить; // Это безопасно, так как мы не выходим из цикла Группировка()
КонецЕсли;
Если Запрос.Документы.Вид() = "ДопСоглашение" Тогда
// Запоминаем данные последнего соглашения
СуммаПоследнегоДопа = Запрос.СуммаДопСогл;
// Устанавливаем флаг, что цель достигнута
ДокументОбработан = 1;
КонецЕсли;
КонецЦикла;
// Выводим данные по объекту в отчет
Таб.ВывестиСекцию("Данные");
КонецЦикла;
КонецЦикла;
Использование оператора Продолжить вместо Прервать гарантирует, что внутренние итераторы платформы 1С 7.7 отработают корректно, и вы не столкнетесь со сбоем позиционирования.
Хотя первый метод работоспособен, в реальной практике разработки на платформе 1С 7.7 опытные специалисты предпочитают использовать объект ТаблицаЗначений. Выгрузка результатов запроса в ТЗ предоставляет программисту полную свободу действий: возможность произвольной сортировки, удаление дубликатов, поиск строк и применение сложных фильтров без привязки к жестким рамкам обхода группировок запроса.
Рассмотрим подробнее этот метод по шагам:
Посмотрим на пример программной реализации этого метода:
// 1. Выгружаем результаты запроса в Таблицу Значений
ТЗ = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТЗ, 0, 0); // Нули означают детальную выгрузку всех строк
// 2. Сортируем таблицу
// Знак минус "-" после имени колонки ДатаДок указывает системе на сортировку по убыванию
ТЗ.Сортировать("Объект, ДатаДок-");
// 3. Обходим таблицу и выводим только актуальные данные
ТЗ.ВыбратьСтроки();
ТекущийОбъект = "";
Номер = 0;
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
// Фильтруем только нужный вид документа, например, "ДопСоглашение"
Если ТЗ.Документы.Вид() <> "ДопСоглашение" Тогда
Продолжить;
КонецЕсли;
// Если этот объект мы уже обрабатывали, значит, сейчас перед нами
// более старое доп. соглашение (так как данные отсортированы по убыванию даты)
Если ТЗ.Объект = ТекущийОбъект Тогда
Продолжить;
КонецЕсли;
// Запоминаем текущий объект как обработанный
ТекущийОбъект = ТЗ.Объект;
// Заполняем переменные для вывода в секцию отчета
Номер = Номер + 1;
ПечОбъект = ТЗ.Объект;
ПечСумма = ТЗ.СуммаДопСогл;
ПечДокумент = ТЗ.Документы;
ПечДата = ТЗ.ДатаДок;
// Выводим строку в печатную форму
Таб.ВывестиСекцию("Данные");
_СуммаДопСогл = _СуммаДопСогл + ПечСумма;
КонецЦикла;
Давайте сопоставим оба решения, чтобы понять, в каких ситуациях их лучше применять:
Изучив эти подходы, вы сможете проектировать стабильные отчеты в 1С 7.7, которые не будут давать сбоев при росте объема базы данных и надежно отобразят только актуальную информацию.