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