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

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

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

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

Анализ ошибок в базовом запросе

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


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

Выясним, какие скрытые проблемы содержит такой подход:

  1. Потеря данных из-за значений NULL: При левом соединении для позиций, по которым цена не найдена, поле Цена принимает значение NULL. В языке запросов 1С любая арифметическая операция со значением NULL (например, NULL * Количество) возвращает NULL, в результате чего строка полностью выпадает из расчета. Поле цены обязательно нужно оборачивать в функцию ЕСТЬNULL(ПрайсЛистСрезПоследних.Цена, 0).
  2. Разыменование полей через точку: Конструкция ПолнаяТаблица.Номенклатура.Артикул в секции соединения порождает неявное соединение со справочником номенклатуры на уровне СУБД, что ухудшает производительность. Связывать таблицы следует напрямую по ссылочным полям либо выгружать артикулы во временную таблицу.
  3. Отсутствие фильтра в параметрах виртуальной таблицы: Если не передавать условие отбора во второй параметр СрезПоследних, СУБД сначала рассчитывает срез по всей таблице регистра, и лишь затем соединяет результат с документом.

Вариант 1. Использование виртуальной таблицы «СрезПоследних» с фильтром по номенклатуре

Если дата расчета цены одинакова для всех строк документа (например, дата проведения или реквизит &ПериодРегистрации), наиболее эффективным решением будет предварительная выгрузка списка номенклатуры во временную таблицу и передача ее в параметры виртуальной таблицы.

Разберем по шагам построение такого запроса:


ВЫБРАТЬ РАЗЛИЧНЫЕ
    ТаблицаДокумента.Номенклатура КАК Номенклатура
ПОМЕСТИТЬ ВТ_ТоварыДокумента
ИЗ
    Документ.Заявка.ПолнаяТаблица КАК ТаблицаДокумента
ГДЕ
    ТаблицаДокумента.Ссылка = &Ссылка
;

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

В этом случае платформа 1С на уровне СУБД сформирует оптимальный срез только для тех товаров, которые присутствуют в документе, с использованием системных индексов.

Вариант 2. Классическая схема через группировку и две временные таблицы (для разных дат строк)

Виртуальная таблица СрезПоследних принимает только один фиксированный скалярный параметр периода. Если в таблице содержатся строки с разными датами (например, при анализе периодических движений или расчете цен на дату каждой отдельной операции), виртуальная таблица не подойдет.

В такой ситуации проанализируем надежный и производительный алгоритм на базе реальной таблицы регистра:

  1. Формируем временную таблицу исходных строк документа с датами среза.
  2. Соединяем исходные строки с физической таблицей регистра по условию Период <= ДатаСреза и вычисляем МАКСИМУМ(Период) для каждой номенклатуры.
  3. Соединяем полученные максимальные периоды с физической таблицей регистра для получения точного значения цены.

Посмотрим на пример реализации запроса:


ВЫБРАТЬ
    ДокументТаблица.Номенклатура КАК Номенклатура,
    ДокументТаблица.Количество КАК Количество,
    ДокументТаблица.Скидка КАК Скидка,
    &ПериодРегистрации КАК ДатаСреза
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ИЗ
    Документ.Заявка.ПолнаяТаблица КАК ДокументТаблица
ГДЕ
    ДокументТаблица.Ссылка = &Ссылка
;

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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Исходные.Номенклатура КАК Номенклатура,
    Исходные.Количество КАК Количество,
    ЕСТЬNULL(ПрайсЛист.Цена, 0) КАК Цена,
    ЕСТЬNULL(ПрайсЛист.Цена, 0) * (Исходные.Скидка / 100) * Исходные.Количество КАК Сумма
ИЗ
    ВТ_ИсходныеДанные КАК Исходные
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_МаксимальныеПериоды КАК МаксПериоды
        ПО Исходные.Номенклатура = МаксПериоды.Номенклатура
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПрайсЛист КАК ПрайсЛист
        ПО МаксПериоды.Номенклатура = ПрайсЛист.Номенклатура
        И МаксПериоды.МаксПериод = ПрайсЛист.Период

Вариант 3. Подзапрос с выбором первого элемента в условии связи

На практике часто встречается способ соединения через подзапрос с конструкцией ВЫБРАТЬ ПЕРВЫЕ 1. Рассмотрим этот пример:


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

Обратите внимание: данный вариант прост в написании, однако конструкция Период В (ВЫБРАТЬ ПЕРВЫЕ 1...) в условии соединения заставляет СУБД выполнять вложенные циклы (Nested Loops) для каждой строки выборки. На больших объемах данных такой запрос может работать значительно медленнее, чем схема с временными таблицами и группировкой по максимальному периоду.

Рекомендации по выбору метода

Подведем итог и выделим ключевые правила работы с периодическими регистрами:

  1. Если дата для всех строк одна — используем РегистрСведений.ИмяРегистра.СрезПоследних с обязательной передачей массива или временной таблицы номенклатуры во второй параметр виртуальной таблицы.
  2. Если даты среза различаются для каждой строки выборки — используем физическую таблицу регистра с вычислением МАКСИМУМ(Период) через временные таблицы.
  3. Все вычисляемые поля с ценами обязательно защищаем функцией ЕСТЬNULL во избежание потери данных при арифметических операциях.
← На главную