Templates and design
| Level | What you do |
|---|---|
| 1 – Configuration | Logo, colours, font, texts, footer, sender line, letter layout, paper and QR code in config/quittio.php (pdf.*), per invoice or per call with a Presentation |
| 2 – Partials | php artisan vendor:publish --tag=quittio-views and change single partials |
| 3 – Own template | php artisan make:quittio-template my-design, then ->template('invoices.my-design.layout') or pdf.template |
| 4 – Own renderer | Quittio::extend('renderer', …) (PDF renderers); PDF/A and the embedded XML stay with the package |
Templates only display data. Totals, VAT, texts of payment terms, QR codes and labels are prepared in the view model; the same data also produces the XML, and the mandatory field guard checks that a template prints every mandatory value.
Default templates
| Engine | Template | Used by |
|---|---|---|
| Blade | quittio::templates.default.layout with partials in quittio::templates.default.partials |
Laravel (pdf.template) |
| Plain PHP | packages/core/resources/templates/default.php |
Quittio::create() (template default); Laravel falls back to it for names that are no Blade view |
Both print the same document. Partials of the Blade template:
| Partial | Content |
|---|---|
styles |
CSS: page margins, font, colours, address field position, watermark for browsers |
footer |
<htmlpagefooter> with the footer columns and “Page {PAGENO} of {nbpg}” |
marks |
Fold and punch marks (option fold_marks) |
sender-line |
Sender line above the address |
recipient |
Address lines of the recipient |
letterhead |
Logo, name of the issuer, the issuer’s address when the footer does not start with it |
meta |
Number, date, valid until, reference document, delivery date, service period, customer number, references, VAT identifier and tax number of the seller, tax representative, VAT identifier of the buyer, deliver-to address |
title |
Title (<h1>) |
references |
Preceding invoices (“Reference: invoice … of …”) |
intro |
Salutation and intro text |
lines |
Line table: groups with subtotals, optional and alternative markers, item numbers, descriptions, line periods, attributes, line allowances and charges, line notes, serial numbers, sub lines, price base quantity, gross unit price |
allowances |
Sum of lines, document allowances and charges (inside totals) |
totals |
Net total, VAT rows, gross total, paid amount, rounding, third party payments, amount due, VAT in accounting currency |
documents |
Packages, open items, fees, interest, totals and balance of documents |
tax-notes |
Exemption reasons and regulatory notes (legalNotes) |
payment |
Payment terms, bank details or payout account, direct debit notice, payment QR code |
notes |
Other notes |
attachments |
Attached documents (identifier and description) |
closing |
Closing text, acceptance field of quotes |
php artisan vendor:publish --tag=quittio-views copies the views to resources/views/vendor/quittio/templates; Laravel then uses your copy of a partial instead of the shipped one. The layout takes the partial prefix from $partials (default quittio::templates.default.partials).
Selecting a template
The template of a run is, in this order: ->template('…') of the call, Presentation::$template of the invoice’s own presentation, the template of the presentation passed to ->presentation(), the template of the configured presentation, the configured template. In Laravel the configured name is default, which the Blade engine maps to pdf.template (default quittio::templates.default.layout); without Laravel it is the template key of Quittio::create() (default default). The same order applies to documents (quotes, reminders …).
Template names may come from invoice data (Presentation::$template, issuer branding of the Managed layer), so they are restricted:
- The Blade engine renders
default, the configuredpdf.template, the shipped views underquittio::templates.(not their partials) and the views listed inpdf.templates(default['invoices.*']; an entry ending in*allows every view below that prefix, e.g.mytheme::*). Another existing view fails with anInvalidArgumentException. Names that are no Blade view are passed to the plain PHP engine. - The plain PHP engine accepts names only (
[A-Za-z0-9_-]segments separated by/, no.., no absolute paths or stream wrappers) and resolves{name}.phpinside its template directories (template_pathsofQuittio::create(), then the shipped templates). An unknown or invalid name fails with anInvalidArgumentException.
php artisan make:quittio-template my-design # resources/views/invoices/my-design (--force overwrites)
The command copies the default layout and partials and points the layout at invoices.my-design.partials. Use it per call with ->template('invoices.my-design.layout') (allowed by invoices.*) or for every invoice with pdf.template.
A plain PHP template receives $invoice and $e (escape function returning HTML-safe text). Blade templates receive $invoice. Print values with {{ }}; only the prepared HTML values lines[].description.html, notes[].html, texts.intro, texts.closing and design.watermarkCss may be printed unescaped.
Other engines (Twig …) implement Quittio\Core\Templates\TemplateEngine::render(string $template, InvoiceViewModel $invoice, array $data = []): string and are passed to Quittio::create(templates: …) or $quittio->withTemplateEngine(…).
View model
Templates receive $invoice (Quittio\Core\View\InvoiceViewModel) and nothing else. Nodes are read-only ViewNode objects: use object syntax ($invoice->seller->name) or array syntax, loop over lists, count(), isEmpty() and toArray() (plain values, formatted). Writing throws a LogicException; a missing key reads as null.
Values that need formatting are FormattedValue objects: {{ $invoice->totals->amountDue }} prints the formatted text (->formatted), ->raw holds the original value (Brick\Money\Money for amounts, Quittio\Core\Support\Date for dates, Period for periods, Brick\Math\BigDecimal for quantities, prices and rates). Templates contain no calculations.
$invoice->label($key, $replace) returns a label with :placeholders replaced (unknown keys return the key), $invoice->title() the title.
Top level
| Key | Content |
|---|---|
number |
Invoice number (BT-1) |
numberLabel, dateLabel |
Labels for number and date (“Invoice number”, “Invoice date”; documents: “Number”, “Date”) |
title |
Document title: Presentation::$title; for self-billed type codes the title self_billed of the country pack’s correction policy (Germany: “Gutschrift”, in English “Self-billed invoice (Gutschrift)”), like the titles of cancellations and corrections; otherwise the label of the type code (title_380 “Invoice”, title_381 “Credit note”, title_384, title_389 “Self-billed invoice”, title_325, title_326, title_386, title_875, title_876, title_877; other self-billed codes use title_389, other credit notes title_381, everything else title_default) |
issueDate, dueDate |
Dates (BT-2, BT-9) |
servicePeriod |
Invoicing period (BG-14), otherwise the delivery date (BT-72), otherwise the period from the earliest start to the latest end of the line periods when every line has one; null otherwise |
servicePeriodLabel |
“Delivery date” for a single date or a one-day period, otherwise “Service period” (German “Leistungsdatum” / “Leistungszeitraum”) |
deliveryDate |
BT-72 when an invoicing period is also set (otherwise servicePeriod shows it) |
currency |
Invoice currency (BT-5) |
document |
document |
documentInfo |
null for invoices, documentInfo for documents |
seller, buyer |
Parties |
issuer, recipient |
The party in letterhead, sender line and footer, and the party in the address field: seller and buyer, on a self-billed invoice buyer and seller |
payee |
Payee (BG-10): name, identifier, legalRegistration; null without |
taxRepresentative |
Seller tax representative (BG-11): name, vatId, address; null without |
delivery |
recipientName, date (BT-72), address (with the recipient name as first line); null without delivery data |
references |
List of key, label, value for the filled references: buyer_reference, purchase_order, sales_order, contract, project (identifier and name), despatch_advice, tender, invoiced_object, accounting_reference |
precedingInvoices |
List of number, issueDate (BG-3) |
lineGroups |
Line groups |
lines |
All top-level lines |
allowances, charges |
Document allowances and charges |
vatRows |
VAT breakdown |
totals |
Totals; null when not calculated |
payment |
Payment; null without payment instructions |
paymentTerms |
Payment terms |
legalNotes |
List of texts: exemption reasons (BT-120, except category S) and notes with subject REG, ABL or TXD (BT-22), without duplicates |
notes |
Other notes: subject (BT-21), content, html |
attachments |
List of id, description, filename, uri (BG-24) |
texts |
salutation (plain text), intro, closing (HTML from Markdown) |
labels |
All labels of the locale, with the overrides of Presentation::$labels |
design |
Design values |
meta |
locale, custom (values of ->custom()), source |
document
| Key | Content |
|---|---|
typeCode |
BT-3 as string, e.g. 380; null for documents |
type |
Name of the type code in the locale; for documents the name of the document type, e.g. “Quote” |
isCreditNote |
Credit note type code |
isSelfBilled |
Self-billed invoice (“Gutschrift”) |
profile, profileKey |
Name and key of the profile of the run, e.g. EN 16931, en16931 |
kind |
invoice, or the document type (quote, order_confirmation, delivery_note, proforma, payment_reminder, account_statement) |
isInvoice |
true for invoices, false for documents |
showPrices |
false for delivery notes |
showTotals |
false for delivery notes, reminders and statements |
Parties
seller and buyer:
| Key | Seller | Buyer |
|---|---|---|
name |
BT-27 | BT-44 |
tradingName |
BT-28 | BT-45 |
displayName |
trading name, otherwise name | trading name, otherwise name |
address |
see below | see below |
vatId |
BT-31 | BT-48 |
taxNumber |
BT-32 | – |
legalRegistration |
BT-30 value | BT-47 value |
additionalLegalInfo |
BT-33 | – |
customerNumber |
– | buyer identifier (BT-46) |
contact |
name, phone, email or null |
name, phone, email or null |
electronicAddress |
BT-34 value | – |
identifiers |
list of BT-29 values | – |
isConsumer |
– | consumer flag |
issuer and recipient are the same nodes. Every address has lines (formatted by the country pack’s address formatter, starting with the name; the country is added in upper case when it differs from the issuer’s country), street, additional, line3, postcode, city, subdivision, countryCode and country (name in the locale). The fallback formatter writes “postcode city”, for GB, IE, US, CA, AU, LV and UA “city subdivision postcode”.
Lines
lines and lineGroups[].lines contain:
| Key | Content |
|---|---|
position, id |
Line identifier (BT-126) |
name |
Item name (BT-153) |
description |
text (BT-154) and html (escaped plain text with line breaks, or Markdown with the option markdown) |
note |
Line note (BT-127) |
sellerId, buyerId |
Item identifiers (BT-155, BT-156) |
quantity |
Quantity (BT-129), formatted without trailing zeros |
unit |
code (BT-130) and label in the locale |
unitPrice |
Net price (BT-146), at least two decimals |
grossUnitPrice |
Price including VAT (gross price mode) |
listPrice |
Gross price before the price discount (BT-148) |
baseQuantity |
Price base quantity (BT-149) |
unitPriceBase |
Base quantity with unit, e.g. “100 pcs”, for printing with the unit price; null for 1 |
showGrossPrice |
Print grossUnitPrice: the line’s showGrossPrice (LineBuilder::showGrossPrice()), otherwise the presentation’s showGrossPrices, and only if a price including VAT exists |
total |
Line net amount (BT-131) |
vatRate |
Rate (BT-152); null without rate and for margin schemes and travel agents |
vatCategory |
Category code (BT-151) |
period |
Line period (BG-26) |
attributes |
List of name, value (BG-32) |
allowances, charges |
Line allowances and charges: reason, amount, percent, label (“Allowance reason 10 %”) |
subLines |
Sub lines with the same keys |
highlight |
LineBuilder::highlight(); the default templates shade the line and mark it in the secondary colour |
groupId |
Line group |
optional, alternativeTo, serialNumbers |
Document line options (quotes, delivery notes) |
lineGroups lists the groups defined with InvoiceBuilder::group() in their order, then one group with id and title null for the lines without (or with an unknown) group; empty groups are left out. Each group has id, title, lines and subtotal (sum of the line net amounts when showSubtotal is set, otherwise null). Print either lineGroups or lines, not both.
Allowances and charges
allowances and charges (BG-20, BG-21) contain reason (text or code), amount, percent, base, vatRate, vatCategory and label (“Allowance reason 3 %”; with several VAT groups followed by the VAT rate or category, e.g. “(19 % VAT)”).
VAT rows
| Key | Content |
|---|---|
label |
Ready text: “19 % VAT on 1.000,00 €”, “VAT exempt (…) on …”, “No VAT (reverse charge) on …”, “0% VAT (zero rate) on …”, “Not subject to VAT: …”, “Special scheme, no separate VAT statement: …” |
category, categoryLabel |
Category code and name |
rate |
Rate (BT-119) |
base |
Taxable amount (BT-116) |
amount |
VAT amount (BT-117); null for margin schemes and travel agents (VATEX-EU-D, -F, -I, -J) |
isSpecialScheme |
Margin scheme or travel agents |
exemptionReason, exemptionReasonCode |
BT-120, BT-121 |
Totals
lineNetSum, allowanceSum, chargeSum, netTotal, vatTotal, vatTotalInTaxCurrency (BT-111), taxCurrencyNote (“VAT in USD: … (rate … of …)” when an exchange rate is set), grossTotal, prepaid, rounding, thirdPartyPayments (list of description, amount, XRechnung extension), amountDue and amountDueLabel.
amountDueLabel is “Amount due”, “Amount to be refunded” on credit notes and negative invoices, “Amount paid out” on self-billed invoices. On a negative invoice (e.g. a cancellation with the strategy negative_invoice) amountDue is printed as a positive refund amount; amountDue->raw and the XML keep the negative BT-115.
Payment
| Key | Content |
|---|---|
meansCode, means, meansText |
Payment means code (BT-81), its name, BT-82 |
remittance |
Remittance information (BT-83), otherwise the invoice number |
accounts |
List of iban (grouped in blocks of four), bic, name |
card |
number (“•••• 1234”), holder; or null |
directDebit |
mandate, creditorId, account; or null |
qrCode |
Payment QR code or null |
isRefund |
Credit note or negative amount due: show no bank details for a transfer by the buyer and no direct debit notice |
isPayout |
Self-billed invoice: the accounts receive the payout |
Payment terms
null without payment terms (BT-20) and due date, otherwise lines (ready texts: the terms text, “Please pay the amount by …” or on self-billed invoices “We will transfer the amount to your account by …”, one text per cash discount, late fee terms), discounts (list of days, percent, until, amount, text) and dueDate. On credit notes, offers and invoices without a positive amount due, discounts are shown as agreement (“For payment within 7 days …”) without date or amount, and no due date text is added.
Design
| Key | Content |
|---|---|
primaryColor, secondaryColor, textColor, mutedColor |
Hex colours (secondaryColor falls back to the primary colour). The default templates print company name, title and the header of the lines in the primary colour, section titles, the line above the amount due and the mark of highlighted lines in the secondary colour |
font |
Font family |
logo |
Prepared logo (Logos) or null |
letterLayout |
Name of the letter layout |
addressTop, addressLeft, addressWidth, addressHeight |
Address field in mm from the top left page corner |
pageMarginTop |
Top page margin of the default templates (20 mm) |
letterheadHeight |
Space above the address field: addressTop − pageMarginTop |
addressZoneTop |
Start of the recipient within the address field (DIN 5008 “Anschriftzone”, 17.7 mm) |
senderLineHeight |
Height of the sender line (5 mm) |
noticeZoneHeight |
addressZoneTop − senderLineHeight: “Zusatz- und Vermerkzone” above the sender line |
foldMarks, punchMark |
Mark positions in mm from the top; empty unless the option fold_marks is set |
paper |
Paper of the presentation |
watermark, watermarkCss |
Watermark text, and the same as escaped CSS string for the HTML overlay |
senderLine |
Configured sender line, otherwise “Name · Street · Postcode City” of the issuer |
footerColumns |
Configured columns, otherwise generated: the issuer’s address lines; phone, e-mail, VAT identifier, tax number; account holder, IBAN, BIC of the first credit transfer (not on refunds and self-billed invoices); legal registration and additional legal information |
footerHasAddress |
The generated footer starts with the issuer’s address |
showItemNumbers, showGrossPrices |
Presentation switches |
documentInfo
Only for documents:
| Key | Content |
|---|---|
type |
Document type value |
validUntil |
Quote validity |
reference, referenceDate |
Preceding document (reference()) |
deliveryDate |
Delivery date |
packages |
List of id, type, weight (“2.5 kg”), dimensions, trackingNumber |
lines |
Line options keyed by line identifier: optional, alternativeTo, serialNumbers |
dunning |
level, deadline, deadlineText (“Please pay the total amount by …”), openItems, fees (description, amount), interest (description, days, amount), openTotal, feeTotal, interestTotal, total; null for other types |
statement |
period, items, openingBalance, balance; null for other types |
showAcceptance |
Print the acceptance field (quotes) |
Open items (dunning.openItems[], statement.items[]) have invoiceNumber, invoiceDate, dueDate, description, amount, paid and open.
Presentation
Quittio\Core\Data\Presentation\Presentation holds everything that only affects the PDF and HTML; it is never written to the XML.
| Property | Default | Effect | Laravel configuration |
|---|---|---|---|
template |
null |
Template of this invoice (Selecting a template) | pdf.template |
logo |
null |
Logo (Logos) | pdf.logo |
colors |
null |
Colors(primary: '#1e40af', text: '#111827', muted: '#6b7280', secondary: null) |
pdf.colors.primary, text, muted, secondary |
font |
null (inter) |
Font family (Fonts) | pdf.font |
senderLine |
null (generated) |
Sender line above the address | pdf.sender_line |
letterLayout |
null (country default) |
Letter layout | pdf.layout |
salutation |
null |
Plain text before the intro | pdf.texts.salutation |
introText, closingText |
null |
Markdown texts before the lines and at the end | pdf.texts.intro, pdf.texts.closing |
footerColumns |
[] (generated) |
list<list<string>>, one list of lines per column |
pdf.footer_columns |
lineGroups |
[] |
list<LineGroup> (id, title, showSubtotal = true) |
– (InvoiceBuilder::group()) |
showGrossPrices |
false |
Print the price including VAT below the net unit price | – |
showItemNumbers |
false |
Print the seller’s item number (BT-155) after the item name | pdf.show_item_numbers |
paymentQr |
auto |
Payment QR code: auto, girocode, swiss-qr-bill, none |
pdf.payment_qr.type |
watermark |
null |
Watermark text | – (option watermark) |
paper |
A4 |
Paper size | pdf.paper |
labels |
[] |
Label overrides, e.g. ['amount_due' => 'Zu zahlen'] |
– |
title |
null |
Title instead of the type code label; also used in the file name and the PDF title | – |
explicit |
[] |
Properties whose value wins even when it equals the default | – |
Line-level hints are LinePresentation (groupId, highlight, showGrossPrice); the builder sets them with LineBuilder::group($groupId), highlight() and showGrossPrice(?bool $show = true) (prints the price including VAT of this line, or with false not, whatever showGrossPrices says; null follows the presentation).
use Quittio\Core\Data\Presentation\Colors;
use Quittio\Core\Data\Presentation\Presentation;
$invoice = InvoiceBuilder::make()
// ...
->presentation(fn (Presentation $p) => $p->with(['logo' => storage_path('app/logo.png'), 'introText' => 'Thank you for your order.']))
->group('hardware', 'Hardware') // line group with subtotal
->line(fn ($l) => $l->name('Router')->quantity(2, 'C62')->netPrice('89.00')->vat(VatPreset::Standard19)->group('hardware'))
->build();
Quittio::make($invoice)
->presentation(new Presentation(colors: new Colors(primary: '#0f766e'), labels: ['amount_due' => 'Total to pay']))
->render();
The design of a run is the invoice presentation over the presentation of the call (->presentation(...)) over the configured design, property by property: a value that equals the default of an empty Presentation (false, [], 'auto', 'A4', null) counts as not set and keeps the value below; objects such as colors replace the value below as a whole; labels are merged key by key. To reset a configured value for one invoice, name the property in explicit: new Presentation(showGrossPrices: false, paper: 'A4', footerColumns: [], explicit: ['showGrossPrices', 'paper', 'footerColumns']) switches gross prices off, prints on A4 although pdf.paper is Letter and prints no footer columns.
Values are checked when rendering, otherwise a ConfigurationException is thrown: colours must be hex values (#1e40af or #fff), fonts plain family names (letters, digits, space, _ and -, at most 64 characters), letter layouts and payment QR code types must be known. An unknown pdf.payment_qr.type already fails when the Laravel configuration is mapped (first use of the facade).
Render options that affect the design (per call with ->option(), without Laravel also in options): fold_marks (bool), markdown (bool), paper (string), watermark (string), girocode_amount (due, discounted) and logo_cache (directory).
Letter layouts
A letter layout (Quittio\Core\View\LetterLayout) fixes the address field on the first page: name, addressTop (45), addressLeft (20), addressWidth (85), addressHeight (45), senderLineHeight (5), foldMarks ([]), punchMark (null) and addressZoneTop (17.7), all in mm. LetterLayout::free() is the layout without norm.
The layout is Presentation::$letterLayout (pdf.layout), otherwise the country pack default, otherwise free. Country packs provide layouts with letterLayouts(); an unknown name fails with a ConfigurationException listing the known layouts.
The German pack provides din5008a (address field 27 mm from the top, fold marks at 87 and 192 mm), din5008b (45 mm, fold marks at 105 and 210 mm, default) and free; the punch mark is at 148.5 mm. The sender line closes the “Zusatz- und Vermerkzone”, the recipient starts in the “Anschriftzone” 17.7 mm below the top of the field. Fold and punch marks are printed with pdf.fold_marks = true (option fold_marks). See Country packs.
Logos
- Logos are local files, data URIs or
http/httpsURLs of PNG, JPEG or SVG images, at most 10 MB and 25 megapixels. Other stream wrappers (ftp://,php://,phar://…), a leading//, backslashes and surrounding whitespace fail with aConfigurationException, as does a local file that does not exist. - A data URI is decoded strictly, checked and re-encoded, so only validated bytes reach the renderer.
- Remote logos are downloaded by Quittio on every render (no cache): 10 s timeout, at most 3 redirects, each checked again, only from hosts whose every IPv4 and IPv6 address is public (no loopback, private, link-local, shared, reserved, documentation or multicast range, also not inside NAT64 addresses); the connection is pinned to the checked address and TLS is verified for the host name. A refused address fails with a
ConfigurationException; an unreachable logo is left out. The downloaded copy is embedded as data URI (also in HTML output). - Raster logos larger than 600 × 200 px are scaled down with GD (transparency is kept); scaled copies of local files are cached in the logo cache (
pdf.cache_path/logos, without Laraveltemp_dir/logos, defaultsys_get_temp_dir()/quittio-mpdf/logos). - For PDF/A (ZUGFeRD/Factur-X, XRechnung in PDF, or
pdf.pdfa = true), every logo is re-encoded as RGB PNG on white, because PDF/A forbids the alpha channel mPDF writes; SVG is rasterised with the PHP extensionimagick(SVG coder only, 300 dpi but at most twice the logo size). Withoutimagickan SVG logo fails with aConfigurationExceptionin PDF/A documents. - SVG logos must be self-contained: no
hreforurl()to files, URLs or nested SVG (embedded PNG, JPEG, GIF or WebP data URIs are allowed) and no entity declarations. - mPDF never fetches anything itself (PDF renderers).
- In HTML output a local logo is referenced by its file path, which browsers cannot load; use a URL or data URI for logos in HTML that is shown in a browser.
- Local logo paths and URLs are trusted configuration: do not take
Presentation::$logoor issuer branding from end users unless your application validates them (e.g. an uploaded file stored by the application).
Fonts
Inter (Regular, Bold, Italic, Bold Italic) is bundled and the default (pdf.font = 'inter'). The font name is used as CSS font-family in the templates and as default font of mPDF. Register more fonts in Laravel:
'font' => 'corporate',
'fonts' => ['corporate' => ['R' => 'Corporate-Regular.ttf', 'B' => 'Corporate-Bold.ttf', 'I' => 'Corporate-Italic.ttf', 'BI' => 'Corporate-BoldItalic.ttf']],
'font_directories' => [resource_path('fonts')],
Use lower case family names, as mPDF looks fonts up in lower case. Fonts must be embeddable TrueType fonts (PDF/A embeds all fonts). php artisan vendor:publish --tag=quittio-fonts copies Inter to resources/fonts/quittio/inter. The Laravel renderer searches pdf.font_directories, then resources/fonts/quittio and its subdirectories automatically, so fonts placed there only need an entry in pdf.fonts. Font files are taken from the first directory that has them: the configured directories come before the bundled Inter and the fonts of mPDF, so a published and edited Inter replaces the bundled one. Without Laravel pass the fonts to the renderer: new MpdfRenderer(tempDir: …, fonts: [...], fontDirectories: [...], defaultFont: 'corporate').
Paper size
The paper is the first of: the invoice presentation’s paper, the presentation of the call and the configured presentation (each only when it is not A4 or names paper in explicit), the option paper (pdf.paper), the country pack default (Germany: A4). pdf.paper = null uses the country default; a configured size wins, also A4. The value is a page format of mPDF such as A4, A5 or Letter.
Watermark
A watermark marks drafts and previews: ->option('watermark', 'DRAFT') or Presentation::$watermark. Watermarked PDFs are rendered without PDF/A (PDF/A does not allow the transparency of watermarks) and never contain e-invoice XML: a watermark together with any format that produces XML (also next to pdf in formats([...])) throws a ConfigurationException, so a draft can never pass as an e-invoice. The default templates print it with mPDF’s <watermarktext> (alpha 0.08) and, for browsers, as rotated CSS overlay (design.watermarkCss). The labels draft and preview (“DRAFT”, “PREVIEW”; German “ENTWURF”, “VORSCHAU”) can be used as text.
Page numbers, headers and footers
The default templates put the footer columns and “Page {PAGENO} of {nbpg}” into an <htmlpagefooter> that every page uses (@page { footer: html_quittioFooter; }). {PAGENO} and {nbpg} are replaced by mPDF; headers work the same way with <htmlpageheader>.
Payment QR codes
paymentQr |
Generator |
|---|---|
auto (default) |
A registered generator (withPaymentQrGenerator(), Laravel pdf.payment_qr.generator), otherwise what the country pack names in CountryDefaults::$paymentQr: none prints no code; otherwise the country pack’s generator (paymentQr()), otherwise the EPC GiroCode for girocode (the default, also without country pack) and no code for other types |
girocode |
EPC GiroCode |
swiss-qr-bill |
The generator registered with withPaymentQrGenerator() (Laravel: pdf.payment_qr.generator). Quittio ships no Swiss QR bill generator; without one the render run fails with a ConfigurationException |
none |
No code |
No code is printed on refunds (credit notes, negative invoices) and self-billed invoices. The EPC GiroCode (EpcGiroCodeGenerator, EPC069-12 version 002) is created when the currency is EUR, the payment means is a (SEPA) credit transfer (30, 58) with a valid IBAN and the amount due is between 0.01 and 999,999,999.99. The payload contains the BIC (optional), the IBAN, the amount, the beneficiary (payee name, otherwise account name, otherwise seller name, at most 70 characters) and the remittance information (BT-83, otherwise the invoice number, at most 140 characters), at most 331 bytes.
pdf.payment_qr.girocode_amount (option girocode_amount) is due (amount due) or discounted (amount after the first cash discount); then qrCode.note says until when the code is valid (“QR code with the amount after cash discount (…), valid until …”). Payment reminders get a code over the total of the reminder (documents).
payment.qrCode holds type (e.g. girocode), payload, dataUri (PNG data URI rendered with GD, SVG without GD; PNG because mPDF’s SVG output violates PDF/A), amount and note. Quittio\Core\Pdf\Qr\QrImage::svg(), png() and dataUri() render any payload. Own generators implement PaymentQrGenerator (key(), supports(InvoiceData), payload(InvoiceData, array $options)) and are returned by a country pack’s paymentQr().
Texts and Markdown
Line descriptions and notes are also part of the e-invoice XML and are printed as plain text (lines[].description.html, notes[].html: escaped, line breaks kept). With pdf.markdown = true (option markdown) they are rendered as Markdown (paragraphs, lists, bold, italic; HTML is shown as text, links keep only their text). Formatting changes the displayed text compared with the XML, e.g. 20*30*40 cm is shown as “203040 cm” with “30” in italics and without the asterisks, so keep it off for texts that must match the XML. Intro and closing texts (texts.intro, texts.closing) are PDF-only and always Markdown; the salutation is plain text.
Translations and locales
The locale of a run is, in this order: ->locale() of the call, the invoice’s locale (InvoiceBuilder::locale()), defaults.locale (locale in Quittio::create()), the country pack default (Germany: de). Labels and code names (units, VAT categories, payment means, type codes …) exist in German, English and French; other locales fall back to English. The XML texts generated for the run (e.g. payment terms) use the same locale.
| Source | Where |
|---|---|
| Shipped labels | packages/core/resources/lang/{de,en,fr}/labels.php (namespace quittio) |
| Code names | codes.php, payment terms texts terms.php |
| Laravel overrides | lang/vendor/quittio/{locale}/labels.php (php artisan vendor:publish --tag=quittio-lang) |
| Country pack overrides | quittio-{code}::labels.{key} in the pack’s translationPath() |
| Per invoice or call | Presentation::$labels |
Without Laravel the translator is Quittio\Core\I18n\ArrayTranslator (files {path}/{locale}/{group}.php, later paths override earlier ones, fallback en). Add a directory with Translations::translator()->addPath($path, 'quittio') or replace the translator with Translations::use(new MyTranslator) (Quittio\Core\I18n\Translator: get(), has(), locale(), addPath()). The Laravel integration bridges to Laravel’s translator.
Values are formatted by IntlFormatter from their exact decimal value: amounts with two decimals and the currency symbol (€, $, £, CHF, otherwise the code; English puts $ and £ in front), prices with at least two decimals, quantities and rates without trailing zeros, separators from ext-intl (fallback: German and English conventions), dates dd.MM.yyyy (de), dd/MM/yyyy (fr, it, es, pt, nl, en), MM/dd/yyyy (en_US) and the medium date format of ext-intl for other locales. Country names come from ext-intl.
PDF renderers
| Renderer | Use |
|---|---|
MpdfRenderer (default) |
HTML to PDF/A-3b with mPDF in pure PHP; embeds the XML and writes the XMP metadata in one pass |
CallbackRenderer(Closure $callback) |
Own rendering (Browsershot, Gotenberg …): fn (string $html, RenderRequest $request): string returns the PDF; PDF/A, metadata and the XML are added by MpdfPostProcessor |
PassthroughRenderer(string $pdf) |
A finished PDF of the application; only PDF/A and the XML are added |
use Quittio\Core\Pdf\Renderers\CallbackRenderer;
use Quittio\Core\Pdf\RenderRequest;
use Spatie\Browsershot\Browsershot;
Quittio::extend('renderer', 'browsershot', fn ($app, array $config) => new CallbackRenderer(
fn (string $html, RenderRequest $request) => Browsershot::html($html)->format($request->paper)->pdf(),
));
// config/quittio.php: 'pdf' => ['renderer' => 'browsershot', 'renderers' => ['browsershot' => [...]]]
The factory receives the container and pdf.renderers.{name}. Without Laravel pass the renderer to Quittio::create(renderer: …) or $quittio->withRenderer(…). A renderer implements PdfRenderer (render(RenderRequest $request): string, supportsEmbedding(): bool). supportsEmbedding() returns true when the renderer creates the PDF/A and embeds the XML itself (MpdfRenderer, and CallbackRenderer/PassthroughRenderer through their post processor); for false Quittio passes the PDF to MpdfPostProcessor whenever the request asks for PDF/A or an embedded XML, so a plain HTML-to-PDF renderer is enough; RenderRequest holds html, title, author, subject, paper, pdfa, embedded (EmbeddedXml: content, filename, relationship, conformanceLevel, description, documentType, version) and font. The post processor imports the pages with FPDI; the result is PDF/A only if the source PDF allows it (embedded fonts, no encryption, no JavaScript), so check it with veraPDF.
A PDF is PDF/A-3b when it embeds XML, otherwise when pdf.pdfa is true (default) and it has no watermark. Its metadata: title “{title} {number}”, author the seller, creator “Quittio”.
mPDF settings of MpdfRenderer: margins 20 (left), 15 (right), 20 (top), 25 (bottom), header and footer 8 mm (the default templates set their own page margins), pdf.renderers.mpdf.simple_tables (default false; true drops the borders of table cells and is not faster), pdf.renderers.mpdf.pdfa_auto (default false; lets mPDF fix PDF/A issues itself), pdf.cache_path as temporary directory (without Laravel temp_dir, default sys_get_temp_dir()/quittio-mpdf). mPDF cannot load anything over HTTP (a denying HTTP client is installed), has no remote base path and does not follow redirects; remote resources of a template are rendered as missing.
mPDF rules for templates
- Layout with tables, not flexbox or grid.
- Inside table cells, mPDF ignores
marginandpaddingof blocks (div,p). Create spacing there with the padding of a table cell, e.g. a nested table with one row per block. - Position elements on the page with
position: absolute(measured from the page edge);position: fixedis measured from the page margins. The address field must be a direct child of<body>. - Repeated table headers via
<thead>. - Headers and footers via
<htmlpageheader>/<htmlpagefooter>, page numbers with{PAGENO}and{nbpg}. page-break-inside: avoidfor the totals block.- Images as PNG or JPEG; fonts must be embeddable.
HTML output and preview
->format('html') returns the rendered template without PDF ($result->html()); documents use ->htmlOnly(). The HTML is the same markup mPDF receives; the watermark is shown with CSS.
/_quittio/preview (local environment, or quittio.preview.enabled = true; path quittio.preview.path, middleware quittio.preview.middleware, not registered while routes are cached) lists the sample scenarios (simple, many-lines, long-texts, mixed-vat, reverse-charge, intra-eu, export, cancellation, sub-lines, foreign-currency, b2c-gross, allowances, attachments, peppol) and shows them as HTML or, with ?format=pdf, as PDF with a “PREVIEW” watermark, the configured design and validation in mode warn. ?template= accepts only default, the configured pdf.template and the views under quittio::templates. (not their partials), ?locale= a locale code; other values return 404. On the command line:
php artisan quittio:preview --scenario=many-lines --format=pdf --locale=en --output=/tmp/preview.pdf
The command writes to storage/app/quittio/preview/{scenario}.{format} by default; --template accepts the same templates as the route.
Mandatory field guard
After rendering a PDF or HTML invoice, the guard checks that the template printed every mandatory value:
- number and issue date;
- the full name of seller and buyer (
seller.nameor the address lines, which start with it; the trading namedisplayNamealone is not enough) and their address (all address lines, or street, postcode and city); - name, quantity and unit (
unit.label, section 14 (4) no. 5 UStG: “Menge und Art”) of every line (inlineGroupsorlines); - label, or base and rate, and the VAT amount of every VAT row;
- the cash discount terms (
paymentTerms.discounts[].textorpaymentTerms.lines), the amount due; - every legal note (exemption reasons and regulatory notes, e.g. “Gutschrift” or the section 14b UStG retention notice), every preceding invoice number;
- the tax representative (name, address, VAT identifier);
- what the country pack requires; Germany: tax number or VAT identifier (also of the tax representative), service date (
servicePeriod,deliveryDateordelivery.date, or the period of every line), the title and the note “Gutschrift” on self-billed invoices, the retention notice of property related services, the reverse charge or exemption note per VAT row, the VAT identifiers of buyer and seller for reverse charge and intra-community supplies (see Country packs).
Lists are checked per entry: printing only the first VAT row or note is not enough. Values that are null are not required. A value counts as printed when the template reads it as a leaf, also in a condition such as @if ($invoice->buyer->vatId) or while a loop reads the entries of a list, or with toArray(); count(), isEmpty(), isset() and reading null values do not count. Values printed through issuer and recipient count for the seller and the buyer, the generated footer counts as the issuer’s address; a custom senderLine or custom footerColumns do not replace the seller’s address.
Mandatory fields are Quittio\Core\Countries\MandatoryField objects (accessKey, description, legalBasis): the access key is a view model path where [*] stands for every index, & joins paths that must all be printed and | separates alternatives. Country packs return them from mandatoryOutput(InvoiceData $invoice).
A template that omits one fails with MandatoryFieldMissing (validation.mandatory_field_guard = exception, default outside production) or logs it (log, default in production); off disables the check. The event MandatoryFieldMissing is dispatched in both modes. Details in Validation.