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