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