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