Дерево значений в управляемой форме. Заполнение и хранение

Задача

Хранить в документе (или справочнике, не суть важно) дерево значений. Дерево должно сохраняться в объекте, как и любой другой реквизит или табличная часть.

У нас дерево будет вот такое:

Пример заполнения:

Одним из способов решить поставленную задачу является «хранилище значений». Я же решил хранить дерево в табличной части. У этого пути есть преимущества:

  • Можно выводить на форму автоматические итоги;
  • Итоги по группировкам можно считать в запросе;
  • В дальнейшем в динамический список документов можно тащить данные из табличной части. Например, тот же итог по одной из колонок.

Заполнение дерева из табличной части

В документ добавлена табличная часть «ПланФакт» с колонками, соответствующими колонкам дерева.

Так как для построения дерева мы будем использовать запрос, то нам понадобится получать типизированную таблицу значений с данными из табличной части. Для этого используем функцию ПолучитьТипизированнуюТаблицу().

&НаСервере
Функция ПолучитьТипизированнуюТаблицу(ЗаполнитьДанными = Ложь)
	
	ПромежуточнаяТаблица = Новый ТаблицаЗначений;
	ПромежуточнаяТаблица.Колонки.Добавить("Клиент", Новый ОписаниеТипов("СправочникСсылка.Партнеры"));
	ПромежуточнаяТаблица.Колонки.Добавить("Марка", Новый ОписаниеТипов("СправочникСсылка.Марки"));
	ПромежуточнаяТаблица.Колонки.Добавить("Сегмент", Новый ОписаниеТипов("СправочникСсылка.СегментыНоменклатуры"));
	ПромежуточнаяТаблица.Колонки.Добавить("ПродажиАППГ", Новый ОписаниеТипов("Число",,,Новый КвалификаторыЧисла(15,2)));
	ПромежуточнаяТаблица.Колонки.Добавить("ПродажиПП", Новый ОписаниеТипов("Число",,,Новый КвалификаторыЧисла(15,2)));
	ПромежуточнаяТаблица.Колонки.Добавить("План", Новый ОписаниеТипов("Число",,,Новый КвалификаторыЧисла(15,2)));
	ПромежуточнаяТаблица.Колонки.Добавить("Факт", Новый ОписаниеТипов("Число",,,Новый КвалификаторыЧисла(15,2)));
	
	Если ЗаполнитьДанными Тогда
		Для Каждого ТекСтр Из Объект.ПланФакт Цикл
			НовСтр = ПромежуточнаяТаблица.Добавить();
			ЗаполнитьЗначенияСвойств(НовСтр, ТекСтр);
		КонецЦикла;
	КонецЕсли;
	
	Возврат ПромежуточнаяТаблица;
	
КонецФункции

Как видно, эта функция создает таблицу значений нужной структуры и заполняет ее данными табличной части документа. Теперь давайте посмотрим, что с этим добром делать.

В форме при открытии вызывается процедура ИзвлечьДерево().

&НаКлиенте
Процедура ИзвлечьДерево(Разворачивать = Истина)
	
	ИзвлечьДеревоНаСервере();
	
	Если Разворачивать Тогда
		РазвернутьДерево();
	КонецЕсли;
	
КонецПроцедуры

&НаСервере
Процедура ИзвлечьДеревоНаСервере()
	
	Запрос = Новый Запрос;
	Запрос.УстановитьПараметр("ПромежуточнаяТаблица", ПолучитьТипизированнуюТаблицу(Истина));
	Запрос.УстановитьПараметр("СкрытьПустые", Объект.СкрытьПустыеСтроки);
	Запрос.Текст =
		"ВЫБРАТЬ
		|	ПромежуточнаяТаблица.Клиент КАК Клиент,
		|	ПромежуточнаяТаблица.Марка КАК Марка,
		|	ПромежуточнаяТаблица.Сегмент КАК Сегмент,
		|	ПромежуточнаяТаблица.ПродажиАППГ КАК ПродажиАППГ,
		|	ПромежуточнаяТаблица.ПродажиПП КАК ПродажиПП,
		|	ПромежуточнаяТаблица.План КАК План,
		|	ПромежуточнаяТаблица.Факт КАК Факт
		|ПОМЕСТИТЬ Данные
		|ИЗ
		|	&ПромежуточнаяТаблица КАК ПромежуточнаяТаблица
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|ВЫБРАТЬ
		|	Данные.Клиент КАК Клиент,
		|	Данные.Марка КАК Марка,
		|	Данные.Сегмент КАК Сегмент,
		|	Данные.ПродажиАППГ КАК ПродажиАППГ,
		|	Данные.ПродажиПП КАК ПродажиПП,
		|	Данные.План КАК План,
		|	Данные.Факт КАК Факт
		|ИЗ
		|	Данные КАК Данные
		|ГДЕ
		|	(&СкрытьПустые = ЛОЖЬ
		|			ИЛИ Данные.ПродажиАППГ <> 0
		|			ИЛИ Данные.ПродажиПП <> 0
		|			ИЛИ Данные.План <> 0
		|			ИЛИ Данные.Факт <> 0)
		|ИТОГИ
		|	СУММА(ПродажиАППГ),
		|	СУММА(ПродажиПП),
		|	СУММА(План),
		|	СУММА(Факт)
		|ПО
		|	Клиент,
		|	Марка";
	
	Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
	ЗначениеВРеквизитФормы(Дерево, "ДеревоПланФакт");
	
КонецПроцедуры

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

Как это выглядит на форме:

Сохранение дерева

Понятно, что мало выводить дерево, надо еще и записывать его — после изменения пользователем. В нашем конкретном случае нужно еще и пересчитывать итоги по группировкам.

Можно было бы написать процедуру, которая выполнит обратное преобразование дерева в таблицу. Но в нашем случае это было бы долго: для пересчета итогов каждый раз пришлось бы перезаполнять всю таблицу. Я решил пойти другим путем и при изменении строки дерева пересчитывать только нужные итоги и обновлять только нужные строки в табличной части.

При этом я учел 2 особенности моей задачи:

  • Пользователь может менять только колонку «План»;
  • Поддерживается только изменение строк нижнего уровня. Изменение итогов по группировкам следует запретить.

Вот как выглядит результат:

&НаКлиенте
Процедура ДеревоПланФактПланПриИзменении(Элемент)
	
	Модифицированность = Истина;
	СтрокаДерева = Элементы.ДеревоПланФакт.ТекущиеДанные;
	СтруктураОтбора = Новый Структура;
	СтруктураОтбора.Вставить("Клиент", СтрокаДерева.Клиент);
	СтруктураОтбора.Вставить("Марка", СтрокаДерева.Марка);
	СтруктураОтбора.Вставить("Сегмент", СтрокаДерева.Сегмент);
	СтрокиТаблицы = Объект.ПланФакт.НайтиСтроки(СтруктураОтбора);
	Для Каждого ТекСтр Из СтрокиТаблицы Цикл
		ТекСтр.План = СтрокаДерева.План;
	КонецЦикла;	
	
	// Пересчитаем суммы в группировках
	СтрокаДереваМарка = СтрокаДерева.ПолучитьРодителя();
	СуммаПоМарке = 0;
	Для Каждого ТекСтр Из СтрокаДереваМарка.ПолучитьЭлементы() Цикл
		СуммаПоМарке = СуммаПоМарке + ТекСтр.План;
	КонецЦикла;
	СтрокаДереваМарка.План = СуммаПоМарке;
	
	СтрокаДереваКлиент = СтрокаДереваМарка.ПолучитьРодителя();
	СуммаПоКлиенту = 0;
	Для Каждого ТекСтр Из СтрокаДереваКлиент.ПолучитьЭлементы() Цикл
		СуммаПоКлиенту = СуммаПоКлиенту + ТекСтр.План;
	КонецЦикла;
	СтрокаДереваКлиент.План = СуммаПоКлиенту;
	
КонецПроцедуры