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