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