Как в запросе 1С распределить суммы между двумя таблицами (FIFO метод)?

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

Часто перед разработчиками встает задача взаимозачета или распределения товарных партий, когда есть две независимые таблицы. Например, таблица «Должники» (кому мы должны или кто нам должен) и таблица «Кредиторы» (или таблица оплат). Необходимо распределить суммы так, чтобы долги закрывались в порядке очереди (FIFO), и при этом ни одна из сторон не заплатила и не получила лишнего. В этой статье мы подробно разберем, как реализовать этот алгоритм непосредственно в языке запросов 1С.

Суть проблемы и визуализация

Представим, что у нас есть две таблицы данных. В первой — список должников с суммами, во второй — список получателей денег (кредиторов) с суммами. Нам нужно сопоставить эти таблицы так, чтобы суммы «перетекали» от первых ко вторым последовательно.

Если мы просто сделаем полное соединение таблиц, мы получим декартово произведение, где суммы задублируются. Нам же нужно конкретное распределение: первый должник гасит первого кредитора; если денег осталось больше — гасит второго, и так далее. Это классическая задача перекрытия интервалов, которую на профессиональном сленге иногда называют «алгоритмом червя» или «слайсингом».

Алгоритм решения: Метод интервалов

Чтобы решить эту задачу одним запросом (или пакетом запросов), нам нужно представить каждую сумму не как число, а как отрезок на числовой прямой. Давайте разберем этот подход по шагам.

  1. Упорядочивание: Мы должны жестко зафиксировать порядок записей в обеих таблицах (например, по дате возникновения долга или по алфавиту, или по внутреннему идентификатору).
  2. Нарастающий итог: Для каждой записи необходимо рассчитать кумулятивную сумму (нарастающий итог) от начала таблицы.
  3. Определение границ: Каждая запись превращается в интервал:
    • Начало = Нарастающий итог предыдущих записей.
    • Конец = Нарастающий итог текущей записи (включая её саму).
  4. Поиск пересечений: Мы соединяем две таблицы по условию пересечения этих интервалов.
  5. Расчет суммы списания: Вычисляем длину пересечения отрезков.

Реализация в языке запросов 1С

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

Шаг 1. Подготовка таблиц с нарастающим итогом

Предположим, данные находятся во временных таблицах ВТ_Должники и ВТ_Кредиторы. У каждой таблицы должно быть поле для сортировки (назовем его Порядок или используем МоментВремени/Ссылка).

Рассмотрим пример формирования интервалов для должников. Для кредиторов логика будет абсолютно идентичной.


// Формируем интервалы для Должников
ВЫБРАТЬ
    Т1.Ссылка КАК Должник,
    Т1.Сумма КАК СуммаДолга,
    // Нарастающий итог
    СУММА(Т2.Сумма) КАК КонецИнтервала, 
    // Начало интервала = (Нарастающий итог - Текущая сумма)
    СУММА(Т2.Сумма) - Т1.Сумма КАК НачалоИнтервала
ПОМЕСТИТЬ ВТ_Должники_Интервалы
ИЗ
    ВТ_Должники КАК Т1
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Должники КАК Т2
        ПО Т2.Порядок <= Т1.Порядок
СГРУППИРОВАТЬ ПО
    Т1.Ссылка,
    Т1.Сумма
;

В данном фрагменте мы соединяем таблицу саму с собой по условию Т2.Порядок <= Т1.Порядок. Это позволяет для каждой строки Т1 собрать все предыдущие строки Т2 и просуммировать их. Так мы получаем поле КонецИнтервала. Вычитая из конца текущую сумму, мы получаем НачалоИнтервала.

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

Аналогично создаем таблицу ВТ_Кредиторы_Интервалы.

Шаг 2. Соединение таблиц по перекрытию

Теперь у нас есть две временные таблицы с полями НачалоИнтервала и КонецИнтервала. Давайте соединим их. Условие пересечения двух отрезков на прямой выглядит так: один отрезок не должен закончиться раньше, чем начнется другой, и не должен начаться позже, чем закончится другой.

Формально условие соединения выглядит так:


Должник.НачалоИнтервала < Кредитор.КонецИнтервала
И
Должник.КонецИнтервала > Кредитор.НачалоИнтервала

Шаг 3. Расчет суммы взаимозачета

После того как мы нашли пересекающиеся пары «Должник — Кредитор», нам нужно вычислить точную сумму, которую этот должник платит этому кредитору. Эта сумма равна длине пересечения их интервалов.

Математическая формула длины пересечения:

Сумма = МИНИМУМ(Конец1, Конец2) - МАКСИМУМ(Начало1, Начало2)

Реализуем это в финальном запросе:


ВЫБРАТЬ
    Д.Должник,
    К.Кредитор,
    // Реализация формулы MIN(...) - MAX(...) через ВЫБОР
    (ВЫБОР 
        КОГДА Д.КонецИнтервала < К.КонецИнтервала ТО Д.КонецИнтервала 
        ИНАЧЕ К.КонецИнтервала 
    КОНЕЦ) 
    - 
    (ВЫБОР 
        КОГДА Д.НачалоИнтервала > К.НачалоИнтервала ТО Д.НачалоИнтервала 
        ИНАЧЕ К.НачалоИнтервала 
    КОНЕЦ) КАК СуммаВзаимозачета
ИЗ
    ВТ_Должники_Интервалы КАК Д
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Кредиторы_Интервалы КАК К
        ПО Д.НачалоИнтервала < К.КонецИнтервала
        И Д.КонецИнтервала > К.НачалоИнтервала

Разберем пример на цифрах

Давайте посмотрим, как этот алгоритм обработает ситуацию, описанную в теме.

Должники (Д):

Кредиторы (К):

Проанализируем пересечения:

  1. Пара Д1 — К1:
    • Интервалы: (0..100) и (0..200).
    • Пересекаются? Да.
    • Сумма = Мин(100, 200) - Макс(0, 0) = 100 - 0 = 100.
    • Д1 полностью закрыл свой долг перед К1.
  2. Пара Д1 — К2:
    • Интервалы: (0..100) и (200..400).
    • Пересекаются? Нет (100 < 200). В выборку не попадает.
  3. Пара Д2 — К1:
    • Интервалы: (100..400) и (0..200).
    • Пересекаются? Да.
    • Сумма = Мин(400, 200) - Макс(100, 0) = 200 - 100 = 100.
    • Д2 отдал часть долга (100) кредитору К1. Кредитор К1 полностью удовлетворен (100 от Д1 + 100 от Д2 = 200).
  4. Пара Д2 — К2:
    • Интервалы: (100..400) и (200..400).
    • Пересекаются? Да.
    • Сумма = Мин(400, 400) - Макс(100, 200) = 400 - 200 = 200.
    • Д2 отдал остаток долга (200) кредитору К2.

В итоге мы получаем таблицу распределения, где каждый остался «при своих», суммы не задвоились, и распределение прошло строго по порядку.

Альтернативный подход: Расчет в коде

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

  1. Запросом выберите данные в две таблицы значений (ТЗ) или выборки, отсортированные по нужному критерию.
  2. Используйте алгоритм двух указателей (циклы) во встроенном языке 1С.

Логика кода будет следующей:


Пока (ВыборкаД.Следующий()) И (ВыборкаК.Следующий()) Цикл
    СуммаКРаспр = Мин(ОстатокДолгаД, ОстатокДолгаК);
    // Записываем результат: Должник, Кредитор, СуммаКРаспр
    
    ОстатокДолгаД = ОстатокДолгаД - СуммаКРаспр;
    ОстатокДолгаК = ОстатокДолгаК - СуммаКРаспр;
    
    // Если долг должника исчерпан, переходим к следующему должнику,
    // но "держим" текущего кредитора, если ему еще должны.
    // И наоборот.
КонецЦикла;

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

Резюме

Для решения задачи распределения долгов в запросе:

  1. Используйте временные таблицы.
  2. Рассчитайте нарастающие итоги для формирования интервалов (Начало и Конец).
  3. Соедините таблицы по условию пересечения интервалов.
  4. Рассчитайте сумму как разность между минимальным концом и максимальным началом отрезков.

Этот метод универсален и подходит для задач партионного учета (FIFO), распределения оплат по накладным и взаимозачетов.

← На главную