При разработке отчетов на Системе компоновки данных (СКД) одной из наиболее частых задач является вывод остатков и оборотов за выбранный период. Очень часто разработчики сталкиваются с тем, что в детальных записях данные выводятся корректно, а вот в общем итоге по колонке Начальный остаток отображается пустое значение (или NULL), в то время как Конечный остаток рассчитывается без проблем. Попробуем подробно разобраться в причинах этого поведения и рассмотрим профессиональные способы решения данной проблемы.
Рассмотрим выражения ресурсов, которые изначально использовались для расчета итогов:
ВычислитьВыражение("Максимум(СуммаНачальныйОстаток)",,,, "Последняя")ВычислитьВыражение("Максимум(СуммаКонечныйОстаток)",,,, "Последняя")Выясним причину, почему конечный остаток рассчитывается, а начальный — нет. Дело в логике работы параметра "Последняя" (пятый параметр функции ВычислитьВыражение()):
Конечный остаток по своей сути находится в самой последней хронологической точке выбранного периода. Когда СКД доходит до расчета общего итога и обращается к "Последней" записи детальных данных, она действительно получает конечный остаток на самую позднюю дату, что абсолютно логично и правильно.
Начальный остаток всего периода хронологически находится в самой первой записи отчета. Попытка получить начальный остаток из "Последней" записи приводит к тому, что СКД пытается взять начальный остаток последней секунды/дня периода. Если в конце периода были движения, этот остаток будет равен конечному остатку предпоследнего периода, а если движений не было или в выборку попали пустые значения из-за левых соединений, функция вернет пустое значение.
Для исправления этой логической ошибки формулу для начального остатка необходимо изменить следующим образом:
ВычислитьВыражение("Максимум(СуммаНачальныйОстаток)",,,, "Первая")
---
Проанализируем структуру запроса, который использовался для получения данных:
ВЫБРАТЬ
ДенежныеСредстваНаличныеОстаткиИОбороты.ПериодСекунда КАК ПериодСекунда,
ДенежныеСредстваНаличныеОстаткиИОбороты.Регистратор КАК Регистратор,
ЕСТЬNULL(ДенежныеСредстваНаличныеОстаткиИОбороты.СуммаНачальныйОстаток, 0) КАК СуммаНачальныйОстаток,
ЕСТЬNULL(ДенежныеСредстваНаличныеОстаткиИОбороты.СуммаКонечныйОстаток, 0) КАК СуммаКонечныйОстаток,
ДенежныеСредстваНаличныеОстаткиИОбороты.СуммаОборот КАК СуммаОборот,
ДенежныеСредстваНаличныеОстаткиИОбороты.СуммаПриход КАК СуммаПриход,
ДенежныеСредстваНаличныеОстаткиИОбороты.СуммаРасход КАК СуммаРасход,
ЕСТЬNULL(ДенежныеСредстваНаличные.СтатьяДвиженияДенежныхСредств, ЗНАЧЕНИЕ(Справочник.СтатьиДвиженияДенежныхСредств.ПустаяСсылка)) КАК СтатьяДвиженияДенежныхСредств
ИЗ
РегистрНакопления.ДенежныеСредстваНаличные.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Авто, Движения, ) КАК ДенежныеСредстваНаличныеОстаткиИОбороты
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДенежныеСредстваНаличные КАК ДенежныеСредстваНаличные
ПО (ДенежныеСредстваНаличныеОстаткиИОбороты.Регистратор = ДенежныеСредстваНаличные.Регистратор)
В этом запросе кроется серьезная архитектурная проблема: виртуальная таблица ОстаткиИОбороты соединяется левым соединением с физической таблицей того же регистра по полю Регистратор.
Поскольку один и тот же регистратор в физической таблице может делать несколько движений (содержать несколько строк), левое соединение начинает дублировать (размножать) строки виртуальной таблицы остатков. Для обычных оборотов это решается группировкой, но для расчета остатков это критично по следующим причинам:
ВычислитьВыражение() больше не может однозначно определить, какая строка на самом деле является первой или последней.---
Использование сложных формул в ресурсах, таких как ВычислитьВыражение() — это крайняя мера. Платформа 1С содержит мощный штатный механизм для автоматического расчета остатков на любых уровнях группировок. Чтобы СКД сама понимала, где взять начальную точку, а где конечную, настроим роли полей на вкладке "Наборы данных":
СуммаНачальныйОстаток. В колонке Роль нажмем на кнопку выбора. В открывшемся окне установим флаг Остаток, выберем тип Начальный. В поле Имя группы укажем понятное имя, например, Сумма.СуммаКонечныйОстаток проделаем аналогичную операцию: установим флаг Остаток, тип выберем Конечный, а в поле Имя группы обязательно укажем то же самое имя — Сумма. Связывание полей остатков в одну группу критически важно для корректной работы СКД.ПериодСекунда, Регистратор) настроим роль Период с указанием их порядковых номеров (для секунд индекс меньше, для дней — больше).После правильной настройки ролей, на вкладке Ресурсы нам достаточно написать стандартные выражения:
СуммаНачальныйОстаток: Сумма(СуммаНачальныйОстаток)СуммаКонечныйОстаток: Сумма(СуммаКонечныйОстаток)СКД автоматически рассчитает общий итог: для начального остатка она сложит значения на самую раннюю дату периода, а для конечного — на самую позднюю.
---
Чтобы исключить пагубное влияние левого соединения, которое размножает строки остатков, разделим получение данных на два набора данных и свяжем их на уровне СКД.
Шаг 1. Создадим НаборДанных1 (Запрос). В нем мы будем выбирать только «чистые» остатки и обороты из виртуальной таблицы:
ВЫБРАТЬ
ДенежныеСредстваОстаткиИОбороты.ПериодСекунда КАК ПериодСекунда,
ДенежныеСредстваОстаткиИОбороты.Регистратор КАК Регистратор,
ДенежныеСредстваОстаткиИОбороты.СуммаНачальныйОстаток КАК СуммаНачальныйОстаток,
ДенежныеСредстваОстаткиИОбороты.СуммаКонечныйОстаток КАК СуммаКонечныйОстаток,
ДенежныеСредстваОстаткиИОбороты.СуммаОборот КАК СуммаОборот
ИЗ
РегистрНакопления.ДенежныеСредстваНаличные.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Авто, Движения, ) КАК ДенежныеСредстваОстаткиИОбороты
Шаг 2. Создадим НаборДанных2 (Запрос). Этот набор будет получать аналитику (например, статьи движения денежных средств) непосредственно из физической таблицы регистра:
ВЫБРАТЬ
ДенежныеСредства.Регистратор КАК Регистратор,
ДенежныеСредства.СтатьяДвиженияДенежныхСредств КАК СтатьяДвиженияДенежныхСредств
ИЗ
РегистрНакопления.ДенежныеСредстваНаличные КАК ДенежныеСредства
Шаг 3. Настроим связи наборов данных. Перейдем на вкладку Связи наборов данных. Добавим новую строку:
НаборДанных1НаборДанных2Регистратор = РегистраторТакой подход гарантирует, что остатки не будут удваиваться из-за левого соединения, а детальная аналитика подтянется СКД в момент компоновки результата.
---
Иногда возникают ситуации, когда стандартные роли использовать невозможно (например, структура данных не является классическим регистром остатков), а в отчете присутствует несколько параллельных разрезов аналитики (например, несколько касс или организаций). Если использовать простое выражение ВычислитьВыражение("СуммаНачальныйОстаток",,, "Первая") в общем итоге, система вернет остаток только по первой попавшейся кассе, проигнорировав остальные.
В таких случаях для расчета общего итога по всем кассам применяется продвинутая функция СКД:
Начальный остаток для общего итога:
Сумма(ВычислитьВыражениеСГруппировкойМассив("ВычислитьВыражение(""СуммаНачальныйОстаток"", , , ""Первая"", ""Первая"")", "Касса"))
Конечный остаток для общего итога:
Сумма(ВычислитьВыражениеСГруппировкойМассив("ВычислитьВыражение(""СуммаКонечныйОстаток"", , , ""Последняя"", ""Последняя"")", "Касса"))
Разберем, как это работает. Функция ВычислитьВыражениеСГруппировкойМассив() сначала рассчитывает выражение в контексте каждого значения группировки "Касса". Внутри этой группировки она находит самое первое (или последнее) значение остатка. Полученные результаты по каждой кассе складываются в массив, а внешняя функция Сумма() складывает элементы этого массива, обеспечивая математически точный и корректный общий итог по всему отчету.