При работе сайтов в режиме b2b объемы заказов обычно имеют внушительные размеры - количество единиц номенклатуры в заказе может превышать десятки единиц.
Для таких вариантов достаточно распространенным решением является генерация pdf-файла с товарной накладной.
В данном посте рассмотрим работу с модулем tcpdf на примере генерации накладной для заказа пользователя.
Библиотека tcpdf была выбрана, т.к в ней нет проблем при работе с битриксом, как это встречается у других библиотек (речь об mbstring.func_overload).
Итак, для начала качаем себе актуальную версию библиотеки. Распаковываем ее, например в папку /bitrix/php_interface/include/tcpdf. Теперь можно приступать к генерации файла.
Для начала можно познакомиться с примерами работы библиотеки TCPDF на официальном сайте. Также очень много есть неофициальных сайтов с примерами. Мне помог сайт: TCPDF практические заметки (документация) на русском.
Далее приступаем непосредственно к созданию файла. Приведу целиком весь код, с комментариями по тексту. В коде использован шрифт Arial, которого не было в поставке. Я нашел данный файл в сети уже сгенерированным и добавил к списку шрифтов.
define("STOP_STATISTICS", true); define('NO_AGENT_CHECK', true); use Bitrix\Sale; use Bitrix\Sale\Order; require($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php'); require_once($_SERVER['DOCUMENT_ROOT'] . "/bitrix/php_interface/include/tcpdf/tcpdf.php"); // для модификации шапки и подвала сайта, необходимо переписать соответствующие функции. // для этого, создадим свой класс унаследовав его от TCPDF: class MdfPDF extends \TCPDF { //Page header public function Header() { // Logo $headerdata = $this->getHeaderData(); $this->Image($headerdata['logo'], 20, 10, $headerdata['logo_width'], '', 'JPG', '', 'T', false, 300, '', false, false, 0, false, false, false); // Set font $this->SetFont('arial', '', 14); // Title /* // если нужно в верхнем колонтитуле вывести заголовок или какой-то текст, эти строки нужно раскомментировать // и состыковать $this->Cell(0, 15, $headerdata['title'], 0, false, 'C', 0, '', 0, false, 'M', 'M'); $this->Cell(0, 15, $headerdata['string'], 0, false, 'C', 0, '', 0, false, 'M', 'M'); */ $this->SetTextColor(85, 85, 85); // цвет шрифта $html = "<br /><hr />"; // добавим линию, отделающую колонтитул от текста $this->writeHTML($html, true, false, true, false, ''); } // Page footer public function Footer() { // Position at 15 mm from bottom $this->SetY(-15); // Set font $this->SetFont('arial', 'I', 8); // Page number $this->SetTextColor(85, 85, 85); $html = "<hr />"; $this->writeHTML($html, true, false, true, false, ''); $this->SetTextColor(0, 0, 0); $this->Cell(0, 10, 'Страница ' . $this->getAliasNumPage() . '/' . $this->getAliasNbPages(), 0, false, 'C', 0, '', 0, false, 'T', 'M'); } } class pdfit { var $pdf; var $orderID; var $siteInfo; var $siteLogo; var $order; var $arUserData; function __construct($orderID, $siteLogo = false) { \Bitrix\Main\Loader::includeModule('sale'); $this->orderID = intval($orderID); // идентификатор заказа для обработки $this->siteLogo = $siteLogo; // пусть к файлу с логотипом сайта } function process() { if ($this->orderID > 0) { $this->getOrder(); if($this->checkOrderAccess()){ $this->getSiteInfo(); // определяем файл, в котором будем хранить сгенерированные накладные, если нужно // к стати, при хранении файлов, стоит также написать обработчик, который эти файлы будет удалять // при удалении заказов $resultfile = $_SERVER['DOCUMENT_ROOT'] .'/upload/invoices/'. $this->siteInfo['SERVER_NAME'] . '_invoice_' . $this->orderID . '.pdf'; if(file_exists($resultfile)){ // если файл уже был создан - просто выводим его header('Content-type: application/pdf'); readfile($resultfile); } else { // иначе - проходим процедуру создания $this->getUserData(); $this->initPdf(); $this->getInvoiceReceiver(); $this->GetHead(); $this->GetOrderItems(); $this->pdf->Output($resultfile, 'I'); // выводим на экран $this->pdf->Output($resultfile, 'F'); // сохраняем файл по указанному пути } } else { ShowError('Возникла ошибка генерации файла с накладной! Свяжитесь с администрацией сайта.'); } } } function getOrder() { $this->order = Sale\Order::load($this->orderID); } function checkOrderAccess(){ global $USER; global $APPLICATION; if(!$this->order) return false; if($USER->IsAdmin()) return true; if(!$USER->IsAuthorized()) return false; if($USER->GetID()==$this->order->getUserId()){ return true; } if($APPLICATION->GetUserRight("sale") >= "W") return true; // оставляем менеджерам возможность просматривать накладные else { $APPLICATION->AuthForm("Доступ к заказу запрещён."); return false; } } function getSiteInfo($siteId = SITE_ID) { if (empty($siteId)) { $siteId = "s1"; } $arSite = false; $obCache = new \CPHPCache(); if ($obCache->InitCache(36000, 'site_' . $siteId, '/')) { $arSite = $obCache->GetVars(); } elseif ($obCache->StartDataCache()) { $arSite = \CSite::GetByID($siteId)->Fetch(); $obCache->EndDataCache($arSite); } $this->siteInfo = $arSite; } function initPdf() { $this->pdf = new \MdfPDF('P', 'mm', 'A4', true, 'UTF-8', false); $this->pdf->SetMargins(20, 20, 10); $this->pdf->SetAutoPageBreak(true, 20); if (is_array($this->siteInfo) && strlen($this->siteInfo['NAME']) > 0) { $this->pdf->SetAuthor($this->siteInfo['NAME']); } $this->setTitleByOrderID(); } function getInvoiceDate() { $date = $this->order->getDateInsert()->getTimestamp(); return 'от ' . FormatDate("j F Y", $date); } function getUserData() { $user_id = (int)$this->order->getUserId(); $rsUser = CUser::GetByID($user_id); $this->arUserData = $rsUser->Fetch(); } function getInvoiceReceiver() { $propertyCollection = $this->order->getPropertyCollection(); $receiver = $propertyCollection->getPayerName()->getValue(); if (!$receiver && !empty($this->arUserData)) { $resline = []; if (strlen($this->arUserData['WORK_COMPANY']) > 0) { $resline[] = $this->arUserData['WORK_COMPANY']; } if (empty($resline)) { if (strlen($this->arUserData['LAST_NAME']) > 0) { $resline[] = $this->arUserData['LAST_NAME']; } if (strlen($this->arUserData['NAME']) > 0) { $resline[] = $this->arUserData['NAME']; } if (strlen($this->arUserData['SECOND_NAME']) > 0) { $resline[] = $this->arUserData['SECOND_NAME']; } } if (empty($resline)) { $resline[] = $this->arUserData['LOGIN']; } if (!empty($resline)) { return implode(' ', $resline); } } return false; } function getInvoceContacts() { $phone = $this->order->getPropertyCollection()->getPhone(); if ($phone) $phone = $phone->getValue; if (!$phone && !empty($this->arUserData)) { if (strlen($this->arUserData['WORK_PHONE']) > 0) { $phone = $this->arUserData['WORK_PHONE']; } elseif (strlen($this->arUserData['PERSONAL_MOBILE']) > 0) { $phone = $this->arUserData['PERSONAL_MOBILE']; } elseif (strlen($this->arUserData['PERSONAL_PHONE']) > 0) { $phone = $this->arUserData['PERSONAL_PHONE']; } else { $phone = false; } } else $phone = false; $email = $this->order->getPropertyCollection()->getUserEmail(); if ($email) $email = $email->getValue(); if (!$email && !empty($this->arUserData)) { if (strlen($this->arUserData['EMAIL']) > 0) { $email = $this->arUserData['EMAIL']; } else $email = false; } else $email = false; $address = $this->order->getPropertyCollection()->getAddress(); if ($address) $address = $address->getValue(); if (!$address) $address = false; return array( 'phone' => $phone, 'email' => $email, 'address' => $address ); } function setTitleByOrderID() { $title = 'Расходная накладная №' . $this->orderID; $this->pdf->SetTitle($title); $this->pdf->SetHeaderData($this->siteLogo, 30, $title, $this->getInvoiceDate()); } function GetHead() { $this->pdf->AddPage(); $headerdata = $this->pdf->getHeaderData(); $this->pdf->SetFont('arial', '', 12); $this->pdf->Write(0, $headerdata['title'], '', 0, 'C', true, 0, false, false, 0); $this->pdf->Write(0, $headerdata['string'], '', 0, 'C', true, 0, false, false, 0); $this->pdf->SetFont('arial', '', 10); if ($receiver = $this->getInvoiceReceiver()) { $this->pdf->Write(0, 'Покупатель: ' . $receiver, '', 0, 'L', true, 0, false, false, 0); } $receiverContacts = $this->getInvoceContacts(); if ($receiverContacts['phone'] && strlen($receiverContacts['phone']) > 0) { $this->pdf->Write(0, 'Тел: ' . $receiverContacts['phone'], '', 0, 'L', true, 0, false, false, 0); } if ($receiverContacts['email'] && strlen($receiverContacts['email']) > 0) { $this->pdf->Write(0, 'E-mail: ' . $receiverContacts['email'], '', 0, 'L', true, 0, false, false, 0); } if ($receiverContacts['address'] && strlen($receiverContacts['address']) > 0) { $this->pdf->Write(0, 'Адрес: ' . $receiverContacts['address'], '', 0, 'L', true, 0, false, false, 0); } $this->pdf->Write(0, "", '', 0, 'C', true, 0, false, false, 0); } function GetOrderItems() { $this->pdf->SetFillColor(51, 51, 51); $this->pdf->SetTextColor(255); $this->pdf->SetDrawColor(85, 85, 85); $this->pdf->SetLineWidth(0.3); $this->pdf->SetFont('arial', 'B'); $coloumns = array( 0 => ['t' => 'ID', 'w' => '20'], 1 => ['t' => 'Товар', 'w' => '100'], 2 => ['t' => 'Цена', 'w' => '20'], 3 => ['t' => 'Кол-во', 'w' => '17'], 4 => ['t' => 'Сумма', 'w' => '20'], ); foreach ($coloumns as $arColoumn) { $this->pdf->Cell($arColoumn['w'], 7, $arColoumn['t'], 1, 0, 'C', 1); } $this->pdf->Ln(); $this->pdf->SetFillColor(224, 235, 255); $this->pdf->SetTextColor(0); $this->pdf->SetFont('arial', ''); $fill = 0; foreach ($this->order->getBasket()->getBasketItems() as $basketItem) { $this->pdf->Cell($coloumns[0]['w'], 6, $basketItem->getProductId(), 'LR', 0, 'C', $fill); $this->pdf->Cell($coloumns[1]['w'], 6, $basketItem->getField('NAME'), 'LR', 0, 'L', $fill); $this->pdf->Cell($coloumns[2]['w'], 6, CCurrencyLang::CurrencyFormat($basketItem->getPrice(), 'USD', true), 'LR', 0, 'L', $fill); $this->pdf->Cell($coloumns[3]['w'], 6, number_format($basketItem->getQuantity()), 'LR', 0, 'C', $fill); $this->pdf->Cell($coloumns[4]['w'], 6, CCurrencyLang::CurrencyFormat($basketItem->getFinalPrice(), 'USD', true), 'LR', 0, 'L', $fill); $this->pdf->Ln(); $fill = !$fill; } $this->pdf->Cell(array_reduce($coloumns, function (&$res, $item) { return $res + $item['w']; }, 0), 0, '', 'T'); $this->pdf->Ln(); $this->pdf->SetFont('arial', 'U',14); $this->pdf->Write(0, 'Товаров на ' . CCurrencyLang::CurrencyFormat($this->order->getBasket()->getPrice(), 'USD', true), '', 0, 'R', true, 0, false, false, 0); } } $orderID = intval($_GET['order']); if($orderID >0){ $pdf = new pdfit($orderID, $_SERVER['DOCUMENT_ROOT'] . '/upload/sitelogo.jpg'); $pdf->process(); } else { ShowError('Идентификатор заказа не указан!'); }
Таким образом, открывая страницу с данным скриптом, передавая в параметр order идентификатор заказ - получим накладную по выбранному заказу.
Рассмотрим теперь, что делает каждая из функций:
getOrder - загружает заказ по идентификатору;
checkOrderAccess
- проверяет права пользователя на доступ к заказу. имеет смысл только в том случае, если и сама папка, где хранятся накладные, также закрыта от доступа;getSiteInfo
- получает информацию о текущем сайте из базы данных;initPdf
- инициирует объект pdf;getInvoiceDate
- выводит отформатированную строку с датой заказа;getUserData
- выводит информацию о пользователе, сделавшем заказ;getInvoiceReceiver
- выводит наименование получателя (или значение, указанное в свойствах заказа, если заполнено, или информацию из профиля пользователя, сделавшего заказ);getInvoceContacts
- контактная информация по заказу;setTitleByOrderID
- устанавливаем заголовок для файла;GetHead
- формируем шапку накладной;GetOrderItems
- самая ценная функция - выводит информацию о товарах.
Разработка сайта
Подайте заявку на разработку сайта на базе готового решения от компании 1С-Битрикс или одного из партнеров компании. Максимально подробно опишите, чему будет посвящен сайт, если это интернет-магазин - что он будет продавать, нужна ли мультиязычность, будут ли разные типы цен (розница, опт, крупный опт), будет ли интеграция с 1С, будет ли выгрузка товаров на различные торговые площадки...
Сопровождение сайта
Вы можете подать заявку на сопровождение вашего сайта на базе 1С-Битрикс. Сопровождение включает в себя: проверка актуальности обновлений сайта, проверка актуальности резервной копии, консультации по сайту. Опишите в заявке, какие еще объемы планируются на сопровождении и на какой срок вы планируете заключить договор на сопровождение - мы подберем подходящий вам бюджет на сопровождение
Работы по сайту
Вы можете подать заявку на выполнение определенного объема работ по сайту. Опишите в заявке объем работ. Это может быть разработка какого-то нового функционала, доработки по имеющемуся функционалу, доработки под требования сео-специалистов. На основании заявки вам будет сформирован бюджет работ, а также названы сроки на выполнение тех или иных работ.