Как проверить отчет СКД на пустоту и почему стандартные функции БСП возвращают Ложь?

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

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

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

Почему стандартная функция проверки на пустоту работает некорректно?

Давайте проанализируем, как устроен алгоритм стандартной проверки. Функция последовательно перебирает элементы компоновки данных через объект ПроцессорКомпоновкиДанных и анализирует заполненность их параметров. Ниже представлен типичный пример такой функции:


Функция ОтчетПустой(ОтчетОбъект, ПроцессорКД = Неопределено)
    Если ПроцессорКД = Неопределено Тогда
        Если ОтчетОбъект.СхемаКомпоновкиДанных = Неопределено Тогда
            Возврат Ложь; // Не СКД отчет.
        КонецЕсли;
        
        КомпоновщикМакетаКД = Новый КомпоновщикМакетаКомпоновкиДанных;
        МакетКД = КомпоновщикМакетаКД.Выполнить(ОтчетОбъект.СхемаКомпоновкиДанных, ОтчетОбъект.КомпоновщикНастроек.ПолучитьНастройки);
        
        ПроцессорКД = Новый ПроцессорКомпоновкиДанных;
        ПроцессорКД.Инициализировать(МакетКД, , , Истина);
    Иначе
        ПроцессорКД.Sбросить();
    КонецЕсли;

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

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

        Попытка
            ЭлементРезультатаКД = ПроцессорКД.Следующий;
        Исключение
            Возврат Ложь;
        КонецПопытки;
    КонецЦикла;

    ПроцессорВыводаРезультатаКД.ЗакончитьВывод;
    Возврат Истина;
КонецФункции

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

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

Разберем детально альтернативные, более надежные и быстрые способы проверки СКД на наличие данных.

---

Решение №1. Временное программное отключение служебных полей (Рекомендуемый метод)

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

Выполним этот алгоритм по шагам:

  1. Создадим копию настроек компоновщика, чтобы не испортить исходные настройки пользователя.
  2. В свойствах ПараметрыВывода программно установим значения НеВыводить для заголовка, параметров и отбора.
  3. Выполним компоновку макета и выведем результат в пустой временный ТабличныйДокумент.
  4. Проверим высоту полученной таблицы. Если высота равна нулю — данных нет.

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


Функция ПроверитьНаличиеДанныхВОтчете(НужныйОтчет) Экспорт

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

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

КонецФункции

Этот метод хорош тем, что он корректно работает со сложными структурами данных (включая кросс-таблицы и диаграммы) и гарантирует 100% точность результата без необходимости обхода коллекций в цикле.

---

Решение №2. Выгрузка результатов в Таблицу значений (с ограничениями)

Самый очевидный способ, который часто применяют начинающие разработчики, — это выгрузка результата работы процессора компоновки в ТаблицаЗначений или ДеревоЗначений с последующим анализом количества строк.

Проанализируем этот подход на примере кода:


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

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

Важные недостатки этого метода:

  1. Высокое потребление ресурсов. При больших объемах данных повторная полная выгрузка всей выборки в память в виде таблицы значений может привести к значительному снижению производительности системы и перерасходу оперативной памяти.
  2. Проблемы со сложной структурой. Метод выгрузки в коллекцию значений может выдавать ошибки или работать некорректно, если структура отчета содержит диаграммы, сводные таблицы или сложные кросс-таблицы. В таких случаях выгрузить результат в плоскую таблицу значений стандартным методом невозможно.

Используйте данный метод только для простых плоских реестров и при условии небольшого объема данных.

---

Решение №3. Использование функции-триггера в ресурсах СКД (Самый быстрый метод)

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

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

Разберем по шагам, как настроить этот механизм:

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

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

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

---

Решение №4. Фильтрация элементов по типу макета при поэлементном выводе

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

Сделать это можно путем анализа имен макетов, которые генерирует система компоновки. У элементов шапки, заголовков и параметров имена макетов обычно содержат фиксированные системные префиксы (например, "МакетЗаголовка", "Параметры" и т.д.).

Проанализируем пример доработки цикла проверки:


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

Такой подход защитит вашу функцию проверки от ложных срабатываний на заголовках отчета и выведенных отборах.

---

Подведем итоги

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

← На главную