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

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

При разработке отчетов с использованием ручного вывода секций макета (через ТабличныйДокумент) программисты часто сталкиваются с задачей фильтрации строк «на лету». Типичный пример: необходимо скрыть из отчета строки номенклатуры, по которым величина заказа равна нулю (КолЗаказать = 0). Для этого разработчики выводят строку группировки, анализируют условия вывода и, если строка не подходит, удаляют её с помощью метода ТаблДок.УдалитьОбласть().

Однако при таком подходе возникает серьезная проблема: итоги по вышестоящим родительским группировкам не пересчитываются автоматически. Родительские строки продолжают отображать исходные суммарные значения, полученные из запроса, которые включают данные удаленных строк. В результате в отчете получается «каша»: общие потребности и запасы по группе равны крупным значениям, хотя ниже выведена всего одна позиция с минимальным значением.

В этой статье мы подробно разберем, почему возникает эта ошибка, и рассмотрим четыре эффективных способа ее решения.

Выясним первопричину некорректного поведения отчета. Метод ТабличныйДокумент.УдалитьОбласть() — это исключительно визуальное действие. Он удаляет блок ячеек из сетки документа, сдвигая соседние ячейки по горизонтали или вертикали. Но табличный документ в 1С не является «умной» интерактивной таблицей вроде Excel с живыми связями (если только вы явно не прописали формулы в ячейках через символ "=").

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

Область.Параметры.КолПотребность = ?(_ВыводитьИтогиПоГруппировке, ВхЗапрВыб.КолПотребностьВнешнаяя, 0);

Поскольку запрос рассчитывает итоги по всей иерархии в базе данных, родительская строка выводится первой и сразу заполняется «грязными» (полными) итогами. Когда алгоритм спускается ниже, обнаруживает нулевой заказ у подчиненного элемента и удаляет его область, значение в уже выведенной родительской строке никак не меняется. Чтобы исправить это, нам необходимо изменить архитектуру подготовки данных.

Вариант 1. Фильтрация данных на уровне запроса (Рекомендуемый)

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

Для этого всю логику расчета полей ТочкаЗаказа, МинЗаказ, Дефицит и КолЗаказать необходимо перенести из кода встроенного языка внутрь текста запроса. Рассмотрим пример реализации расчета заказа в запросе с помощью конструкции ВЫБОР КОГДА:


ВЫБРАТЬ
    Товары.Номенклатура КАК Номенклатура,
    Товары.КолОстаток КАК КолОстаток,
    Товары.КолПотребность КАК КолПотребность,
    // Расчет точки заказа
    Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + Товары.КолМинОстаток КАК ТочкаЗаказа,
    
    // Расчет минимального заказа
    ВЫБОР
        КОГДА Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + (Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + Товары.КолМинОстаток - Товары.КолОстаток) < 0
            ТОГДА 0
        ИНАЧЕ Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + (Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + Товары.КолМинОстаток - Товары.КолОстаток)
    КОНЕЦ КАК МинЗаказ
ПОМЕСТИТЬ ВТ_ПредварительныйРасчет
ИЗ
    РегистрНакопления.ЗапасыИПотребности КАК Товары
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ.Номенклатура,
    ВТ.КолОстаток,
    ВТ.КолПотребность,
    // Вычисляем итоговое количество к заказу с учетом дефицита и нормы пополнения
    ВЫБОР
        КОГДА ВТ.МинЗаказ > 0 ИЛИ (ВТ.КолПотребность - ВТ.КолОстаток) > 0
            ТОГДА ВТ.МинЗаказ // Здесь можно добавить логику функции МАКС()
        ИНАЧЕ 0
    КОНЕЦ КАК КолЗаказать
ИЗ
    ВТ_ПредварительныйРасчет КАК ВТ
ГДЕ
    // Фильтруем данные: исключаем позиции, которые не требуют заказа
    (ВЫБОР
        КОГДА ВТ.МинЗаказ > 0 ИЛИ (ВТ.КолПотребность - ВТ.КолОстаток) > 0
            ТОГДА ВТ.МинЗаказ
        ИНАЧЕ 0
    КОНЕЦ) > 0
ИТОГИ
    СУММА(КолОстаток),
    СУММА(КолПотребность),
    СУММА(КолЗаказать)
ПО
    Номенклатура ИЕРАРХИЯ

При таком подходе в выборку ВхЗапрВыб попадут только те строки, у которых расчетное количество заказа больше нуля. Платформа 1С самостоятельно соберет правильные итоги по иерархии номенклатуры. Нам останется лишь последовательно вывести области макета в табличный документ без использования метода УдалитьОбласть().

Вариант 2. Использование Дерева Значений (Двухпроходный анализ)

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

Разберем этот метод по шагам:

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

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


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

КонецПроцедуры

После вызова процедуры ОбработатьИПересчитатьДерево(ДеревоДанных) наше дерево значений будет содержать только те позиции номенклатуры, по которым реально требуется заказ, а все родительские узлы (группы номенклатуры) получат математически точные, пересчитанные итоги. Нам останется лишь запустить простой обход дерева и вывести его в макет.

Вариант 3. Возврат значений через рекурсивную функцию (Локальный фикс)

Если вам требуется быстро починить существующий код без радикального изменения структуры отчета, можно переписать процедуру ВывестиГруппировкуПечать, превратив её в Функцию, которая возвращает структуру с рассчитанными итогами.

Идея заключается в следующем: при выводе родительской строки мы запоминаем область ячеек, в которую она записалась. Затем рекурсивно вызываем функцию для подчиненных элементов. Подчиненные элементы выводятся на печать (или удаляются) и возвращают наверх структуру с суммами фактически выведенных данных. Получив эти данные от «детей», родитель возвращается к своей области в табличном документе и перезаписывает параметры итогов.

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


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

Этот способ требует внимательной работы с индексами колонок (например, ВхТаблДок.Область(НомерСтрокиРодителя, НомерКолонки)), однако он позволяет обойтись без полной перестройки архитектуры отчета.

Вариант 4. Перевод отчета на Систему компоновки данных (СКД)

Использование ручного вывода группировок макета в 1С 8.3 считается устаревшим подходом (наследием платформы 1С 7.7). Современный стандарт разработки — это Система компоновки данных (СКД).

В СКД описанная проблема решается автоматически за счет декларативного описания структуры отчета:

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

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

Подведем итог. Выбор решения зависит от масштабности вашей задачи:

← На главную