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