getDirectoryRead(DirectoryList::ROOT); $this->regular = \Zend_Pdf_Font::fontWithPath($root->getAbsolutePath('lib/internal/GnuFreeFont/FreeSerif.ttf')); $this->bold = \Zend_Pdf_Font::fontWithPath($root->getAbsolutePath('lib/internal/GnuFreeFont/FreeSerifBold.ttf')); } public function getPdf(InvoiceInterface $invoice): \Zend_Pdf { $order = $invoice->getOrder(); $store = $this->storeManager->getStore((int)$order->getStoreId()); $websiteId = (int)$store->getWebsiteId(); $locale = (string)$this->scopeConfig->getValue( self::CONFIG_LOCALE, ScopeInterface::SCOPE_WEBSITE, $websiteId ); if ($locale === '') { $locale = (string)$this->scopeConfig->getValue( 'general/locale/code', ScopeInterface::SCOPE_STORE, (int)$store->getId() ); } $previousLocale = $this->translate->getLocale(); $this->localeResolver->setLocale($locale); $this->translate->setLocale($locale)->loadData(Area::AREA_FRONTEND, true); try { return $this->render($invoice, $locale); } finally { $this->localeResolver->setLocale($previousLocale); $this->translate->setLocale($previousLocale)->loadData(Area::AREA_ADMINHTML, true); } } private function render(InvoiceInterface $invoice, string $locale): \Zend_Pdf { $pdf = new \Zend_Pdf(); $page = $this->newPage($pdf); $order = $invoice->getOrder(); $storeId = (int)$order->getStoreId(); $websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId(); $companyName = trim((string)$this->scopeConfig->getValue( self::CONFIG_COMPANY_NAME, ScopeInterface::SCOPE_WEBSITE, $websiteId )); if ($companyName === '') { $companyName = $this->config('general/store_information/name', $storeId) ?: (string)$this->storeManager->getStore($storeId)->getFrontendName(); } $invoiceTitle = trim((string)$this->scopeConfig->getValue( self::CONFIG_TITLE, ScopeInterface::SCOPE_WEBSITE, $websiteId )) ?: $companyName; $this->text($page, $this->truncate($invoiceTitle, 38), 298, 785, 21, true, self::DARK, 'center'); $this->text($page, (string)__('VAT INVOICE'), 540, 810, 11, true, self::GREY, 'right'); $this->line($page, 46, 764, 549, 764, self::DARK, 1.8); $this->labelValue($page, (string)__('INVOICE NO.'), '#' . $invoice->getIncrementId(), 46, 730); $this->labelValue( $page, (string)__('INVOICE DATE'), $this->formatDate((string)$invoice->getCreatedAt(), $locale), 316, 730 ); $this->text($page, (string)__('Order #%1', $order->getIncrementId()), 46, 691, 10, true, self::ACCENT); $billing = $order->getBillingAddress(); $shippingAddress = $order->getShippingAddress(); $customerAddress = $billing && $billing->getId() ? $billing : $shippingAddress; $customerName = $customerAddress ? $customerAddress->getName() : $order->getCustomerName(); $this->labelValue($page, (string)__('CUSTOMER NAME'), (string)$customerName, 46, 662); $this->labelValue($page, (string)__('COMPANY NAME'), $this->truncate($companyName, 32), 316, 662); $this->sectionTitle($page, (string)__('CUSTOMER ADDRESS DETAILS'), 46, 605, 232); $this->sectionTitle($page, (string)__('COMPANY ADDRESS DETAILS'), 316, 605, 233); $this->drawLines($page, $this->customerAddress($customerAddress, $locale), 46, 575, 232); $companyLines = $this->companyAddress($storeId, $locale); $vat = $this->config('general/store_information/merchant_vat_number', $storeId); if ($vat !== '') { $companyLines[] = (string)__('Company VAT Number: %1', $vat); } $this->drawLines($page, $companyLines, 316, 575, 233); $this->line($page, 46, 477, 549, 477, [0.82, 0.80, 0.76], 0.45); $paymentTitle = ''; try { $paymentTitle = (string)$order->getPayment()->getMethodInstance()->getTitle(); } catch (\Throwable $exception) { $paymentTitle = (string)$order->getPayment()->getMethod(); } $this->labelValue($page, (string)__('PAYMENT METHOD'), $paymentTitle, 46, 450); $shipping = $order->getIsVirtual() ? (string)__('N/A') : (string)$order->getShippingDescription(); $this->labelValue($page, (string)__('SHIPPING METHOD'), $shipping, 316, 450); $y = 390; $this->tableHeader($page, $y); $y -= 38; foreach ($invoice->getAllItems() as $item) { if ($item->getOrderItem() && $item->getOrderItem()->getParentItem()) { continue; } if ($y < 145) { $page = $this->newPage($pdf); $this->text($page, (string)__('VAT INVOICE #%1', $invoice->getIncrementId()), 46, 805, 13, true); $y = 770; $this->tableHeader($page, $y); $y -= 38; } $this->itemRow($page, $item, $order, $y); $y -= 56; } if ((float)$invoice->getShippingAmount() !== 0.0) { $this->shippingRow($page, $invoice, $order, $y); $y -= 56; } if ($y < 135) { $page = $this->newPage($pdf); $y = 760; } $this->totals($page, $invoice, $order, $y - 8); $this->footer($page, $storeId); return $pdf; } private function newPage(\Zend_Pdf $pdf): \Zend_Pdf_Page { $page = $pdf->newPage(\Zend_Pdf_Page::SIZE_A4); $pdf->pages[] = $page; $page->setFillColor($this->color(self::PAPER)); $page->drawRectangle(0, 0, 595, 842); return $page; } private function tableHeader(\Zend_Pdf_Page $page, float $y): void { $page->setFillColor($this->color(self::DARK)); $page->drawRectangle(46, $y, 549, $y - 28); $headers = [ [(string)__('ORDER I.D.'), 57], [(string)__('DESCRIPTION'), 137], [(string)__('QTY'), 320], [(string)__('ITEM PRICE'), 400], [(string)__('VAT RATE'), 466], [(string)__('TOTAL PRICE'), 537], ]; foreach ($headers as [$label, $x]) { $this->text($page, $label, $x, $y - 18, 8, true, [1, 1, 1], $x > 300 ? 'right' : 'left'); } } private function itemRow($page, $item, $order, float $y): void { $this->text($page, '#' . $order->getIncrementId(), 57, $y - 13, 9, false, self::GREY); $name = $this->truncate(html_entity_decode((string)$item->getName(), ENT_QUOTES | ENT_HTML5), 35); $this->text($page, $name, 137, $y - 12, 9, true); $this->text($page, (string)__('SKU: %1', $item->getSku()), 137, $y - 28, 8, false, self::GREY); $this->text($page, $this->qty((float)$item->getQty()), 320, $y - 13, 9, false, self::DARK, 'right'); $qty = max(1.0, (float)$item->getQty()); $this->text($page, $this->money($order, (float)$item->getRowTotal() / $qty), 400, $y - 13, 9, false, self::DARK, 'right'); $this->text($page, $this->percentage((float)$item->getTaxPercent()), 466, $y - 13, 9, false, self::DARK, 'right'); $total = (float)$item->getRowTotal() + (float)$item->getTaxAmount() - (float)$item->getDiscountAmount(); $this->text($page, $this->money($order, $total), 537, $y - 13, 9, false, self::DARK, 'right'); $this->line($page, 46, $y - 43, 549, $y - 43, [0.86, 0.84, 0.80], 0.35); } private function shippingRow($page, $invoice, $order, float $y): void { $this->text($page, (string)__('Shipping'), 137, $y - 12, 9, true); $this->text($page, '1', 320, $y - 13, 9, false, self::DARK, 'right'); $this->text($page, $this->money($order, (float)$invoice->getShippingAmount()), 400, $y - 13, 9, false, self::DARK, 'right'); $shippingAmount = (float)$invoice->getShippingAmount(); $shippingTaxRate = $shippingAmount > 0 ? (float)$invoice->getShippingTaxAmount() / $shippingAmount * 100 : 0.0; $this->text($page, $this->percentage($shippingTaxRate), 466, $y - 13, 9, false, self::DARK, 'right'); $total = (float)$invoice->getShippingAmount() + (float)$invoice->getShippingTaxAmount(); $this->text($page, $this->money($order, $total), 537, $y - 13, 9, false, self::DARK, 'right'); $this->line($page, 46, $y - 43, 549, $y - 43, [0.86, 0.84, 0.80], 0.35); } private function totals($page, $invoice, $order, float $y): void { $x = 330; $net = (float)$invoice->getGrandTotal() - (float)$invoice->getTaxAmount(); $this->text($page, (string)__('Total Excl. VAT'), $x, $y, 10, false, self::GREY); $this->text($page, $this->money($order, $net), 547, $y, 10, false, self::DARK, 'right'); $this->text($page, (string)__('VAT Amount'), $x, $y - 26, 10, false, self::GREY); $this->text($page, $this->money($order, (float)$invoice->getTaxAmount()), 547, $y - 26, 10, false, self::DARK, 'right'); $this->line($page, $x, $y - 36, 549, $y - 36, self::DARK, 0.8); $this->text($page, (string)__('Total Price (incl. VAT)'), $x, $y - 60, 12, true, self::GREY); $this->text($page, $this->money($order, (float)$invoice->getGrandTotal()), 547, $y - 60, 14, true, self::ACCENT, 'right'); } private function footer($page, int $storeId): void { $this->line($page, 46, 60, 549, 60, [0.82, 0.80, 0.76], 0.4); $this->text($page, (string)__('Thank you for your order.'), 46, 38, 9, true); $email = $this->config('trans_email/ident_support/email', $storeId); $phone = $this->config('general/store_information/phone', $storeId); $this->text($page, trim($email . ($phone ? ' | ' . $phone : '')), 549, 38, 8, false, self::GREY, 'right'); } private function sectionTitle($page, string $title, float $x, float $y, float $width): void { $this->text($page, $title, $x, $y, 9, true, self::ACCENT); $this->line($page, $x, $y - 10, $x + $width, $y - 10, [0.82, 0.80, 0.76], 0.4); } private function labelValue($page, string $label, string $value, float $x, float $y): void { $this->text($page, $label . ':', $x, $y, 9, false, self::GREY); $this->text($page, $value, $x, $y - 16, 11, true, self::DARK); } private function drawLines($page, array $lines, float $x, float $y, int $width): void { foreach (array_slice($lines, 0, 6) as $line) { $this->text($page, $this->truncate((string)$line, (int)($width / 5.2)), $x, $y, 9); $y -= 18; } } private function customerAddress($address, string $locale): array { if (!$address) { return []; } $lines = array_values(array_filter([ $address->getCompany(), ...((array)$address->getStreet()), trim((string)$address->getCity() . ' ' . (string)$address->getPostcode()), $this->regionName((string)$address->getRegionId()) ?: $address->getRegion(), $this->countryName((string)$address->getCountryId(), $locale), ])); return array_map('strval', $lines); } private function companyAddress(int $storeId, string $locale): array { $regionId = $this->config('general/store_information/region_id', $storeId); $countryId = $this->config('general/store_information/country_id', $storeId); return array_values(array_filter([ $this->config('general/store_information/street_line1', $storeId), $this->config('general/store_information/street_line2', $storeId), trim($this->config('general/store_information/city', $storeId) . ' ' . $this->config('general/store_information/postcode', $storeId)), $this->regionName($regionId), $this->countryName($countryId, $locale), ])); } private function countryName(string $countryId, string $locale): string { if ($countryId === '') { return ''; } $country = $this->countryFactory->create()->loadByCode($countryId); return $country->getId() ? (string)$country->getName($locale) : $countryId; } private function regionName(string $regionId): string { if ($regionId === '') { return ''; } $region = $this->regionFactory->create()->load($regionId); return $region->getId() ? (string)$region->getName() : $regionId; } private function config(string $path, int $storeId): string { return trim((string)$this->scopeConfig->getValue($path, ScopeInterface::SCOPE_STORE, $storeId)); } private function money($order, float $amount): string { return (string)$order->getOrderCurrency()->format($amount, [], false); } private function qty(float $qty): string { return rtrim(rtrim(number_format($qty, 4, '.', ''), '0'), '.'); } private function percentage(float $rate): string { return rtrim(rtrim(number_format($rate, 4, '.', ''), '0'), '.') . '%'; } private function formatDate(string $date, string $locale): string { $formatter = new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE); return (string)$formatter->format(new \DateTime($date)); } private function truncate(string $value, int $length): string { return mb_strlen($value) > $length ? mb_substr($value, 0, $length - 1) . '…' : $value; } private function text( $page, string $text, float $x, float $y, float $size = 9, bool $bold = false, array $color = self::DARK, string $align = 'left' ): void { $font = $bold ? $this->bold : $this->regular; $page->setFont($font, $size)->setFillColor($this->color($color)); if ($align !== 'left') { $width = $this->textWidth($text, $font, $size); $x -= $align === 'center' ? $width / 2 : $width; } $page->drawText($text, $x, $y, 'UTF-8'); } private function textWidth(string $text, $font, float $size): float { $encoded = @iconv('UTF-8', 'UTF-16BE//IGNORE', $text) ?: ''; $chars = []; for ($i = 0, $length = strlen($encoded); $i < $length; $i += 2) { $chars[] = (ord($encoded[$i]) << 8) | ord($encoded[$i + 1]); } if (!$chars) { return 0.0; } return array_sum($font->widthsForGlyphs($font->glyphNumbersForCharacters($chars))) / $font->getUnitsPerEm() * $size; } private function line($page, float $x1, float $y1, float $x2, float $y2, array $color, float $width): void { $page->setLineColor($this->color($color))->setLineWidth($width)->drawLine($x1, $y1, $x2, $y2); } private function color(array $rgb): \Zend_Pdf_Color_Rgb { return new \Zend_Pdf_Color_Rgb($rgb[0], $rgb[1], $rgb[2]); } }