Одной из классических задач в разработке на платформе 1С является получение остатков на каждую дату внутри определенного периода. Это необходимо для построения графиков, анализа оборачиваемости или формирования отчетов по динамике складских запасов. Основная сложность заключается в том, что виртуальные таблицы остатков в 1С выдают данные только на конкретный момент, а таблицы остатков и оборотов — только за те дни, когда совершались движения. Если в какой-то день движений не было, запрос просто «пропустит» эту дату. Разберем по шагам, как правильно сформировать запрос, чтобы получить корректные остатки на конец каждого дня.
Проанализируем ситуацию, с которой столкнулся автор темы. При получении остатков на каждый день часто возникает путаница с временными границами. В 1С дата без указания времени (например, 01.01.2023) технически является началом дня (01.01.2023 00:00:00). Если мы хотим получить остаток, актуальный на конец суток, нам необходимо явно указывать время 23:59:59. В противном случае движения, совершенные в течение дня, не попадут в расчет остатка на эту дату.
Для того чтобы в итоговой выборке были все дни, даже те, по которым не было движений, нам нужно сначала создать временную таблицу, содержащую непрерывную последовательность дат. Рассмотрим популярный метод генерации чисел, который затем преобразуется в даты. Мы используем перекрестное соединение (CROSS JOIN) нескольких небольших выборок, чтобы получить нужное количество строк (например, для покрытия периода в несколько лет).
Рассмотрим пример кода для генерации таблицы дат с использованием функции ДОБАВИТЬКДАТЕ:
ВЫБРАТЬ
КОНЕЦПЕРИОДА(ДОБАВИТЬКДАТЕ(&НачалоПериода, ДЕНЬ, t.n), ДЕНЬ) КАК Дата
ПОМЕСТИТЬ ТаблицаДат
ИЗ (
ВЫБРАТЬ
t1.a * 10 + t2.b КАК n
ИЗ
(ВЫБРАТЬ 0 КАК a ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 3) КАК t1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ
(ВЫБРАТЬ 0 КАК b ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 9) КАК t2
ПО ИСТИНА
) КАК t
ГДЕ
ДОБАВИТЬКДАТЕ(&НачалоПериода, ДЕНЬ, t.n) <= &КонецПериода;
Важный момент: использование функции КОНЕЦПЕРИОДА гарантирует, что каждая дата в нашей таблице будет установлена на последнюю секунду дня (23:59:59). Это критически важно для корректного соединения с таблицей остатков.
Теперь нам нужно получить данные из регистра накопления. Использование виртуальной таблицы ОстаткиИОбороты с периодичностью ДЕНЬ — это хороший способ получить данные о конечных остатках на те даты, когда были изменения. Проанализируем пример получения таких данных:
ВЫБРАТЬ
ТоварыНаСкладахОстаткиИОбороты.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстаткиИОбороты.Период КАК Период,
ТоварыНаСкладахОстаткиИОбороты.ВНаличииКонечныйОстаток КАК Остаток
ПОМЕСТИТЬ РеальныеОстатки
ИЗ
РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, День, , Номенклатура В (&СписокНоменклатуры)) КАК ТоварыНаСкладахОстаткиИОбороты;
Это самый сложный этап. Нам нужно для каждой даты из ТаблицаДат найти «самый свежий» доступный остаток из таблицы РеальныеОстатки. Выясним причину, по которой обычное левое соединение здесь не сработает: если 5-го числа движений не было, а 4-го были, то простое соединение по равенству дат оставит 5-е число с пустым (NULL) значением.
Для решения применим соединение по условию РеальныеОстатки.Период <= ТаблицаДат.Дата с последующей группировкой. Мы найдем максимальную дату периода, которая меньше или равна текущей дате из таблицы дат, и возьмем остаток именно на ту дату.
Посмотрим на пример реализации этой логики:
ВЫБРАТЬ
Дни.Дата КАК Период,
Движения.Номенклатура КАК Номенклатура,
МАКСИМУМ(Движения.Период) КАК ПоследняяДатаСДвижением
ПОМЕСТИТЬ ДатыПоследнихОстатков
ИЗ
ТаблицаДат КАК Дни
ЛЕВОЕ СОЕДИНЕНИЕ РеальныеОстатки КАК Движения
ПО Движения.Период <= Дни.Дата
СГРУППИРОВАТЬ ПО
Дни.Дата,
Движения.Номенклатура;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ДатыПоследнихОстатков.Период КАК Дата,
ДатыПоследнихОстатков.Номенклатура КАК Номенклатура,
ЕСТЬNULL(РеальныеОстатки.Остаток, 0) КАК ОстатокКонечный
ИЗ
ДатыПоследнихОстатков КАК ДатыПоследнихОстатков
ЛЕВОЕ СОЕДИНЕНИЕ РеальныеОстатки КАК РеальныеОстатков
ПО ДатыПоследнихОстатков.Номенклатура = РеальныеОстатки.Номенклатура
И ДатыПоследнихОстатков.ПоследняяДатаСДвижением = РеальныеОстатки.Период;
Рассмотрим более современный и производительный способ решения этой задачи без написания сложных многоуровневых запросов. В Системе компоновки данных (СКД) предусмотрен встроенный механизм дополнения периодов.
ОстаткиИОбороты с периодичностью День.Период.День.При таком подходе СКД сама сгенерирует недостающие строки для дней без движений и автоматически «протянет» значения ресурсов (остатков) из предыдущих периодов. Это избавляет от необходимости вручную создавать ТаблицаДат и выполнять тяжелые соединения типа «меньше или равно», которые могут существенно замедлять работу базы данных при больших объемах данных.
Проанализируем ситуацию с нагрузкой на сервер. Запросы, использующие соединение <=, имеют квадратичную сложность и могут выполняться очень долго на больших справочниках номенклатуры. Чтобы ускорить работу, придерживаемся следующих правил:
Номенклатура, Дата, Период) с помощью инструкции ИНДЕКСИРОВАТЬ ПО.РеальныеОстатки.ДанныеПроизводственногоКалендаря (в типовых конфигурациях). Это быстрее и проще.Граница(КонецДня(НужнаяДата), ВидГраницы.Включая), чтобы гарантированно захватить документы, записанные последней секундой дня.Таким образом, мы рассмотрели два основных метода получения ежедневных остатков: через «ручную» генерацию таблицы дат в запросе и через механизмы дополнения периодов в СКД. Выбор метода зависит от конкретной задачи, но для формирования отчетов СКД является предпочтительным вариантом.