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