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