Разработка интерфейсов в виде кросс-таблиц (матриц) — это классическая задача в платформе 1С:Предприятие. С точки зрения пользователя, вводить плановые показатели по месяцам гораздо удобнее в горизонтальную строку, где под каждый период создана отдельная колонка. Однако базы данных (включая регистры накопления и сведений в 1С) эффективно работают только с плоскими структурами, в которых период является не колонкой, а отдельным измерением или реквизитом строки.
В этой статье мы подробно разберем, как спроектировать правильную архитектуру для решения этой задачи, программно создать динамические колонки на управляемой форме, корректно очистить их при смене периода и выполнить обратное преобразование (транспонирование) данных для записи в физическую таблицу документа и регистр накопления.
Начинающие разработчики часто пытаются привязать динамически создаваемые колонки формы напрямую к табличной части документа или вовсе отказаться от ТЧ, записывая данные формы сразу в независимый регистр сведений (как предлагалось в некоторых сообщениях на форуме). Однако наиболее надежным, стандартным и транзакционно безопасным подходом является паттерн «Плоская табличная часть — Экранная динамическая таблица».
Рассмотрим структуру этого паттерна:
ПродакшнПлан) со стандартными плоскими реквизитами: Категория (СправочникСсылка), Период (Дата, начало месяца) и Количество (Число).ТаблицаЗначений (например, ЭкраннаяТаблица), которая изначально содержит только одну статическую колонку — Категория. Остальные колонки-периоды будут добавляться динамически в зависимости от выбранного пользователем интервала дат.ЭкраннаяТаблица сворачивается обратно в плоскую табличную часть документа ПродакшнПлан.Этот подход позволяет сохранить объектную технику работы платформы 1С. Корректно работает история изменений, копирование документов, интерактивная пометка на удаление и стандартная блокировка объектов.
При создании динамических колонок важно помнить, что имя реквизита формы должно подчиняться правилам именования переменных во встроенном языке 1С. Оно не может начинаться с цифры и не может содержать специальные символы (точки, тире, пробелы). Использование формата вида "01.2017" или просто "Январь" приведет к синтаксической ошибке или к дублированию имен при переходе через год.
Для создания уникальных и валидных имен колонок мы рекомендуем использовать шаблон с текстовым префиксом, содержащим год и месяц: м_ГГГГ_ММ (например, м_2023_01, м_2023_02). Текстовое представление (например, "Январь 2023") мы будем записывать в свойство Заголовок элемента формы.
Поскольку пользователь может менять период документа многократно в течение одного сеанса работы с формой, перед генерацией новых колонок необходимо корректно удалять старые элементы и реквизиты. Невыполнение этого шага приведет к ошибке дублирования имен при повторном вызове кода.
Рассмотрим процедуру очистки динамических реквизитов формы:
&НаСервере
Процедура УдалитьДинамическиеКолонки()
МассивУдаляемыхРеквизитов = Новый Массив;
// Получаем текущую структуру реквизитов нашей экранной таблицы
МассивРеквизитовФормы = ПолучитьРеквизиты("ЭкраннаяТаблица");
Для Каждого Реквизит Из МассивРеквизитовФормы Цикл
// Проверяем наличие нашего префикса "м_"
Если Лев(Реквизит.Имя, 2) = "м_" Тогда
МассивУдаляемыхРеквизитов.Добавить("ЭкраннаяТаблица." + Реквизит.Имя);
// Удаляем визуальный элемент отображения с формы
ИмяЭлемента = "ЭкраннаяТаблица" + Реквизит.Имя;
ЭлементФормы = Элементы.Найти(ИмяЭлемента);
Если ЭлементФормы <> Неопределено Тогда
Элементы.Удалить(ЭлементФормы);
КонецЕсли;
EndЕсли;
КонецЦикла;
// Удаляем реквизиты из структуры данных формы
Если МассивУдаляемыхРеквизитов.Количество() > 0 Тогда
ИзменитьРеквизиты(, МассивУдаляемыхРеквизитов);
КонецЕсли;
КонецПроцедуры
Для создания реквизитов используется метод управляемой формы ИзменитьРеквизиты(). Этот метод выполняет тяжелую операцию по реструктуризации данных на сервере и передаче измененного контекста клиенту. Критически важно вызывать его всего один раз, передавая весь массив добавляемых и удаляемых реквизитов, а не вызывать его в цикле для каждого месяца.
Разберем процедуру генерации новых реквизитов и элементов формы:
&НаСервере
Процедура СоздатьДинамическиеКолонки()
// Сначала полностью очистим форму от старых динамических колонок
УдалитьДинамическиеКолонки();
НачалоИнтервала = Объект.ДатаНачала;
КонецИнтервала = Объект.ДатаОкончания;
МассивДобавляемыхРеквизитов = Новый Массив;
СписокПериодов = Новый СписокЗначений; // Для хранения соответствия ИмяКолонки -> Дата
ТекущийМесяц = НачалоМесяца(НачалоИнтервала);
КонечныйМесяц = НачалоМесяца(КонецИнтервала);
// Шаг 2.1: Сбор данных для добавления новых реквизитов
Пока ТекущийМесяц <= КонечныйМесяц Цикл
// Формируем программное имя по шаблону: м_ГГГГ_ММ
ИмяКолонки = "м_" + Формат(ТекущийМесяц, "ДФ=yyyy_MM");
// Создаем описание типа (для планирования обычно используется число)
ОписаниеТиповЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3));
// Конструируем новый реквизит формы
НовыйРеквизит = Новый РеквизитФормы(ИмяКолонки, ОписаниеТиповЧисло, "ЭкраннаяТаблица", Формат(ТекущийМесяц, "ДФ='MMMM yyyy'"), Истина);
МассивДобавляемыхРеквизитов.Добавить(НовыйРеквизит);
// Сохраняем дату для последующего обратного транспонирования
СписокПериодов.Добавить(ТекущийМесяц, ИмяКолонки);
ТекущийМесяц = ДобавитьМесяц(ТекущийМесяц, 1);
КонецЦикла;
// Шаг 2.2: Добавление реквизитов ОДНИМ вызовом (оптимально по производительности)
Если МассивДобавляемыхРеквизитов.Количество() > 0 Тогда
ИзменитьРеквизиты(МассивДобавляемыхРеквизитов);
КонецЕсли;
// Шаг 2.3: Визуализация колонок на форме
ТекущийМесяц = НачалоМесяца(НачалоИнтервала);
Пока ТекущийМесяц <= КонечныйМесяц Цикл
ИмяКолонки = "м_" + Формат(ТекущийМесяц, "ДФ=yyyy_MM");
ЗаголовокКолонки = Формат(ТекущийМесяц, "ДФ='MMMM yyyy'");
// Добавляем элемент поля ввода в таблицу на форме
НовыйЭлементФормы = Элементы.Добавить("ЭкраннаяТаблица" + ИмяКолонки, Тип("ПолеФормы"), Элементы.ЭкраннаяТаблица);
НовыйЭлементФормы.Вид = ВидПоляФормы.ПолеВвода;
НовыйЭлементФормы.ПутьКДанным = "ЭкраннаяТаблица." + ИмяКолонки;
НовыйЭлементФормы.Заголовок = ЗаголовокКолонки;
НовыйЭлементФормы.Ширина = 12;
ТекущийМесяц = ДобавитьМесяц(ТекущийМесяц, 1);
КонецЦикла;
// Сохраняем список периодов в реквизит формы для использования при записи
ЗначениеВРеквизитФормы(СписокПериодов, "АдресПериодов");
КонецПроцедуры
После создания структуры колонок нам нужно заполнить ее данными, которые уже сохранены в плоской табличной части документа. Для этого мы применим алгоритм группировки строк по аналитике (Категориям):
&НаСервере
Процедура ЗаполнитьЭкраннуюТаблицуИзТЧ()
// Очищаем текущие данные на экране
ЭкраннаяТаблица.Очистить();
// Сворачиваем исходную ТЧ для получения уникального списка категорий
ТаблицаКатегорий = Объект.ПродакшнПлан.Выгрузить();
ТаблицаКатегорий.Свернуть("Категория");
Для Каждого СтрКатегория Из ТаблицаКатегорий Цикл
НоваяСтрокаЭкран = ЭкраннаяТаблица.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаЭкран, СтрКатегория);
// Находим все записи ТЧ по этой категории и распределяем по колонкам
Отбор = Новый Структура("Категория", СтрКатегория.Категория);
СтрокиТЧ = Объект.ПродакшнПлан.НайтиСтроки(Отбор);
Для Каждого СтрокаТЧ Из СтрокиТЧ Цикл
// Формируем имя колонки по дате из строки ТЧ
ИмяКолонки = "м_" + Формат(СтрокаТЧ.Период, "ДФ=yyyy_MM");
// Записываем значение в динамическую колонку
Попытка
НоваяСтрокаЭкран[ИмяКолонки] = СтрокаТЧ.Количество;
Исключение
// Исключение может возникнуть, если период ТЧ выходит за рамки выбранного интервала
Продолжить;
КонецПопытки;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Когда пользователь отредактировал данные в динамической таблице и нажал кнопку «Записать» или «Провести», перед транзакцией записи объекта нам необходимо «схлопнуть» экранную таблицу обратно в плоскую структуру ТЧ документа ПродакшнПлан.
Для этого в модуле формы переопределим обработчик события ПередЗаписьюНаСервере:
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
// Полностью очищаем физическую табличную часть документа
ТекущийОбъект.ПродакшнПлан.Очистить();
// Восстанавливаем список соответствия Период -> Колонка
СписокПериодов = РеквизитФормыВЗначение("АдресПериодов");
Для Каждого СтрокаЭкран Из ЭкраннаяТаблица Цикл
Для Каждого ЭлементПериода Из СписокПериодов Цикл
ИмяКолонки = ЭлементПериода.Представление;
ДатаПериода = ЭлементПериода.Значение;
ЗначениеКоличества = СтрокаЭкран[ИмяКолонки];
// Не переносим пустые строки для экономии места в СУБД
Если ЗначениеКоличества = 0 Или ЗначениеКоличества = Неопределено Тогда
Продолжить;
КонецЕсли;
// Добавляем запись в плоскую структуру ТЧ документа
НоваяСтрокаТЧ = ТекущийОбъект.ПродакшнПлан.Добавить();
НоваяСтрокаТЧ.Категория = СтрокаЭкран.Категория;
НоваяСтрокаТЧ.Период = ДатаПериода;
НоваяСтрокаТЧ.Количество = ЗначениеКоличества;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Теперь, когда данные документа надежно сохранены в стандартной плоской табличной части, процедура создания движений по регистрам становится тривиальной. В модуле объекта документа в обработчике события ОбработкаПроведения мы пишем классический код проведения по табличной части:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Очистка старых движений по регистру
Движения.ВыпускПродукцииПлановый.Записывать = Истина;
Движения.ВыпускПродукцииПлановый.Очистить();
// Рекомендуется использовать запрос для оптимизации проведения
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Категория КАК Категория,
| Док.Период КАК Период,
| Док.Количество КАК Количество
|ИЗ
| Документ.ПланыПроизводства.ПродакшнПлан КАК Док
|ГДЕ
| Док.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
// Создаем движение по регистру накопления
Движение = Движения.ВыпускПродукцииПлановый.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
// Переносим данные из плоской строки
Движение.Период = ВыборкаДетальныеЗаписи.Период;
Движение.Категория = ВыборкаДетальныеЗаписи.Категория;
Движение.Количество = ВыборкаДетальныеЗаписи.Количество;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Если в вашей прикладной задаче планирования требуется сложная логика отображения (например, динамические итоги по строкам и колонкам, цветовое выделение ячеек при превышении лимитов, вывод иерархических заголовков), программное управление реквизитами формы через ИзменитьРеквизиты() становится чрезвычайно сложным в поддержке.
В современной практике разработки на платформе 1С для таких интерфейсов часто используют альтернативный подход — вывод кросс-таблицы в ПолеТабличногоДокумента:
ТабличныйДокумент с включенным свойством Редактирование.Категория и Период).ПриИзмененииСодержимогоОбласти. Программа считывает расшифровку измененной ячейки, новое значение и сразу фиксирует изменения в скрытом реквизите формы или перезаписывает соответствующую строку в ТЧ документа.Этот подход значительно производительнее серверного перестроения реквизитов формы, так как исключает необходимость перекомпиляции контекста формы при каждой смене периода или добавлении аналитики.