Каждый разработчик 1С рано или поздно сталкивается с мистической ситуацией: один и тот же запрос, выполненный в коде модуля (например, при открытии формы или проведении документа) и в консоли запросов, выдает совершенно разные результаты. В консоли мы видим идеальные, правильные остатки, а в коде программа упорно выбирает закрытые накладные или неверные суммы.
Давайте подробно разберем, почему возникает такое поведение платформы «1С:Предприятие», проанализируем неочевидные механизмы работы виртуальных таблиц и выясним, как решить эту проблему раз и навсегда.
Это самая распространенная и скрытая причина расхождения данных. Разберем по шагам, как это работает.
В консоли запросов разработчик чаще всего работает под учетной записью с полными правами (роль «Полные права» или администратор базы). В этом режиме механизм RLS (Record-Level Security) полностью отключен.
В то же время код в модуле документа или формы выполняется в сеансе обычного пользователя, у которого настроены ограничения доступа (например, по организациям, контрагентам, подразделениям или складам). Если в запросе используется ключевое слово РАЗРЕШЕННЫЕ, система принудительно отсекает данные, на которые у пользователя нет прав.
Как это влияет на виртуальную таблицу остатков?
Регистр взаиморасчетов хранит как документы отгрузки (Реализации), так и платежные документы (Платежные поручения, кассовые ордера), которые эти отгрузки закрывают. Если у пользователя настроен RLS, при котором он имеет право видеть накладную, но не имеет прав на просмотр платежного документа, закрывающего ее, происходит следующее:
Остатки рассчитывает баланс по накладной без учета скрытого платежа.Решение: Если запрос используется для реализации критически важной бизнес-логики (например, проверка лимита кредита или расчет дней задолженности), его выполнение необходимо перенести в привилегированный режим. Это позволит платформе рассчитать остатки по реальным данным базы данных, исключая искажения из-за ограничений прав пользователя.
// Переход в привилегированный режим в коде модуля
УстановитьПривилегированныйРежим(Истина);
Запрос = Новый Запрос;
// Настройка и выполнение запроса...
Выборка = Запрос.Выполнить().Выбрать();
УстановитьПривилегированныйРежим(Ложь);
Обратим внимание на то, как вызывается виртуальная таблица остатков в исходном коде автора:
РегистрНакопления.ВзаиморасчетыСКонтрагентамиПоДокументамРасчетов.Остатки( , ДоговорКонтрагента = &ДоговорКонтрагента ...)
Первый параметр виртуальной таблицы — Период — оставлен пустым. Проанализируем, к чему это ведет.
Когда параметр даты не задан, платформа рассчитывает остатки на текущую дату и время (на момент выполнения запроса). Если консоль запросов была запущена в один момент времени, а код в модуле — в другой (пусть даже разница составляет несколько секунд или миллисекунд), результаты могут отличаться. Особенно это критично, если в системе параллельно работают другие пользователи или фоновые задания, которые проводят документы в этот микроинтервал.
Более того, если запрос выполняется внутри транзакции (например, при проведении или записи), остатки «на сейчас» могут учитывать еще не зафиксированные движения текущего сеанса, в то время как консоль запросов в параллельном сеансе их гарантированно не увидит.
Решение: Всегда передавайте в параметры виртуальной таблицы явный момент времени. Если вам нужны остатки на актуальный момент времени при открытии формы, используйте объект Граница с исключением или включением движений конкретного документа.
// Использование границы для получения точных остатков
ГраницаВремени = Новый Граница(ТекущаяДата(), ВидГраницы.Включая);
Запрос.УстановитьПараметр("Период", ГраницаВремени);
Рассмотрим фрагмент кода, где формируется текст запроса:
"И " + ?(ЗначениеЗаполнено(АдресДоставки) И ТипЗнч(АдресДоставки) = Тип("СправочникСсылка.АдресаДоставки"), "И АдресДоставки = &АдресДоставки", "")
В этом условии кроется потенциальная опасность. Если в модуле формы переменная АдресДоставки по какой-то причине оказалась пустой, имела тип Неопределено или тип, отличный от СправочникСсылка.АдресаДоставки, условие отбора по адресу доставки просто не добавится в итоговый текст запроса.
При этом в консоли отчетов разработчик мог вручную жестко прописать этот параметр или скопировать запрос с уже зафиксированным условием. Если «Адрес доставки» является измерением регистра накопления, то отсутствие отбора по нему приведет к агрегации остатков по всем адресам данного контрагента. В результате закрытая в разрезе конкретного адреса накладная покажет общий долг по договору.
Решение: Обязательно проверяйте сформированный текст запроса в режиме отладки. Извлеките свойство Запрос.Текст непосредственно перед вызовом метода Выполнить() и сравните его символ в символ с тем текстом, который вы вставляете в консоль запросов.
Давайте проанализируем саму логику бизнес-задачи. Автор пытается найти дату первой сделки с контрагентом, используя для этого виртуальную таблицу Остатков.
С точки зрения методологии учета в 1С это грубая ошибка:
Остатки хранит информацию только по тем записям, по которым в данный момент есть ненулевой баланс (долг).Таким образом, запрос автора ищет не «дату первой сделки», а «дату самого старого неоплаченного долга». Если бизнес-требование заключается именно в поиске первой исторической сделки, запрашивать данные нужно из таблицы оборотов или напрямую из физической таблицы движений регистра.
Пример правильного запроса для получения даты первой сделки (через обороты):
ВЫБРАТЬ ПЕРВЫЕ 1
Взаиморасчеты.Период КАК ДатаПервойСделки
ИЗ
РегистрНакопления.ВзаиморасчетыСКонтрагентамиПоДокументамРасчетов.Обороты(, , Регистратор, ДоговорКонтрагента = &ДоговорКонтрагента) КАК Взаиморасчеты
УПОРЯДОЧИТЬ ПО
ДатаПервойСделки ВОЗР
Если же вам действительно нужно найти старейший неоплаченный долг, использование таблицы остатков оправдано. Однако в коде автора условие ГДЕ СуммаВзаиморасчетовОстаток > 0 наложено вне параметров виртуальной таблицы. Это заставляет СУБД сначала прочитать все остатки по договору, а затем отфильтровать их в секции ГДЕ, что снижает производительность.
Если вы столкнулись с расхождением данных между модулем и консолью, выполните следующие действия по шагам:
Запрос.Выполнить(), вычислите значение Запрос.Текст через табло и скопируйте полученную строку в консоль.АдресДоставки или ДоговорКонтрагента) точно соответствуют типам данных в базе, а не являются «битыми» ссылками или строками.Граница, чтобы зафиксировать состояние базы данных на определенный момент времени.