Часто перед разработчиками встает задача взаимозачета или распределения товарных партий, когда есть две независимые таблицы. Например, таблица «Должники» (кому мы должны или кто нам должен) и таблица «Кредиторы» (или таблица оплат). Необходимо распределить суммы так, чтобы долги закрывались в порядке очереди (FIFO), и при этом ни одна из сторон не заплатила и не получила лишнего. В этой статье мы подробно разберем, как реализовать этот алгоритм непосредственно в языке запросов 1С.
Представим, что у нас есть две таблицы данных. В первой — список должников с суммами, во второй — список получателей денег (кредиторов) с суммами. Нам нужно сопоставить эти таблицы так, чтобы суммы «перетекали» от первых ко вторым последовательно.
Если мы просто сделаем полное соединение таблиц, мы получим декартово произведение, где суммы задублируются. Нам же нужно конкретное распределение: первый должник гасит первого кредитора; если денег осталось больше — гасит второго, и так далее. Это классическая задача перекрытия интервалов, которую на профессиональном сленге иногда называют «алгоритмом червя» или «слайсингом».
Чтобы решить эту задачу одним запросом (или пакетом запросов), нам нужно представить каждую сумму не как число, а как отрезок на числовой прямой. Давайте разберем этот подход по шагам.
Начало = Нарастающий итог предыдущих записей.Конец = Нарастающий итог текущей записи (включая её саму).Разберем, как написать такой запрос. Поскольку в языке запросов 1С отсутствуют оконные функции (наподобие SUM() OVER(...) в SQL), расчет нарастающего итога мы будем выполнять через соединение таблицы самой с собой.
Предположим, данные находятся во временных таблицах ВТ_Должники и ВТ_Кредиторы. У каждой таблицы должно быть поле для сортировки (назовем его Порядок или используем МоментВремени/Ссылка).
Рассмотрим пример формирования интервалов для должников. Для кредиторов логика будет абсолютно идентичной.
// Формируем интервалы для Должников
ВЫБРАТЬ
Т1.Ссылка КАК Должник,
Т1.Сумма КАК СуммаДолга,
// Нарастающий итог
СУММА(Т2.Сумма) КАК КонецИнтервала,
// Начало интервала = (Нарастающий итог - Текущая сумма)
СУММА(Т2.Сумма) - Т1.Сумма КАК НачалоИнтервала
ПОМЕСТИТЬ ВТ_Должники_Интервалы
ИЗ
ВТ_Должники КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Должники КАК Т2
ПО Т2.Порядок <= Т1.Порядок
СГРУППИРОВАТЬ ПО
Т1.Ссылка,
Т1.Сумма
;
В данном фрагменте мы соединяем таблицу саму с собой по условию Т2.Порядок <= Т1.Порядок. Это позволяет для каждой строки Т1 собрать все предыдущие строки Т2 и просуммировать их. Так мы получаем поле КонецИнтервала. Вычитая из конца текущую сумму, мы получаем НачалоИнтервала.
Важное примечание: Такой метод расчета нарастающего итога имеет «квадратичную сложность». Если у вас в таблице тысячи записей, запрос может работать медленно. В таких случаях лучше рассчитывать нарастающий итог предварительно в коде 1С или использовать индексирование.
Аналогично создаем таблицу ВТ_Кредиторы_Интервалы.
Теперь у нас есть две временные таблицы с полями НачалоИнтервала и КонецИнтервала. Давайте соединим их. Условие пересечения двух отрезков на прямой выглядит так: один отрезок не должен закончиться раньше, чем начнется другой, и не должен начаться позже, чем закончится другой.
Формально условие соединения выглядит так:
Должник.НачалоИнтервала < Кредитор.КонецИнтервала
И
Должник.КонецИнтервала > Кредитор.НачалоИнтервала
После того как мы нашли пересекающиеся пары «Должник — Кредитор», нам нужно вычислить точную сумму, которую этот должник платит этому кредитору. Эта сумма равна длине пересечения их интервалов.
Математическая формула длины пересечения:
Сумма = МИНИМУМ(Конец1, Конец2) - МАКСИМУМ(Начало1, Начало2)
Реализуем это в финальном запросе:
ВЫБРАТЬ
Д.Должник,
К.Кредитор,
// Реализация формулы MIN(...) - MAX(...) через ВЫБОР
(ВЫБОР
КОГДА Д.КонецИнтервала < К.КонецИнтервала ТО Д.КонецИнтервала
ИНАЧЕ К.КонецИнтервала
КОНЕЦ)
-
(ВЫБОР
КОГДА Д.НачалоИнтервала > К.НачалоИнтервала ТО Д.НачалоИнтервала
ИНАЧЕ К.НачалоИнтервала
КОНЕЦ) КАК СуммаВзаимозачета
ИЗ
ВТ_Должники_Интервалы КАК Д
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Кредиторы_Интервалы КАК К
ПО Д.НачалоИнтервала < К.КонецИнтервала
И Д.КонецИнтервала > К.НачалоИнтервала
Давайте посмотрим, как этот алгоритм обработает ситуацию, описанную в теме.
Должники (Д):
Кредиторы (К):
Проанализируем пересечения:
В итоге мы получаем таблицу распределения, где каждый остался «при своих», суммы не задвоились, и распределение прошло строго по порядку.
Хотя решение запросом выглядит элегантно, стоит упомянуть его недостатки. Расчет нарастающих итогов через JOIN требователен к ресурсам. Если вы работаете с большими объемами данных, более эффективно будет использовать гибридный метод.
Логика кода будет следующей:
Пока (ВыборкаД.Следующий()) И (ВыборкаК.Следующий()) Цикл
СуммаКРаспр = Мин(ОстатокДолгаД, ОстатокДолгаК);
// Записываем результат: Должник, Кредитор, СуммаКРаспр
ОстатокДолгаД = ОстатокДолгаД - СуммаКРаспр;
ОстатокДолгаК = ОстатокДолгаК - СуммаКРаспр;
// Если долг должника исчерпан, переходим к следующему должнику,
// но "держим" текущего кредитора, если ему еще должны.
// И наоборот.
КонецЦикла;
Этот подход работает линейно (O(N)), что значительно быстрее на больших массивах данных, чем сложные соединения в запросе.
Для решения задачи распределения долгов в запросе:
Этот метод универсален и подходит для задач партионного учета (FIFO), распределения оплат по накладным и взаимозачетов.