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