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