При разработке групповых обработок или переносов данных мы часто сталкиваемся с ситуацией, когда сложная бизнес-логика (расчет скидок, перезаполнение НДС, подбор связанных реквизитов) «зашита» разработчиками конфигурации непосредственно в модуле формы документа, в процедурах ПриИзменении элементов управления. Если мы просто изменяем данные объекта (например, ДокументОбъект.Товары[0].СтавкаНДС = ...), эта логика не срабатывает, и документ остается несогласованным.
В этой статье мы подробно разберем, как эмулировать работу пользователя: открыть форму программно, изменить данные и заставить платформу выполнить все связанные события. Особое внимание уделим неочевидным проблемам: почему код может игнорировать первую строку таблицы и как на это влияет механизм «ленивой» инициализации страниц формы.
Представим задачу: нужно во всех документах «Реализация товаров и услуг» изменить ставку НДС. Просто поменять реквизит недостаточно, так как при изменении ставки должны пересчитаться суммы НДС и Итого. Вся логика пересчета находится в модуле формы в процедуре ТоварыСтавкаНДСПриИзменении.
Мы решаем открыть форму программно, пройтись циклом по строкам и вызвать это событие. Однако здесь нас подстерегают две основные сложности, с которыми столкнулись участники обсуждения:
Это самый важный теоретический момент. В управляемых формах таблица, которую видит пользователь (ТаблицаФормы), и данные, с которыми мы работаем (ДанныеФормыКоллекция), связаны, но работают по-разному.
Когда мы хотим программно установить курсор на строку, мы используем свойство Элементы.Товары.ТекущаяСтрока. Многие ошибочно полагают, что туда нужно передать номер строки (0, 1, 2...).
Это ошибка! Свойство ТекущаяСтрока ожидает Идентификатор строки. Это внутреннее число, которое платформа присваивает строке. Оно может быть любым (например, 1, 100500, 7) и не обязано совпадать с порядковым номером.
Рассмотрим правильный алгоритм обхода строк для позиционирования:
// Правильный обход строк таблицы формы
// Нам нужно перебирать коллекцию данных (Объект.Товары),
// но устанавливать текущую строку в элементах формы (Элементы.Товары)
Для Каждого СтрокаДанных Из Объект.Товары Цикл
// 1. Получаем Идентификатор строки из данных
ИдентификаторСтроки = СтрокаДанных.ПолучитьИдентификатор();
// 2. Устанавливаем текущую строку в интерфейсе по Идентификатору
Элементы.Товары.ТекущаяСтрока = ИдентификаторСтроки;
// Теперь платформа "видит", что мы стоим на этой строке
// Можно выполнять действия...
КонецЦикла;
Если вы попытаетесь передать туда просто индекс (например, счетчик цикла), платформа будет искать строку с таким идентификатором. Если строки с ID=0 не существует (а идентификаторы часто начинаются с 1), то позиционирование не произойдет, и код отработает впустую или с ошибкой.
В обсуждении была выявлена интересная ошибка: код перебора строк работал для всех строк, кроме первой (с индексом 0), пока пользователь не догадался программно активировать страницу, на которой лежит таблица.
Причина кроется в оптимизации работы интерфейса 1С. Платформа не отрисовывает и иногда не инициализует полностью элементы управления, которые находятся на скрытых вкладках (механизм отложенной инициализации).
Если таблица «Товары» находится на второй вкладке, а форма открывается на первой, то таблица технически «спит». Попытка программно установить ТекущаяСтрока может быть проигнорирована платформой для первой итерации, так как фокус ввода еще не находится в нужном контексте.
Решение: Перед началом манипуляций с таблицей необходимо программно активировать нужную страницу (вкладку).
// Открываем форму
ФормаДокумента = ОткрытьФорму("Документ.РеализацияТоваровУслуг.Форма.ФормаДокумента", ПараметрыОткрытия);
// ОБЯЗАТЕЛЬНО: Активируем страницу с таблицей
// Иначе первая строка может не обработаться корректно из-за ленивой инициализации
ФормаДокумента.Элементы.ГруппаСтраницы.ТекущаяСтраница = ФормаДокумента.Элементы.СтраницаТовары;
// Активируем саму таблицу
ФормаДокумента.ТекущийЭлемент = ФормаДокумента.Элементы.Товары;
После того как мы правильно спозиционировались на строке, нам нужно изменить данные и вызвать событие. Рассмотрим три подхода, от «хакерского» до архитектурно правильного.
Этот метод заставляет платформу «думать», что пользователь выбрал значение в поле ввода. Он сложен в реализации, но универсален, если вы не можете менять конфигурацию.
Суть метода заключается в подмене владельца формы. Обычно ОповеститьОВыборе используется для форм подбора, но мы можем использовать его для эмуляции ввода.
// Пример функции для эмуляции ввода (из обсуждения)
&НаКлиенте
Функция ИнтерактивноЗаписатьВЭлементУправления(ЭлементУправления, Значение, Форма)
// Запоминаем текущее состояние
СтарыйВладелец = Форма.ВладелецФормы;
// Подменяем владельца формы на тот элемент, который хотим "изменить"
Форма.ВладелецФормы = ЭлементУправления;
// Эмулируем выбор значения.
// Это запустит цепочку событий: ОбработкаВыбора -> ПриИзменении
Форма.ОповеститьОВыборе(Значение);
// Возвращаем владельца на место
Форма.ВладелецФормы = СтарыйВладелец;
КонецФункции
Более надежный и понятный способ. Если у вас есть возможность использовать расширения конфигурации, добавим в форму документа специальный метод-обертку.
В модуле формы (расширение):
&НаКлиенте
Процедура Расш_ВыполнитьПриИзменении(ИмяЭлемента) Экспорт
// Получаем элемент по имени
Элемент = Элементы[ИмяЭлемента];
// Вызываем стандартные обработчики, привязанные к этому элементу
// Примечание: прямой вызов обработчика события по имени процедуры
// надежнее всего делать, если вы знаете точное имя процедуры.
// Например:
Если ИмяЭлемента = "ТоварыСтавкаНДС" Тогда
ТоварыСтавкаНДСПриИзменении(Элемент);
КонецЕсли;
КонецПроцедуры
Во внешней обработке:
// ... код открытия формы и позиционирования на строке ...
// Меняем данные в объекте (через форму)
ФормаДокумента.Объект.Товары[Индекс].СтавкаНДС = НоваяСтавка;
// Вызываем наш экспортный метод
ФормаДокумента.Расш_ВыполнитьПриИзменении("ТоварыСтавкаНДС");
Самый правильный с точки зрения производительности и поддержки способ. Если логика пересчета НДС нужна и в форме, и при загрузке данных, она не должна жить в модуле формы.
Правильно будет вынести расчеты в Общий модуль, например РасчетыВДокументахКлиентСервер.
ПересчитатьСуммуНДС(СтрокаТабличнойЧасти) в общем модуле.ПриИзменении вызываем эту процедуру.Этот метод работает в сотни раз быстрее, так как не требует отрисовки интерфейса на клиенте.
Если рефакторинг невозможен и нужно идти через открытие формы, объединим знания в рабочий алгоритм:
&НаКлиенте
Процедура ИзменитьСтавкиВДокументе(СсылкаНаДокумент)
ПараметрыФормы = Новый Структура("Ключ", СсылкаНаДокумент);
Форма = ОткрытьФорму("Документ.РеализацияТоваровУслуг.Форма.ФормаДокумента", ПараметрыФормы);
// 1. АКТИВИРУЕМ СТРАНИЦУ (Решение проблемы "первой строки")
// Предположим, страница называется "СтраницаТовары"
Форма.Элементы.ГруппаСтраницы.ТекущаяСтраница = Форма.Элементы.СтраницаТовары;
// 2. Проходим по данным формы
Товары = Форма.Объект.Товары;
Для Каждого СтрокаТЧ Из Товары Цикл
// 3. ПОЛУЧАЕМ ИДЕНТИФИКАТОР (Не используем индекс!)
ИД = СтрокаТЧ.ПолучитьИдентификатор();
// 4. Позиционируем интерфейс
Форма.Элементы.Товары.ТекущаяСтрока = ИД;
// Меняем данные
СтрокаТЧ.СтавкаНДС = ПредопределенноеЗначение("Перечисление.СтавкиНДС.БезНДС");
// 5. Вызываем событие
// Здесь используем Способ 2 (экспортный метод) или прямой вызов,
// если он доступен через контекст.
// Для примера предположим, что мы добавили экспортный метод в расширение:
Форма.ВыполнитьОбработкуИзменения("ТоварыСтавкаНДС");
КонецЦикла;
Форма.Записать();
Форма.Закрыть();
КонецПроцедуры
Работая с программным изменением форм, помните:
ТекущаяСтрока всегда идет Идентификатор.