Почему в подписке на ОбработкаПроведения запрос не видит движения документа и как это исправить?

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

При разработке дополнительной функциональности в конфигурациях 1С разработчики часто используют подписки на события. Типичная задача — отреагировать на проведение документа (например, КорректировкаДолга или РеализацияТоваровУслуг) и проанализировать результат его движений с помощью запроса к базе данных. Однако при выполнении запроса к регистрам выясняется, что движений текущего документа там нет. Проанализируем, почему возникает такое поведение платформы и как правильно получить сформированные данные.

Выясним причину: как платформа 1С записывает движения

Разберем жизненный цикл проведения документа. Когда инициируется проведение, платформа запускает транзакцию и вызывает процедуру ОбработкаПроведения модуля документа, а затем последовательно выполняет назначенные обработчики подписок на это событие.

В типовых конфигурациях (таких как «Управление торговлей 10.3», «Управление производственным предприятием» и других) внутри процедуры ОбработкаПроведения вызываются функции формирования движений (например, ДвиженияПоРегистрам()). Важно понимать: эти процедуры лишь заполняют коллекцию движений в оперативной памяти объекта и устанавливают свойство Записывать = Истина для соответствующего набора записей:

Движения.РасчетыСКонтрагентами.Записывать = Истина;

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

Почему же в других подписках или документах запросы иногда видят движения? Рассмотрим эти ситуации:

  1. Новая методика проведения: в ряде документов используется методика, при которой движения формируются в памяти, затем принудительно записываются вызовом Движения.Записать(), и лишь потом запросом проверяются отрицательные остатки. Если подписка привязана к такому документу, данные в базе данных к моменту выполнения подписки уже присутствуют.
  2. Сложные партионные алгоритмы: типовой код партионного учета или распределения взаиморасчетов по ФИФО может внутри себя явно вызывать метод Записать() для промежуточного сохранения движений, чтобы следующие шаги алгоритма могли работать с ними через запросы.
  3. Разные события: подписка может быть назначена не на событие документа, а на события набора записей регистра (например, ПриЗаписи).

Разберем три проверенных способа решения поставленной задачи.

Способ 1. Использование коллекции движений из оперативной памяти (рекомендуемый)

Наиболее оптимальный подход с точки зрения производительности и параллельности работы пользователей — не обращаться к СУБД за теми данными, которые уже есть в памяти текущего объекта. В обработчик подписки первым параметром передается Источник (сам объект документа).

Разберем по шагам, как выгрузить сформированные движения в таблицу значений и передать их в запрос:

  1. Получаем набор записей нужного регистра из свойства Источник.Движения.
  2. Выгружаем движения в таблицу значений с помощью метода Выгрузить().
  3. Передаем полученную таблицу значений в запрос в качестве параметра.
  4. В тексте запроса помещаем таблицу значений во временную таблицу и соединяем ее с остальными данными базы данных.

Посмотрим на пример реализации такого запроса:


Процедура ОбработкаПроведенияПодписка(Источник, Отказ, РежимПроведения) Экспорт

    // 1. Выгружаем движения из памяти
    ТаблицаДвижений = Источник.Движения.РасчетыСКонтрагентами.Выгрузить();
    
    Если ТаблицаДвижений.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;

    // 2. Формируем запрос с использованием временной таблицы
    Запрос = Новый Запрос;
    Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
    
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    ТЗ.Сделка КАК Сделка,
        |    ТЗ.ДоговорКонтрагента КАК ДоговорКонтрагента,
        |    ТЗ.СуммаУпр КАК СуммаУпр
        |ПОМЕСТИТЬ ВТ_ДвиженияДокумента
        |ИЗ
        |    &ТаблицаДвижений КАК ТЗ
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |    ЗаказПокупателя.Ссылка КАК Ссылка,
        |    ВТ_ДвиженияДокумента.СуммаУпр КАК СуммаДвижения
        |ИЗ
        |    Документ.ЗаказПокупателя КАК ЗаказПокупателя
        |        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ДвиженияДокумента КАК ВТ_ДвиженияДокумента
        |        ПО (ВТ_ДвиженияДокумента.Сделка = ЗаказПокупателя.Ссылка)
        |            И (ВТ_ДвиженияДокумента.ДоговорКонтрагента = ЗаказПокупателя.ДоговорКонтрагента)
        |ГДЕ
        |    ЗаказПокупателя.Ссылка = &Заказ";

    Запрос.УстановитьПараметр("ТаблицаДвижений", ТаблицаДвижений);
    Запрос.УстановитьПараметр("Заказ", Источник.Сделка); // или соответствующий реквизит

    Результат = Запрос.Выполнить();
    // Дальнейшая обработка результата...

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

Этот способ исключает лишние операции записи на диск и предотвращает возникновение избыточных блокировок данных в транзакции.

Способ 2. Явная принудительная запись набора движений в подписке

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

Рассмотрим, как это сделать корректно:


Процедура ОбработкаПроведенияПодписка(Источник, Отказ, РежимПроведения) Экспорт

    // Записываем набор движений в СУБД в рамках текущей транзакции
    Источник.Движения.РасчетыСКонтрагентами.Записать();

    // Теперь движения присутствуют в СУБД, и запрос сможет их прочитать
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    РасчетыСКонтрагентамиОстаткиИОбороты.Регистратор,
        |    РасчетыСКонтрагентамиОстаткиИОбороты.СуммаУпрПриход,
        |    РасчетыСКонтрагентамиОстаткиИОбороты.СуммаУпрРасход
        |ИЗ
        |    РегистрНакопления.РасчетыСКонтрагентами.ОстаткиИОбороты(
        |            ,
        |            &МоментОкончания,
        |            Регистратор,
        |            ,
        |            Сделка = &Заказ) КАК РасчетыСКонтрагентамиОстаткиИОбороты";

    Запрос.УстановитьПараметр("МоментОкончания", Новый Граница(Источник.МоментВремени(), ВидГраницы.Включая));
    Запрос.УстановитьПараметр("Заказ", НужныйЗаказ);

    Выборка = Запрос.Выполнить().Выбрать();
    // Обработка выборки...

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

Обратим внимание на важные особенности этого способа:

  1. Граница периода: при обращении к виртуальным таблицам обязательно задаем параметр границы с включением точки времени документа (ВидГраницы.Включая), иначе запись текущей секунды может не попасть в итоги.
  2. Транзакционность: если далее в подписке будет установлено Отказ = Истина, платформа откатит транзакцию целиком, и записанные движения не останутся в базе данных.
  3. Блокировки: вызов Записать() накладывает монопольные блокировки на таблицы регистра раньше штатного времени, что в высоконагруженных системах может приводить к таймаутам ожидания блокировок.

Способ 3. Перенос логики в подписку на событие регистра

Если задача заключается исключительно в постобработке записей конкретного регистра, эффективнее изменить точку интеграции. Вместо события документа ОбработкаПроведения создадим подписку на событие регистра накопления: ПриЗаписи набора записей (объект метаданных РегистрНакопленияНаборЗаписей.<ИмяРегистра>).

В этом случае источником выступает сам набор записей. Проверим тип регистратора и выполним нужные действия:


Процедура РегистрРасчетыСКонтрагентамиПриЗаписи(Источник, Отказ) Экспорт

    // Проверяем, что регистратор является нужным документом
    РегистраторСсылка = Источник.Отбор.Регистратор.Значение;
    Если ТипЗнч(РегистраторСсылка) <> Тип("ДокументСсылка.КорректировкаДолга") Тогда
        Возврат;
    КонецЕсли;

    // В момент события ПриЗаписи данные уже находятся в СУБД в рамках транзакции
    // Здесь можно выполнять запросы к регистру с отбором по РегистраторСсылка
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    Расчеты.Сделка КАК Сделка,
        |    Расчеты.СуммаУпр КАК СуммаУпр
        |ИЗ
        |    РегистрНакопления.РасчетыСКонтрагентами КАК Расчеты
        |ГДЕ
        |    Расчеты.Регистратор = &Регистратор";

    Запрос.УстановитьПараметр("Регистратор", РегистраторСсылка);
    Результат = Запрос.Выполнить();

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

Выбор конкретного решения зависит от контекста: для легковесных проверок и расчетов оптимален Способ 1 с выгрузкой в таблицу значений; для комплексного анализа итогов по виртуальным таблицам подойдет Способ 2 или перенос логики на события регистратора через Способ 3.

← На главную