Как программно проверить, что отчет СКД пустой, перед отправкой или сохранением?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В процессе разработки автоматизированных систем на платформе 1С:Предприятие часто возникает задача автоматической рассылки или сохранения отчетов во внешние файлы (Excel, PDF, Word). При этом крайне важно избежать отправки «пустых» файлов, содержащих исключительно шапку, заголовки таблиц и пустые строки итогов. Отправка таких отчетов пользователям снижает качество сервиса и несет лишнюю нагрузку на почтовые серверы.

Определить визуально, пуст ли отчет, для пользователя не составляет труда. Однако программно сделать это в системе компоновки данных (СКД) не так просто, как кажется на первый взгляд. В этой статье мы подробно разберем, почему стандартные подходы могут возвращать неверный результат, проанализируем различные методы решения этой проблемы и выберем наиболее быстрый и надежный способ проверки.

Почему простая выгрузка в Таблицу Значений работает некорректно?

Первая идея, которая приходит в голову большинству разработчиков, — выгрузить результат компоновки данных в коллекцию (например, в ТаблицаЗначений или ДеревоЗначений) и проверить количество строк через метод Количество(). Рассмотрим стандартный код такой проверки:


КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, КомпоновщикНастроек.ПолучитьНастройки(), , , Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));

ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки);

ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ТаблицаЗначений = Новый ТаблицаЗначений;
ПроцессорВывода.УстановитьОбъект(ТаблицаЗначений);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);

Если ТаблицаЗначений.Количество() = 0 Тогда
    ВызватьИсключение "Отчет пустой!";
КонецЕсли;

Протестировав данный код на пустой базе данных, мы с удивлением обнаружим, что метод ТаблицаЗначений.Количество() возвращает значение больше нуля (обычно 1 или более строк), хотя реальных данных в информационной базе нет. Выясним причину такого поведения платформы.

Дело в том, что по умолчанию генератор макета компоновки для коллекции значений переносит в результат всю структуру отчета. Если в настройках компоновщика включен вывод общих итогов или присутствуют родительские группировки, то СКД сформирует строки под эти итоги и группировки. В результате в полученной таблице значений окажется одна строка, все колонки ресурсов которой будут заполнены значениями NULL или 0. Проверка на Количество() > 0 в данном случае не сработает.

Если мы попытаемся применить условие Количество() > 1, то столкнемся с другой проблемой: если отчет содержит ровно одну легитимную запись, система посчитает его пустым и не отправит получателю.

Решение проблемы выгрузки в Таблицу Значений

Чтобы метод выгрузки в ТаблицаЗначений работал корректно и действительно возвращал пустую коллекцию при отсутствии данных, необходимо предварительно подготовить настройки отчета перед компоновкой макета. Нам нужно принудительно отключить вывод общих итогов и оставить в структуре только детальные записи.

Рассмотрим, как это сделать программно:


// Создаем копию настроек, чтобы не испортить исходные настройки пользователя
НастройкиДляПроверки = КомпоновщикНастроек.ПолучитьНастройки();

// Отключаем вывод общих итогов по вертикали и горизонтали
ПараметрВыводаИтоги = НастройкиДляПроверки.ПараметрыВывода.НайтиЗначениеПараметра(Новый ПараметрКомпоновкиДанных("HorizontalOverallPlacement"));
Если ПараметрВыводаИтоги <> Неопределено Тогда
    ПараметрВыводаИтоги.Значение = РасположениеИтоговКомпоновкиДанных.Нет;
    ПараметрВыводаИтоги.Использование = Истина;
КонецЕсли;

ПараметрВыводаИтогиВерт = НастройкиДляПроверки.ПараметрыВывода.НайтиЗначениеПараметра(Новый ПараметрКомпоновкиДанных("VerticalOverallPlacement"));
Если ПараметрВыводаИтогиВерт <> Неопределено Тогда
    ПараметрВыводаИтогиВерт.Значение = РасположениеИтоговКомпоновкиДанных.Нет;
    ПараметрВыводаИтогиВерт.Использование = Истина;
КонецЕсли;

// Оставляем только детальные записи в структуре, удаляя лишние группировки
НастройкиДляПроверки.Структура.Очистить();
НоваяГруппировка = НастройкиДляПроверки.Структура.Добавить(Тип("ГруппировкаКомпоновкиДанных"));
НоваяГруппировка.Использование = Истина;

// Теперь генерируем макет и выгружаем в ТЗ
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, НастройкиДляПроверки, , , Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));

ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки);

ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ДанныеТЗ = Новый ТаблицаЗначений;
ПроцессорВывода.УстановитьОбъект(ДанныеТЗ);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);

Если ДанныеТЗ.Количество() = 0 Тогда
    Сообщить("Отчет не содержит данных!");
КонецЕсли;

При таком подходе, если запрос СКД вернет пустой результат, полученная таблица значений гарантированно будет иметь 0 строк, так как группировочные и итоговые строки мы предварительно отключили.

Выполнение запроса макета напрямую (почему это плохой способ?)

Существует еще один популярный способ, который часто предлагают на форумах: получить текст запроса из сформированного макета компоновки и выполнить его стандартным объектом Запрос базы данных.

Давайте посмотрим, как выглядит этот программный код:


КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, КомпоновщикНастроек.Настройки);

// Предположим, что у нас один набор данных с именем НаборДанных1
Если МакетКомпоновки.НаборыДанных.Найти("НаборДанных1") <> Неопределено Тогда
    ТекстЗапроса = МакетКомпоновки.НаборыДанных.НаборДанных1.Запрос;
    Запрос = Новый Запрос(ТекстЗапроса);
    
    // Переносим параметры из макета в запрос
    Для Каждого ПараметрЗапроса Из МакетКомпоновки.ЗначенияПараметров Цикл
        Запрос.УстановитьПараметр(ПараметрЗапроса.Имя, ПараметрЗапроса.Значение);
    КонецЦикла;
    
    Если Запрос.Выполнить().Пустой() Тогда
        Сообщить("В отчете нет данных!");
    КонецЕсли;
КонецЕсли;

Проанализируем этот способ подробнее и выделим его критические недостатки:

  1. Снижение производительности. Мы фактически заставляем СУБД выполнять один и тот же тяжелый запрос дважды. Сначала для проверки на пустоту, а затем — для реального вывода отчета на экран или в файл. Если отчет строится по миллионам записей, время его генерации увеличится ровно в два раза.
  2. Несовместимость с внешними источниками. Если в вашей схеме компоновки данных используются внешние наборы данных (например, предварительно подготовленная таблица значений, переданная в процессор компоновки), то у макета не будет свойства Запрос. Код аварийно завершится с ошибкой.
  3. Сложные связи наборов данных. В сложных схемах, где используются связи между несколькими наборами данных, СКД оптимизирует выполнение и разбивает его на множество мелких подзапросов. Вытащить один результирующий запрос программно становится нерешаемой задачей.
  4. Постобработка данных. Данный метод полностью игнорирует настройки условного оформления, отборы на уровне вычисляемых полей и другие механизмы постобработки данных внутри СКД. Запрос может вернуть строки, но на уровне вывода они будут отсечены СКД, и пользователь все равно получит пустую форму.

Оптимальное решение: поэлементный вывод и анализ параметров

Наиболее производительным, надежным и универсальным методом является поэлементный вывод результата компоновки данных. Этот же подход используется в стандартных библиотеках фирмы 1С (в частности, в Библиотеке Стандартных Подсистем — БСП, общий модуль ОтчетыСервер, функция ОтчетПустой).

Суть метода заключается в том, что мы не выводим весь отчет целиком в память. Вместо этого мы инициализируем стандартный ПроцессорКомпоновкиДанных и начинаем пошагово считывать элементы результата с помощью метода Следующий(). Как только мы находим первый элемент, содержащий реальное заполненное значение параметра макета, мы немедленно прерываем цикл. Это означает, что в отчете есть как минимум одна строка с данными.

Посмотрим на пример реализации данного алгоритма:


// Инициализируем макет и процессоры как обычно
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, КомпоновщикНастроек.ПолучитьНастройки(), ДанныеРасшифровки);

ПроцессорКомпоновкиДанных = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновки, , ДанныеРасшифровки, Истина);

ПустойРезультат = Истина;

// Начинаем поэлементный обход без полного вывода в документ
Пока Истина Цикл
    ЭлементРезультата = ПроцессорКомпоновкиДанных.Следующий();
    Если ЭлементРезультата = Неопределено Тогда
        Прервать; // Достигнут конец отчета
    КонецЕсли;
    
    // Проверяем элементы макета на наличие заполненных значений
    Для Каждого ПараметрЭлемента Из ЭлементРезультата.ЗначенияПараметров Цикл
        // Исключаем системные параметры и проверяем значимые данные
        Если ЗначениеЗаполнено(ПараметрЭлемента.Значение) Тогда
            ПустойРезультат = Ложь;
            Прервать; // Найдена первая строка с данными, прекращаем внутренний цикл
        КонецЕсли;
    КонецЦикла;
    
    Если Не ПустойРезультат Тогда
        Прервать; // Прекращаем внешний обход, отчет точно не пуст!
    КонецЕсли;
КонецЦикла;

Если ПустойРезультат Тогда
    Сообщить("Отчет пустой! Запись и отправка отменены.");
Иначе
    Сообщить("В отчете есть данные, продолжаем формирование...");
    // Здесь выполняем полноценный вывод в ТабличныйДокумент
КонецЕсли;

Преимущества поэлементного обхода:

Проверка готового Табличного Документа (пост-анализ)

Иногда возникают ситуации, когда отчет уже сформирован в объект ТабличныйДокумент (например, пользователь нажал кнопку «Сформировать» на форме), и нам нужно проверить его состояние перед сохранением на диск. Простая проверка свойства ТабДок.ВысотаТаблицы здесь не поможет, так как шапка отчета и заголовки колонок всегда заполняют строки, делая высоту больше нуля.

В этом случае мы можем применить алгоритм выборочной проверки ячеек. Мы знаем, что шапка отчета обычно занимает фиксированное количество строк сверху (например, первые 5-10 строк). Следовательно, мы можем проигнорировать эту область и проверить только ячейки, идущие ниже.

Разберем пример функции проверки готового табличного документа:


Функция ТабличныйДокументПустой(ТабДок, СтрокаНачалаДанных = 7) Экспорт
    
    // Если высота документа меньше строки начала данных, значит кроме шапки ничего нет
    Если ТабДок.ВысотаТаблицы < СтрокаНачалаДанных Тогда
        Возврат Истина;
    КонецЕсли;
    
    // Обходим ячейки в поисках любого текстового наполнения
    Для НомерСтроки = СтрокаНачалаДанных По ТабДок.ВысотаТаблицы Цикл
        Для НомерКолонки = 1 По ТабДок.ШиринаТаблицы Цикл
            ТекущаяОбласть = ТабДок.Область(НомерСтроки, НомерКолонки);
            Если ЗначениеЗаполнено(ТекущаяОбласть.Текст) Тогда
                // Нашли заполненную ячейку
                Возврат Ложь;
            КонецЕсли;
        КонецЦикла;
    КонецЦикла;
    
    Возврат Истина;
КонецФункции

Этот способ уступает в производительности поэлементному обходу процессора компоновки, но он незаменим, когда мы работаем с уже готовым, сформированным табличным документом и не имеем прямого доступа к процессору вывода СКД.

Резюме и рекомендации

Подведем итоги нашего анализа:

  1. Не используйте повторные запросы к базе данных для проверки пустоты отчета — это создает двойную нагрузку на сервер СУБД и неприменимо для отчетов с внешними источниками.
  2. Если вы используете выгрузку в ТаблицаЗначений, не забудьте программно отключить общие итоги и группировки в настройках перед компоновкой макета, чтобы избежать «ложных» строк.
  3. Используйте поэлементный обход процессора компоновки как наиболее производительный и соответствующий стандартам разработки 1С (БСП) метод. Он гарантирует высокую скорость работы и минимальное потребление ресурсов сервера.
← На главную