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