Как в запросе 1С получить цену по первой и последней партии товара на остатке?

Программист 1С v8.3 (Управляемые формы) Управленческий учет Торговля и дистрибуция
← На главную

В работе с динамическими списками часто возникает задача отобразить не просто одну цену, а некий ценовой диапазон. Классический пример — показать цену "ОТ" и "ДО" для товаров с партионным учетом. При этом цена "ОТ" должна соответствовать самой ранней партии, которая еще есть в наличии, а цена "ДО" — самой поздней. Давайте разберем, как правильно построить такой запрос, избегая распространенных ошибок.

Основная сложность заключается в том, что простое использование агрегатных функций МИНИМУМ(Цена) и МАКСИМУМ(Цена) не даст нужного результата. Эти функции найдут минимальную и максимальную цену из всех партий товара, а не цену конкретно первой и последней по дате партии. Проанализируем ситуацию и построим корректный запрос по шагам.

Анализ исходной проблемы и неверного подхода

Рассмотрим первоначальный вариант запроса, который работал некорректно. Его главная ошибка — попытка получить минимальную и максимальную цену напрямую, без привязки к датам партий.


// Неверный подход
ВЫБРАТЬ
    СправочникНоменклатура.Ссылка КАК Номенклатура,
    ...
    МИНИМУМ(Цены.Цена) КАК ЦенаОТ,
    МАКСИМУМ(Цены.Цена) КАК ЦенаДО
ИЗ
    Справочник.Номенклатура КАК СправочникНоменклатура
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СвободныеОстатки.Остатки(...) КАК Остатки
        ПО СправочникНоменклатура.Ссылка = Остатки.Номенклатура
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(...) КАК Цены
        ПО СправочникНоменклатура.Ссылка = Цены.Номенклатура
...
СГРУППИРОВАТЬ ПО
    СправочникНоменклатура.Ссылка,
    ...

Почему это не работает?

  1. Запрос выбирает все цены, когда-либо установленные для номенклатуры, и находит среди них минимум и максимум. Он не учитывает, есть ли товары из этих партий в остатке.
  2. Если цена самой ранней по дате партии (например, 150 руб.) выше, чем у более поздней (например, 120 руб.), то МИНИМУМ(Цена) вернет 120, что не является ценой первой партии.

Для решения задачи необходимо сначала определить, какие именно партии являются первой и последней из имеющихся в наличии, и только потом получать их цены. Для этого идеально подходит использование временных таблиц.

Пошаговое построение корректного запроса

Разделим сложную задачу на несколько простых логических шагов. Это не только упростит написание запроса, но и сделает его более читаемым и легким для отладки.

Шаг 1. Получаем все партии, имеющие положительный остаток

В первую очередь, нам нужен список всех партий, которые физически есть на складе. Важно получить не только ссылку на партию, но и ее дату (или другой реквизит, по которому можно определить последовательность), так как именно по ней мы будем искать "первую" и "последнюю".


ВЫБРАТЬ
    Остатки.Номенклатура,
    Остатки.Партия,
    Остатки.Партия.Дата КАК ДатаПартии // Предполагаем, что у справочника "Партии" есть реквизит "Дата"
ПОМЕСТИТЬ ПартииВНаличии
ИЗ
    РегистрНакопления.СвободныеОстатки.Остатки(&Период, Склад = &Склад) КАК Остатки
ГДЕ
    Остатки.ВНаличииОстаток - Остатки.ВРезервеСоСкладаОстаток > 0

Важный момент: если у вашего справочника Партии нет реквизита `Дата`, но есть ссылка на документ-основание (например, ПриходныйДокумент), то дату нужно получать через него: Остатки.Партия.ПриходныйДокумент.Дата. Главное — получить поле для сортировки.

Шаг 2. Определяем даты первой и последней партии для каждой номенклатуры

Теперь, имея список всех актуальных партий, сгруппируем его по номенклатуре и найдем минимальную и максимальную дату. Это ключевой шаг, который определит наши временные границы.


ВЫБРАТЬ
    ПартииВНаличии.Номенклатура,
    МИНИМУМ(ПартииВНаличии.ДатаПартии) КАК ДатаПервойПартии,
    МАКСИМУМ(ПартииВНаличии.ДатаПартии) КАК ДатаПоследнейПартии
ПОМЕСТИТЬ ГраничныеДатыПартий
ИЗ
    ПартииВНаличии КАК ПартииВНаличии
СГРУППИРОВАТЬ ПО
    ПартииВНаличии.Номенклатура

Шаг 3. Находим конкретные партии и их цены

Зная граничные даты для каждой номенклатуры, мы можем найти сами партии и получить их цены. Для этого соединим таблицу партий в наличии (ПартииВНаличии) с таблицей граничных дат (ГраничныеДатыПартий). Соединение будем делать дважды: один раз для поиска первой партии, второй — для последней.

Чтобы избежать дублей, если на одну дату приходится несколько партий, используем агрегатную функцию МАКСИМУМ() для цены. Это выберет одну цену, если партий с одинаковой датой несколько.


ВЫБРАТЬ
    ГраничныеДаты.Номенклатура,
    МАКСИМУМ(ЦеныПервой.Цена) КАК ЦенаПервойПартии,
    МАКСИМУМ(ЦеныПоследней.Цена) КАК ЦенаПоследнейПартии
ПОМЕСТИТЬ ИтоговыеЦены
ИЗ
    ГраничныеДатыПартий КАК ГраничныеДаты
    // Получаем цену для первой партии
    ЛЕВОЕ СОЕДИНЕНИЕ ПартииВНаличии КАК ПервыеПартии
        ПО ГраничныеДаты.Номенклатура = ПервыеПартии.Номенклатура
        И ГраничныеДаты.ДатаПервойПартии = ПервыеПартии.ДатаПартии
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК ЦеныПервой
        ПО ПервыеПартии.Партия = ЦеныПервой.Партия
        И ЦеныПервой.ВидЦены = &ВидЦены

    // Получаем цену для последней партии
    ЛЕВОЕ СОЕДИНЕНИЕ ПартииВНаличии КАК ПоследниеПартии
        ПО ГраничныеДаты.Номенклатура = ПоследниеПартии.Номенклатура
        И ГраничныеДаты.ДатаПоследнейПартии = ПоследниеПартии.ДатаПартии
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК ЦеныПоследней
        ПО ПоследниеПартии.Партия = ЦеныПоследней.Партия
        И ЦеныПоследней.ВидЦены = &ВидЦены
СГРУППИРОВАТЬ ПО
    ГраничныеДаты.Номенклатура

Шаг 4. Собираем итоговый запрос для динамического списка

Наконец, объединяем все полученные данные. Основной таблицей будет справочник Номенклатура. К нему присоединяем данные об остатках и нашу таблицу с рассчитанными ценами ИтоговыеЦены. Чтобы избежать дублирования строк из-за нескольких партий в остатках, используем группировку по номенклатуре и агрегируем остатки через СУММА().


ВЫБРАТЬ
    СпрНоменклатура.Ссылка КАК Номенклатура,
    СпрНоменклатура.Артикул,
    СпрНоменклатура.МинимальныйЗаказ,
    СпрНоменклатура.КоличествоВУпаковке,
    СпрНоменклатура.ОсобыйТовар,
    ЕСТЬNULL(СуммарныеОстатки.Количество, 0) КАК Количество,
    ЕСТЬNULL(СуммарныеОстатки.КоличествоВРезерве, 0) КАК КоличествоВРезерве,
    ЕСТЬNULL(ГрафикОтгрузки.КоличествоПодЗаказ, 0) КАК КоличествоПодЗаказ,
    
    // Выводим цену "ОТ" для особых товаров
    ВЫБОР
        КОГДА СпрНоменклатура.ОсобыйТовар = ИСТИНА И ИтоговыеЦены.ЦенаПервойПартии <> ИтоговыеЦены.ЦенаПоследнейПартии
            ТОГДА ИтоговыеЦены.ЦенаПервойПартии
        ИНАЧE 0
    КОНЕЦ КАК ЦенаМИН,
    
    // Цена "ДО" - всегда цена последней партии
    ЕСТЬNULL(ИтоговыеЦены.ЦенаПоследнейПартии, 0) КАК ЦенаМАКС
ИЗ
    Справочник.Номенклатура КАК СпрНоменклатура
    
    // Присоединяем итоговые цены
    ЛЕВОЕ СОЕДИНЕНИЕ ИтоговыеЦены КАК ИтоговыеЦены
        ПО СпрНоменклатура.Ссылка = ИтоговыеЦены.Номенклатура

    // Присоединяем суммарные остатки (вложенным запросом, чтобы избежать дублей)
    ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
            Остатки.Номенклатура,
            СУММА(Остатки.ВНаличииОстаток - Остатки.ВРезервеСоСкладаОстаток) КАК Количество,
            СУММА(Остатки.ВРезервеСоСкладаОстаток) КАК КоличествоВРезерве
        ИЗ
            РегистрНакопления.СвободныеОстатки.Остатки(&Период, Склад = &Склад) КАК Остатки
        СГРУППИРОВАТЬ ПО
            Остатки.Номенклатура) КАК СуммарныеОстатки
        ПО СпрНоменклатура.Ссылка = СуммарныеОстатки.Номенклатура

    // Присоединяем данные по заказам
    ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
            График.Номенклатура,
            СУММА(График.КоличествоНеобеспеченоОстаток) КАК КоличествоПодЗаказ
        ИЗ
            РегистрНакопления.ГрафикОтгрузкиТоваров.Остатки(&Период, Склад = &Склад) КАК График
        СГРУППИРОВАТЬ ПО
            График.Номенклатура) КАК ГрафикОтгрузки
        ПО СпрНоменклатура.Ссылка = ГрафикОтгрузки.Номенклатура
ГДЕ
    СпрНоменклатура.ЭтоГруппа = ЛОЖЬ
    И СпрНоменклатура.Ссылка В ИЕРАРХИИ(&РодительТовара)

Этот финальный запрос решает поставленную задачу: он корректно находит цены для первой и последней партии из наличия и агрегирует общие остатки, что исключает ошибку дублирования ключевых полей в динамическом списке.

Рекомендация по оптимизации для высоконагруженных систем

Для динамических списков, которые используются очень активно, выполнение такого сложного запроса в реальном времени может создавать нагрузку. Есть альтернативный, более производительный подход.

Использование pre-расчета:

  1. Создайте отдельный периодический РегистрСведений, например, "ЦеныПартийНоменклатуры", с измерениями Номенклатура, Склад и ресурсами ЦенаПервойПартии, ЦенаПоследнейПартии.
  2. Создайте регламентное задание, которое будет с определенной периодичностью (например, раз в 5-10 минут) выполнять описанный выше сложный запрос и записывать результаты в этот новый регистр.
  3. Запрос для динамического списка в этом случае станет предельно простым и быстрым. Вам нужно будет лишь сделать ЛЕВОЕ СОЕДИНЕНИЕ со справочником номенклатуры и этим новым регистром.

Этот подход позволяет перенести основную вычислительную нагрузку в фоновый режим, обеспечивая мгновенное открытие и работу формы для пользователя.

← На главную