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