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