При реализации собственного механизма списания запасов по средней стоимости разработчики часто сталкиваются с классической ошибкой: количественный остаток на складе обнуляется, а на остатке регистра накопления зависает копеечная сумма (например, 0,01 руб.). Проанализируем, почему возникает такая ситуация, как платформа 1С позиционирует документы на временной оси и как правильно организовать алгоритм списания себестоимости.
Появление нулевого остатка с ненулевой стоимостью обычно вызвано сочетанием двух факторов: методической ошибкой при округлении сумм и некорректным получением остатков на временной оси.
Рассмотрим типовую ситуацию: на складе числится 3 единицы товара общей себестоимостью 15,91 руб. Мы оформляем три отдельных расходных документа по 1 штуке. Если мы просто рассчитываем списание по стандартной формуле пропорции с округлением до двух знаков:
СебестоимостьСписания = Окр(КоличествоСписания * СуммаОстаток / КоличествоОстаток, 2);
Для первой штуки получаем: 15,91 / 3 = 5,3033..., после округления — 5,30 руб. Если при проведении второго и третьего документов система видит первоначальный остаток (3 штуки и 15,91 руб.), то каждый из трех документов спишет по 5,30 руб. В сумме будет списано: 5,30 * 3 = 15,90 руб. В результате количество товара станет равным 0, а сумма остатка составит 0,01 руб.
Почему же последующие документы видят первоначальный остаток, если они проводятся последовательно? Причина кроется в передаче обычной даты вместо объекта МоментВремени.
Разберем фундаментальное отличие между значением типа Дата и объектом МоментВремени. Дата хранит значение с точностью до секунды. Если несколько документов проведены в пределах одной и той же секунды, их дата совпадает. Когда мы передаем в параметр виртуальной таблицы остатков дату:
ВЫБРАТЬ
ЗапасыОстатки.Номенклатура КАК Номенклатура,
ЗапасыОстатки.КоличествоОстаток КАК КоличествоОстаток,
ЗапасыОстатки.СуммаОстаток КАК СуммаОстаток
ИЗ
РегистрНакопления.Запасы.Остатки(&Период, Номенклатура = &Номенклатура) КАК ЗапасыОстатки
Если в &Период передана просто Дата документа, виртуальная таблица возвращает остатки на начало указанной секунды. Все документы, зарегистрированные в одну секунду, увидят один и тот же исходный остаток, полностью игнорируя движения друг друга. Смещение даты на одну секунду вперед (Дата + 1) является грубой ошибкой: при высокой интенсивности операций в эту секунду могут попасть чужие приходные накладные, что исказит учет.
Объект МоментВремени решает эту задачу. Он включает в себя не только дату и время, но и ссылку на конкретный документ (Ссылка). Это позволяет платформе однозначно упорядочивать документы на временной оси даже при абсолютно одинаковом времени создания. Передавая в запрос МоментВремени, каждый следующий документ увидит остатки с учетом движений всех предыдущих документов.
Посмотрим на пример правильной передачи точки среза остатков в процедуре проведения документа. В модуль проведения документа необходимо передавать свойство МоментВремени() текущего документа:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗапасыОстатки.Номенклатура КАК Номенклатура,
| ЗапасыОстатки.КоличествоОстаток КАК КоличествоОстаток,
| ЗапасыОстатки.СуммаОстаток КАК СуммаОстаток
|ИЗ
| РегистрНакопления.Запасы.Остатки(&Период, Номенклатура В (&СписокНоменклатуры)) КАК ЗапасыОстатки";
Запрос.УстановитьПараметр("Период", ЭтотОбъект.МоментВремени());
Запрос.УстановитьПараметр("СписокНоменклатуры", ТаблицаТоваров.ВыгрузитьКолонку("Номенклатура"));
Если списание рассчитывается во внешней обработке или специализированном алгоритме, объект формируется явно через конструктор:
Момент = Новый МоментВремени(ТекДокумент.Дата, ТекДокумент.Ссылка);
Запрос.УстановитьПараметр("Период", Момент);
Даже при правильном упорядочивании документов на временной оси округление промежуточных списаний может приводить к расхождению копеек. Классическое решение этой задачи, применяемое в экзаменационных стандартах «1С:Специалист», заключается в проверке: полностью ли мы списываем остаток товара.
Разберем по шагам логику расчета себестоимости:
СуммаОстаток).Рассмотрим программную реализацию этого алгоритма:
Если СтрДокумента.Количество = СтрокаОстатка.КоличествоОстаток Тогда
// Списываем позицию под ноль: списываем весь суммовой остаток
СебестоимостьСписания = СтрокаОстатка.СуммаОстаток;
Иначе
// Списываем часть партии: рассчитываем среднюю
СебестоимостьСписания = Окр(СтрДокумента.Количество * СтрокаОстатка.СуммаОстаток / СтрокаОстатка.КоличествоОстаток, 2);
// Защита от превышения суммы остатка из-за округления
Если СебестоимостьСписания > СтрокаОстатка.СуммаОстаток Тогда
СебестоимостьСписания = СтрокаОстатка.СуммаОстаток;
КонецЕсли;
КонецЕсли;
При таком подходе в нашем исходном примере расчет пройдет корректно:
Окр(1 * 15.91 / 3, 2) = 5,30 руб. На остатке: 2 шт. и 10,61 руб.Окр(1 * 10.61 / 2, 2) = 5,31 руб. На остатке: 1 шт. и 5,30 руб.Для обеспечения стабильной работы механизма в многопользовательской среде и предотвращения других сопутствующих ошибок соблюдайте следующие правила разработки:
БлокировкаДанных с отбором по списываемой номенклатуре.Свернуть() или средствами запроса, иначе первая строка не успеет зафиксировать списание для второй строки.