Как правильно реализовать списание партий: сначала указанные в документе, а остаток по FIFO?

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

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

Анализ проблемы «статичных остатков» в запросе

Разберем ситуацию, с которой столкнулся автор темы. Основная ошибка заключается в попытке соединить таблицу документа с таблицей остатков в одном запросе и сразу получить результат для списания. Проблема в том, что запрос в 1С — это статичный снимок данных. Если в документе две строки с одной и той же номенклатурой, запрос по каждой из них увидит один и тот же исходный остаток в базе данных. Он не «знает», что первая строка уже «забрала» часть товара, поэтому для второй строки он выдаст те же самые цифры, что приведет к некорректному списанию и «уходу в минус».

Для решения этой задачи нам необходимо использовать динамический расчет остатков. Мы проанализируем два наиболее эффективных метода: итеративный обход в коде с использованием ТаблицыЗначений и продвинутый метод запроса с «нарастающим итогом».

Метод 1: Итеративная обработка через Таблицу Значений

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

Рассмотрим по шагам, как это реализовать:

  1. Подготовка данных документа: Сгруппируем строки табличной части по Номенклатура и Партия. Это важно, чтобы не обрабатывать одну и ту же позицию несколько раз.
  2. Получение остатков: Одним запросом получим все остатки по нужным позициям номенклатуры из регистра. Результат выгрузим в ТаблицаЗначений.
  3. Сортировка: В таблице остатков установим сортировку по ДатаПартии (для соблюдения FIFO).
  4. Распределение (Двухэтапный проход):

Рассмотрим пример кода для реализации логики распределения:


// 1. Сначала списываем конкретно указанные партии
Для Каждого СтрокаДока Из ТЧДокумента Цикл
    Если ЗначениеЗаполнено(СтрокаДока.Партия) Тогда
        ПараметрыОтбора = Новый Структура("Номенклатура, Партия", СтрокаДока.Номенклатура, СтрокаДока.Партия);
        НайденныеСтроки = ТаблицаОстатков.НайтиСтроки(ПараметрыОтбора);
        
        Для Каждого СтрокаОстатка Из НайденныеСтроки Цикл
            Списать = Мин(СтрокаДока.Количество, СтрокаОстатка.Остаток);
            СтрокаДока.Количество = СтрокаДока.Количество - Списать;
            СтрокаОстатка.Остаток = СтрокаОстатка.Остаток - Списать;
        КонецЦикла;
    КонецЕсли;
КонецЦикла;

// 2. Вторым проходом закрываем остатки по FIFO (где партия не была указана или её не хватило)
Для Каждого СтрокаДока Из ТЧДокумента Цикл
    Если СтрокаДока.Количество > 0 Тогда
        ПараметрыОтбора = Новый Структура("Номенклатура", СтрокаДока.Номенклатура);
        // Важно: Строки должны быть предварительно отсортированы по дате партии
        НайденныеСтроки = ТаблицаОстатков.НайтиСтроки(ПараметрыОтбора);
        
        Для Каждого СтрокаОстатка Из НайденныеСтроки Цикл
            Если СтрокаОстатка.Остаток <= 0 Тогда Продолжить; КонецЕсли;
            
            Списать = Мин(СтрокаДока.Количество, СтрокаОстатка.Остаток);
            // Здесь фиксируем списание в движения или временную таблицу
            РегистрироватьСписание(СтрокаДока.Номенклатура, СтрокаОстатка.Партия, Списать);
            
            СтрокаДока.Количество = СтрокаДока.Количество - Списать;
            СтрокаОстатка.Остаток = СтрокаОстатка.Остаток - Списать;
            
            Если СтрокаДока.Количество = 0 Тогда Прервать; КонецЕсли;
        КонецЦикла;
    КонецЕсли;
КонецЦикла;

Метод 2: Использование запроса с приоритезацией и нарастающим итогом

Проанализируем более сложный, но производительный способ — решение задачи средствами языка запросов. Чтобы реализовать логику "сначала указанная партия, потом остальные", мы можем применить искусственную сортировку.

Выясним причину, почему это работает: мы добавляем в запрос вычисляемое поле (вес), которое определяет порядок списания. Для партии, которая совпадает с указанной в документе, вес будет равен 0, для всех остальных — 1. Затем внутри каждой группы весов мы сортируем данные по дате.

Рассмотрим структуру такого запроса:


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

После подготовки такой таблицы в МенеджерВременныхТаблиц, мы можем применить технику нарастающего итога. Суть её в том, что для каждой строки остатков мы вычисляем сумму всех предыдущих строк. Если накопленная сумма меньше требуемого количества в документе, значит, эта партия подлежит списанию.

Важные нюансы и рекомендации

При реализации любого из методов обратите внимание на следующие критические моменты:

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

← На главную