Как объединить несколько колонок Дерева Значений в одну с сохранением иерархии в 1С

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

При разработке интерфейсов и отчетов в среде 1С программисты часто сталкиваются с необходимостью вывода иерархических данных. Типичный пример: структура Контрагент — Договор. По умолчанию платформа 1С при выгрузке таких данных в ДеревоЗначений распределяет их по разным колонкам. На форме это выглядит избыточно: для каждого уровня вложенности создается отдельный вертикальный столбец, что затрудняет восприятие данных пользователем.

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

Постановка задачи

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

В стандартном дереве значений у нас будут две колонки: "Контрагент" и "Договор". Наша цель — получить дерево с одной-единственной колонкой (например, "Объект"), где на первом уровне будут отображаться контрагенты, а при их раскрытии — договоры. При этом важно сохранить все сопутствующие числовые или текстовые колонки (суммы взаиморасчетов, даты, ответственные менеджеры) на своих местах.

---

Решение 1. Исправление ошибки запроса через ИТОГИ и ВЫБОР

Попытка автора темы решить задачу "в лоб" через простой запрос с конструкцией вида ИТОГИ контрагент КАК договор закономерно привела к ошибке компиляции запроса "Повторяющийся псевдоним". Платформа не позволяет назначать разные источники данных одному и тому же итоговому полю напрямую.

Однако существует изящный способ обойти это ограничение. Мы можем использовать условную конструкцию ВЫБОР внутри секции ИТОГИ. Этот прием заставляет платформу динамически определять, какое значение выводить в результирующее поле на каждом конкретном уровне группировки.

Проанализируем пример такого запроса:


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

Запрос.УстановитьПараметр("ВременнаяТаблицаИсточника", ТаблицаДанных);
РезультатЗапроса = Запрос.Выполнить();

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

Как это работает: При выгрузке результата запроса с обходом ПоГруппировкам платформа 1С автоматически создает ДеревоЗначений. На верхнем уровне группировки (Контрагент) значение поля "ПолеДоговор" для итогов будет равно NULL, поэтому сработает ветка условия КОГДА, и в "ОбщуюКолонку" запишется ссылка на Контрагента. На детальном уровне (Договор) условие вернет ложь, и запишется ссылка на Договор. Мы получаем идеальное дерево в одну колонку без написания сложных ручных циклов.

---

Решение 2. Оптимизированный рекурсивный обход

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

Рассмотрим пошагово алгоритм переноса данных во вспомогательное дерево:

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

Посмотрим на программную реализацию этого метода:


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

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

Важный совет по оптимизации: Использование метода ЗаполнитьЗначенияСвойств избавляет нас от необходимости вручную перебирать сопутствующие колонки в цикле для каждой строки. Это существенно повышает скорость работы алгоритма на больших объемах данных.

---

Решение 3. Использование возможностей Системы Компоновки Данных (СКД)

Очень часто задача по слиянию колонок дерева возникает исключительно ради красивого визуального отображения данных пользователю на управляемой форме. Если ваша цель — просто показать информацию, нет никакой необходимости физически трансформировать структуру ДереваЗначений в памяти программным кодом.

Выясним, как решить задачу стандартными средствами СКД за пару кликов в настройках компоновщика:

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

---

Решение 4. Конвертация плоской таблицы через Построитель Запроса

Если на входе у вас есть плоская ТаблицаЗначений, и вам нужно максимально быстро построить из неё свернутое дерево в одну колонку, отличной альтернативой ручному кодингу станет объект ПостроительЗапроса. Он работает на уровне ядра платформы C++ и обрабатывает коллекции значительно быстрее, чем аналогичный код на встроенном языке 1С.

Рассмотрим пример быстрой программной сборки дерева:


Построитель = Новый ПостроительЗапроса;
// Передаем исходную таблицу значений в качестве источника данных
Построитель.ИсточникДанных = Новый ОписаниеИсточникаДанных(ИсходнаяТаблицаЗначений);

// Определяем измерения, которые сформируют иерархию дерева
ГруппировкаКонтрагент = Построитель.ИзмеренияСтроки.Добавить("Контрагент");
ГруппировкаКонтрагент.ТипИзмерения = ТипИзмеренияПостроителяЗапроса.Иерархия;

ГруппировкаДоговор = Построитель.ИзмеренияСтроки.Добавить("Договор");

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

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

Резюме

Мы подробно разобрали четыре различных способа объединения колонок иерархии в одну. Выбор конкретного метода зависит от вашей архитектурной задачи:

  1. Для вывода на форму или в отчет используйте стандартную настройку СКД "В одной колонке".
  2. При получении данных из базы данных применяйте конструкцию ВЫБОР в ИТОГАХ запроса.
  3. Для быстрой трансформации плоских таблиц используйте Построитель Запроса.
  4. Если данные уже находятся в памяти в виде дерева значений — применяйте рекурсивный обход с методом ЗаполнитьЗначенияСвойств().
← На главную