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

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

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

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

Шаг 1. Выбор правильного источника данных: Документы или Регистры?

Первый вопрос, который мы должны решить: откуда брать данные? В исходной задаче рассматривался вариант выборки из документа ЗаказПокупателю. Давайте разберем, почему это может быть методически неверно.

Почему документ "Заказ покупателя" не подходит для анализа продаж?

Документ заказа отражает намерение клиента купить товар. Если вам нужно детально проанализировать оплату и отгрузку по заказам, лучше использовать специализированные отчеты. Однако:

  1. Заказ может быть отменен.
  2. Заказ может быть оплачен, но не отгружен.
  3. Заказ может быть отгружен частично.
  4. Сумма отгрузки может отличаться от суммы заказа (замены, скидки в момент реализации), поэтому прибыль по заказам клиентов часто оценивают без закрытия месяца.

Для получения реальной картины продаж (включая аномальные и пиковые продажи) необходимо обращаться к регистрам накопления. Обычно это регистр Продажи (в конфигурациях вроде "Управление торговлей") или регистр Закупки.

Регистры накопления хранят уже «свернутые» и подтвержденные движения, что позволяет проводить полноценный анализ закупок, продаж и остатков товара.

Шаг 2. Работа с датами: Параметры против Функций запроса

Нам необходимо получить данные за "Текущий год". В языке 1С есть два основных способа задать этот период. Давайте рассмотрим оба.

Способ 1. Передача дат через параметры (Рекомендуемый)

В этом случае мы вычисляем даты начала и окончания года непосредственно в коде встроенного языка 1С, используя функции НачалоГода() и КонецГода(), и передаем их в запрос как параметры.


// Вычисляем даты в коде 1С
ДатаНачала = НачалоГода(ТекущаяДата());
ДатаОкончания = КонецГода(ТекущаяДата());

// Устанавливаем параметры запроса
Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", ДатаОкончания);

Этот способ предпочтителен, так как он делает текст запроса более универсальным и позволяет СУБД (SQL Server) кэшировать план выполнения запроса.

Способ 2. Использование функций языка запросов

Язык запросов 1С также имеет свои функции для работы с датами: НАЧАЛОПЕРИОДА и КОНЕЦПЕРИОДА. Можно передать в запрос только текущую дату, а границы периода вычислить внутри.


ГДЕ
    Документ.Дата МЕЖДУ НАЧАЛОПЕРИОДА(&ТекущаяДата, ГОД) И КОНЕЦПЕРИОДА(&ТекущаяДата, ГОД)

Однако, как мы увидим далее, при работе с виртуальными таблицами регистров первый способ (передача готовых границ периода) является стандартом.

Шаг 3. Использование виртуальных таблиц и их параметров

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

Неправильный подход (Фильтрация через ГДЕ):

Если мы напишем запрос следующим образом:


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

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

Правильный подход (Параметры виртуальной таблицы):

У виртуальной таблицы Обороты есть параметры. Первый и второй параметры — это как раз границы периода. Если передать даты туда, SQL-сервер сразу рассчитает итоги только за нужный промежуток времени.


ВЫБРАТЬ
    ПродажиОбороты.СуммаОборот
ИЗ
    РегистрНакопления.Продажи.Обороты(&ДатаНачала, &ДатаОкончания, , Контрагент = &Клиент) КАК ПродажиОбороты

Обратите внимание: отбор по Контрагенту мы также помещаем в параметры виртуальной таблицы (четвертый параметр), а не в секцию ГДЕ.

Шаг 4. Ошибка "Декартово произведение"

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


ИЗ
    РегистрНакопления.Продажи.Обороты КАК ПродажиОбороты,
    РегистрНакопления.Продажи КАК Продажи

Перечисление таблиц через запятую без явного соединения (ЛЕВОЕ СОЕДИНЕНИЕ и т.д.) создает Декартово произведение (Cross Join). Каждая строка первой таблицы соединяется с каждой строкой второй таблицы. Если у вас 100 записей оборотов и 1000 записей движений, результат вернет 100 000 строк. Сумма, полученная таким запросом, будет завышена в тысячи раз и абсолютно неверна. Всегда следите за тем, чтобы таблицы были соединены по ключевым полям, или используйте только одну таблицу, если вторая не нужна.

Шаг 5. Получение итоговой суммы: Цикл vs Агрегатные функции

Часто новички пишут код так: выбирают запросом детальные записи, а потом в цикле Пока Выборка.Следующий() складывают суммы в переменную. Это работает, но это медленно и неоптимально.

Виртуальная таблица .Обороты автоматически группирует данные и суммирует ресурсы. Поле СуммаОборот — это уже готовая сумма за период. Нам не нужно использовать циклы для суммирования. Нам достаточно выгрузить результат или выбрать первую строку выборки, если мы ожидаем одну цифру.

Итоговое решение

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

Мы будем использовать:

  1. Встроенные функции НачалоГода и КонецГода для определения границ.
  2. Виртуальную таблицу РегистрНакопления.Продажи.Обороты.
  3. Передачу периода и отбора по клиенту в параметры виртуальной таблицы.

// 1. Подготовка параметров
ДатаНачала = НачалоГода(ТекущаяДата());
ДатаОкончания = КонецГода(ТекущаяДата()); // Время будет 23:59:59
СсылкаНаКлиента = Объект.Клиент; // Или другой источник ссылки на справочник Контрагенты

// 2. Создание запроса
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    ЕСТЬNULL(ПродажиОбороты.СуммаОборот, 0) КАК СуммаЗакупокЗаГод
    |ИЗ
    |    РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , Контрагент = &Контрагент) КАК ПродажиОбороты";

// 3. Установка параметров
Запрос.УстановитьПараметр("НачалоПериода", ДатаНачала);
Запрос.УстановитьПараметр("КонецПериода", ДатаОкончания);
Запрос.УстановитьПараметр("Контрагент", СсылкаНаКлиента);

// 4. Выполнение и получение результата
Результат = Запрос.Выполнить();
СуммаЗакупокЗаГод = 0;

Если НЕ Результат.Пустой() Тогда
    Выборка = Результат.Выбрать();
    Выборка.Следующий();
    СуммаЗакупокЗаГод = Выборка.СуммаЗакупокЗаГод;
КонецЕсли;

// Теперь переменная СуммаЗакупокЗаГод содержит нужное значение
Сообщить("Сумма закупок за год: " + СуммаЗакупокЗаГод);

Важные нюансы

Функция ЕСТЬNULL (ISNULL)

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

Граница периода

Функция КонецГода(ТекущаяДата()) устанавливает время на последнюю секунду года. При использовании параметров виртуальной таблицы регистров накопления, документы, проведенные ровно в эту секунду, попадают в выборку. Это стандартное поведение, и оно обычно корректно для получения оборотов.

Используя этот подход, вы гарантируете высокую скорость работы отчета и правильность данных управленческого учета.

← На главную