Часто разработчики 1С сталкиваются с ситуацией, когда два документа, например, "Реализация товаров" и "Возврат от клиента", проводятся одной и той же датой. Логично ожидать, что в регистре накопления они будут следовать в порядке их проведения. Однако на практике запись о возврате может оказаться перед записью о реализации. Это приводит к серьезным проблемам, особенно при расчете остатков, когда запрос на момент проведения реализации вдруг "видит" будущий возврат и показывает неверные данные. Разберемся, почему так происходит и как этим управлять.
Проанализируем типичный случай: имеется два документа с датой 01.01.2020. Сначала проводится реализация, затем — возврат на ее основании. Но в движениях регистра накопления возврат стоит первой строкой. Как это контролировать и обеспечить правильную последовательность?
Ключ к пониманию этой ситуации лежит в объекте МоментВремени. Когда мы говорим о дате документа в 1С, на самом деле платформа оперирует более точным понятием — моментом времени. Он уникален для каждого документа и состоит из двух частей:
Когда несколько документов имеют абсолютно одинаковую дату и время (до секунды), платформа для обеспечения уникальности и порядка сортирует их по внутреннему представлению ссылки на документ. Это внутреннее представление не всегда совпадает с хронологией создания документов пользователем. Поэтому документ "Возврат...", созданный позже, может получить "меньший" идентификатор и оказаться в регистре раньше "Реализации...".
Именно эта особенность и приводит к путанице и ошибкам в расчетах, если не учитывать ее при написании запросов.
Рассмотрим код, который приводит к проблеме. Допустим, при открытии проведенного документа реализации нам нужно получить остатки товаров на момент его проведения. Часто можно встретить такой вариант запроса:
// НЕПРАВИЛЬНЫЙ вариант
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыОстатки.КоличествоОстаток
|ИЗ
| РегистрНакопления.МойРегистр.Остатки(&ДатаКон, ) КАК ТоварыОстатки";
// Здесь кроется ошибка!
Запрос.УстановитьПараметр("ДатаКон", Новый Граница(МоментВремени(), ВидГраницы.Исключая));
В чем здесь ошибка? Функция МоментВремени() без параметров возвращает текущий системный момент времени — тот момент, когда исполняется этот код. Это не имеет никакого отношения к моменту времени проведения документа, который был создан в прошлом.
В результате в параметр &ДатаКон попадает, например, "25.10.2023 15:30:00". Запрос к виртуальной таблице остатков вернет данные на этот момент, включив в расчет все движения, которые были сделаны в день проведения нашего документа (01.01.2020), в том числе и "неправильно отсортированный" возврат. Как следствие, мы получаем некорректные отрицательные остатки и ломаем всю дальнейшую логику.
Чтобы получить корректные остатки именно на момент проведения конкретного документа, нужно использовать не текущий момент времени, а момент времени самого этого документа. Сделать это можно с помощью метода МоментВремени(), вызванного у объекта или ссылки на документ.
Давайте исправим наш запрос:
// ПРАВИЛЬНЫЙ вариант
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыОстатки.КоличествоОстаток
|ИЗ
| РегистрНакопления.МойРегистр.Остатки(&МоментВремениДокумента, ) КАК ТоварыОстатки";
// Код выполняется в модуле формы или объекта документа
// "ЭтотОбъект" или "Ссылка" - ссылка на текущий документ
МоментПроведения = ЭтотОбъект.МоментВремени();
// Устанавливаем границу, ИСКЛЮЧАЯ движения самого документа
Запрос.УстановитьПараметр("МоментВремениДокумента", Новый Граница(МоментПроведения, ВидГраницы.Исключая));
Рассмотрим этот код подробнее:
ЭтотОбъект.МоментВремени().Граница. Этот объект позволяет точно позиционироваться на временной оси движений регистров.ВидГраницы.Исключая. Это критически важный момент. Он означает, что мы хотим получить срез данных (остатки) на момент времени строго до того, как наш документ сделал свои движения. Это позволяет нам увидеть "чистую" картину остатков перед проведением.Важно понимать разницу:
ВидГраницы.Исключая — движения, сделанные в указанный момент времени, не попадут в выборку. Идеально для контроля остатков перед проведением.ВидГраницы.Включая — движения, сделанные в указанный момент времени, попадут в выборку. Используется, когда нужно получить состояние после проведения документа.Автор в теме форума пришел к похожему решению, использовав Новый Граница(Дата, ВидГраницы.Исключая). Это тоже рабочий вариант, так как параметр Дата документа при подстановке в запрос будет неявно преобразован к моменту времени (началу секунды). Однако использование метода МоментВремени() является более явным, точным и предпочтительным.
Хотя правильная работа с запросами — это основной метод, существуют и другие подходы, которые могут быть полезны в некоторых ситуациях.
Самый простой, "силовой" метод — принудительно задать документам разное время. Например, в обработке проведения документа "Возврат" можно добавить код, который установит его время на одну секунду позже, чем у документа-основания "Реализация".
// В модуле объекта "ВозвратТоваровОтКлиента"
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Устанавливаем время только при интерактивном проведении,
// чтобы не мешать перепроведению документов
Если РежимЗаписи = РежимЗаписи.Проведение И ЭтоНовый() Тогда
Если ЗначениеЗаполнено(ДокументОснование) Тогда
// Устанавливаем время возврата на секунду позже реализации
ЭтотОбъект.Дата = ДокументОснование.Дата + 1;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Плюсы: Просто и гарантирует порядок.
Минусы: Не всегда применимо, может нарушать хронологию, если пользователь вручную меняет время, и является "костылем", который маскирует, а не решает проблему на уровне запросов.
В некоторых случаях проблемы с порядком документов указывают на то, что сама методология учета может быть неоптимальной. Например, для задач расчета себестоимости часто применяется не оперативный расчет в момент проведения, а отложенный. Все движения накапливаются за месяц, а затем специальный документ "Закрытие месяца" производит расчет средневзвешенной цены и корректирует движения всех документов реализации. Этот подход используется в типовых конфигурациях 1С, он более надежен и производителен, но требует существенного изменения архитектуры решения.
В итоге, столкнувшись с проблемой "неправильного" порядка записей в регистре, в первую очередь проверьте свои запросы. В 99% случаев решение кроется в корректном использовании объекта Граница и получении среза данных на момент времени документа, а не на текущий системный момент.