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