При построении аналитических отчетов и вычислении себестоимости часто возникает задача определения конкретных партий (документов прихода), которые формируют текущий количественный остаток номенклатуры на складе по принципу FIFO (First In, First Out). Рассмотрим логику решения этой задачи, особенности реализации в языке запросов 1С и альтернативные архитектурные варианты.
Проанализируем ситуацию на простом примере движений по складу:
Итоговый остаток на складе равен 4 шт. Согласно принципу FIFO, в первую очередь списываются самые старые партии. Посмотрим, как распределился расход объемом 16 шт. (6 + 10):
Таким образом, текущий остаток 4 шт. формируется поступлением 4 (2 шт.) и частью поступления 2 (2 шт.). Выясним, как получить этот результат с помощью языка запросов 1С.
Поскольку в языке запросов 1С отсутствуют аналитические оконные функции (такие как SUM(...) OVER(...)), нарастающие итоги вычисляются через связывание таблицы партий самой с собой по условию неравенства дат или моментов времени.
Разберем алгоритм по шагам:
РегистрНакопления.ОстаткиНоменклатуры.Остатки().Рассмотрим пример построения такого запроса:
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.КоличествоОстаток КАК ВсегоОстаток
ПОМЕСТИТЬ ВТ_ОбщийОстаток
ИЗ
РегистрНакопления.ОстаткиНоменклатуры.Остатки(&Период, Номенклатура = &Номенклатура) КАК Остатки
;
ВЫБРАТЬ
Движения.Номенклатура КАК Номенклатура,
Движения.Регистратор КАК ДокументПоступления,
Движения.Период КАК Период,
Движения.Количество КАК КоличествоПриход
ПОМЕСТИТЬ ВТ_Приходы
ИЗ
РегистрНакопления.ОстаткиНоменклатуры КАК Движения
ГДЕ
Движения.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
И Движения.Номенклатура = &Номенклатура
И Движения.Период <= &Период
;
ВЫБРАТЬ
П1.Номенклатура КАК Номенклатура,
П1.ДокументПоступления КАК ДокументПоступления,
П1.КоличествоПриход КАК КоличествоПриход,
ЕСТЬНУЛЛ(СУММА(П2.КоличествоПриход), 0) КАК НарастающийИтогПозже
ПОМЕСТИТЬ ВТ_ПриходыСИтогами
ИЗ
ВТ_Приходы КАК П1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Приходы КАК П2
ПО П1.Номенклатура = П2.Номенклатура
И П1.Период < П2.Период
ГРУППИРОВАТЬ ПО
П1.Номенклатура,
П1.ДокументПоступления,
П1.КоличествоПриход,
П1.Период
;
ВЫБРАТЬ
Т1.Номенклатура КАК Номенклатура,
Т1.ДокументПоступления КАК ДокументПоступления,
ВЫБОР
КОГДА Т1.НарастающийИтогПозже + Т1.КоличествоПриход <= Т2.ВсегоОстаток
ТОГДА Т1.КоличествоПриход
ИНАЧЕ Т2.ВсегоОстаток - Т1.НарастающийИтогПозже
КОНЕЦ КАК ОстатокПоПартии
ИЗ
ВТ_ПриходыСИтогами КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ОбщийОстаток КАК Т2
ПО Т1.Номенклатура = Т2.Номенклатура
ГДЕ
Т1.НарастающийИтогПозже < Т2.ВсегоОстаток
При использовании данного подхода в реальных системах следует учитывать несколько важных факторов:
O(N²). На больших объемах документов этот запрос может вызывать высокую нагрузку на СУБД.Период не даст однозначного порядка. В условиях соединения необходимо дополнительно сравнивать МоментВремени или уникальную ссылку документа.Самый производительный и методически верный способ в архитектуре 1С — хранение аналитики партий непосредственно в регистре накопления остатков. Для этого в измерение регистра добавляется ссылка на документ поступления (Партия). При проведении документов расхода списки партий списываются алгоритмически по FIFO, а остаток по партиям получается простым запросом к виртуальной таблице Остатки() без использования сложной математики в запросе.
Если менять структуру регистра нельзя, оптимальным по скорости решением будет выборка всех приходов в обратном хронологическом порядке и выборка общего остатка. Далее в коде модуля выполняется простой цикл обхода:
ОстатокДляРаспределения = ОбщийОстаток;
Пока ВыборкаПриходов.Следующий() И ОстатокДляРаспределения > 0 Цикл
КоличествоПоПартии = МИН(ВыборкаПриходов.Количество, ОстатокДляРаспределения);
// Записываем результат по партии
ОстатокДляРаспределения = ОстатокДляРаспределения - КоличествоПоПартии;
КонецЦикла;
Этот способ работает значительно быстрее вычисления нарастающих итогов средствами SQL при большом количестве документов.