Как скрыть или удалить строки и родительские группировки с нулевыми показателями в табличном документе 1С

Программист 1С v8.3 (Обычные формы) Управленческий учет
← На главную

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

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

---

Причина сложности: алгоритм «заглядывания вперед» (Look-ahead)

Когда мы последовательно обходим дерево или выборку запроса в цикле, алгоритм работает сверху вниз:

  1. Программа получает родительскую запись (Группу).
  2. Выводит ее в табличный документ (например, строку «Материалы» или «Валенки»).
  3. Переходит к подчиненным элементам и рассчитывает для них показатели.

Если в процессе расчета выясняется, что у всех подчиненных элементов показатель _КолЗаказать равен 0, то ранее выведенная строка группы становится «пустой» и только портит внешний вид отчета. Нам необходимо либо заранее знать, есть ли внутри группы ненулевые данные, либо уметь «откатывать» вывод назад.

---

Решение №1: Динамическое удаление областей по замеру высоты таблицы (Метод «Отката»)

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

Разберем этот алгоритм на примере рекурсивной процедуры ВывестиГруппировкуПечать. Проанализируем ключевые шаги реализации этого метода.

Шаг 1. Получение и вывод родительской области. Программа получает область макета и выводит ее в табличный документ, формируя строку группировки верхнего уровня:


Область = ВхМакет.ПолучитьОбласть(_ИмяСекции + "|Начало");
// ... Заполнение параметров области ...
УдаляемаяОбласть = ВхТаблДок.Вывести(Область, ВхУровень);

Шаг 2. Фиксация высоты таблицы и границ области. Чтобы иметь возможность удалить именно ту строку, которую мы только что вывели, нам нужно точно определить ее координаты. Мы определяем координаты выведенной строки на основе текущей высоты таблицы и высоты выведенной области макета:


УдаляемаяОбласть = ВхТаблДок.Область(ВхТаблДок.ВысотаТаблицы - Область.ВысотаТаблицы + 1, , ВхТаблДок.ВысотаТаблицы, );
СтараяВысота = ВхТаблДок.ВысотаТаблицы;

Шаг 3. Рекурсивный вызов дочерних группировок. Программа запускает обход подчиненных элементов. Если они содержат ненулевые значения, они будут выведены, и общая высота табличного документа увеличится:


Если ВхНомГр + 1 < ВхТаблГр.Количество() Тогда
    _ЗапрВыб = ВхЗапрВыб.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией, ВхТаблГр[ВхНомГр + 1].ИмяИзмерения);
    ВывестиГруппировкуПечать(ВхМакет, _ЗапрВыб, "Строки", ВхНомГр + 1, ВхТаблГр, ВхУровень + 1, ВхТаблДок);
КонецЕсли;

Шаг 4. Проверка изменений и удаление «пустой» группы. После завершения рекурсивного вывода мы сравниваем текущую высоту таблицы со значением СтараяВысота. Если они равны, это значит, что дочерние элементы не выводились. Мы удаляем родительскую строку с помощью метода УдалитьОбласть:


Если ВхТаблДок.ВысотаТаблицы = СтараяВысота Тогда
    ВхТаблДок.УдалитьОбласть(УдаляемаяОбласть, ТипСмещенияТабличногоДокумента.ПоВертикали);
КонецЕсли;

Посмотрим на полный и скорректированный код рекурсивной процедуры, реализующей этот подход:


Процедура ВывестиГруппировкуПечать(ВхМакет, ВхЗапрВыб, ВхТипГр, ВхНомГр, ВхТаблГр, ВхУровень, ВхТаблДок, ВхТаблПакетныйЗаказПоставщику = Неопределено)
    
    _ИмяСекции = ?(ВхТипГр = "Строки", "Гр" + ВхУровень, "ГрИтоги");
    _ИмяГр     = ВхТаблГр[ВхНомГр].ИмяИзмерения;
    
    Пока ВхЗапрВыб.Следующий() Цикл
        
        // Получаем и выводим начало строки группировки
        Область = ВхМакет.ПолучитьОбласть(_ИмяСекции + "|Начало");
        _ЗначениеГр = ВхЗапрВыб[_ИмяГр];
        
        // Заполнение параметров (Номенклатура, единицы измерения и т.д.)
        Если ТипЗнч(_ЗначениеГр) = Тип("СправочникСсылка.Номенклатура") Тогда
            Если _ЗначениеГр.ЭтоГруппа Тогда
                Область.Параметры.ЗначениеГрСтр = ВРЕГ(_ЗначениеГр.Наименование);
            Иначе
                Область.Параметры.ЗначениеГрСтр = _ЗначениеГр.Наименование;
            КонецЕсли;
        Иначе
            Область.Параметры.ЗначениеГрСтр = _ЗначениеГр;
        КонецЕсли;
        
        ВхТаблДок.Вывести(Область, ВхУровень);
        
        // Присоединяем секцию с показателями заказа
        ОбластьПоказатели = ВхМакет.ПолучитьОбласть(_ИмяСекции + "|ИтогоЗаказ");
        
        // Здесь происходит расчет потребности к заказу
        _КолЗаказать = 0;
        _КолДефицит = -ВхЗапрВыб.КолПрофицит;
        _МинЗаказ = ВхЗапрВыб.КолРасходЗаДень * ВхЗапрВыб.СрокПоставкиДней;
        
        Если _МинЗаказ > 0 Или _КолДефицит > 0 Тогда
            _КолЗаказать = Макс(_МинЗаказ, _КолДефицит);
        КонецЕсли;
        
        ОбластьПоказатели.Параметры.КолЗаказать = _КолЗаказать;
        ВхТаблДок.Присоединить(ОбластьПоказатели);
        
        // Фиксируем область строки и высоту таблицы до ухода на рекурсию
        УдаляемаяОбласть = ВхТаблДок.Область(ВхТаблДок.ВысотаТаблицы - Область.ВысотаТаблицы + 1, , ВхТаблДок.ВысотаТаблицы, );
        СтараяВысота = ВхТаблДок.ВысотаТаблицы;
        
        // Проверяем, является ли текущая запись итогом по иерархии (группой номенклатуры)
        Если ВхЗапрВыб.ТипЗаписи() = ТипЗаписиЗапроса.ИтогПоИерархии Тогда
            
            // Выбираем подчиненные элементы этой же группировки
            _ЗапрВыб = ВхЗапрВыб.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией, ВхТаблГр[ВхНомГр].ИмяИзмерения);
            ВывестиГруппировкуПечать(ВхМакет, _ЗапрВыб, "Строки", ВхНомГр, ВхТаблГр, ВхУровень + 1, ВхТаблДок, ВхТаблПакетныйЗаказПоставщику);
            
            // Если высота таблицы не изменилась, значит внутри группы все строки нулевые - удаляем группу
            Если ВхТаблДок.ВысотаТаблицы = СтараяВысота Тогда
                ВхТаблДок.УдалитьОбласть(УдаляемаяОбласть, ТипСмещенияТабличногоДокумента.ПоВертикали);
            КонецЕсли;
            
        Иначе
            
            // Если есть следующая детальная группировка (например, Варианты Исполнения)
            Если ВхНомГр + 1 < ВхТаблГр.Количество() Тогда
                
                _ЗапрВыб = ВхЗапрВыб.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией, ВхТаблГр[ВхНомГр + 1].ИмяИзмерения);
                ВывестиГруппировкуПечать(ВхМакет, _ЗапрВыб, "Строки", ВхНомГр + 1, ВхТаблГр, ВхУровень + 1, ВхТаблДок, ВхТаблПакетныйЗаказПоставщику);
                
                // Если подчиненные варианты не вывелись, удаляем номенклатуру-владельца
                Если ВхТаблДок.ВысотаТаблицы = СтараяВысота Тогда
                    ВхТаблДок.УдалитьОбласть(УдаляемаяОбласть, ТипСмещенияТабличногоДокумента.ПоВертикали);
                КонецЕсли;
                
            ИначеЕсли _КолЗаказать = 0 Тогда
                
                // Это конечный детальный элемент, и у него нулевой заказ - удаляем его строку
                ВхТаблДок.УдалитьОбласть(УдаляемаяОбласть, ТипСмещенияТабличногоДокумента.ПоВертикали);
                
            КонецЕсли;
            
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Важное исправление ошибки смещения: Обратите внимание, что в исходном обсуждении на форуме предлагалось использовать ТипСмещенияТабличногоДокумента.ПоГоризонтали. Это грубая ошибка верстки. При удалении строк необходимо использовать смещение ПоВертикали. Смещение по горизонтали сдвигает ячейки справа налево, нарушая структуру колонок отчета и превращая печатную форму в хаотичный набор ячеек.

---

Почему решение с удалением областей — это «костыль»?

Несмотря на то, что представленный выше код решает задачу визуального скрытия строк, он имеет несколько критических недостатков, которые делают его неприменимым в реальных высоконагруженных базах данных:

---

Как сделать правильно? Архитектурные альтернативы

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

Вариант А. Предварительный расчет дерева значений

Вместо того чтобы производить расчеты непосредственно при выводе макета в табличный документ, разделим этот процесс на два независимых этапа:

  1. Этап вычислений: Выгружаем результат запроса в объект ДеревоЗначений. Обходим дерево снизу вверх. Рассчитываем показатели для детальных записей, суммируем их на уровне родительских группировок. Удаляем из дерева строки, у которых расчетное значение равно 0 (при обходе коллекции для удаления строк всегда двигаемся с конца к началу).
  2. Этап вывода: Очищенное от нулевых значений дерево выводим в табличный документ простым и быстрым циклом без использования ресурсоемких методов удаления.

Рассмотрим пример кода для предварительной фильтрации дерева значений:


// Функция производит расчет и удаляет пустые ветки дерева
Процедура ОчиститьДеревоОтНулей(СтрокиДерева)
    
    Индекс = СтрокиДерева.Количество() - 1;
    Пока Индекс >= 0 Цикл
        ТекущаяСтрока = СтрокиДерева[Индекс];
        
        // Рекурсивно обрабатываем подчиненные строки
        Если ТекущаяСтрока.Строки.Количество() > 0 Тогда
            ОчиститьДеревоОтНулей(ТекущаяСтрока.Строки);
        КонецЕсли;
        
        // Расчет показателя для текущего узла
        Если ТекущаяСтрока.Строки.Количество() = 0 Тогда
            // Это детальный лист дерева, проверяем его показатель
            Если ТекущаяСтрока.КолЗаказать = 0 Тогда
                СтрокиДерева.Удалить(ТекущаяСтрока);
            КонецЕсли;
        Иначе
            // Это родительский узел, проверяем, остались ли у него живые дети
            Если ТекущаяСтрока.Строки.Количество() = 0 Тогда
                СтрокиДерева.Удалить(ТекущаяСтрока);
            КонецЕсли;
        КонецЕсли;
        
        Индекс = Индекс - 1;
    КонецЦикла;
    
КонецПроцедуры

Вариант Б. Перенос расчетов в запрос

Это самый оптимальный путь с точки зрения производительности. Все математические операции по вычислению дефицита, проверке сроков поставки и округлению до минимальной партии закупки можно реализовать средствами языка запросов 1С.

Используя конструкции ВЫБОР КОГДА ... ТОГДА, мы можем рассчитать поле КолЗаказать на уровне СУБД, а затем наложить условие в секции ГДЕ или ИМЕЮТ:


// Пример фрагмента запроса с расчетом и фильтрацией
ВЫБРАТЬ
    Номенклатура,
    ВЫБОР
        КОГДА МинЗаказ > КолДефицит ТОГДА МинЗаказ
        ИНАЧЕ КолДефицит
    КОНЕЦ КАК КолЗаказать
ПОМЕСТИТЬ ВременнаяТаблица
ИЗ
    ИсходныеДанные;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Номенклатура,
    КолЗаказать
ИЗ
    ВременнаяТаблица КАК Таб
ГДЕ
    Таб.КолЗаказать > 0

После этого выборка запроса будет содержать исключительно те данные, которые гарантированно нужно вывести на печать. Код вывода упростится до обычного последовательного заполнения параметров макета.

Вариант В. Использование Системы компоновки данных (СКД)

Если перед вами стоит задача разработки нового отчета, настоятельно рекомендуем отказаться от ручного вывода в пользу СКД. В компоновщике данных вы можете описать расчет поля КолЗаказать на вкладке «Вычисленные поля» или в ресурсах, а затем установить стандартный пользовательский или фиксированный отбор КолЗаказать Больше 0. Система компоновки данных самостоятельно оптимизирует SQL-запрос, выполнит агрегацию по группировкам и автоматически скроет все пустые родительские папки без написания какого-либо программного кода.

← На главную