Как правильно реализовать списание товаров по партиям в 1С?

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

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

Анализ исходного кода и работа над ошибками

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

1. Запись движений внутри цикла

В представленном фрагменте кода оператор ДвиженияРН.Записать() находится внутри цикла по номенклатуре. Это грубейшая ошибка производительности. При проведении документа, содержащего 100 строк разных товаров, система 100 раз обратится к базе данных для физической записи набора. Правило: подготавливаем все движения в оперативной памяти (в коллекции набора записей) и вызываем метод Записать() один раз в самом конце процедуры.

2. Подмена измерения Партия

Автор совершает логическую ошибку в строке: НовоеДвижениеРасход.Партия = Ссылка;. В партионном учете измерение Партия должно содержать ссылку на документ поступления (тот самый документ, который образовал остаток). Записывая туда Ссылка (текущий документ реализации), мы фактически превращаем документ в "самоприход", что делает невозможным расчет себестоимости при следующей продаже этого товара.

3. Использование агрегатов МАКСИМУМ в запросе

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

Разбор правильного алгоритма партионного списания

Давайте пошагово разберем, как должен выглядеть надежный механизм списания. Мы будем использовать методику, принятую в типовых конфигурациях и рекомендуемую фирмой "1С".

Шаг 1: Подготовка набора записей и блокировка данных

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


Движения.ПартииТоваров.Записывать = Истина;
Движения.ПартииТоваров.Очистить();
Движения.ПартииТоваров.Записать(); // Очищаем старые движения перед запросом остатков

Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ПартииТоваров");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = ТаблицаТоваров; // Таблица с номенклатурой из документа
ЭлементБлокировки.ИспользоватьИзИсточникДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();

Шаг 2: Формирование запроса к остаткам

Запрос должен выполнять две задачи: получить необходимые данные из документа (сколько надо списать) и сопоставить их с остатками в регистре (что есть в наличии). Важно использовать МоментВремени и Границу, чтобы при перепроведении документ не "видел" свои собственные старые движения.

Проанализируем структуру эффективного запроса:

  1. Временная таблица ДокТовары: группируем товары из документа по номенклатуре, суммируя количество.
  2. Получение остатков: соединяем временную таблицу с виртуальной таблицей Остатки регистра ПартииТоваров.
  3. Упорядочивание: для реализации ФИФО обязательно добавляем УПОРЯДОЧИТЬ ПО Партия.МоментВремени ВОЗР.

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

Шаг 3: Обход выборки и расчет списания

Рассмотрим процесс распределения требуемого количества по доступным партиям. Выясним причину, почему важно использовать итоги в запросе: это позволяет на уровне группировки Номенклатура сразу проверить наличие общего остатка товара на складе.


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

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

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

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

Важные замечания по расчету суммы

Разберем ситуацию с "копейками". При расчете себестоимости часто возникает погрешность округления. Если в партии осталось 3 единицы товара на общую сумму 100 рублей, то при списании одной единицы мы получим 33.3333... рубля. Если мы будем всегда использовать формулу (СуммаОстаток / КоличествоОстаток) * Списываем, то после списания всех трех единиц на остатке в регистре может зависнуть 0.01 руб. при нулевом количестве.

Решение: всегда проверяйте, не является ли текущее списываемое количество последним остатком по данной партии. Если да — просто обнуляйте сумму, присваивая ей значение ВыборкаПартии.СуммаОстаток без дополнительных вычислений.

Заключение

Подведем итог. Качественный код партионного списания должен соответствовать трем критериям:

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

← На главную