При интеграции 1С с внешними системами (интернет-магазинами, кассовыми аппаратами Front-end или CRM) разработчики часто сталкиваются с задачей автоматического создания документов розничной торговли. Одной из наиболее сложных операций в этом процессе является программное оформление возвратов товаров через документ Отчет о розничных продажах.
Особенно остро эта проблема проявляется в конфигурации 1С:Бухгалтерия предприятия 3.0 при использовании упрощенной системы налогообложения (УСН) с объектом налогообложения "Доходы минус расходы". При попытке провести программно созданный документ с заполненной табличной частью возвратов система выдает критические ошибки транзакционных блокировок или несоответствия типов метаданных. В этой статье мы подробно проанализируем причины возникновения данных ошибок и разберем эффективные методы их решения с примерами кода.
При программном заполнении и проведении документа ОтчетОРозничныхПродажах разработчики обычно сталкиваются с двумя типами ошибок в зависимости от того, когда был совершен возврат.
Ошибка 1. Возврат товара в день покупки (день в день)
При попытке провести документ, в котором одновременно присутствуют и продажа товара, и его возврат в один и тот же день, возникает следующая ошибка:
Ошибка при вызове метода контекста (Записать): Ошибка при выполнении обработчика - 'ОбработкаПроведения': {ОбщийМодуль.НалоговыйУчетУСН.Модуль}: Ошибка при вызове метода контекста (Заблокировать): Неверный тип значения: РегистрНакопления.РасходыПриУСН.Партия
Дополнительно в окно сообщений выводится текст: "Документом реализовано товара на 1 ед. меньше, чем возвращается".
Эта ошибка связана с тем, что подсистема налогового учета УСН пытается сопоставить возвращаемый товар с партией его поступления. Так как продажа и возврат проводятся в рамках одного документа (или в один день), алгоритм проведения не может корректно определить партию для формирующегося сторно расходов. В результате в объект блокировки данных передается пустое значение или значение неверного типа (например, дата вместо ссылки на документ), что вызывает падение платформы 1С при вызове метода Заблокировать().
Данное поведение также было официально зарегистрировано разработчиками фирмы 1С под кодом ошибки 10197456.
Ошибка 2. Возврат товара, проданного в предыдущие дни
При попытке оформить возврат за прошлые периоды возникает другая ошибка:
Ошибка при вызове метода контекста (Записать): Ошибка при выполнении обработчика - 'ОбработкаПроведения': {ОбщийМодуль.НалоговыйУчетУСН.Модуль}: Значение не является значением объектного типа (Метаданные)
Причина этого сбоя кроется в некорректном программном заполнении реквизитов табличной части Возвраты. Разработчики часто по аналогии с интерактивным вводом заполняют реквизит ДатаРеализации простой датой:
ВозвратТовара.ДатаРеализации = ДатаРеализации;
Однако в современных релизах 1С:Бухгалтерии 3.0 для корректного партионного учета УСН требуется заполнение реквизита ДокументРеализации. В этот реквизит должна передаваться именно ссылка на документ продажи (например, на предыдущий ОтчетОРозничныхПродажах). Когда общий модуль налогового учета УСН пытается выполнить метод получения метаданных у значения этого реквизита (ожидая получить объект типа ДокументСсылка), а вместо этого получает тип Дата, выполнение кода прерывается с ошибкой объектного типа.
---
Для розничной торговли самым простым, надежным и методологически верным решением проблемы возвратов день в день является сворачивание (неттинг) показателей продаж еще на этапе подготовки данных в вашей обработке.
Рассмотрим этот подход подробнее:
Товары и аналогичную строку в табличную часть Возвраты, необходимо уменьшить количество продаваемого товара в табличной части Товары на количество возвращенного.Оплата.Этот подход гарантирует отсутствие каких-либо конфликтов блокировок в регистре РасходыПриУСН и полностью исключает появление ошибки "Неверный тип значения: РегистрНакопления.РасходыПриУСН.Партия".
---
Если возврат происходит за прошлые дни, то свернуть продажи внутри одного отчета невозможно, так как продажа была отражена в закрытом периоде. В этом случае мы обязаны заполнить табличную часть Возвраты. Чтобы проведение прошло успешно, выполним следующие шаги:
Возвраты. Реквизит ДокументРеализации должен быть обязательно заполнен ссылкой на исходный документ продажи.ОтчетОРозничныхПродажах в информационной базе и подставить его в качестве ссылки.---
Давайте разберем по шагам правильную реализацию программного создания документа ОтчетОРозничныхПродажах. Мы переработаем исходный алгоритм так, чтобы разделять возвраты на "день в день" (которые мы будем сворачивать) и возвраты за прошлые периоды (для которых мы будем корректно заполнять документ реализации).
Ниже представлен готовый к использованию код интеграционной процедуры:
&НаСервере
Функция ЗагрузитьОтчетРП(УзелXML)
// Инициализация основных кодов и учетных параметров
КодОрганизации = "00-000001";
КодСклада = "00-000001";
КодСтатьиДДС = "00-000008";
КодСчетаКассы = "50.01";
КодСчетаУчета = "41.01";
КодСчетаДоходов = "90.01.1";
КодСчетаРасходов = "90.02.1";
Попытка
ДатаДокумента = Дата(УзелXML.ПолучитьАтрибут("date"));
// 1. Проверка на дублирование загрузки
Если НайтиОтчетРП(ДатаДокумента) Тогда
Лог("Отчет за этот день уже загружен: " + ДатаДокумента);
Возврат Ложь;
КонецЕсли;
// 2. Получение и проверка справочников
Организация = Справочники.Организации.НайтиПоКоду(КодОрганизации);
Склад = Справочники.Склады.НайтиПоКоду(КодСклада);
СтатьяДДС = Справочники.СтатьиДвиженияДенежныхСредств.НайтиПоКоду(КодСтатьиДДС);
СчетКассы = ПланыСчетов.Хозрасчетный.НайтиПоКоду(КодСчетаКассы);
СчетУчета = ПланыСчетов.Хозрасчетный.НайтиПоКоду(КодСчетаУчета);
СчетДоходов = ПланыСчетов.Хозрасчетный.НайтиПоКоду(КодСчетаДоходов);
СчетРасходов = ПланыСчетов.Хозрасчетный.НайтиПоКоду(КодСчетаРасходов);
ВидОплаты = Справочники.ВидыОплатОрганизаций.НайтиПоНаименованию("Карта");
Если Организация.Пустая() Или Склад.Пустая() Или СтатьяДДС.Пустая() Тогда
Лог("Ошибка: Не найдены ключевые справочники для заполнения шапки документа.");
Возврат Ложь;
КонецЕсли;
// 3. Создаем объект документа
ОтчетРП = СоздатьОтчетРП(Организация, ДатаДокумента, Склад, СтатьяДДС, СчетКассы);
// Инициализируем таблицы для предварительного анализа (сворачивания данных)
ТаблицаПродаж = Новый ТаблицаЗначений;
ТаблицаПродаж.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаПродаж.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
ТаблицаПродаж.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
ТаблицаПродаж.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
ТаблицаВозвратов = Новый ТаблицаЗначений;
ТаблицаВозвратов.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаВозвратов.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
ТаблицаВозвратов.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
ТаблицаВозвратов.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
ТаблицаВозвратов.Колонки.Добавить("ДатаРеализации", Новый ОписаниеТипов("Дата"));
ТаблицаВозвратов.Колонки.Добавить("ДокументРеализации", Новый ОписаниеТипов("ДокументСсылка.ОтчетОРозничныхПродажах"));
СуммаОплатыКартой = 0;
СуммаВозвратаКартой = 0;
// 4. Первичный обход XML дерева и наполнение временных таблиц
Для Каждого УзелСтрокаДокумента Из УзелXML.ДочерниеУзлы Цикл
Если УзелСтрокаДокумента.ИмяУзла = "item" Тогда
Номенклатура = Справочники.Номенклатура.FindByCode(УзелСтрокаДокумента.ПолучитьАтрибут("code"));
Если Не Номенклатура.Пустая() Тогда
НоваяСтрока = ТаблицаПродаж.Добавить();
НоваяСтрока.Номенклатура = Номенклатура;
НоваяСтрока.Количество = Число(УзелСтрокаДокумента.ПолучитьАтрибут("count"));
НоваяСтрока.Цена = Число(УзелСтрокаДокумента.ПолучитьАтрибут("price"));
НоваяСтрока.Сумма = НоваяСтрока.Количество * НоваяСтрока.Цена;
КонецЕсли;
ИначеЕсли УзелСтрокаДокумента.ИмяУзла = "pay" Тогда
СуммаОплатыКартой = СуммаОплатыКартой + Число(УзелСтрокаДокумента.ПолучитьАтрибут("sum"));
ИначеЕсли УзелСтрокаДокумента.ИмяУзла = "return" Тогда
Номенклатура = Справочники.Номенклатура.FindByCode(УзелСтрокаДокумента.ПолучитьАтрибут("code"));
Если Не Номенклатура.Пустая() Тогда
НоваяСтрока = ТаблицаВозвратов.Добавить();
НоваяСтрока.Номенклатура = Номенклатура;
НоваяСтрока.Количество = Число(УзелСтрокаДокумента.ПолучитьАтрибут("count"));
НоваяСтрока.Цена = Число(УзелСтрокаДокумента.ПолучитьАтрибут("price"));
НоваяСтрока.Сумма = НоваяСтрока.Количество * НоваяСтрока.Цена;
НоваяСтрока.ДатаРеализации = Дата(УзелСтрокаДокумента.ПолучитьАтрибут("sale_date"));
КонецЕсли;
ИначеЕсли УзелСтрокаДокумента.ИмяУзла = "returnpay" Тогда
СуммаВозвратаКартой = СуммаВозвратаКартой + Число(УзелСтрокаДокумента.ПолучитьАтрибут("sum"));
КонецЕсли;
КонецЦикла;
// 5. Алгоритм сворачивания возвратов "день в день"
ИндексСтроки = ТаблицаВозвратов.Количество() - 1;
Пока ИндексСтроки >= 0 Цикл
ТекущийВозврат = ТаблицаВозвратов[ИндексСтроки];
// Если возврат совершен в тот же день, что и дата отчета
Если НачалоДня(ТекущийВозврат.ДатаРеализации) = НачалоДня(ДатаДокумента) Тогда
// Ищем соответствующую строку продажи для уменьшения количества
ПараметрыОтбора = Новый Структура("Номенклатура, Цена", ТекущийВозврат.Номенклатура, ТекущийВозврат.Цена);
НайденныеСтрокиПродаж = ТаблицаПродаж.НайтиСтроки(ПараметрыОтбора);
ОстатокВозврата = ТекущийВозврат.Количество;
Для Каждого СтрокаПродажи Из НайденныеСтрокиПродаж Цикл
Если СтрокаПродажи.Количество >= ОстатокВозврата Тогда
СтрокаПродажи.Количество = СтрокаПродажи.Количество - ОстатокВозврата;
СтрокаПродажи.Сумма = СтрокаПродажи.Количество * СтрокаПродажи.Цена;
ОстатокВозврата = 0;
Прервать;
Иначе
ОстатокВозврата = ОстатокВозврата - СтрокаПродажи.Количество;
СтрокаПродажи.Количество = 0;
СтрокаПродажи.Сумма = 0;
КонецЕсли;
КонецЦикла;
// Удаляем запись из таблицы возвратов, так как она успешно зачтена в продажах
ТаблицаВозвратов.Удалить(ИндексСтроки);
КонецЕсли;
ИндексСтроки = ИндексСтроки - 1;
КонецЦикла;
// Удаляем из таблицы продаж строки с нулевым количеством (полностью возвращенные товары)
ИндексСтроки = ТаблицаПродаж.Количество() - 1;
Пока ИндексСтроки >= 0 Цикл
Если ТаблицаПродаж[ИндексСтроки].Количество = 0 Тогда
ТаблицаПродаж.Удалить(ИндексСтроки);
КонецЕсли;
ИндексСтроки = ИндексСтроки - 1;
КонецЦикла;
// Корректируем сумму оплаты по картам с учетом свернутых возвратов
ИтоговаяСуммаОплаты = Макс(0, СуммаОплатыКартой - СуммаВозвратаКартой);
// 6. Перенос чистых продаж в документ
Для Каждого СтрокаПродажи Из ТаблицаПродаж Цикл
ДобавитьСтрокуВОтчетРП(ОтчетРП, СтрокаПродажи.Номенклатура, СтрокаПродажи.Количество, СтрокаПродажи.Цена, СчетУчета, СчетРасходов, СчетДоходов);
КонецЦикла;
// Запись оплат картой
Если ИтоговаяСуммаОплаты > 0 Тогда
ДобавитьОплатуВОтчетРП(ОтчетРП, ВидОплаты, ИтоговаяСуммаОплаты);
КонецЕсли;
// 7. Перенос возвратов прошлых периодов с поиском документов реализации
Для Каждого СтрокаВозврата Из ТаблицаВозвратов Цикл
// Находим реальный документ продажи в базе по номенклатуре и дате
ДокументРеализации = НайтиДокументПродажиДляВозврата(СтрокаВозврата.Номенклатура, СтрокаВозврата.ДатаРеализации);
Если ЗначениеЗаполнено(ДокументРеализации) Тогда
ДобавитьСтрокуВозвратаВОтчетРП(ОтчетРП, СтрокаВозврата.Номенклатура, СтрокаВозврата.Количество, СтрокаВозврата.Цена, ДокументРеализации, СчетУчета, СчетРасходов, СчетДоходов);
Иначе
Лог("Предупреждение: Не найден исходный документ реализации для товара: " + СтрокаВозврата.Номенклатура + ". Будет подставлено пустое значение партии.");
ДобавитьСтрокуВозвратаВОтчетРП(ОтчетРП, СтрокаВозврата.Номенклатура, СтрокаВозврата.Количество, СтрокаВозврата.Цена, Неопределено, СчетУчета, СчетРасходов, СчетДоходов);
КонецЕсли;
КонецЦикла;
// Перед проведением обязательно вызываем заполнение типовых реквизитов
ОтчетРП.Записать(РежимЗаписиДокумента.Проведение);
Лог("Документ успешно загружен и проведен: " + ОтчетРП.Ссылка);
Возврат Истина;
Исключение
Лог("Ошибка при загрузке документа: " + ОписаниеОшибки());
Возврат Ложь;
КонецПопытки;
КонецФункции
Проанализируем вспомогательную функцию для поиска документов-партий реализации за прошлые дни. Это ключевой элемент, позволяющий избежать ошибки Значение не является значением объектного типа (Метаданные).
&НаСервере
Функция НайтиДокументПродажиДляВозврата(Номенклатура, ДатаРеализации)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ 1
| ОтчетОРозничныхПродажахТовары.Ссылка КАК Ссылка
|ИЗ
| Документ.ОтчетОРозничныхПродажах.Товары КАК ОтчетОРозничныхПродажахТовары
|ГДЕ
| ОтчетОРозничныхПродажахТовары.Номенклатура = &Номенклатура
| И ОтчетОРозничныхПродажахТовары.Ссылка.Проведен = ИСТИНА
| И ОтчетОРозничныхПродажахТовары.Ссылка.Дата МЕЖДУ &НачалоДня И &КонецДня";
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
Запрос.УстановитьПараметр("НачалоДня", НачалоДня(ДатаРеализации));
Запрос.УстановитьПараметр("КонецДня", КонецДня(ДатаРеализации));
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат Выборка.Ссылка;
КонецЕсли;
Возврат Неопределено;
КонецФункции
Теперь скорректируем саму процедуру добавления строки возврата. Обратите внимание, что вместо примитивного типа даты в реквизит ДокументРеализации (или в зависимости от релиза в реквизит ДокументПартии) передается найденная нами ссылка.
&НаСервере
Процедура ДобавитьСтрокуВозвратаВОтчетРП(ОтчетРП, Номенклатура, Количество, ЦенаТовара, ДокументРеализации, СчетУчета, СчетРасходов, СчетДоходов)
ВозвратТовара = ОтчетРП.Возвраты.Добавить();
ВозвратТовара.Номенклатура = Номенклатура;
ВозвратТовара.Количество = Количество;
ВозвратТовара.Цена = ЦенаТовара;
ВозвратТовара.Сумма = ЦенаТовара * Количество;
ВозвратТовара.СчетУчета = СчетУчета;
ВозвратТовара.СчетРасходов = СчетРасходов;
ВозвратТовара.СчетДоходов = СчетДоходов;
ВозвратТовара.Субконто = Справочники.НоменклатурныеГруппы.НайтиПоНаименованию("Основная номенклатурная группа");
ВозвратТовара.СтавкаНДС = Перечисления.СтавкиНДС.БезНДС;
// Передаем ссылку на исходную продажу для корректного УСН-анализа партий
Если ЗначениеЗаполнено(ДокументРеализации) Тогда
ВозвратТовара.ДокументРеализации = ДокументРеализации;
КонецЕсли;
КонецПроцедуры
---
Для того чтобы минимизировать риски возникновения ошибок блокировок при работе с транзакционными механизмами 1С:Бухгалтерии 3.0, рекомендуем придерживаться следующих правил:
ЗаполнитьЗначенияСвойств или специализированных методов БСП в модулях менеджеров документов).Применяя данные методики и исправленный программный код, вы сможете настроить стабильный импорт отчетов о продажах из любых сторонних источников в 1С:Бухгалтерию 3.0, обеспечив при этом безупречную работу партионного и налогового учета УСН.