Как реализовать перемещение товаров с выбором конкретной партии и списанием по FIFO

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

При разработке механизмов проведения документов в 1С часто возникает задача реализации гибкого списания партий. Типичный сценарий: если пользователь в табличной части документа явно указал партию, система должна списать товар именно с неё. Если же партия не указана или товара на ней недостаточно, остаток должен списываться по стандартному методу FIFO (First In, First Out). Эта задача является классической при подготовке к экзамену «1С:Специалист по платформе». В данной статье мы подробно разберем, как построить оптимальный запрос и программный алгоритм для решения этой задачи.

Анализ постановки задачи

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

  1. Если в строке документа указана партия: сначала списываем с неё. Если товара на этой партии меньше, чем нужно, остаток добираем из других имеющихся партий этой же номенклатуры, начиная с самых старых (FIFO).
  2. Если в строке документа партия не указана: списываем весь объем по методу FIFO.

Шаг 1. Подготовка данных документа во временной таблице

Первым делом нам необходимо собрать все данные из табличной части документа, сгруппировав их по номенклатуре и партии. Это важно, так как пользователь может ввести несколько строк с одним и тем же товаром. Рассмотрим структуру первого пакета запроса. Обратите внимание на обязательную индексацию по полю Номенклатура, это существенно ускорит последующее соединение с виртуальными таблицами остатков.


ВЫБРАТЬ
    ТЧ.Номенклатура КАК Номенклатура,
    ТЧ.Партия КАК ПартияДокумента,
    СУММА(ТЧ.Количество) КАК КоличествоДокумент
ПОМЕСТИТЬ ВТ_ДокТЧ
ИЗ
    Документ.ПеремещениеТоваров.СписокНоменклатуры КАК ТЧ
ГДЕ
    ТЧ.Ссылка = &Ссылка
СГРУППИРОВАТЬ ПО
    ТЧ.Номенклатура,
    ТЧ.Партия
ИНДЕКСИРОВАТЬ ПО
    Номенклатура
;

Шаг 2. Получение остатков и реализация логики приоритетов

Теперь проанализируем самый ответственный этап — соединение данных документа с остатками в регистре. Чтобы реализовать условие «сначала указанная партия, потом остальные», мы введем искусственное поле Приоритет. Выясним, как оно работает:

Мы будем использовать ЛЕВОЕ СОЕДИНЕНИЕ от временной таблицы документа к виртуальной таблице остатков регистра ОстаткиНоменклатуры.Остатки. Соединение выполняется только по номенклатуре. Это позволит нам увидеть все доступные партии для каждого товара из документа.

В поле выбора мы напишем условие: если партия из регистра совпадает с партией, указанной в документе, то присваиваем такой строке приоритет «0». Всем остальным партиям присваиваем приоритет «1». При сортировке строки с приоритетом «0» поднимутся вверх.


ВЫБРАТЬ
    ВТ_ДокТЧ.Номенклатура КАК Номенклатура,
    ВТ_ДокТЧ.КоличествоДокумент КАК КоличествоДокумент,
    ВТ_ДокТЧ.ПартияДокумента КАК ПартияДокумента,
    Остатки.Партия КАК ПартияРегистра,
    ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК КоличествоОстаток,
    ВЫБОР
        КОГДА Остатки.Партия = ВТ_ДокТЧ.ПартияДокумента ТОГДА 0
        ИНАЧЕ 1
    КОНЕЦ КАК Приоритет
ИЗ
    ВТ_ДокТЧ КАК ВТ_ДокТЧ
ЛЕВОЕ СОЕДИНЕНИЕ 
    РегистрНакопления.ПродуктыНаСкладах.Остатки(&МоментВремени, 
        Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_ДокТЧ)
        И Склад = &СкладИсточник) КАК Остатки
ПО 
    ВТ_ДокТЧ.Номенклатура = Остатки.Номенклатура
УПОРЯДОЧИТЬ ПО
    Номенклатура,
    Приоритет,
    Остатки.Партия.МоментВремени
ИТОГИ
    МАКСИМУМ(КоличествоДокумент),
    СУММА(КоличествоОстаток)
ПО
    Номенклатура,
    ПартияДокумента

Разберем подробнее, почему в УПОРЯДОЧИТЬ ПО крайне важен порядок полей. Сначала мы группируем всё по номенклатуре. Затем внутри номенклатуры выводим ту партию, которая была выбрана пользователем (Приоритет = 0). После неё пойдут все остальные партии, отсортированные по времени их возникновения (Остатки.Партия.МоментВремени) — это и есть механизм FIFO.

Шаг 3. Обработка результата в модуле проведения

После выполнения запроса нам необходимо правильно обойти результат и сформировать движения. Мы будем использовать два уровня выборки: по группировкам (Номенклатура + ПартияДокумента) и детальные записи.

Рассмотрим пример кода на встроенном языке 1С:


РезультатЗапроса = Запрос.Выполнить();
ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаНоменклатура.Следующий() Цикл
    
    // Проверка общего остатка по номенклатуре для контроля
    Если ВыборкаНоменклатура.КоличествоДокумент > ВыборкаНоменклатура.КоличествоОстаток Тогда
        Сообщение = Новый СообщениеПользователю;
        Сообщение.Текст = "Недостаточно товара " + ВыборкаНоменклатура.Номенклатура + 
                          ". Требуется: " + ВыборкаНоменклатура.КоличествоДокумент + 
                          ", в наличии: " + ВыборкаНоменклатура.КоличествоОстаток;
        Сообщение.Сообщить();
        Отказ = Истина;
    КонецЕсли;

    Если Отказ Тогда Продолжить; КонецЕсли;

    // Списание порциями
    ОсталосьСписать = ВыборкаНоменклатура.КоличествоДокумент;
    ВыборкаДетали = ВыборкаНоменклатура.Выбрать();
    
    Пока ВыборкаДетали.Следующий() И ОсталосьСписать > 0 Цикл
        
        Если ВыборкаДетали.КоличествоОстаток = 0 Тогда Продолжить; КонецЕсли;
        
        СписываемСейчас = Мин(ОсталосьСписать, ВыборкаДетали.КоличествоОстаток);
        
        // Формируем движение расхода
        Движение = Движения.ПродуктыНаСкладах.ДобавитьРасход();
        Движение.Период = Дата;
        Движение.Номенклатура = ВыборкаДетали.Номенклатура;
        Движение.Склад = СкладИсточник;
        Движение.Партия = ВыборкаДетали.ПартияРегистра;
        Движение.Количество = СписываемСейчас;
        
        // Формируем движение прихода на новый склад (если это перемещение)
        ДвижениеПриход = Движения.ПродуктыНаСкладах.ДобавитьПриход();
        ДвижениеПриход.Период = Дата;
        ДвижениеПриход.Номенклатура = ВыборкаДетали.Номенклатура;
        ДвижениеПриход.Склад = СкладПриемник;
        ДвижениеПриход.Партия = ВыборкаДетали.ПартияРегистра; // Обычно партия сохраняется
        ДвижениеПриход.Количество = СписываемСейчас;

        ОсталосьСписать = ОсталосьСписать - СписываемСейчас;
        
    КонецЦикла;
КонецЦикла;

Важные замечания по производительности и логике

Проанализируем несколько тонких моментов, которые часто упускают начинающие разработчики:

  1. Момент времени: Всегда передавайте в параметры виртуальной таблицы &МоментВремени. Это гарантирует корректное получение остатков в случае перепроводимого документа, так как МоментВремени включает в себя и дату, и ссылку на сам документ.
  2. Пустые ссылки: Если в документе партия не выбрана, поле ПартияДокумента будет содержать пустую ссылку. В этом случае условие Остатки.Партия = ВТ_ДокТЧ.ПартияДокумента никогда не выполнится (так как в регистре обычно нет записей с пустой партией), и все строки получат Приоритет = 1. Система автоматически перейдет к списанию строго по FIFO, что и требуется по условию.
  3. Избыточность данных: Обратите внимание, что при левом соединении по номенклатуре строки документа могут «размножаться» в результате запроса (одна строка документа соединяется со многими строками остатков). Именно поэтому мы используем итоги МАКСИМУМ(КоличествоДокумент), чтобы внутри одной группировки видеть реальную потребность, а не сумму.
  4. Разделение складов: Не забывайте устанавливать фильтр по складу источнику в параметрах виртуальной таблицы остатков, иначе запрос вернет остатки по всем складам предприятия.

Резюме: Мы рассмотрели универсальный способ решения задачи партионного учета с приоритетом. Основная идея заключается в вычислении веса (приоритета) каждой строки в запросе и последующей двухуровневой обработке выборки в коде. Такой подход является наиболее производительным и соответствует стандартам разработки на платформе 1С:Предприятие.

← На главную