Отчеты и аналитические выборки
Для собственных отчетов используйте ORM-таблицы модуля sale. Они позволяют отфильтровать данные, выбрать только нужные поля и рассчитать агрегаты в базе данных без загрузки объектов заказов.
ORM-выборки подходят только для чтения. Изменяйте заказ, корзину, оплаты и отгрузки через объектную модель. Она проверяет данные, сохраняет связанные объекты, запускает события и записывает историю изменений.
Готовые отчеты в административном разделе используйте для штатных показателей и ручного анализа. ORM нужен, когда показатель нужно рассчитать для собственного интерфейса, фоновой задачи или интеграции, а состав полей, фильтры и группировки задает приложение. Собственный отчет читает поля профильных таблиц модуля и может отличаться от административного отчета составом и алгоритмом показателей.
Подготовить отчет
Подготовка включает выбор ORM-таблицы и параметров запроса. Источник определяет доступные поля, а параметры ограничивают выборку нужным сайтом, периодом, валютой и состояниями.
Выбрать источник данных
Источник зависит от показателя, который нужен отчету.
|
Источник |
Что нужно |
Основные поля для отчетов |
|
|
Заказы и общие показатели заказа |
|
|
|
Товары в активных заказах и брошенных корзинах |
|
|
|
Отдельные оплаты заказа |
|
|
|
Отдельные отгрузки заказа |
|
|
|
Сохраненные показатели покупателя по сайту и валюте |
|
|
|
Общие поля архивных заказов и позиции их корзин |
|
Заказ может содержать несколько оплат и отгрузок. Поэтому поле OrderTable.PAYED показывает состояние заказа целиком, а поле PaymentTable.PAID — состояние конкретной оплаты. Аналогично поле OrderTable.DEDUCTED относится к заказу, а поле ShipmentTable.DEDUCTED — к отдельной отгрузке.
Задать параметры выборки
Перед запросом определите параметры отчета:
-
период в одной часовой зоне,
-
идентификатор сайта
LID, -
валюту
CURRENCYдля денежных агрегатов, -
состояния, которые нужно включить, например оплаченные или неотмененные заказы,
-
максимальное число строк и способ перехода к следующей части данных.
Используйте полуоткрытый интервал дат: начало включается в выборку, конец не включается. Например, отчет за июль должен использовать границы 2026-07-01 00:00:00 и 2026-08-01 00:00:00. Такой фильтр не теряет записи в последнюю секунду месяца и не создает пересечения между соседними периодами.
if (!\Bitrix\Main\Loader::includeModule('sale'))
{
throw new \RuntimeException('Модуль sale не установлен');
}
$dateFrom = \Bitrix\Main\Type\DateTime::createFromPhp(
new \DateTime('2026-07-01 00:00:00')
);
$dateTo = \Bitrix\Main\Type\DateTime::createFromPhp(
new \DateTime('2026-08-01 00:00:00')
);
$siteId = 's1';
$currency = 'RUB';
$userId = 123; // Идентификатор покупателя
Общие переменные имеют следующие типы и форматы:
-
$dateFromи$dateTo— объектыBitrix\Main\Type\DateTime, -
$siteId— строковый идентификатор сайта, -
$currency— трехбуквенный код валюты, -
$userId— положительный идентификатор покупателя.
В рабочем коде формируйте даты в часовой зоне проекта. Не передавайте в фильтр непроверенные строки из HTTP-запроса.
Получить данные для отчета
Профильные ORM-таблицы позволяют получить заказы и связанные данные без загрузки объектной модели. Выберите сценарий по показателю, который должен содержать отчет.
Получить заказы за период
Для списка заказов достаточно OrderTable::getList(). Выберите только поля, которые нужны в строке отчета, и ограничьте результат.
$orders = \Bitrix\Sale\Internals\OrderTable::getList([
'select' => [
'ID',
'ACCOUNT_NUMBER',
'DATE_INSERT',
'USER_ID',
'PRICE',
'CURRENCY',
'PAYED',
'SUM_PAID',
'DEDUCTED',
'STATUS_ID',
'CANCELED',
],
'filter' => [
'=LID' => $siteId,
'=CURRENCY' => $currency,
'>=DATE_INSERT' => $dateFrom,
'<DATE_INSERT' => $dateTo,
],
'order' => ['ID' => 'ASC'],
'limit' => 100,
]);
$orderRows = [];
while ($order = $orders->fetch())
{
$orderRows[] = $order;
}
Результат содержит не более 100 заказов. Запрос не загружает корзину, оплаты и отгрузки как PHP-объекты.
Для большого периода обрабатывайте строки частями по возрастанию ID. После каждой части запоминайте последний идентификатор и добавляйте фильтр >ID в следующий запрос. Такой переход не требует читать и пропускать все предыдущие строки, как при использовании offset — количества строк, которые нужно пропустить перед началом результата.
$lastOrderId = 0;
$batchSize = 500;
do
{
$batch = \Bitrix\Sale\Internals\OrderTable::getList([
'select' => ['ID', 'PRICE', 'CURRENCY', 'PAYED', 'SUM_PAID'],
'filter' => [
'>ID' => $lastOrderId,
'=LID' => $siteId,
'=CURRENCY' => $currency,
'>=DATE_INSERT' => $dateFrom,
'<DATE_INSERT' => $dateTo,
],
'order' => ['ID' => 'ASC'],
'limit' => $batchSize,
])->fetchAll();
foreach ($batch as $order)
{
$lastOrderId = (int)$order['ID'];
// Добавьте строку в отчет или передайте ее в пакетную обработку
}
}
while (count($batch) === $batchSize);
Если отчет должен возобновляться после сбоя, сохраняйте $lastOrderId в контрольной записи фоновой задачи. Обновляйте контрольную запись только после успешной обработки всей части.
Посчитать оплаченные заказы
Агрегаты рассчитывайте в базе данных через ExpressionField.
Пример. Запрос считает полностью оплаченные и неотмененные заказы, их полную стоимость и фактически оплаченную сумму.
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Sale\Internals\OrderTable;
$paidSummary = OrderTable::getList([
'select' => ['ORDER_COUNT', 'ORDER_TOTAL', 'PAID_TOTAL'],
'filter' => [
'=LID' => $siteId,
'=CURRENCY' => $currency,
'=PAYED' => 'Y',
'=CANCELED' => 'N',
'>=DATE_INSERT' => $dateFrom,
'<DATE_INSERT' => $dateTo,
],
'runtime' => [
new ExpressionField('ORDER_COUNT', 'COUNT(*)'),
new ExpressionField('ORDER_TOTAL', 'SUM(%s)', ['PRICE']),
new ExpressionField('PAID_TOTAL', 'SUM(%s)', ['SUM_PAID']),
],
])->fetch();
$orderCount = (int)($paidSummary['ORDER_COUNT'] ?? 0);
$orderTotal = (float)($paidSummary['ORDER_TOTAL'] ?? 0);
$paidTotal = (float)($paidSummary['PAID_TOTAL'] ?? 0);
Поля ORDER_TOTAL и PAID_TOTAL имеют смысл только внутри одной валюты. Для отчета по нескольким валютам сгруппируйте результат по CURRENCY или выполните отдельный запрос для каждой валюты. Не складывайте денежные значения без пересчета по явно выбранному курсу.
Найти заказы с неоплаченными платежами
Поле PaymentTable.PAID позволяет найти неоплаченные оплаты. Связь ORDER дает доступ к полям заказа без вызова Order::load().
$payments = \Bitrix\Sale\Internals\PaymentTable::getList([
'select' => [
'ID',
'ORDER_ID',
'ORDER_ACCOUNT_NUMBER' => 'ORDER.ACCOUNT_NUMBER',
'PAY_SYSTEM_NAME',
'SUM',
'CURRENCY',
'DATE_BILL',
'DATE_PAY_BEFORE',
],
'filter' => [
'=PAID' => 'N',
'>SUM' => 0,
'=ORDER.LID' => $siteId,
'=ORDER.CANCELED' => 'N',
'>=ORDER.DATE_INSERT' => $dateFrom,
'<ORDER.DATE_INSERT' => $dateTo,
],
'order' => ['ORDER_ID' => 'ASC', 'ID' => 'ASC'],
'limit' => 500,
]);
$unpaidPayments = $payments->fetchAll();
Результат содержит строки оплат, а не уникальные заказы. Если у заказа две неоплаченные оплаты, он появится дважды. Группируйте по ORDER_ID, только если отчету нужна одна строка на заказ.
Не заменяйте фильтр PaymentTable.PAID фильтром OrderTable.PAYED, если отчет строится по отдельным оплатам. Заказ может быть частично оплачен несколькими способами.
Построить выборку по отгрузкам
Класс ShipmentTable хранит обычные и системные отгрузки. Системная отгрузка нужна объектной модели для товаров, которые еще не распределены по обычным отгрузкам. Исключайте системную отгрузку из отчета по доставке с помощью фильтра '=SYSTEM' => 'N'.
$shipments = \Bitrix\Sale\Internals\ShipmentTable::getList([
'select' => [
'ID',
'ORDER_ID',
'ORDER_ACCOUNT_NUMBER' => 'ORDER.ACCOUNT_NUMBER',
'STATUS_ID',
'ALLOW_DELIVERY',
'DEDUCTED',
'CANCELED',
'DELIVERY_ID',
'DELIVERY_NAME',
'PRICE_DELIVERY',
'CURRENCY',
'TRACKING_NUMBER',
],
'filter' => [
'=SYSTEM' => 'N',
'=CANCELED' => 'N',
'=ORDER.LID' => $siteId,
'>=ORDER.DATE_INSERT' => $dateFrom,
'<ORDER.DATE_INSERT' => $dateTo,
],
'order' => ['ORDER_ID' => 'ASC', 'ID' => 'ASC'],
'limit' => 500,
]);
$shipmentRows = $shipments->fetchAll();
Поле ALLOW_DELIVERY показывает разрешение доставки, а DEDUCTED — состояние отгрузки товаров. Не считайте разрешенную, но еще не отгруженную запись завершенной доставкой. Для отчета по выполненным отгрузкам добавьте фильтр '=DEDUCTED' => 'Y'.
Получить показатели по товарам
Класс BasketTable позволяет сгруппировать позиции активных заказов по товару.
Пример. База данных считает количество заказанных единиц и сумму позиций неотмененных заказов.
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Sale\Internals\BasketTable;
$productSummary = BasketTable::getList([
'select' => [
'PRODUCT_ID',
'NAME',
'CURRENCY',
'QUANTITY_TOTAL',
'ROW_TOTAL',
],
'filter' => [
'>ORDER_ID' => 0,
'=CURRENCY' => $currency,
'=ORDER.LID' => $siteId,
'=ORDER.CANCELED' => 'N',
'>=ORDER.DATE_INSERT' => $dateFrom,
'<ORDER.DATE_INSERT' => $dateTo,
],
'runtime' => [
new ExpressionField('QUANTITY_TOTAL', 'SUM(%s)', ['QUANTITY']),
new ExpressionField(
'ROW_TOTAL',
'SUM(%s * %s)',
['PRICE', 'QUANTITY']
),
],
'group' => ['PRODUCT_ID', 'NAME', 'CURRENCY'],
'order' => ['QUANTITY_TOTAL' => 'DESC'],
'limit' => 20,
]);
$orderedProducts = $productSummary->fetchAll();
Фильтр '>ORDER_ID' => 0 исключает позиции, которые еще не привязаны к заказу. Сумма ROW_TOTAL использует цену, сохраненную в позиции корзины, и не запрашивает текущую цену товара из модуля catalog.
Название товара может изменяться между заказами. Если отчет должен объединять строки независимо от сохраненного названия, группируйте только по PRODUCT_ID и CURRENCY, а название получайте отдельно пакетным запросом к каталогу.
Получить статистику покупателя
Класс BuyerStatistic читает сохраненные показатели для комбинации пользователя, сайта и валюты. Такой запрос подходит для рейтинга покупателей и карточки клиента.
$buyerStatistic = \Bitrix\Sale\BuyerStatistic::getList([
'select' => [
'USER_ID',
'LID',
'CURRENCY',
'LAST_ORDER_DATE',
'SUM_PAID',
'COUNT_FULL_PAID_ORDER',
'COUNT_PART_PAID_ORDER',
],
'filter' => [
'=USER_ID' => $userId,
'=LID' => $siteId,
'=CURRENCY' => $currency,
],
'limit' => 1,
])->fetch();
Метод BuyerStatistic::calculate() пересчитывает строку по активным и архивным заказам. Не запускайте пересчет при каждом чтении отчета. Используйте его в контролируемой служебной задаче, импорте или восстановлении статистики. После вызова проверяйте ошибки и предупреждения результата.
Подробный сценарий пересчета и обработки результата есть в статье Покупатели и внутренние счета.
Учесть архив заказов
После архивирования заказ исчезает из OrderTable, а его позиции — из BasketTable. Поэтому обычные запросы к активным таблицам не дают полный результат за период, в который попали архивные заказы.
Метод Bitrix\Sale\Archive\Manager::getList() читает общие поля архивного заказа. Метод Manager::getBasketList() читает сохраненные позиции его корзины. Архивная таблица заказа содержит дату, покупателя, стоимость, оплаченную сумму, валюту, статус и признаки оплаты, отгрузки и отмены, но не повторяет все связи активной объектной модели.
Для полного отчета объедините два источника:
-
Получите активные заказы через
OrderTable. -
Получите архивные заказы через
Archive\Managerс теми же границами периода, сайта и валюты. -
Приведите результаты к одной структуре.
-
Объедините строки или агрегаты в коде отчета.
$archivedOrders = \Bitrix\Sale\Archive\Manager::getList([
'select' => [
'ID',
'ORDER_ID',
'ACCOUNT_NUMBER',
'DATE_INSERT',
'USER_ID',
'PRICE',
'SUM_PAID',
'CURRENCY',
'PAYED',
'DEDUCTED',
'STATUS_ID',
'CANCELED',
],
'filter' => [
'=LID' => $siteId,
'=CURRENCY' => $currency,
'>=DATE_INSERT' => $dateFrom,
'<DATE_INSERT' => $dateTo,
],
'order' => ['ID' => 'ASC'],
'limit' => 500,
]);
$archivedOrderRows = $archivedOrders->fetchAll();
Метод Manager::getList() возвращает только первую часть архива. Для большого результата повторяйте запрос с фильтром >ID по последнему первичному идентификатору архивной записи и сохраняйте контрольную точку после успешной обработки части.
Запросы к PaymentTable и ShipmentTable не заменяют чтение архива. Если отчету нужны подробности оплат или отгрузок архивного заказа, сначала проверьте, доступны ли нужные данные в сохраненном архивном представлении. Не подставляйте отсутствующие значения из текущих активных таблиц.
Оптимизировать и проверить отчет
После получения данных выберите способ обработки, ограничьте нагрузку на базу данных и проверьте результат. Эти действия помогают избежать лишних запросов, завышенных агрегатов и публикации неполного отчета.
Выбрать между ORM и объектом заказа
Используйте ORM, если отчету нужны сохраненные поля, фильтрация, сортировка, группировка или агрегаты. Загружайте Bitrix\Sale\Order, только если сценарию требуется поведение объектной модели.
|
Задача |
Подход |
|
Показать список заказов, оплат или отгрузок |
ORM-таблица |
|
Посчитать количество, сумму или распределение по состояниям |
ORM и |
|
Получить несколько связанных полей без изменения данных |
ORM-связь и явный |
|
Рассчитать скидки, изменить состояние или сохранить заказ |
Объект |
|
Выполнить логику провайдера товара или проверить объект перед сохранением |
Объектная модель |
Если без объекта нельзя выполнить логику обработки заказа, обрабатывайте ограниченные части данных. Отдельно измеряйте время выполнения и число запросов. Не вызывайте Order::load() для каждой строки ORM-результата: такой цикл создает дополнительные запросы на заказ и связанные коллекции.
Как не перегрузить базу данных
Соблюдайте правила для регулярных и больших отчетов:
-
выбирайте только нужные поля вместо
select => ['*'], -
ограничивайте запрос сайтом, периодом, валютой и применимыми состояниями,
-
рассчитывайте
COUNT,SUMи группировки в базе данных, -
обрабатывайте большие наборы частями с устойчивой сортировкой,
-
получайте связанные данные пакетными запросами, если одна ORM-связь не решает задачу,
-
не загружайте объект заказа в цикле только ради чтения сохраненных полей,
-
не смешивайте активные и архивные данные без явного правила объединения,
-
запускайте тяжелые отчеты в фоновой задаче и сохраняйте готовый результат, если одни и те же показатели запрашиваются часто.
Один заказ может иметь несколько корзинных позиций, оплат и отгрузок. Если присоединить все три связи к OrderTable в одном запросе, строки перемножатся. Например, две позиции, две оплаты и две отгрузки могут дать восемь строк одного заказа и завысить агрегаты. Считайте показатели в отдельных сгруппированных запросах, затем объединяйте результаты по ORDER_ID.
Как проверить результат и учесть ограничения
Перед публикацией отчета проверьте следующие условия:
-
пустой результат отличается от ошибки выполнения запроса,
-
начало и конец периода используют одну часовую зону,
-
денежный агрегат не смешивает валюты,
-
значения
PAYEDзаказа иPAIDоплаты используются по назначению, -
системные отгрузки исключены из статистики доставки,
-
отмененные заказы и отгрузки включены или исключены осознанно,
-
архивные заказы учтены, если период пересекается с архивом,
-
код, который показывает отчет, проверяет права пользователя и не раскрывает чужие заказы,
-
выборочная сверка нескольких строк с карточками заказов дает те же суммы и состояния.
Метод getList() возвращает результат без строк, если данные не соответствуют фильтру. Ошибка в поле, фильтре или выполнении запроса приводит к исключению. Перехватывайте его на границе фоновой задачи или контроллера, записывайте ошибку в журнал и не публикуйте частичный отчет как успешный.
try
{
$rows = \Bitrix\Sale\Internals\OrderTable::getList([
'select' => ['ID', 'PRICE', 'CURRENCY'],
'filter' => [
'=LID' => $siteId,
'=CURRENCY' => $currency,
'>=DATE_INSERT' => $dateFrom,
'<DATE_INSERT' => $dateTo,
],
'limit' => 100,
])->fetchAll();
}
catch (\Throwable $exception)
{
throw new \RuntimeException(
'Не удалось получить данные отчета',
0,
$exception
);
}
if ($rows === [])
{
// За выбранный период данных нет
}
Пустой массив означает штатное отсутствие данных. Исключение означает, что отчет построить не удалось.
Если во время подготовки отчета нужно исправить данные, завершите чтение и выполните отдельный сценарий через объектную модель с проверкой результата save(). ORM не запускает жизненный цикл заказа и не предназначен для изменения отчетных строк.
Подробнее о синтаксисе фильтров, связей, группировок и вычисляемых полей читайте в статье Выборка данных в ORM.