В практической разработке на платформе «1С:Предприятие 8.3» часто возникает задача представить табличную часть (ТЧ) документа не в виде плоского списка, а в виде иерархической структуры. Классический пример — спецификации номенклатуры, сметы или заказы покупателей, где товары сгруппированы по папкам (группам номенклатуры), и по каждой группе необходимо автоматически рассчитывать промежуточные суммы прямо «на лету».
Сложность заключается в том, что физически табличная часть любого документа в базе данных 1С всегда является плоской таблицей. В этой статье мы подробно разберем, как обойти это ограничение в управляемых формах и реализовать полноценное интерактивное дерево с автоматическим суммированием показателей по иерархии.
Поскольку напрямую отобразить плоскую табличную часть в виде дерева в интерфейсе управляемой формы невозможно, мы применим стандартный архитектурный шаблон-посредник. На форме документа мы создадим вспомогательный реквизит с типом ДеревоЗначений (на клиенте он представлен коллекцией ДанныеФормыДерево). Именно этот реквизит будет связан с элементом интерфейса типа «Таблица» с режимом отображения «Дерево».
Процесс работы с данными в этой схеме выглядит следующим образом:
Давайте пошагово разберем реализацию каждого этапа этой схемы.
---
Для начала откроем управляемую форму нашего документа в конфигураторе и выполним предварительные настройки:
ДеревоТоваров и установим для него тип данных ДеревоЗначений.ДеревоТоваров воссоздадим колонки, которые соответствуют нашей исходной табличной части (например: Номенклатура с типом справочника, Количество, Цена, Сумма с числовыми типами).ЭтоГруппа с булевым типом данных. Она понадобится нам для того, чтобы отличать строки-группы (папки номенклатуры) от строк-элементов (конкретных товаров) при условном оформлении и расчете итогов.ДеревоТоваров в левое окно элементов формы. Автоматически создастся элемент интерфейса типа «Таблица».---
Теперь нам нужно написать алгоритм, который при открытии документа считает данные из табличной части Товары, определит иерархическую вложенность номенклатуры и построит красивое дерево.
Наиболее эффективным способом построения дерева с автоиерархией является использование специального механизма обхода результатов запроса. Напишем процедуру, которая будет вызываться в обработчике формы ПриЧтенииНаСервере или ПриСозданииНаСервере:
&НаСервере
Процедура ЗаполнитьДеревоНаФорме()
// Очищаем текущее дерево на форме
ЗначениеВРеквизитФормы(Новый ДеревоЗначений, "ДеревоТоваров");
// Нам нужно выгрузить данные из ТЧ документа во временную таблицу,
// чтобы связать их с иерархией справочника Номенклатура
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ДокТовары.Номенклатура КАК Номенклатура,
| ДокТовары.Количество КАК Количество,
| ДокТовары.Сумма КАК Сумма
|ПОМЕСТИТЬ ВТ_ТоварыДокумента
|ИЗ
| &ТаблицаТоваров КАК ДокТовары
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| НоменклатураСпр.Ссылка КАК Номенклатура,
| НоменклатураСпр.ЭтоГруппа КАК ЭтоГруппа,
| ЕСТЬNULL(ВТ_ТоварыДокумента.Количество, 0) КАК Количество,
| ЕСТЬNULL(ВТ_ТоварыДокумента.Сумма, 0) КАК Сумма
|ИЗ
| Справочник.Номенклатура КАК НоменклатураСпр
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ТоварыДокумента КАК ВТ_ТоварыДокумента
| ПО НоменклатураСпр.Ссылка = ВТ_ТоварыДокумента.Номенклатура
|ГДЕ
| НоменклатураСпр.Ссылка В ИЕРАРХИИ(
| ВЫБРАТЬ РАЗЛИЧНЫЕ Номенклатура ИЗ ВТ_ТоварыДокумента
| )
|УПОРЯДОЧИТЬ ПО
| НоменклатураСпр.Ссылка ИЕРАРХИЯ";
// Передаем ТЧ документа в качестве параметра запроса
Запрос.УстановитьПараметр("ТаблицаТоваров", Объект.Товары.Выгрузить());
РезультатЗапроса = Запрос.Выполнить();
// Выгружаем результат с обходом по группировкам с иерархией
ДеревоОбъект = РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
// Теперь рассчитаем первичные итоги по иерархии для родительских строк.
// Метод Итог() у объекта ДеревоЗначений быстро выполняет рекурсивное суммирование.
РассчитатьИтогиДереваРекурсивно(ДеревоОбъект.Строки);
// Заполняем реквизит формы из полученного прикладного объекта дерева
ЗначениеВРеквизитФормы(ДеревоОбъект, "ДеревоТоваров");
КонецПроцедуры
&НаСервере
Процедура РассчитатьИтогиДереваРекурсивно(СтрокиДерева)
Для Каждого СтрокаДерева Из СтрокиДерева Цикл
// Если это группа (имеет подчиненные строки)
Если СтрокаДерева.Строки.Количество() > 0 Тогда
// Сначала рассчитываем нижележащие уровни (глубокий обход)
РассчитатьИтогиДереваРекурсивно(СтрокаДерева.Строки);
// Вычисляем суммы по подчиненным строкам текущей группы
СтрокаДерева.Количество = СтрокаДерева.Строки.Итог("Количество", Истина);
СтрокаДерева.Сумма = СтрокаДерева.Строки.Итог("Сумма", Истина);
СтрокаДерева.ЭтоГруппа = Истина;
Иначе
СтрокаДерева.ЭтоГруппа = Ложь;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
---
Когда пользователь меняет значение колонки Количество или Сумма в строке конкретного товара на форме, родительские группы должны мгновенно реагировать на это изменение и пересчитывать свои итоги. Отправлять при каждом изменении чисел вызовы на сервер нецелесообразно, так как это снизит отзывчивость интерфейса. Мы реализуем быстрый расчет на клиенте.
Для числовых полей ввода в таблице формы назначим обработчик события ПриИзменении. Рассмотрим пример реализации расчета для колонки количества:
&НаКлиенте
Процедура ДеревоТоваровКоличествоПриИзменении(Элемент)
ТекущаяСтрока = Элементы.ДеревоТоваров.ТекущиеДанные;
Если ТекущаяСтрока = Неопределено Тогда
Возврат;
КонецЕсли;
// Пересчитываем итоги вверх по дереву начиная от родителя текущей измененной строки
ПересчитатьИтогиВверхПоДереву(ТекущаяСтрока);
КонецПроцедуры
&НаКлиенте
Процедура ПересчитатьИтогиВверхПоДереву(ТекущаяСтрока)
Родитель = ТекущаяСтрока.ПолучитьРодителя();
// Цикл подъема от текущей строки к корневым элементам дерева
Пока Родитель <> Неопределено Цикл
ПодчиненныеЭлементы = Родитель.ПолучитьЭлементы();
НовоеКоличество = 0;
НоваяСумма = 0;
// Суммируем данные по всем непосредственным потомкам текущего родителя
Для Каждого ПодчиненнаяСтрока Из ПодчиненныеЭлементы Цикл
НовоеКоличество = НовоеКоличество + ПодчиненнаяСтрока.Количество;
НоваяСумма = НоваяСумма + ПодчиненнаяСтрока.Сумма;
КонецЦикла;
// Записываем новые итоги в родительскую строку
Родитель.Количество = НовоеКоличество;
Родитель.Сумма = НоваяСумма;
// Переходим на уровень выше
Родитель = Родитель.ПолучитьРодителя();
КонецЦикла;
КонецПроцедуры
---
Перед тем как документ будет записан в базу данных, нам нужно выгрузить все данные из экранного дерева обратно в реальную табличную часть Товары. При этом важно помнить, что строки, соответствующие группам номенклатуры (папкам), в ТЧ переносить не нужно — они используются только для группировки в интерфейсе.
Для этого в модуле формы переопределим стандартный обработчик ПередЗаписьюНаСервере:
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
// Получаем прикладной объект Дерева Значений из реквизита формы
ДеревоОбъект = РеквизитФормыВЗначение("ДеревоТоваров");
// Очищаем табличную часть документа перед перезаполнением
ТекущийОбъект.Товары.Очистить();
// Запускаем рекурсивный перенос конечных элементов (товаров) в ТЧ
ВыгрузитьСтрокиДереваВТабличнуюЧасть(ДеревоОбъект.Строки, ТекущийОбъект.Товары);
КонецПроцедуры
&НаСервере
Процедура ВыгрузитьСтрокиДереваВТабличнуюЧасть(СтрокиДерева, ТабличнаяЧастьДокумента)
Для Каждого СтрокаДерева Из СтрокиДерева Цикл
// Если строка является конечным товаром (у нее нет подчиненных строк)
Если СтрокаДерева.Строки.Количество() = 0 И СтрокаДерева.ЭтоГруппа = Ложь Тогда
// Записываем данные в плоскую ТЧ документа
НоваяСтрокаТЧ = ТабличнаяЧастьДокумента.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаТЧ, СтрокаДерева);
Иначе
// Если это группа — рекурсивно обходим ее ветки
ВыгрузитьСтрокиДереваВТабличнуюЧасть(СтрокаДерева.Строки, ТабличнаяЧастьДокумента);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
---
Чтобы с табличной частью-деревом было удобно работать, настроим правила условного оформления формы и установим необходимые свойства:
ТолькоПросмотр в значение Истина.ДеревоТоваров.ЭтоГруппа Равно Истина.ДеревоТоваровКоличество и ДеревоТоваровСумма.Шрифт в значение Жирный.Цвет фона установим, например, в мягкий серый или бежевый оттемент.ДеревоТоваров.ЭтоГруппа Равно Истина.Номенклатура.Раскрывать все уровни.---
Разберем еще один подход, упомянутый в сообщениях форума — использование элемента интерфейса «Поле табличного документа» на форме документа вместо дерева значений.
Этот метод основан на полной отрисовке макета прямо в коде, аналогично тому, как выводятся отчеты или печатные формы. При открытии формы создается табличный документ с иерархическими группировками строк, а при изменении ячеек пользователем данные считываются по координатам ячеек и записываются в ТЧ.
Преимущества метода:
Недостатки метода:
Рекомендуется использовать этот метод только для сложных расчетных форм (например, комплексных строительных смет), где визуальная составляющая важнее динамического изменения состава строк.
---
Проанализируем все рассмотренные варианты решения задачи отображения иерархии табличной части:
| Критерий сравнения | Реквизит ДеревоЗначений (Шаблон 1) | Поле табличного документа (Шаблон 2) | Шрифтовое оформление плоской ТЧ (Шаблон 3) |
|---|---|---|---|
| Сложность реализации | Средняя (требуется написать 3-4 процедуры для синхронизации и расчета). | Высокая (требуется детальный контроль координат ячеек и перехват событий). | Низкая (настраивается в несколько кликов через условное оформление). |
| Интерактивность и удобство ввода | Высокая (стандартные механизмы платформы по вводу и изменению строк). | Низкая (требует ручной обработки ввода данных пользователем). | Высокая (работа происходит в стандартной таблице). |
| Точность и наглядность иерархии | Идеальная (настоящие раскрывающиеся ветки дерева). | Высокая (иерархия отображается с помощью группировок табличного документа). | Низкая (иерархия имитируется отступами и шрифтами, ветки нельзя сворачивать). |
Итог: Для подавляющего большинства прикладных бизнес-задач оптимальным выбором является использование реквизита формы с типом ДеревоЗначений с настроенным условным оформлением. Оно обеспечивает надежное хранение данных, естественное поведение интерфейса 1С и высокую скорость разработки.