Как вывести дерево значений на форму с динамическими колонками: решаем задачу транспонирования и хранения данных в 1С 8.3

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

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

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

Архитектура хранения данных: почему одна табличная часть — это плохо

Прежде чем перейти к визуализации, проанализируем структуру хранения данных в документе. Нам необходимо хранить информацию об отправителе (ОргОбъект, ТехническоеМесто, объем переданного товара) и данные от получателей (Клиенты и объем полученного ими товара).

Некоторые разработчики предлагают хранить все данные в одной плоской табличной части следующего вида:

Рассмотрим ситуацию, когда у одного технического места может быть 15–20 получателей. При таком подходе строки с данными «ОргОбъект», «Техническое место» и «Передано» будут дублироваться 15–20 раз для каждого клиента. Это приводит к сильной избыточности данных, раздуванию информационной базы, усложнению расчетов и потенциальным ошибкам контроля целостности данных (например, когда в разных строках для одного и того же тех. места случайно укажут разный объем переданного товара).

Правильное решение: организовать хранение данных в двух табличных частях (нормализованная структура):

  1. ТЧ «ДанныеОтПоставщика». Содержит поля:
    • КлючСтроки (Число или уникальный идентификатор строки);
    • ОргОбъект (СправочникСсылка);
    • ТехническоеМесто (СправочникСсылка);
    • Передано (Число).
  2. ТЧ «ДанныеОтКлиентов». Содержит поля:
    • КлючСтроки (Связь с первой табличной частью);
    • Клиент (СправочникСсылка);
    • Получено (Число).

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

Выясняем причину ошибки СКД при выводе в дерево значений

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

В этот момент платформа возвращает ошибку: «Ошибка генерации макета по причине: Не поддерживается вывод таблиц и диаграмм в универсальную коллекцию значений».

Выясним причину этого поведения. Архитектура платформы «1С:Предприятие» накладывает жесткое ограничение: процессор вывода в коллекцию значений (будь то ТаблицаЗначений или ДеревоЗначений) умеет работать только с линейными структурами (детальными записями и простыми группировками строк). Вывод кросс-таблиц (где группировки определены и по строкам, и по колонкам) поддерживается исключительно для ТабличныйДокумент.

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

Реализация динамического матричного ввода на управляемой форме

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

Шаг 1. Создание базовой структуры на форме

В конфигураторе в реквизитах управляемой формы документа создадим новый реквизит ДеревоДанных с типом ДеревоЗначений. Добавим в него только постоянные колонки, которые не зависят от состава клиентов:

Вытащим это дерево на форму в виде элемента управления «Таблица». Постоянные колонки также перенесем в визуальную структуру элемента формы.

Шаг 2. Определение списка динамических колонок

Нам необходимо определить, какие клиенты будут отображаться в качестве колонок. Мы можем выбрать всех уникальных клиентов, которые уже есть в табличной части текущего документа, либо вообще всех активных клиентов из справочника (если документ заполняется с нуля).

Напишем функцию на сервере для получения этого списка:


&НаСервере
Функция ПолучитьСписокКлиентов()
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ РАЗЛИЧНЫЕ
        |	ДокументТаблицаКлиентов.Клиент КАК Клиент
        |ИЗ
        |	Документ.НашДокумент.ДанныеОтКлиентов КАК ДокументТаблицаКлиентов
        |ГДЕ
        |	ДокументТаблицаКлиентов.Ссылка = &Ссылка";
    
    Запрос.УстановитьПараметр("Ссылка", Объект.Ссылка);
    Возврат Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Клиент");
КонецФункции

Шаг 3. Программное создание реквизитов и элементов формы

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

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


&НаСервере
Процедура СоздатьДинамическиеКолонкиНаСервере()
    
    СписокКлиентов = ПолучитьСписокКлиентов();
    
    // Подготовим массив для добавления новых реквизитов
    НовыеРеквизиты = Новый Массив;
    
    Для Каждого ТекущийКлиент Из СписокКлиентов Цикл
        
        // Формируем уникальное имя колонки
        ИмяКолонки = "Клиент_" + СтрЗаменить(Строка(ТекущийКлиент.УникальныйИдентификатор()), "-", "_");
        
        // Создаем описание типа "Число" для хранения объемов получателя
        МассивТипов = Новый Массив;
        МассивТипов.Добавить(Тип("Число"));
        ОписаниеТиповЧисло = Новый ОписаниеТипов(МассивТипов, , , Новый КвалификаторыЧисла(15, 3));
        
        // Создаем новый реквизит, подчиненный нашему ДеревуДанных
        НовыйРеквизит = Новый РеквизитФормы(ИмяКолонки, ОписаниеТиповЧисло, "ДеревоДанных", ТекущийКлиент.Наименование);
        НовыеРеквизиты.Добавить(НовыйРеквизит);
        
    КонецЦикла;
    
    // Изменяем структуру реквизитов формы (добавляем новые колонки в дерево на сервере)
    ЭтаФорма.ИзменитьРеквизиты(НовыеРеквизиты);
    
    // Теперь создадим элементы управления (колонки в таблице на форме)
    Для Каждого ТекущийКлиент Из СписокКлиентов Цикл
        
        ИмяКолонки = "Клиент_" + СтрЗаменить(Строка(ТекущийКлиент.УникальныйИдентификатор()), "-", "_");
        
        // Добавляем элемент "Поле ввода" внутрь таблицы формы "ДеревоДанных"
        ЭлементКолонка = Элементы.Добавить(ИмяКолонки, Тип("ПолеФормы"), Элементы.ДеревоДанных);
        ЭлементКолонка.Вид = ВидПоляФормы.ПолеВвода;
        ЭлементКолонка.ПутьКДанным = "ДеревоДанных." + ИмяКолонки;
        ЭлементКолонка.Заголовок = ТекущийКлиент.Наименование;
        ЭлементКолонка.Ширина = 12;
        
    КонецЦикла;
    
КонецПроцедуры

Шаг 4. Заполнение дерева данными с помощью запроса

Для заполнения дерева значений воспользуемся запросом, который объединяет две наши табличные части по полю КлючСтроки и использует конструкцию ИТОГИ ПО для построения иерархической структуры.


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

Шаг 5. Обратное сохранение измененных данных в документ

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

Для этого в обработчике формы ПередЗаписьюНаСервере мы напишем алгоритм свертки дерева:


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

Важные нюансы оптимизации работы с динамической формой

При реализации данного механизма важно помнить о нескольких технических тонкостях платформы «1С:Предприятие 8.3»:

Проанализировав данную проблему, мы выяснили, что грамотное сочетание нормализованной структуры хранения (две связанные ТЧ с уникальным ключом) и механизма динамического изменения реквизитов управляемой формы позволяет обойти любые ограничения СКД. Это дает возможность разрабатывать эргономичные, быстрые и удобные для пользователя интерфейсы распределения ресурсов в стиле шахматки прямо в среде 1С.

← На главную