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