При разработке отчетов с использованием ручного вывода секций макета (через ТабличныйДокумент) программисты часто сталкиваются с задачей фильтрации строк «на лету». Типичный пример: необходимо скрыть из отчета строки номенклатуры, по которым величина заказа равна нулю (КолЗаказать = 0). Для этого разработчики выводят строку группировки, анализируют условия вывода и, если строка не подходит, удаляют её с помощью метода ТаблДок.УдалитьОбласть().
Однако при таком подходе возникает серьезная проблема: итоги по вышестоящим родительским группировкам не пересчитываются автоматически. Родительские строки продолжают отображать исходные суммарные значения, полученные из запроса, которые включают данные удаленных строк. В результате в отчете получается «каша»: общие потребности и запасы по группе равны крупным значениям, хотя ниже выведена всего одна позиция с минимальным значением.
В этой статье мы подробно разберем, почему возникает эта ошибка, и рассмотрим четыре эффективных способа ее решения.
Выясним первопричину некорректного поведения отчета. Метод ТабличныйДокумент.УдалитьОбласть() — это исключительно визуальное действие. Он удаляет блок ячеек из сетки документа, сдвигая соседние ячейки по горизонтали или вертикали. Но табличный документ в 1С не является «умной» интерактивной таблицей вроде Excel с живыми связями (если только вы явно не прописали формулы в ячейках через символ "=").
В коде процедуры ВывестиГруппировкуПечать данные для родительских группировок берутся напрямую из выборки запроса:
Область.Параметры.КолПотребность = ?(_ВыводитьИтогиПоГруппировке, ВхЗапрВыб.КолПотребностьВнешнаяя, 0);
Поскольку запрос рассчитывает итоги по всей иерархии в базе данных, родительская строка выводится первой и сразу заполняется «грязными» (полными) итогами. Когда алгоритм спускается ниже, обнаруживает нулевой заказ у подчиненного элемента и удаляет его область, значение в уже выведенной родительской строке никак не меняется. Чтобы исправить это, нам необходимо изменить архитектуру подготовки данных.
Самый правильный и производительный способ решения проблемы — сделать так, чтобы строки с нулевым заказом вообще не попадали в выборку запроса. Если данные будут отсекаться на уровне СУБД, платформа 1С автоматически рассчитает корректные итоги по группировкам (конструкция ИТОГИ ПО в запросе), и нам не придется писать сложный код обхода табличного документа.
Для этого всю логику расчета полей ТочкаЗаказа, МинЗаказ, Дефицит и КолЗаказать необходимо перенести из кода встроенного языка внутрь текста запроса. Рассмотрим пример реализации расчета заказа в запросе с помощью конструкции ВЫБОР КОГДА:
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
Товары.КолОстаток КАК КолОстаток,
Товары.КолПотребность КАК КолПотребность,
// Расчет точки заказа
Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + Товары.КолМинОстаток КАК ТочкаЗаказа,
// Расчет минимального заказа
ВЫБОР
КОГДА Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + (Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + Товары.КолМинОстаток - Товары.КолОстаток) < 0
ТОГДА 0
ИНАЧЕ Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + (Товары.КолРасходЗаДень * Товары.СрокПоставкиДней + Товары.КолМинОстаток - Товары.КолОстаток)
КОНЕЦ КАК МинЗаказ
ПОМЕСТИТЬ ВТ_ПредварительныйРасчет
ИЗ
РегистрНакопления.ЗапасыИПотребности КАК Товары
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.Номенклатура,
ВТ.КолОстаток,
ВТ.КолПотребность,
// Вычисляем итоговое количество к заказу с учетом дефицита и нормы пополнения
ВЫБОР
КОГДА ВТ.МинЗаказ > 0 ИЛИ (ВТ.КолПотребность - ВТ.КолОстаток) > 0
ТОГДА ВТ.МинЗаказ // Здесь можно добавить логику функции МАКС()
ИНАЧЕ 0
КОНЕЦ КАК КолЗаказать
ИЗ
ВТ_ПредварительныйРасчет КАК ВТ
ГДЕ
// Фильтруем данные: исключаем позиции, которые не требуют заказа
(ВЫБОР
КОГДА ВТ.МинЗаказ > 0 ИЛИ (ВТ.КолПотребность - ВТ.КолОстаток) > 0
ТОГДА ВТ.МинЗаказ
ИНАЧЕ 0
КОНЕЦ) > 0
ИТОГИ
СУММА(КолОстаток),
СУММА(КолПотребность),
СУММА(КолЗаказать)
ПО
Номенклатура ИЕРАРХИЯ
При таком подходе в выборку ВхЗапрВыб попадут только те строки, у которых расчетное количество заказа больше нуля. Платформа 1С самостоятельно соберет правильные итоги по иерархии номенклатуры. Нам останется лишь последовательно вывести области макета в табличный документ без использования метода УдалитьОбласть().
Если алгоритмы расчета параметров слишком сложные (например, они используют функции общих модулей, интеграции или сложные формулы, которые невозможно перенести в запрос), то оптимальным решением будет предварительная обработка данных в оперативной памяти с помощью объекта ДеревоЗначений.
Разберем этот метод по шагам:
ДеревоДанных = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);Посмотрим на пример программной реализации этого алгоритма:
Процедура ОбработатьИПересчитатьДерево(ТекущаяСтрокаИлиДерево)
// Сначала рекурсивно обрабатываем подчиненные строки (движемся снизу вверх)
Индекс = ТекущаяСтрокаИлиДерево.Строки.Количество() - 1;
Пока Индекс >= 0 Цикл
ПодчиненнаяСтрока = ТекущаяСтрокаИлиДерево.Строки[Индекс];
// Рекурсивный вызов для ухода на самый глубокий уровень иерархии
ОбработатьИПересчитатьДерево(ПодчиненнаяСтрока);
Индекс = Индекс - 1;
КонецЦикла;
// Теперь обрабатываем текущий уровень, когда все подчиненные уже отфильтрованы и пересчитаны
Если ТипЗнч(ТекущаяСтрокаИлиДерево) = Тип("СтрокаДереваЗначений") Тогда
Если ТекущаяСтрокаИлиДерево.Строки.Количество() = 0 Тогда
// Это конечный элемент (лист дерева) - выполняем расчет заказа
РасчитатьПоказателиДляСтроки(ТекущаяСтрокаИлиДерево);
// Если заказ не нужен, помечаем строку на удаление
Если ТекущаяСтрокаИлиДерево.КолЗаказать = 0 Тогда
ТекущаяСтрокаИлиДерево.Родитель.Строки.Удалить(ТекущаяСтрокаИлиДерево);
КонецЕсли;
Иначе
// Это родительская ветка - пересчитываем её итоги по оставшимся детям
СуммаПотребности = 0;
СуммаЗапасов = 0;
СуммаЗаказа = 0;
Для Каждого СтрИзДетей Из ТекущаяСтрокаИлиДерево.Строки Цикл
СуммаПотребности = СуммаПотребности + СтрИзДетей.КолПотребность;
СуммаЗапасов = СуммаЗапасов + СтрИзДетей.КолОстаток;
СуммаЗаказа = SumмаЗаказа + СтрИзДетей.КолЗаказать;
КонецЦикла;
ТекущаяСтрокаИлиДерево.КолПотребность = СуммаПотребности;
ТекущаяСтрокаИлиДерево.КолОстаток = СуммаЗапасов;
ТекущаяСтрокаИлиДерево.КолЗаказать = СуммаЗаказа;
// Если у родителя не осталось живых дочерних строк, его тоже можно удалить
Если ТекущаяСтрокаИлиДерево.Строки.Количество() = 0 И ТекущаяСтрокаИлиДерево.Родитель <> Неопределено Тогда
ТекущаяСтрокаИлиДерево.Родитель.Строки.Удалить(ТекущаяСтрокаИлиДерево);
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
После вызова процедуры ОбработатьИПересчитатьДерево(ДеревоДанных) наше дерево значений будет содержать только те позиции номенклатуры, по которым реально требуется заказ, а все родительские узлы (группы номенклатуры) получат математически точные, пересчитанные итоги. Нам останется лишь запустить простой обход дерева и вывести его в макет.
Если вам требуется быстро починить существующий код без радикального изменения структуры отчета, можно переписать процедуру ВывестиГруппировкуПечать, превратив её в Функцию, которая возвращает структуру с рассчитанными итогами.
Идея заключается в следующем: при выводе родительской строки мы запоминаем область ячеек, в которую она записалась. Затем рекурсивно вызываем функцию для подчиненных элементов. Подчиненные элементы выводятся на печать (или удаляются) и возвращают наверх структуру с суммами фактически выведенных данных. Получив эти данные от «детей», родитель возвращается к своей области в табличном документе и перезаписывает параметры итогов.
Проанализируем пример такой реализации:
Функция ВывестиГруппировкуПечать(ВхМакет, ВхЗапрВыб, ВхТипГр, ВхНомГр, ВхТаблГр, ВхУровень, ВхТаблДок)
// Структура для накопления итогов по фактически выведенным строкам
ИтогиВетки = Новый Структура("Потребность, Остаток, Заказать", 0, 0, 0);
// Запоминаем высоту таблицы перед выводом текущей группировки
СтараяВысота = ВхТаблДок.ВысотаТаблицы;
// Получаем область родительской группировки и выводим её
ОбластьРодителя = ВхМакет.ПолучитьОбласть("Гр" + ВхУровень + "|Начало");
// Заполняем первоначальные параметры
ОбластьРодителя.Параметры.ЗначениеГрСтр = ВхЗапрВыб[ВхТаблГр[ВхНомГр].ИмяИзмерения];
ВхТаблДок.Вывести(ОбластьРодителя, ВхУровень);
// Запоминаем точный адрес выведенной строки родителя для последующего обновления
НомерСтрокиРодителя = ВхТаблДок.ВысотаТаблицы;
// Флаг, определяющий, вывелся ли хоть один подчиненный элемент
ЕстьВыведенныеДети = Ложь;
// Переходим к выводу подчиненных элементов
Если ВхНомГр + 1 < ВхТаблГр.Количество Тогда
ВыборкаДетей = ВхЗапрВыб.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией, ВхТаблГр[ВхНомГр + 1].ИмяИзмерения);
Пока ВыборкаДетей.Следующий() Цикл
// Рекурсивный вызов возвращает итоги по выведенной ветке
ИтогиПодчиненного = ВывестиГруппировкуПечать(ВхМакет, ВыборкаДетей, "Строки", ВхНомГр + 1, ВхТаблГр, ВхУровень + 1, ВхТаблДок);
Если ИтогиПодчиненного.Заказать > 0 Тогда
ЕстьВыведенныеДети = Истина;
// Накапливаем итоги для текущего родителя
ИтогиВетки.Потребность = ИтогиВетки.Потребность + ИтогиПодчиненного.Потребность;
ИтогиВетки.Остаток = ИтогиВетки.Остаток + ИтогиПодчиненного.Остаток;
ИтогиВетки.Заказать = ИтогиВетки.Заказать + ИтогиПодчиненного.Заказать;
КонецЕсли;
КонецЦикла;
Иначе
// Это самый нижний уровень (детальная запись)
// Выполняем расчет показателей для текущей строки
КоличествоЗаказать = РассчитатьЗаказНаОсновеДанных(ВхЗапрВыб);
Если КоличествоЗаказать > 0 Тогда
ИтогиВетки.Потребность = ВхЗапрВыб.КолПотребностьВнешнаяя;
ИтогиВетки.Остаток = ВхЗапрВыб.КолОстаток;
ИтогиВетки.Заказать = КоличествоЗаказать;
ЕстьВыведенныеДети = Истина;
КонецЕсли;
EndЕсли;
// Корректировка родительской строки
Если ЕстьВыведенныеДети Тогда
// Возвращаемся к строке родителя в ТабличномДокументе и обновляем её ячейки новыми итогами
ВхТаблДок.Область(НомерСтрокиРодителя, 2).Текст = ИтогиВетки.Потребность;
ВхТаблДок.Область(НомерСтрокиРодителя, 3).Текст = ИтогиВетки.Остаток;
ВхТаблДок.Область(НомерСтрокиРодителя, 4).Текст = ИтогиВетки.Заказать;
Иначе
// Если ни один ребенок не вывелся (у всех заказ = 0), удаляем саму родительскую строку
ОбластьУдаления = ВхТаблДок.Область(СтараяВысота + 1, , ВхТаблДок.ВысотаТаблицы, );
ВхТаблДок.УдалитьОбласть(ОбластьУдаления, ТипСмещенияТабличногоДокумента.ПоВертикали);
// Обнуляем итоги ветки, так как она полностью удалена
ИтогиВетки.Потребность = 0;
ИтогиВетки.Остаток = 0;
ИтогиВетки.Заказать = 0;
КонецЕсли;
Возврат ИтогиВетки;
КонецФункции
Этот способ требует внимательной работы с индексами колонок (например, ВхТаблДок.Область(НомерСтрокиРодителя, НомерКолонки)), однако он позволяет обойтись без полной перестройки архитектуры отчета.
Использование ручного вывода группировок макета в 1С 8.3 считается устаревшим подходом (наследием платформы 1С 7.7). Современный стандарт разработки — это Система компоновки данных (СКД).
В СКД описанная проблема решается автоматически за счет декларативного описания структуры отчета:
КолЗаказать во вкладку Вычисляемые поля или рассчитываете её в запросе.Потребность, Запас и КолЗаказать как Ресурсы отчета со значением расчета Сумма(...).КолЗаказать Больше 0.Платформа 1С самостоятельно произведет фильтрацию записей СУБД, уберет ненужные строки, а затем правильно и быстро соберет итоги по всем родительским группировкам без написания единой строчки кода на встроенном языке. Это повысит скорость работы отчета и облегчит его дальнейшую поддержку.
Подведем итог. Выбор решения зависит от масштабности вашей задачи: