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