Как отобразить табличную часть документа в виде иерархического дерева с суммированием итогов в 1С 8.3

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

Сложность заключается в том, что физически табличная часть любого документа в базе данных 1С всегда является плоской таблицей. В этой статье мы подробно разберем, как обойти это ограничение в управляемых формах и реализовать полноценное интерактивное дерево с автоматическим суммированием показателей по иерархии.

Архитектурная концепция: Связка «ТЧ документа <-> Дерево на форме»

Поскольку напрямую отобразить плоскую табличную часть в виде дерева в интерфейсе управляемой формы невозможно, мы применим стандартный архитектурный шаблон-посредник. На форме документа мы создадим вспомогательный реквизит с типом ДеревоЗначений (на клиенте он представлен коллекцией ДанныеФормыДерево). Именно этот реквизит будет связан с элементом интерфейса типа «Таблица» с режимом отображения «Дерево».

Процесс работы с данными в этой схеме выглядит следующим образом:

  1. При чтении данных на сервере: мы извлекаем плоские данные из реальной табличной части документа, строим на их основе иерархическое дерево значений (с учетом родительских групп номенклатуры) и заполняем им реквизит формы.
  2. При интерактивной работе на клиенте: пользователь добавляет, удаляет или изменяет строки непосредственно в дереве на форме. При этом срабатывают обработчики, которые автоматически пересчитывают числовые итоги по родительским веткам дерева.
  3. Перед записью документа на сервере: мы выполняем обратное преобразование — «сворачиваем» наше иерархическое дерево обратно в плоскую структуру табличной части документа, игнорируя при записи промежуточные группировочные строки, так как они нужны только для визуализации.

Давайте пошагово разберем реализацию каждого этапа этой схемы.

---

Шаг 1. Создание реквизитов формы и настройка интерфейса

Для начала откроем управляемую форму нашего документа в конфигураторе и выполним предварительные настройки:

  1. В окне «Реквизиты» формы добавим новый реквизит. Назовем его, например, ДеревоТоваров и установим для него тип данных ДеревоЗначений.
  2. Внутри созданного реквизита ДеревоТоваров воссоздадим колонки, которые соответствуют нашей исходной табличной части (например: Номенклатура с типом справочника, Количество, Цена, Сумма с числовыми типами).
  3. Обязательно добавим служебную колонку ЭтоГруппа с булевым типом данных. Она понадобится нам для того, чтобы отличать строки-группы (папки номенклатуры) от строк-элементов (конкретных товаров) при условном оформлении и расчете итогов.
  4. Перетащим реквизит ДеревоТоваров в левое окно элементов формы. Автоматически создастся элемент интерфейса типа «Таблица».
  5. В свойствах созданной таблицы формы найдем свойство «Режим отображения» и установим для него значение «Дерево».

---

Шаг 2. Заполнение дерева данными ТЧ при открытии формы

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

Наиболее эффективным способом построения дерева с автоиерархией является использование специального механизма обхода результатов запроса. Напишем процедуру, которая будет вызываться в обработчике формы ПриЧтенииНаСервере или ПриСозданииНаСервере:


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

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

---

Шаг 3. Интерактивный пересчет сумм на клиенте

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

Для числовых полей ввода в таблице формы назначим обработчик события ПриИзменении. Рассмотрим пример реализации расчета для колонки количества:


&НаКлиенте
Процедура ДеревоТоваровКоличествоПриИзменении(Элемент)
	
	ТекущаяСтрока = Элементы.ДеревоТоваров.ТекущиеДанные;
	Если ТекущаяСтрока = Неопределено Тогда
		Возврат;
	КонецЕсли;
	
	// Пересчитываем итоги вверх по дереву начиная от родителя текущей измененной строки
	ПересчитатьИтогиВверхПоДереву(ТекущаяСтрока);
	
КонецПроцедуры

&НаКлиенте
Процедура ПересчитатьИтогиВверхПоДереву(ТекущаяСтрока)
	
	Родитель = ТекущаяСтрока.ПолучитьРодителя();
	
	// Цикл подъема от текущей строки к корневым элементам дерева
	Пока Родитель <> Неопределено Цикл
		
		ПодчиненныеЭлементы = Родитель.ПолучитьЭлементы();
		
		НовоеКоличество = 0;
		НоваяСумма = 0;
		
		// Суммируем данные по всем непосредственным потомкам текущего родителя
		Для Каждого ПодчиненнаяСтрока Из ПодчиненныеЭлементы Цикл
			НовоеКоличество = НовоеКоличество + ПодчиненнаяСтрока.Количество;
			НоваяСумма = НоваяСумма + ПодчиненнаяСтрока.Сумма;
		КонецЦикла;
		
		// Записываем новые итоги в родительскую строку
		Родитель.Количество = НовоеКоличество;
		Родитель.Сумма = НоваяСумма;
		
		// Переходим на уровень выше
		Родитель = Родитель.ПолучитьРодителя();
		
	КонецЦикла;
	
КонецПроцедуры

---

Шаг 4. Сворачивание дерева в ТЧ перед записью документа

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

Для этого в модуле формы переопределим стандартный обработчик ПередЗаписьюНаСервере:


&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
	
	// Получаем прикладной объект Дерева Значений из реквизита формы
	ДеревоОбъект = РеквизитФормыВЗначение("ДеревоТоваров");
	
	// Очищаем табличную часть документа перед перезаполнением
	ТекущийОбъект.Товары.Очистить();
	
	// Запускаем рекурсивный перенос конечных элементов (товаров) в ТЧ
	ВыгрузитьСтрокиДереваВТабличнуюЧасть(ДеревоОбъект.Строки, ТекущийОбъект.Товары);
	
КонецПроцедуры

&НаСервере
Процедура ВыгрузитьСтрокиДереваВТабличнуюЧасть(СтрокиДерева, ТабличнаяЧастьДокумента)
	
	Для Каждого СтрокаДерева Из СтрокиДерева Цикл
		
		// Если строка является конечным товаром (у нее нет подчиненных строк)
		Если СтрокаДерева.Строки.Количество() = 0 И СтрокаДерева.ЭтоГруппа = Ложь Тогда
			
			// Записываем данные в плоскую ТЧ документа
			НоваяСтрокаТЧ = ТабличнаяЧастьДокумента.Добавить();
			ЗаполнитьЗначенияСвойств(НоваяСтрокаТЧ, СтрокаДерева);
			
		Иначе
			
			// Если это группа — рекурсивно обходим ее ветки
			ВыгрузитьСтрокиДереваВТабличнуюЧасть(СтрокаДерева.Строки, ТабличнаяЧастьДокумента);
			
		КонецЕсли;
		
	КонецЦикла;
	
КонецПроцедуры

---

Шаг 5. Визуальное оформление и юзабилити интерфейса

Чтобы с табличной частью-деревом было удобно работать, настроим правила условного оформления формы и установим необходимые свойства:

---

Альтернативный вариант: Использование Поля табличного документа

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

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

Преимущества метода:

Недостатки метода:

Рекомендуется использовать этот метод только для сложных расчетных форм (например, комплексных строительных смет), где визуальная составляющая важнее динамического изменения состава строк.

---

Сравнительный анализ решений

Проанализируем все рассмотренные варианты решения задачи отображения иерархии табличной части:

Критерий сравнения Реквизит ДеревоЗначений (Шаблон 1) Поле табличного документа (Шаблон 2) Шрифтовое оформление плоской ТЧ (Шаблон 3)
Сложность реализации Средняя (требуется написать 3-4 процедуры для синхронизации и расчета). Высокая (требуется детальный контроль координат ячеек и перехват событий). Низкая (настраивается в несколько кликов через условное оформление).
Интерактивность и удобство ввода Высокая (стандартные механизмы платформы по вводу и изменению строк). Низкая (требует ручной обработки ввода данных пользователем). Высокая (работа происходит в стандартной таблице).
Точность и наглядность иерархии Идеальная (настоящие раскрывающиеся ветки дерева). Высокая (иерархия отображается с помощью группировок табличного документа). Низкая (иерархия имитируется отступами и шрифтами, ветки нельзя сворачивать).

Итог: Для подавляющего большинства прикладных бизнес-задач оптимальным выбором является использование реквизита формы с типом ДеревоЗначений с настроенным условным оформлением. Оно обеспечивает надежное хранение данных, естественное поведение интерфейса 1С и высокую скорость разработки.

← На главную