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