При разработке отчетов в Системе компоновки данных (СКД) разработчики часто сталкиваются с необходимостью построения произвольной (пользовательской) иерархии. Классический пример из практики — отчет по затратам на производство в конфигурации «1С:Управление производственным предприятием» (УПП), где требуется вывести структуру подчиненности артикулов и полуфабрикатов. Однако стандартные механизмы СКД рассчитаны на строгое дерево, где у каждого подчиненного элемента может быть только один родитель. Если один и тот же артикул затраты входит в разные спецификации или заказы (то есть принадлежит к разным родительским узлам), возникает сетевая структура (граф). В этом случае СКД начинает некорректно суммировать ресурсы, задваивая или даже затраивая итоги на верхних уровнях группировок.
В данной статье мы подробно проанализируем, почему возникает эта проблема, и разберем практические способы ее решения, включая уникализацию группировок и предварительную обработку исходной таблицы значений перед передачей в компоновку.
Рассмотрим ситуацию, которую описывает автор темы. В исходных данных у нас есть плоская таблица, полученная из регистров затрат:
Пример исходных данных:
1А, Подчиненный артикул — 2Б, Сумма — 111А, Подчиненный артикул — 2Б, Сумма — 124Д, Подчиненный артикул — 2Б, Сумма — 20Обратите внимание: артикул 2Б встречается на разных уровнях и у разных родителей. Он входит напрямую в узел 1А и одновременно в узел 4Д (который сам является подчиненным по отношению к 1А).
При построении обычной иерархии по артикулам СКД видит элемент 2Б как единую сущность. Вместо того чтобы локально рассчитать сумму для каждой ветки отдельно, система складывает все показатели. В результате по ветке 1А мы получаем сумму 11 + 12 + 20 = 43, и по ветке 4Д мы также получаем 20 + 11 + 12 = 43. Итоги верхнего уровня полностью искажаются, так как СКД не может сопоставить конкретное вхождение элемента с нужной веткой иерархии.
Давайте разберем три основных способа решения этой проблемы.
Это наиболее надежный и изящный способ решить проблему в рамках СКД без изменения структуры метаданных. Его суть заключается в том, чтобы сделать записи уникальными для алгоритмов расчета СКД, но сохранить красивый и понятный вид для конечного пользователя.
Выполним этот процесс по шагам:
Шаг 1. Модификация исходного запроса или таблицы значений.
При формировании данных нам необходимо добавить уникальный ключ для каждой строки. В качестве такого ключа может выступать полный путь (например, «1А/2Б» и «4Д/2Б»), комбинация артикула и номера строки, либо уникальный идентификатор строки. Добавим в наш набор данных два новых поля: УникальныйКлюч и УникальныйКлючРодителя.
Пример кода формирования таблицы значений для СКД:
// Создаем структуру ТЗ с уникальными ключами
ТаблицаДанных = Новый ТаблицаЗначений;
ТаблицаДанных.Колонки.Добавить("Артикул", Новый ОписаниеТипов("Строка"));
ТаблицаДанных.Колонки.Добавить("АртикулРодитель", Новый ОписаниеТипов("Строка"));
ТаблицаДанных.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число"));
ТаблицаДанных.Колонки.Добавить("УникальныйКлюч", Новый ОписаниеТипов("Строка"));
ТаблицаДанных.Колонки.Добавить("УникальныйКлючРодителя", Новый ОписаниеТипов("Строка"));
// Заполняем ТЗ уникальными ключами на основании пути или позиций
Для Каждого СтрокаДанных Из ИсходныйРегистр Цикл
НоваяСтрока = ТаблицаДанных.Добавить();
НоваяСтрока.Артикул = СтрокаДанных.Артикул;
НоваяСтрока.АртикулРодитель = СтрокаДанных.АртикулРодитель;
НоваяСтрока.Сумма = СтрокаДанных.Сумма;
// Создаем уникальный ключ, связывая текущий элемент с его родителем и уровнем
НоваяСтрока.УникальныйКлюч = СтрокаДанных.АртикулРодитель + "/" + СтрокаДанных.Артикул;
НоваяСтрока.УникальныйКлючРодителя = НайтиУникальныйКлючРодителя(СтрокаДанных.АртикулРодитель);
КонецЦикла;
Шаг 2. Настройка связей наборов данных в СКД.
В схеме компоновки данных на вкладке Связи наборов данных мы настраиваем связь не по исходным полям Артикул -> АртикулРодитель, а по нашим новым полям: УникальныйКлюч -> УникальныйКлючРодителя. Благодаря этому СКД начнет воспринимать одну и ту же затрату на разных ветках как абсолютно разные элементы дерева.
Шаг 3. Оформление представления для пользователя.
Если вывести отчет в таком виде, пользователи увидят технические ключи вида «1А/2Б», что неприемлемо. Чтобы вернуть отчету исходный понятный вид, выполним следующее:
УникальныйКлюч перейдем в свойства поля.Артикул.В результате СКД произведет математический расчет иерархии и суммирование итогов по уникальным веткам (что гарантирует математическую точность), но на уровне визуализации сгруппирует данные и покажет пользователю привычные наименования артикулов.
Если структура отчета слишком сложная или связи наборов данных в СКД работают медленно из-за большого объема информации, правильным архитектурным решением будет отказаться от расчета итогов силами компоновки. Мы можем полностью рассчитать суммы для каждого узла иерархии в коде 1С перед передачей таблицы в СКД.
Проанализируем алгоритм этого решения:
Пример программного расчета и подготовки таблицы значений:
// Пример рекурсивного расчета сумм по веткам
Процедура РассчитатьСуммыУзлов(ДеревоДанных)
Для Каждого СтрокаУровня Из ДеревоДанных.Строки Цикл
// Если у узла есть подчиненные элементы, сначала рассчитываем их
Если СтрокаУровня.Строки.Количество() > 0 Тогда
РассчитатьСуммыУзлов(СтрокаУровня);
// Суммируем только непосредственных потомков для текущей ветки
НоваяСумма = 0;
Для Каждого ПодчиненнаяСтрока Из СтрокаУровня.Строки Цикл
НоваяСумма = НоваяСумма + ПодчиненнаяСтрока.Сумма;
КонецЦикла;
СтрокаУровня.Сумма = НоваяСумма;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
После того как мы передали предрасчитанную таблицу в СКД, нам необходимо правильно настроить вкладку Ресурсы. Вместо стандартного выражения Сумма(Сумма) для иерархических группировок мы указываем функцию Максимум(Сумма) или Среднее(Сумма). Поскольку все суммы на каждом уровне уже посчитаны нами вручную и зафиксированы в строках ТЗ, СКД не будет складывать их повторно, а просто отобразит корректное готовое значение для каждого узла сети.
Если мы не хотим менять структуру данных и переписывать код подготовки ТЗ, мы можем попробовать решить проблему на уровне формул ресурсов СКД.
Для того чтобы СКД не собирала данные со всей таблицы при расчете итогов по конкретному артикулу, мы можем использовать агрегатные функции с ограничением по группировкам. Нам поможет функция ВычислитьВыражениеСГруппировкойМассив.
В выражении ресурса для проблемного поля мы можем прописать формулу:
Сумма(ВычислитьВыражениеСГруппировкойМассив("Сумма", "Артикул"))
Или, если требуется свернуть дублирующиеся значения внутри конкретной ветки:
Сумма(Свернуть(ВычислитьВыражениеСГруппировкойМассив("Сумма", "Артикул")))
Важное примечание: Этот способ эффективен только в том случае, если в иерархии нет циклов (зацикливаний, когда элемент является родителем самого себя через несколько уровней), а задвоение сумм происходит исключительно из-за одинаковых наименований артикулов на разных уровнях подчинения. В сложных сетевых структурах данный метод может давать погрешности, поэтому приоритетными остаются первый и второй способы.
Мы проанализировали проблему неверного суммирования в СКД при построении произвольной иерархии для сетевых структур. Для получения гарантированно точного результата рекомендуется использовать Способ 1 (создание уникальных ключей с подменой представления), так как он полностью сохраняет интерактивность отчета и позволяет использовать стандартные возможности СКД по расчету итогов. Если же отчет строится по сложным правилам распределения затрат, оптимальным выбором будет Способ 2 (предварительный расчет сумм в ТЗ), который переносит тяжелые математические вычисления на сторону сервера 1С и гарантирует предсказуемость результата.