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

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

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

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

Давайте проведем совместный аудит исходного кода и пошагово исправим все недочеты.

Анализ классических ошибок начинающего разработчика

Рассмотрим код, который изначально предложил автор темы:


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

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

  1. Грубейшая ошибка производительности (отсутствие параметров виртуальной таблицы): В запросе автора полностью отсутствуют параметры у виртуальной таблицы СрезПоследних. Это означает, что при выполнении запроса СУБД сначала выберет последние цены абсолютно по всей номенклатуре и по всем типам цен, которые есть в вашей базе данных, загрузит этот гигантский массив в оперативную память и только потом (если бы стояло условие ГДЕ) начала бы отсекать лишнее. На реальной базе с тысячами товаров этот код заблокирует работу пользователей. Фильтрацию в виртуальных таблицах нужно делать исключительно внутри параметров самой таблицы!
  2. Передача менеджера справочника вместо ссылки: Строка кода Запрос.УстановитьПараметр("ТипЦен", Справочники.ТипЦен) передает в запрос объект метаданных — менеджер справочника, а не конкретный элемент (например, Оптовая или Розничная цена). База данных физически не может сравнить поле регистра (которое содержит ссылку СправочникСсылка.ТипыЦенНоменклатуры) с системным менеджером. В результате мы получим ошибку выполнения запроса. Нам нужно передавать конкретную ссылку, выбранную в документе.
  3. Не учитывается фактор времени (Дата документа): Цены в компании постоянно меняются. Если мы не передаем в срез последних дату документа, система вернет цену, актуальную на текущую секунду времени. Если бухгалтер решит провести документ вчерашней или прошломесячной датой, подставится сегодняшняя (неверная) цена. Нам обязательно нужно передавать дату документа в качестве параметра Период.
  4. Неправильная обработка результата запроса: Метод Выгрузить() возвращает объект типа ТаблицаЗначений. Попытка сравнить таблицу с числом Если ЦеныНоменклатуры <> 0 Тогда вызовет ошибку несовместимости типов. Более того, у самой таблицы значений нет свойства Цена (это имя колонки, а не свойство таблицы). Для получения одиночного значения выгружать данные в таблицу неэффективно — гораздо быстрее и правильнее использовать выборку.

Шаг 1. Пишем правильную серверную функцию получения цены

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

Посмотрим на пример правильной реализации на сервере:


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

Обратите внимание на конструкцию РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, Номенклатура = &Номенклатура И ТипЦен = &ТипЦен). Именно такое написание гарантирует максимальную производительность. СУБД выберет из базы данных только одну единственную запись, соответствующую нашим условиям.

Шаг 2. Реализуем вызов на клиенте при изменении товара

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

Выясним, как правильно написать этот обработчик на клиенте:


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

Шаг 3. Обрабатываем изменение типа цен в шапке документа

Очень часто разработчики забывают про важнейший пользовательский сценарий: что произойдет, если в табличной части уже заполнено 10 товаров, а пользователь решит изменить «Тип цен» в шапке документа?

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

Разберем по шагам, как это реализовать:


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

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

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

Важное замечание для работы в типовых конфигурациях

Если вы разрабатываете функционал внутри современной типовой конфигурации (например, 1С:Управление торговлей 11, 1С:ERP, 1С:Комплексная автоматизация 2 или 1С:УНФ), писать собственные запросы к регистру цен напрямую крайне не рекомендуется.

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

← На главную