Почему во временной таблице запроса 1С появляются неверные (задвоенные) начальные остатки и как это исправить?

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

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

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

Исходная ситуация и анализ проблемы

Рассмотрим практический пример, с которым столкнулся автор темы на форуме. Изначально был написан простой и понятный запрос для получения данных по займам (счет 58.03) за определенный период:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ХозрасчетныйОстаткиИОбороты.Субконто1 КАК Контрагент,
    |	ХозрасчетныйОстаткиИОбороты.Subkonto2 КАК Договор,
    |	ХозрасчетныйОстаткиИОбороты.СуммаНачальныйОстатокДт КАК НачальныйОстаток,
    |	ХозрасчетныйОстаткиИОбороты.СуммаОборотДт КАК Выдано,
    |	ХозрасчетныйОстаткиИОбороты.СуммаОборотКт КАК Возвращено,
    |	ХозрасчетныйОстаткиИОбороты.СуммаКонечныйОстатокДт КАК КонечныйОстаток
    |ИЗ
    |	РегистрБухгалтерии.Хозрасчетный.ОстаткиИОбороты(&НачПериода, &КонПериода, Регистратор, , Счет = &СчетЗаймы, , ) КАК ХозрасчетныйОстаткиИОбороты
    |ИТОГИ
    |	СУММА(НачальныйОстаток),
    |	СУММА(Выдано),
    |	СУММА(Возвращено),
    |	СУММА(КонечныйОстаток)
    |ПО
    |	ОБЩИЕ,
    |	Контрагент,
    |	Договор";

При выполнении этого запроса напрямую всё работает идеально. Начальных остатков на начало выбранного периода нет, есть только обороты и верный конечный остаток. Результаты полностью совпадают с показателями стандартного отчета ОСВ по счету 58.03.

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


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

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

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

В чем причина искажения остатков при переносе в ВТ?

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

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

2. Что происходит при помещении данных во временную таблицу:
Временная таблица (в нашем примере — Займы) на уровне СУБД представляет собой обычную физическую таблицу в базе данных (типа #temp). Когда мы переносим туда данные, вся информация о метаданных, ролях полей, связи с регистрами и временных границах безвозвратно теряется. Поля НачальныйОстаток и КонечныйОстаток превращаются в обычные числовые колонки (типа NUMERIC), содержащие статические числа.

Поскольку в параметрах виртуальной таблицы была выбрана периодичность Регистратор, СУБД записала во временную таблицу детальные хронологические записи по каждому документу-регистратору за период. Например, если в течение сентября по договору было проведено три документа выдачи займа, во временной таблице появятся три строки:

Когда во втором запросе пакета мы обращаемся к этой временной таблице и пишем директиву ИТОГИ СУММА(НачальныйОстаток), СУММА(КонечныйОстаток), СУБД выполняет прямое математическое сложение всех числовых значений по колонкам. Она складывает 0 + 10 000 + 15 000 и получает итоговый начальный остаток 25 000 руб.! То же самое происходит и с конечным остатком (10 000 + 15 000 + 18 000 = 43 000 руб.). Это классическая ошибка сложения промежуточных балансовых показателей, аналогичная суммированию строк в Excel по всей колонке остатков.

---

Решение проблемы

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

Вариант 1. Отказ от периодичности по регистратору (Самый простой способ)

Если во временной таблице вам не нужны детальные записи в разрезе каждого первичного документа (регистратора), а требуются лишь общие итоги по контрагентам и договорам за период, достаточно просто убрать периодичность Регистратор из параметров виртуальной таблицы.

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


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

Что изменилось? Теперь СУБД сразу сворачивает все записи за выбранный период до уникальных пар "Контрагент - Договор". Во временную таблицу попадает ровно одна запись по каждому договору с чистым начальным остатком на &НачПериода и конечным остатком на &КонПериода. При последующем суммировании во втором запросе складывать промежуточные остатки больше не с чем, поэтому итоговые показатели рассчитываются абсолютно корректно.

Вариант 2. Настройка ролей полей в Системе компоновки данных (СКД)

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

Рассмотрим пошаговый алгоритм настройки ролей:

  1. Откройте схему компоновки данных и перейдите на вкладку Наборы данных.
  2. Найдите ваш набор данных - запрос и выберите поля из временной таблицы (НачальныйОстаток и КонечныйОстаток).
  3. В колонке Путь к данным убедитесь, что имена полей указаны корректно.
  4. Перейдите в колонку Роль для поля НачальныйОстаток и нажмите кнопку выбора. В открывшемся окне укажите:
    • Тип: Остатки;
    • Имя группы: Сумма (любое произвольное имя для связи начального и конечного остатка);
    • Тип остатка: Начальный;
    • Поле связи с периодом: укажите поле периода (например, Регистратор или ПериодСекунда).
  5. Для поля КонечныйОстаток выполните аналогичную настройку, но укажите тип остатка — Конечный, а имя группы должно совпадать с именем группы начального остатка (в нашем примере — Сумма).
  6. Для поля периода (Регистратор или Период) настройте роль Период с указанием его порядкового номера на временной оси.

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

Вариант 3. Математический расчет остатков во втором запросе

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

В этом случае применяется расчетный метод:

  1. Обороты (Выдано, Возвращено) во втором запросе суммируются стандартным математическим образом через СУММА().
  2. Чистый начальный остаток получается в отдельной временной таблице без детализации по периодам/регистраторам и соединяется с общим пакетом.
  3. Конечный остаток не выбирается напрямую из детальных записей, а рассчитывается по классической математической формуле:
    Конечный остаток = Начальный остаток + Оборот Дт - Оборот Кт.

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ХозрасчетныйОстатки.Субконто1 КАК Контрагент,
    |	ХозрасчетныйОстатки.Субконто2 КАК Договор,
    |	ХозрасчетныйОстатки.СуммаОстатокДт КАК НачальныйОстаток
    |ПОМЕСТИТЬ ВТ_НачальныеОстатки
    |ИЗ
    |	РегистрБухгалтерии.Хозрасчетный.Остатки(&НачПериода, Счет = &СчетЗаймы, , ) КАК ХозрасчетныйОстатки
    |;
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ХозрасчетныйОбороты.Субконто1 КАК Контрагент,
    |	ХозрасчетныйОбороты.Субконто2 КАК Договор,
    |	ХозрасчетныйОбороты.Регистратор КАК Регистратор,
    |	ХозрасчетныйОбороты.СуммаОборотДт КАК Выдано,
    |	ХозрасчетныйОбороты.СуммаОборотКт КАК Возвращено
    |ПОМЕСТИТЬ ВТ_ОборотыСДетализацией
    |ИЗ
    |	РегистрБухгалтерии.Хозрасчетный.Обороты(&НачПериода, &КонПериода, Регистратор, Счет = &СчетЗаймы, , , , ) КАК ХозрасчетныйОбороты
    |;
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ЕСТЬNULL(ВТ_ОборотыСДетализацией.Контрагент, ВТ_НачальныеОстатки.Контрагент) КАК Контрагент,
    |	ЕСТЬNULL(ВТ_ОборотыСДетализацией.Договор, ВТ_НачальныеОстатки.Договор) КАК Договор,
    |	ВТ_ОборотыСДетализацией.Регистратор КАК Регистратор,
    |	ЕСТЬNULL(ВТ_НачальныеОстатки.НачальныйОстаток, 0) КАК НачальныйОстаток,
    |	ЕСТЬNULL(ВТ_ОборотыСДетализацией.Выдано, 0) КАК Выдано,
    |	ЕСТЬNULL(ВТ_ОборотыСДетализацией.Возвращено, 0) КАК Возвращено
    |ПОМЕСТИТЬ ВТ_ИтоговыеДанные
    |ИЗ
    |	ВТ_ОборотыСДетализацией КАК ВТ_ОборотыСДетализацией
    |		ПОЛНОЕ СОЕДИНЕНИЕ ВТ_НачальныеОстатки КАК ВТ_НачальныеОстатки
    |		ПО ВТ_ОборотыСДетализацией.Контрагент = ВТ_НачальныеОстатки.Контрагент
    |			И ВТ_ОборотыСДетализацией.Договор = ВТ_НачальныеОстатки.Договор
    |;
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ВТ_ИтоговыеДанные.Контрагент КАК Контрагент,
    |	ВТ_ИтоговыеДанные.Договор КАК Договор,
    |	ВТ_ИтоговыеДанные.Регистратор КАК Регистратор,
    |	ВТ_ИтоговыеДанные.НачальныйОстаток КАК НачальныйОстаток,
    |	ВТ_ИтоговыеДанные.Выдано КАК Выдано,
    |	ВТ_ИтоговыеДанные.Возвращено КАК Возвращено,
    |	ВТ_ИтоговыеДанные.НачальныйОстаток + ВТ_ИтоговыеДанные.Выдано - ВТ_ИтоговыеДанные.Возвращено КАК КонечныйОстаток
    |ИЗ
    |	ВТ_ИтоговыеДанные КАК ВТ_ИтоговыеДанные
    |ИТОГИ
    |	МАКСИМУМ(НачальныйОстаток),
    |	СУММА(Выдано),
    |	СУММА(Возвращено),
    |	МАКСИМУМ(НачальныйОстаток) + СУММА(Выдано) - СУММА(Возвращено) КАК КонечныйОстаток
    |ПО
    |	ОБЩИЕ,
    |	Контрагент,
    |	Договор";

Разберем подробнее логику этого пакета запросов:

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

Для предотвращения ошибок расчета остатков в запросах 1С рекомендуем придерживаться простых правил:

← На главную