Почему двоятся итоги по остаткам в СКД при соединении с регистратором и как это исправить?

Программист 1С v8.3 (Управляемые формы) Управленческий учет IT и автоматизация бизнеса
← На главную

Разработка отчетов на Системе компоновки данных (СКД) в 1С — это мощный инструмент, который скрывает под капотом множество сложных механизмов. Один из самых частых и неприятных сюрпризов, с которым сталкиваются как начинающие, так и опытные разработчики — это искажение, задвоение или появление некорректных (в том числе минусовых) итогов по остаткам при попытке вывести детальные движения по регистраторам.

В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем внутренние механизмы СКД при расчете итогов и рассмотрим два эффективных способа решения: через тонкую настройку ролей полей и через «золотой стандарт» проектирования — разделение на несколько наборов данных.

---

В чем суть проблемы: почему СКД «сходит с ума» при левом соединении?

Рассмотрим типичную ситуацию. Нам необходимо построить отчет по остаткам и оборотам партий товаров. При этом нам нужно отфильтровать или изменить определенные движения (например, исключить внутренние управленческие операции или движения по конкретным видам документов), но сохранить вывод начальных и конечных остатков в разрезе номенклатуры и складов.

Часто разработчики идут по пути наименьшего сопротивления: пишут один сложный запрос. В нем они берут виртуальную таблицу ОстаткиИОбороты и связывают ее левым соединением с вложенным запросом, который выбирает только нужные детальные движения по регистраторам. На первый взгляд, логика выглядит рабочей:


ВЫБРАТЬ
    ПартииТоваровОстаткиИОбороты.Номенклатура,
    ПартииТоваровОстаткиИОбороты.Склад,
    ПартииТоваровОстаткиИОбороты.Партия,
    Движение.Регистратор,
    ПартииТоваровОстаткиИОбороты.КоличествоНачальныйОстаток,
    ПартииТоваровОстаткиИОбороты.КоличествоКонечныйОстаток,
    Движение.КоличествоПриход,
    Движение.КоличествоРасход
ИЗ
    РегистрНакопления.ПартииТоваров.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Авто, , ) КАК ПартииТоваровОстаткиИОбороты
ЛЕВОЕ СОЕДИНЕНИЕ 
    (ВЫБРАТЬ ... ИЗ РегистрНакопления.ПартииТоваров.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Регистратор, Движения, ) ...) КАК Движение
ПО 
    ПартииТоваровОстаткиИОбороты.Номенклатура = Движение.Номенклатура
    И ПартииТоваровОстаткиИОбороты.Склад = Движение.Склад
    И ПартииТоваровОстаткиИОбороты.Партия = Движение.Партия
    И ПартииТоваровОстаткиИОбороты.Регистратор = Движение.Регистратор

Что происходит на уровне СУБД при таком соединении? Если по одной номенклатурной позиции в выбранном периоде было несколько движений (несколько регистраторов), то строка с начальным и конечным остатком из левой таблицы размножится (продублируется) ровно столько раз, сколько движений было найдено в правой таблице. Например, если начальный остаток составлял 10 штук, а детальных движений было 5, то в результирующей выборке СУБД мы получим 5 строк, в каждой из которых начальный остаток будет равен 10.

Если СКД не дать дополнительных инструкций, то при расчете общих итогов или итогов по группировкам (например, по Складу или Номенклатуре) она применит стандартную функцию суммирования СУММА(). Вместо корректных 10 штук мы получим в итогах 50 штук! Произойдет классическое задвоение (в данном случае — запятерение) остатков.

---

Способ 1. Тонкая настройка ролей полей на вкладке «Наборы данных»

Если вы решили оставить логику в рамках одного запроса, вам придется вручную объяснить СКД, какие поля являются остатками, как они связаны и в какой временной последовательности они должны рассчитываться. Для этого перейдем на вкладку «Наборы данных» в схеме компоновки данных.

Шаг 1. Связывание парных полей остатков в группы

Для того чтобы СКД понимала, что остатки не нужно бездумно суммировать по детальным записям, их необходимо объединить в логические пары.

  1. Найдите поле начального остатка (например, КоличествоНачальныйОстаток) и в колонке «Роль» нажмите на кнопку настройки.
  2. В открывшемся окне укажите тип поля: Начальный остаток.
  3. В поле «Имя» (Имя группы) впишите понятное строковое имя, например, "Количество".
  4. Повторите операцию для конечного остатка (например, КоличествоКонечныйОстаток). Укажите тип поля: Конечный остаток.
  5. Критически важно: впишите в поле «Имя» точно такое же строковое имя — "Количество".

Благодаря одинаковому имени группы СКД поймет, что эти два поля — неразрывная пара «Начало-Конец» одного и того же ресурса. Расчет промежуточных и общих итогов теперь будет вестись по специальному алгоритму, учитывающему движение времени, а не простым сложением.

Шаг 2. Определение ролей для периодов

Если в группировках отчета используется Регистратор, то с точки зрения СКД он выступает не просто как ссылочное поле, а как период. Поскольку регистратор не является типом даты, СКД обязательно требует наличия реального поля времени в запросе (например, ПериодСекунда). Нам нужно правильно расставить роли для обоих полей.

  1. Для поля Регистратор откройте настройку роли. Установите флаг «Период».
  2. Задайте ему порядковый номер периода: 1 (это самый детальный уровень времени).
  3. Для поля ПериодСекунда также откройте настройку роли. Установите флаг «Период».
  4. Задайте ему порядковый номер периода: 2.

Важное правило иерархии периодов: периоды должны иметь строго последовательные номера уровней, начиная с 1 для самого младшего периода. Если вы перепутаете их местами или дадите одинаковые номера, СКД начнет некорректно сопоставлять остатки по временной оси, что приведет к ложным отрицательным остаткам на начало периода (проблема, описанная в Сообщении 4 на форуме).

Шаг 3. Игнорирование NULL значений при левом соединении

Поскольку мы используем левое соединение, для тех товаров, по которым не было движений в выбранном интервале времени, поле Регистратор вернет значение NULL. Это ломает логику расчета остатков по периодам.

Чтобы решить эту проблему, в настройке роли периода для поля Регистратор обязательно установите флаг «Игнорировать NULL». Это укажет СКД, что пустые значения регистратора не нужно интерпретировать как отдельную временную точку, и расчет остатков по таким строкам пройдет корректно.

Шаг 4. Настройка роли «Измерение» для вычисляемых полей

Часто в запросах используются вычисляемые выражения, заменяющие или дополняющие реальные измерения регистра (например, поле КодАртикул или СкладРемзона из примера автора). Если вы выводите эти поля в группировки вместо стандартных измерений, СКД теряет контекст.

Для таких полей на вкладке «Наборы данных» в свойствах роли установите флаг «Измерение», а в поле «Родитель» обязательно укажите настоящее измерение (например, для поля КодАртикул родителем укажите Номенклатура, а для поля СкладРемзона родителем укажите Склад). Это восстановит логическую цепочку агрегации данных.

---

Способ 2. Разделение на два набора данных — «Золотой стандарт» СКД

Настройка ролей в одном запросе — рабочий, но весьма хрупкий метод. Малейшее изменение структуры запроса или добавление новых условий может снова сломать расчет итогов. Профессиональные разработчики предпочитают использовать архитектурно правильный подход — разделение получения остатков и получения оборотов по разным наборам данных с их последующей связью средствами СКД.

Давайте разберем этот процесс по шагам.

Шаг 1. Создание НабораДанных1 (Остатки)

Этот набор данных будет отвечать исключительно за получение начальных и конечных остатков в разрезе измерений. Мы не используем здесь детализацию по регистраторам, поэтому строки гарантированно не будут дублироваться.


ВЫБРАТЬ
    ПартииТоваровОстатки.Номенклатура КАК Номенклатура,
    ПартииТоваровОстатки.Склад КАК Склад,
    ПартииТоваровОстатки.Партия КАК Партия,
    ПартииТоваровОстатки.КоличествоНачальныйОстаток КАК КоличествоНачальныйОстаток,
    ПартииТоваровОстатки.КоличествоКонечныйОстаток КАК КоличествоКонечныйОстаток
ИЗ
    РегистрНакопления.ПартииТоваров.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Авто, , ) КАК ПартииТоваровОстатки

Шаг 2. Создание НабораДанных2 (Движения)

Этот набор данных выбирает детальные обороты и регистраторы. Здесь мы можем применять любые фильтры по видам операций и документов, не боясь испортить картину по общим остаткам.


ВЫБРАТЬ
    ПартииТоваровОбороты.Номенклатура КАК Номенклатура,
    ПартииТоваровОбороты.Склад КАК Склад,
    ПартииТоваровОбороты.Партия КАК Партия,
    ПартииТоваровОбороты.Регистратор КАК Регистратор,
    ПартииТоваровОбороты.ПериодСекунда КАК ПериодСекунда,
    ПартииТоваровОбороты.КоличествоПриход КАК КоличествоПриход,
    ПартииТоваровОбороты.КоличествоРасход КАК КоличествоРасход
ИЗ
    РегистрНакопления.ПартииТоваров.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Регистратор, Движения, ) КАК ПартииТоваровОбороты
ГДЕ
    ПартииТоваровОбороты.Регистратор.ВидОперации <> &ВидОперации
    И ПартииТоваровОбороты.Регистратор.Управленческий = ЛОЖЬ

Шаг 3. Настройка связей наборов данных

Теперь перейдем на вкладку «Связи наборов данных». Нам нужно связать два созданных набора так, чтобы СКД самостоятельно выполнила их корректное слияние в памяти.

  1. Добавьте новую строку связи.
  2. Источник связи: выберите НаборДанных1.
  3. Приемник связи: выберите НаборДанных2.
  4. В колонке «Условие связи» сопоставьте ключевые поля:
    • Номенклатура = Номенклатура
    • Склад = Склад
    • Партия = Партия
  5. В колонках параметров связи передайте параметры начала и конца периода (это необходимо для того, чтобы СКД оптимизировала запросы к СУБД и не выбирала лишние движения по всей базе данных).

При таком подходе СКД получает данные об остатках из первого набора, а детальные движения — из второго. Слияние строк происходит на уровне компоновщика в оперативной памяти компьютера. Остатки рассчитываются строго один раз по уникальным комбинациям измерений, а обороты распределяются ниже по иерархии группировок. Результат получается стопроцентно точным, без риска получить задвоение или некорректный знак остатка.

---

Резюме и рекомендации

Мы проанализировали две разные стратегии борьбы с задвоением итогов в СКД. Подведем итоги:

← На главную