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