Как в запросе 1С объединить приход и расход товаров с расчетом оценки по ценам на дату документа?

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

При построении складских и управленческих отчетов (в частности, аналогов товарного отчета ТОРГ-29) часто возникает необходимость свести воедино операции поступления и выбытия номенклатуры, рассчитав их суммовую оценку по фиксированному виду цены. Задача существенно усложняется, когда требуется применить не просто текущую цену на конец периода, а стоимость, действовавшую непосредственно в момент проведения каждого документа, а также отразить суммы переоценки складских остатков.

Разберем эту задачу по шагам: от базового объединения приходных и расходных движений до многоэтапного сопоставления цен на дату регистратора в пакетных запросах 1С.

Шаг 1. Объединение движений прихода и расхода с правильными знаками

Проанализируем распространенную ошибку: попытку соединить таблицы поступления и отгрузки через левое или полное соединение. Соединение связывает строки по совпадению полей, в то время как операции прихода и расхода происходят в разные моменты времени и оформляются разными документами. Для построения хронологической ведомости таблицы необходимо объединять по вертикали с помощью конструкции ОБЪЕДИНИТЬ ВСЕ.

Рассмотрим ключевые правила эффективного объединения данных:

  1. Установка фильтров в источниках: условия по складу и организации следует накладывать сразу в секции ГДЕ или параметрах виртуальных таблиц исходных выборок, чтобы не передавать лишние данные во временные таблицы.
  2. Смена знака для расхода: при выборке количества из таблиц отгрузки или расхода умножаем количество на -1, чтобы при последующем суммировании автоматически получить корректный баланс.
  3. Исключение избыточных полей: выбираем только те разрезы, которые требуются для соединения с регистром цен и итогового отчета (регистратор, дата, номенклатура, характеристика, количество).

Посмотрим на пример формирования первичной временной таблицы движений:


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

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ТоварыКОтгрузке.Регистратор,
    ТоварыКОтгрузке.Регистратор.Дата,
    ТоварыКОтгрузке.Номенклатура,
    ТоварыКОтгрузке.Характеристика,
    ТоварыКОтгрузке.Количество * -1
ИЗ
    РегистрНакопления.ТоварыКОтгрузке КАК ТоварыКОтгрузке
ГДЕ
    ТоварыКОтгрузке.Склад = &Склад;

Шаг 2. Получение среза цен на дату каждого документа

Выясним причину, почему нельзя просто обратиться к виртуальной таблице РегистрСведений.ЦеныНоменклатуры.СрезПоследних. В языке запросов 1С параметр периода виртуальной таблицы среза последних не может принимать динамическое значение (поле запроса Регистратор.Дата). Он принимает только фиксированную дату из параметров запроса.

Чтобы определить цену, актуальную на момент проведения каждого отдельного регистратора, применим алгоритм поиска последней даты установки цен:

  1. Выбираем все записи регистра сведений ЦеныНоменклатуры по заданному виду цены в отдельную таблицу.
  2. Соединяем таблицу документов с историей цен по условию: номенклатура и характеристика совпадают, а дата установки цены меньше либо равна дате проведения документа (Цены.Период <= Документы.РегистраторДата).
  3. Группируем результат по регистратору и позиции номенклатуры, вычисляя МАКСИМУМ(Цены.Период). Это значение дает точную дату последней смены цены перед документом.
  4. Повторно соединяем полученный максимум с регистром цен для извлечения самого числового значения стоимости.

Рассмотрим реализацию этого механизма с использованием регистра накопления ТоварыОрганизаций:


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

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

Шаг 3. Расчет стоимостной оценки движений и остатка на начало периода

Теперь объединим оценку остатка на начало выбранного периода с рассчитанными суммами документов внутри периода. Для начального остатка срез последних на дату &НачПер работает штатно, так как дата является константой.

Посмотрим на завершающий запрос пакета:


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

ОБЪЕДИНИТЬ ВСЕ

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

Шаг 4. Учет документов изменения цен (переоценка остатка)

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

Сумма переоценки = (НоваяЦена - СтараяЦена) * ТекущийОстаток

Для вычисления этой дельты в запросе потребуется:

  1. Определить для каждого документа изменения цены предыдущую действующую цену. Это делается соединением таблицы цен самой с собой по условию ЦеныДо.Период < ЦеныНовые.Период с взятием максимума по ЦеныДо.Период.
  2. Рассчитать остаток товара на дату документа изменения цен путем суммирования всех оборотов от начала ведения учета до даты переоценки.
  3. Добавить строки переоценки через ОБЪЕДИНИТЬ ВСЕ к общей таблице движений.

Рассмотрим фрагмент расчета дельты цен:


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

Архитектурные ограничения и альтернативные подходы

Проанализируем производительность описанного решения. Построение накопительных остатков на произвольные даты и неэквивалентные соединения по датам (ДатаДокумента >= ДатаЦены) приводят к декартовому произведению строк. При увеличении количества документов за период запрос начинает выполняться критически медленно.

Если на больших объемах данных запрос перегружает СУБД, рекомендуем использовать следующие проверенные альтернативы:

  1. Связи наборов данных в СКД: в Системе компоновки данных можно создать два набора данных — «Документы» и «Цены». Во вкладке «Связи наборов данных» можно связать их по номенклатуре и передать поле Регистратор.Дата в качестве параметра периода виртуальной таблицы СрезПоследних. СКД выполнит срез цен для каждой строки документа оптимизированным способом.
  2. Расчет переоценки на встроенном языке (в один проход): одним простым запросом выбираются начальные остатки, вторым — хронологический поток документов движений и переоценок. Затем в цикле на встроенном языке поддерживается актуальный количественный остаток склада и текущая цена каждой позиции. При наступлении события переоценки сумма дельты вычисляется за $O(1)$, что обеспечивает линейную сложность $O(N)$ и выполняется в разы быстрее многоэтапного тяжелого SQL-запроса.
← На главную