Почему не совпадают начальные и конечные остатки в виртуальной таблице «ОстаткиИОбороты» и как это исправить

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

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

В этой статье мы подробно разберем внутренние механизмы работы платформы 1С с виртуальными таблицами остатков и оборотов, выясним фундаментальные причины возникновения этой проблемы и рассмотрим практические варианты её решения.

В чем суть проблемы: разбор исходного запроса

Рассмотрим ситуацию на примере запроса, который часто пишут начинающие разработчики. В нем данные выбираются из регистра накопления РасчетыСКлиентами.ОстаткиИОбороты, помещаются во временную таблицу с группировкой, а затем из этой временной таблицы выбираются с отбором по организации:


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

При выполнении подобного запроса разработчик обнаруживает, что конечный остаток за Январь равен 100 000 рублей, а начальный остаток за Февраль внезапно становится равен 80 000 рублей. Куда исчезли 20 000 рублей? Ответ кроется в специфике СУБД и оптимизации запросов платформой 1С.

---

Причина №1. Отсутствие записей (движений) по аналитике в конкретном периоде

Когда мы указываем периодичность МЕСЯЦ в параметрах виртуальной таблицы, мы ожидаем, что СУБД вернет нам непрерывную цепочку данных по каждому ключевому значению. Однако СУБД работает иначе:

  1. Если по конкретному значению измерения АналитикаУчетаПоПартнерам в условном «Месяце 2» не было никаких движений (оборотов), то СУБД не формирует строку для этого ключа за этот месяц.
  2. В «Месяце 1» строка была (так как было движение), и там зафиксирован конечный остаток. В «Месяце 3» движение снова появилось — строка создана, и в ней вывелся начальный остаток (переданный из «Месяца 1»).
  3. Но в «Месяце 2» по этому контрагенту образовалась пустота. Когда во втором запросе мы производим агрегацию СУММА(РН.ОН) и СУММА(РН.ОК) в разрезе периодов, данные за «Месяц 2» по этому контрагенту просто не попадают в сумму. Соответственно, общий начальный остаток «Месяца 2» уменьшается на величину «зависшего» остатка этого контрагента.

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

---

Причина №2. Обращение к реквизитам измерений «через точку» и неверное наложение отборов

Проанализируем строку отбора во втором запросе: ГДЕ РН.Организация = &орг. Поле Организация получено как РН.АналитикаУчетаПоПартнерам.Организация. Здесь допущены сразу две серьезные ошибки проектирования:

  1. Отбор наложен на результат временной таблицы, а не в параметрах виртуальной таблицы. Это заставляет СУБД сначала рассчитать остатки по абсолютно всем контрагентам и организациям в базе данных за весь период, записать этот гигантский массив во временную таблицу втОстатки, и только потом отсечь лишнее. Это приводит к колоссальному падению производительности.
  2. Обращение через точку в группировке. Когда платформа транслирует запрос в SQL, она выполняет неявное левое соединение (LEFT JOIN) таблицы регистра с таблицей справочника партнеров, чтобы получить организацию. Если в группировку итогового запроса не входит само физическое измерение АналитикаУчетаПоПартнерам, СУБД не сможет корректно сопоставить и спроецировать рассчитанные остатки на результат выборки. Происходит искажение математического ожидания итогов.

---

Как правильно решить проблему на практике

Существует три основных архитектурных подхода к решению этой проблемы в зависимости от того, где и как выводятся данные.

Решение 1. Использование Системы компоновки данных (СКД)

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

Для этого выполните следующие шаги:

  1. Перенесите отбор по организации непосредственно в параметры виртуальной таблицы. Запрос должен выглядеть лаконично, без обращения к реквизитам через точку во внешних условиях:
    
    ВЫБРАТЬ
        РН.Период КАК Период,
        РН.АналитикаУчетаПоПартнерам КАК АналитикаУчетаПоПартнерам,
        РН.СуммаНачальныйОстаток КАК СуммаНачальныйОстаток,
        РН.СуммаКонечныйОстаток КАК СуммаКонечныйОстаток
    ИЗ
        РегистрНакопления.РасчетыСКлиентами.ОстаткиИОбороты(&ДН, &ДК, МЕСЯЦ, , АналитикаУчетаПоПартнерам.Организация = &Организация) КАК РН
    
  2. В схеме компоновки данных перейдите на вкладку Наборы данных.
  3. Для полей СуммаНачальныйОстаток и СуммаКонечныйОстаток обязательно настройте Роли. Укажите, что это поля остатка, задайте имя группы и свяжите их между собой (для начального остатка укажите связь с конечным, и наоборот).
  4. Для поля Период укажите роль периода с соответствующим типом и точностью.
  5. В настройках структуры отчета для группировки по периоду (месяцу) зайдите в свойства группировки и установите Тип дополнения — Месяц, а также укажите даты начала и окончания периода. СКД сама сгенерирует недостающие строки для пустых месяцев и виртуально «протянет» остатки по ним.

---

Решение 2. Использование координатной сетки в запросе (без СКД)

Если результат необходимо получить строго в коде (например, для программной обработки или выгрузки в Таблицу Значений), СКД использовать не получится. В этом случае нам нужно самостоятельно «выровнять» периоды в запросе при помощи генерации координатной сетки (календаря периодов).

Рассмотрим детальный алгоритм реализации этого метода:


// Шаг 1: Получаем все уникальные ключи аналитики, по которым были движения или остатки
ВЫБРАТЬ РАЗЛИЧНЫЕ
    РН.АналитикаУчетаПоПартнерам КАК АналитикаУчетаПоПартнерам
ПОМЕСТИТЬ втАналитики
ИЗ
    РегистрНакопления.РасчетыСКлиентами.ОстаткиИОбороты(&ДН, &ДК, МЕСЯЦ, , АналитикаУчетаПоПартнерам.Организация = &Организация) КАК РН
;

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

// Шаг 3: Перемножаем аналитики и периоды (Декартово произведение), чтобы получить полную сетку
ВЫБРАТЬ
    втПериоды.Период КАК Период,
    втАналитики.АналитикаУчетаПоПартнерам КАК АналитикаУчетаПоПартнерам
ПОМЕСТИТЬ втСетка
ИЗ
    втПериоды КАК втПериоды
    СОЕДИНИТЬ втАналитики КАК втАналитики ПО ИСТИНА
;

// Шаг 4: Соединяем сетку с реальными данными остатков и оборотов
ВЫБРАТЬ
    втСетка.Период КАК Период,
    втСетка.АналитикаУчетаПоПартнерам КАК АналитикаУчетаПоПартнерам,
    ЕСТЬNULL(ДанныеРН.СуммаНачальныйОстаток, 0) КАК СуммаНачальныйОстаток,
    ЕСТЬNULL(ДанныеРН.СуммаКонечныйОстаток, 0) КАК СуммаКонечныйОстаток
ИЗ
    втСетка КАК втСетка
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.FastОстатки КАК ДанныеРН 
        ПО втСетка.Период = ДанныеРН.Период 
        И втСетка.АналитикаУчетаПоПартнерам = ДанныеРН.АналитикаУчетаПоПартнерам

Этот метод гарантирует, что для каждого периода (даже «пустого», где не было движений) в результирующей таблице будет присутствовать строка с ключом аналитики. Однако, чтобы остатки правильно перетекали из месяца в месяц в пустых строках, потребуется дополнительно обработать полученную таблицу запросом либо программно на встроенном языке.

---

Решение 3. Программный расчет остатков на встроенном языке (наиболее производительный метод)

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

Проанализируем этот алгоритм на примере кода:


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

Запрос.УстановитьПараметр("ДН", НачалоДня(ДатаНачала));
Запрос.УстановитьПараметр("ДК", КонецДня(ДатаКонца));
Запрос.УстановитьПараметр("Организация", ВыбраннаяОрганизация);

РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

// Для хранения текущего остатка по каждой аналитике используем Соответствие
ТекущиеОстатки = Новый Соответствие;

// Заполняем первоначальные остатки на начало периода запросом к таблице Остатки...
// (код получения первоначального остатка опущен для лаконичности)

Пока Выборка.Следующий() Цикл
    Аналитика = Выборка.АналитикаУчетаПоПартнерам;
    Период = Выборка.Период;
    ОборотЗаМесяц = Выборка.Оборот;
    
    // Получаем накопленный остаток
    НачОстаток = ТекущиеОстатки.Получить(Аналитика);
    Если НачОстаток = Неопределено Тогда
        НачОстаток = 0; // Или значение из таблицы первоначальных остатков
    КонецЕсли;
    
    КонОстаток = НачОстаток + ОборотЗаМесяц;
    
    // Записываем новые данные обратно в соответствие для следующего месяца
    ТекущиеОстатки.Вставить(Аналитика, КонОстаток);
    
    // Выводим данные в результирующую таблицу
    Сообщить(СтрШаблон("Период: %1 | Аналитика: %2 | Нач: %3 | Изм: %4 | Кон: %5", 
        Формат(Период, "ДФ=MM.yyyy"), Аналитика, НачОстаток, ОборотЗаМесяц, КонОстаток));
КонецЦикла;

Данный подход полностью исключает влияние платформенных особенностей виртуальных таблиц остатков и оборотов на конечный результат. Мы оперируем физическими таблицами оборотов (которые работают в СУБД значительно быстрее виртуальных таблиц с периодичностью) и самостоятельно контролируем непрерывность математических расчетов.

← На главную