В практике разработки на платформе 1С:Предприятие 8.3 часто возникает задача построения отчета, который должен показывать остатки товаров, денежных средств или взаиморасчетов не только на моменты совершения операций, но и на каждую календарную дату внутри выбранного периода. Основная сложность заключается в том, что виртуальные таблицы остатков в 1С хранят данные только в точках изменения состояния, и если по счету или складу не было движений в течение недели, стандартный запрос не вернет строк за эти дни. В этой статье мы подробно разберем, как решить эту задачу эффективно и профессионально.
Прежде чем перейти к коду, давайте проанализируем ситуацию. Виртуальная таблица ОстаткиИОбороты при использовании периодичности День возвращает данные только за те дни, когда были зафиксированы движения (приходы или расходы). Если в понедельник товар пришел, а следующая продажа была только в пятницу, то за вторник, среду и четверг система в обычном режиме не выдаст строк. Однако пользователю в отчете важно видеть актуальный остаток на каждый из этих дней.
Для решения этой задачи мы воспользуемся проверенным методом: созданием временной таблицы всех дат за период и последующим соединением её с данными регистра. Разберем этот процесс по шагам.
Первым делом нам необходимо создать таблицу, в которой будут перечислены все дни выбранного периода. Существует несколько способов сделать это, но самым быстрым и правильным с точки зрения производительности является выборка из уже существующей таблицы с большим количеством записей (например, из журнала документов или регистра сведений) с ограничением количества строк.
Рассмотрим пример формирования таблицы дат:
ВЫБРАТЬ
ДОБАВИТЬКДАТЕ(&НачалоПериода, ДЕНЬ, Номера.Номер) КАК Период
ПОМЕСТИТЬ ВТ_Календарь
ИЗ
(ВЫБРАТЬ
(т1.Номер + т10.Номер * 10 + т100.Номер * 100) КАК Номер
ИЗ
(ВЫБРАТЬ 0 КАК Номер ОБЪЕДИНИТЬ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВЫБРАТЬ 9) КАК т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ (ВЫБРАТЬ 0 КАК Номер ОБЪЕДИНИТЬ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВЫБРАТЬ 9) КАК т10 ПО ИСТИНА
ВНУТРЕННЕЕ СОЕДИНЕНИЕ (ВЫБРАТЬ 0 КАК Номер ОБЪЕДИНИТЬ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВЫБРАТЬ 9) КАК т100 ПО ИСТИНА
) КАК Номера
ГДЕ
ДОБАВИТЬКДАТЕ(&НачалоПериода, ДЕНЬ, Номера.Номер) <= &КонецПериода
;
ИНДЕКСИРОВАТЬ ПО
Период
В данном фрагменте кода мы создаем последовательность чисел от 0 до 999 и прибавляем их к начальной дате. Это позволяет получить таблицу всех дат периода. Обратите внимание на использование инструкции ИНДЕКСИРОВАТЬ ПО — это критически важно для последующего соединения таблиц, особенно если период большой.
Теперь проанализируем, как получить данные из регистра. Нам нужны остатки на начало периода и все обороты внутри периода. Для этого лучше всего подходит виртуальная таблица ОстаткиИОбороты.
ВЫБРАТЬ
ТоварыНаСкладахОстаткиИОбороты.Период КАК Период,
ТоварыНаСкладахОстаткиИОбороты.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстаткиИОбороты.КоличествоКонечныйОстаток КАК КонечныйОстаток
ПОМЕСТИТЬ ВТ_ДанныеРегистра
ИЗ
РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, День, , ) КАК ТоварыНаСкладахОстаткиИОбороты
Выясним причину, по которой обычное соединение может не сработать. Если мы просто соединим даты с остатками, у нас все равно останутся «дырки» (NULL), так как остатки есть не на каждый день. Нам нужно использовать Левое соединение календаря с данными и последующую обработку итогов.
Рассмотрим пример итогового запроса с использованием механизма СОЕДИНЕНИЕ и функции ЕСТЬNULL:
ВЫБРАТЬ
Календарь.Период КАК Период,
Данные.Номенклатура КАК Номенклатура,
Данные.КонечныйОстаток КАК КонечныйОстаток
ИЗ
ВТ_Календарь КАК Календарь
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДанныеРегистра КАК Данные
ПО Календарь.Период >= Данные.Период
Внимание! Такое соединение «всех со всеми» по условию >= может привести к огромному количеству строк в результате (декартово произведение), что крайне негативно скажется на производительности. Проанализируем более оптимальный путь.
Если ваша задача — построить отчет, то самым эффективным методом будет использование Системы Компоновки Данных (СКД). В СКД есть встроенный механизм дополнения периодов. Разберем по шагам, как его настроить:
Период.День.В этом случае платформа сама сгенерирует недостающие строки и «протянет» остатки на пустые даты. Это избавляет программиста от написания сложных и медленных запросов вручную.
Если данные нужны не в отчете, а в алгоритме (например, для заполнения документа), проанализируем способ обхода выборки в цикле. Мы выбираем данные, упорядоченные по дате, и используем переменную для хранения «текущего остатка».
ТекущийОстаток = 0;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Если Выборка.КонечныйОстаток <> NULL Тогда
ТекущийОстаток = Выборка.КонечныйОстаток;
КонецЕсли;
// Теперь переменная ТекущийОстаток содержит актуальное значение
// даже для тех дат, по которым не было движений
Сообщить("Дата: " + Выборка.Период + " Остаток: " + ТекущийОстаток);
КонецЦикла;
Рассмотрим важные моменты, которые помогут ускорить работу системы:
МАКСИМУМ(Период), чтобы найти ближайшую дату «в прошлом», на которую было движение.Подведем итог. Мы проанализировали несколько подходов к получению остатков на каждую дату. Для отчетов идеальным выбором является механизм дополнения периодов в СКД. Для чистого кода на языке запросов оптимально использовать временную таблицу дат с последующим соединением и программной постобработкой или использованием коррелированных подзапросов (с осторожностью к производительности).
Используя предложенные методы, вы сможете создавать точные и быстрые отчеты, которые корректно отображают динамику изменения остатков в вашей системе 1С.