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