Почему запрос возвращает разные остатки в коде проведения документа и в консоли запросов?

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

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

Анализируем исходную ситуацию

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


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

Запрос.УстановитьПараметр("Артикул", Парам);  
Запрос.УстановитьПараметр("Склад", Справочники.Склады.НайтиПоКоду("000000001"));
Запрос.УстановитьПараметр("Дата", Новый Граница(ТекущаяДата(), ВидГраницы.Включая));

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

Причина 1: Контекст транзакции и момент времени

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

Рассмотрим, что происходит в момент перепроведения документа:

  1. Платформа начинает транзакцию.
  2. Если в свойствах документа установлено "Удалять движения автоматически", то старые движения документа удаляются.
  3. Выполняется код процедуры ОбработкаПроведения.
  4. В конце транзакции новые движения записываются и фиксируются.

Если мы выполняем запрос в пункте 3, а консоль запросов запускаем параллельно, то консоль может видеть состояние данных "между" удалениями и новыми записями, либо вообще не видеть изменений, пока транзакция не завершена. Важно помнить: виртуальная таблица Остатки учитывает текущие движения документа только в том случае, если они уже были явно записаны в базу данных в этой транзакции.

Причина 2: Использование ТекущаяДата() вместо Момента Времени

Использование ТекущаяДата() в запросах внутри модуля документа — это критическая ошибка. В системе 1С несколько событий могут произойти в одну и ту же секунду. Для однозначного определения позиции документа на временной оси используется объект МоментВремени, который включает в себя дату, время и ссылку на сам документ.

Разберем, как правильно передавать параметры в виртуальную таблицу:

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

Проанализируем исправленный фрагмент установки параметров:


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

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

Причина 3: Настройки параметров виртуальной таблицы

Посмотрим на условие в ГДЕ. Программист написал:


ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура = &Артикул)
ГДЕ ТоварыНаСкладахОстатки.Склад = &Склад

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

Правильный подход: передавать все измерения в параметры виртуальной таблицы. Это позволяет СУБД использовать индексы и возвращать уже готовый результат по конкретному складу. Перепишем запрос правильно:


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

Причина 4: Запись движений перед выполнением запроса

Выясним причину, по которой автор вопроса в итоге получил правильный результат. В сообщении 37 он упоминает, что "в контексте документа" данные отличаются. Это происходит потому, что в типовых конфигурациях или при ручном формировании движений записи сначала попадают в коллекцию Движения.ТоварыНаСкладах (в оперативную память), но еще не записаны в физические таблицы базы данных.

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


// Формируем движения
НовоеДвижение = Движения.ТоварыНаСкладах.ДобавитьРасход();
НовоеДвижение.Номенклатура = ВыбранныйТовар;
НовоеДвижение.Склад = ВыбранныйСклад;
НовоеДвижение.Количество = 1;

// ЗАПИСЫВАЕМ движения в БД, чтобы виртуальная таблица их "увидела"
Движения.ТоварыНаСкладах.Записать(); 

// Теперь запрос с ВидГраницы.Включая покажет остаток с учетом этого расхода

Как гарантированно решить проблему расхождения данных?

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

  1. Проверьте типы параметров. Часто разработчики путают ссылку на справочник и строку (например, Артикул). В запросе Номенклатура = &Артикул параметр &Артикул должен быть ссылкой на элемент справочника Номенклатура, а не строковым кодом.
  2. Используйте МоментВремени(). Никогда не используйте ТекущаяДата() внутри модуля документа. Если документ перепроводится "задним числом", ТекущаяДата() вернет остатки на "сейчас", а не на момент совершения хозяйственной операции.
  3. Синхронизируйте условия в консоли и в коде. Откройте консоль запросов непосредственно из сеанса, где вы тестируете проведение. Убедитесь, что в консоли вы выбираете точно ту же дату (до секунды) и тот же склад.
  4. Учитывайте автоматическое удаление движений. Посмотрите в свойствах документа на закладке "Движения", какой режим удаления установлен. Если выбрано "Не удалять автоматически", то ваш запрос в начале процедуры проведения увидит старые движения этого же документа, что приведет к завышению или занижению остатка.

Итог: правильная структура запроса к остаткам

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


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

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

Соблюдение этих правил гарантирует, что ваши отчеты и механизмы контроля остатков будут работать предсказуемо как в толстом клиенте, так и на сервере, а результаты в консоли запросов всегда будут поддаваться логическому объяснению.

← На главную