Почему СКД неправильно считает начальный и конечный остатки, и как это исправить при изменении оборотов в запросе?

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

При разработке отчетов в системе компоновки данных (СКД) на платформе 1С:Предприятие разработчики часто сталкиваются с ситуацией, когда виртуальная таблица ОстаткиИОбороты начинает выдавать непредсказуемые, искаженные или откровенно неверные значения начального и конечного остатка на уровнях группировок. Особенно остро эта проблема проявляется, когда в самом тексте запроса стандартные поля прихода и расхода подменяются пользовательскими формулами через конструкцию ВЫБОР КОГДА, а системные поля периодов заменяются бизнес-сущностями (например, заказом клиента).

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

Выясним причину: почему ломаются итоги в СКД?

Для того чтобы понять природу возникновения неверных остатков (например, когда начальный остаток внезапно принимает произвольное значение вместо положенного нуля), необходимо заглянуть под капот механизмов компоновки данных. Проанализируем три ключевые ошибки, которые допускают разработчики в запросах:

  1. Нарушение математического баланса (Кастомные обороты vs Физические остатки). Для СКД математическое равенство КонечныйОстаток = НачальныйОстаток + Приход - Расход является незыблемым правилом. На нем строятся все алгоритмы оптимизации запросов и расчета итогов по группировкам. Если мы берем физические поля остатков из таблицы ОстаткиИОбороты (НачисленоНачальныйОстаток и НачисленоКонечныйОстаток), но при этом искусственно переопределяем поля прихода и расхода через ВЫБОР КОГДА, это равенство моментально нарушается. Платформа пытается сопоставить измененные обороты с неизменными остатками из базы данных и, не найдя математического соответствия, выводит на уровне группировок хаотичные «мусорные» цифры.
  2. Подмена системного поля «Регистратор» другим объектом. Расчет хронологии остатков в СКД жестко привязан к временной шкале. Стандартная цепочка периодов выглядит как ПериодСекунда -> Регистратор. Если мы заменяем поле регистратора на другое измерение (например, Регистратор.ЗаказКлиента) и объявляем его в настройках роли как период, СКД теряет возможность однозначно определить хронологическую последовательность движений. Платформа больше не понимает, какая запись в пределах одной секунды была первой (для получения начального остатка), а какая последней (для получения конечного).
  3. Неявные соединения и проблема граничных записей (NULL-значения). При использовании периодичности Авто виртуальная таблица ОстаткиИОбороты генерирует специальные служебные записи на начало и конец периода отчета. У этих записей поле Регистратор всегда равно NULL. Обращение к реквизитам через точку (например, Регистратор.ЗаказКлиента) заставляет СУБД выполнять неявное левое соединение. В случае, когда регистратор равен NULL, это соединение может отсечь граничные записи или вернуть неверный результат, что полностью разрушает логику расчета остатков в СКД.

Почему решение с разделением наборов данных — это плохая практика?

Часто на форумах можно встретить совет: разделить один запрос на два набора данных — отдельно выбрать остатки, отдельно обороты, а затем связать их на вкладке «Связи наборов данных» или объединить. Рассмотрим подробнее, почему этот подход («костыль») крайне нежелателен в промышленной разработке:

Давайте разберем по шагам, как решить эту задачу правильно и элегантно.

Разберем по шагам решение №1: Настройка ролей полей в СКД

Если ваши изменения полей прихода и расхода носят косметический характер и вы хотите сохранить использование стандартных полей остатков из виртуальной таблицы, нам необходимо правильно настроить роли полей в схеме компоновки данных.

Рассмотрим структуру правильного запроса, в котором мы сохраняем оригинальные поля периодов и аккуратно обходим проблему пустых регистраторов:


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

После написания запроса переходим на вкладку «Набор данных» в СКД и настраиваем колонку «Роль» для наших полей строго по следующему алгоритму:

  1. Для поля ПериодСекунда указываем роль Период, Порядковый номер: 1.
  2. Для поля Регистратор указываем роль Период, Порядковый номер: 2. Обязательно устанавливаем флаг «Игнорировать NULL». Это укажет системе, что пустые регистраторы в граничных записях не нужно пытаться сопоставлять с реальными документами.
  3. Для поля ЗаказКлиента (если по нему предполагается делать группировку в отчете) задаем роль Измерение.
  4. Для поля НачальныйОстаток задаем роль Остатки. Имя: Начислено, Тип: Начальный.
  5. Для поля КонечныйОстаток задаем роль Остатки. Имя: Начислено (имена у начального и конечного остатка должны строго совпадать для установления логической связи), Тип: Конечный.

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

Проанализируем альтернативное решение №2: Расчет нарастающего итога (Профессиональный подход)

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

Единственный верный способ получить 100% точные остатки на любом уровне группировки — рассчитать их динамически прямо в СКД методом нарастающего итога. Рассмотрим, как это сделать:

Шаг 1. Корректируем запрос. Мы полностью убираем поля остатков из запроса и выбираем только обороты и начальный остаток на самую раннюю границу (точку старта):


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

Шаг 2. Добавляем вычисляемые поля. Переходим на вкладку «Вычисляемые поля» и создаем два поля:

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

Для поля КонечныйОстатокДинамический в выражении ресурса пишем формулу расчета нарастающего итога:


ВычислитьВыражение("Сумма(Приход - Расход)", "ПериодСекунда", , "Первая", "Текущая")

Для поля НачальныйОстатокДинамический выражение ресурса будет рассчитываться как конечный остаток минус обороты текущего периода:


ВычислитьВыражение("Сумма(Приход - Расход)", "ПериодСекунда", , "Первая", "Предыдущая")

Давайте подробно разберем параметры функции ВычислитьВыражение в нашем примере:

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

Заключение

Мы проанализировали внутренние механизмы СКД по расчету остатков и итогов. Попытки решить проблему «в лоб» через объединение наборов данных приводят к деградации производительности системы и ошибкам при наложении отборов. Использование правильной настройки ролей периодов (включая флаг «Игнорировать NULL») или переход на динамический расчет нарастающего итога с помощью функции ВычислитьВыражение — это стандарты качественной разработки на платформе 1С:Предприятие, гарантирующие стабильную и быструю работу ваших отчетов.

← На главную