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

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

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

Попытка реализовать это путем создания множества физических табличных частей в конфигураторе обречена на провал, поскольку структура метаданных статична. Мы не можем "на лету" программно добавить новую табличную часть в структуру документа СУБД во время работы пользователя. Однако платформа 1С:Предприятие 8 предоставляет мощные инструменты для динамического конструирования интерфейса. Разберем наиболее эффективные архитектурные паттерны для решения этой задачи в управляемых формах.

Архитектурный принцип: "Одна таблица в базе — много таблиц на форме"

Прежде чем переходить к коду, определим ключевое правило проектирования баз данных: физически в документе должна быть ровно одна табличная часть. Назовем ее, например, НачисленияПоСотрудникам. В этой табличной части обязательно должен присутствовать реквизит, по которому будет происходить разделение данных (в нашем случае — Подразделение с типом ссылки на соответствующий справочник).

Для пользователя мы создадим иллюзию существования множества независимых таблиц. Платформа 1С позволяет разместить на управляемой форме несколько элементов типа ТаблицаФормы, каждый из которых будет связан с одной и той же табличной частью объекта, но отображать только отфильтрованные строки. Для этого используется ключевое свойство ОтборСтрок.

Решение 1. Динамическое создание вкладок и таблиц на форме с фильтрацией

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

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


&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
    
    // Инициализируем отрисовку интерфейса по подразделениям
    ПостроитьДинамическийИнтерфейсНаФорме();
    
КонецПроцедуры

Теперь детально разберем процедуру построения интерфейса. Мы будем использовать метод Элементы.Добавить() для динамического создания элементов формы.


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

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

Тем не менее, рекомендуется подстраховаться и включить у колонки Подразделение свойство "Использовать всегда", а также обрабатывать событие ПриИзменении для контроля бизнес-логики.

Решение 2. Использование Дерева Значений на форме

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

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

  1. Создаем на форме реквизит ДеревоНачислений с типом ДеревоЗначений.
  2. Создаем колонки дерева: Подразделение, Сотрудник, Показатель, Сумма.
  3. При открытии формы считываем данные из плоской табличной части документа и заполняем дерево.
  4. При записи документа производим обратную процедуру — сворачиваем дерево в плоскую табличную часть.

Рассмотрим код заполнения дерева на сервере:


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

Для сохранения данных обратно в документ перед записью на сервере мы пишем зеркальный код:


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

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

Решение 3. Интерактивный тумблер (быстрый фильтр)

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

Рассмотрим, как это работает:

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

Посмотрим на пример реализации обработчика изменения тумблера:


&НаКлиенте
Процедура ТекущееПодразделениеОтборПриИзменении(Элемент)
    
    // Накладываем отбор на таблицу формы в реальном времени без обращения к серверу!
    ЭлементТаблица = Элементы.НачисленияПоСотрудникам;
    
    Если ЗначениеЗаполнено(ТекущееПодразделениеОтбор) Тогда
        
        СтруктураОтбора = Новый Структура("Подразделение", ТекущееПодразделениеОтбор);
        ЭлементТаблица.ОтборСтрок = Новый ФиксированнаяСтруктура(СтруктураОтбора);
        
    Иначе
        
        // Если отбор сброшен, показываем все строки
        ЭлементТаблица.ОтборСтрок = Неопределено;
        
    КонецЕсли;
    
КонецПроцедуры

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

Решение 4. Использование Поля Табличного Документа (интерфейс а-ля Excel)

Бывают ситуации, когда требования пользователей непреклонны: "Мы хотим видеть все подразделения на одном экране, с красивыми шапками, объединенными ячейками, жирным шрифтом для итогов и формулами прямо как в Excel". Обычные таблицы управляемых форм для этого не приспособлены.

В такой ситуации проанализируем возможность использования элемента формы ПолеТабличногоДокумента с включенным свойством Редактирование = Истина. Алгоритм работы следующий:

  1. При открытии формы на сервере мы формируем макет табличного документа. Выводим секции заголовков подразделений, строки для ввода сотрудников и итоговые формулы.
  2. Данные из физической табличной части документа переносятся в ячейки табличного документа.
  3. Пользователь редактирует показатели прямо в ячейках таблицы.
  4. Перед записью документа программный код осуществляет парсинг (обход) ячеек табличного документа, вытаскивает заполненные данные сотрудников по секциям и сохраняет их в плоскую табличную часть документа для последующего проведения и отражения в регистрах.

Этот подход сложен в программировании, так как требует жесткой привязки к координатам ячеек или использования именованных областей макета. Однако он гарантирует 100% удовлетворение требований пользователей, привыкших к работе в Excel.

Выводы и рекомендации

Мы детально разобрали четыре различных подхода к реализации интерактивного добавления табличных частей. Подведем итоги совместной работы:

← На главную