Свёртка дерева значений в 1С: от таблицы до узлов формы

· 8 мин

Содержание

Разработчик получает дерево значений из запроса с итогами и хочет «свернуть» его: объединить одинаковые строки и получить суммы по номенклатуре или по периодам. Открывает синтакс-помощник, ищет ДеревоЗначений.Свернуть() и не находит. У дерева значений такого метода нет, он есть только у таблицы значений.

Слово «свернуть» в 1С означает две разные вещи. Первая: сгруппировать данные и просуммировать числа, то есть свёртка в смысле ТаблицаЗначений.Свернуть(). Вторая: визуально схлопнуть узлы дерева на форме, чтобы остались только корневые строки. Ниже разобраны оба случая для платформы 8.3 с обычными и управляемыми формами.

Что у дерева значений есть, а чего нет

Дерево значений хранит строки с подчинением: у каждой строки есть коллекция Строки, а в ней вложенные строки любой глубины. Колонки общие для всех уровней.

Методы самого дерева и его строк: Строки.Добавить(), Строки.Найти(), Строки.Сортировать(), Строки.Удалить(), Родитель, Колонки. Группирующего метода нет, потому что непонятно, что считать одинаковыми строками: узлы разных уровней, листья или всё сразу.

Поэтому «свернуть дерево» решается одним из четырёх приёмов:

  • 🧮 перенести листья в таблицу значений и вызвать Свернуть();
  • 🔁 пройти дерево рекурсивно и посчитать итоги по узлам вручную;
  • 🗄 не строить лишнее дерево, а сгруппировать данные на уровне запроса;
  • 👁 схлопнуть узлы на форме, если нужна только видимость.

Выбор зависит от задачи. Если нужен плоский результат без иерархии, берите первый способ. Если иерархия должна остаться, а в узлах нужны суммы, берите второй или третий.

ЗадачаСпособИерархия после
Суммы по ключевым колонкам без уровнейТаблица значений + Свернуть()Теряется
Итоги узлов по листьямРекурсия по СтрокиСохраняется
Группировка по регистратору, периоду, измерениямЗапрос с СГРУППИРОВАТЬ ПО или ИТОГИЗадаётся запросом
Убрать раскрытые ветки с экранаСвернуть() у элемента формыСохраняется

Сначала определите, что именно вы сворачиваете

Прежде чем писать код, ответьте на три вопроса. От ответов зависит, какой из способов ниже не приведёт к потере данных.

  1. Откуда дерево. Если оно пришло из запроса с ИТОГИ, группировка уже сделана, и лишняя свёртка только исказит суммы. Если собрано вручную из справочника с иерархией или из обработки файла, группировки нет.
  2. Где лежат числа. Только в листьях или ещё и в узлах. В первом случае итоги узлов нужно считать, во втором их легко задвоить.
  3. Что нужно на выходе. Плоский список, дерево с суммами или просто аккуратный вид формы.

Короткая проверка на реальных данных: выгрузите дерево в таблицу и сравните сумму по колонке до и после свёртки. Общий итог по листьям обязан совпасть, иначе где-то потерялась или задвоилась строка.

Что вам нужно получить?

Свёртка через таблицу значений

Это самый быстрый способ, если иерархия не нужна. Алгоритм: создать таблицу с теми же колонками, обойти дерево, скопировать листья, свернуть.

Сначала сам обход. Рекурсивная процедура берёт коллекцию строк дерева и складывает каждую строку в таблицу:

Процедура ВыгрузитьДеревоВТаблицу(СтрокиДерева, Таблица)
	Для Каждого СтрокаДерева Из СтрокиДерева Цикл
		Если СтрокаДерева.Строки.Количество() = 0 Тогда
			НоваяСтрока = Таблица.Добавить();
			ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДерева);
		Иначе
			ВыгрузитьДеревоВТаблицу(СтрокаДерева.Строки, Таблица);
		КонецЕсли;
	КонецЦикла;
КонецПроцедуры

Условие Строки.Количество() = 0 отбирает только листья. Если в узлах тоже лежат собственные числа, уберите условие и копируйте все строки, но тогда проверьте, что итоги узлов не посчитаны заранее: иначе одно число войдёт дважды.

Теперь вызов:

Таблица = Новый ТаблицаЗначений;
Таблица.Колонки.Добавить("Номенклатура");
Таблица.Колонки.Добавить("Склад");
Таблица.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));

ВыгрузитьДеревоВТаблицу(Дерево.Строки, Таблица);

Таблица.Свернуть("Номенклатура, Склад", "Количество");

Первый параметр метода Свернуть — колонки группировки, второй — колонки суммирования, оба строкой через запятую. Пример: в таблице три строки «Гвозди / Основной / 10», «Гвозди / Основной / 5», «Болты / Основной / 7». После свёртки останется две: «Гвозди / Основной / 15» и «Болты / Основной / 7».

Колонки, которые не названы ни в группировке, ни в суммировании, из таблицы пропадают. Нужна ещё одна колонка с текстом, добавьте её в первый параметр или заранее скопируйте таблицу через Таблица.Скопировать().

💡 Колонки суммирования должны быть числовыми. Если в колонке лежат пустые значения, а не нули, задайте тип колонки при создании: тогда пустое значение будет приведено к 0.

Свёртка по периодам, регистраторам и измерениям

Когда деревом выгружали данные регистра, строки обычно различаются именно этими полями. Сворачивать нужно по тому набору колонок, который определяет «одну» запись по смыслу задачи.

Три типичных набора колонок группировки:

  1. По измерениям — например, Номенклатура, Склад. Движения разных дней и документов складываются в один остаток.
  2. По периоду — к измерениям добавляется колонка Период. Если период хранится с точностью до секунды, а нужен месяц, сначала приведите его: НачалоМесяца(Период) в отдельной колонке.
  3. По регистратору — колонка Регистратор добавляется в группировку, когда нужны суммы по каждому документу отдельно.

Хитрость с периодом: свёртка сравнивает значения точно. Две даты, отличающиеся на секунду, остаются разными строками. Поэтому сначала создайте дополнительную колонку с началом месяца и заполните её циклом по таблице, а затем сворачивайте по ней, а не по исходной дате.

Таблица.Колонки.Добавить("Месяц");
Для Каждого СтрокаТаблицы Из Таблица Цикл
	СтрокаТаблицы.Месяц = НачалоМесяца(СтрокаТаблицы.Период);
КонецЦикла;
Таблица.Свернуть("Номенклатура, Месяц", "Количество");

Если нужно сохранить и сумму, и количество строк, добавьте перед свёрткой числовую колонку Счётчик, заполните её единицами и включите в суммирование.

Когда иерархия нужна: итоги по узлам вручную

Часто нужно дерево, где у каждого узла стоит сумма его потомков. Таблица значений тут не поможет, потому что она плоская. Остаётся рекурсия: сначала считаем подчинённые строки, потом складываем их в родителя.

Процедура ПосчитатьИтогиУзлов(СтрокаДерева, ИмяКолонки)
	Для Каждого Подчинённая Из СтрокаДерева.Строки Цикл
		ПосчитатьИтогиУзлов(Подчинённая, ИмяКолонки);
		СтрокаДерева[ИмяКолонки] = СтрокаДерева[ИмяКолонки] + Подчинённая[ИмяКолонки];
	КонецЦикла;
КонецПроцедуры

Для Каждого Корень Из Дерево.Строки Цикл
	ПосчитатьИтогиУзлов(Корень, "Сумма");
КонецЦикла;

Порядок важен: вызов для потомка стоит до сложения. Тогда к моменту суммирования у подчинённой строки уже готов свой итог, и числа поднимаются снизу вверх на любую глубину.

Условие работы: у узла в колонке Сумма до начала лежит 0 или его собственное значение, а не пустое значение без типа. Иначе сложение завершится ошибкой типов. Проверьте это, когда создаёте колонки с описанием типов.

Пример с числами. Корень «Склады» без собственной суммы, под ним «Основной» с листьями 100 и 50 и «Резервный» с листом 30. После обхода у «Основной» будет 150, у «Резервный» 30, у «Склады» 180.

Убрать лишние узлы после подсчёта

Иногда после подсчёта нужно оставить только узлы верхнего уровня, удалив детализацию. Удаляйте строки с конца, иначе индексы собьются:

Всего = Корень.Строки.Количество();
Для Сч = 1 По Всего Цикл
	Корень.Строки.Удалить(Всего - Сч);
КонецЦикла;

Количество строк запоминается до цикла, потому что внутри оно уменьшается. Удаляется строка по индексу: на первом шаге последняя, на втором предпоследняя и так до первой (индексы в коллекции строк начинаются с нуля). Если нужно сохранить часть строк, добавьте проверку условия внутри цикла перед удалением.

Свёртка на стороне запроса

Самый дешёвый по ресурсам способ: не строить громоздкое дерево, а получить сгруппированные данные сразу из запроса. База сделает группировку быстрее, чем цикл на сервере 1С.

Для плоской свёртки — СГРУППИРОВАТЬ ПО с агрегатными функциями:

ВЫБРАТЬ
	Движения.Номенклатура КАК Номенклатура,
	Движения.Склад КАК Склад,
	СУММА(Движения.Количество) КАК Количество
ИЗ
	РегистрНакопления.ТоварыНаСкладах КАК Движения
СГРУППИРОВАТЬ ПО
	Движения.Номенклатура,
	Движения.Склад

Для иерархии с итогами — секция ИТОГИ ... ПО. Результат выгружается сразу в дерево значений:

Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

Итоги по группировкам уже посчитаны, вручную складывать ничего не нужно. Группировка по периоду задаётся в запросе функцией НАЧАЛОПЕРИОДА(Период, МЕСЯЦ), группировка по регистратору просто добавляется в список полей.

💡 Если данные нужны отчёту, проверьте, нельзя ли обойтись системой компоновки данных: группировки в схеме строят иерархию без ручного кода.

Свернуть узлы дерева на форме

Это другая задача: данные остаются как есть, меняется только вид. На управляемой форме у таблицы, показывающей дерево, есть методы Развернуть и Свернуть. Они принимают идентификатор строки.

Свернуть все корневые узлы (переменная Корень здесь — строка верхнего уровня):

Для Каждого Корень Из Дерево.ПолучитьЭлементы() Цикл
	Элементы.Дерево.Свернуть(Корень.ПолучитьИдентификатор());
КонецЦикла;

Здесь Дерево — реквизит формы типа «Дерево значений», а Элементы.Дерево — таблица формы, которая его показывает. Имена у вас свои.

Развернуть всё, вместе с потомками:

Для Каждого Корень Из Дерево.ПолучитьЭлементы() Цикл
	Элементы.Дерево.Развернуть(Корень.ПолучитьИдентификатор(), Истина);
КонецЦикла;

Второй параметр Истина раскрывает и все вложенные уровни. Обычно эти команды вешают на кнопки в командной панели формы или вызывают в ПриОткрытии, чтобы дерево сразу показывалось компактным.

После перезаполнения реквизита дерева (например, в ОбновитьДанные) состояние узлов сбрасывается. Вызывайте свёртку или развёртку после загрузки данных, а не до неё.

Если дерево показывается пользователю в обычной форме, у табличного поля есть похожие методы работы с узлами. Но на платформе 8.3 новые формы пишутся управляемыми, поэтому ориентируйтесь на пример выше.

Скорость работы и типичные ошибки

Свёртка на больших объёмах упирается в два места: обход дерева и вызов Свернуть.

Что ускоряет:

  • 📉 выбирайте в запросе только нужные поля, лишние колонки замедляют и обход, и свёртку;
  • 📉 группируйте в запросе, если это возможно: сервер БД справится быстрее цикла;
  • 📉 не вызывайте Найти() по строкам внутри цикла по большому дереву, заранее подготовьте соответствие;
  • 📉 не копируйте таблицу целиком при каждом шаге, свёртка меняет таблицу на месте.

Ошибки, которые встречаются чаще всего:

СимптомПричинаЧто сделать
После Свернуть пропали колонкиОни не вошли ни в один параметрВключите в группировку или скопируйте таблицу заранее
Суммы вдвое больше ожидаемыхКопировали и узлы, и листья с готовыми итогамиБерите только листья
Сумма узла не сходитсяРекурсивный вызов стоит после сложенияСначала рекурсия, затем сложение
Ошибка «операции сложения не применимы»В колонке пустое значение без типаЗадайте тип колонки при создании
Свёртка по дате не объединяет строкиВремя у дат разноеПриведите дату к началу месяца или дня
После обновления формы дерево раскрытоСостояние узлов сбросилосьВызывайте Свернуть после заполнения

Частые вопросы

Есть ли в 1С метод Свернуть у дерева значений?
Нет. Метод Свернуть есть у таблицы значений. Для дерева используйте перенос листьев в таблицу, рекурсивный обход или группировку в запросе.
Чем свёртка таблицы значений отличается от свёртки узлов формы?
Первая группирует данные и суммирует числа, строки исчезают. Вторая только прячет вложенные строки на экране, данные остаются прежними.
Можно ли свернуть дерево и сохранить иерархию?
Через таблицу нельзя, она плоская. Сохранить уровни можно запросом с секцией ИТОГИ или рекурсивным подсчётом итогов в узлах.
Как сложить числа в узлах дерева по листьям?
Обойдите дерево рекурсивно: сначала обработайте подчинённые строки, затем прибавьте их значения к родителю. Вложенность любой глубины это выдерживает.
Почему после свёртки по дате строки не объединились?
Метод сравнивает значения точно, и даты с разным временем различаются. Добавьте колонку с началом дня или месяца и сворачивайте по ней.