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