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