Как распределить сумму оплаты по строкам задолженности в 1С: решаем задачу ФИФО

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

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

Суть проблемы и исходные данные

Посмотрим на пример. У нас есть две таблицы. Первая содержит детализацию долгов (начислений) по строкам документа:

  1. Док1 | Строка 1 | Сумма: 100
  2. Док1 | Строка 2 | Сумма: 400
  3. Док1 | Строка 3 | Сумма: 600

Вторая таблица содержит общую сумму оплаты по этому документу:

  1. Док1 | Оплачено: 400

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

  1. Док1 | Строка 2 | Остаток: 100 (так как первые 100 рублей ушли на первую строку, от 400 осталось 300; из 400 рублей второй строки вычитаем 300, остается 100)
  2. Док1 | Строка 3 | Остаток: 600 (сюда оплата уже не дошла)

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

Способ 1: Классический вариант в запросе (Самосоединение таблиц)

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

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

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


ВЫБРАТЬ
    Таблица1.Ссылка,
    Таблица1.НомерСтроки,
    Таблица1.Сумма,
    СУММА(ЕСТЬNULL(ТаблицаКопия.Сумма, 0)) КАК НарастающийИтог
ПОМЕСТИТЬ ВТ_СНарастающимИтогом
ИЗ
    ВТ_Детализация КАК Таблица1
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Детализация КАК ТаблицаКопия
        ПО Таблица1.Ссылка = ТаблицаКопия.Ссылка
            И Таблица1.НомерСтроки >= ТаблицаКопия.НомерСтроки
СГРУППИРОВАТЬ ПО
    Таблица1.Ссылка,
    Таблица1.НомерСтроки,
    Таблица1.Сумма

Важное ограничение: У этого метода есть критический недостаток — экспоненциальное падение производительности (сложность O(N²)). Если в документе будут тысячи строк, базе данных придется строить тяжелое декартово произведение, и запрос будет выполняться недопустимо долго.

Способ 2: Идеальное решение для СКД (Система компоновки данных)

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

Разберем по шагам, как это настроить. В ресурсах или в вычисляемых полях схемы компоновки пропишем следующую формулу:

ВычислитьВыражение("Сумма(Сумма)", "Группировка", , "Первая", "Текущая")

В чем преимущество этого подхода? Платформа 1С сама выполнит линейный расчет (сложность O(N)) от первой строки группировки до текущей. Мы не утяжеляем SQL-запрос сложными соединениями, данные обрабатываются быстро и прозрачно. Получив это значение как ресурс НарастающийИтогСКД, мы прямо в СКД можем добавить вычисляемое поле для определения непогашенного остатка по строке с учетом поля Оплачено.

Способ 3: Современный подход — Оконные функции (платформа 8.3.24 и выше)

Начиная с версии платформы 1С:Предприятие 8.3.24, в языке запросов появились долгожданные аналитические оконные функции. Это настоящий прорыв для подобных задач. Теперь мы можем рассчитать нарастающий итог прямо в выборке без каких-либо левых соединений и потери производительности.

Посмотрим на синтаксис оконной функции для нашей задачи:


ВЫБРАТЬ
    Таб1.Ссылка,
    Таб1.НомерСтроки,
    Таб1.Сумма КАК СуммаСтроки,
    СУММА(Таб1.Сумма) ОВЕР (
        ПАРТИЦИРОВАТЬ ПО Таб1.Ссылка 
        СОРТИРОВАТЬ ПО Таб1.НомерСтроки
    ) КАК НарастающийИтог
ИЗ
    ТаблицаДолгов КАК Таб1

Конструкция ПАРТИЦИРОВАТЬ ПО изолирует расчет итога в рамках каждого отдельного документа (разрез), а СОРТИРОВАТЬ ПО обеспечивает правильный порядок сложения от первой строки к последней. После вычисления этого поля нам остается только левым соединением добавить сумму из таблицы оплат и вычислить остаток (вычитая оплату из нарастающего итога и корректируя результат в пределах нуля и суммы самой строки).

Способ 4: Расчет в серверном коде (циклический обход)

Часто бизнес требует рассчитывать просроченную задолженность по особым правилам, которые сильно отличаются от стандартного ФИФО. Например, нужно учитывать приоритеты договоров, сроки годности, частичные штрафы и пени.

В таких ситуациях профессиональные разработчики 1С рекомендуют отказываться от решения задачи исключительно инструментами запроса. Выясним причину: чрезмерно сложные запросы трудны в отладке, тяжело читаются и могут работать неоптимально.

Гораздо более производительный и гибкий подход:

  1. Получить запросом две простые линейные таблицы (Начисления и Оплаты). Обязательно отсортировать их по нужным критериям (дата, номер строки).
  2. Выгрузить результаты в ТаблицаЗначений или обходить через выборку.
  3. В модуле на встроенном языке (серверный контекст) запустить линейный цикл (while / для каждого), который будет вычитать сумму оплат из начислений, формируя результирующую таблицу распределения.

Этот метод работает максимально быстро (поскольку выполняется в оперативной памяти сервера приложений за один проход, алгоритм O(N)), легко отлаживается и позволяет внедрить сколь угодно сложную логику распределения сумм.

← На главную