390 lines
16 KiB
PHP
390 lines
16 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Esun\VatInvoice\Model\Pdf;
|
|
|
|
use Magento\Framework\App\Area;
|
|
use Magento\Framework\App\Config\ScopeConfigInterface;
|
|
use Magento\Framework\App\Filesystem\DirectoryList;
|
|
use Magento\Framework\Filesystem;
|
|
use Magento\Framework\Locale\ResolverInterface;
|
|
use Magento\Framework\Translate;
|
|
use Magento\Sales\Api\Data\InvoiceInterface;
|
|
use Magento\Directory\Model\CountryFactory;
|
|
use Magento\Directory\Model\RegionFactory;
|
|
use Magento\Store\Model\ScopeInterface;
|
|
use Magento\Store\Model\StoreManagerInterface;
|
|
|
|
class VatInvoice
|
|
{
|
|
private const CONFIG_LOCALE = 'sales/esun_vat_invoice/locale';
|
|
private const CONFIG_TITLE = 'sales/esun_vat_invoice/title';
|
|
private const CONFIG_COMPANY_NAME = 'sales/esun_vat_invoice/company_name';
|
|
private const ACCENT = [0.0902, 0.6627, 0.3412];
|
|
private const DARK = [0.10, 0.10, 0.10];
|
|
private const GREY = [0.38, 0.38, 0.38];
|
|
private const PAPER = [0.985, 0.978, 0.965];
|
|
|
|
private \Zend_Pdf_Resource_Font $regular;
|
|
private \Zend_Pdf_Resource_Font $bold;
|
|
|
|
public function __construct(
|
|
private readonly ScopeConfigInterface $scopeConfig,
|
|
private readonly StoreManagerInterface $storeManager,
|
|
private readonly ResolverInterface $localeResolver,
|
|
private readonly Translate $translate,
|
|
private readonly CountryFactory $countryFactory,
|
|
private readonly RegionFactory $regionFactory,
|
|
Filesystem $filesystem
|
|
) {
|
|
$root = $filesystem->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]);
|
|
}
|
|
}
|