В процессе разработки отчетов по производственным спецификациям или сложным вложенным структурам программисты 1С часто сталкиваются с задачей «разузлования». Суть проблемы заключается в том, что стандартные механизмы агрегации в Системе компоновки данных (СКД) отлично справляются с суммированием, но не имеют встроенной функции для произведения значений по иерархической ветке. Например, если для сборки одного комплекта мебели требуется 5 обеденных групп, а в каждую группу входит 4 стула, то на выходе мы должны получить 20 стульев. Если же внутри стула есть еще вложения (ножки, винты), расчет становится многоуровневым.
Разберем подробно, как решить эту задачу различными методами: от программной подготовки данных до использования продвинутых функций СКД.
Рассмотрим наиболее гибкий и понятный способ, который применила автор темы. Если структура иерархии заранее не определена и количество вложений может быть любым, мы можем подготовить данные во внешней таблице значений (ТЗ) перед передачей их в СКД. Основная идея заключается в использовании рекурсивной процедуры, которая проходит по всем уровням спецификации и вычисляет накопленный множитель.
Проанализируем пример кода, который выполняет эту задачу:
Процедура Посчитать(ТаблицаРезультат, ССпецификация, Множитель = 1)
// Обходим все комплектующие текущей спецификации
Для Каждого СтрокаСпецификации Из ССпецификация.ИсходныеКомплектующие Цикл
// Пытаемся найти, не обрабатывали ли мы уже эту номенклатуру в данном контексте
ПараметрыПоиска = Новый Структура("Спецификация, СпецификацияРодитель, Номенклатура",
СтрокаСпецификации.Спецификация, ССпецификация, СтрокаСпецификации.Номенклатура);
ЕстьСтрока = ТаблицаРезультат.НайтиСтроки(ПараметрыПоиска);
Если ЕстьСтрока.Количество() = 0 Тогда
НоваяСтрока = ТаблицаРезультат.Добавить;
// Важный момент: передаем накопленный множитель
НоваяСтрока.Множитель = Множитель;
НоваяСтрока.СпецификацияРодитель = ССпецификация;
Иначе
НоваяСтрока = ЕстьСтрока[0];
НоваяСтрока.Множитель = НоваяСтрока.Множитель * Множитель;
КонецЕсли;
НоваяСтрока.Спецификация = СтрокаСпецификации.Спецификация;
НоваяСтрока.Номенклатура = СтрокаСпецификации.Номенклатура;
// Записываем базовое количество из спецификации
НоваяСтрока.СКоличество = СтрокаСпецификации.Количество;
// Если у комплектующей есть своя спецификация — заходим глубже (рекурсия)
Если НЕ СтрокаСпецификации.Спецификация = Справочники.СпецификацииНоменклатуры.ПустаяСсылка Тогда
Посчитать(ТаблицаРезультат, СтрокаСпецификации.Спецификация, СтрокаСпецификации.Количество * Множитель);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
В данном алгоритме параметр Множитель передает вниз по дереву накопленный коэффициент. На каждом уровне вложенности количество из текущей строки умножается на множитель, пришедший от родителя. Итоговое количество в СКД будет рассчитываться как СКоличество * Множитель.
Выясним, как можно решить задачу без написания сложного кода, используя только возможности языка запросов и ресурсов СКД. Поскольку произведение чисел равно экспоненте от суммы их логарифмов (A * B * C = exp(ln(A) + ln(B) + ln(C))), мы можем «обмануть» систему, превратив операцию умножения в операцию сложения.
ЛогарифмКоличества, которое вычисляется как LOG(Количество).Сумма(ЛогарифмКоличества).РезультатКоличество с выражением EXP(Сумма(ЛогарифмКоличества)).Этот математический трюк позволяет СКД автоматически пробрасывать произведение коэффициентов через все уровни иерархии, используя стандартный механизм суммирования ресурсов. Однако стоит учитывать, что этот метод чувствителен к нулевым значениям (логарифм нуля не определен).
Рассмотрим более современный и «нативный» для СКД способ — использование функции ВычислитьВыражение с контекстом Иерархия. Этот метод позволяет получить доступ к данным вышестоящих уровней прямо в процессе компоновки.
В поле ресурса можно прописать выражение следующего вида:
ВычислитьВыражение("Количество", "Иерархия", , "Первая", "Текущая")
Это выражение возвращает массив всех значений количества от самого верхнего узла (корня) до текущей строки. Чтобы перемножить эти значения, нам потребуется небольшая экспортная функция в общем модуле, которая примет массив и вернет результат произведения:
Функция ПеремножитьМассив(МассивЗначений) Экспорт
Результат = 1;
Для Каждого Значение Из МассивЗначений Цикл
Результат = Результат * Значение;
КонецЦикла;
Возврат Результат;
КонецФункции
В СКД итоговое выражение будет выглядеть так: ОбщийМодуль.ПеремножитьМассив(ВычислитьВыражение("Количество", "Иерархия", , "Первая", "Текущая")).
Проанализируем ситуацию для самых свежих версий платформы 1С. С появлением Обобщенных Табличных Выражений (CTE) задача разузлования решается одним-единственным запросом на стороне сервера СУБД. Мы можем описать рекурсивную часть запроса, где в каждой итерации количество текущего узла умножается на накопленный множитель родителя.
Пример структуры такого запроса (псевдокод):
ВЫБРАТЬ ... ИЗ Справочник.Спецификации КАК Начало
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ ... Начало.Количество * Присоединенный.Количество ...
ИЗ Начало СОЕДИНЕНИЕ Справочник.Спецификации КАК Присоединенный
Это самый производительный метод для работы с очень большими деревьями данных, так как расчет происходит непосредственно в базе данных без передачи промежуточных таблиц на сервер приложений.
При реализации любого из перечисленных методов необходимо учитывать следующие критически важные моменты:
ДеревоРесурсныхСпецификаций. Иногда проще вызвать штатную функцию разузлования, которая уже умеет учитывать кратность, доли стоимости и минимальные партии.Таким образом, мы рассмотрели различные подходы к решению задачи умножения количеств в иерархии СКД. Выбор конкретного метода зависит от версии вашей платформы, сложности структуры данных и требований к производительности отчета.