Как получить данные по продажам контрагентов, если в заказе указана категория?

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

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

Анализ структуры данных и постановка задачи

Рассмотрим ситуацию подробнее. У нас есть документ ВнутреннийЗаказ с реквизитом Заказчик (составной тип: Справочник.Контрагенты, Справочник.Категории). Также имеется вспомогательный справочник КатегорииКонтрагентов, где поле Владелец — это контрагент, а реквизит Значение — это категория. Наша цель — получить отчет, который покажет резервы по заказу и средние продажи за период.

Основная проблема заключается в том, что если в заказе указана категория, запрос должен найти всех контрагентов, принадлежащих этой категории, и просуммировать их продажи из регистра Продажи.Обороты. При этом важно избежать «размножения» строк и некорректного суммирования резервов.

Шаг 1: Формирование временных таблиц с заказами и резервами

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


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

Затем соединим полученные данные с виртуальной таблицей остатков регистра ТоварыВРезервеНаСкладах.Остатки. Обратите внимание на использование индексации по полям связи — это критически важно для производительности при работе с временными таблицами.


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

Шаг 2: Реализация связи через категории для получения оборотов

Теперь перейдем к самому сложному моменту — связке заказа (где может быть категория) с реальными продажами контрагентов. Рассмотрим алгоритм соединения:

  1. Мы берем таблицу Резервы.
  2. Левым соединением подключаем справочник КатегорииКонтрагентов по условию соответствия клиента из резерва и значения категории.
  3. Вторым левым соединением подключаем таблицу Продажи.Обороты. Здесь используется хитрое условие связи: если в КатегорииКонтрагентов найден владелец (контрагент), мы берем его. Если нет (значит, в заказе и так указан контрагент), берем напрямую Резервы.Клиент.

Разберем, как это выглядит в коде запроса:


ВЫБРАТЬ
    Резервы.Клиент,
    Резервы.Канал,
    Резервы.Номенклатура,
    Резервы.ДатаНачалаСрочногоРезерва КАК ДатаНачалаСрочногоРезерва,
    Резервы.ДатаКонцаСрочногоРезерва КАК ДатаКонцаСрочногоРезерва,
    ВЫРАЗИТЬ(Резервы.ПервоначальноеЗначениеРезерва КАК ЧИСЛО(15, 3)) КАК ПервоначальноеЗнач,
    ЕСТЬNULL(Резервы.ТекЗначение, 0) КАК ТекЗначение,
    ВЫРАЗИТЬ(ЕСТЬNULL(ПродажиОбороты.КоличествоОборот / 3 + 0.4999999999, 0) КАК ЧИСЛО(15, 0)) КАК Среднее
ИЗ
    Резервы КАК Резервы
        ЛЕВОЕ СОЕДИНЕНИЕ Справочник.КатегорииКонтрагентов КАК КатегорииКонтрагентов
        ПО Резервы.Клиент = КатегорииКонтрагентов.Значение
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Продажи.Обороты(ДОБАВИТЬКДАТЕ(&ДатаДок, МЕСЯЦ, -3), &ДатаДок, , ) КАК ПродажиОбороты
        ПО Резервы.Номенклатура = ПродажиОбороты.Номенклатура
            И (ЕСТЬNULL(КатегорииКонтрагентов.Владелец, Резервы.Клиент) = ПродажиОбороты.Контрагент)

Функция ЕСТЬNULL(КатегорииКонтрагентов.Владелец, Резервы.Клиент) является ключевой. Она позволяет универсально определить контрагента для соединения с регистром продаж независимо от того, что указано в заказе.

Шаг 3: Решение проблемы дублей через группировку

При использовании левого соединения с категориями возникает побочный эффект — дублирование строк. Если в одну категорию входит 10 контрагентов, то одна строка заказа «размножится» на 10 строк в результате. При этом показатели ПервоначальноеЗнач и ТекЗначение будут продублированы для каждого контрагента, что приведет к неверным итогам в отчете.

Выясним причину: это происходит потому, что мы соединяем один объект (заказ на категорию) с множеством объектов (продажи разных контрагентов). Чтобы исправить это, необходимо выполнить финальную группировку данных. Проанализируем, как схлопнуть результат:


ВЫБРАТЬ
    Резервы.Клиент КАК Клиент,
    Резервы.Номенклатура КАК Номенклатура,
    МАКСИМУМ(Резервы.ПервоначальноеЗначениеРезерва) КАК ПервоначальноеЗнач,
    МАКСИМУМ(Резервы.ТекЗначение) КАК ТекЗначение,
    СУММА(ВЫРАЗИТЬ(ЕСТЬNULL(ПродажиОбороты.КоличествоОборот / 3 + 0.4999999999, 0) КАК ЧИСЛО(15, 0))) КАК СреднееИтого
ИЗ
    Резервы КАК Резервы
    // ... здесь те же соединения, что выше ...
ГРУППИРОВАТЬ ПО
    Резервы.Клиент,
    Резервы.Номенклатура

Важный момент: Для полей остатков (резервов) мы используем функцию МАКСИМУМ (или СРЕДНЕЕ), так как они одинаковы для всех строк одного заказа. А для поля Среднее мы используем СУММА, так как нам нужно сложить продажи всех контрагентов, входящих в категорию.

Советы по оптимизации и использованию СКД

Если вы строите этот отчет в Системе компоновки данных (СКД), можно обойтись без сложной группировки в самом запросе. Рассмотрим настройки ресурсов:

  1. Для поля ПервоначальноеЗнач в поле «Выражение» укажите Максимум(ПервоначальноеЗнач).
  2. Для поля Среднее укажите Сумма(Среднее).
  3. В настройках группировки укажите расчет «По Номенклатуре» и «По Заказу».

Также рекомендуется использовать оператор ВЫРАЗИТЬ(Клиент КАК Справочник.Контрагенты), если вы точно знаете, с каким типом работаете в данный момент. Это поможет оптимизатору базы данных 1С не обращаться к лишним таблицам составного типа (например, к складам или подразделениям), что значительно ускорит выполнение тяжелых отчетов.

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

← На главную