Как правильно реализовать списание партий по ФИФО в 1С 8.3? Разбор типичных ошибок и готовое решение

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

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

В этой статье мы подробно разберем, как устроен правильный алгоритм партионного списания, проанализируем ошибки из обсуждения на форуме и соберем идеальный, оптимизированный шаблон кода для документа Расходная накладная.

Представим исходные условия нашей задачи. В системе присутствует регистр накопления остатков ПартииТоваров со следующей структурой:

---

Анализ классических ошибок разработчика

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

1. Пропадание последнего движения из-за неверной логики цикла

В сообщениях 14 и 21 автора мы видим следующий фрагмент кода:


ПеремКолвоКСписанию = ПеремКолвоКСписанию - РезультатКол;
Если ПеремКолвоКСписанию = 0 Тогда
    Прервать;
Иначе         
    Движение = Движения.ПартииТоваров.Добавить();
    ...
КонецЕсли;

Проанализируем эту ситуацию: если нам требуется списать 10 единиц товара и первая же найденная партия содержит ровно 10 единиц, переменная ПеремКолвоКСписанию после вычитания становится равной нулю. Программа сразу же заходит в ветку Если и выполняет инструкцию Прервать. В результате движение списания по регистру вообще не записывается! Запись движения должна происходить до проверки условия выхода из цикла.

2. Списание остатка вместо количества из документа

В сообщениях 21 и 25 автор инициализирует переменную списания так:

ПеремКолвоКСписанию = ВыборкаНомен.Остаток;

Здесь ВыборкаНомен.Остаток — это суммарный остаток товара на складе, полученный из регистра накопления. Получается, что программа пытается списать абсолютно весь товар, который числится в системе, полностью игнорируя то количество, которое пользователь указал в табличной части документа. Списывать нужно строго требуемое количество из документа.

3. Неэффективное левое соединение (Антипаттерн)

В сообщении 58 автор связывает временную таблицу товаров документа с виртуальной таблицей остатков через ЛЕВОЕ СОЕДИНЕНИЕ, оставляя параметры виртуальной таблицы пустыми:


ИЗ
    ВТТаблицаНоменклатуры КАК ВТТаблицаНоменклатуры
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ПартииТоваров.Остатки(&МоментВр, ) КАК ПартииТоваровОстатки
    ПО ВТТаблицаНоменклатуры.Номенклатура = ПартииТоваровОстатки.Номенклатура

Выясним причину неэффективности такого решения. При выполнении этого запроса СУБД сначала выберет остатки по всей номенклатуре, которая когда-либо регистрировалась в системе, а затем будет сопоставлять этот гигантский массив с несколькими строками документа. На реальных базах данных это приведет к колоссальному падению производительности. Нам необходимо передавать список номенклатуры в параметры виртуальной таблицы остатков.

4. Проблема «округления копеек» при списании последней партии

При расчете стоимости списываемой партии пропорционально количеству неизбежно возникает погрешность округления:


Движение.Сумма = ВыборкаПартия.СуммаОстаток * РезультатКол / ВыборкаПартия.Остаток;

Если партия списывается полностью (в ноль), из-за округления на остатках регистра может зависнуть копеечная сумма (например, количество станет равным 0, а сумма останется 0.01 или -0.02). Чтобы избежать этого, мы должны явно проверять: если списываемое количество совпадает с остатком по партии, то списывать нужно всю оставшуюся сумму партии до копейки без деления.

5. Отсутствие управляемых блокировок данных

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

6. Очистка существующих движений при перепроведении

При повторном проведении документа (например, задним числом) запрос остатков вернет данные, которые уже включают в себя старые движения этого же документа. В результате себестоимость рассчитается неверно. Нам необходимо предварительно очищать движения документа перед формированием нового запроса остатков.

---

Пошаговое построение правильного алгоритма ФИФО

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

Шаг 1. Очистка старых движений документа

В самом начале процедуры ОбработкаПроведения мы должны принудительно очистить и записать набор записей регистра по нашему документу. Это гарантирует, что при перепроведении система «забудет» старые движения накладной и получит чистые остатки.

Шаг 2. Использование управляемой блокировки

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

Шаг 3. Подготовка данных документа

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

Шаг 4. Оптимальный запрос остатков

Наш запрос будет решать сразу несколько задач:

  1. Помещать товары документа во временную таблицу;
  2. Выбирать остатки партий только по тем позициям, которые есть во временной таблице (фильтрация на уровне параметров виртуальной таблицы остатков);
  3. Упорядочивать партии по моменту времени их возникновения (для соблюдения правила ФИФО);
  4. Рассчитывать итоги по номенклатуре для удобного обхода в цикле.

Для корректного получения остатков при оперативном и неоперативном проведении в качестве параметра виртуальной таблицы остатков мы будем использовать объект Граница со значением Исключая относительно момента времени нашего документа.

---

Готовое программное решение

Посмотрим на финальный программный код процедуры ОбработкаПроведения, в котором учтены все вышеперечисленные правила и оптимизации:


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

---

Разбор ключевых преимуществ представленного решения

Проанализируем, почему данный вариант программного кода является эталонным и рекомендуется к применению на практике (в том числе при сдаче экзамена «1С:Специалист по платформе»):

Таким образом, мы детально рассмотрели процесс списания по ФИФО и устранили все потенциально опасные уязвимости алгоритма, сделав проведение документов надежным и быстродействующим.

← На главную