Как отловить движения по регистру накопления при отмене проведения документа в 1С

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

При разработке механизмов разграничения прав доступа, логирования изменений или кастомного контроля проведения в 1С:Предприятии часто возникает задача — проанализировать движения, которые документ делает в регистрах накопления. Если при обычном проведении или записи новых движений задача решается достаточно тривиально (новые строки доступны непосредственно в коллекции набора записей), то при отмене проведения разработчики сталкиваются с серьезной проблемой. Набор записей передается на запись абсолютно пустым, и стандартный обход коллекции не дает никаких результатов.

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

В чем заключается основная проблема?

Рассмотрим стандартный сценарий контроля движений. Предположим, у нас есть регистр накопления ТоварыНаСкладах с измерением Склад. Нам нужно запретить определенным пользователям изменять движения по конкретным складам. При этом запрет должен работать одинаково надежно как при проведении документа, так и при его распроведении.

Если мы напишем код в модуле набора записей регистра накопления в обработчике ПередЗаписью, то увидим следующее поведение:

  1. При проведении документа: В событие передается заполненный набор записей. Мы можем легко обойти коллекцию ЭтотОбъект (или параметр Источник в подписке на событие), выгрузить список складов и проверить права текущего пользователя.
  2. При отмене проведения: Платформа очищает движения документа. В этот момент обработчик ПередЗаписью вызывается снова, но коллекция ЭтотОбъект.Количество() возвращает 0. Документ записывает пустой набор движений, чтобы затереть старые записи в базе данных. В результате мы не можем узнать, по каким складам ранее были сделаны движения, и не можем заблокировать операцию для неавторизованного пользователя.

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

Главный секрет транзакций СУБД: Запрос к физической таблице

Чтобы решить эту задачу красиво и универсально, нам нужно понять логику работы транзакции записи в платформе 1С. Когда вызывается обработчик ПередЗаписью набора записей регистра накопления, транзакция записи в СУБД уже началась, но физического удаления старых строк из таблиц базы данных еще не произошло.

Платформа очистила коллекцию в оперативной памяти (в объекте 1С), но в самой базе данных строки прежних движений по этому регистратору все еще физически существуют и заблокированы для изменения другими сеансами. Это значит, что мы можем выполнить обычный запрос к таблице этого же регистра накопления с отбором по нашему регистратору прямо из обработчика ПередЗаписью!

Давайте разберем по шагам, как реализовать этот подход на практике.

Пошаговая реализация перехвата удаляемых движений

Для реализации универсального контроля лучше всего использовать Подписку на событие. Это позволит не изменять типовой код конфигурации и централизованно управлять логикой контроля для всех документов-регистраторов.

Создадим подписку на событие со следующими свойствами:

Теперь напишем код обработчика. Нам необходимо решить две задачи: получить новые планируемые движения и получить старые (удаляемые) движения.


Процедура ПередЗаписьюНабораТоварыНаСкладах(Источник, Отказ, Замещение) Экспорт

    // Проверяем, что запись производится в рамках проведения или отмены проведения документа
    Если Источник.ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;

    // Нам нужен регистратор, чтобы понять, чьи движения перезаписываются
    Регистратор = Неопределено;
    Для Каждого ЭлементОтбора Из Источник.Отбор Цикл
        Если ЭлементОтбора.Имя = "Регистратор" Тогда
            Если ЭлементОтбора.Использование Тогда
                Регистратор = ЭлементОтбора.Значение;
            КонецЕсли;
            Прервать;
        КонецЕсли;
    КонецЦикла;

    Если Не ЗначениеЗаполнено(Регистратор) Тогда
        Возврат; // Без регистратора (например, при очистке всего регистра) проверку не выполняем
    КонецЕсли;

    // Шаг 1. Получаем новые движения (которые сейчас записываются в БД)
    НовыеСклады = Новый Массив;
    Для Каждого Запись Из Источник Цикл
        Если НовыеСклады.Найти(Запись.Склад) = Неопределено Тогда
            НовыеСклады.Добавить(Запись.Склад);
        КонецЕсли;
    КонецЦикла;

    // Шаг 2. Получаем старые движения из физической таблицы СУБД (которые сейчас будут удалены)
    СтарыеСклады = ПолучитьСкладыИзСуществующихДвижений(Регистратор);

    // Шаг 3. Объединяем списки складов для проверки прав
    ВсеЗадействованныеСклады = Новый Массив;
    
    Для Каждого Склад Из НовыеСклады Цикл
        ВсеЗадействованныеСклады.Добавить(Склад);
    КонецЦикла;
    
    Для Каждого Склад Из СтарыеСклады Цикл
        Если ВсеЗадействованныеСклады.Найти(Склад) = Неопределено Тогда
            ВсеЗадействованныеСклады.Добавить(Склад);
        КонецЕсли;
    КонецЦикла;

    // Шаг 4. Выполняем проверку прав текущего пользователя
    Если НЕ ДоступРазрешен(ВсеЗадействованныеСклады) Тогда
        Отказ = Истина;
        ВызватьИсключение "У вас нет прав на изменение движений по одному из складов (" + Регистратор + ")!";
    КонецЕсли;

КонецПроцедуры

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


Функция ПолучитьСкладыИзСуществующихДвижений(Регистратор)

    РезультатМассив = Новый Массив;

    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ РАЗЛИЧНЫЕ
        |	ТоварыНаСкладах.Склад КАК Склад
        |ИЗ
        |	РегистрНакопления.ТоварыНаСкладах КАК ТоварыНаСкладах
        |ГДЕ
        |	ТоварыНаСкладах.Регистратор = &Регистратор";

    Запрос.УстановитьПараметр("Регистратор", Регистратор);
    
    Выборка = Запрос.Выполнить().Выбрать();
    Пока Выборка.Следующий() Цикл
        РезультатМассив.Добавить(Выборка.Склад);
    КонецЦикла;

    Возврат РезультатМассив;

КонецФункции

И, наконец, простая заглушка для функции проверки доступности складов (в реальной системе здесь будет обращение к параметрам сеанса или к ролям пользователя):


Функция ДоступРазрешен(СписокСкладов)
    
    // Здесь должна быть логика проверки. Например:
    // ДоступныеСклады = ПараметрыСеанса.ДоступныеТекущемуПользователюСклады;
    // Для каждого Склад из СписокСкладов...
    
    Возврат Истина; 
КонецФункции

Как этот алгоритм работает в различных сценариях

Проанализируем поведение нашей подписки в трех ключевых ситуациях:

  1. Первое проведение документа:
    • В базе данных еще нет движений по этому регистратору. Запрос вернет пустой результат (СтарыеСклады пуст).
    • В самом наборе записей (Источник) содержатся новые строки. Мы получаем список НовыеСклады и проверяем права только на те склады, куда сейчас ложатся остатки.
  2. Перепроведение документа (изменение склада с "Склад А" на "Склад Б"):
    • В базе данных еще лежат старые движения по "Склад А". Наш запрос вернет "Склад А" в списке СтарыеСклады.
    • В наборе записей у нас уже подготовлены новые движения по "Склад Б". Мы получим его в НовыеСклады.
    • Система объединит оба склада. Программа проверит права пользователя и на "Склад А", и на "Склад Б". Это критически важно: если у пользователя есть права на Склад Б, но нет прав на Склад А, он не сможет «угнать» товар со Склада А, просто перепроведя документ. Система выдаст отказ.
  3. Полная отмена проведения (распроведение):
    • В наборе записей пусто (НовыеСклады пуст).
    • Запрос к базе данных возвращает склады, по которым документ двигал остатки ранее.
    • Программа проверяет права на эти «старые» склады. Если прав нет — распроведение блокируется.

Альтернативный вариант: Использование Дополнительных Свойств

Рассмотрим альтернативную ситуацию. Вы можете спросить: «Не скажется ли выполнение дополнительного запроса к базе данных в транзакции записи на производительности системы под высокой нагрузкой?»

Для высоконагруженных систем действительно стараются минимизировать любые лишние запросы внутри транзакций СУБД. В качестве альтернативы можно использовать механизм ДополнительныеСвойства объектов 1С.

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

  1. В подписке на событие документа ПередЗаписью (или в обработчиках проведения/отмены проведения самого документа) мы анализируем табличные части документа и определяем задействованные склады.
  2. Записываем этот список складов в структуру дополнительных свойств документа-регистратора: ЭтотОбъект.ДополнительныеСвойства.Вставить("ЗадействованныеСклады", СписокСкладов).
  3. Поскольку запись движений происходит в рамках единого транзакционного пространства выполнения документа, в подписке на событие ПередЗаписью набора записей регистра мы можем попытаться обратиться к свойствам регистратора: Источник.Отбор.Регистратор.Значение.ДополнительныеСвойства.

Однако у этого подхода есть существенные минусы:

Именно поэтому использование точечного, легковесного индексированного запроса по регистратору непосредственно в обработчике ПередЗаписью набора записей регистра накопления считается в среде разработчиков 1С стандартом де-факто (Best Practice).

Влияние свойства «Удаление движений» у документов

Также важно упомянуть настройку метаданных документов Удаление движений. Она определяет, как именно платформа очищает старые записи перед проведением:

Резюме

Мы подробно разобрали, как решить проблему анализа движений при отмене проведения документов. Ключевой вывод — не стоит пытаться собирать данные по метаданным документов или искать обходные пути передачи контекста. Достаточно использовать особенности транзакционности платформы 1С:Предприятие 8 и выполнить прямой точечный запрос к таблице регистра по регистратору в обработчике события ПередЗаписью набора записей. Это решение является наиболее чистым, надежным и простым в поддержке.

← На главную