Как программно заполнить колонку с дополнительными ценами в табличной части на управляемой форме 1С

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет IT и автоматизация бизнеса
← На главную

В процессе разработки в среде «1С:Предприятие 8» на управляемых формах часто возникает задача динамического заполнения дополнительных колонок в табличных частях. Например, в конфигурации «1С:Управление торговлей 11» при работе с документом Заказ поставщику менеджеру может потребоваться сопоставить текущие цены поставщика с розничными, оптовыми или какими-либо другими альтернативными ценами. Для этого на форму выводят реквизит выбора вида цены и добавляют колонку для вывода цены сравнения в табличную часть Товары.

Казалось бы, задача простая: сделать запрос к регистру сведений и заполнить колонку. Однако начинающие разработчики регулярно совершают архитектурные ошибки, которые приводят либо к отсутствию изменений на экране, либо к фатальной ошибке блокировки данных («Данные были изменены или удалены другим пользователем»). В этой статье мы подробно разберем, почему возникают эти проблемы, проанализируем неверные подходы и напишем правильное, оптимизированное решение.

Разбор типичных ошибок разработчиков

Прежде чем перейти к правильному коду, крайне важно понять, почему «интуитивные» решения часто приводят к неработоспособности системы. Рассмотрим три главные ошибки, которые совершил автор темы на форуме.

Ошибка №1: Изменение данных через ПолучитьОбъект() в контексте формы

Посмотрим на фрагмент кода, который пытался использовать автор:


ОбъектЗаказПоставщику = Объект.Ссылка.ПолучитьОбъект();
НайденноеЗначение = ОбъектЗаказПоставщику["Товары"].НайтиСтроки(Отбор);
// ... заполнение цен ...
// ОбъектЗаказПоставщику.Записать();

Вызов метода ПолучитьОбъект() считывает данные напрямую из базы данных и создает в оперативной памяти сервера новый, независимый экземпляр объекта. Когда вы меняете его табличную часть, эти изменения происходят в данном изолированном экземпляре. Наша форма (на которой работает пользователь) связана с реквизитом Объект типа ДанныеФормыСтруктура. Изменения в изолированном объекте СУБД никак не отражаются на элементах формы, поэтому пользователь на экране ничего не видит.

Если же программист решает «дожать» ситуацию и вызывает метод Записать() у этого полученного объекта, то данные записываются в СУБД. При этом у документа меняется версия данных. Когда пользователь на форме нажимает кнопку «Записать», платформа 1С сравнивает версию данных формы с версией данных в БД. Поскольку в базе версия уже обновилась из-за фоновой записи, платформа блокирует операцию и выдает ошибку транзакции. Правило: в модуле управляемой формы никогда не используйте метод ПолучитьОбъект() для изменения текущих реквизитов формы. Работать нужно только напрямую с коллекцией Объект.Товары.

Ошибка №2: Запрос к табличной части документа из базы данных

В попытках составить запрос к товарам документа автор пишет:


ИЗ
    Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
ГДЕ
    ЗаказПоставщикуТовары.Ссылка = &Ссылка

Такой запрос обращается к таблицам СУБД. Это означает, что:

  1. Если документ новый и еще ни разу не записывался, запрос вернет пустой результат (документа в СУБД еще нет).
  2. Если пользователь изменил количество, добавил новые строки или удалил старые в режиме редактирования (но еще не нажал кнопку сохранения), запрос вернет старые, неактуальные данные из базы, проигнорировав изменения на экране.

Правило: данные для анализа необходимо забирать непосредственно из текущей экранной коллекции формы, выгружая ее в таблицу значений и передавая в запрос как параметр.

Ошибка №3: Ошибки логики SQL и сравнения с NULL

Попытка написать условие Цена <> NULL в параметрах виртуальной таблицы среза последних или в секции ГДЕ ошибочна по своей сути. В языке запросов 1С (как и в стандартном SQL) любое сравнение с NULL через стандартные операторы = или <> всегда возвращает ложь. Проверка на пустое значение должна выполняться только с помощью оператора ЕСТЬ NULL (например, НЕ Цена ЕСТЬ NULL). К тому же, в физической таблице регистра сведений цен значений NULL быть не может — они возникают лишь в результате левого соединения таблиц.

---

Пошаговый алгоритм правильной реализации

Для реализации задачи нам необходимо выполнить следующие действия:

  1. Перехватить событие изменения реквизита дополнительного вида цен на клиенте.
  2. Передать управление на сервер, передав контекст формы.
  3. Выгрузить текущие строки табличной части во временную таблицу значений, чтобы иметь доступ к несохраненным данным.
  4. Свернуть таблицу для удаления дубликатов номенклатуры и характеристик, снизив нагрузку на СУБД.
  5. Выполнить запрос к регистру сведений ЦеныНоменклатуры (или ЦеныНоменклатурыПоставщиков в зависимости от бизнес-задачи), соединив его с нашей таблицей товаров.
  6. Обойти результат запроса и обновить значения непосредственно в коллекции управляемой формы Объект.Товары с помощью эффективного метода НайтиСтроки().

Рассмотрим реализацию подробнее.

Шаг 1. Создание обработчиков на клиенте и сервере

На форме документа для реквизита, содержащего дополнительный вид цены (например, ДополнительныйВидЦен с типом СправочникСсылка.ВидыЦен), создаем клиентский обработчик события ПриИзменении. Из него мы будем вызывать серверную процедуру без контекстной передачи всего документа без необходимости, но с возможностью прямого изменения данных формы.


&НаКлиенте
Процедура ДополнительныйВидЦенПриИзменении(Элемент)
    
    // Вызываем серверную процедуру для заполнения цен сравнения
    ЗаполнитьЦеныСравненияНаСервере();
    
КонецПроцедуры

Шаг 2. Реализация серверной логики получения и заполнения данных

Ниже представлен детальный программный код серверной процедуры. Проанализируем каждый шаг работы с данными.


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

Важные архитектурные нюансы решения

Давайте проанализируем, почему этот код работает быстро и не вызывает ошибок блокировок данных:

Альтернативный вариант: Использование цен поставщиков

Если перед вами стоит задача сравнивать цены не со своими собственными продажными ценами (прайс-листом компании), а с ценами поставщиков, то структура хранения данных будет немного иной. Цены контрагентов хранятся в регистре сведений ЦеныНоменклатурыПоставщиков, а аналитика ведется в разрезе справочника ВидыЦенПоставщиков.

В таком случае реквизит на форме должен иметь тип ссылки на справочник ВидыЦенПоставщиков, а левое соединение в запросе будет выглядеть следующим образом:


ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатурыПоставщиков.СрезПоследних(
    &Период,
    ВидЦеныПоставщика = &ВидЦеныПоставщика
        И (Номенклатура, Характеристика) В
            (ВЫБРАТЬ
                Т.Номенклатура,
                Т.Характеристика
            ИЗ
                ВТ_Товары КАК Т)
) КАК ЦеныНоменклатурыПоставщиковСрезПоследних
ПО ВТ_Товары.Номенклатура = ЦеныНоменклатурыПоставщиковСрезПоследних.Номенклатура
    И ВТ_Товары.Характеристика = ЦеныНоменклатурыПоставщиковСрезПоследних.Характеристика

Применяя данные подходы, вы сможете реализовывать быстрый, отказоустойчивый код для работы с управляемыми формами 1С, соответствующий стандартам разработки фирмы «1С».

← На главную