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