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