Реализация механизмов партионного учета — одна из базовых и в то же время ответственных задач для разработчика 1С. Методы FIFO (First In, First Out — первым пришел, первым ушел) и LIFO (Last In, First Out — последним пришел, первым ушел) определяют последовательность списания партий товаров со склада. В данной статье мы подробно разберем, как написать минимально простой, но рабочий код для проведения документа реализации по регистру накопления остатков с возможностью выбора метода списания.
Для корректного списания нам необходимо решить три подзадачи:
Рассмотрим пример регистра накопления РегистрПартий (тип «Остатки»), где измерениями являются Номенклатура, Склад и Партия (документ поступления), а ресурсами — Количество и Сумма.
Первым делом мы должны сгруппировать строки табличной части документа. Это важно, так как один и тот же товар может встречаться в документе несколько раз. Проанализируем фрагмент кода, подготавливающий временную таблицу списания:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РасходнаяНакладнаяТовары.Номенклатура КАК Номенклатура,
| СУММА(РасходнаяНакладнаяТовары.Количество) КАК Количество
|ПОМЕСТИТЬ ВТ_РасходТоваров
|ИЗ
| Документ.РасходнаяНакладная.Товары КАК РасходнаяНакладнаяТовары
|ГДЕ
| РасходнаяНакладнаяТовары.Ссылка = &Ссылка
|СГРУППИРОВАТЬ ПО
| РасходнаяНакладнаяТовары.Номенклатура
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Обратите внимание на использование конструкции ИНДЕКСИРОВАТЬ ПО. При работе с большими документами индексация временной таблицы по номенклатуре существенно ускорит последующее соединение с таблицами остатков регистра.
Теперь нам нужно получить остатки из регистра РегистрПартий. Ключевой момент здесь — сортировка. Для FIFO мы сортируем партии по возрастанию момента времени (от старых к новым), а для LIFO — по убыванию (от новых к старым).
Рассмотрим, как динамически менять текст запроса в зависимости от настроек системы:
// Базовая часть запроса для получения остатков
Запрос.Текст = Запрос.Текст + ";
|ВЫБРАТЬ
| Остатки.Номенклатура,
| Остатки.Партия,
| Остатки.КоличествоОстаток,
| Остатки.СуммаОстаток,
| Остатки.Партия.МоментВремени КАК ПартияМоментВремени
|ИЗ
| РегистрНакопления.РегистрПартий.Остатки(&МоментВремени, Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_РасходТоваров)) КАК Остатки
|УПОРЯДОЧИТЬ ПО
| ПартияМоментВремени ВОЗР"; // По умолчанию FIFO
// Если выбран метод LIFO, меняем сортировку
Если МетодСписания = Перечисления.МетодыСписания.LIFO Тогда
Запрос.Текст = СтрЗаменить(Запрос.Текст, "ПартияМоментВремени ВОЗР", "ПартияМоментВремени УБЫВ");
КонецЕсли;
Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), ВидГраницы.Исключая));
Использование объекта Граница с видом Исключая гарантирует, что при перепроведении документа мы увидим остатки, которые были до записи этого документа. Это критически важно для предотвращения «самосписания».
После выполнения запроса мы получаем выборку, которую нужно обойти. Основная сложность здесь — расчет суммы списания и корректное закрытие партий в ноль.
РезультатЗапроса = Запрос.Выполнить();
ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаНоменклатура.Следующий() Цикл
КоличествоКСписанию = ВыборкаНоменклатура.Количество;
ВыборкаДетальныеЗаписи = ВыборкаНоменклатура.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() И КоличествоКСписанию > 0 Цикл
// Определяем, сколько можем взять из текущей партии
КоличествоРасход = Мин(КоличествоКСписанию, ВыборкаДетальныеЗаписи.КоличествоОстаток);
// Расчет суммы (себестоимости)
Если КоличествоРасход = ВыборкаДетальныеЗаписи.КоличествоОстаток Тогда
// Если списываем всё под ноль — забираем всю сумму, чтобы не было "хвостов"
СуммаРасход = ВыборкаДетальныеЗаписи.СуммаОстаток;
Иначе
СуммаРасход = ВыборкаДетальныеЗаписи.СуммаОстаток / ВыборкаДетальныеЗаписи.КоличествоОстаток * КоличествоРасход;
КонецЕсли;
// Формируем движение
Движение = Движения.РегистрПартий.ДобавитьРасход();
Движение.Период = Дата;
Движение.Номенклатура = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Партия = ВыборкаДетальныеЗаписи.Партия;
Движение.Количество = КоличествоРасход;
Движение.Сумма = СуммаРасход;
КоличествоКСписанию = КоличествоКСписанию - КоличествоРасход;
КонецЦикла;
// Проверка на нехватку товара
Если КоличествоКСписанию > 0 Тогда
Сообщить("Товара " + ВыборкаНоменклатура.Номенклатура + " не хватает в количестве " + КоличествоКСписанию);
Отказ = Истина;
КонецЕсли;
КонецЦикла;
Проанализируем ситуацию с расчетом суммы. Пропорциональный расчет СуммаОстаток / КоличествоОстаток * КоличествоРасход часто приводит к появлению микроскопических остатков (например, 0.01 рубля при 0 количестве) из-за округления. Поэтому мы применили правило: при списании последнего количества партии списывается вся оставшаяся сумма.
Рассмотрим также вопрос управляемых блокировок. В промышленном решении перед выполнением запроса обязательно нужно установить блокировку на те элементы номенклатуры, которые мы собираемся списывать. Это предотвратит чтение некорректных остатков другими сеансами в момент проведения нашего документа.
Пример установки блокировки:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.РегистрПартий");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = СписокТоваровДокумента; // Таблица с номенклатурой
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();
В приведенном примере мы использовали так называемую «старую методику» (сначала читаем остатки, потом записываем движения). Она проще в реализации и нагляднее для понимания принципов FIFO/LIFO. Однако в высоконагруженных системах рекомендуется «новая методика»:
Новая методика позволяет минимизировать время блокировки таблиц базы данных, так как чтение остатков происходит после того, как документ уже «заявил» свои права на товар.
Мы выяснили причину важности сортировки по моменту времени партии и разобрали алгоритм, исключающий появление копеечных «хвостов». Данный код является фундаментом для построения любой системы складского учета. Помните, что помимо FIFO и LIFO существует еще метод FEFO (списание по срокам годности), который реализуется аналогично, но с сортировкой по реквизиту СрокГодности в справочнике или регистре партий.