При разработке сложных учетных механизмов в платформе «1С:Предприятие 8» (например, калькуляций, планов продаж или документов распределения затрат) разработчики часто сталкиваются с необходимостью группировки данных. Первое очевидное решение, которое приходит в голову — хранить ссылки на группы (папки) справочника непосредственно в строках табличной части (ТЧ) документа. Однако при реализации этого механизма на практике нередко возникают странные ошибки: данные визуально пропадают, не записываются или записываются некорректно.
В этой статье мы подробно разберем, может ли группа справочника физически храниться в табличной части, как правильно настроить свойства метаданных, проанализируем типичную ошибку «исчезновения» строк при записи и рассмотрим лучшие практики по выводу плоских таблиц документов в виде красивых иерархических деревьев.
С технической точки зрения (на уровне таблиц СУБД) ссылка на группу справочника и ссылка на элемент имеют абсолютно одинаковый тип данных — СправочникСсылка.<ИмяСправочника>. Обе они представляют собой уникальный 128-битный идентификатор (UUID). Платформа «1С:Предприятие» без каких-либо ограничений позволяет записывать ссылки на группы в любые реквизиты табличных частей. База данных успешно сохранит эти данные.
Однако то, как ведет себя интерфейс программы при попытке выбрать группу, регулируется специальным свойством реквизита.
Проанализируем свойство реквизита метаданных ВыборГруппИЭлементов (в англоязычной версии — ChoiceFoldersAndItems). Этот параметр определяет поведение платформы при интерактивной работе пользователя:
Важный нюанс: Ограничение свойства ВыборГруппИЭлементов действует только на интерактивные действия пользователя (ввод на сервере/клиенте через форму выбора, автоподбор). Если мы выполняем заполнение табличной части программно, платформа полностью игнорирует это свойство!
Рассмотрим пример кода:
НоваяСтрока = Объект.Изделия.Добавить();
НоваяСтрока.Номенклатура = ТекущийЭлементНоменклатуры;
// Программная запись группы в реквизит ТЧ
НоваяСтрока.Группа = ТекущийЭлементНоменклатуры.Родитель;
Этот код отработает без ошибок даже в том случае, если для реквизита Группа в конфигураторе жестко задано значение «Только элементы». Ошибок записи в базу данных не возникнет, пока на форму или в модуль объекта не будут добавлены кастомные проверки заполнения.
Рассмотрим ситуацию, с которой часто сталкиваются разработчики: код заполнения написан, в процессе выполнения строки ТЧ визуально заполняются, но после вызова метода Записать() табличная часть кажется абсолютно пустой.
Выясним причину этой проблемы. Как показала практика, дело кроется в банальной невнимательности и усталости программиста. Разберем ошибочный алгоритм заполнения:
Процедура ЗаполнитьПрограммно()
// Разработчик забыл вызвать очистку коллекции:
// Объект.Изделия.Очистить();
Для Каждого СтрДанных Из ИсходнаяТаблица Цикл
НоваяСтрока = Объект.Изделия.Добавить();
НоваяСтрока.Номенклатура = СтрДанных.Номенклатура;
НоваяСтрока.Группа = СтрДанных.Номенклатура.Родитель;
КонецЦикла;
КонецПроцедуры
Если перед вызовом этого алгоритма табличная часть документа уже содержала пустые или старые строки (например, созданные интерактивно при открытии формы), новые строки с заполненными группами будут добавлены в самый конец табличной части. При открытии формы пользователь видит первые строки таблицы, которые остались пустыми, и делает ложный вывод, что данные «исчезли» при записи. Достаточно прокрутить таблицу до конца, чтобы обнаружить все успешно записанные данные.
Правило разработки: Всегда перед программным циклом заполнения табличных частей вызывайте метод Очистить() для соответствующей коллекции.
Обратите внимание на строку НоваяСтрока.Группа = НоваяСтрока.Номенклатура.Родитель; в цикле. Обращение к реквизитам через точку от ссылочного типа (в данном случае получение родителя номенклатуры) приводит к неявному обращению к базе данных на каждой итерации цикла. Если в документе рассчитываются тысячи строк номенклатуры, система сделает тысячи отдельных запросов к СУБД, что вызовет жесткое падение производительности.
Разберем по шагам, как правильно оптимизировать этот код с помощью запроса и кэширования данных в памяти:
Процедура ЗаполнитьТЧОптимизированно(МассивНоменклатуры)
// Обязательно очищаем ТЧ перед заполнением
Объект.Изделия.Очистить();
// Получаем всех родителей одним запросом
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Номенклатура,
| Номенклатура.Родитель КАК Родитель
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка В(&МассивНоменклатуры)";
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
ТаблицаРодителей = Запрос.Выполнить().Выгрузить();
// Создаем быстрое соответствие в оперативной памяти
СоответствиеРодителей = Новый Соответствие;
Для Каждого Стр Из ТаблицаРодителей Цикл
СоответствиеРодителей.Вставить(Стр.Номенклатура, Стр.Родитель);
КонецЦикла;
// Быстро заполняем строки ТЧ без обращений к БД в цикле
Для Каждого ТекНоменклатура Из МассивНоменклатуры Цикл
НоваяСтрока = Объект.Изделия.Добавить();
НоваяСтрока.Номенклатура = ТекНоменклатура;
РодительНоменклатуры = СоответствиеРодителей.Получить(ТекНоменклатура);
Если РодительНоменклатуры <> Неопределено Тогда
НоваяСтрока.Группа = РодительНоменклатуры;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Хранить группы в плоской таблице — это лишь половина дела. Для удобства пользователей интерфейс расчета себестоимости или калькуляции должен быть представлен в виде визуального дерева. Физически табличная часть документа в 1С всегда плоская. Напрямую отобразить её иерархически стандартными средствами платформы нельзя. Рассмотрим два основных способа решения этой задачи.
Этот паттерн проектирования позволяет хранить и отображать в документе структуру любой вложенности. Для этого в табличную часть добавляются служебные реквизиты связи.
Проанализируем структуру реквизитов ТЧ:
КлючСтроки (тип: Число или УникальныйИдентификатор) — уникальный маркер текущей строки.КлючСвязи (тип: Число или УникальныйИдентификатор) — указывает на КлючСтроки родителя.На форму документа вместо стандартной таблицы выводится элемент управления с типом ДеревоЗначений (например, реквизит формы с именем ДеревоДанныхФормы). При открытии формы плоская ТЧ считывается и программно преобразуется в дерево. Перед записью документа выполняется обратная операция.
Рассмотрим программный код формирования дерева на форме при чтении документа:
&НаСервере
Процедура СформироватьДеревоНаФорме()
// Преобразуем реквизит формы в объект ДеревоЗначений
Дерево = РеквизитВФормуЗначение("ДеревоДанныхФормы");
Дерево.Строки.Очистить();
ТаблицаТЧ = Объект.Изделия.Выгрузить();
// Сначала находим все корневые строки (у которых КлючСвязи пустой или равен 0)
КорневыеСтроки = ТаблицаТЧ.НайтиСтроки(Новый Структура("КлючСвязи", 0));
Для Каждого СтрТЧ Из КорневыеСтроки Цикл
НоваяВетка = Дерево.Строки.Добавить();
ЗаполнитьЗначенияСвойств(НоваяВетка, СтрТЧ);
// Рекурсивно заполняем подчиненные ветки дерева
ЗаполнитьПодчиненныеВетки(НоваяВетка, ТаблицаТЧ);
КонецЦикла;
// Возвращаем дерево обратно в реквизит управляемой формы
ЗначениеВРеквизитФормы(Дерево, "ДеревоДанныхФормы");
КонецПроцедуры
&НаСервере
Процедура ЗаполнитьПодчиненныеВетки(РодительскаяСтрокаДерева, ТаблицаТЧ)
ПараметрыОтбора = Новый Структура("КлючСвязи", РодительскаяСтрокаДерева.КлючСтроки);
ПодчиненныеСтроки = ТаблицаТЧ.НайтиСтроки(ПараметрыОтбора);
Для Каждого СтрТЧ Из ПодчиненныеСтроки Цикл
НоваяСтрокаДерева = РодительскаяСтрокаДерева.Строки.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаДерева, СтрТЧ);
// Рекурсивный спуск на следующий уровень иерархии
ЗаполнитьПодчиненныеВетки(НоваяСтрокаДерева, ТаблицаТЧ);
КонецЦикла;
КонецПроцедуры
Перед непосредственной записью документа на сервере нам необходимо выполнить обратную операцию — «выпрямить» иерархическое дерево обратно в плоскую структуру ТЧ документа. Для этого мы рекурсивно обходим все строки дерева значений и выгружаем их в табличную часть.
&НаСервере
Процедура ВыгрузитьДеревоВТабличнуюЧасть()
Объект.Изделия.Очистить();
Дерево = РеквизитВФормуЗначение("ДеревоДанныхФормы");
ОбходДереваДляЗаписи(Дерево.Строки);
КонецПроцедуры
&НаСервере
Процедура ОбходДереваДляЗаписи(КоллекцияСтрок)
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
НоваяСтрокаТЧ = Объект.Изделия.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаТЧ, СтрокаДерева);
// Если у текущей строки дерева есть подчиненные элементы
Если СтрокаДерева.Строки.Количество() > 0 Тогда
ОбходДереваДляЗаписи(СтрокаДерева.Строки);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Если иерархия строк документа жестко привязана к структуре самого справочника, то хранить группы справочника в документе вообще не требуется. В базе данных сохраняются только плоские строки с номенклатурой. При открытии формы система динамически определяет родительские папки для каждой номенклатуры, строит виртуальные узлы в дереве значений на форме и распределяет по ним элементы.
Этот подход гарантирует чистоту информационной базы, отсутствие избыточных данных и исключает рассинхронизацию структуры данных при возможном перемещении элементов справочника между папками в будущем.
Мы выяснили, что платформа «1С:Предприятие» полностью поддерживает хранение групп справочников в табличных частях. При разработке таких алгоритмов всегда помните о своевременной очистке коллекций перед программным заполнением и избегайте обращений «через точку» в циклах к родительским реквизитам. Использование деревьев значений на управляемых формах позволит вам выводить плоские структуры документов в максимально удобном для пользователей виде.