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