При разработке отчетов в системе «1С:Предприятие» программисты часто используют Систему Компоновки Данных (СКД). Однако в некоторых случаях — например, при сложной логике расчета интервалов задолженности или при формировании нетиповых печатных форм — приходится прибегать к ручному программному выводу отчета в макет. В таких ситуациях задачи фильтрации данных ложатся полностью на плечи разработчика.
В этой статье мы подробно разберем, как правильно установить проверку (отбор) в цикле обхода данных, рассмотрим типичные логические ошибки начинающих разработчиков, такие как эффект «залипания» значений и неверное использование операторов перехода, а также изучим архитектурные паттерны для оптимизации производительности отчетов.
Представим сценарий: нам необходимо сформировать отчет по взаиморасчетам с клиентами. В отчете должна выводиться информация по платежам только тех контрагентов, у которых сумма долга со сроком более 30 дней строго больше нуля (Сумма30 > 0). Все остальные строки, не удовлетворяющие этому условию, выводить в табличный документ не нужно.
Сложность заключается в том, что расчет срока долга и распределение сумм по интервалам (в частности, расчет переменной Сумма30) происходят непосредственно в цикле обхода выборки, уже после выполнения запроса. Это делает невозможным простую фильтрацию данных на уровне предложения ГДЕ в тексте запроса.
Проанализируем фрагмент исходного кода, который часто вызывает затруднения у разработчиков:
// Фрагмент кода внутри цикла обхода выборки
Если ВсегоВсего <> СуммаТекущая Тогда
ПрошлыйМесяц2 = ДобавитьМесяц(ПрошлыйМесяц, -1);
Сумма30 = ПосчитатьСколькоСуммыПлатежаЗаПериод(ВыборкаДок.Регистратор, НачалоМесяца(ПрошлыйМесяц2), КонецМесяца(ПрошлыйМесяц2));
Сумма30 = Мин(Сумма30, ВсегоВсего - СуммаТекущая);
КонецЕсли;
Если мы попытаемся вставить проверку вывода сразу после этого блока, мы можем столкнуться с неожиданным поведением системы. Выясним причины этих ошибок и разберем правильные варианты реализации.
Обратите внимание на то, что переменная Сумма30 рассчитывается строго внутри условной конструкции Если ВсегоВсего <> СуммаТекущая Тогда. Если на определенной итерации цикла это условие не выполняется (например, для текущего клиента показатели равны), ветка расчетов пропускается.
В результате переменная Сумма30 сохраняет свое значение, полученное на предыдущей итерации цикла от совершенно другого контрагента! Программа проверит старое значение, посчитает его верным и выведет строку текущего клиента, у которого на самом деле долга нет. Это классическая ошибка инициализации данных в цикле.
Правило: Любые расчетные переменные, участвующие в условиях вывода строк, должны принудительно сбрасываться (инициализироваться нулем или неопределенным значением) в самом начале каждой итерации цикла.
Иногда для исключения ненужных строк разработчики пытаются использовать оператор перехода к следующей итерации цикла. Посмотрим на ошибочный пример:
Если Сумма30 > 0 Тогда
Продолжить;
КонецЕсли;
Данный код работает с точностью до наоборот. Оператор Продолжить прерывает выполнение текущей итерации и возвращает указатель в начало цикла. Таким образом, написав такое условие, мы пропустим вывод именно тех строк, где долг от 30 дней присутствует, и выведем пустые строки или строки с нулевым долгом.
Правильное отрицание условия должно выглядеть следующим образом:
Если Сумма30 <= 0 Тогда
Продолжить; // Пропускаем итерацию, если долга нет
КонецЕсли;
Если условие ложно (то есть Сумма30 больше нуля), управление передается коду вывода области в табличный документ.
Если структура отчета простая (линейный список без иерархии и сложных группировок), мы можем применить прямое условие перед методом Вывести() табличного документа. Разберем этот алгоритм по шагам:
Сумма30 нулевым значением.Если...Тогда.Проанализируем пример кода, реализующий эту логику:
ТабДок = Новый ТабличныйДокумент;
Макет = ПолучитьМакет("МакетОтчета");
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// 1. Обязательный сброс значения перед расчетами
Сумма30 = 0;
// 2. Расчет показателей (алгоритм разработчика)
Если Выборка.ВсегоВсего <> Выборка.СуммаТекущая Тогда
ПрошлыйМесяц2 = ДобавитьМесяц(ПрошлыйМесяц, -1);
Сумма30 = ПосчитатьСколькоСуммыПлатежаЗаПериод(Выборка.Регистратор, НачалоМесяца(ПрошлыйМесяц2), КонецМесяца(ПрошлыйМесяц2));
Сумма30 = Мин(Сумма30, Выборка.ВсегоВсего - Выборка.СуммаТекущая);
КонецЕсли;
// 3. Проверка условия вывода
Если Сумма30 > 0 Тогда
// Заполняем параметры области макета данными из выборки и расчетов
ОбластьСтрока.Параметры.Заполнить(Выборка);
ОбластьСтрока.Параметры.СуммаДолга30 = Сумма30;
// Выводим только проверенную строку
ТабДок.Вывести(ОбластьСтрока);
КонецЕсли;
КонецЦикла;
Этот способ прост в реализации, однако он имеет существенные недостатки при работе со сложными иерархическими отчетами или при больших объемах данных.
Для повышения читаемости кода и разделения логики получения данных и их визуализации рекомендуется использовать промежуточный буфер в виде объекта ТаблицаЗначений. Посмотрим, как это работает:
Выгрузить().Сумма30).Рассмотрим детальный пример реализации этого паттерна:
// Шаг 1: Получение исходной таблицы значений
ТаблицаДанных = РезультатЗапроса.Выгрузить();
ТаблицаДанных.Колонки.Добавить("Сумма30", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
// Шаг 2: Расчет данных в строках таблицы
Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
РасчетнаяСумма = 0;
Если СтрокаДанных.ВсегоВсего <> СтрокаДанных.СуммаТекущая Тогда
ПрошлыйМесяц2 = ДобавитьМесяц(ПрошлыйМесяц, -1);
РасчетнаяСумма = ПосчитатьСколькоСуммыПлатежаЗаПериод(СтрокаДанных.Регистратор, НачалоМесяца(ПрошлыйМесяц2), КонецМесяца(ПрошлыйМесяц2));
РасчетнаяСумма = Мин(РасчетнаяСумма, СтрокаДанных.ВсегоВсего - СтрокаДанных.СуммаТекущая);
КонецЕсли;
СтрокаДанных.Сумма30 = РасчетнаяСумма;
КонецЦикла;
// Шаг 3: Фильтрация таблицы (удаление неподходящих строк)
ИндексСтроки = ТаблицаДанных.Количество() - 1;
Пока ИндексСтроки >= 0 Цикл
СтрокаДляПроверки = ТаблицаДанных[ИндексСтроки];
Если СтрокаДляПроверки.Сумма30 <= 0 Тогда
ТаблицаДанных.Удалить(СтрокаДляПроверки);
КонецЕсли;
ИндексСтроки = ИндексСтроки - 1;
КонецЦикла;
// Шаг 4: Вывод подготовленных данных в макет
ТабДок = Новый ТабличныйДокумент;
Макет = ПолучитьМакет("МакетОтчета");
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
Для Каждого СтрокаВывода Из ТаблицаДанных Цикл
ОбластьСтрока.Параметры.Заполнить(СтрокаВывода);
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
Преимущество метода: Мы полностью разделили расчетную часть и построение визуальной формы. Код стал чище, его проще отлаживать. Кроме того, перед выводом мы всегда можем точно узнать количество строк, которые действительно попадут в финальный отчет.
Если ваш отчет содержит группировки (например, Контрагент -> Договоры -> Накладные), простое использование оператора Продолжить на детальных строках приведет к серьезному визуальному дефекту. Программа выведет заголовок группировки (Контрагента), начнет обходить его накладные, поймет, что долга старше 30 дней по ним нет, и пропустит детальные строки. В итоге в табличном документе отобразятся «пустые» контрагенты без подчиненных строк.
Для решения этой проблемы мы применим паттерн отложенного вывода (буферизации областей):
Сумма30.Рассмотрим схематичный пример реализации отложенного вывода:
ВыборкаКонтрагенты = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Контрагент");
Пока ВыборкаКонтрагенты.Следующий() Цикл
// Получаем область шапки группы, но пока НЕ выводим ее
ОбластьШапкаГруппы = Макет.ПолучитьОбласть("ШапкаГруппы");
ОбластьШапкаГруппы.Параметры.Заполнить(ВыборкаКонтрагенты);
БуферДетальныхСтрок = Новый Массив;
ВыборкаДетали = ВыборкаКонтрагенты.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
Сумма30 = 0;
// ... Расчет показателя Сумма30 по детальной строке ...
Если Сумма30 > 0 Тогда
ОбластьСтрока = Макет.ПолучитьОбласть("ДетальнаяСтрока");
ОбластьСтрока.Параметры.Заполнить(ВыборкаДетали);
ОбластьСтрока.Параметры.Сумма30 = Сумма30;
// Сохраняем готовую область в буфер
БуферДетальныхСтрок.Добавить(ОбластьСтрока);
КонецЕсли;
КонецЦикла;
// Если в буфере есть хотя бы одна подошедшая строка, выводим группу целиком
Если БуферДетальныхСтрок.Количество() > 0 Тогда
ТабДок.Вывести(ОбластьШапкаГруппы); // Вывод родителя
Для Каждого ОбластьИзБуфера Из БуферДетальныхСтрок Цикл
ТабДок.Вывести(ОбластьИзБуфера); // Вывод деталей
КонецЦикла;
КонецЕсли;
КонецЦикла;
Анализируя строку кода Сумма30 = ПосчитатьСколькоСуммыПлатежаЗаПериод(...), выполняющуюся внутри цикла, необходимо указать на серьезную архитектурную проблему производительности. Данный вызов порождает так называемую проблему N+1 запроса. Если в выборке отчета будет 10 000 клиентов, программа выполнит 10 000 отдельных мелких запросов к базе данных для получения платежей, что приведет к катастрофическому замедлению работы системы и перегрузке СУБД.
Решение проблемы:
НайтиСтроки().Применение этих рекомендаций позволит вашим отчетам работать быстро, стабильно и выдавать пользователям исключительно корректную и отфильтрованную информацию.