Реализация алгоритма LIFO (Last-In, First-Out) исключительно средствами языка запросов 1С — это классическая задача, требующая глубокого понимания работы с временными таблицами, накопительными итогами и математическими интервалами. В отличие от FIFO, где списание идет от самых ранних партий, LIFO требует обращения к последним поступившим остаткам. Рассмотрим подробно, как построить такой запрос, разберем логику формирования очередей и проанализируем основные нюансы производительности.
Математически задача списания партий (хоть FIFO, хоть LIFO) сводится к сопоставлению двух «линеек» (шкал): шкалы приходов и шкалы расходов. Представим, что все приходы выстроены в одну линию друг за другом, и каждый приход занимает определенный интервал (например, от 10-го до 15-го килограмма). Расходы также выстраиваются в линию. Наша задача — найти пересечение интервала конкретного расхода с интервалами имеющихся приходов.
Для FIFO мы выстраиваем приходы по дате по возрастанию. Для LIFO — по убыванию. В этом и заключается главный секрет: если мы инвертируем порядок накопления суммы приходов, стандартный механизм распределения превратится в LIFO. При необходимости можно ознакомиться с моделью распределения суммы по базе с поддержкой FIFO и LIFO.
Проанализируем ситуацию, когда у нас есть две таблицы: приходы (партии) и расходы. Для начала поместим их во временные таблицы. Важно помнить, что для корректной работы LIFO необходимо учитывать не только дату, но и МоментВремени, чтобы правильно обрабатывать документы в пределах одной секунды.
// Помещаем приходы во временную таблицу
ВЫБРАТЬ
Приход.Номенклатура КАК Номенклатура,
Приход.Партия КАК Партия,
Приход.Дата КАК ДатаПартии,
Приход.Количество КАК Количество
ПОМЕСТИТЬ ВТ_Приходы
ИЗ
РегистрНакопления.ОстаткиТоваров КАК Приход;
// Помещаем расходы во временную таблицу
ВЫБРАТЬ
Расход.Номенклатура КАК Номенклатура,
Расход.Дата КАК ДатаРасхода,
Расход.Количество КАК Количество
ПОМЕСТИТЬ ВТ_Расходы
ИЗ
Документ.РеализацияТоваровУслуг КАК Расход;
Разберем самый важный этап. Для реализации LIFO нам нужно, чтобы «первыми» в списке приходов шли самые «свежие» документы. Рассмотрим, как вычислить границы интервалов для каждой партии. Выясним причину, почему обычное соединение здесь меняется: мы соединяем таблицу саму с собой так, чтобы для каждой партии П1 сумма собиралась из всех партий П2, которые появились позже нее.
ВЫБРАТЬ
Т1.Номенклатура КАК Номенклатура,
Т1.Партия КАК Партия,
Т1.Количество КАК КоличествоПартии,
СУММА(Т2.Количество) - Т1.Количество КАК КоличествоДо,
СУММА(Т2.Количество) КАК КоличествоПосле
ПОМЕСТИТЬ ВТ_ИнтервалыПриходов
ИЗ
ВТ_Приходы КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Приходы КАК Т2
ПО Т1.Номенклатура = Т2.Номенклатура
// Ключевое условие для LIFO: берем те, что позже или равны
И Т1.ДатаПартии <= Т2.ДатаПартии
СГРУППИРОВАТЬ ПО
Т1.Номенклатура,
Т1.Партия,
Т1.Количество
В этом запросе поле КоличествоДо — это нижняя граница интервала партии, а КоличествоПосле — верхняя граница. Благодаря условию Т1.ДатаПартии <= Т2.ДатаПартии, самая поздняя партия получит интервал от 0 до своего количества, став «первой» в очереди на списание. При необходимости оптимизировать вычисления на больших объемах данных можно использовать метод быстрого определения интервалов в запросе.
Аналогичным образом подготовим шкалу расходов. Здесь порядок обычно хронологический (списываем расходы по мере их возникновения), поэтому используем стандартное сравнение Т1.Дата >= Т2.Дата.
ВЫБРАТЬ
Т1.Номенклатура КАК Номенклатура,
Т1.ДатаРасхода КАК ДатаРасхода,
Т1.Количество КАК КоличествоРасхода,
СУММА(Т2.Количество) - Т1.Количество КАК КоличествоДо,
СУММА(Т2.Количество) КАК КоличествоПосле
ПОМЕСТИТЬ ВТ_ИнтервалыРасходов
ИЗ
ВТ_Расходы КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Расходы КАК Т2
ПО Т1.Номенклатура = Т2.Номенклатура
И Т1.ДатаРасхода >= Т2.ДатаРасхода
СГРУППИРОВАТЬ ПО
Т1.Номенклатура,
Т1.ДатаРасхода,
Т1.Количество
Теперь проанализируем ситуацию объединения двух шкал. Нам нужно найти такие пары «Расход — Партия», у которых интервалы пересекаются. Формула пересечения отрезков [A1, A2] и [B1, B2] выглядит так: Максимум(0, Минимум(A2, B2) - Максимум(A1, B1)).
ВЫБРАТЬ
Расходы.ДатаРасхода КАК ДатаРасхода,
Приходы.Партия КАК Партия,
ВЫБОР
КОГДА Расходы.КоличествоПосле < Приходы.КоличествоДо
ТОГДА 0
КОГДА Приходы.КоличествоПосле < Расходы.КоличествоДо
ТОГДА 0
ИНАЧЕ (ВЫБОР КОГДА Расходы.КоличествоПосле < Приходы.КоличествоПосле
ТОГДА Расходы.КоличествоПосле
ИНАЧЕ Приходы.КоличествоПосле КОНЕЦ) -
(ВЫБОР КОГДА Расходы.КоличествоДо > Приходы.КоличествоДо
ТОГДА Расходы.КоличествоДо
ИНАЧЕ Приходы.КоличествоДо КОНЕЦ)
КОНЕЦ КАК КоличествоСписания
ИЗ
ВТ_ИнтервалыРасходов КАК Расходы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ИнтервалыПриходов КАК Приходы
ПО Расходы.Номенклатура = Приходы.Номенклатура
// Условие пересечения интервалов
И Расходы.КоличествоПосле > Приходы.КоличествоДо
И Приходы.КоличествоПосле > Расходы.КоличествоДо
ГДЕ
// Отсекаем нулевые списания на всякий случай
(ВЫБОР КОГДА Расходы.КоличествоПосле < Приходы.КоличествоПосле
ТОГДА Расходы.КоличествоПосле
ИНАЧЕ Приходы.КоличествоПосле КОНЕЦ) >
(ВЫБОР КОГДА Расходы.КоличествоДо > Приходы.КоличествоДо
ТОГДА Расходы.КоличествоДо
ИНАЧЕ Приходы.КоличествоДо КОНЕЦ)
Рассмотрим несколько критических моментов, которые могут повлиять на результат:
Приходы.ДатаПартии <= Расходы.ДатаРасхода.O(n^2). Если записей в таблице приходов по одной номенклатуре больше нескольких тысяч, запрос начнет значительно тормозить.Номенклатура и Дата/МоментВремени. Это критически важно для скорости выполнения соединений.Если ваша база работает на современной платформе, мы можем значительно упростить расчет накопительного итога, используя функцию OVER. Это работает в разы быстрее, так как не требует тяжелых соединений.
ВЫБРАТЬ
Т.Номенклатура,
Т.Партия,
Т.Количество,
СУММА(Т.Количество) ОБЪЕДИНЯЕТ (УПОРЯДОЧИТЬ ПО Т.ДатаПартии УБЫВ) - Т.Количество КАК КоличествоДо,
СУММА(Т.Количество) ОБЪЕДИНЯЕТ (УПОРЯДОЧИТЬ ПО Т.ДатаПартии УБЫВ) КАК КоличествоПосле
ИЗ
ВТ_Приходы КАК Т
Посмотрим на этот пример: оператор ОБЪЕДИНЯЕТ (аналог SQL OVER) позволяет вычислить бегущую сумму. Сортировка УБЫВ сразу расставляет партии в порядке LIFO. Это наиболее современный и эффективный способ решения задачи.
Мы разберем по шагам логику LIFO: главное отличие от FIFO заключается в направлении суммирования остатков. Используя метод интервалов, мы можем получить прозрачный и точный расчет партий внутри одного запроса, не прибегая к перебору в цикле на языке 1С. Это особенно полезно при построении сложных отчетов в СКД или при массовом пересчете себестоимости.