Как автоматически подгрузить цену из справочника в документ при выборе услуги в 1С?

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

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

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

Анализ ошибки: Разница между «ЭтотОбъект» и «Объект»

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


&НаКлиенте
Процедура УслугаПриИзменении(Элемент)
    Если Не ЭтотОбъект.Услуга.ПустаяСсылка Тогда
         // Ошибка возникает здесь
         ЭтотОбъект.Стоимость = ПолучитьСтоимостьУслуги(ЭтотОбъект.Услуга);
    КонецЕсли;
КонецПроцедуры

При выполнении этого кода система выдает ошибку: «Поле объекта не обнаружено (Услуга)». Давайте разберем, почему это происходит.

В управляемых формах (которые используются в 1С 8.2, 8.3 и выше) существует строгое разделение контекстов:

  1. Модуль объекта (на сервере): здесь переменная ЭтотОбъект действительно содержит сам документ и доступ к его реквизитам.
  2. Модуль формы (на клиенте и сервере): здесь мы работаем с формой.

В модуле формы (в процедурах с директивой &НаКлиенте) переменная ЭтотОбъект (или просто ЭтаФорма) ссылается на программный объект УправляемаяФорма. У самой формы нет реквизитов документа (таких как «Услуга» или «Стоимость») напрямую в свойствах. Данные документа хранятся в основном реквизите формы, который по умолчанию называется Объект.

Следовательно, чтобы обратиться к данным редактируемого документа из клиентского кода формы, нам нужно обращаться через Объект.

Правильная реализация клиентской части

Исправим обращение к данным. Вместо ЭтотОбъект.Услуга будем использовать Объект.Услуга. То же самое касается и присвоения результата: писать нужно в Объект.Стоимость.

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

Скорректируем код процедуры:


&НаКлиенте
Процедура УслугаПриИзменении(Элемент)
    // Передаем текущее значение услуги в функцию получения цены
    Объект.Стоимость = ПолучитьСтоимостьУслуги(Объект.Услуга);
КонецПроцедуры

Теперь рассмотрим, как должна выглядеть функция получения стоимости.

Реализация серверной функции: Оптимизация и контекст

Получить значение реквизита ссылочного типа (например, узнать цену по ссылке на услугу) можно только на сервере. На клиенте ссылка — это просто идентификатор (GUID) и представление, данных внутри нее «не видно».

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

  1. &НаСервере — передает на сервер контекст всей формы (все данные, таблицы и т.д.). Это «тяжелый» вызов.
  2. &НаСервереБезКонтекста — не передает форму на сервер. Мы передаем только конкретные параметры и получаем результат. Это работает гораздо быстрее и меньше нагружает сеть.

Так как для получения цены нам нужна только ссылка на услугу, а результат — число, идеальным выбором будет &НаСервереБезКонтекста.

Ошибочный подход: Поиск по ссылке

Иногда разработчики пишут так:

Стоимость = Справочники.Услуги.НайтиПоСсылке(Услуга).Стоимость;

В этом нет смысла. Переменная Услуга — это уже ссылка. Искать ссылку по ссылке не нужно. Система и так знает, какой это элемент.

Простой подход: Обращение через точку

Самый простой способ получить данные — обратиться к реквизиту через точку от ссылки:


&НаСервереБезКонтекста
Функция ПолучитьСтоимостьУслуги(Услуга)
    
    // Проверяем, выбрана ли услуга вообще
    Если ЗначениеЗаполнено(Услуга) Тогда
        // Обращение к базе данных происходит здесь неявно
        Возврат Услуга.Стоимость;
    Иначе
        Возврат 0;
    КонецЕсли;
    
КонецФункции

Важное примечание про производительность: Когда мы пишем Услуга.Стоимость, платформа неявно считывает объект справочника из базы данных. Если справочник «тяжелый» (много реквизитов, табличные части, хранилища значений), это может быть неоптимально, так как считывается весь объект ради одного числа. В высоконагруженных системах лучше использовать Запрос.

Профессиональный подход: Использование Запроса

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


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

КонецФункции

Защита от ошибок типов данных

В обсуждении справедливо заметили: «А что если тип реквизита Услуга станет составным?». Например, в документ можно будет выбрать не только «Услугу», но и «Товар», у которого нет реквизита «Стоимость», а есть «Цена».

Если мы оставим простое обращение через точку Услуга.Стоимость, и в поле придет ссылка на справочник «Товары», код вызовет исключение (ошибку), так как поля «Стоимость» у товара нет.

Чтобы защитить код, можно использовать проверку типов или попытку получения значения, но наиболее надежным (и архитектурно верным) способом является проверка типа перед запросом или использование функции БСП (Библиотеки Стандартных Подсистем) ОбщегоНазначения.ЗначениеРеквизитаОбъекта(), если она внедрена в вашу конфигурацию.

Если писать код вручную с защитой от смены типа, можно добавить проверку:


Если ТипЗнч(Услуга) <> Тип("СправочникСсылка.Услуги") Тогда
    // Если это не услуга, возвращаем 0 или обрабатываем иначе
    Возврат 0;
КонецЕсли;

Итоговое решение

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


&НаКлиенте
Процедура УслугаПриИзменении(Элемент)
    // 1. Обращаемся к данным через Объект
    // 2. Вызываем функцию без контекста
    Объект.Стоимость = ПолучитьСтоимостьУслуги(Объект.Услуга);
КонецПроцедуры

&НаСервереБезКонтекста
Функция ПолучитьСтоимостьУслуги(ТекущаяУслуга)
    
    // 3. Проверяем заполненность (защита от пустой ссылки)
    Если Не ЗначениеЗаполнено(ТекущаяУслуга) Тогда
        Возврат 0;
    КонецЕсли;
    
    // 4. Получаем данные. 
    // Для простых задач допустимо обращение через точку (если справочник легкий)
    // Для оптимизации используйте Запрос (см. выше)
    Возврат ТекущаяУслуга.Стоимость;
    
КонецФункции

Дополнительная рекомендация по архитектуре

Хотя хранение цены в реквизите справочника допустимо для учебных задач или очень простых систем, в реальном учете это считается плохой практикой. Причина проста: цены меняются.

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

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

← На главную