Почему не рассчитывается себестоимость сопутствующих товаров в документе «Отчет о розничных продажах» и как это исправить

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

При автоматизации розничной торговли часто возникает задача сопутствующего списания расходных материалов. Например, при продаже кофе необходимо автоматически списывать стаканчик, крышку и размешиватель. На практике разработчики часто пытаются решить эту задачу «в лоб» — программно формируя движения по регистрам в момент проведения документа ОтчетОРозничныхПродажах. Однако при закрытии месяца выясняется неприятная деталь: по основному товару (кофе) себестоимость списывается корректно, а по сопутствующим материалам себестоимость остается нулевой.

В этой статье мы подробно разберем, почему ручные движения ломают логику закрытия месяца в «1С:Управление торговлей 11» (а также в 1С:КА 2 и 1С:ERP), где именно в коде происходят эти проверки и как правильно реализовать этот механизм с точки зрения архитектуры системы.

Архитектурная причина: почему «ручные» движения игнорируются расчетом себестоимости

В современных конфигурациях 1С на платформе ERP-решений (включая УТ 11) расчет себестоимости устроен принципиально иначе, чем в старых редакциях (например, УТ 10.3). Программа не производит окончательное списание партий в момент проведения первичного документа. Вместо этого при закрытии месяца запускается сложный математический движок — решение системы линейных алгебраических уравнений (СЛАУ).

Этот движок работает исключительно с жестко определенными первичными регистрами накопления:

  1. РегистрНакопления.СебестоимостьТоваров — ключевой количественно-стоимостной регистр учета запасов.
  2. РегистрНакопления.ПартииТоваровОрганизаций — партионный регистр (используется при методе оценки ФИФО).

Регистр ДвиженияНоменклатураДоходыРасходы, по которому разработчики часто делают ручные движения, является вторичным оборотным регистром. Движения по нему формируются первичным документом только в части количества. Суммовые показатели в этот регистр транслируются и записываются самим документом РасчетСебестоимостиТоваров в момент закрытия месяца.

Если алгоритм расчета себестоимости не смог сопоставить движения по первичному контуру (из-за отсутствия аналитик, незаполненного вида запасов или отсутствия записей в регистре партий), то расчет СЛАУ просто проигнорирует эти строки, и себестоимость в регистр ДвиженияНоменклатураДоходыРасходы записана не будет.

Где искать проблему в коде: анализ процедур расчета

Давайте проанализируем, в каких общих модулях и функциях происходит магия расчета себестоимости, чтобы понять, почему ручные проводки отсекаются системой. Для этого обратимся к общему модулю РасчетСебестоимости (или РасчетСебестоимостиПрикладныеАлгоритмы в зависимости от версии релиза):

  1. Функция СформироватьУзлыКорректировкиСписанияСтоимости. Здесь система группирует все выбытия товаров в так называемые «узлы распределения». Каждый узел — это уникальная комбинация организации, склада, аналитики номенклатуры и, что критически важно, Вида запасов. Если при программной записи движений в документ ОтчетОРозничныхПродажах вид запасов был определен неверно или остался пустым, СЛАУ не сможет сопоставить расход с приходом.
  2. Функция ТекстЗапросаДвиженияСтоимости. Этот запрос собирает данные о стоимости партий по регистрам. В нем накладываются жесткие условия на разрезы учета и хозяйственные операции. Если ваши кастомные движения не соответствуют стандартным ожиданиям этого запроса, себестоимость по ним останется нулевой.

Способ 1. Использование механизма «Наборов» (Решение без программирования)

Прежде чем приступать к написанию кода, всегда стоит проверить возможности типового функционала. В 1С:УТ 11 для подобных задач предусмотрен специальный тип номенклатуры — Набор.

Рассмотрим по шагам, как настроить эту схему:

  1. Создадим новую карточку номенклатуры «Кофе в стакане» и укажем тип номенклатуры Набор.
  2. В настройках набора перейдем в правила сборки и добавим комплектующие: кофе (товар), стаканчик (товар), размешиватель (товар) в нужных пропорциях.
  3. В карточке набора укажем способ распределения цены (например, цена распределяется по долям стоимости комплектующих, либо вся цена относится на основную позицию — кофе, а сопутствующие материалы имеют нулевую долю стоимости).

При выборе такого набора в документе ОтчетОРозничныхПродажах на складе будут автоматически заблокированы и списаны все комплектующие по их реальной себестоимости. При этом пользователь видит только одну понятную строку продажи, а система отрабатывает списание материалов в штатном режиме без единой строчки кастомного кода.

Способ 2. Программное дозаполнение табличной части «Товары» (Правильный код)

Если бизнес-процесс требует, чтобы сопутствующие товары списывались скрыто от интерфейса пользователя, но при этом корректно учитывались в себестоимости, правильнее всего отказаться от ручного формирования движений по регистрам. Вместо этого необходимо программно добавлять сопутствующие товары непосредственно в табличную часть «Товары» документа ОтчетОРозничныхПродажах.

Разберем этот алгоритм. При проведении документа программа должна проанализировать табличную часть, найти строки с кофе и автоматически добавить строки со стаканами и размешивателями с нулевой ценой.

Посмотрим на пример реализации в модуле объекта документа ОтчетОРозничныхПродажах перед выполнением стандартного проведения:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Алгоритм автоматического добавления сопутствующих товаров
    Если РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
        ДобавитьСопутствующиеТовары();
    КонецЕсли;
    
КонецПроцедуры

Процедура ДобавитьСопутствующиеТовары()
    
    // 1. Сначала очистим ранее автоматически добавленные строки, чтобы избежать зацикливания
    // Для этого сопутствующие товары можно помечать в каком-либо служебном реквизите или определять по номенклатурной группе
    Индекс = Товары.Количество() - 1;
    Пока Индекс >= 0 Цикл
        СтрокаТЧ = Товары[Индекс];
        Если СтрокаТЧ.Номенклатура.Родитель = Справочники.Номенклатура.НайтиПоНаименованию("Сопутствующие товары") Тогда
            Товары.Удалить(Индекс);
        КонецЕсли;
        Индекс = Индекс - 1;
    КонецЦикла;
    
    // 2. Анализируем продажи и добавляем расходники
    ТаблицаПродаж = Товары.Выгрузить();
    Для Каждого СтрокаПродажи Из ТаблицаПродаж Цикл
        
        // Если продали кофе, то нужно списать стакан и размешиватель
        Если СтрокаПродажи.Номенклатура = Справочники.Номенклатура.НайтиПоНаименованию("Кофе") Тогда
            
            // Добавляем стаканчик
            НоваяСтрока = Товары.Добавить();
            ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаПродажи);
            НоваяСтрока.Номенклатура = Справочники.Номенклатура.НайтиПоНаименованию("Стаканчик бумажный");
            НоваяСтрока.Количество = СтрокаПродажи.Количество; // 1 стакан на 1 порцию
            НоваяСтрока.Цена = 0;
            НоваяСтрока.Сумма = 0;
            НоваяСтрока.СуммаНДС = 0;
            
            // Добавляем размешиватель
            НоваяСтрокаРазмешиватель = Товары.Добавить();
            ЗаполнитьЗначенияСвойств(НоваяСтрокаРазмешиватель, СтрокаПродажи);
            НоваяСтрокаРазмешиватель.Номенклатура = Справочники.Номенклатура.НайтиПоНаименованию("Размешиватель пластиковый");
            НоваяСтрокаРазмешиватель.Количество = СтрокаПродажи.Количество;
            НоваяСтрокаРазмешиватель.Цена = 0;
            НоваяСтрокаРазмешиватель.Сумма = 0;
            НоваяСтрокаРазмешиватель.СуммаНДС = 0;
            
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Почему этот способ гарантирует расчет себестоимости? При таком подходе система воспринимает сопутствующие товары как обычную позицию номенклатуры, проданную со скидкой 100% (по нулевой цене). Стандартный механизм проведения сам заполнит правильные КлючиАналитикиУчетаНоменклатуры, определит нужные ВидыЗапасв и сделает корректные движения по регистрам СебестоимостьТоваров и ПартииТоваровОрганизаций. В конце месяца регламентный документ без проблем рассчитает и спишет себестоимость этих материалов на финансовый результат розничных продаж.

Способ 3. Как исправить накопленные ошибки прошлых периодов

Если база данных уже содержит множество документов за закрытые или текущие периоды, по которым движения сформированы некорректно, простое изменение кода проведения не исправит ситуацию «задним числом». Нам потребуется написать внешнюю обработку для корректировки данных.

Проанализируем пошаговый алгоритм исправления исторических данных:

  1. Пишем запрос, который находит все документы ОтчетОРозничныхПродажах за период, требующий корректировки.
  2. Для каждого найденного документа программно заполняем табличную часть «Товары» сопутствующими позициями (по аналогии со Способом 2).
  3. Записываем и проводим документ. Внимание: чтобы миновать возможные блокировки периодов и ускорить процесс, рекомендуется сначала записывать документ в режиме загрузки данных, а затем запускать только проведение по регистрам.

Рассмотрим пример кода внешней обработки для исправления исторических документов:


Процедура ВосстановитьДокументыЗаПериод(НачалоПериода, КонецПериода) Экспорт
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ОтчетОРозничныхПродажах.Ссылка КАК Ссылка
        |ИЗ
        |	Документ.ОтчетОРозничныхПродажах КАК ОтчетОРозничныхПродажах
        |ГДЕ
        |	ОтчетОРозничныхПродажах.Проведен
        |	И ОтчетОРозничныхПродажах.Дата МЕЖДУ &НачалоПериода И &КонецПериода";
    
    Запрос.УстановитьПараметр("НачалоПериода", НачалоПериода);
    Запрос.УстановитьПараметр("КонецПериода", КонецПериода);
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    Пока Выборка.Следующий() Цикл
        
        ДокОбъект = Выборка.Ссылка.ПолучитьОбъект();
        
        // Блокируем объект от совместного доступа
        ДокОбъект.Заблокировать();
        
        // Вызываем логику заполнения ТЧ
        // (Процедура ДобавитьСопутствующиеТовары должна быть доступна или перенесена в обработку)
        ДобавитьСопутствующиеТоварыКонтекст(ДокОбъект);
        
        // Записываем документ без проведения с признаком Загрузка, чтобы избежать проверок
        ДокОбъект.ОбменДанными.Загрузка = Истина;
        ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
        ДокОбъект.ОбменДанными.Загрузка = Ложь;
        
        // Теперь проводим документ в штатном режиме для формирования правильных движений
        Попытка
            ДокОбъект.Записать(РежимЗаписиДокумента.Проведение, РежимПроведенияДокумента.Неоперативный);
            Сообщить("Успешно обработан документ: " + Строка(Выборка.Ссылка));
        Исключение
            Сообщить("Ошибка проведения документа " + Строка(Выборка.Ссылка) + ": " + ОписаниеОшибки());
        КонецПопытки;
        
    КонецЦикла;
    
КонецПроцедуры

После успешного выполнения обработки во всех скорректированных документах появятся штатные движения по регистрам учета запасов. Нам останется лишь запустить регламентную операцию Закрытие месяца. Программа пересчитает себестоимость, заполнит отсутствующие суммовые показатели в регистре СебестоимостьТоваров и автоматически перенесет правильные суммы в оборотный регистр ДвиженияНоменклатураДоходыРасходы.

← На главную