При разработке документов в 1С иногда возникает задача, когда количество колонок в табличной части заранее не определено и может меняться динамически. Например, в документе "Заказ контрагента" может потребоваться вводить количество заказанной продукции по различным видам номенклатуры, при этом список номенклатуры постоянно обновляется и задается не в конфигураторе, а внешними данными. В такой ситуации стандартные подходы с фиксированным набором реквизитов табличной части становятся неэффективными.
Изначально разработчики могут столкнуться с идеей динамического добавления реквизитов непосредственно в табличную часть объекта (например, Объект.Товары) через метод ИзменитьРеквизиты(), как это было в сообщении 2. Однако этот подход имеет существенные ограничения в части сохранения данных. Рассмотрим, как эффективно организовать хранение и отображение таких динамических данных.
В ходе обсуждения на форуме были предложены и проработаны несколько подходов, каждый из которых имеет свои преимущества и недостатки. Мы подробно разберем наиболее оптимальный и гибкий подход – вертикальное хранение данных, а также затронем другие варианты.
Суть подхода: Вместо попытки создать множество колонок непосредственно в табличной части документа, мы храним данные в "вертикальном" или нормализованном виде, а затем программно "разворачиваем" их в "горизонтальную" кросс-таблицу для отображения на форме. Представьте, что вместо отдельных колонок для каждой номенклатуры (например, "КоличествоМолоко", "КоличествоХлеб"), мы создаем строки, где каждая строка содержит ссылку на номенклатуру и ее количество.
Этот подход был предложен в сообщении 1 и далее развит в сообщении 5, где автор в итоге понял его преимущества (сообщение 10). Он является наиболее гибким и масштабируемым решением.
Для реализации этого подхода, особенно при наличии нескольких измерений (например, "Контрагент", "Номенклатура", "Количество"), предлагается использовать несколько табличных частей в документе или даже регистр сведений. Рассмотрим вариант с тремя табличными частями, как было предложено в обсуждении (сообщение 5):
Основная табличная часть документа (например, Товары или КонтрагентыЗаказа):
НомерСтроки, Контрагент (тип СправочникСсылка.Контрагенты), Договор и т.д.УИДСтроки с типом УникальныйИдентификатор. Этот УИД будет выступать в качестве "ключа" для связи.Табличная часть для определения динамических колонок (например, НоменклатураКолонок):
Номенклатура (тип СправочникСсылка.Номенклатура). Мы можем также добавить сюда дополнительные параметры, если они нужны для каждой колонки (например, ЕдиницаИзмеренияКолонки).Табличная часть для хранения значений динамических колонок (например, ЗаказанныеКоличества):
КонтрагентыЗаказа (через УИДСтрокиОсновнойТЧ), ссылку на номенклатуру из НоменклатураКолонок (НоменклатураКолонки), а также сами значения.УИДСтрокиОсновнойТЧ (тип УникальныйИдентификатор, связь с первой ТЧ), НоменклатураКолонки (тип СправочникСсылка.Номенклатура, связь со второй ТЧ), КоличествоЯщиков (тип Число), КоличествоШтук (тип Число).Для пользователя данные должны выглядеть как обычная табличная часть с динамически создаваемыми колонками. Для этого нам потребуется реализовать следующую логику при открытии формы документа:
ПриСозданииНаСервере) мы анализируем данные из табличной части, определяющей динамические колонки (например, НоменклатураКолонок, или можем получить список всей актуальной номенклатуры из регистра сведений). Для каждой уникальной номенклатуры из этого списка мы динамически создаем реквизиты формы (используя метод ИзменитьРеквизиты()) и соответствующие элементы управления ПолеФормы, привязывая их к этим реквизитам.ГруппаФормы с видом ГруппаКолонок.ЗаказанныеКоличества. Для этого мы проходим по строкам основной ТЧ формы (например, Объект.КонтрагентыЗаказа) и для каждой строки ищем соответствующие значения в ЗаказанныеКоличества, устанавливая их в динамически созданные реквизиты формы.Пример кода, иллюстрирующий динамическое создание реквизитов и элементов формы, а также их заполнение при открытии документа:
// В модуле формы документа
// Предполагаем, что у нас есть основная табличная часть "Объект.КонтрагентыЗаказа"
// и дополнительные табличные части "Объект.НоменклатураКолонок" и "Объект.ЗаказанныеКоличества"
// Реквизит формы для хранения списка динамических колонок,
// чтобы потом знать, что сохранять
&НаКлиенте
Перем СписокДинамическихКолонок; // Тип: СписокЗначений. Значение = СсылкаНаНоменклатуру, Представление = ИмяРеквизитаЯщиков
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
Если Объект.ЭтоНовый Тогда
// Заполняем основную табличную часть, если новый документ
// Например, добавляем строку для контрагента
НоваяСтрока = Объект.КонтрагентыЗаказа.Добавить();
НоваяСтрока.Контрагент = Справочники.Контрагенты.НайтиПоНаименованию("Основной поставщик"); // Пример
НоваяСтрока.УИДСтроки = Новый УникальныйИдентификатор();
КонецЕсли;
// Шаг 1: Определяем, какие колонки нам нужны.
// Это может быть список из ТЧ НоменклатураКолонок,
// или из регистра сведений, или все номенклатуры за период.
// Сформируем запрос для получения списка номенклатуры, которая должна быть колонками.
// Включаем номенклатуру, которая уже есть в документе, и потенциально новую из справочника/регистра.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| ЗаказанныеКоличества.НоменклатураКолонки КАК Номенклатура
|ИЗ
| Документ.ЗаказКлиента.ЗаказанныеКоличества КАК ЗаказанныеКоличества
|ГДЕ
| ЗаказанныеКоличества.Ссылка = &Ссылка
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ РАЗЛИЧНЫЕ
| НоменклатураКолонок.Номенклатура
|ИЗ
| Документ.ЗаказКлиента.НоменклатураКолонок КАК НоменклатураКолонок
|ГДЕ
| НоменклатураКолонок.Ссылка = &Ссылка
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ РАЗЛИЧНЫЕ
| РегСвед.Номенклатура
|ИЗ
| РегистрСведений.АктуальныйАссортимент КАК РегСвед
|ГДЕ
| ИСТИНА
|
|УПОРЯДОЧИТЬ ПО
| Номенклатура.Наименование"; // Можно сортировать по порядку из ТЧ НоменклатураКолонок
Запрос.УстановитьПараметр("Ссылка", Объект.Ссылка);
СписокДинамическихКолонок = Новый СписокЗначений; // Инициализируем реквизит формы
ВыборкаНоменклатуры = Запрос.Выполнить().Выбрать();
// Шаг 2: Создаем реквизиты формы и элементы управления
Перем нРеквизиты, нГруппа, нЭлемент;
// Получаем группу, куда будем добавлять динамические колонки.
// Предполагаем, что есть элемент формы "ТабличнаяЧастьКонтрагентыЗаказа"
ЭлементыТЧ = Элементы.ТабличнаяЧастьКонтрагентыЗаказа;
Пока ВыборкаНоменклатуры.Следующий() Цикл
// Генерируем уникальные имена реквизитов для каждой номенклатуры
ИмяРеквизитаКоличествоЯщиков = "КоличествоЯщиков_" + ВыборкаНоменклатуры.Номенклатура.УникальныйИдентификатор();
ИмяРеквизитаКоличествоШтук = "КоличествоШтук_" + ВыборкаНоменклатуры.Номенклатура.УникальныйИдентификатор();
// Добавляем информацию о колонке в СписокДинамическихКолонок, чтобы потом знать, что сохранять
СписокДинамическихКолонок.Добавить(ВыборкаНоменклатуры.Номенклатура, ИмяРеквизитаКоличествоЯщиков);
// Создаем реквизиты формы для Ящиков и Штук, привязывая их к Объекту.КонтрагентыЗаказа
нРеквизиты = Новый Массив;
нРеквизиты.Добавить(Новый РеквизитФормы(ИмяРеквизитаКоличествоЯщиков, Новый ОписаниеТипов("Число", , Новый КвалификаторыЧисла(10, 2)), "Объект.КонтрагентыЗаказа", "Ящики", Истина));
нРеквизиты.Добавить(Новый РеквизитФормы(ИмяРеквизитаКоличествоШтук, Новый ОписаниеТипов("Число", , Новый КвалификаторыЧисла(10, 0)), "Объект.КонтрагентыЗаказа", "Количество", Истина));
ИзменитьРеквизиты(нРеквизиты);
// Создаем группу колонок для этой номенклатуры (для красивого заголовка)
нГруппа = Элементы.Вставить("ГруппаКолонок_" + ВыборкаНоменклатуры.Номенклатура.УникальныйИдентификатор(), Тип("ГруппаФормы"), ЭлементыТЧ);
нГруппа.Вид = ВидГруппыФормы.ГруппаКолонок;
нГруппа.Заголовок = ВыборкаНоменклатуры.Номенклатура.Наименование;
нГруппа.ОтображатьВШапке = Истина;
нГруппа.Группировка = ГруппировкаКолонок.Горизонтальная;
нГруппа.РастягиватьПоГоризонтали = Ложь; // Важно, чтобы группа не растягивалась
// Добавляем поле для количества ящиков
нЭлемент = Элементы.Добавить(ИмяРеквизитаКоличествоЯщиков, Тип("ПолеФормы"), нГруппа);
нЭлемент.Вид = ВидПоляФормы.ПолеВвода;
нЭлемент.ПутьКДанным = "Объект.КонтрагентыЗаказа." + ИмяРеквизитаКоличествоЯщиков;
нЭлемент.Заголовок = "Ящики";
нЭлемент.Ширина = 8;
// Добавляем поле для количества штук
нЭлемент = Элементы.Добавить(ИмяРеквизитаКоличествоШтук, Тип("ПолеФормы"), нГруппа);
нЭлемент.Вид = ВидПоляФормы.ПолеВвода;
нЭлемент.ПутьКДанным = "Объект.КонтрагентыЗаказа." + ИмяРеквизитаКоличествоШтук;
нЭлемент.Заголовок = "Штуки";
нЭлемент.Ширина = 8;
КонецЦикла;
// Шаг 3: Заполняем значения реквизитов формы из сохраненных данных
// Это важно сделать, если документ не новый, чтобы отобразить ранее введенные данные.
Если НЕ Объект.ЭтоНовый Тогда
// Загружаем сохраненные количества из ТЧ ЗаказанныеКоличества
ЗапросДанных = Новый Запрос;
ЗапросДанных.Текст =
"ВЫБРАТЬ
| ЗаказанныеКоличества.УИДСтрокиОсновнойТЧ,
| ЗаказанныеКоличества.НоменклатураКолонки,
| ЗаказанныеКоличества.КоличествоЯщиков,
| ЗаказанныеКоличества.КоличествоШтук
|ИЗ
| Документ.ЗаказКлиента.ЗаказанныеКоличества КАК ЗаказанныеКоличества
|ГДЕ
| ЗаказанныеКоличества.Ссылка = &Ссылка";
ЗапросДанных.УстановитьПараметр("Ссылка", Объект.Ссылка);
ТаблицаЗаказанныхКоличеств = ЗапросДанных.Выполнить().Выгрузить();
Для Каждого СтрокаОсновнойТЧФормы Из Объект.КонтрагентыЗаказа Цикл // Итерируем по строкам основной ТЧ на форме
// Находим все записи для текущей строки основной ТЧ
СтрокиПоЭтойТЧ = ТаблицаЗаказанныхКоличеств.НайтиСтроки(Новый Структура("УИДСтрокиОсновнойТЧ", СтрокаОсновнойТЧФормы.УИДСтроки));
Для Каждого СтрокаКоличеств Из СтрокиПоЭтойТЧ Цикл
// Формируем имена динамических реквизитов
ИмяРеквизитаКоличествоЯщиков = "КоличествоЯщиков_" + СтрокаКоличеств.НоменклатураКолонки.УникальныйИдентификатор();
ИмяРеквизитаКоличествоШтук = "КоличествоШтук_" + СтрокаКоличеств.НоменклатураКолонки.УникальныйИдентификатор();
// Проверяем, существует ли такой динамический реквизит на форме и заполняем его
Если СтрокаОсновнойТЧФормы.Свойство(ИмяРеквизитаКоличествоЯщиков) Тогда
СтрокаОсновнойТЧФормы[ИмяРеквизитаКоличествоЯщиков] = СтрокаКоличеств.КоличествоЯщиков;
КонецЕсли;
Если СтрокаОсновнойТЧФормы.Свойство(ИмяРеквизитаКоличествоШтук) Тогда
СтрокаОсновнойТЧФормы[ИмяРеквизитаКоличествоШтук] = СтрокаКоличеств.КоличествоШтук;
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
При записи документа необходимо выполнить обратное преобразование: взять данные из динамически созданных реквизитов формы и записать их в наши "вертикальные" табличные части. Это должно происходить в процедуре ПередЗаписьюНаСервере или ПриЗаписиНаСервере.
Важно: как было верно отмечено в обсуждении (сообщение 7), после записи документа данные в добавленных динамических реквизитах формы могут "очищаться", поскольку объект на форме обновляется из базы данных, а динамических реквизитов в метаданных нет. Поэтому процедуру заполнения (из Шага 3 выше) необходимо будет повторно вызвать после записи или обновления данных объекта, либо предусмотреть обновление данных формы другим способом.
Для удобства хранения информации о динамически добавленных реквизитах (чтобы знать, какие колонки были на форме и какие данные из них нужно сохранять), можно использовать дополнительный реквизит формы типа СписокЗначений. В нем мы можем хранить УИДы номенклатуры (которая является колонками) и соответствующие имена динамических реквизитов формы. Мы уже добавили его в примере выше.
Пример кода из обсуждения, адаптированный для сохранения данных:
// В модуле формы документа
// Реквизит формы: СписокДинамическихКолонок, тип СписокЗначений
// Каждый элемент содержит Значение = Номенклатура (СправочникСсылка.Номенклатура)
// (Представление может быть именем реквизита, если нужно)
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ПараметрыЗаписи)
// Очищаем табличную часть с количествами перед перезаполнением,
// чтобы избежать дублирования или устаревших данных.
Объект.ЗаказанныеКоличества.Очистить();
// Также очищаем и пересоздаем табличную часть, которая хранит список колонок,
// чтобы она соответствовала текущему состоянию формы.
Объект.НоменклатураКолонок.Очистить();
Для Каждого ЭлементСпискаКолонок Из СписокДинамическихКолонок Цикл
НоваяСтрокаНоменклатураКолонок = Объект.НоменклатураКолонок.Добавить();
НоваяСтрокаНоменклатураКолонок.Номенклатура = ЭлементСпискаКолонок.Значение;
КонецЦикла;
// Итерируем по основной табличной части на форме (Объект.КонтрагентыЗаказа)
Для Каждого СтрокаОсновнойТЧФормы Из Объект.КонтрагентыЗаказа Цикл
// Итерируем по списку всех динамических колонок, которые были созданы на форме
Для Каждого ЭлементСпискаКолонок Из СписокДинамическихКолонок Цикл
// Номенклатура, к которой относится эта колонка
НоменклатураКолонки = ЭлементСпискаКолонок.Значение;
// Имена динамических реквизитов, которые были созданы на форме
ИмяРеквизитаЯщиков = "КоличествоЯщиков_" + НоменклатураКолонки.УникальныйИдентификатор();
ИмяРеквизитаШтук = "КоличествоШтук_" + НоменклатураКолонки.УникальныйИдентификатор();
Перем КоличествоЯщиков, КоличествоШтук;
// Проверяем наличие свойств и получаем значения из динамических реквизитов строки формы
Если СтрокаОсновнойТЧФормы.Свойство(ИмяРеквизитаЯщиков) Тогда
КоличествоЯщиков = СтрокаОсновнойТЧФормы[ИмяРеквизитаЯщиков];
Иначе
КоличествоЯщиков = 0;
КонецЕсли;
Если СтрокаОсновнойТЧФормы.Свойство(ИмяРеквизитаШтук) Тогда
КоличествоШтук = СтрокаОсновнойТЧФормы[ИмяРеквизитаШтук];
Иначе
КоличествоШтук = 0;
КонецЕсли;
// Если есть данные для записи (не нулевые), добавляем их в "вертикальную" ТЧ ЗаказанныеКоличества
Если КоличествоЯщиков > 0 ИЛИ КоличествоШтук > 0 Тогда
СтрокаТЧКоличеств = Объект.ЗаказанныеКоличества.Добавить();
СтрокаТЧКоличеств.УИДСтрокиОсновнойТЧ = СтрокаОсновнойТЧФормы.УИДСтроки;
СтрокаТЧКоличеств.НоменклатураКолонки = НоменклатураКолонки;
СтрокаТЧКоличеств.КоличествоЯщиков = КоличествоЯщиков;
СтрокаТЧКоличеств.КоличествоШтук = КоличествоШтук;
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
ЗаказанныеКоличества просто не будет создана. Это существенно экономит место в базе данных и ускоряет работу с данными, как было отмечено в сообщении 19.ЗаказанныеКоличества, а не в десятки динамических реквизитов на форме.Изначальный подход автора, представленный в сообщении 2, предполагал динамическое создание реквизитов непосредственно в табличной части объекта (Объект.Товары) через метод ИзменитьРеквизиты(). Этот подход позволяет визуально создать необходимые колонки на форме.
Критически важно понимать: динамически добавленные реквизиты формы, которые не соответствуют реквизитам метаданных объекта (документа), не сохраняются автоматически при записи документа в базу данных. Значения, введенные пользователем в эти поля, будут потеряны при перезагрузке формы или при записи объекта, так как объект будет обновлен из базы данных, где этих реквизитов нет (сообщение 5, 7).
Для сохранения данных из таких динамических реквизитов формы необходимо явно прописывать логику их записи в какое-либо постоянное хранилище. Таким хранилищем может быть:
ДополнительныеДанные), которая будет иметь структуру вида: УИДСтрокиОсновнойТЧ, ИмяРеквизита, ЗначениеРеквизита. В момент записи документа, мы итерируем по динамическим реквизитам формы и записываем их значения в эту дополнительную табличную часть.Документ, УИДСтроки, ИмяРеквизита) и ресурса (Значение). Тип ресурса Значение в этом случае должен быть составным, чтобы поддерживать различные типы данных (Число, Строка, Булево, Ссылка и т.д.).При открытии документа необходимо будет считать данные из этого хранилища и заполнить ими динамически созданные реквизиты формы.
Пример кода для динамического создания реквизитов (из сообщения 2, без сохранения):
// Этот код динамически добавляет реквизиты и элементы на форму.
// Он актуален для визуального отображения, но требует доработки для сохранения.
// (Предполагается, что Выборка.Номенклатура - это коллекция номенклатур, которые должны стать колонками)
Пока Выборка.Следующий() Цикл
// Здесь предполагается, что "Объект.Товары" - это основная табличная часть
// В данном случае, автор пытался добавить реквизиты прямо в нее.
// Динамически создаем реквизит формы для ссылки на номенклатуру,
// если она используется как заголовок группы или для связи.
нРеквизиты = Новый Массив;
нРеквизиты.Добавить(Новый РеквизитФормы("Номенклатура"+Выборка.Номенклатура.Код, Новый ОписаниеТипов("СправочникСсылка.Номенклатура"), "Объект.Товары", Выборка.Номенклатура, Истина));
ИзменитьРеквизиты(нРеквизиты);
// Создаем группу колонок для каждой номенклатуры
нГруппа = Элементы.Вставить("НоменклатураГруппа_"+Выборка.Номенклатура.Код, Тип("ГруппаФормы"), Элементы.Товары);
нГруппа.Вид = ВидГруппыФормы.ГруппаКолонок;
нГруппа.Заголовок = Выборка.Номенклатура;
нГруппа.ОтображатьВШапке = Истина;
нГруппа.Группировка = ГруппировкаКолонок.Горизонтальная;
// Добавляем реквизит для количества ящиков
нРеквизиты = Новый Массив;
нРеквизиты.Добавить(Новый РеквизитФормы("КоличествоЯщиков"+Выборка.Номенклатура.Код, Новый ОписаниеТипов("Число", , Новый КвалификаторыЧисла(10, 2)), "Объект.Товары", "Ящики", Истина));
ИзменитьРеквизиты(нРеквизиты);
// Добавляем элемент формы для количества ящиков
нЭлемент = Элементы.Добавить("КоличествоЯщиковЭлемент_"+Выборка.Номенклатура.Код, Тип("ПолеФормы"), нГруппа);
нЭлемент.Вид = ВидПоляФормы.ПолеВвода;
нЭлемент.ПутьКДанным = "Объект.Товары.КоличествоЯщиков"+Выборка.Номенклатура.Код;
нЭлемент.Заголовок = "Ящики";
// Добавляем реквизит для общего количества
нРеквизиты = Новый Массив;
нРеквизиты.Добавить(Новый РеквизитФормы("Количество"+Выборка.Номенклатура.Код, Новый ОписаниеТипов("Число", , Новый КвалификаторыЧисла(10, 0)), "Объект.Товары", "Количество", Истина));
ИзменитьРеквизиты(нРеквизиты);
// Добавляем элемент формы для общего количества
нЭлемент = Элементы.Добавить("КоличествоЭлемент_"+Выборка.Номенклатура.Код, Тип("ПолеФормы"), нГруппа);
нЭлемент.Вид = ВидПоляФормы.ПолеВвода;
нЭлемент.ПутьКДанным = "Объект.Товары.Количество"+Выборка.Номенклатура.Код;
нЭлемент.Заголовок = "Штуки";
КонецЦикла;
Еще одним подходом для хранения произвольных динамических данных, хотя и менее структурированным для прямого доступа, является использование реквизита документа с типом ХранилищеЗначения. Этот метод не был явно предложен в обсуждении, но является распространенной альтернативой для хранения неструктурированных данных.
В документе создается реквизит, например, ДополнительныеДанные, с типом ХранилищеЗначения. В этот реквизит можно помещать сериализованные данные, такие как ТаблицаЗначений, Массив или Структура, которые содержат всю информацию о динамических колонках и их значениях. Перед записью документа мы собираем все динамические данные в нужную структуру (например, ТаблицуЗначений) и помещаем ее в ХранилищеЗначения. При открытии документа мы извлекаем эту структуру из ХранилищаЗначения, десериализуем ее и используем для заполнения динамических реквизитов формы.
ХранилищаЗначения нельзя напрямую использовать в запросах, системе компоновки данных (СКД) или для индексирования. Для работы с ними необходимо каждый раз извлекать их, десериализовать и обрабатывать программно. Это делает аналитику и отчетность более сложной и менее производительной.ХранилищаЗначения (например, добавление нового поля в ТаблицуЗначений) необходимо предусмотреть логику миграции данных в старых документах, что может быть непросто.При выборе подхода для хранения данных динамически добавляемых реквизитов всегда анализируйте требования к аналитике, отчетам, скорости работы и сложности сопровождения. Если предполагается активная работа с данными динамических колонок в отчетах и запросах, то вертикальное хранение является предпочтительным, так как обеспечивает нормализованную структуру данных, легкую для запросов и последующей аналитики.
Если же динамические колонки используются исключительно для ввода данных и не требуют сложной аналитики, а приоритет отдается быстрой реализации на форме, то подходы с динамическими реквизитами формы или ХранилищемЗначения могут быть рассмотрены, но с учетом их существенных недостатков в части сохранения и дальнейшей работы с данными. Наш опыт показывает, что в долгосрочной перспективе более структурированное хранение данных всегда окупается.
Всегда помните, что реквизиты формы предназначены для работы с данными на форме, но их значения, не связанные с основным объектом метаданных, не сохраняются автоматически. Для их сохранения всегда требуется реализация программных методов.
В случае, описанном в теме форума, где на основании документа "Заказ" будут формироваться "Реализации", вертикальный подход с тремя табличными частями выглядит наиболее обоснованным, поскольку он позволяет легко передавать данные в другие документы и формировать по ним отчеты, не привязываясь к динамическому составу колонок на форме. Это решение также хорошо масштабируется, что было отмечено в сообщении 17 и 19.