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

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

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

В этой статье мы подробно разберем внутренние механизмы платформы «1С:Предприятие», которые приводят к возникновению этой коллизии, выясним причины такого поведения и рассмотрим четыре практических способа решения этой проблемы.

Физическая суть проблемы: почему так происходит?

Для того чтобы понять природу этой ошибки, проанализируем поведение платформы в момент, когда пользователь нажимает кнопку «Провести» для уже проведенного ранее документа в оперативном режиме. Здесь одновременно срабатывают три ключевых механизма платформы:

  1. Оперативный сдвиг времени (Оперативная отметка). При оперативном проведении документа платформа автоматически изменяет его дату и время, сдвигая их на текущее время сервера (или на секунду позже самого последнего проведенного документа в системе). Таким образом, внутри обработчика ОбработкаПроведения метод МоментВремени() возвращает уже новую (сдвинутую вперед) дату и время.
  2. Режим удаления движений по умолчанию. Начиная с версии платформы 8.2 и во всех версиях 8.3, для документов свойство «Удаление движений» по умолчанию установлено в значение «Удалять автоматически при отмене проведения». Это оптимизация, направленная на сокращение времени блокировок БД. При таком режиме во время *перепроведения* (когда документ уже был проведен и проводится снова) платформа не удаляет старые движения из таблиц базы данных автоматически перед началом выполнения кода ОбработкаПроведения. Старые движения продолжают физически находиться в базе данных и будут заменены новыми только в самом конце транзакции проведения.
  3. Суть коллизии границ периода. Представим ситуацию на временной шкале:
    • Первоначально документ был проведен в 12:00:00. В базе данных записаны его движения с периодом 12:00:00.
    • Мы запускаем оперативное перепроведение в 12:05:00. Платформа сдвигает время документа на 12:05:00.
    • В коде проведения вызывается запрос, куда передается параметр: Новый Граница(МоментВремени(), ВидГраницы.Исключая). В нашем случае это граница на 12:05:00 (исключая).
    • Так как старые движения документа еще физически лежат в базе данных и имеют период 12:00:00, они оказываются хронологически раньше, чем граница 12:05:00.
    • Запрос к виртуальной таблице оборотов или остатков честно считывает эти движения из базы данных, так как они попадают в период «до 12:05:00». В результате алгоритм получает искаженные (завышенные) данные.

Рассмотрим подробнее четыре способа исправления этой ситуации, которые применяются в современной практике разработки на платформе «1С:Предприятие».

Способ 1. Принудительная запись пустого набора записей (Классический подход)

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

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

Проанализируем пример кода:


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

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

Способ 2. Использование «Новой методики» проведения (Рекомендуемый)

Фирма «1С» настоятельно рекомендует использовать так называемую «новую методику» проведения (или пост-контроль). Ее суть заключается в том, что проверка остатков и оборотов выполняется не до записи движений, а после нее.

Давайте разберем этот алгоритм по шагам:

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

При использовании этой методики нам вообще не требуется исключать движения текущего документа с помощью класса Граница. Наоборот, движения текущего документа должны учитываться в запросе, так как мы проверяем результирующее состояние регистра. Это полностью решает проблему коллизии дат.

Способ 3. Корректировка данных внутри запроса без записи в БД

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

Для этого мы можем составить пакетный запрос:

  1. В первом запросе пакета мы получаем обороты или остатки из виртуальной таблицы на нужную дату (эти данные будут ошибочно содержать старые движения документа).
  2. Во втором запросе пакета мы выбираем движения этого же документа напрямую из реальной (физической) таблицы регистра накопления, используя фильтр по регистратору: ГДЕ Регистратор = &Ссылка.
  3. В третьем запросе пакета мы соединяем полученные таблицы по ключевым полям (номенклатура, контрагент и т.д.) и вычитаем показатели старых движений из общих итогов.

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


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

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

Результат = Запрос.Выполнить();

Этот метод работает быстрее, так как блокировка накладывается только в момент финальной записи новых движений в самом конце процедуры проведения.

Способ 4. Изменение свойства конфигурации «Удаление движений»

Существует еще один способ, который решает проблему на уровне метаданных документа. Если открыть палитру свойств вашего документа в конфигураторе и изменить значение свойства «Удаление движений» со значения «Удалять автоматически при отмене проведения» на «Удалять автоматически», поведение платформы изменится.

При таком значении свойства платформа перед запуском процедуры ОбработкаПроведения будет сама физически очищать все движения документа в базе данных. Соответственно, в момент выполнения запроса никаких старых движений документа в БД уже не останется.

Важное предупреждение: Данный способ считается устаревшим. Фирма «1С» не рекомендует использовать режим «Удалять автоматически» в современных высоконагруженных конфигурациях. Это связано с тем, что очистка движений платформой происходит в неявном виде и накладывает избыточные блокировки СУБД, снижая общую параллельность работы пользователей в системе.

Резюме

Мы проанализировали причину коллизии при оперативном проведении документов в 1С. Она кроется во временном разрыве между смещенной вперед оперативной датой документа и датой его старых движений, которые еще физически не удалены из базы данных.

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

← На главную