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