Как подставить цену номенклатуры в документ из регистра сведений при изменении строки

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

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

В этой статье мы подробно разберем, как правильно организовать получение цены на сервере, исправим популярные ошибки объектного метода ПолучитьПоследнее(), научимся писать производительные запросы к виртуальным таблицам и разберем безопасные способы отбора по типу цен.

Разбор ошибки объектного метода ПолучитьПоследнее

В самом начале обсуждения была сделана попытка получить цену с помощью встроенного метода менеджера регистров сведений:


&НаСервере
Функция ПолучитьЦенуНаСервере(Номенклатура)
    ЦенаОтбор = Новый Структура;
    ЦенаОтбор.Вставить("Номенклатура", Номенклатура);
    Данные = РегистрыСведений.ЦеныНоменклатуры.ПолучитьПоследнее( , ЦенаОтбор);
    Возврат Данные.Цена;
КонецФункции

Этот код привел к ошибке выполнения: «Значение не является значением объектного типа (Цена)». Выясним причину такого поведения системы. Метод ПолучитьПоследнее() возвращает структуру, содержащую значения ресурсов регистра. Однако, если в регистре сведений нет ни одной записи, соответствующей переданному отбору (например, для выбранного товара еще не была установлена цена), метод вернет пустую структуру или значение Неопределено. Попытка обратиться к свойству Цена у пустого объекта неизбежно вызывает аварийное завершение программы.

Проанализируем, как написать этот код безопасно с использованием объектной техники:


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

Переход на запросы: синтаксис и типичные ошибки

Использование объектного метода ПолучитьПоследнее() удобно, но профессиональные стандарты разработки в 1С рекомендуют использовать запросы. Запросы работают быстрее, позволяют гибко настраивать отборы и соединять данные из нескольких таблиц. При написании запросов вручную новички часто совершают синтаксические ошибки, которые приводят к сбоям компиляции.

Рассмотрим пример ошибочного кода из обсуждения:


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

Обратим внимание на пробелы после точек: ЦеныНоменклатурыСрезПоследних. Цена и РегистрСведений. ЦеныНоменклатуры. Платформа 1С критична к таким лишним символам внутри конструкций обращений. В запросах и программном коде пробелы между точкой и именем свойства или таблицы недопустимы. Всегда используйте Конструктор запроса — это встроенный инструмент, который убережет вас от опечаток и сформирует синтаксически верный текст запроса автоматически.

Оптимизация запроса: параметры виртуальной таблицы

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


// НЕПРАВИЛЬНО И НЕПРОИЗВОДИТЕЛЬНО!
"ВЫБРАТЬ
|   ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
|   РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатурыСрезПоследних
|ГДЕ
|   ЦеныНоменклатурыСрезПоследних.Номенклатура = &Номенклатура"

Проанализируем, почему этот запрос будет работать медленно на реальной базе данных. Когда мы накладываем условие в секции ГДЕ, СУБД сначала строит виртуальную таблицу среза последних цен абсолютно по всей номенклатуре, которая есть в базе данных (это может быть миллион записей). И только после построения этой огромной таблицы в памяти система начинает отсекать строки по условию Номенклатура = &Номенклатура.

Правильный способ — передавать условия отбора непосредственно в параметры виртуальной таблицы. В этом случае СУБД сразу выберет из базы данных срез последних только по одной конкретной позиции номенклатуры:


// ПРАВИЛЬНО И ОПТИМАЛЬНО!
"ВЫБРАТЬ
|   ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
|   РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, Номенклатура = &Номенклатура) КАК ЦеныНоменклатурыСрезПоследних"

Реализация отбора по типу цен (Закупочная цена)

Часто в регистре сведений цены хранятся в разрезе различных типов (например: «Оптовая», «Розничная», «Закупочная»). Если нам нужна конкретная цена, мы обязаны передать ее тип в параметры запроса. Рассмотрим, как это сделать правильно.

Для поиска конкретного типа цен программисты иногда используют метод поиска по наименованию:

ТипЦенЗакупочная = Справочники.ТипыЦен.НайтиПоНаименованию("Закупочная");

Использование поиска по наименованию в коде — это "антипаттерн" (плохая практика). Если пользователь в режиме «1С:Предприятие» решит отредактировать название типа цен (например, исправит на «Закупочная базовая» или случайно поставит лишний пробел в конце), ваш код перестанет находить элемент, и цена в документах перестанет заполняться. Разберем более надежные альтернативы:

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

Итоговый рабочий вариант решения задачи

Объединим все правила и напишем готовый рабочий механизм автоматической подстановки цены в табличную часть документа при изменении номенклатуры.

Разработаем обработчик события изменения номенклатуры на клиенте:


&НаКлиенте
Процедура ТабличнаяЧасть1НоменклатураПриИзменении(Элемент)
    
    // Получаем текущую строку табличной части
    ТекДанные = Элементы.ТабличнаяЧасть1.ТекущиеДанные;
    
    Если ТекДанные <> Неопределено Тогда
        // Проверяем, заполнена ли номенклатура
        Если ЗначениеЗаполнено(ТекДанные.Номенклатура) Тогда
            // Передаем дату документа и номенклатуру на сервер для получения цены
            ТекДанные.Цена = ПолучитьЦенуНоменклатуры(ТекДанные.Номенклатура, Объект.Дата);
            
            // Рассчитываем сумму по строке
            ТекДанные.Сумма = ТекДанные.Цена * ТекДанные.Количество;
        Иначе
            ТекДанные.Цена = 0;
            ТекДанные.Сумма = 0;
        КонецЕсли;
    КонецЕсли;
    
КонецПроцедуры

Теперь реализуем эффективную серверную функцию для работы с запросом:


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

Продвинутый уровень: Опасность запросов в цикле

Рассмотренный выше алгоритм идеально подходит для интерактивного изменения строк пользователем (когда строки меняются по одной). Однако, если вам потребуется заполнить или пересчитать цены для всей табличной части сразу (например, при нажатии кнопки «Заполнить цены» или при изменении типа цен в шапке документа), ни в коем случае нельзя вызывать функцию получения цены в цикле для каждой строки.

Вызов серверного запроса в цикле по строкам табличной части порождает огромные задержки из-за многократных клиент-серверных переходов и перегружает систему СУБД. В таких ситуациях необходимо получать все цены одним запросом. Проанализируем правильный паттерн массового заполнения:


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

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

← На главную