При работе с запросами в 1С многие разработчики сталкиваются с ситуацией, которая на первый взгляд кажется нелогичной. При попытке получить остатки на конец дня с помощью функции КОНЕЦПЕРИОДА(&Дата, "ДЕНЬ"), которая возвращает время 23:59:59, движения, сделанные ровно в эту секунду, не попадают в результат. При этом та же самая конструкция в виртуальной таблице оборотов работает корректно, включая все движения до конца дня. Это несоответствие порождает вопросы и может стать настоящей ловушкой.
Давайте подробно разберемся, почему так происходит, и какие существуют правильные способы получения остатков на конец периода.
Основная причина такого поведения кроется в фундаментальном различии предназначения и механизма работы виртуальных таблиц Остатки и Обороты.
Виртуальная таблица Обороты предназначена для анализа движений за интервал времени. Когда мы задаем ей начальную и конечную точки, она собирает все записи регистра, которые попали в этот интервал, включая его границы. Это логично, ведь оборот — это сумма всех операций за период.
Виртуальная таблица Остатки, напротив, показывает срез данных на определенный момент времени. Это состояние системы, "фотография" данных. Когда мы передаем в ее параметры дату и время, например 2023-12-31 23:59:59, платформа по умолчанию показывает нам остатки на начало этой секунды. То есть, она показывает состояние системы до того, как произошли события в указанный момент времени.
Это поведение подтверждается и официальной документацией. В синтакс-помощнике для параметра <МоментВремени> таблицы Остатки сказано:
"...если МоментВремени - то в остатках не будут учтены движения регистра, произошедшие в тот же момент времени."
Платформа для оптимизации рассчитывает и хранит итоги на определенные, заранее заданные периоды (например, на начало месяца, начало дня). Чтобы получить остатки на произвольный момент, она берет ближайшие рассчитанные итоги и "докручивает" их движениями до нужного момента, но не включая его. Это сделано для обеспечения высокой производительности, но требует от разработчика понимания этого механизма.
Зная причину проблемы, мы можем легко ее обойти, используя инструменты, которые предоставляет платформа. Рассмотрим три наиболее распространенных и правильных подхода.
Это самый правильный, явный и "идеологически верный" способ. В платформе 1С существует специальный объект Граница, который предназначен именно для таких случаев — когда нужно точно указать, включать или не включать пограничное значение в интервал.
Объект Граница содержит два параметра: само значение (наша дата) и вид границы (Включая или Исключая).
Чтобы получить остатки с учетом всех движений до конца дня, нужно передать в параметр виртуальной таблицы не просто дату, а объект Граница с видом Включая.
Посмотрим на пример:
Пусть у нас есть параметр запроса &КонПериода, в котором хранится дата, на конец которой мы хотим получить остатки.
ВЫБРАТЬ
ТоварыНаСкладахОстатки.Номенклатура,
ТоварыНаСкладахОстатки.КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(
&ГраницаПериода,
Номенклатура В (&СписокНоменклатуры)) КАК ТоварыНаСкладахОстатки
В коде на 1С перед выполнением запроса мы должны правильно установить значение параметра &ГраницаПериода:
// Получаем конец дня (например, 31.12.2023 23:59:59)
КонецДня = КОНЕЦПЕРИОДА(КонПериода, "ДЕНЬ");
// Устанавливаем параметр запроса с использованием объекта Граница
Запрос.УстановитьПараметр("ГраницаПериода", Новый Граница(КонецДня, ВидГраницы.Включая));
Такой код явно указывает платформе: "Рассчитай остатки на момент времени КонецДня и, пожалуйста, включи в расчет все движения, которые произошли ровно в этот момент". Это наиболее читаемый и надежный подход.
Это старый и проверенный прием, который также дает корректный результат. Логика его проста: остатки на конец дня 31.12.2023 — это то же самое, что остатки на самое начало следующего дня, то есть на 01.01.2024 00:00:00.
Поскольку виртуальная таблица Остатки по умолчанию работает в режиме "не включая", то, указав в качестве момента времени начало следующего дня, мы получим срез, который учтет все движения предыдущего дня, включая 23:59:59.
Посмотрим на пример:
В запросе параметр остается тем же:
ВЫБРАТЬ
ТоварыНаСкладахОстатки.Номенклатура,
ТоварыНаСкладахОстатки.КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(
&МоментВремени,
Номенклатура В (&СписокНоменклатуры)) КАК ТоварыНаСкладахОстатки
А вот установка параметра будет выглядеть иначе:
// Получаем конец дня для нашей даты
КонецДня = КОНЕЦПЕРИОДА(КонПериода, "ДЕНЬ");
// Прибавляем одну секунду, чтобы получить начало следующего дня
НачалоСледующегоДня = ДОБАВИТЬКДАТЕ(КонецДня, "СЕКУНДА", 1);
// Альтернативный и более надежный вариант:
// НачалоСледующегоДня = НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(КонПериода, "ДЕНЬ", 1), "ДЕНЬ");
// Устанавливаем параметр запроса
Запрос.УстановитьПараметр("МоментВремени", НачалоСледующегоДня);
Этот способ абсолютно рабочий, но менее явный, чем использование Границы. Разработчик, который будет читать этот код позже, должен будет догадаться о причине такого смещения даты.
Если вы работаете с отчетами, особенно на СКД (Система Компоновки Данных), или используете стандартный объект Период, то платформа часто берет работу с границами на себя. У стандартного периода свойство ДатаОкончания уже является границей, включающей конечный момент времени. Для реализации более сложных вариантов вычислений в отчетах можно применить методику расчета нарастающего итога и дельты в СКД.
При настройке параметров виртуальной таблицы в конструкторе запроса на СКД, если вы используете стандартный период, система автоматически подставит правильные значения границ. В качестве примера построения подобных отчетов по регистру «Товары на складах» можно использовать разработку для вывода движений товаров с расчетом нарастающего итога.
В коде это может выглядеть так:
// Где-то в отчете у нас есть стандартный параметр Период
// Его свойство ДатаОкончания уже содержит нужную нам Границу
// Мы просто передаем его в запрос
Запрос.УстановитьПараметр("КонецПериода", Период.ДатаОкончания);
Этот способ удобен при работе со стандартными механизмами, но при написании произвольных запросов лучше контролировать границы явно через первые два способа.
Проанализировав ситуацию, мы видим, что "проблема" с невключением последней секунды дня в таблицу остатков — это не ошибка, а особенность архитектуры платформы 1С. Ключ к пониманию лежит в разнице между срезом на момент времени (Остатки) и данными за интервал (Обороты). Для точного управления границами запроса всегда используйте объект Граница — это сделает ваш код более надежным, понятным и предсказуемым. Помните об этой особенности, и вы избежите многих часов отладки и поиска "пропавших" данных.