Как правильно настроить вложенные циклы при списании по ФИФО в 1С:Предприятие

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

Разработка механизма проведения документов по методу ФИФО (First In, First Out) с одновременным контролем остатков — одна из фундаментальных задач в платформе «1С:Предприятие 8». Однако при добавлении в алгоритм дополнительных аналитик (например, группировок по видам номенклатуры) разработчики часто сталкиваются с логическими ошибками во вложенных циклах обхода результатов запроса. В этой статье мы подробно разберем, почему возникают такие проблемы, как правильно работать с многоуровневыми выборками и как оптимизировать алгоритм списания партий, сделав его надежным и соответствующим стандартам фирмы «1С».

В чем суть проблемы и почему код работал неверно?

Проанализируем исходную ситуацию. Разработчик попытался реализовать проведение документа «Расходная» списанием материалов со счета учета по партиям (в зависимости от даты поступления) и фиксацией продаж. В запросе были настроены итоги по двум уровням группировки:

  1. ВидНоменклатуры — верхний уровень (Товар, Услуга и т. д.).
  2. Номенклатура — подчиненный уровень (конкретная позиция).

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

1. Путаница в обращениях к разным уровням выборок. Это наиболее критическая ошибка автора темы. В цикле обхода конкретной номенклатуры (Пока Выборка.Следующий() Цикл) разработчик проверял остатки следующим образом:


Если ВыборкаНоменклатура.КоличествоОстаток < ВыборкаНоменклатура.Количество Тогда

Переменная ВыборкаНоменклатура представляет собой выборку по группировке ВидНоменклатуры. Обращаясь к ней внутри дочернего цикла, система брала суммарные показатели по всему виду номенклатуры (например, общий остаток и общую потребность по всем «Товарам» в документе вместе взятым), а не по конкретной списываемой позиции. Для исправления этой логической ошибки обращаться необходимо строго к текущей дочерней выборке — Выборка.

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


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

Когда потребность полностью закрывалась (КоличествоНадоСписать становилось равным 0), цикл по детальным записям (партиям) не прекращал свою работу. Он переходил к следующей партии. Условие ВыборкаДетальныеЗаписи.КоличествоОстаток <= КоличествоНадоСписать (например, 10 <= 0) возвращало Ложь, код уходил в ветку Иначе, где переменной КоличествоКСписанию присваивалось значение КоличествоНадоСписать (то есть 0). После этого система добавляла пустую строку в движения регистра ОстаткиМатериалов.

3. Архитектурная избыточность итогов запроса. Настройка итогов по ВидНоменклатуры в данном сценарии усложняет алгоритм проведения. Использование функции МАКСИМУМ(Количество) на уровне вида номенклатуры возвращает максимальное количество одной из позиций, а не сумму потребности, что делает данные верхнего уровня некорректными для расчетов.

---

Пошаговое исправление и оптимизация алгоритма

Разберем, как спроектировать этот узел правильно. Мы уберем избыточную группировку по ВидНоменклатуры из секции ИТОГИ запроса. Сам вид номенклатуры мы оставим в качестве обычного поля. Это позволит нам обходить выборку в один простой цикл по номенклатуре, внутри которого мы будем разделять логику для товаров (с партионным списанием) и услуг (без списания остатков).

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


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

Обратите внимание на то, как рассчитываются итоги. Мы группируем результат только по полю Номенклатура. Для полей количества и суммы из документа мы используем агрегатную функцию МАКСИМУМ, чтобы избежать их искусственного завышения при левом соединении с таблицей остатков (ведь у одной позиции номенклатуры может быть несколько партий на остатках).

---

Реализация эффективного партионного обхода в коде

Теперь спроектируем обработку результатов запроса. Вместо сложных многоуровневых условий воспользуемся лаконичным и надежным подходом:

Проанализируем готовое решение проведения документа:


Процедура ОбработкаПроведения(Отказ, Режим)
    
    // Подготовка регистров к записи движений
    Движения.ОстаткиМатериалов.Записывать = Истина;
    Движения.Продажи.Записывать = Истина;
    
    // Стираем старые движения текущего документа перед повторным проведением
    Движения.ОстаткиМатериалов.Записать();
    Движения.Продажи.Записать();
    
    Запрос = Новый Запрос;
    // (Текст запроса подготовлен на предыдущем шаге)
    Запрос.Текст = ...; 
    
    Запрос.УстановитьПараметр("Ссылка", Ссылка);
    Запрос.УстановитьПараметр("Момент", МоментВремени());
    
    РезультатЗапроса = Запрос.Выполнить();
    Если РезультатЗапроса.Пустой() Тогда
        Возврат;
    КонецЕсли;
    
    // Выбираем итоги по номенклатуре
    ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Номенклатура");
    
    Пока ВыборкаНоменклатура.Следующий() Цикл
        
        // Разделяем логику в зависимости от вида номенклатуры
        Если ВыборкаНоменклатура.ВидНоменклатуры = ПредопределенноеЗначение("Перечисление.ВидыНоменклатуры.Товар") Тогда
            
            // 1. КОНТРОЛЬ ОСТАТКОВ
            Если ВыборкаНоменклатура.КоличествоОстаток < ВыборкаНоменклатура.Количество Тогда
                
                Отказ = Истина;
                Нехватка = ВыборкаНоменклатура.Количество - ВыборкаНоменклатура.КоличествоОстаток;
                
                Сообщение = Новый СообщениеПользователю();
                Сообщение.Текст = "Не хватает " + Нехватка + " ед. товара """ + ВыборкаНоменклатура.Номенклатура + """!";
                Сообщение.Сообщить();
                
                Продолжить; // Переходим к следующему товару, движения по текущему не пишем
                
            КонецЕсли;
            
            // 2. СПИСАНИЕ ПАРТИЙ (ФИФО)
            КоличествоНадоСписать = ВыборкаНоменклатура.Количество;
            ВыборкаДетали = ВыборкаНоменклатура.Выбрать(); // Обход детальных записей (партий)
            
            Пока ВыборкаДетали.Следующий() И КоличествоНадоСписать > 0 Цикл
                
                // Вычисляем объем списания по текущей партии
                КоличествоКСписанию = Мин(КоличествоНадоСписать, ВыборкаДетали.КоличествоОстаток);
                
                // Расчет стоимости к списанию с защитой от погрешностей округления
                Если КоличествоКСписанию = ВыборкаДетали.КоличествоОстаток Тогда
                    СтоимостьКСписанию = ВыборкаДетали.СтоимостьОстаток; // Забираем всю себестоимость партии
                Иначе
                    СтоимостьКСписанию = ВыборкаДетали.СтоимостьОстаток / ВыборкаДетали.КоличествоОстаток * КоличествоКСписанию;
                КонецЕсли;
                
                // Запись в регистр остатков материалов
                Движение = Движения.ОстаткиМатериалов.ДобавитьРасход();
                Движение.Период = Дата;
                Движение.Материал = ВыборкаДетали.Номенклатура;
                Движение.Партия = ВыборкаДетали.Партия;
                Движение.Количество = КоличествоКСписанию;
                Движение.Стоимость = СтоимостьКСписанию;
                
                // Уменьшаем оставшуюся потребность
                КоличествоНадоСписать = КоличествоНадоСписать - КоличествоКСписанию;
                
            КонецЦикла;
            
        КонецЕсли;
        
        // 3. ОТРАЖЕНИЕ ВЫРУЧКИ В РЕГИСТРЕ ПРОДАЖ
        // Данная запись формируется для любого вида номенклатуры (и товаров, и услуг)
        ДвижениеПродажи = Движения.Продажи.Добавить();
        ДвижениеПродажи.Период = Дата;
        ДвижениеПродажи.Номенклатура = ВыборкаНоменклатура.Номенклатура;
        ДвижениеПродажи.Клиент = Клиент;
        ДвижениеПродажи.Мастер = Мастер;
        ДвижениеПродажи.Количество = ВыборкаНоменклатура.Количество;
        ДвижениеПродажи.Выручка = ВыборкаНоменклатура.Сумма;
        
    КонецЦикла;
    
КонецПроцедуры

Подведем итоги проделанной работы

Мы полностью переработали архитектуру проведения документа. Давайте зафиксируем ключевые правила, которые мы применили:

  1. Ориентация на нужный уровень выборки: Данные по остаткам конкретного товара всегда считываются из группировки по номенклатуре, а не из родительских записей верхнего уровня.
  2. Ограничение итераций цикла: Условие КоличествоНадоСписать > 0 гарантирует, что система мгновенно прекратит обход партий, как только потребность будет покрыта. Это исключило появление пустых движений в регистре.
  3. Прозрачность кода: Избавление от лишних итогов по видам номенклатуры в запросе позволило сократить количество вложенных циклов и упростить чтение и отладку кода.
  4. Контроль копеек: Конструкция полного списания себестоимости при обнулении остатка партии предотвращает появление копеечных хвостов, которые часто засоряют базу данных при длительной эксплуатации систем учета.
← На главную