При разработке отчетов и обработок в среде 1С часто возникает задача транспонирования данных — разворота строк таблицы в колонки. Типичный пример: имеется исходная таблица значений (ТЗ) со списком сотрудников, статьями финансирования и начисленными суммами. Чтобы сделать таблицу легкочитаемой, статьи финансирования необходимо «развернуть» по горизонтали, превратив их в динамические колонки, и распределить по ним соответствующие суммы. В этой статье мы подробно, шаг за шагом, разберем, как решить эту задачу на платформе 1С:Предприятие 8.3.
Когда мы работаем со статическими колонками таблицы значений, мы можем обращаться к ним напрямую через точку, например: СтрокаТЗ.Сумма. Однако, если имена колонок создаются динамически на основании справочника (например, наименований статей финансирования), обратиться к ним через точку не получится. Код вида СтрокаТЗ.ИмяПеременнойКолонки вызовет ошибку компиляции или выполнения «Поле объекта не обнаружено», поскольку система будет искать колонку с буквальным именем «ИмяПеременнойКолонки».
Для обхода этого ограничения в платформе 1С предусмотрен специальный синтаксис — обращение к свойствам объекта через квадратные скобки: СтрокаТЗ[ИмяПеременнойКолонки]. Внутри скобок передается строковая переменная, содержащая реальное имя колонки, созданной в ТЗ.
---
Рассмотрим классический и наиболее производительный алгоритм разворота строк в колонки с использованием объекта Соответствие. Этот объект позволит нам быстро сопоставлять элементы справочника (статьи финансирования) с именами динамически созданных колонок таблицы значений.
Выполним задачу по шагам:
Соответствие, в котором ключом будет ссылка на статью финансирования, а значением — строковое имя созданной колонки.Посмотрим на практический пример реализации этого алгоритма:
// Функция выполняет свертку исходной таблицы и разворот статей финансирования в колонки
Процедура РаспределитьСуммыПоСтатьям(ИсходнаяТЗ)
// 1. Создаем итоговую таблицу значений
ИтоговаяТЗ = Новый ТаблицаЗначений;
ИтоговаяТЗ.Колонки.Добавить("Сотрудник", Новый ОписаниеТипов("СправочникСсылка.Сотрудники"));
ИтоговаяТЗ.Колонки.Добавить("ОбщаяСумма", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
// Соответствие для быстрой связи: СтатьяСсылка -> ИмяКолонки
СоответствиеКолонок = Новый Соответствие;
// 2. Сначала соберем все уникальные статьи финансирования, чтобы создать колонки
ТаблицаСтатей = ИсходнаяТЗ.Скопировать();
ТаблицаСтатей.Свернуть("СтатьяФинансирования");
Для Каждого СтрСтатья Из ТаблицаСтатей Цикл
СсылкаНаСтатью = СтрСтатья.СтатьяФинансирования;
Если Не ЗначениеЗаполнено(СсылкаНаСтатью) Тогда
Продолжить;
КонецЕсли;
// Преобразуем наименование статьи в валидный идентификатор для имени колонки
ИмяНовойКолонки = ПреобразоватьВИдентификаторКолонки(СсылкаНаСтатью.Наименование);
// Добавляем колонку в итоговую ТЗ, если её еще нет
Если ИтоговаяТЗ.Колонки.Найти(ИмяНовойКолонки) = Неопределено Тогда
ИтоговаяТЗ.Колонки.Добавить(ИмяНовойКолонки, Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
КонецЕсли;
// Запоминаем связь статьи и колонки
СоответствиеКолонок.Вставить(СсылкаНаСтатью, ИмяНовойКолонки);
КонецЦикла;
// 3. Заполняем итоговую таблицу данными и распределяем суммы
Для Каждого СтрокаИсходная Из ИсходнаяТЗ Цикл
// Ищем уже существующую строку по сотруднику в итоговой таблице
ПараметрыПоиска = Новый Структура("Сотрудник", СтрокаИсходная.Сотрудник);
НайденныеСтроки = ИтоговаяТЗ.НайтиСтроки(ПараметрыПоиска);
Если НайденныеСтроки.Количество() > 0 Тогда
СтрокаПолучатель = НайденныеСтроки[0];
Иначе
СтрокаПолучатель = ИтоговаяТЗ.Добавить();
СтрокаПолучатель.Сотрудник = СтрокаИсходная.Сотрудник;
КонецЕсли;
// Суммируем общий итог по строке
СтрокаПолучатель.ОбщаяСумма = СтрокаПолучатель.ОбщаяСумма + СтрокаИсходная.Сумма;
// Определяем имя динамической колонки по статье финансирования
ИмяКолонкиДляСуммы = СоответствиеКолонок.Получить(СтрокаИсходная.СтатьяФинансирования);
Если ИмяКолонкиДляСуммы <> Неопределено Тогда
// Самый важный момент: динамическое обращение через квадратные скобки
СтрокаПолучатель[ИмяКолонкиДляСуммы] = СтрокаПолучатель[ИмяКолонкиДляСуммы] + СтрокаИсходная.Сумма;
КонецЕсли;
КонецЦикла;
// ИтоговаяТЗ готова для дальнейшего использования или вывода на форму
КонецПроцедуры
---
Важным нюансом при создании колонок является правильное формирование их имен. Имя колонки в 1С должно отвечать правилам создания идентификаторов переменной: оно не может содержать пробелов, спецсимволов, знаков препинания и не может начинаться с цифры.
Проанализируем функцию очистки строк, которая преобразует произвольное наименование статьи финансирования в безопасный идентификатор 1С:
&НаСервере
Функция ПреобразоватьВИдентификаторКолонки(Знач Наименование)
// Удаляем все недопустимые символы
Наименование = СтрЗаменить(Наименование, " ", "_");
Наименование = СтрЗаменить(Наименование, "-", "_");
Наименование = СтрЗаменить(Наименование, "/", "_");
Наименование = СтрЗаменить(Наименование, "\", "_");
Наименование = СтрЗаменить(Наименование, "(", "");
Наименование = СтрЗаменить(Наименование, ")", "");
Наименование = СтрЗаменить(Наименование, "%", "");
Наименование = СтрЗаменить(Наименование, "№", "Номер");
// Если наименование начинается с цифры, добавим префикс
ПервыйСимвол = Лев(Наименование, 1);
Если Найти("0123456789", ПервыйСимвол) > 0 Тогда
Наименование = "Статья_" + Наименование;
КонецЕсли;
Возврат Наименование;
КонецФункции
Важный совет: Если в вашей базе данных возможна ситуация, когда две разные статьи финансирования после очистки спецсимволов преобразуются в одно и то же имя (например, «Статья - 1» и «Статья / 1» станут Статья__1), обязательно добавьте в алгоритм проверку уникальности имен колонок. При совпадении имен добавляйте к идентификатору цифровой постфикс (_1, _2 и так далее).
---
Создать таблицу значений в оперативной памяти сервера — это только половина задачи. Если вам необходимо показать полученный результат пользователю на управляемой форме, вам потребуется программно создать реквизиты и элементы формы для отображения динамических колонок.
Рассмотрим, как правильно программно перестроить интерфейс формы под новую структуру таблицы значений:
&НаСервере
Процедура ОтобразитьТЗНаФорме(ИтоговаяТЗ)
// 1. Очищаем старые динамические элементы и реквизиты формы, если они были созданы ранее
МассивУдаляемыхРеквизитов = Новый Массив;
Для Каждого РеквизитФормы Из ЭтаФорма.ПолучитьРеквизиты("ДинамическаяТаблицаДанных") Цикл
// Пропускаем основные статические колонки, удаляем только динамические
Если РеквизитФормы.Имя <> "Сотрудник" И РеквизитФормы.Имя <> "ОбщаяСумма" Тогда
МассивУдаляемыхРеквизитов.Добавить("ДинамическаяТаблицаДанных." + РеквизитФормы.Имя);
// Удаляем визуальный элемент управления с формы
ЭлементФормы = Элементы.Найти("ДинамическаяТаблицаДанных" + РеквизитФормы.Имя);
Если ЭлементФормы <> Неопределено Тогда
Элементы.Удалить(ЭлементФормы);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Если МассивУдаляемыхРеквизитов.Количество() > 0 Тогда
ЭтаФорма.ИзменитьРеквизиты(, МассивУдаляемыхРеквизитов);
КонецЕсли;
// 2. Создаем массив новых реквизитов для добавления
МассивДобавляемыхРеквизитов = Новый Массив;
Для Каждого КолонкаТЗ Из ИтоговаяТЗ.Колонки Цикл
Если КолонкаТЗ.Имя = "Сотрудник" Или КолонкаТЗ.Имя = "ОбщаяСумма" Тогда
Продолжить; // Они уже созданы на форме статически
КонецЕсли;
// Создаем новый реквизит формы внутри нашей таблицы "ДинамическаяТаблицаДанных"
НовыйРеквизит = Новый РеквизитФормы(КолонкаТЗ.Имя, КолонкаТЗ.ТипЗначения, "ДинамическаяТаблицаДанных", КолонкаТЗ.Заголовок);
МассивДобавляемыхРеквизитов.Добавить(НовыйРеквизит);
КонецЦикла;
// Применяем изменения структуры реквизитов формы
ЭтаФорма.ИзменитьРеквизиты(МассивДобавляемыхРеквизитов);
// 3. Создаем визуальные элементы (колонки) в таблице на форме
РодительТаблица = Элементы.ДинамическаяТаблицаДанных;
Для Каждого Реквизит Из МассивДобавляемыхРеквизитов Цикл
НовыйЭлемент = Элементы.Добавить("ДинамическаяТаблицаДанных" + Реквизит.Имя, Тип("ПолеФормы"), РодительТаблица);
НовыйЭлемент.Вид = ВидПоляФормы.ПолеВвода;
НовыйЭлемент.ПутьКДанным = "ДинамическаяТаблицаДанных." + Реквизит.Имя;
НовыйЭлемент.ПоложениеЗаголовка = ПоложениеЗаголовкаЭлементаФормы.Нет;
КонецЦикла;
// 4. Копируем данные из нашей заполненной ТЗ в реквизит формы
ЗначениеВРеквизитФормы(ИтоговаяТЗ, "ДинамическаяТаблицаДанных");
КонецПроцедуры
---
Для продвинутых разработчиков существует альтернативный и крайне эффективный способ свертки и транспонирования данных — программное использование СКД в качестве расчетного движка. Нам не нужно выводить отчет на экран, мы можем заставить СКД сгруппировать данные и выгрузить их напрямую в плоскую ТаблицуЗначений.
Преимущество этого метода в том, что платформа 1С берет на себя всю логику группировки по строкам, динамическое создание колонок кросс-таблицы и расчет итогов по вертикали и горизонтали.
Разберем по шагам логику этого подхода в программном коде:
&НаСервере
Функция СвернутьТЗЧерезСКД(ИсходнаяТЗ)
// Создаем схему компоновки данных программно
СхемаКомпоновкиДанных = Новый СхемаКомпоновкиДанных;
// Добавляем источник данных
ИсточникДанных = СхемаКомпоновкиДанных.ИсточникиДанных.Добавить();
ИсточникДанных.Имя = "ИсточникДанных1";
ИсточникДанных.ТипИсточникаДанных = "Local";
// Добавляем набор данных - объект (наша исходная ТЗ)
НаборДанных = СхемаКомпоновкиДанных.НаборыДанных.Добавить(Тип("НаборДанныхОбъектСхемыКомпоновкиДанных"));
НаборДанных.Имя = "НаборДанных1";
НаборДанных.ИмяОбъекта = "ТаблицаДанных";
ИсточникДанных.ИсточникДанных = ИсточникДанных.Имя;
// Описываем поля набора данных
ДобавитьПолеВНабор(НаборДанных, "Сотрудник", "Сотрудник", Тип("СправочникСсылка.Сотрудники"));
ДобавитьПолеВНабор(НаборДанных, "СтатьяФинансирования", "СтатьяФинансирования", Тип("СправочникСсылка.СтатьиФинансированияЗарплата"));
ДобавитьПолеВНабор(НаборДанных, "Сумма", "Сумма", Новый ОписаниеТипов("Число"));
// Добавляем ресурсы схемы
ПолеРесурса = СхемаКомпоновкиДанных.ПоляРесурсов.Добавить();
ПолеРесурса.ПутьКПолю = "Сумма";
ПолеРесурса.Выражение = "Сумма(Сумма)";
// Настраиваем структуру таблицы (Кросс-таблица) в СКД
КомпоновщикНастроек = Новый КомпоновщикНастроекКомпоновкиДанных;
КомпоновщикНастроек.Инициализировать(Новый ИсточникДоступныхНастроекКомпоновкиДанных(СхемаКомпоновкиДанных));
Настройки = КомпоновщикНастроек.Настройки;
// Создаем структуру Таблица
СтруктураТаблица = Настройки.Структура.Добавить(Тип("ТаблицаКомпоновкиДанных"));
// Строки: Группировка по сотруднику
ГруппировкаСтроки = СтруктураТаблица.Строки.Добавить();
ГруппировкаСтроки.Использование = Истина;
ПолеСтроки = ГруппировкаСтроки.ПоляГруппировки.Элементы.Добавить(Тип("ПолеГруппировкиКомпоновкиДанных"));
ПолеСтроки.Поле = Новый ПолеКомпоновкиДанных("Сотрудник");
ВыбранноеПолеСтроки = ГруппировкаСтроки.Выбор.Элементы.Добавить(Тип("АвтоВыбранноеПолеКомпоновкиДанных"));
// Колонки: Группировка по статьям финансирования
ГруппировкаКолонки = СтруктураТаблица.Колонки.Добавить();
ГруппировкаКолонки.Использование = Истина;
ПолеКолонки = ГруппировкаКолонки.ПоляГруппировки.Элементы.Добавить(Тип("ПолеГруппировкиКомпоновкиДанных"));
ПолеКолонки.Поле = Новый ПолеКомпоновкиДанных("СтатьяФинансирования");
ВыбранноеПолеКолонки = ГруппировкаКолонки.Выбор.Элементы.Добавить(Тип("АвтоВыбранноеПолеКомпоновкиДанных"));
// Общие выбранные поля (Ресурсы)
ВыбранныйРесурс = Настройки.Выбор.Элементы.Добавить(Тип("ВыбранноеПолеКомпоновкиДанных"));
ВыбранныйРесурс.Поле = Новый ПолеКомпоновкиДанных("Сумма");
// Выполняем компоновку в коллекцию значений (Таблицу Значений)
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки,,, Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ВнешниеНаборы = Новый Структура("ТаблицаДанных", ИсходнаяТЗ);
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, ВнешниеНаборы);
РезультатТЗ = Новый ТаблицаЗначений;
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ПроцессорВывода.УстановитьОбъект(РезультатТЗ);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);
Возврат РезультатТЗ;
КонецФункции
Процедура ДобавитьПолеВНабор(НаборДанных, ИмяПоля, ПутьКПолю, ТипПоля)
Поле = НаборДанных.Поля.Добавить(Тип("ПолеНабораДанныхСхемыКомпоновкиДанных"));
Поле.Имя = ИмяПоля;
Поле.ПутьКПолю = ПутьКПолю;
Поле.Заголовок = ИмяПоля;
Поле.ТипЗначения = ТипПоля;
КонецПроцедуры
Этот подход возвращает идеально сгруппированную и свернутую плоскую таблицу значений, в которой автоматически созданы колонки для каждой статьи финансирования, встретившейся в исходной ТЗ, а суммы корректно распределены по строкам-сотрудникам. Метод СКД избавляет разработчика от ручного написания сложных вложенных циклов.
---
Мы проанализировали несколько различных подходов к заполнению динамических колонок в ТЗ на платформе 1С 8.3: