Как правильно заполнить цены в табличной части внешней обработки из регистра сведений «Цены номенклатуры»?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Заполнение цен в табличной части документов или внешних обработок — одна из самых частых задач, с которыми сталкивается начинающий разработчик в среде «1С:Предприятие 8». На первый взгляд алгоритм кажется простым: выбрать цены из регистра и записать их в таблицу. Однако при реализации этой задачи на практике возникает множество неочевидных нюансов, синтаксических ошибок и проблем с производительностью системы.

В этой статье мы подробно разберем, почему простой код «из коробки» может не работать, проанализируем типичные ошибки разработчиков на форумах и рассмотрим профессиональные способы решения этой задачи с использованием современных стандартов разработки 1С.

Разбор типичных ошибок при заполнении табличной части

Давайте проанализируем исходный код, который часто пытаются использовать начинающие специалисты (подобный пример описан в первом сообщении темы):


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
    |	ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
    |ИЗ
    |	РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатурыСрезПоследних";

РезультатЗапроса = Запрос.Выполнить; // Ошибка 1
ТаблицаЦен = Запрос.Выполнить.Выгрузить; // Ошибка 2

Для КАЖДОГО СтрокаТЧ ИЗ Объект.Товары Цикл
    СтрокаЦен = ТаблицаЦен.Найти(СтрокаТЧ["Номенклатура"],"Номенклатура");
    СтрокаТЧ["Цена"] = СтрокаЦен["Цена"]; // Ошибка 3
КонецЦикла;

Выясним причины, почему данный код либо вообще не будет компилироваться, либо приведет к аварийному завершению работы программы.

Ошибка 1: Пропущенные скобки при вызове метода. В языке 1С метод Выполнить() объекта Запрос является функцией. Без использования круглых скобок платформа выдаст ошибку компиляции. Корректная запись выглядит так: Запрос.Выполнить().

Ошибка 2: Двойное выполнение запроса к базе данных. В строке получения таблицы цен разработчик снова вызывает метод Выполнить(). Мало того, что это приводит к повторному (совершенно лишнему) обращению к СУБД, так еще и синтаксически выгрузка результата запроса должна происходить через вызов метода: Запрос.Выполнить().Выгрузить().

Ошибка 3: Опасность падения системы (обращение к «Неопределено»). Метод таблицы значений Найти() возвращает строку таблицы только в том случае, если искомая номенклатура действительно есть в полученных ценах. Если для какого-то товара цена в регистре не задана, метод вернет значение Неопределено. В следующей строке при попытке обратиться к свойству СтрокаЦен["Цена"] система упадет с критической ошибкой: «Значение не является значением объектного типа (Цена)».

Ошибка 4: Передача коллекции управляемой формы напрямую в запрос. Часто на форумах (например, в Сообщении 3) советуют передавать табличную часть в параметры запроса напрямую: Запрос.УстановитьПараметр("Параметр", Объект.Товары);. Делать так категорически нельзя! На управляемой форме тип данных таблицы — это ДанныеФормыКоллекция, а на стороне сервера при работе с объектом — ТабличнаяЧасть. Ни один из этих типов не может быть передан в параметры запроса как массив или список значений для конструкции В (&Параметр). Подобный код вызовет ошибку выполнения.

Ошибка 5: Отсутствие отборов по измерениям регистра (Вид цены). Регистр сведений ЦеныНоменклатуры практически во всех типовых конфигурациях хранит данные не просто по номенклатуре, но и в разрезе измерений (например, «Вид цены» или «Тип цен»). Если не установить этот отбор в параметрах виртуальной таблицы СрезПоследних, запрос вернет несколько строк цен на одну и ту же позицию (розничную, оптовую, закупочную и т.д.). В результате метод Найти() выберет случайную (первую попавшуюся в индексе) цену, что приведет к некорректному заполнению данных.

---

Решение 1: Исправленный классический алгоритм (для небольших таблиц)

Если перед вами стоит задача быстро исправить текущий код в контексте простой внешней обработки, рассмотрим, как переписать его безопасно и правильно. Мы добавим необходимые отборы, выгрузим номенклатуру в массив для оптимизации запроса и сделаем проверку на пустые значения.

Разберем по шагам правильную реализацию на сервере:


&НаСервере
Процедура ЗаполнитьЦеныНаСервере()
	
	// Шаг 1. Проверяем, есть ли данные в табличной части
	Если Объект.Товары.Количество() = 0 Тогда
		Возврат;
	КонецЕсли;
	
	// Шаг 2. Выгружаем уникальные ссылки номенклатуры в массив для передачи в параметр запроса
	МассивНоменклатуры = Объект.Товары.ВыгрузитьКолонку("Номенклатура");
	
	// Шаг 3. Инициализируем запрос и устанавливаем параметры
	Запрос = Новый Запрос;
	Запрос.Текст = 
		"ВЫБРАТЬ
		|	ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
		|	ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
		|ИЗ
		|	РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
		|			&Период,
		|			Номенклатура В (&МассивНоменклатуры)
		|				И ВидЦены = &ВидЦены
		|	) КАК ЦеныНоменклатурыСрезПоследних";
	
	Запрос.УстановитьПараметр("Период", ТекущаяДата());
	Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
	Запрос.УстановитьПараметр("ВидЦены", Объект.ВидЦены); // Предполагается, что ВидЦены вынесен на форму обработки
	
	// Выполняем запрос один раз и выгружаем результат в Таблицу Значений
	ТаблицаЦен = Запрос.Выполнить().Выгрузить();
	
	// Шаг 4. Обходим ТЧ в цикле и безопасно заполняем цены
	Для Каждого СтрокаТЧ Из Объект.Товары Цикл
		
		СтрокаЦен = ТаблицаЦен.Найти(СтрокаТЧ.Номенклатура, "Номенклатура");
		
		// Обязательная проверка на Неопределено
		Если СтрокаЦен <> Неопределено Тогда
			СтрокаТЧ.Цена = СтрокаЦен.Price;
		Иначе
			СтрокаТЧ.Цена = 0; // Или оставляем прежнее значение
		EndIf;
		
	КонецЦикла;
	
КонецПроцедуры

Важное примечание по поводу обновления интерфейса: В сообщениях на форуме рекомендуют вызывать метод формы ОбновитьОтображениеДанных(). Обратите внимание, что если заполнение выполняется в серверной процедуре формы (контекстный вызов &НаСервере), платформа 1С автоматически отслеживает изменения реквизита Объект. Когда управление возвращается на клиента, данные на экране обновятся автоматически без вызова дополнительных методов.

---

Решение 2: Профессиональный способ (Best Practice) через пакетный запрос

Построчный обход табличной части в цикле с поиском через метод Найти() — не самый производительный вариант. На больших объемах данных (сотни и тысячи строк) такой алгоритм может заметно замедлить работу интерфейса.

Профессиональные разработчики используют пакетные запросы с использованием временных таблиц и левого соединения, а затем полностью перезаполняют табличную часть в один прием с помощью метода Загрузить(). Это сводит всю работу к одному быстрому запросу на уровне СУБД.

Рассмотрим этот элегантный и быстрый способ подробнее:


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

Почему этот способ является эталонным:

  1. Быстродействие: Вся логика сопоставления номенклатуры и цен перекладывается на СУБД, которая оптимизирует эту операцию на уровне индексов.
  2. Отсутствие циклов: Мы избавляемся от ресурсоемких обходов коллекций в коде на встроенном языке.
  3. Расчет связанных полей: Запрос сразу рассчитывает сумму (Количество * Цена) и обрабатывает отсутствие цены через функцию ЕСТЬNULL, что исключает ошибки падения базы.
  4. Сохранение порядка: Сортировка по полю НомерСтроки гарантирует, что порядок строк в табличной части останется неизменным.

---

Особенности новых конфигураций (ERP 2.5 / УТ 11.5 / КА 2.5)

Если вы пишете внешнюю обработку для современных версийERP, Управления торговлей или Комплексной автоматизации, важно учитывать изменения в архитектуре ценообразования.

В новых редакциях классический регистр сведений ЦеныНоменклатуры больше не используется для хранения актуальных цен номенклатуры. Вместо него внедрен новый механизм, и данные хранятся в регистре ЦеныНоменклатуры25.

Если в вашем коде указан старый регистр, запрос просто вернет пустой результат (так как регистр физически пуст), и колонки цен останутся незаполненными. Для современных конфигураций обязательно замените в тексте запроса имя таблицы регистра на РегистрСведений.ЦеныНоменклатуры25.СрезПоследних.

← На главную