Как реализовать алгоритм LIFO с помощью запроса в 1С?

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

Реализация алгоритма LIFO (Last-In, First-Out) исключительно средствами языка запросов 1С — это классическая задача, требующая глубокого понимания работы с временными таблицами, накопительными итогами и математическими интервалами. В отличие от FIFO, где списание идет от самых ранних партий, LIFO требует обращения к последним поступившим остаткам. Рассмотрим подробно, как построить такой запрос, разберем логику формирования очередей и проанализируем основные нюансы производительности.

Теоретическая основа: Метод накопительных итогов

Математически задача списания партий (хоть FIFO, хоть LIFO) сводится к сопоставлению двух «линеек» (шкал): шкалы приходов и шкалы расходов. Представим, что все приходы выстроены в одну линию друг за другом, и каждый приход занимает определенный интервал (например, от 10-го до 15-го килограмма). Расходы также выстраиваются в линию. Наша задача — найти пересечение интервала конкретного расхода с интервалами имеющихся приходов.

Для FIFO мы выстраиваем приходы по дате по возрастанию. Для LIFO — по убыванию. В этом и заключается главный секрет: если мы инвертируем порядок накопления суммы приходов, стандартный механизм распределения превратится в LIFO. При необходимости можно ознакомиться с моделью распределения суммы по базе с поддержкой FIFO и LIFO.

Шаг 1. Подготовка исходных данных

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


// Помещаем приходы во временную таблицу
ВЫБРАТЬ
    Приход.Номенклатура КАК Номенклатура,
    Приход.Партия КАК Партия,
    Приход.Дата КАК ДатаПартии,
    Приход.Количество КАК Количество
ПОМЕСТИТЬ ВТ_Приходы
ИЗ
    РегистрНакопления.ОстаткиТоваров КАК Приход;

// Помещаем расходы во временную таблицу
ВЫБРАТЬ
    Расход.Номенклатура КАК Номенклатура,
    Расход.Дата КАК ДатаРасхода,
    Расход.Количество КАК Количество
ПОМЕСТИТЬ ВТ_Расходы
ИЗ
    Документ.РеализацияТоваровУслуг КАК Расход;

Шаг 2. Расчет накопительных итогов для LIFO

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


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

В этом запросе поле КоличествоДо — это нижняя граница интервала партии, а КоличествоПосле — верхняя граница. Благодаря условию Т1.ДатаПартии <= Т2.ДатаПартии, самая поздняя партия получит интервал от 0 до своего количества, став «первой» в очереди на списание. При необходимости оптимизировать вычисления на больших объемах данных можно использовать метод быстрого определения интервалов в запросе.

Шаг 3. Расчет накопительного итога для расходов

Аналогичным образом подготовим шкалу расходов. Здесь порядок обычно хронологический (списываем расходы по мере их возникновения), поэтому используем стандартное сравнение Т1.Дата >= Т2.Дата.


ВЫБРАТЬ
    Т1.Номенклатура КАК Номенклатура,
    Т1.ДатаРасхода КАК ДатаРасхода,
    Т1.Количество КАК КоличествоРасхода,
    СУММА(Т2.Количество) - Т1.Количество КАК КоличествоДо,
    СУММА(Т2.Количество) КАК КоличествоПосле
ПОМЕСТИТЬ ВТ_ИнтервалыРасходов
ИЗ
    ВТ_Расходы КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Расходы КАК Т2
    ПО Т1.Номенклатура = Т2.Номенклатура
    И Т1.ДатаРасхода >= Т2.ДатаРасхода
СГРУППИРОВАТЬ ПО
    Т1.Номенклатура,
    Т1.ДатаРасхода,
    Т1.Количество

Шаг 4. Соединение шкал и вычисление объема списания

Теперь проанализируем ситуацию объединения двух шкал. Нам нужно найти такие пары «Расход — Партия», у которых интервалы пересекаются. Формула пересечения отрезков [A1, A2] и [B1, B2] выглядит так: Максимум(0, Минимум(A2, B2) - Максимум(A1, B1)).


ВЫБРАТЬ
    Расходы.ДатаРасхода КАК ДатаРасхода,
    Приходы.Партия КАК Партия,
    ВЫБОР
        КОГДА Расходы.КоличествоПосле < Приходы.КоличествоДо 
            ТОГДА 0
        КОГДА Приходы.КоличествоПосле < Расходы.КоличествоДо 
            ТОГДА 0
        ИНАЧЕ (ВЫБОР КОГДА Расходы.КоличествоПосле < Приходы.КоличествоПосле 
                     ТОГДА Расходы.КоличествоПосле 
                     ИНАЧЕ Приходы.КоличествоПосле КОНЕЦ) - 
              (ВЫБОР КОГДА Расходы.КоличествоДо > Приходы.КоличествоДо 
                     ТОГДА Расходы.КоличествоДо 
                     ИНАЧЕ Приходы.КоличествоДо КОНЕЦ)
    КОНЕЦ КАК КоличествоСписания
ИЗ
    ВТ_ИнтервалыРасходов КАК Расходы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ИнтервалыПриходов КАК Приходы
    ПО Расходы.Номенклатура = Приходы.Номенклатура
    // Условие пересечения интервалов
    И Расходы.КоличествоПосле > Приходы.КоличествоДо
    И Приходы.КоличествоПосле > Расходы.КоличествоДо
ГДЕ
    // Отсекаем нулевые списания на всякий случай
    (ВЫБОР КОГДА Расходы.КоличествоПосле < Приходы.КоличествоПосле 
           ТОГДА Расходы.КоличествоПосле 
           ИНАЧЕ Приходы.КоличествоПосле КОНЕЦ) > 
    (ВЫБОР КОГДА Расходы.КоличествоДо > Приходы.КоличествоДо 
           ТОГДА Расходы.КоличествоДо 
           ИНАЧЕ Приходы.КоличествоДо КОНЕЦ)

Важные нюансы и оптимизация

Рассмотрим несколько критических моментов, которые могут повлиять на результат:

  1. Списание «из будущего»: Бухгалтерский LIFO обычно подразумевает, что мы списываем последнюю партию из тех, что уже пришли на момент расхода. В приведенном выше коде это не учитывается (берется LIFO по всей таблице). Чтобы ограничить выборку, в соединение таблиц приходов и расходов необходимо добавить условие: Приходы.ДатаПартии <= Расходы.ДатаРасхода.
  2. Производительность: Соединение таблицы «саму на себя» для получения накопительного итога имеет квадратичную сложность O(n^2). Если записей в таблице приходов по одной номенклатуре больше нескольких тысяч, запрос начнет значительно тормозить.
  3. Индексация: Обязательно индексируйте временные таблицы по полям Номенклатура и Дата/МоментВремени. Это критически важно для скорости выполнения соединений.

Использование оконных функций (для Платформы 8.3.20+)

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


ВЫБРАТЬ
    Т.Номенклатура,
    Т.Партия,
    Т.Количество,
    СУММА(Т.Количество) ОБЪЕДИНЯЕТ (УПОРЯДОЧИТЬ ПО Т.ДатаПартии УБЫВ) - Т.Количество КАК КоличествоДо,
    СУММА(Т.Количество) ОБЪЕДИНЯЕТ (УПОРЯДОЧИТЬ ПО Т.ДатаПартии УБЫВ) КАК КоличествоПосле
ИЗ
    ВТ_Приходы КАК Т

Посмотрим на этот пример: оператор ОБЪЕДИНЯЕТ (аналог SQL OVER) позволяет вычислить бегущую сумму. Сортировка УБЫВ сразу расставляет партии в порядке LIFO. Это наиболее современный и эффективный способ решения задачи.

Заключение

Мы разберем по шагам логику LIFO: главное отличие от FIFO заключается в направлении суммирования остатков. Используя метод интервалов, мы можем получить прозрачный и точный расчет партий внутри одного запроса, не прибегая к перебору в цикле на языке 1С. Это особенно полезно при построении сложных отчетов в СКД или при массовом пересчете себестоимости.

← На главную