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