Свёртка дерева значений в 1С: от таблицы до узлов формы
Содержание
Разработчик получает дерево значений из запроса с итогами и хочет «свернуть» его: объединить одинаковые строки и получить суммы по номенклатуре или по периодам. Открывает синтакс-помощник, ищет ДеревоЗначений.Свернуть() и не находит. У дерева значений такого метода нет, он есть только у таблицы значений.
Слово «свернуть» в 1С означает две разные вещи. Первая: сгруппировать данные и просуммировать числа, то есть свёртка в смысле ТаблицаЗначений.Свернуть(). Вторая: визуально схлопнуть узлы дерева на форме, чтобы остались только корневые строки. Ниже разобраны оба случая для платформы 8.3 с обычными и управляемыми формами.
Что у дерева значений есть, а чего нет
Дерево значений хранит строки с подчинением: у каждой строки есть коллекция Строки, а в ней вложенные строки любой глубины. Колонки общие для всех уровней.
Методы самого дерева и его строк: Строки.Добавить(), Строки.Найти(), Строки.Сортировать(), Строки.Удалить(), Родитель, Колонки. Группирующего метода нет, потому что непонятно, что считать одинаковыми строками: узлы разных уровней, листья или всё сразу.
Поэтому «свернуть дерево» решается одним из четырёх приёмов:
- 🧮 перенести листья в таблицу значений и вызвать
Свернуть(); - 🔁 пройти дерево рекурсивно и посчитать итоги по узлам вручную;
- 🗄 не строить лишнее дерево, а сгруппировать данные на уровне запроса;
- 👁 схлопнуть узлы на форме, если нужна только видимость.
Выбор зависит от задачи. Если нужен плоский результат без иерархии, берите первый способ. Если иерархия должна остаться, а в узлах нужны суммы, берите второй или третий.
| Задача | Способ | Иерархия после |
|---|---|---|
| Суммы по ключевым колонкам без уровней | Таблица значений + Свернуть() | Теряется |
| Итоги узлов по листьям | Рекурсия по Строки | Сохраняется |
| Группировка по регистратору, периоду, измерениям | Запрос с СГРУППИРОВАТЬ ПО или ИТОГИ | Задаётся запросом |
| Убрать раскрытые ветки с экрана | Свернуть() у элемента формы | Сохраняется |
Сначала определите, что именно вы сворачиваете
Прежде чем писать код, ответьте на три вопроса. От ответов зависит, какой из способов ниже не приведёт к потере данных.
- Откуда дерево. Если оно пришло из запроса с
ИТОГИ, группировка уже сделана, и лишняя свёртка только исказит суммы. Если собрано вручную из справочника с иерархией или из обработки файла, группировки нет. - Где лежат числа. Только в листьях или ещё и в узлах. В первом случае итоги узлов нужно считать, во втором их легко задвоить.
- Что нужно на выходе. Плоский список, дерево с суммами или просто аккуратный вид формы.
Короткая проверка на реальных данных: выгрузите дерево в таблицу и сравните сумму по колонке до и после свёртки. Общий итог по листьям обязан совпасть, иначе где-то потерялась или задвоилась строка.
Свёртка через таблицу значений
Это самый быстрый способ, если иерархия не нужна. Алгоритм: создать таблицу с теми же колонками, обойти дерево, скопировать листья, свернуть.
Сначала сам обход. Рекурсивная процедура берёт коллекцию строк дерева и складывает каждую строку в таблицу:
Процедура ВыгрузитьДеревоВТаблицу(СтрокиДерева, Таблица)
Для Каждого СтрокаДерева Из СтрокиДерева Цикл
Если СтрокаДерева.Строки.Количество() = 0 Тогда
НоваяСтрока = Таблица.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДерева);
Иначе
ВыгрузитьДеревоВТаблицу(СтрокаДерева.Строки, Таблица);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Условие Строки.Количество() = 0 отбирает только листья. Если в узлах тоже лежат собственные числа, уберите условие и копируйте все строки, но тогда проверьте, что итоги узлов не посчитаны заранее: иначе одно число войдёт дважды.
Теперь вызов:
Таблица = Новый ТаблицаЗначений;
Таблица.Колонки.Добавить("Номенклатура");
Таблица.Колонки.Добавить("Склад");
Таблица.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
ВыгрузитьДеревоВТаблицу(Дерево.Строки, Таблица);
Таблица.Свернуть("Номенклатура, Склад", "Количество");
Первый параметр метода Свернуть — колонки группировки, второй — колонки суммирования, оба строкой через запятую. Пример: в таблице три строки «Гвозди / Основной / 10», «Гвозди / Основной / 5», «Болты / Основной / 7». После свёртки останется две: «Гвозди / Основной / 15» и «Болты / Основной / 7».
Колонки, которые не названы ни в группировке, ни в суммировании, из таблицы пропадают. Нужна ещё одна колонка с текстом, добавьте её в первый параметр или заранее скопируйте таблицу через Таблица.Скопировать().
💡 Колонки суммирования должны быть числовыми. Если в колонке лежат пустые значения, а не нули, задайте тип колонки при создании: тогда пустое значение будет приведено к 0.
Свёртка по периодам, регистраторам и измерениям
Когда деревом выгружали данные регистра, строки обычно различаются именно этими полями. Сворачивать нужно по тому набору колонок, который определяет «одну» запись по смыслу задачи.
Три типичных набора колонок группировки:
- По измерениям — например,
Номенклатура, Склад. Движения разных дней и документов складываются в один остаток. - По периоду — к измерениям добавляется колонка
Период. Если период хранится с точностью до секунды, а нужен месяц, сначала приведите его:НачалоМесяца(Период)в отдельной колонке. - По регистратору — колонка
Регистратордобавляется в группировку, когда нужны суммы по каждому документу отдельно.
Хитрость с периодом: свёртка сравнивает значения точно. Две даты, отличающиеся на секунду, остаются разными строками. Поэтому сначала создайте дополнительную колонку с началом месяца и заполните её циклом по таблице, а затем сворачивайте по ней, а не по исходной дате.
Таблица.Колонки.Добавить("Месяц");
Для Каждого СтрокаТаблицы Из Таблица Цикл
СтрокаТаблицы.Месяц = НачалоМесяца(СтрокаТаблицы.Период);
КонецЦикла;
Таблица.Свернуть("Номенклатура, Месяц", "Количество");
Если нужно сохранить и сумму, и количество строк, добавьте перед свёрткой числовую колонку Счётчик, заполните её единицами и включите в суммирование.
Когда иерархия нужна: итоги по узлам вручную
Часто нужно дерево, где у каждого узла стоит сумма его потомков. Таблица значений тут не поможет, потому что она плоская. Остаётся рекурсия: сначала считаем подчинённые строки, потом складываем их в родителя.
Процедура ПосчитатьИтогиУзлов(СтрокаДерева, ИмяКолонки)
Для Каждого Подчинённая Из СтрокаДерева.Строки Цикл
ПосчитатьИтогиУзлов(Подчинённая, ИмяКолонки);
СтрокаДерева[ИмяКолонки] = СтрокаДерева[ИмяКолонки] + Подчинённая[ИмяКолонки];
КонецЦикла;
КонецПроцедуры
Для Каждого Корень Из Дерево.Строки Цикл
ПосчитатьИтогиУзлов(Корень, "Сумма");
КонецЦикла;
Порядок важен: вызов для потомка стоит до сложения. Тогда к моменту суммирования у подчинённой строки уже готов свой итог, и числа поднимаются снизу вверх на любую глубину.
Условие работы: у узла в колонке Сумма до начала лежит 0 или его собственное значение, а не пустое значение без типа. Иначе сложение завершится ошибкой типов. Проверьте это, когда создаёте колонки с описанием типов.
Пример с числами. Корень «Склады» без собственной суммы, под ним «Основной» с листьями 100 и 50 и «Резервный» с листом 30. После обхода у «Основной» будет 150, у «Резервный» 30, у «Склады» 180.
Убрать лишние узлы после подсчёта
Иногда после подсчёта нужно оставить только узлы верхнего уровня, удалив детализацию. Удаляйте строки с конца, иначе индексы собьются:
Всего = Корень.Строки.Количество();
Для Сч = 1 По Всего Цикл
Корень.Строки.Удалить(Всего - Сч);
КонецЦикла;
Количество строк запоминается до цикла, потому что внутри оно уменьшается. Удаляется строка по индексу: на первом шаге последняя, на втором предпоследняя и так до первой (индексы в коллекции строк начинаются с нуля). Если нужно сохранить часть строк, добавьте проверку условия внутри цикла перед удалением.
Свёртка на стороне запроса
Самый дешёвый по ресурсам способ: не строить громоздкое дерево, а получить сгруппированные данные сразу из запроса. База сделает группировку быстрее, чем цикл на сервере 1С.
Для плоской свёртки — СГРУППИРОВАТЬ ПО с агрегатными функциями:
ВЫБРАТЬ
Движения.Номенклатура КАК Номенклатура,
Движения.Склад КАК Склад,
СУММА(Движения.Количество) КАК Количество
ИЗ
РегистрНакопления.ТоварыНаСкладах КАК Движения
СГРУППИРОВАТЬ ПО
Движения.Номенклатура,
Движения.Склад
Для иерархии с итогами — секция ИТОГИ ... ПО. Результат выгружается сразу в дерево значений:
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Итоги по группировкам уже посчитаны, вручную складывать ничего не нужно. Группировка по периоду задаётся в запросе функцией НАЧАЛОПЕРИОДА(Период, МЕСЯЦ), группировка по регистратору просто добавляется в список полей.
💡 Если данные нужны отчёту, проверьте, нельзя ли обойтись системой компоновки данных: группировки в схеме строят иерархию без ручного кода.
Свернуть узлы дерева на форме
Это другая задача: данные остаются как есть, меняется только вид. На управляемой форме у таблицы, показывающей дерево, есть методы Развернуть и Свернуть. Они принимают идентификатор строки.
Свернуть все корневые узлы (переменная Корень здесь — строка верхнего уровня):
Для Каждого Корень Из Дерево.ПолучитьЭлементы() Цикл
Элементы.Дерево.Свернуть(Корень.ПолучитьИдентификатор());
КонецЦикла;
Здесь Дерево — реквизит формы типа «Дерево значений», а Элементы.Дерево — таблица формы, которая его показывает. Имена у вас свои.
Развернуть всё, вместе с потомками:
Для Каждого Корень Из Дерево.ПолучитьЭлементы() Цикл
Элементы.Дерево.Развернуть(Корень.ПолучитьИдентификатор(), Истина);
КонецЦикла;
Второй параметр Истина раскрывает и все вложенные уровни. Обычно эти команды вешают на кнопки в командной панели формы или вызывают в ПриОткрытии, чтобы дерево сразу показывалось компактным.
После перезаполнения реквизита дерева (например, в ОбновитьДанные) состояние узлов сбрасывается. Вызывайте свёртку или развёртку после загрузки данных, а не до неё.
Если дерево показывается пользователю в обычной форме, у табличного поля есть похожие методы работы с узлами. Но на платформе 8.3 новые формы пишутся управляемыми, поэтому ориентируйтесь на пример выше.
Скорость работы и типичные ошибки
Свёртка на больших объёмах упирается в два места: обход дерева и вызов Свернуть.
Что ускоряет:
- 📉 выбирайте в запросе только нужные поля, лишние колонки замедляют и обход, и свёртку;
- 📉 группируйте в запросе, если это возможно: сервер БД справится быстрее цикла;
- 📉 не вызывайте
Найти()по строкам внутри цикла по большому дереву, заранее подготовьте соответствие; - 📉 не копируйте таблицу целиком при каждом шаге, свёртка меняет таблицу на месте.
Ошибки, которые встречаются чаще всего:
| Симптом | Причина | Что сделать |
|---|---|---|
После Свернуть пропали колонки | Они не вошли ни в один параметр | Включите в группировку или скопируйте таблицу заранее |
| Суммы вдвое больше ожидаемых | Копировали и узлы, и листья с готовыми итогами | Берите только листья |
| Сумма узла не сходится | Рекурсивный вызов стоит после сложения | Сначала рекурсия, затем сложение |
| Ошибка «операции сложения не применимы» | В колонке пустое значение без типа | Задайте тип колонки при создании |
| Свёртка по дате не объединяет строки | Время у дат разное | Приведите дату к началу месяца или дня |
| После обновления формы дерево раскрыто | Состояние узлов сбросилось | Вызывайте Свернуть после заполнения |
Частые вопросы
Есть ли в 1С метод Свернуть у дерева значений?
Свернуть есть у таблицы значений. Для дерева используйте перенос листьев в таблицу, рекурсивный обход или группировку в запросе.Чем свёртка таблицы значений отличается от свёртки узлов формы?
Можно ли свернуть дерево и сохранить иерархию?
ИТОГИ или рекурсивным подсчётом итогов в узлах.