В практической работе разработчиков и пользователей конфигурации «Зарплата и управление персоналом», редакция 3.1 (ЗУП 3.1) периодически встречается крайне неприятная и трудноуловимая ошибка (зарегистрированная в базе ошибок фирмы «1С» под номером 00110944). Суть проблемы заключается в следующем: если у расчетчика одновременно открыто несколько документов-ведомостей (например, «Ведомость в банк» или «Ведомость в кассу»), то в процессе автоматического заполнения или подбора сотрудников в одной из ведомостей может непроизвольно перезаписаться и затереться данными вторая ведомость, открытая на соседней вкладке.
В этой статье мы подробно разберем, почему возникает данное поведение платформы и конфигурации, проанализируем механизм работы глобальных оповещений и рассмотрим два способа устранения этой проблемы с помощью расширения конфигурации.
Чтобы понять природу ошибки, рассмотрим сценарий, при котором она гарантированно воспроизводится:
Такое поведение не только замедляет работу расчетной службы, но и несет в себе риски выплаты неверных сумм, если пользователь не заметит подмены данных в свернутой вкладке.
Давайте проанализируем программный код, отвечающий за возврат выбранных документов из формы подбора обратно в форму ведомости. В форме выбора документов-оснований при нажатии кнопки подтверждения выполняется следующий клиентский код:
&НаКлиенте
Процедура ВыбратьВыполнить(Команда)
ВыбранныеДокументы = Новый СписокЗначений;
Для Каждого ЭлементСпискаДокументов Из СписокДокументов Цикл
Если ЭлементСпискаДокументов.Пометка Тогда
ВыбранныеДокументы.Добавить(ЭлементСпискаДокументов.Значение);
КонецЕсли;
КонецЦикла;
Оповестить(
ВедомостьНаВыплатуЗарплатыКлиентРасширенный.ИмяСобытияВыбораДокументовОснованийВедомости,
ВыбранныеДокументы,
ЭтаФорма
);
Закрыть();
КонецПроцедуры
Обратите внимание на метод Оповестить(). Это стандартный метод платформы «1С:Предприятие», который посылает глобальное оповещение всем открытым формам в рамках текущего сеанса пользователя. Каждая активная управляемая форма содержит стандартный обработчик ОбработкаОповещения.
Посмотрим, как это событие обрабатывается в форме ведомости:
&НаКлиенте
Процедура ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
ВедомостьНаВыплатуЗарплатыКлиентРасширенный.ОбработкаОповещения(ЭтотОбъект, ИмяСобытия, Параметр, Источник);
КонецПроцедуры
Когда срабатывает метод Оповестить(), событие прилетает абсолютно во все открытые ведомости. Поскольку обработчик внутри формы ведомости написан без проверки того, «кому» именно предназначается это сообщение, каждая из открытых ведомостей послушно передает контекст своей формы (параметр ЭтотОбъект) в общий модуль ВедомостьНаВыплатуЗарплатыКлиентРасширенный для последующего заполнения.
В результате та ведомость, которая просто мирно существовала на другой вкладке панели задач, перехватывает общее событие оповещения и запускает процедуру своего перезаполнения чужими данными.
Проанализируем первый способ решения проблемы. Он заключается в том, чтобы научить общий модуль различать, какая именно форма инициировала вызов. При вызове метода Оповестить() третьим параметром передается ЭтаФорма (форма подбора).
Если форма подбора открывалась с указанием владельца (а это, как правило, вызывающая форма ведомости), то в параметре Источник обработчика оповещения будет содержаться контекст формы подбора, у которой заполнен реквизит ВладелецФормы. Мы можем использовать это для локализации обработчика.
Для реализации этого метода создадим расширение и скорректируем обработчик в модулях ведомостей (например, ВедомостьНаВыплатуЗарплатыВБанк, ВедомостьНаВыплатуЗарплатыВКассу и др.), заменив вызов общего модуля:
&НаКлиенте
&Вместо("ОбработкаОповещения")
Процедура Расш_ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
// Проверяем, совпадает ли текущая форма с владельцем источника оповещения
Если Источник <> Неопределено И Источник.ВладелецФормы = ЭтотОбъект Тогда
ВедомостьНаВыплатуЗарплатыКлиентРасширенный.ОбработкаОповещения(ЭтотОбъект, ИмяСобытия, Параметр, Источник);
Иначе
// Если это "чужое" оповещение, вызываем стандартную обработку для других типов событий
ПродолжитьВызов(ИмяСобытия, Параметр, Источник);
КонецЕсли;
КонецПроцедуры
Плюсы метода: Простая реализация, минимальное количество изменений в коде.
Минусы метода: Этот способ считается обходным («костылем»), так как свойство ВладелецФормы может быть не определено, если форма выбора открывалась нетиповым образом. Передача тяжелых контекстов управляемых форм через параметры оповещения также может приводить к изшбыточному расходу памяти.
Рассмотрим более надежный способ, соответствующий стандартам разработки фирмы «1С» и Библиотеки Стандартных Подсистем (БСП). Данный метод базируется на адресации вызовов с использованием уникального идентификатора формы (UUID).
Суть метода заключается в том, чтобы при открытии вспомогательной формы выбора передавать в нее уникальный идентификатор вызывающей формы ведомости. При отправке оповещения форма выбора будет вкладывать этот идентификатор в отправляемый пакет данных, а принимающая форма ведомости сравнит его со своим собственным UUID.
Разберем по шагам реализацию этой схемы в расширении:
В коде формы ведомости, где вызывается открытие формы подбора документов-оснований, мы добавляем в параметры открытия наш уникальный идентификатор:
&НаКлиенте
Процедура ОткрытьФормуВыбораДокументовОснований()
ПараметрыОткрытия = Новый Структура;
// Передаем уникальный идентификатор текущей формы ведомости
ПараметрыОткрытия.Вставить("ИдентификаторВызывающейФормы", ЭтотОбъект.УникальныйИдентификатор);
ОткрытьФорму(
"Документ.ВедомостьНаВыплатуЗарплатыВБанк.Форма.ФормаВыбораДокументовОснований",
ПараметрыОткрытия,
ЭтотОбъект
);
КонецПроцедуры
В форме выбора документов-оснований создадим новый реквизит формы с типом УникальныйИдентификатор или Строка (например, АдресатОповещенияID). Заполним его при создании формы на сервере:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
Если Параметры.Свойство("ИдентификаторВызывающейФормы") Тогда
Объект.АдресатОповещенияID = Параметры.ИдентификаторВызывающейФормы;
КонецЕсли;
КонецПроцедуры
Модифицируем процедуру отправки оповещения в форме выбора. Вместо простой передачи списка значений упакуем результат и сохраненный идентификатор в структуру:
&НаКлиенте
Процедура ВыбратьВыполнить(Команда)
ВыбранныеДокументы = Новый СписокЗначений;
// ... логика обхода и заполнения списка документов ...
// Формируем структуру с адресатом
ПакетДанных = Новый Структура;
ПакетДанных.Вставить("АдресатID", Объект.АдресатОповещенияID);
ПакетДанных.Вставить("Значение", ВыбранныеДокументы);
Оповестить(
ВедомостьНаВыплатуЗарплатыКлиентРасширенный.ИмяСобытияВыбораДокументовОснованийВедомости,
ПакетДанных,
ЭтаФорма
);
Закрыть();
КонецПроцедуры
Теперь скорректируем обработчик события в форме ведомости. Мы будем выполнять обработку только в том случае, если пришедший в структуре идентификатор совпадает с собственным идентификатором формы:
&НаКлиенте
&Вместо("ОбработкаОповещения")
Процедура Расш_ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
Если ИмяСобытия = ВедомостьНаВыплатуЗарплатыКлиентРасширенный.ИмяСобытияВыбораДокументовОснованийВедомости Тогда
// Проверяем, является ли параметр структурой адресации
Если ТипЗнч(Параметр) = Тип("Структура") И Параметр.Свойство("АдресатID") Тогда
// Если идентификатор не совпадает с текущим - игнорируем это оповещение
Если Параметр.АдресатID <> ЭтотОбъект.УникальныйИдентификатор Тогда
Возврат;
КонецЕсли;
// Распаковываем реальный параметр для передачи в типовой алгоритм
РеальныйПараметр = Параметр.Значение;
Иначе
РеальныйПараметр = Параметр;
КонецЕсли;
ВедомостьНаВыплатуЗарплатыКлиентРасширенный.ОбработкаОповещения(ЭтотОбъект, ИмяСобытия, РеальныйПараметр, Источник);
Иначе
// Обработка остальных системных оповещений
ПродолжитьВызов(ИмяСобытия, Параметр, Источник);
КонецЕсли;
КонецПроцедуры
Плюсы метода: Полное соответствие стандартам безопасной разработки 1С. Исключены любые побочные эффекты кэширования памяти. Оповещение гарантированно отработает только в той форме, которая действительно ожидала завершения подбора.
Рассмотренная ошибка в типовом механизме «1С:Зарплата и управление персоналом 3.1» наглядно иллюстрирует важность точечной адресации при использовании механизма глобальных оповещений в платформе «1С:Предприятие 8». Использование УникальныйИдентификатор при вызове метода Оповестить() позволяет полностью изолировать формы друг от друга и исключить риск случайной потери данных пользователями при параллельной работе с несколькими однотипными документами.