Как транспонировать таблицу значений в дерево со складами в колонках

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

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

Ограничения платформы при работе с кросс-таблицами

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

Объект ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений поддерживает вывод структуры только в виде списка (в таблицу значений) или дерева (в дерево значений). Если в настройках компоновщика задана структура «Таблица» (кросс-таблица со строками и колонками), вызов метода Вывести() вызовет исключение. Штатными средствами компоновщика кросс-таблицу можно вывести исключительно в ТабличныйДокумент.

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

Способ 1. Построение динамического запроса с секцией ИТОГИ

Рассмотрим наиболее производительный и чистый подход. Если требуется получить не просто плоскую таблицу, а именно ДеревоЗначений с автоматическим расчетом промежуточных итогов по группировкам («Вид номенклатуры — Номенклатура»), оптимально применить динамический запрос с предложением ИТОГИ.

Разберем по шагам логику этого решения:

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

Посмотрим на пример программной реализации:


// 1. Получаем список уникальных складов
ТаблицаСкладов = ИсходнаяТЗ.Скопировать();
ТаблицаСкладов.Свернуть("Склад");

// 2. Формируем динамический текст запроса
ТекстПолейВыбора = "";
ТекстИтогов = "";

Для Номер = 1 По ТаблицаСкладов.Количество() Цикл
    ИмяПараметра = "Склад_" + Формат(Номер, "ЧГ=0");
    ИмяПоля = "СкладКолонка_" + Формат(Номер, "ЧГ=0");
    
    ТекстПолейВыбора = ТекстПолейВыбора + ",
        |    СУММА(ВЫБОР КОГДА ИсходныеДанные.Склад = &" + ИмяПараметра + " ТОГДА ИсходныеДанные.Остаток ИНАЧЕ 0 КОНЕЦ) КАК " + ИмяПоля;
    
    ТекстИтогов = ТекстИтогов + ",
        |    СУММА(" + ИмяПоля + ")";
КонецЦикла;

ТекстЗапроса = 
    "ВЫБРАТЬ
    |    ИсходныеДанные.ВидНоменклатуры КАК ВидНоменклатуры,
    |    ИсходныеДанные.Номенклатура КАК Номенклатура" 
    + ТекстПолейВыбора + "
    |ПОМЕСТИТЬ ВТ_Данные
    |ИЗ
    |    &ТаблицаДанных КАК ИсходныеДанные
    |СГРУППИРОВАТЬ ПО
    |    ИсходныеДанные.ВидНоменклатуры,
    |    ИсходныеДанные.Номенклатура
    |;
    |ВЫБРАТЬ
    |    ВТ_Данные.ВидНоменклатуры,
    |    ВТ_Данные.Номенклатура" 
    + ТекстПолейВыбора + "
    |ИЗ
    |    ВТ_Данные КАК ВТ_Данные
    |ИТОГИ" 
    + Сред(ТекстИтогов, 2) + "
    |ПО
    |    ОБЩИЕ,
    |    ВидНоменклатуры,
    |    Номенклатура";

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

Номер = 1;
Для Каждого СтрСклад Из ТаблицаСкладов Цикл
    Запрос.УстановитьПараметр("Склад_" + Формат(Номер, "ЧГ=0"), СтрСклад.Склад);
    Номер = Номер + 1;
КонецЦикла;

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

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

Способ 2. Программный обход строк таблицы значений

Проанализируем классический алгоритмический вариант, когда разворот выполняется исключительно в оперативной памяти без вызова механизмов СУБД. Это удобно, если исходная таблица небольшая и требуется получить развернутую плоскую ТаблицаЗначений.

Разберем по шагам последовательность действий:

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

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


// Получаем уникальные склады
ТЗСклады = ИсходнаяТЗ.Скопировать();
ТЗСклады.Свернуть("Склад");

// Создаем результирующую ТЗ
НоваяТЗ = Новый ТаблицаЗначений;
НоваяТЗ.Колонки.Добавить("Номенклатура");

// Соответствие для быстрого поиска имени колонки по ссылке на склад
КартаКолонок = Новый Соответствие;
Номер = 1;

Для Каждого СтрСклад Из ТЗСклады Цикл
    ИмяКолонки = "Склад_" + Формат(Номер, "ЧГ=0");
    НоваяТЗ.Колонки.Добавить(ИмяКолонки, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)), Сокращенно(Строка(СтрСклад.Склад)));
    КартаКолонок.Вставить(СтрСклад.Склад, ИмяКолонки);
    Номер = Номер + 1;
КонецЦикла;

// Сортируем исходную таблицу для последовательной группировки
ИсходнаяТЗ.Сортировать("Номенклатура");

ТекущаяНоменклатура = Неопределено;
ТекущаяСтрокаНовойТЗ = Неопределено;

Для Каждого СтрИсходная Из ИсходнаяТЗ Цикл
    Если СтрИсходная.Номенклатура <> ТекущаяНоменклатура Тогда
        ТекущаяНоменклатура = СтрИсходная.Номенклатура;
        ТекущаяСтрокаНовойТЗ = НоваяТЗ.Добавить();
        ТекущаяСтрокаНовойТЗ.Номенклатура = ТекущаяНоменклатура;
    КонецЕсли;
    
    ИмяКолонки = КартаКолонок[СтрИсходная.Склад];
    Если ИмяКолонки <> Неопределено Тогда
        ТекущаяСтрокаНовойТЗ[ИмяКолонки] = ТекущаяСтрокаНовойТЗ[ИмяКолонки] + СтрИсходная.Остаток;
    КонецЕсли;
КонецЦикла;

При таком подходе мы получаем плоскую кросс-таблицу за один проход по отсортированному массиву строк.

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

Если задача заключается только в том, чтобы отобразить подсортировку складов пользователю на форме документа в виде раскрывающегося дерева с итогами, зачастую эффективнее отказаться от элемента управления ТабличноеПоле (или таблицы формы) и использовать ПолеТабличногоДокумента:

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