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