Печатные формы заказа

Печатная форма представляет данные заказа в виде счета, накладной, акта или другого документа для печати. Объектный API модуля sale предоставляет заказ и связанные коллекции. PHP-шаблон выводит текущие данные заказа. Если готовый документ нужно хранить, сохраните файл документа и метаданные отдельно.

Чтобы подготовить печатную форму, загрузите актуальный заказ, соберите данные, передайте их в PHP-шаблон и добавьте форму в меню «Печать заказа».

Чем отличается печатная форма от связанных документов

Печатная форма не меняет заказ и при каждом открытии строится по доступным в этот момент данным. Другие документы модуля sale могут иметь собственное состояние и жизненный цикл.

Результат

Как формируется

Где изучить подробнее

Печатная форма заказа

PHP-шаблон получает заказ или подготовленный массив и выводит HTML

Собрать данные заказа, Передать данные в HTML-шаблон

Счет или платежная форма

Обработчик платежной системы использует данные оплаты и настройки способа оплаты

Оплаты и платежные системы

Кассовый чек

Подсистема касс связывает фискальный документ с оплатой или отгрузкой

Кассы и чеки

Документ перевозчика

Служба доставки или транспортная заявка получает данные отгрузки

Доставка и отгрузки, Транспортные заявки

Выгрузка заказа

Механизм обмена передает структурированные данные во внешнюю систему

Обмен, импорт и экспорт заказов

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

Данные печатной формы

Для печатной формы нужны данные заказа и связанных объектов. Сначала определите источники данных, затем подготовьте структуру массива для шаблона.

Источники данных

Когда администратор выбирает форму в меню «Печать заказа», контроллер загружает заказ и подключает выбранный PHP-файл. Перед этим штатный маршрут проверяет сессию, доступ к модулю sale и операцию просмотра для статуса заказа. В шаблоне доступен объект $order типа Bitrix\Sale\Order.

Данные для печатной формы берите из объектов заказа и связанных коллекций:

Данные формы

Объект или коллекция

Роль в сценарии

Номер, дата, статус, покупатель, сумма и валюта

Bitrix\Sale\Order

Основные реквизиты заказа

Товары, количество, цена, скидка и НДС

Bitrix\Sale\Basket

Табличная часть формы

Контактные данные и реквизиты покупателя

Bitrix\Sale\PropertyValueCollection

Значения свойств конкретного заказа

Сумма, статус, платежная система и компания

Bitrix\Sale\PaymentCollection

Данные одной или нескольких оплат

Служба доставки, стоимость, трек-номер и состав

Bitrix\Sale\ShipmentCollection

Данные пользовательских отгрузок

Поиск идентификаторов и отдельных полей заказов

Bitrix\Sale\Internals\OrderTable

Предварительная выборка без формирования документа

Класс OrderTable подходит для поиска и списков. Чтобы получить корзину, свойства, оплаты и отгрузки для одной формы, загрузите объект Order. Общие способы чтения и поиска заказов описаны в статье Изменение и чтение заказа.

Структура данных для шаблона

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

Ключ

Содержимое

Если данных нет

ORDER

Номер, дата, статус, покупатель, сумма и валюта заказа

Печатную форму нельзя сформировать без этого блока

ITEMS

Позиции корзины

Пустой массив

PROPERTIES

Значения свойств покупателя

Пустой массив

PAYMENTS

Оплаты заказа

Пустой массив

SHIPMENTS

Пользовательские отгрузки и их позиции

Пустой массив

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

Создать печатную форму

Создание формы состоит из загрузки заказа, подготовки данных, вывода HTML и подключения шаблона к административному меню.

Загрузить актуальный заказ

Если код запускается не из административного механизма печати, подключите модуль sale и загрузите заказ непосредственно перед подготовкой формы.

use Bitrix\Main\Loader;
use Bitrix\Sale\Order;

if (!Loader::includeModule('sale'))
{
    throw new \RuntimeException('Не удалось подключить модуль sale');
}

$orderId = 123;
if ($orderId <= 0)
{
    throw new \InvalidArgumentException(
        'Идентификатор заказа должен быть положительным числом'
    );
}

$order = Order::load($orderId);

if (!$order)
{
    throw new \RuntimeException("Заказ {$orderId} не найден");
}

Метод Order::load() возвращает объект заказа или null.

Штатный маршрут административной печати проверяет сессию, доступ к модулю и операцию просмотра для статуса заказа. Метод не проверяет право пользователя на просмотр. Эту проверку должен выполнить код, который загружает заказ.

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

Собрать данные заказа

Вынесите функцию подготовки данных в отдельный файл проекта в /local. Она получает уже загруженный заказ, ничего не изменяет и не вызывает Order::save().

<?php

use Bitrix\Sale\Order;

function buildOrderPrintData(Order $order): array
{
    // Соберите основные данные заказа
    $dateInsert = $order->getDateInsert();
    $data = [
        'ORDER' => [
            'ID' => $order->getId(),
            'ACCOUNT_NUMBER' => (string) $order->getField(
                'ACCOUNT_NUMBER'
            ),
            'DATE_INSERT' => $dateInsert
                ? $dateInsert->format('Y-m-d H:i:s')
                : null,
            'STATUS_ID' => (string) $order->getField('STATUS_ID'),
            'USER_ID' => $order->getUserId(),
            'PRICE' => $order->getPrice(),
            'CURRENCY' => $order->getCurrency(),
            'PAID' => $order->isPaid(),
            'CANCELED' => $order->isCanceled(),
        ],
        'ITEMS' => [],
        'PROPERTIES' => [],
        'PAYMENTS' => [],
        'SHIPMENTS' => [],
    ];

    // Добавьте позиции корзины
    foreach ($order->getBasket() as $basketItem)
    {
        $data['ITEMS'][] = [
            'ID' => $basketItem->getId(),
            'PRODUCT_ID' => (int) $basketItem->getField('PRODUCT_ID'),
            'NAME' => (string) $basketItem->getField('NAME'),
            'QUANTITY' => $basketItem->getQuantity(),
            'BASE_PRICE' => $basketItem->getBasePrice(),
            'PRICE' => $basketItem->getPrice(),
            'DISCOUNT_PRICE' => $basketItem->getDiscountPrice(),
            'LINE_TOTAL' => $basketItem->getFinalPrice(),
            'CURRENCY' => (string) $basketItem->getField('CURRENCY'),
            'VAT_RATE' => $basketItem->getVatRate(),
            'VAT_INCLUDED' => $basketItem->isVatInPrice(),
        ];
    }

    // Добавьте свойства заказа
    foreach ($order->getPropertyCollection() as $propertyValue)
    {
        $property = $propertyValue->getProperty();

        $data['PROPERTIES'][] = [
            'ID' => $propertyValue->getPropertyId(),
            'CODE' => (string) ($property['CODE'] ?? ''),
            'NAME' => $propertyValue->getName(),
            'VALUE' => $propertyValue->getValue(),
        ];
    }

    // Добавьте оплаты
    foreach ($order->getPaymentCollection() as $payment)
    {
        $data['PAYMENTS'][] = [
            'ID' => $payment->getId(),
            'PAY_SYSTEM_ID' => $payment->getPaymentSystemId(),
            'PAY_SYSTEM_NAME' => $payment->getPaymentSystemName(),
            'SUM' => $payment->getSum(),
            'PAID' => $payment->isPaid(),
            'COMPANY_ID' => (int) $payment->getField('COMPANY_ID'),
        ];
    }

    // Добавьте пользовательские отгрузки и их позиции
    foreach ($order->getShipmentCollection() as $shipment)
    {
        if ($shipment->isSystem())
        {
            continue;
        }

        $shipmentData = [
            'ID' => $shipment->getId(),
            'DELIVERY_ID' => $shipment->getDeliveryId(),
            'DELIVERY_NAME' => $shipment->getDeliveryName(),
            'PRICE' => $shipment->getPrice(),
            'SHIPPED' => $shipment->isShipped(),
            'TRACKING_NUMBER' => (string) $shipment->getField(
                'TRACKING_NUMBER'
            ),
            'COMPANY_ID' => $shipment->getCompanyId(),
            'ITEMS' => [],
        ];

        foreach ($shipment->getShipmentItemCollection() as $shipmentItem)
        {
            $basketItem = $shipmentItem->getBasketItem();
            if (!$basketItem)
            {
                continue;
            }

            $shipmentData['ITEMS'][] = [
                'BASKET_ID' => $shipmentItem->getBasketId(),
                'NAME' => (string) $basketItem->getField('NAME'),
                'QUANTITY' => $shipmentItem->getQuantity(),
                'PRICE' => $basketItem->getPrice(),
                'CURRENCY' => (string) $basketItem->getField(
                    'CURRENCY'
                ),
            ];
        }

        $data['SHIPMENTS'][] = $shipmentData;
    }

    return $data;
}

Выбрать оплату или отгрузку для документа

Если форма относится к одной оплате или отгрузке, найдите объект в коллекции загруженного заказа. Такой поиск одновременно проверяет связь объекта с заказом.

$paymentId = 456;
$payment = $order
    ->getPaymentCollection()
    ->getItemById($paymentId)
;

if (!$payment)
{
    throw new \RuntimeException(
        sprintf(
            'Оплата %d не найдена в заказе %d',
            $paymentId,
            $order->getId()
        )
    );
}

$shipmentId = 789;
$shipment = $order
    ->getShipmentCollection()
    ->getItemById($shipmentId)
;

if (!$shipment || $shipment->isSystem())
{
    throw new \RuntimeException(
        sprintf(
            'Отгрузка %d не найдена в заказе %d',
            $shipmentId,
            $order->getId()
        )
    );
}

Работа с оплатами и отгрузками подробнее описана в статьях Оплаты и платежные системы и Доставка и отгрузки.

Передать данные в HTML-шаблон

В PHP-шаблоне административной печати доступен объект $order. До подключения шаблона подключите файл с функцией buildOrderPrintData(). Функция преобразует объект заказа в согласованный массив.

Метод getValue() возвращает сохраненное значение, которое не всегда готово для печати. Для местоположения, файла или пользовательского типа преобразуйте значение с учетом его типа до передачи в шаблон. Например, для файла извлеките имя из файловой структуры, для местоположения замените внутренний код на понятное название, а для пользовательского типа используйте правила его форматирования. Форматы значений и структура файлового свойства описаны в статье Свойства заказа.

<?php

use Bitrix\Sale\Order;

// Проверьте входной объект заказа
/** @var Order $order */
if (!$order instanceof Order)
{
    throw new \RuntimeException('Заказ не загружен');
}

// Подготовьте данные для шаблона
$documentData = buildOrderPrintData($order);

// Подготовьте функции безопасного форматирования и вывода
$escape = static fn(mixed $value): string => htmlspecialchars(
    (string) $value,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

$formatMoney = static fn(float $value, string $currency): string =>
    number_format($value, 2, ',', ' ') . ' ' . $currency;

$formatPropertyValue = static function (mixed $value): string
{
    if (!is_array($value))
    {
        return (string) $value;
    }

    return implode(', ', array_map(
        static fn(mixed $item): string => (string) $item,
        $value
    ));
};
?>

<h1>
    Накладная к заказу №
    <?= $escape($documentData['ORDER']['ACCOUNT_NUMBER']) ?>
</h1>

<p>
    Дата заказа:
    <?= $escape($documentData['ORDER']['DATE_INSERT']) ?>
</p>

<table>
    <thead>
        <tr>
            <th>Товар</th>
            <th>Количество</th>
            <th>Сумма</th>
        </tr>
    </thead>
    <tbody>
        <?php foreach ($documentData['ITEMS'] as $item)
        { ?>
            <tr>
                <td><?= $escape($item['NAME']) ?></td>
                <td><?= $escape($item['QUANTITY']) ?></td>
                <td>
                    <?= $escape($formatMoney(
                        $item['LINE_TOTAL'],
                        $item['CURRENCY']
                    )) ?>
                </td>
            </tr>
        <?php } ?>
    </tbody>
</table>

<p>
    Итого:
    <?= $escape($formatMoney(
        $documentData['ORDER']['PRICE'],
        $documentData['ORDER']['CURRENCY']
    )) ?>
</p>

<h2>Данные покупателя</h2>

<dl>
    <?php foreach ($documentData['PROPERTIES'] as $property)
    { ?>
        <dt><?= $escape($property['NAME']) ?></dt>
        <dd>
            <?= $escape($formatPropertyValue($property['VALUE'])) ?>
        </dd>
    <?php } ?>
</dl>

Шаблон отвечает только за представление. Он не загружает связанные объекты заново, не выполняет запросы к внешним сервисам и не сохраняет заказ. Экранируйте названия товаров, свойства и другие текстовые значения непосредственно при выводе. Метод getViewHtml() может вернуть HTML, поэтому не выводите его без проверки и выбранной политики экранирования.

Добавить форму в меню печати

Административный механизм ищет PHP-файлы сначала в /bitrix/admin/reports/, затем в /bitrix/modules/sale/reports/. Чтобы добавить форму в меню «Печать заказа», создайте файл /bitrix/admin/reports/project_waybill.php. В файле используйте код:

<title>Накладная проекта</title>
<?php

require $_SERVER['DOCUMENT_ROOT']
    . '/local/templates/documents/project_waybill.php';

Тег <title> задает название пункта меню. Основную логику храните в /local, а в файле формы оставьте только название и подключение этой логики. Каталог /bitrix/admin/reports/ может отсутствовать в новом окружении, поэтому добавьте создание каталога и копирование файла формы в процесс развертывания проекта.

После развертывания:

  1. Откройте карточку заказа в административном разделе.

  2. Нажмите Печать заказа.

  3. Выберите новый пункт меню, например, Накладная проекта.

  4. Сверьте номер заказа, товары, количество, свойства покупателя и итоговую сумму.

Форма откроется в новом окне. Механизм не сохранит HTML и не добавит запись документа в заказ.

Как обеспечить актуальность данных

Загружайте заказ непосредственно перед формированием формы. Ранее собранный массив не изменится, если другой процесс обновит заказ, оплату или отгрузку.

Не вызывайте Order::save() из шаблона. Печать должна быть операцией чтения и не менять статус, корзину, свойства, оплаты или отгрузки.

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

Продолжить изучение