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