Как записать данные из динамических колонок таблицы значений формы в плоский регистр накопления в 1С?

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

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

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

Архитектурный шаблон «Плоская ТЧ документа — Экранная динамическая ТЗ»

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

Рассмотрим структуру этого паттерна:

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

Этот подход позволяет сохранить объектную технику работы платформы 1С. Корректно работает история изменений, копирование документов, интерактивная пометка на удаление и стандартная блокировка объектов.

Правила именования динамических колонок

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

Для создания уникальных и валидных имен колонок мы рекомендуем использовать шаблон с текстовым префиксом, содержащим год и месяц: м_ГГГГ_ММ (например, м_2023_01, м_2023_02). Текстовое представление (например, "Январь 2023") мы будем записывать в свойство Заголовок элемента формы.

Шаг 1. Очистка старых динамических колонок на форме

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

Рассмотрим процедуру очистки динамических реквизитов формы:


&НаСервере
Процедура УдалитьДинамическиеКолонки()
    
    МассивУдаляемыхРеквизитов = Новый Массив;
    
    // Получаем текущую структуру реквизитов нашей экранной таблицы
    МассивРеквизитовФормы = ПолучитьРеквизиты("ЭкраннаяТаблица");
    Для Каждого Реквизит Из МассивРеквизитовФормы Цикл
        // Проверяем наличие нашего префикса "м_"
        Если Лев(Реквизит.Имя, 2) = "м_" Тогда
            МассивУдаляемыхРеквизитов.Добавить("ЭкраннаяТаблица." + Реквизит.Имя);
            
            // Удаляем визуальный элемент отображения с формы
            ИмяЭлемента = "ЭкраннаяТаблица" + Реквизит.Имя;
            ЭлементФормы = Элементы.Найти(ИмяЭлемента);
            Если ЭлементФормы <> Неопределено Тогда
                Элементы.Удалить(ЭлементФормы);
            КонецЕсли;
        EndЕсли;
    КонецЦикла;
    
    // Удаляем реквизиты из структуры данных формы
    Если МассивУдаляемыхРеквизитов.Количество() > 0 Тогда
        ИзменитьРеквизиты(, МассивУдаляемыхРеквизитов);
    КонецЕсли;
    
КонецПроцедуры

Шаг 2. Оптимальное программное создание колонок на форме

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

Разберем процедуру генерации новых реквизитов и элементов формы:


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

Шаг 3. Заполнение экранной таблицы существующими данными

После создания структуры колонок нам нужно заполнить ее данными, которые уже сохранены в плоской табличной части документа. Для этого мы применим алгоритм группировки строк по аналитике (Категориям):


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

Шаг 4. Обратное свертывание (транспонирование) перед записью

Когда пользователь отредактировал данные в динамической таблице и нажал кнопку «Записать» или «Провести», перед транзакцией записи объекта нам необходимо «схлопнуть» экранную таблицу обратно в плоскую структуру ТЧ документа ПродакшнПлан.

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


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

Шаг 5. Запись данных в регистр накопления

Теперь, когда данные документа надежно сохранены в стандартной плоской табличной части, процедура создания движений по регистрам становится тривиальной. В модуле объекта документа в обработчике события ОбработкаПроведения мы пишем классический код проведения по табличной части:


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

Альтернативный современный UI-подход: Использование Табличного Документа

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

В современной практике разработки на платформе 1С для таких интерфейсов часто используют альтернативный подход — вывод кросс-таблицы в ПолеТабличногоДокумента:

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

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

← На главную