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