При разработке механизмов проведения документов в 1С часто возникает задача реализации гибкого списания партий. Типичный сценарий: если пользователь в табличной части документа явно указал партию, система должна списать товар именно с неё. Если же партия не указана или товара на ней недостаточно, остаток должен списываться по стандартному методу FIFO (First In, First Out). Эта задача является классической при подготовке к экзамену «1С:Специалист по платформе». В данной статье мы подробно разберем, как построить оптимальный запрос и программный алгоритм для решения этой задачи.
Для реализации корректного списания нам необходимо работать с двумя источниками данных: табличной частью документа (наша потребность) и регистром накопления (наши остатки). Основная сложность заключается в том, что в одной табличной части могут быть строки как с заполненной партией, так и с пустой. Проанализируем требуемую логику списания:
Первым делом нам необходимо собрать все данные из табличной части документа, сгруппировав их по номенклатуре и партии. Это важно, так как пользователь может ввести несколько строк с одним и тем же товаром. Рассмотрим структуру первого пакета запроса. Обратите внимание на обязательную индексацию по полю Номенклатура, это существенно ускорит последующее соединение с виртуальными таблицами остатков.
ВЫБРАТЬ
ТЧ.Номенклатура КАК Номенклатура,
ТЧ.Партия КАК ПартияДокумента,
СУММА(ТЧ.Количество) КАК КоличествоДокумент
ПОМЕСТИТЬ ВТ_ДокТЧ
ИЗ
Документ.ПеремещениеТоваров.СписокНоменклатуры КАК ТЧ
ГДЕ
ТЧ.Ссылка = &Ссылка
СГРУППИРОВАТЬ ПО
ТЧ.Номенклатура,
ТЧ.Партия
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
Теперь проанализируем самый ответственный этап — соединение данных документа с остатками в регистре. Чтобы реализовать условие «сначала указанная партия, потом остальные», мы введем искусственное поле Приоритет. Выясним, как оно работает:
Мы будем использовать ЛЕВОЕ СОЕДИНЕНИЕ от временной таблицы документа к виртуальной таблице остатков регистра ОстаткиНоменклатуры.Остатки. Соединение выполняется только по номенклатуре. Это позволит нам увидеть все доступные партии для каждого товара из документа.
В поле выбора мы напишем условие: если партия из регистра совпадает с партией, указанной в документе, то присваиваем такой строке приоритет «0». Всем остальным партиям присваиваем приоритет «1». При сортировке строки с приоритетом «0» поднимутся вверх.
ВЫБРАТЬ
ВТ_ДокТЧ.Номенклатура КАК Номенклатура,
ВТ_ДокТЧ.КоличествоДокумент КАК КоличествоДокумент,
ВТ_ДокТЧ.ПартияДокумента КАК ПартияДокумента,
Остатки.Партия КАК ПартияРегистра,
ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК КоличествоОстаток,
ВЫБОР
КОГДА Остатки.Партия = ВТ_ДокТЧ.ПартияДокумента ТОГДА 0
ИНАЧЕ 1
КОНЕЦ КАК Приоритет
ИЗ
ВТ_ДокТЧ КАК ВТ_ДокТЧ
ЛЕВОЕ СОЕДИНЕНИЕ
РегистрНакопления.ПродуктыНаСкладах.Остатки(&МоментВремени,
Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_ДокТЧ)
И Склад = &СкладИсточник) КАК Остатки
ПО
ВТ_ДокТЧ.Номенклатура = Остатки.Номенклатура
УПОРЯДОЧИТЬ ПО
Номенклатура,
Приоритет,
Остатки.Партия.МоментВремени
ИТОГИ
МАКСИМУМ(КоличествоДокумент),
СУММА(КоличествоОстаток)
ПО
Номенклатура,
ПартияДокумента
Разберем подробнее, почему в УПОРЯДОЧИТЬ ПО крайне важен порядок полей. Сначала мы группируем всё по номенклатуре. Затем внутри номенклатуры выводим ту партию, которая была выбрана пользователем (Приоритет = 0). После неё пойдут все остальные партии, отсортированные по времени их возникновения (Остатки.Партия.МоментВремени) — это и есть механизм FIFO.
После выполнения запроса нам необходимо правильно обойти результат и сформировать движения. Мы будем использовать два уровня выборки: по группировкам (Номенклатура + ПартияДокумента) и детальные записи.
Рассмотрим пример кода на встроенном языке 1С:
РезультатЗапроса = Запрос.Выполнить();
ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаНоменклатура.Следующий() Цикл
// Проверка общего остатка по номенклатуре для контроля
Если ВыборкаНоменклатура.КоличествоДокумент > ВыборкаНоменклатура.КоличествоОстаток Тогда
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно товара " + ВыборкаНоменклатура.Номенклатура +
". Требуется: " + ВыборкаНоменклатура.КоличествоДокумент +
", в наличии: " + ВыборкаНоменклатура.КоличествоОстаток;
Сообщение.Сообщить();
Отказ = Истина;
КонецЕсли;
Если Отказ Тогда Продолжить; КонецЕсли;
// Списание порциями
ОсталосьСписать = ВыборкаНоменклатура.КоличествоДокумент;
ВыборкаДетали = ВыборкаНоменклатура.Выбрать();
Пока ВыборкаДетали.Следующий() И ОсталосьСписать > 0 Цикл
Если ВыборкаДетали.КоличествоОстаток = 0 Тогда Продолжить; КонецЕсли;
СписываемСейчас = Мин(ОсталосьСписать, ВыборкаДетали.КоличествоОстаток);
// Формируем движение расхода
Движение = Движения.ПродуктыНаСкладах.ДобавитьРасход();
Движение.Период = Дата;
Движение.Номенклатура = ВыборкаДетали.Номенклатура;
Движение.Склад = СкладИсточник;
Движение.Партия = ВыборкаДетали.ПартияРегистра;
Движение.Количество = СписываемСейчас;
// Формируем движение прихода на новый склад (если это перемещение)
ДвижениеПриход = Движения.ПродуктыНаСкладах.ДобавитьПриход();
ДвижениеПриход.Период = Дата;
ДвижениеПриход.Номенклатура = ВыборкаДетали.Номенклатура;
ДвижениеПриход.Склад = СкладПриемник;
ДвижениеПриход.Партия = ВыборкаДетали.ПартияРегистра; // Обычно партия сохраняется
ДвижениеПриход.Количество = СписываемСейчас;
ОсталосьСписать = ОсталосьСписать - СписываемСейчас;
КонецЦикла;
КонецЦикла;
Проанализируем несколько тонких моментов, которые часто упускают начинающие разработчики:
&МоментВремени. Это гарантирует корректное получение остатков в случае перепроводимого документа, так как МоментВремени включает в себя и дату, и ссылку на сам документ.ПартияДокумента будет содержать пустую ссылку. В этом случае условие Остатки.Партия = ВТ_ДокТЧ.ПартияДокумента никогда не выполнится (так как в регистре обычно нет записей с пустой партией), и все строки получат Приоритет = 1. Система автоматически перейдет к списанию строго по FIFO, что и требуется по условию.МАКСИМУМ(КоличествоДокумент), чтобы внутри одной группировки видеть реальную потребность, а не сумму.Резюме: Мы рассмотрели универсальный способ решения задачи партионного учета с приоритетом. Основная идея заключается в вычислении веса (приоритета) каждой строки в запросе и последующей двухуровневой обработке выборки в коде. Такой подход является наиболее производительным и соответствует стандартам разработки на платформе 1С:Предприятие.