InvoicePdfGenerator
in package
Domain wrapper over {@see PdfGenerator} for invoices.
Builds the exact $args key contract that the migrated Latte templates in the database depend on,
generates the QR payment, prepares the target folder and picks the PDF variant. Returns relative
paths of the PDF and QR for invoice.pdf / invoice.qrCode; the caller persists them
(InvoiceManager::completedSave()) through a post-commit updateColumn.
Best-effort: generate() never throws. A partial failure (QR without currency, missing variant,
render error) is only logged and yields null in the matching key, so the already saved invoice stays
untouched.
IMPORTANT: args are materialized from the invoice into a plain array before any find(). A later
clearDoctrine in the variant generator would otherwise detach the object graph being read.
Table of Contents
Constants
- EXEMPT_CODES : array<string|int, mixed> = [\App\UI\Api\System\Models\Enums\TaxTreatmentCo...
- Exempt supplies: the VAT column shows 0 percent on the document (section 29/2).
- MONEY_SCALE : int = 3
- Scale of the Invoicer money columns (`decimal(10,3)`); see {@see formatMoney()}.
- PDF_DIR_KEY : string = 'invoicerInvoicePdfDir'
- PDF_DIR_MODULE : int = 7
- QR_FILE : string = 'qrcode.png'
- SETTING_MODULE : int = 7
- Module index of Invoicer in the settings table (`SettingPresenter::MODULE`).
- TAX_DOCUMENT_CODES : array<string|int, mixed> = [\App\UI\Api\System\Models\Enums\TaxTreatmentCo...
- Treatment codes for which the document is a tax document (sections 28/29 of the VAT Act).
Properties
- $config : Config
- $countryManager : CountryManager
- $currencyManager : CurrencyManager
- $invoiceTypeManager : InvoiceTypeManager
- $languageManager : LanguageManager
- $localizationManager : LocalizationManager
- $orderNumberResolver : InvoiceOrderNumberResolver|null
- $pdfGenerator : PdfGenerator
- $priceFormatContext : PriceFormatContext
- $settingContext : SettingContext
- $taxManager : TaxManager
- $translator : Translator
Methods
- __construct() : mixed
- formatMoney() : string|null
- Formats a money value for display in the form, with the rules of {@see formatQuantity()} and the `decimal(10,3)` scale.
- formatQuantity() : string|null
- Formats a line quantity for printing in a PDF template.
- generate() : array{pdf: ?string, qrCode: ?string, pdfTemplateVariantId: ?int}
- Generates the QR and PDF into the invoice folder and returns their relative paths (or `null`).
- splitTaxNote() : array{0: ?string, 1: ?string}
- Splits the snapshot `tax_note` into [document language, Czech].
- storedFilesPresent() : bool
- Tells whether the files referenced by the `pdf` / `qr_code` columns exist on disk.
- applySymbol() : void
- buildArgs() : array<string, mixed>
- Builds the `$args` key contract of the legacy `InvoiceService::createVariablesForPdf`.
- buildFileName() : string
- Builds the file name `{prefix_}{invoiceId}_{timestamp}.pdf`.
- buildVatSummary() : array<int, array{rate: string, rateKey: string, base: float, vat: float, total: float}>
- Builds the VAT breakdown per rate for the footer of the modern PDF template.
- folderPath() : string
- generateQrCode() : string|null
- Generates the QR payment into the invoice folder and returns the relative path to `qrcode.png`.
- invoiceLocalization() : object|null
- Returns the document localization for the amount format, or null (historical documents) to fall back to the currency.
- lineToArray() : array<string, mixed>
- Converts a line (shipper, payment method or item; they share getters) to a plain array of the legacy contract.
- localizeArgs() : array<string, mixed>
- Applies the language and localization dependent parts of the args.
- orderNumber() : string|null
- Returns the order number for `$orderNumber`, or null without an order or without the order module.
- resolveVariant() : int|null
- Resolves the PDF variant for a document without a `pdf_template_variant_id` snapshot.
- resolveVariantFromMatrix() : int|null
- settingIntOrNull() : int|null
- Returns the PDF layout setting (see {@see InvoiceGenerator::settingIntOrNull()}); `null` means the default layout, which `PdfGenerator` resolves itself.
- spreadMixedRateVoucher() : array<string|int, array{rate: string, rateKey: string, base: float, vat: float, total: float}>
- Spreads a mixed-rate voucher across buckets proportionally to the item bases (fallback: bases of all buckets).
- taxArgs() : array<string, mixed>
- Adds the `$tax` template args (VAT treatment of the document).
- typeArgs() : array<string, mixed>
- Adds the document type to the template args.
Constants
EXEMPT_CODES
Exempt supplies: the VAT column shows 0 percent on the document (section 29/2).
private
array<string|int, mixed>
EXEMPT_CODES
= [\App\UI\Api\System\Models\Enums\TaxTreatmentCode::IntraCommunitySupply, \App\UI\Api\System\Models\Enums\TaxTreatmentCode::Export]
MONEY_SCALE
Scale of the Invoicer money columns (`decimal(10,3)`); see {@see formatMoney()}.
private
int
MONEY_SCALE
= 3
PDF_DIR_KEY
private
string
PDF_DIR_KEY
= 'invoicerInvoicePdfDir'
PDF_DIR_MODULE
private
int
PDF_DIR_MODULE
= 7
QR_FILE
private
string
QR_FILE
= 'qrcode.png'
SETTING_MODULE
Module index of Invoicer in the settings table (`SettingPresenter::MODULE`).
private
int
SETTING_MODULE
= 7
NOTE: a different constant from PDF_DIR_MODULE, which is for the config path, not settings, even though the number is the same.
TAX_DOCUMENT_CODES
Treatment codes for which the document is a tax document (sections 28/29 of the VAT Act).
private
array<string|int, mixed>
TAX_DOCUMENT_CODES
= [\App\UI\Api\System\Models\Enums\TaxTreatmentCode::Domestic, \App\UI\Api\System\Models\Enums\TaxTreatmentCode::ReverseCharge, \App\UI\Api\System\Models\Enums\TaxTreatmentCode::IntraCommunitySupply, \App\UI\Api\System\Models\Enums\TaxTreatmentCode::Export, \App\UI\Api\System\Models\Enums\TaxTreatmentCode::DomesticReverseCharge]
Drives $tax['taxDocument'], the "corrective tax document" sentence on cancellations and credit
notes. NON_PAYER (domestic non-payer = commercial document), OUTSIDE_SCOPE and VOUCHER_ISSUE
are not tax documents.
Properties
$config read-only
private
Config
$config
$countryManager read-only
private
CountryManager
$countryManager
$currencyManager read-only
private
CurrencyManager
$currencyManager
$invoiceTypeManager read-only
private
InvoiceTypeManager
$invoiceTypeManager
$languageManager read-only
private
LanguageManager
$languageManager
$localizationManager read-only
private
LocalizationManager
$localizationManager
$orderNumberResolver read-only
private
InvoiceOrderNumberResolver|null
$orderNumberResolver
= null
$pdfGenerator read-only
private
PdfGenerator
$pdfGenerator
$priceFormatContext read-only
private
PriceFormatContext
$priceFormatContext
$settingContext read-only
private
SettingContext
$settingContext
$taxManager read-only
private
TaxManager
$taxManager
$translator read-only
private
Translator
$translator
Methods
__construct()
public
__construct(PdfGenerator $pdfGenerator, InvoiceTypeManager $invoiceTypeManager, CurrencyManager $currencyManager, Config $config, SettingContext $settingContext, PriceFormatContext $priceFormatContext, LocalizationManager $localizationManager, TaxManager $taxManager, LanguageManager $languageManager, CountryManager $countryManager, Translator $translator[, InvoiceOrderNumberResolver|null $orderNumberResolver = null ]) : mixed
Parameters
- $pdfGenerator : PdfGenerator
- $invoiceTypeManager : InvoiceTypeManager
- $currencyManager : CurrencyManager
- $config : Config
- $settingContext : SettingContext
- $priceFormatContext : PriceFormatContext
- $localizationManager : LocalizationManager
- $taxManager : TaxManager
- $languageManager : LanguageManager
- $countryManager : CountryManager
- $translator : Translator
- $orderNumberResolver : InvoiceOrderNumberResolver|null = null
formatMoney()
Formats a money value for display in the form, with the rules of {@see formatQuantity()} and the `decimal(10,3)` scale.
public
static formatMoney(int|float|string|null $price) : string|null
Only what the admin sees changes; input still accepts both separators (Decimal::fromInput()
normalizes the comma and spaces).
IMPORTANT: do not pass the computed line fields (priceWithVat, totalPrice, ...) here. The client
invoice-calc.js overwrites them via toFixed(3), so the format would revert to a dot after the
first change and just flicker.
Parameters
- $price : int|float|string|null
Return values
string|nullformatQuantity()
Formats a line quantity for printing in a PDF template.
public
static formatQuantity(int|float|string|null $quantity[, string $decimalSeparator = ',' ]) : string|null
The Latte variants live in the database and are never edited (the content passes through TinyMCE,
which would break the markup), so the format has to be produced here when building $args:
- a whole number is printed as is ("2"), so renders of existing documents stay byte-identical;
- a decimal uses the given separator (comma by default) without trailing zeros ("2,5", "2,25");
null(missing shipper or payment) staysnull, son:if="!empty(...)"in templates still works.
Static and public on purpose: a pure function over a scalar that must be testable without the whole PDF pipeline and usable in admin listings. Delegates to QuantityFormat::quantity(), the single format of a document quantity (form, listing, PDF, e-mail).
Parameters
- $quantity : int|float|string|null
- $decimalSeparator : string = ','
Return values
string|nullgenerate()
Generates the QR and PDF into the invoice folder and returns their relative paths (or `null`).
public
generate(Invoice $invoice) : array{pdf: ?string, qrCode: ?string, pdfTemplateVariantId: ?int}
pdfTemplateVariantId is the variant the PDF was actually generated with (the document snapshot or a
fresh matrix resolve). The caller (InvoiceManager::completedSave()) stores it on a document
without a snapshot, so later re-edits stop resolving the live matrix.
Parameters
- $invoice : Invoice
Return values
array{pdf: ?string, qrCode: ?string, pdfTemplateVariantId: ?int}splitTaxNote()
Splits the snapshot `tax_note` into [document language, Czech].
public
static splitTaxNote(string|null $taxNote) : array{0: ?string, 1: ?string}
A bilingual clause is stored as two lines (document language, then Czech); a Czech document has one
line, so the Czech copy is null.
Parameters
- $taxNote : string|null
Return values
array{0: ?string, 1: ?string}storedFilesPresent()
Tells whether the files referenced by the `pdf` / `qr_code` columns exist on disk.
public
storedFilesPresent(Invoice $invoice, string|null $pdfRel, string|null $qrRel) : bool
Used to decide that a save without changes must not regenerate the PDF
(InvoiceManager::completedSave()). The lookup uses the same
folder generate() writes to (invoice folder plus the file name of the stored relative path), so the
check cannot point elsewhere than the write. An empty pdf means the file is missing; the QR may be null
when there was nothing to encode, but a filled qr_code must exist.
Parameters
- $invoice : Invoice
- $pdfRel : string|null
- $qrRel : string|null
Return values
boolapplySymbol()
private
applySymbol(QRPlatba $qr, string $setter, string|null $value) : void
Parameters
- $qr : QRPlatba
- $setter : string
- $value : string|null
buildArgs()
Builds the `$args` key contract of the legacy `InvoiceService::createVariablesForPdf`.
private
buildArgs(Invoice $invoice, Currency|null $currency, string|null $qrAbsPath) : array<string, mixed>
IMPORTANT: the templates migrated from the legacy database depend on these exact keys. They are
scalars plus seller, customer, shipperTariff, paymentMethod, deliveryData, items[],
giftList[], voucherList[], qrCodeImg, meta_title and vatSummary (see buildVatSummary()).
Effective VAT: for a non-payer (header useVat = 0) the vat rates of lines and vouchers go to the
PDF as null (database values stay, the PDF just does not use them). This mirrors InvoiceCalculator
and invoice-calc.js, which also ignore VAT when the header has it off.
Parameters
Return values
array<string, mixed>buildFileName()
Builds the file name `{prefix_}{invoiceId}_{timestamp}.pdf`.
private
buildFileName(int $invoiceId, InvoiceType|null $invoiceType, int|null $languageId) : string
The prefix comes from the per-language InvoiceTypeText.filename, sanitized to [A-Za-z0-9_-].
The timestamp separates regenerations in the same folder.
Parameters
- $invoiceId : int
- $invoiceType : InvoiceType|null
- $languageId : int|null
Return values
stringbuildVatSummary()
Builds the VAT breakdown per rate for the footer of the modern PDF template.
private
buildVatSummary(bool $headerUseVat, array<int, array<string, mixed>> $lines, array<int, array<string, mixed>> $vouchers[, int $itemCount = null ]) : array<int, array{rate: string, rateKey: string, base: float, vat: float, total: float}>
Sums the lines (items, shipperTariff, paymentMethod, all already through lineToArray()) by
their vat rate, then subtracts the vouchers so the breakdown matches the final header amounts after
discounts (the calculator computes totalPrice / totalPriceWithVat after vouchers too):
- the fixed part of a voucher (
price/priceWithVat) is subtracted from the bucket of its ownvatrate; - the percentage part is
percents / 100of the sum of all bucket bases, split across buckets proportionally to their base. The calculator takespercentsfrom the sum of the original lines, so the share uses the base before this voucher is subtracted.
Lines with name === null (missing shipper or payment method, neutral zeros) and lines with vat === null
(non-payer header or a line with useVat off) are skipped, otherwise a false bucket of rate 0 appears. A
voucher with vat === null is not subtracted. For a non-payer ($headerUseVat false) the result is always
an empty list.
A voucher of an e-shop document may carry a mixed effective rate (for example 20.0847 from a discount over
items with 21 and 15 percent). No bucket has such a rate, so it is spread across buckets proportionally to
the item bases (a percentage discount comes from items, not from shipping or payment), and the cent
remainder of VAT goes to the largest bucket. The breakdown then matches the voucher price /
priceWithVat and the header exactly. A rate that is a whole number after rounding to 4 places is
subtracted from its own bucket.
A group is keyed by the normalized rate string (VatRate::key(): '21', '5.5'), so the rates 5 and 5.5
never merge (a float or int key would cut 5.5 to 5). rate is the template text, rateKey the canonical
group key.
Parameters
- $headerUseVat : bool
- $lines : array<int, array<string, mixed>>
-
items[] + shipperTariff + paymentMethod
- $vouchers : array<int, array<string, mixed>>
-
voucherList from buildArgs()
- $itemCount : int = null
-
how many leading elements of
$linesare items (the rest is shipping and payment)
Return values
array<int, array{rate: string, rateKey: string, base: float, vat: float, total: float}>folderPath()
private
folderPath(int $invoiceId, DateTime $issuedAt, bool $abs) : string
Parameters
- $invoiceId : int
- $issuedAt : DateTime
- $abs : bool
Return values
stringgenerateQrCode()
Generates the QR payment into the invoice folder and returns the relative path to `qrcode.png`.
private
generateQrCode(Invoice $invoice, Currency|null $currency, string $folderAbs, string $folderRel) : string|null
The amount is useVat ? totalWithVat : total. Returns null (no QR) for an amount of 0 or less,
a missing currency, or neither bank account nor IBAN.
Parameters
Return values
string|nullinvoiceLocalization()
Returns the document localization for the amount format, or null (historical documents) to fall back to the currency.
private
invoiceLocalization(Invoice $invoice) : object|null
The invoice comes from the Api manager (clearDoctrine()), so a lazy association cannot be relied on:
the scalar id is read from the read-only mirror and the entity is loaded separately.
Parameters
- $invoice : Invoice
Return values
object|nulllineToArray()
Converts a line (shipper, payment method or item; they share getters) to a plain array of the legacy contract.
private
lineToArray(InvoiceShipper|InvoicePaymentMethod|InvoiceItem|null $line, bool $headerUseVat) : array<string, mixed>
A null line (missing shipper or payment method) yields neutral zeros so template n:if="!empty(...)" checks work.
Parameters
- $line : InvoiceShipper|InvoicePaymentMethod|InvoiceItem|null
- $headerUseVat : bool
-
effective VAT switch, mirroring
InvoiceCalculator; for a non-payer (header off) or a line withoutuseVat,vatgoes to the PDF asnull(the*WithVatamounts were already computed without VAT)
Return values
array<string, mixed>localizeArgs()
Applies the language and localization dependent parts of the args.
private
localizeArgs(array<string, mixed> $args, Currency|null $currency, object|null $localization) : array<string, mixed>
dateFormatis the short date format of the document localization; templates use{$dueDate|printDate:($dateFormat ?? 'd.m.Y')}, and without a localization it staysd.m.Y.- Line
quantityuses the localization decimal separator (whole numbers are unchanged, so a Czech document renders byte-identically).
Parameters
- $args : array<string, mixed>
- $currency : Currency|null
- $localization : object|null
Return values
array<string, mixed>orderNumber()
Returns the order number for `$orderNumber`, or null without an order or without the order module.
private
orderNumber(int|null $orderId) : string|null
Parameters
- $orderId : int|null
Return values
string|nullresolveVariant()
Resolves the PDF variant for a document without a `pdf_template_variant_id` snapshot.
private
resolveVariant(Invoice $invoice, InvoiceType|null $invoiceType) : int|null
Two modes: a document with tax_treatment uses the matrix of document type by treatment code (exact
match, then *); a document without it (legacy) uses the old matrix unchanged. An existing snapshot
is handled by the caller (generate()) and always wins.
Parameters
- $invoice : Invoice
- $invoiceType : InvoiceType|null
Return values
int|nullresolveVariantFromMatrix()
private
resolveVariantFromMatrix(Invoice $invoice, InvoiceType|null $invoiceType) : int|null
Parameters
- $invoice : Invoice
- $invoiceType : InvoiceType|null
Return values
int|nullsettingIntOrNull()
Returns the PDF layout setting (see {@see InvoiceGenerator::settingIntOrNull()}); `null` means the default layout, which `PdfGenerator` resolves itself.
private
settingIntOrNull(string $key) : int|null
Parameters
- $key : string
Return values
int|nullspreadMixedRateVoucher()
Spreads a mixed-rate voucher across buckets proportionally to the item bases (fallback: bases of all buckets).
private
spreadMixedRateVoucher(array<string|int, array{rate: string, rateKey: string, base: float, vat: float, total: float}> $buckets, array<string|int, float> $itemBases, float $price, float $priceWithVat) : array<string|int, array{rate: string, rateKey: string, base: float, vat: float, total: float}>
The VAT remainder (bucket rates times shares differ from the voucher VAT by cents) goes to the bucket with
the largest weight, so the subtracted sums equal price / priceWithVat exactly.
Parameters
- $buckets : array<string|int, array{rate: string, rateKey: string, base: float, vat: float, total: float}>
- $itemBases : array<string|int, float>
- $price : float
- $priceWithVat : float
Return values
array<string|int, array{rate: string, rateKey: string, base: float, vat: float, total: float}>taxArgs()
Adds the `$tax` template args (VAT treatment of the document).
private
taxArgs(array<string, mixed> $args, Invoice $invoice, InvoiceType|null $invoiceType) : array<string, mixed>
IMPORTANT: the key is always present (Latte reports an undefined variable without it). For a document
without tax_treatment all values are empty and templates branch on !empty($tax['code']), so a legacy
document renders unchanged.
Taken from the tax snapshot of the document (not recomputed): code, note / noteCs (the clause in the
document language and in Czech; tax_note stores it as two lines, see
DocumentLocale::taxNote()), notes (all distinct clauses; items carry no code
of their own, so at most one), rateCountry, chargeVat, customerVatIdVerified(At),
summaryReportCode, flags. Derived: rateCountryName, exempt, taxDocument, sellerStatus (seller
status on tax_date from the status history) and title, the document name by type, code and status
(document.invoicer.tax.title.*, in the document language). The title also replaces the PDF meta_title,
which would otherwise carry the internal variant name.
Parameters
- $args : array<string, mixed>
- $invoice : Invoice
- $invoiceType : InvoiceType|null
Return values
array<string, mixed>typeArgs()
Adds the document type to the template args.
private
typeArgs(array<string, mixed> $args, InvoiceType|null $invoiceType, int|null $languageId) : array<string, mixed>
invoiceTypeKey: cancellation and credit note share the PDF variants, so the heading and closing text branch in the template by this key ({if ($invoiceTypeKey ?? '') === 'DOBROPIS'}).meta_titleof a corrective document is the type name in the document language plus the number; the variant name would be wrong in the PDF title of a credit note.
Parameters
- $args : array<string, mixed>
- $invoiceType : InvoiceType|null
- $languageId : int|null