При разработке отчетов в системе «1С:Предприятие 8» программисты часто сталкиваются с необходимостью вывести остатки по счетам в разрезе месяцев. Типичная задача — построить кросс-таблицу, где в строках выводятся счета или субконто, а в колонках — месяцы отчетного года. Однако при попытке решить эту задачу простым запросом к виртуальной таблице остатков и оборотов разработчики сталкиваются с серьезным препятствием: пропуском периодов, в которых не было движений.
В этой статье мы подробно разберем, почему возникает проблема «выпадающих» месяцев, как её элегантно решить с помощью Системы компоновки данных (СКД), рассмотрим альтернативные варианты реализации на уровне чистого запроса, а также разберем важную методологическую ошибку, связанную с попыткой получить остатки с корреспонденцией счетов.
Рассмотрим стандартное обращение к виртуальной таблице РегистрБухгалтерии.Хозрасчетный.ОстаткиИОбороты. Если мы укажем периодичность Месяц, то СУБД вернет строки только за те периоды, в которых были зафиксированы реальные хозяйственные операции (обороты) по выбранным счетам.
Представим ситуацию: на начало года у нас числится дебетовый остаток по счету 60.01 в размере 50 000 рублей. В январе, феврале и марте никаких движений по этому контрагенту не происходило. Если мы сделаем обычный запрос за первый квартал с периодичностью по месяцам, то система вернет нам пустой результат для февраля и марта, либо вообще проигнорирует эти месяцы в выборке. Для построения красивого и достоверного отчета нам обязательно нужно, чтобы остаток «перетекал» из месяца в месяц, даже если в них не было оборотов.
Выясним, какими способами можно решить эту задачу в платформе «1С:Предприятие 8».
---
Если ваш отчет строится на базе Системы компоновки данных (СКД), то платформа берет на себя всю рутину по расчету и переносу остатков на «пустые» периоды. Это самый быстрый, надежный и рекомендуемый способ, который не создает лишней нагрузки на СУБД.
Разберем по шагам, как правильно настроить СКД для решения нашей задачи.
Шаг 1. Настройка ролей полей в наборе данных
Для того чтобы СКД понимала, как правильно рассчитывать остатки на начало и конец каждого периода, необходимо задать роли для соответствующих полей на вкладке «Набор данных»:
ПериодМесяц) и откройте его свойства в колонке «Роль».СуммаНачальныйОстатокДебет) настройте роль «НачальныйОстаток». В свойствах роли обязательно укажите связь с вашим полем периода (ПериодМесяц) и выберите имя противоположного остатка (конечного).СуммаКонечныйОстатокДебет) аналогично настройте роль «КонечныйОстаток», связав его с полем периода.Без этих настроек СКД не сможет корректно «протянуть» остатки по периодам, и вы получите неверные данные при группировке.
Шаг 2. Настройка дополнения в структуре отчета
Теперь перейдем на вкладку «Настройки» в СКД, где определяется структура вывода данных:
ПериодМесяц.&НачалоПериода и &КонецПериода).После выполнения этих настроек СКД автоматически сгенерирует недостающие колонки месяцев в кросс-таблице и заполнит их актуальными остатками, перенесенными из предыдущих периодов.
---
Если перед вами стоит задача получить остатки программно в кодовой базе (например, для последующей выгрузки в JSON, отправки по API или формирования специфического табличного документа вручную), то стандартные механизмы СКД применить не удастся. Нам потребуется составить оптимальный SQL-запрос.
Для крупных баз данных самым производительным решением является сборка запроса через конструкцию ОБЪЕДИНИТЬ ВСЕ для каждого месяца. Мы получаем срез остатков на конкретные даты (границы месяцев) и объединяем их в одну результирующую таблицу.
Проанализируем пример кода на языке 1С, который динамически собирает такой запрос:
// Определяем массив дат, на которые нам нужны остатки (например, концы месяцев года)
МассивДат = Новый Массив;
ТекущаяДата = НачалоМесяца(ДатаНачала);
Пока ТекущаяДата <= КонецМесяца(ДатаОкончания) Цикл
МассивДат.Добавить(КонецМесяца(ТекущаяДата));
ТекущаяДата = ДобавитьМесяц(ТекущаяДата, 1);
КонецЦикла;
// Начинаем динамическое построение текста запроса
ТекстЗапроса = "";
ПараметрыЗапроса = Новый Структура;
Для Индекс = 0 По МассивДат.Количество() - 1 Цикл
ИмяПараметра = "ДатаГраницы_" + Формат(Индекс, "ЧГ=0");
ПараметрыЗапроса.Вставить(ИмяПараметра, МассивДат[Индекс]);
Если Индекс > 0 Тогда
ТекстЗапроса = ТекстЗапроса + "
|ОБЪЕДИНИТЬ ВСЕ
|";
КонецЕсли;
ТекстЗапроса = ТекстЗапроса + "
|ВЫБРАТЬ
| &" + ИмяПараметра + " КАК ПериодМесяц,
| Остатки.Счет КАК Счет,
| Остатки.СуммаОстатокДт КАК СуммаОстатка
|ИЗ
| РегистрБухгалтерии.Хозрасчетный.Остатки(&" + ИмяПараметра + ", Счет В (&СписокСчетов), , ) КАК Остатки";
КонецЦикла;
Запрос = Новый Запрос(ТекстЗапроса);
Для Каждого КлючИЗначение Из ПараметрыЗапроса Цикл
Запрос.УстановитьПараметр(КлючИЗначение.Ключ, КлючИЗначение.Значение);
КонецЦикла;
Запрос.УстановитьПараметр("СписокСчетов", МассивИнтересующихСчетов);
РезультатЗапроса = Запрос.Выполнить();
Этот подход работает максимально быстро на больших базах данных, так как СУБД выполняет простые и прямолинейные выборки остатков на конкретные контрольные точки без сложных математических расчетов нарастающим итогом «на лету».
---
Еще один вариант получения непрерывных периодов в чистом запросе — это использование левого соединения с таблицей дат. В качестве источника непрерывного временного ряда обычно выступает производственный календарь (регистр сведений ДанныеПроизводственногоКалендаря) или аналогичная вспомогательная таблица.
Посмотрим на пример построения такого запроса:
ВЫБРАТЬ РАЗЛИЧНЫЕ
НАЧАЛОПЕРИОДА(Календарь.Дата, МЕСЯЦ) КАК ПериодМесяц
ПОМЕСТИТЬ ВТ_Периоды
ИЗ
РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
ГДЕ
Календарь.Дата МЕЖДУ &НачалоПериода И &КонецПериода
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Периоды.ПериодМесяц КАК ПериодМесяц,
Остатки.Счет КАК Счет,
ЕСТЬNULL(Остатки.СуммаОстаток, 0) КАК СуммаОстатка
ИЗ
ВТ_Периоды КАК ВТ_Периоды
ЛЕВОЕ СОЕДИНЕНИЕ РегистрБухгалтерии.Хозрасчетный.ОстаткиИОбороты(
&НачалоПериода,
&КонецПериода,
Месяц,
ДвиженияИГраницыПериода,
Счет В (&СписокСчетов),
,
) КАК Остатки
ПО ВТ_Периоды.ПериодМесяц = Остатки.Период
Обратите внимание: данный способ требует аккуратности. Метод ДвиженияИГраницыПериода в параметрах виртуальной таблицы гарантирует нам наличие записей только на крайних точках (в начале и конце общего интервала). Если в середине года по какому-то субконто или счету не было движений в течение нескольких месяцев, прямое левое соединение вернет значения NULL для промежуточных месяцев, и нам придется дорабатывать алгоритм в запросе, чтобы «протянуть» предыдущие значения остатков.
---
В вопросе автора темы прозвучало требование: «в нескольких строках требуется остаток например по 60 счету где дебетовый счет 10.01». Проанализируем эту ситуацию подробнее, так как здесь кроется классическая логическая ошибка начинающих разработчиков.
Остатки по своей природе не имеют корреспонденции!
Корреспонденция (связь Дебет — Кредит) существует исключительно у оборотов (проводка в базе данных физически фиксирует, что средства ушли с одного счета и пришли на другой). Когда мы говорим об остатке на конкретном счете (например, долг на счете 60 перед поставщиком на конец месяца), система хранит только общую сумму долга в разрезе аналитики (Контрагенты, Договоры, Документы расчетов). База данных не хранит информацию о том, за счет какого именно дебетового счета этот остаток образовался.
Если бизнес-заказчику действительно требуется сопоставить остаток долга с приобретенными ценностями (например, отделить долг за материалы на счете 10.01 от долга за услуги), эту задачу решают двумя путями:
Хозрасчетный.ОборотыДтКт, где в параметрах четко указываем счет дебета (10.01) и счет кредита (60.01).---
Подведем итоги нашего разбора:
ОБЪЕДИНИТЬ ВСЕ для получения срезов остатков на конкретные даты. Это гарантирует максимальное быстродействие на больших базах.