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