Как получить последнюю цену номенклатуры для расчета наценки в 1С:Управление торговлей

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

В процессе разработки в среде 1С:Предприятие 8.3 часто возникает задача автоматического расчета показателей на основе цен. Типичный пример — расчет процента наценки в документе при изменении цены продажи. Для этого нам необходимо оперативно и правильно «вытащить» последнюю цену поступления или закупочную цену из регистра сведений. В этой статье мы подробно разберем, как это сделать эффективно, избегая типичных ошибок вроде «деления на ноль» или неверной выборки из-за учета серий.

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

Рассмотрим ситуацию: разработчик пытается получить цену в цикле, обращаясь к серверной функции для каждой строки табличной части. Это классическая ошибка производительности. Проанализируем, почему исходный подход может не работать или работать медленно:

  1. Запросы в цикле: Вызов функции ЦенаПоступления(Номенклатура) внутри цикла по товарам создает избыточную нагрузку на сервер БД.
  2. Деление на ноль: Если цена в регистре не найдена, запрос возвращает пустоту или 0, что приводит к критической ошибке при расчете формулы наценки.
  3. Контекст формы: При работе на управляемых формах данные Объект.Товары не всегда доступны для методов таблиц (например, ВыгрузитьКолонку) напрямую без преобразования в прикладной объект.
  4. Учет серий: Если в конфигурации (например, УТ 11) используется серийный учет, запрос может вернуть цену, привязанную к конкретной серии, в то время как нам нужна общая цена номенклатуры.

Шаг 1. Оптимизация получения данных через массив

Вместо того чтобы «мучить» базу данных отдельными запросами, мы подготовим массив всей номенклатуры из документа и получим все цены одним пакетом. Выясним, как правильно передать данные на сервер. Рассмотрим исправленный механизм подготовки данных в процедуре Расш7_ТоварыЦенаПриИзмененииПослеНаСервере.

Для корректной работы с методами таблицы значений на сервере нам необходимо преобразовать данные формы в объект. Воспользуемся методом РеквизитФормыВЗначение.


&НаСервере
Процедура Расш7_ТоварыЦенаПриИзмененииПослеНаСервере()
    // Преобразуем данные формы в объект для использования метода ВыгрузитьКолонку()
    ТекОбъект = РеквизитФормыВЗначение("Объект");
    
    // Собираем все ссылки на номенклатуру из табличной части в массив
    МассивНоменклатуры = ТекОбъект.Товары.ВыгрузитьКолонку("Номенклатура");
    
    // Получаем таблицу цен одним запросом
    ТаблицаЦен = ПолучитьТаблицуЦенПоступления(МассивНоменклатуры, Объект.Дата);
    
    // Перебираем строки и выполняем расчет
    Для Каждого СтрокаТовары Из ТекОбъект.Товары Цикл
        // Ищем цену в полученной таблице
        СтрокаЦены = ТаблицаЦен.Найти(СтрокаТовары.Номенклатура, "Номенклатура");
        
        Если СтрокаЦены <> Неопределено И СтрокаЦены.Цена > 0 Тогда
            ЦенаЗакупа = СтрокаЦены.Цена;
            // Пример расчета наценки с учетом НДС (функция РасчетСтавки — пользовательская)
            СтавкаКоэффициент = РасчетСтавки(СтрокаТовары.СтавкаНДС);
            ЦенаБезНДС = ЦенаЗакупа * СтавкаКоэффициент;
            
            СтрокаТовары.Процент = ((СтрокаТовары.Цена - ЦенаБезНДС) / ЦенаБезНДС) * 100;
        Иначе
            // Обязательно обрабатываем ситуацию, когда цена не найдена, чтобы избежать деления на 0
            СтрокаТовары.Процент = 0;
        КонецЕсли;
    КонецЦикла;
    
    // Возвращаем измененные данные в реквизит формы
    ЗначениеВРеквизитФормы(ТекОбъект, "Объект");
КонецПроцедуры

Шаг 2. Правильный запрос к срезу последних с фильтрацией серий

Разберем детально, как должен выглядеть запрос к виртуальной таблице РегистрСведений.ЦеныНоменклатуры.SrezPoslednih. Нам важно не просто получить цену, но и убедиться, что мы берем «базовую» цену без привязки к конкретным сериям (если это предусмотрено спецификой задачи), а также правильно сгруппировать результат.

Проанализируем структуру запроса. Мы используем параметры виртуальной таблицы для фильтрации по виду цены и списку номенклатуры. Также добавим условие на пустую серию, чтобы не получить случайную цену из какой-то конкретной поставки.


&НаСервере
Функция ПолучитьТаблицуЦенПоступления(МассивНоменклатуры, ДатаСреза)
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
    |	ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
    |ИЗ
    |	РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
    |		&ДатаСреза,
    |		ВидЦены = &ВидЦены
    |			И Номенклатура В (&МассивНоменклатуры)
    |			И УТР_Серия = ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)
    |	) КАК ЦеныНоменклатурыСрезПоследних
    |ГДЕ
    |	ЦеныНоменклатурыСрезПоследних.Цена > 0";

    // Указываем конкретный вид цены. В реальной задаче лучше передавать его как параметр или настройку.
    ВидЦеныЗакуп = Справочники.ВидыЦен.НайтиПоНаименованию("Цена приобретения");
    
    Запрос.УстановитьПараметр("ДатаСреза", ДатаСреза);
    Запрос.УстановитьПараметр("ВидЦены", ВидЦеныЗакуп);
    Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
    
    Возврат Запрос.Выполнить().Выгрузить();
КонецФункции

Шаг 3. Обработка ошибок «Поле не входит в группу» и NULL

Посмотрим на частую синтаксическую ошибку в запросах с группировкой. Если вы используете агрегатные функции (например, МАКСИМУМ(Период)), все остальные поля обязаны быть в секции СГРУППИРОВАТЬ ПО. Однако, при работе с виртуальной таблицей СрезПоследних, группировка часто не требуется, так как система сама возвращает последнюю запись на указанный момент времени для каждой комбинации измерений.

Важный нюанс про NULL: Многие начинающие разработчики используют функцию ЕСТЬNULL в запросах к виртуальным таблицам. Помните: если записи в регистре нет, виртуальная таблица СрезПоследних просто не вернет строку для этой номенклатуры. ЕСТЬNULL имеет смысл только в случае ЛЕВОГО СОЕДИНЕНИЯ вашей таблицы товаров с регистром цен. В нашем примере выше мы используем метод таблицы значений Найти(), который вернет Неопределено, если строки нет — это и есть наша защита.

Шаг 4. Учет серий и характеристик

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

Для получения «общей» цены мы явно задали условие:

И УТР_Серия = ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)

Это отсекает партионные цены и возвращает только ту, которая установлена для номенклатуры в целом.

Рекомендации для профессиональной разработки

Рассмотрим альтернативный, более «правильный» путь для типовых конфигураций (УТ 11, ERP, КА). Вместо написания прямых запросов к регистрам, рекомендуется использовать встроенные механизмы:

Таким образом, мы проанализировали ситуацию, исправили неэффективный код с запросом в цикле, решили проблему с контекстом управляемой формы и добавили фильтрацию по сериям. Использование пакетной обработки данных (через массив и таблицу значений) — это наиболее стабильный и производительный метод решения подобных задач.

← На главную