Мы, как разработчики и аналитики 1С, часто сталкиваемся с необходимостью получать данные об остатках и оборотах из регистров накопления. Виртуальная таблица ОстаткиИОбороты является мощным инструментом для этих целей. Однако иногда возникают ситуации, когда два, казалось бы, похожих запроса к этой таблице возвращают различные результаты. Давайте вместе разберем, почему это происходит и в чем заключается концептуальная ошибка в понимании работы с остатками.
Автор темы спрашивает, почему два его запроса возвращают разные результаты:
ВЫБРАТЬ
а.Склад КАК Склад,
СУММА(а.КоличествоНачальныйОстаток) КАК КоличествоНачальныйОстаток,
а.Период КАК Период
ИЗ
(ВЫБРАТЬ
ТоварыНаСкладахОстаткиИОбороты.Склад КАК Склад,
ТоварыНаСкладахОстаткиИОбороты.КоличествоНачальныйОстаток КАК КоличествоНачальныйОстаток,
ТоварыНаСкладахОстаткиИОбороты.КоличествоКонечныйОстаток КАК КоличествоКонечныйОстаток,
ТоварыНаСкладахОстаткиИОбороты.Период КАК Период
ИЗ
РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Месяц, , Склад = &Склад) КАК ТоварыНаСкладахОстаткиИОбороты) КАК а
СГРУППИРОВАТЬ ПО
а.Склад,
а.Период
ВЫБРАТЬ
а.Склад КАК Склад,
СУММА(а.КоличествоНачальныйОстаток) КАК КоличествоНачальныйОстаток,
а.Период КАК Период
ИЗ
(ВЫБРАТЬ
ТоварыНаСкладахОстаткиИОбороты.Склад КАК Склад,
ТоварыНаСкладахОстаткиИОбороты.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстаткиИОбороты.КоличествоНачальныйОстаток КАК КоличествоНачальныйОстаток,
ТоварыНаСкладахОстаткиИОбороты.КоличествоКонечныйОстаток КАК КоличествоКонечныйОстаток,
ТоварыНаСкладахОстаткиИОбороты.Период КАК Период
ИЗ
РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Месяц, , Склад = &Склад) КАК ТоварыНаСкладахОстаткиИОбороты) КАК а
СГРУППИРОВАТЬ ПО
а.Склад,
а.Период
Автор ожидает, что результаты этих двух запросов должны совпадать, но этого не происходит. Давайте выясним причину.
Первопричина расхождений кроется в фундаментальном непонимании принципа учета остатков. Поле КоличествоНачальныйОстаток, которое возвращает виртуальная таблица ОстаткиИОбороты с периодичностью, например, "Месяц", представляет собой остаток на начало каждого конкретного месяца в запрашиваемом интервале. Суммирование остатка на 1 января, остатка на 1 февраля, остатка на 1 марта и так далее является бессмысленной с точки зрения учета операцией.
Представим себе аналогию: если вы сложите свой баланс на банковском счете на первое число каждого месяца (например, 1000 руб. на 1 января, 1200 руб. на 1 февраля, 900 руб. на 1 марта), полученная сумма (1000 + 1200 + 900 = 3100 руб.) не будет иметь никакого практического значения. Она не отражает ни текущий баланс, ни общий приход за период, ни любой другой осмысленный показатель.
Остатки — это точечные значения на определенный момент времени, их суммирование не имеет учетного смысла. Правильным остатком на начало всего периода выборки будет только остаток на самое первое число этого периода. Для получения остатков на начало каждого последующего периода, нам необходимо взять начальный остаток первого периода и последовательно прибавлять к нему обороты за прошедшие периоды.
Как метко заметили коллеги на форуме:
(0) а зачем начальные и конечные остатки суммировать? Они не суммируются.
возьмешь по складу - начальный остаток будет один, возмешь в разрезе номенклатуры - будет 100 начальных остатков, возмешь по регистратору - будет тысячи остатков.
почему во всех трех случаях суммы начальных и конечных остатков должны совпадать??
Суммы оборотов - да, совпадают, а остатков - нет
И еще одно важное уточнение:
(0) В общем случае, правильным будет только начальный итог и конечный. Промежуточные итоги, нужно считать самому, складывая нач ост с оборотами за период.
Разница в результатах запросов также усугубляется техническими особенностями того, как платформа 1С формирует виртуальную таблицу ОстаткиИОбороты, особенно при добавлении дополнительных измерений (в данном случае — Номенклатура).
Виртуальная таблица ОстаткиИОбороты по умолчанию оптимизирована для производительности. Она не выводит строки для тех комбинаций измерений и периодов, по которым в данном конкретном периоде не было ни остатка на начало, ни оборотов (прихода/расхода). Это ключевой технический момент, который приводит к расхождениям между первым и вторым запросами.
Первый запрос (без детализации по Номенклатура):
В этом запросе мы получаем данные в разрезе только склада. Если в каком-то месяце по складу в целом (то есть, по любой из номенклатур на этом складе) было движение (приход или расход), то запись для этого месяца по складу будет сформирована. КоличествоНачальныйОстаток будет рассчитан корректно для всего склада на начало этого месяца.
Второй запрос (с детализацией по Номенклатура):
Здесь детализация гораздо выше – мы выбираем данные в разрезе склада и номенклатуры. Представим такую ситуацию: товар "Номенклатура1" имел остаток на начало февраля (например, 10 шт. на 01.02.2023), но в течение февраля по нему не было ни прихода, ни расхода. В этом случае, в результатах виртуальной таблицы за февраль строки для "Номенклатура1" может просто не быть. Поскольку нет движений, платформа считает, что выводить эту строку для оптимизации не нужно.
Соответственно, когда мы затем группируем результат по складу, сумма начальных остатков за февраль будет занижена, так как остаток по "Номенклатуре1" в нее не попадет.
Это объясняет, почему, по словам автора, "правильные остатки только в первом и последнем месяце выборки". На границах всего запрашиваемого интервала (&НачалоПериода и &КонецПериода) платформа принудительно рассчитывает и включает остатки, даже если движений не было. Но для промежуточных месяцев без движений по конкретной номенклатуре запись может отсутствовать, что приводит к неверному агрегированному итогу.
Коллеги на форуме подтверждают этот механизм:
(0) Если движений по номенклатуре не было, то в итогах на следующий месяц записи за период не будет (это ко 2-ой части запроса).
Т.е.:
01.01.2022 Номенклатура1 НачОстаток=10, Приход=0, Расход=0
01.02.2022 <Движений не было, записи нет>
01.03.2022 Номенклатура1 НачОстаток=10, Приход=0, Расход=1
Если по складу берете остатки, то по нему если движений за месяц не было, то записи тоже не будет. Выведет только записи по отборам в периоде.
Рассмотрим детальный пример, чтобы наглядно показать, почему суммирование начальных остатков в разных разрезах дает разные результаты, как было описано в сообщении 18:
(17) Детальная таблица(НачОст-Приход-Расход-КонОст):
1 3 2 2
2 5 2 5
5 2 3 4
СуммаНачОст = 1+2+5 = 8
Те же самые данные, только более укрупненно(НачОст-Приход-Расход-КонОст):
1 10 7 4
СуммаНачОст = 1
Внимание вопрос: почему не совпадают?
В этом примере мы видим, что детальные записи имеют разные начальные остатки. Когда мы суммируем эти детальные остатки (1+2+5=8), мы получаем одну сумму. Но когда мы смотрим на эти же данные в укрупненном виде, начальный остаток для укрупненного объекта будет только один (1). Суммировать начальные остатки детальных строк и сравнивать это с начальным остатком укрупненной строки — некорректно. Остаток на начало - это всегда одно значение на данный момент времени для данного набора измерений.
Если вам необходимы корректные начальные остатки на начало каждого месяца (или другой периодичности) в выбранном интервале, и вы хотите учитывать позиции без движений, необходимо изменить подход. Простое агрегирование КоличествоНачальныйОстаток по периодичности "Месяц" из ОстаткиИОбороты не даст вам корректного "скользящего" остатка по всем позициям в промежуточных месяцах без движений.
Для корректного построения отчета с остатками на начало каждого промежуточного периода (например, месяца), включая те периоды, когда не было движений, рекомендуется следующий подход:
Получение начального остатка на самую раннюю дату:
Сначала мы получаем остаток на начало всего запрашиваемого интервала по всем необходимым измерениям (например, Склад и Номенклатура). Для этого используем виртуальную таблицу Остатки:
ВЫБРАТЬ
ТоварыНаСкладахОстатки.Склад КАК Склад,
ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстатки.КоличествоОстаток КАК КоличествоНачальныйОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(&НачалоПериода, ) КАК ТоварыНаСкладахОстатки
ГДЕ
ТоварыНаСкладахОстатки.Склад = &Склад
Этот запрос даст нам стартовую точку для каждой пары Склад-Номенклатура.
Получение оборотов за каждый промежуточный период:
Затем мы получаем обороты (приход и расход) за каждый месяц в разрезе тех же измерений:
ВЫБРАТЬ
ТоварыНаСкладахОбороты.Период КАК Период,
ТоварыНаСкладахОбороты.Склад КАК Склад,
ТоварыНаСкладахОбороты.Номенклатура КАК Номенклатура,
СУММА(ТоварыНаСкладахОбороты.КоличествоПриход) КАК Приход,
СУММА(ТоварыНаСкладахОбороты.КоличествоРасход) КАК Расход
ИЗ
РегистрНакопления.ТоварыНаСкладах.Обороты(&НачалоПериода, &КонецПериода, Месяц, ) КАК ТоварыНаСкладахОбороты
ГДЕ
ТоварыНаСкладахОбороты.Склад = &Склад
СГРУППИРОВАТЬ ПО
ТоварыНаСкладахОбороты.Период,
ТоварыНаСкладахОбороты.Склад,
ТоварыНаСкладахОбороты.Номенклатура
Этот запрос предоставит нам все движения по каждому месяцу для каждой номенклатуры на складе.
Построение "скользящего" остатка:
После получения этих двух наборов данных (начальные остатки на начало всего периода и помесячные обороты), нам потребуется программно (в коде 1С) или с помощью более сложного запроса (например, с использованием временных таблиц, операторов объединения и упорядочивания) рассчитать "скользящий" остаток. Это означает, что для каждой пары Склад-Номенклатура:
КоличествоНачальныйОстаток на &НачалоПериода.ОстатокНаКонецМесяца = НачальныйОстатокНаНачалоПериода + ПриходЗаМесяц - РасходЗаМесяц.ОстатокНаКонецМесяца = ОстатокНаКонецПредыдущегоМесяца + ПриходЗаМесяц - РасходЗаМесяц.Если в каком-то месяце по конкретной номенклатуре не было оборотов, то ее остаток на конец этого месяца будет равен остатку на конец предыдущего месяца. Этот подход гарантирует, что мы получим корректные остатки на начало каждого промежуточного периода для всех номенклатур, которые имели остаток в пределах выбранного интервала, даже если по ним не было движений в конкретном месяце.
Таким образом, расхождение в результатах двух запросов к виртуальной таблице ОстаткиИОбороты обусловлено двумя основными факторами:
Концептуальная ошибка: Некорректное суммирование начальных остатков по различным промежуточным периодам. Остатки — это точечные значения на определенный момент времени, их суммирование не имеет учетного смысла.
Техническая особенность платформы: Оптимизация виртуальной таблицы ОстаткиИОбороты, которая по умолчанию не возвращает записи для комбинаций измерений и периодов, где не было оборотов. Это приводит к "пропускам" в данных при детализации, а затем к неверным агрегированным итогам при группировке по укрупненным измерениям.
Для корректного анализа остатков по периодам важно понимать, что НачальныйОстаток, возвращаемый для периодичности "Месяц", является остатком на начало конкретного месяца, а не накопительной суммой. Всегда стремитесь брать остаток на начало всего интервала и применять к нему обороты, чтобы получить актуальный остаток на любую последующую дату.