# Quittio E-invoicing for Laravel. Quittio creates, validates, reads and renders electronic invoices based on the European standard EN 16931 – ZUGFeRD/Factur-X, XRechnung, UBL and Peppol BIS from one data model. [Get started](getting-started.md){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } [Formats and profiles](formats.md){: .btn .fs-5 .mb-4 .mb-md-0 } --- Germany is the first market; everything country-specific (VAT rates, legal texts, XRechnung) lives in country packs, so the core stays country-neutral. > **Status:** version 0.1, the first release. APIs may still change until version 1.0, and the legal wording of the country packs must be confirmed by a tax adviser. ## Packages | Package | Purpose | |---|---| | `quittio/core` | Country-neutral core: EN 16931 data model, calculation, profiles, CII and UBL writers/readers, ZUGFeRD/Factur-X PDF/A-3, validation. Usable without Laravel. | | `quittio/laravel` | Laravel integration: facade, configuration, Blade templates, commands, testing helpers. | | `quittio/country-de` | Country pack for Germany: UStG mandatory fields, tax presets, XRechnung profiles and BR-DE rules, DIN 5008 letter layouts. | | `quittio/quittio` | Everything in one package: core, Laravel integration and the country pack Germany. | | `quittio/managed` | Managed layer (commercial licence, not released yet): issuer profiles, drafts, gapless numbering, archive with hashes, cancellations and corrections, mail delivery, audit log, retention. | ## Requirements - PHP 8.3 or newer with `ctype`, `dom`, `gd`, `libxml`, `mbstring`, `xmlwriter`, `zlib`; `intl` recommended, `zip` for `quittio:schemas:download` - Laravel 13 (only for `quittio/laravel`) - Optional: Java 17+ for the KoSIT validator, veraPDF or Docker for PDF/A checks, `opis/json-schema` for JSON input validation ## Supported formats | Format | Content | |---|---| | `zugferd` / `factur-x` | PDF/A-3 with embedded CII XML (profiles MINIMUM to EXTENDED, XRECHNUNG) | | `xrechnung-cii`, `xrechnung-ubl` | XRechnung 3.0 as plain XML | | `cii`, `ubl` | Plain CII or UBL 2.1 XML in any profile | | `peppol` | UBL according to Peppol BIS Billing 3.0 | | `pdf`, `html`, `json` | PDF without XML, HTML preview, normalised data model | ## Quick example ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() // seller and bank account from config/quittio.php ->number('RE-2026-0042') ->deliveryDate('2026-09-30') ->buyer(Party::make('Kunde AG') // or a model: ->buyer($customer) ->vatId('DE987654321') ->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE')) ->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour) ->line('Hosting', netPrice: '49.00') // quantity 1, VAT 19 % by default ->dueInDays(14) ->render(); // ZUGFeRD: PDF/A-3 with embedded XML $result->pdf(); $result->save('invoices', '2026/'); $result->cancel('ST-2026-0001', reason: 'Charged twice'); // the cancellation, rendered ``` ## Documentation New to Quittio? Follow [Getting started](getting-started.md); new to e-invoicing? Read [E-invoicing basics](concepts.md) first. | Topic | Content | |---|---| | [Getting started](getting-started.md) | Installation, your company in the configuration, a first invoice, rendering, cancellation, use without Laravel | | [E-invoicing basics](concepts.md) | EN 16931, CII and UBL, ZUGFeRD, XRechnung, Peppol, profiles, rule codes, which format you need, German terms | | [Examples](examples.md) | Complete examples: service invoices, VAT rates, online shops, small businesses, XRechnung, customers abroad, final invoices, cancellations, orders, drafts, reminders, incoming invoices | | [Building invoices](building-invoices.md) | Every builder method: parties, lines, VAT, payment, references, calculation, cancellations, data model | | [Formats and profiles](formats.md) | Output formats, the automatic format choice, embedded files and XMP | | [Validation](validation.md) | What is checked (levels L1 to L7), error messages, KoSIT and veraPDF | | [Templates and design](templates.md) | Logo, colours, texts, partials, own templates, view model, letter layouts, fonts | | [Quotes and other documents](documents.md) | Quotes, order confirmations, delivery notes, proforma invoices, reminders, statements | | [Reading incoming invoices](reading.md) | XML and PDF input, findings, limits | | [Laravel integration](laravel.md) | Facade, rendering, storage, queue, events, mail, Blade templates, preview | | [Configuration](configuration.md) | Every key of `config/quittio.php` | | [Artisan commands](commands.md) | All commands and publish tags | | [Testing](testing.md) | Fake, assertions, factories, scenarios | | [Country packs](country-packs.md) | The German pack: tax presets, rules, XRechnung, reminders; writing a pack | | [Managed layer](managed.md) | Preview of the unreleased package: issuer profiles, customers, numbering, archive, cancellations and corrections, delivery, audit log, immutability, retention | | [Troubleshooting](troubleshooting.md) | Common errors with cause and fix | | [FAQ](faq.md) | Sending, the German e-invoicing obligation, Java, temporary files, queued invoices | | [Field reference](field-reference.md) | Every field of the data model with BT, cardinality and XML paths (generated) | | API reference | Every public class and method with its parameters and doc comment (generated) | | [AI assistants](ai.md) | llms.txt, the MCP server of this documentation, Laravel Boost guidelines and skill | ## Licence Open core: `quittio/core`, `quittio/laravel`, `quittio/country-de` and `quittio/quittio` are released under the MIT licence. The Managed layer `quittio/managed` will be commercially licensed; it is not released yet. # Getting started In this tutorial you install Quittio, enter your company once and then create, render and cancel a first invoice. It takes about 15 minutes. If e-invoices, ZUGFeRD or XRechnung are new to you, read [E-invoicing basics](concepts.md) first (5 minutes); you can also start right away and look terms up there. You need PHP 8.3 or newer and Laravel 13. Without Laravel, see [Without Laravel](#without-laravel). ## Installation ```bash composer require quittio/quittio # core, Laravel integration and country pack Germany php artisan quittio:install # publishes config/quittio.php ``` ## Set up your company Every invoice names the seller: your company. You enter it once in `config/quittio.php`; `Quittio::invoice()` then adds it to every invoice. The keys you need first: ```php // config/quittio.php (excerpt) 'seller' => [ 'name' => env('QUITTIO_SELLER_NAME'), // e.g. Muster GmbH 'vat_id' => env('QUITTIO_SELLER_VAT_ID'), // e.g. DE123456789 'tax_number' => env('QUITTIO_SELLER_TAX_NUMBER'), // e.g. 12/345/67890 'address' => ['line1' => 'Hauptstraße 1', 'postcode' => '12345', 'city' => 'Musterstadt', 'country' => 'DE'], 'contact' => ['name' => 'Max Muster', 'phone' => '+49 123 456789', 'email' => 'rechnung@muster.de'], 'bank_accounts' => [ ['iban' => 'DE02120300000000202051', 'bic' => 'BYLADEM1001', 'name' => 'Muster GmbH'], ], // ... ], ``` | Key | Needed | Why | |---|---|---| | `name`, `address` | Always | German invoices must name the seller with the full address | | `vat_id` or `tax_number` | Always | German invoices must show one of them; the VAT identification number (USt-IdNr.) is preferred | | `contact`, `bank_accounts` | Recommended | Printed on the invoice; required for XRechnung (public buyers). The first account with an IBAN is used | | `small_business` | Small businesses only | `true` (or `QUITTIO_SELLER_SMALL_BUSINESS=true`) marks every invoice as small business invoice without VAT (German Kleinunternehmer) | A seller without VAT identification number (e.g. a small business with a tax number only) also needs a trade register number (`legal_registration`) or another identifier (`identifiers`, as `[['value' => '…', 'scheme' => null]]`), because EN 16931 requires one of them (rule `BR-CO-26`). Every other key, such as logo, colours, default VAT and payment target, is explained in [Configuration](configuration.md) and in the comments of `config/quittio.php`. To see the PDF design with sample data: ```bash php artisan quittio:preview --scenario=simple # storage/app/quittio/preview/simple.pdf ``` ## Building an invoice Start an invoice with `Quittio::invoice()`, add what is specific to this invoice, and end with `render()`: ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() // your company from config/quittio.php ->number('RE-2026-0042') ->deliveryDate('2026-09-30') // date of supply, mandatory in Germany ->buyer(Party::make('Kunde AG') ->vatId('DE987654321') ->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE')) ->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour, description: 'Customer portal, sprint 3') ->line('Hosting', netPrice: '49.00', unit: UnitCode::Month) ->line('Handbook of VAT law', netPrice: '39.00', vat: VatPreset::Reduced) ->dueInDays(14)->cashDiscount(days: 7, percent: '2.00') ->render(); return $result->download(); // or $result->save('invoices', '2026/') ``` What the lines do: - `number()` is your invoice number. The invoice date is today unless you call `issueDate('2026-10-03')`. Gapless numbering is your application's job, or that of the [Managed layer](managed.md). - `deliveryDate()` is the date the goods were delivered or the service was performed. German invoices must state it, even when it is the invoice date. - `buyer()` is your customer. `Party::make()` takes the name; add the address and, for businesses, the VAT identification number. You can also pass your own customer model ([Application models](#application-models)). - `line()` adds a line with a name and named values. `netPrice` is the price per unit without VAT. Without `quantity` the quantity is 1; without `vat` the line gets the standard rate of your country (19 % in Germany). The third line uses the reduced rate of 7 %. - `dueInDays(14)` sets the payment target, `cashDiscount()` an early payment discount (Skonto). - `render()` calculates totals and VAT, checks the invoice, chooses the format and produces the PDF and the XML ([Rendering](#rendering)). Totals and VAT are always calculated by Quittio; you never pass them in. Amounts and quantities are strings (`'95.00'`), because floating point numbers cannot store every amount exactly; floats are rejected. Rounding follows EN 16931: half up, two decimals for amounts, VAT per rate. ### One way to write it Every part of an invoice has one usual form. The documentation and the examples use these forms; the other forms exist for special cases and are explained in [Building invoices](building-invoices.md). | You need | Write | |---|---| | A line with price, quantity, unit, VAT or description | `->line('Hosting', netPrice: '49.00', quantity: 12, unit: UnitCode::Month)`. Without `quantity` it is 1, without `vat` the [default VAT](building-invoices.md#default-vat) applies (19 % for a German seller). | | A line with more than that: item number, line discount, list price, period, sub lines, line group | `->line(Line::make('Pump', netPrice: '1450.00', quantity: 2)->sellerId('P-200'))`. `Line::make()` takes the same named values as `line()` and returns the line builder for the other methods. | | Many lines from your data | `->lines($order->items)` with models that implement `ProvidesInvoiceLine`, or `->lines($rows)` with arrays such as `['name' => 'Hosting', 'netPrice' => '49.00']` | | The customer | `->buyer($customer)` with a model that implements `ProvidesParty`, otherwise `->buyer(Party::make('Kunde AG')->address(…))` | | Payment terms | `->dueInDays(14)->cashDiscount(days: 7, percent: '2.00')` | | The result | `->render()` for PDF and XML, `->build()` for the data only, e.g. to store a draft | The seller comes from the configuration; set it in code only for another company (see [Starting invoices](laravel.md#starting-invoices)). References, notes, further parties, allowances and charges, direct debit, attachments, gross prices and all other options are described in [Building invoices](building-invoices.md); the German VAT presets (`VatPreset::Standard`, `Reduced`, `ReverseChargeEu` …) in [Tax presets](country-packs.md#tax-presets). ### Application models Your own models can provide invoice data through three contracts in `Quittio\Core\Contracts`: `ProvidesParty` (`fillParty(PartyBuilder $party)`, e.g. a customer as buyer), `ProvidesInvoiceLine` (`fillLine(LineBuilder $line)`, e.g. order items) and `ProvidesInvoiceData` (`toInvoiceData()`, e.g. an order for `Quittio::make($order)`). ```php use Quittio\Core\Builder\PartyBuilder; use Quittio\Core\Contracts\ProvidesParty; use Quittio\Laravel\Facades\Quittio; class User extends Authenticatable implements ProvidesParty { public function fillParty(PartyBuilder $party): void { $party->name($this->company ?? $this->name) ->vatId($this->vat_id) ->address($this->street, $this->postcode, $this->city, $this->country) ->electronicAddress($this->email, 'EM'); } } $invoice = Quittio::invoice() ->buyer($user) // ProvidesParty ->lines($order->items) // items implement ProvidesInvoiceLine // ... ->build(); ``` The invoice keeps a copy of these values; changing the model later does not change invoices that were already built. More in [Application models](building-invoices.md#application-models); to store invoices and link them to the user (`HasInvoices` trait, `$user->invoices()`), see [Customers and references](managed.md#customers-and-references) in the Managed layer. ## Rendering `render()` without argument chooses the format for you: for a German seller ZUGFeRD for businesses (a PDF with the XML inside), XRechnung for public authorities and a plain PDF for consumers ([Which format do I need?](concepts.md#which-format-do-i-need)). Name a format when a customer asks for one: ```php $result = Quittio::invoice()->buyer($user)->line('Consulting', netPrice: '120.00')->render('xrechnung-cii'); ``` The result holds everything that was produced: ```php $result->pdf(); // the PDF (PDF/A-3 with embedded XML for ZUGFeRD) $result->xml(); // the XML $result->filename(); // Rechnung_RE-2026-0042.pdf $result->validation(); // findings of the validation, including warnings $result->save('invoices', '2026/'); // to a filesystem disk and path return $result->download(); // or ->stream(), or return $result Mail::to($customer)->send(new YourInvoiceMail($result)); // your Mailable; the result is Attachable ``` Every render calculates, validates and renders. An invalid invoice throws a `ValidationException` that names the rule, the field and how to fix it ([Troubleshooting](troubleshooting.md)). For invoice data you built earlier (`->build()` instead of `->render()`), or for more control, use `Quittio::make()`: ```php use Quittio\Laravel\Facades\Quittio; $result = Quittio::make($invoice)->render(); // format chosen automatically $result = Quittio::make($invoice)->format('zugferd')->locale('en')->render(); // format and language $result = Quittio::make($invoice)->formats(['zugferd', 'xrechnung-cii'])->render(); // several formats at once $result->as('xrechnung-cii')->xml(); Quittio::make($invoice)->queue()->save('invoices', '2026/'); // render in a queue job ``` All formats and profiles are described in [Formats and profiles](formats.md); storage, downloads, queues, events and mail in [Laravel integration](laravel.md). ## Cancellation An issued invoice is never changed or deleted. To cancel it, you issue a cancellation invoice (German Stornorechnung): a new invoice with its own number that refers to the original and reverses its amounts. ```php $cancellation = $result->cancel('ST-2026-0001', reason: 'Charged twice'); // rendered, in the formats of $result ``` For an invoice you stored as data, build the cancellation and render it: ```php $cancellation = Quittio::make(Quittio::cancellation($original, 'ST-2026-0001', reason: 'Charged twice'))->render(); ``` `$original` is the issued, calculated invoice, e.g. `$result->data()` or `InvoiceData::fromJson()` of what you stored. In Germany the cancellation is a credit note with positive amounts by default (type code 381); `strategy: 'negative_invoice'` produces an invoice with negative amounts instead (type code 380, also `corrections.cancellation_strategy`). Both are permitted in Germany; the document is called "Stornorechnung" (cancellation invoice), never "Gutschrift" (which means a self-billed invoice in German law). `lines:` cancels part of an invoice (whole lines, quantities or amounts): ```php $part = $result->cancel('ST-2026-0002', lines: [ ['line' => '1', 'quantity' => '1'], ['line' => '2', 'amount' => '50.00'], ]); ``` When an invoice is cancelled in several parts, pass the earlier parts as `history:`, so that the VAT of all parts adds up exactly to the VAT of the original. Strategies, partial cancellations and the history are described in [Cancellations and corrections](building-invoices.md#cancellations-and-corrections); the Managed layer keeps the history itself ([Managed layer](managed.md#cancellations-and-corrections)). ## Without Laravel The core (`quittio/core`, plus a country pack such as `quittio/country-de`) works in any PHP application. Create an instance with `Quittio\Core\Quittio::create()` and build invoices with `InvoiceBuilder::make()`; there is no configuration file, so the seller is set in code and the default VAT is registered once: ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Core\Builder\Party; use Quittio\Core\Quittio; use Quittio\Core\Tax\CountryDefaultVat; use Quittio\Country\DE\CountryDE; $quittio = Quittio::create([ 'validation' => ['mode' => 'error', 'levels' => ['L1', 'L2', 'L3', 'L4', 'L7']], 'pdf' => ['b2c_embed_xml' => false], ], [new CountryDE]); InvoiceBuilder::useDefaultVat(new CountryDefaultVat($quittio->countries())); // lines without vat(): 19 % $invoice = InvoiceBuilder::make() ->seller(Party::make('Muster GmbH')->vatId('DE123456789')->address('Hauptstraße 1', '12345', 'Musterstadt', 'DE')) // ... as above, without Quittio::invoice() ->build(); $result = $quittio->make($invoice)->format('zugferd')->render(); file_put_contents($result->filename(), $result->pdf()); // or $result->save('/path/to/dir') $cancellation = $quittio->cancellation($result->data(), 'ST-2026-0001', reason: 'Charged twice'); ``` `Quittio::create(Config|array $config = [], iterable $countryPacks = [], ?PdfRenderer $renderer = null, ?TemplateEngine $templates = null, ?LoggerInterface $logger = null, ?FormatResolver $formatResolver = null)` takes the configuration as array or as `Quittio\Core\Config` object (`new Config(...)`, `Config::fromArray([...])`, `$config->with([...])`). The array keys are flatter than those of the Laravel `config/quittio.php`; the Laravel integration translates its configuration itself: | Key | Default | Laravel key | |---|---|---| | `format` | `null` (automatic choice; also `'auto'`) | `defaults.format` | | `profile` | `en16931` | `defaults.profile` | | `locale`, `country` | `null` (country pack default, seller country) | `defaults.locale`, `defaults.country` | | `due_days` | `null` | `defaults.due_days` | | `template`, `template_paths` | `default`, `[]` (directories with `{name}.php`) | `pdf.template` (Blade view) | | `filename` | `{title}_{number}.{ext}` | `pdf.filename` | | `pdf.pdfa`, `pdf.b2c_embed_xml`, `pdf.girocode_amount` | `true`, `false`, `due` | `pdf.pdfa`, `format_resolver.b2c_embed_xml`, `pdf.payment_qr.girocode_amount` | | `auto_upgrade_profile` | `true` | `format_resolver.auto_upgrade_profile` | | `validation.mode` | `error` (`warn`, `off`) | `validation.mode` | | `validation.levels` | `['L1', 'L2', 'L3', 'L4', 'L7']` | `validation.levels` (`php`, `xsd`) | | `validation.block_on_warnings` | `false` | `validation.block_on_warnings` | | `validation.locale` | `null` (the language of the invoice; `'app'` = the language of the translator; or a locale) | `validation.locale` (default: the language of the application) | | `validation.kosit`, `validation.verapdf` | disabled; `['enabled' => true, 'mode' => …, …]` enables L5 and L6 (KoSIT scenario sets only in `configurations`) | `validation.levels` (`kosit`, `verapdf`) and `validation.drivers.*` | | `mandatory_fields` | `exception` (`log`, `off`) | `validation.mandatory_field_guard` | | `temp_dir` | system temp directory | `pdf.cache_path` | | `options` | `[]` (e.g. `paper`, `markdown`, `fold_marks`, `watermark`) | `pdf.paper`, `pdf.markdown`, `pdf.fold_marks` | | `presentation` | `null` (a `Presentation` object with the design defaults) | `pdf.*` design keys | | `reader.max_xml_bytes`, `reader.max_pdf_bytes`, `reader.max_decompressed_bytes` | 10, 25 and 50 MB | `reader.*` | | `corrections.cancellation_strategy` | `null` (country pack default) | `corrections.cancellation_strategy` | The validation keys are described in [Validation](validation.md), the output keys in [Output configuration](formats.md#output-configuration), the Laravel keys in [Configuration](configuration.md). The instance offers the methods of the facade: `make()`, `edit()` (continue existing invoice data), `cancellation()`, `document()`, `read()`, `readFile()`, `validateFile()` and `validatePath()`, plus `registerCountry()`, `setRenderer()`, `setTemplateEngine()`, `setFormatResolver()`, `setPostProcessor()` (PDF/A and XML embedding for renderers that cannot embed), `addSerializer()`, `addValidator()`, `clearValidators()`, `configureRules()` and `ruleCodes()` ([switching rules off](validation.md#switching-rules-off-and-changing-their-severity)) and `onMandatoryFieldsMissing()`. A custom `Quittio\Core\Calculation\TotalsCalculator`, passed to `Quittio::create(calculator: …)` (or the constructor), calculates render runs, documents, read invoices, edits and cancellations; `calculator()` returns it. Without one, the EN 16931 calculator applies the calculation policy of `calculation.declared_amounts` ([Declared amounts](building-invoices.md#declared-amounts)). The core uses plain PHP templates (`resources/templates/default.php`); pass your own `TemplateEngine` to `Quittio::create()` for Twig or others. ## JSON `InvoiceData::toArray()`, `toJson()`, `fromArray()` and `fromJson()` convert the model losslessly. The JSON Schema is shipped in `packages/core/resources/schema/invoice.schema.json`; with `opis/json-schema` installed, `(new JsonSchemaValidator)->validate($json)` checks input before hydration. Details in [Arrays and JSON](building-invoices.md#arrays-and-json); all fields are listed in the [field reference](field-reference.md). ## Next steps - [Examples](examples.md): complete invoices for common situations, from small businesses and XRechnung to customers abroad and reminders. - [Building invoices](building-invoices.md): every builder method. - [Templates and design](templates.md): logo, colours, texts and your own template. - [Validation](validation.md) and [Troubleshooting](troubleshooting.md): what the checks do and how to fix errors. - [Laravel integration](laravel.md): storage, queues, events and mail. # E-invoicing basics This page explains the terms you meet in Quittio and in this documentation, in about five minutes. You do not need to know them to render a first invoice ([Getting started](getting-started.md)), but they explain why Quittio asks for certain data and what the formats and error codes mean. ## What an e-invoice is An e-invoice is an invoice in a structured, machine-readable format: an XML file that accounting software can read without a person typing the values. A PDF that only shows the invoice is not an e-invoice, even when it is sent by e-mail. Many countries now require e-invoices between businesses. In Germany every domestic business must be able to receive them since 2025; sending them becomes mandatory step by step until 2028, with exceptions such as invoices up to 250 € ([German country pack](country-packs.md#germany-quittiocountry-de)). Invoices to consumers are not affected. Quittio produces both parts from one set of data: the XML for the software and, where useful, a PDF for people. ## EN 16931 and business terms **EN 16931** is the European standard that defines what an e-invoice contains: seller, buyer, lines, VAT, totals, payment and so on. Every national format in the EU is based on it. The standard gives every field a number: | Abbreviation | Meaning | Example | |---|---|---| | BT-*n* | Business term: a single field | BT-1 invoice number, BT-27 seller name, BT-10 buyer reference | | BG-*n* | Business group: a group of fields | BG-5 seller address, BG-25 invoice line | This documentation names these numbers in parentheses, e.g. "buyer reference (BT-10)", because validation messages and the formats use them. You never type them in your code: the builder methods have plain names (`buyerReference()`). The [Field reference](field-reference.md) lists every field with its number. ## Syntaxes: CII and UBL EN 16931 describes the content, not the file. Two XML **syntaxes** write it down: | Syntax | Full name | Used by | |---|---|---| | CII | UN/CEFACT Cross Industry Invoice | ZUGFeRD/Factur-X, XRechnung | | UBL | OASIS Universal Business Language 2.1 | Peppol, XRechnung | Both carry the same information. Which one you need depends on the format the recipient expects. ## Formats A **format** is what Quittio produces. The common ones: | Format | What it is | Typical recipient | |---|---|---| | ZUGFeRD / Factur-X | A PDF (PDF/A-3) with the CII XML embedded: people read the PDF, software reads the XML. ZUGFeRD (Germany) and Factur-X (France) are the same standard. | Businesses | | XRechnung | The German standard for public authorities: an XML file only, CII or UBL, with additional German rules | Public buyers in Germany | | Peppol BIS Billing | UBL XML sent over the Peppol network, an international delivery network for e-invoices | Businesses and authorities connected to Peppol | | CII, UBL | Plain XML in either syntax | Recipients that ask for it | | PDF | A PDF without XML (PDF/A-3 for archiving) | Consumers, small amount invoices, countries without an obligation | [Formats and profiles](formats.md) lists every format with its keys (`zugferd`, `xrechnung-cii` …). ## Profiles A **profile** decides how much of EN 16931 an XML file contains and which extra rules apply. ZUGFeRD/Factur-X has the levels MINIMUM, BASIC WL, BASIC, EN 16931 and EXTENDED; XRechnung and Peppol are profiles with national or network rules on top of EN 16931. | Profile | Use | |---|---| | `en16931` | The standard itself; the default for business invoices | | `extended` | More fields than EN 16931 (e.g. sub lines); Quittio switches to it automatically when you use them | | `xrechnung` | XRechnung rules (public buyers in Germany) | | `peppol` | Peppol BIS Billing 3.0 | | `minimum`, `basic-wl`, `basic` | Reduced levels without (all) lines; not valid e-invoices under EN 16931, only for recipients that ask for them | You rarely choose a profile yourself: each format has a default, and the [automatic choice](formats.md#automatic-choice) picks format and profile from the invoice. ## Rules and validation The standards come with **business rules**: conditions every invoice must meet, such as "the sum of the line amounts equals the net total" or "a seller address is required". They have codes, and Quittio's error messages show them: | Code | Source | |---|---| | `BR-…`, `BR-CO-…`, `BR-CL-…` | EN 16931 (e.g. `BR-CO-26`: the seller needs a VAT identifier or another identifier) | | `BR-DE-…` | XRechnung (Germany) | | `PEPPOL-…` | Peppol BIS Billing | | `DE-USTG-…`, `DE-…` | Quittio's German country pack: mandatory content of German invoices under the VAT Act (UStG) | | `QUITTIO-…` | Quittio itself | Quittio checks an invoice before it renders it. An error stops the rendering with a `ValidationException` that names the rule, the field and a hint; warnings are reported but do not stop it. Optional external validators (the official KoSIT validator for XRechnung and EN 16931, veraPDF for PDF/A) check the finished files. See [Validation](validation.md) and, for common errors, [Troubleshooting](troubleshooting.md). ## Which format do I need? Usually none: without `->format()`, Quittio chooses for each invoice ([Automatic choice](formats.md#automatic-choice)). For a German seller: | Your customer | Format Quittio chooses | Why | |---|---|---| | A business in Germany | ZUGFeRD (profile EN 16931) | E-invoice that is also a readable PDF | | A public authority in Germany (buyer reference is a Leitweg-ID) | XRechnung (CII) | Required by public buyers | | A consumer | PDF | No e-invoice obligation | | Any customer in Germany, invoice up to 250 € marked with `->de()->smallAmount()` | PDF | Small amount invoices are exempt | | A business in another EU country | ZUGFeRD (profile EN 16931) | Readable PDF with standard XML | | A business reachable via Peppol (with a Peppol lookup) | Peppol BIS | Delivery over the Peppol network | | A customer outside the EU | PDF | No EU e-invoice rules | Ask for a format with `->render('xrechnung-cii')` or `->format(…)` when a customer requires a specific one. ## How Quittio works ```text config/quittio.php ──► Quittio::invoice() ──► …->line(…)->buyer(…) ──► render() (your company) (invoice builder) (your invoice data) │ ▼ calculation → format and profile → validation → XML and PDF → result │ $result->pdf(), ->xml(), ->save(), ->download() ``` 1. **Configuration:** your company (the seller), bank account and defaults are set once in `config/quittio.php` ([Configuration](configuration.md)). 2. **Building:** `Quittio::invoice()` starts an invoice with that seller; you add the number, customer, lines and payment terms ([Building invoices](building-invoices.md)). 3. **Rendering:** `render()` calculates totals and VAT, chooses format and profile, validates, writes the XML and renders the PDF from a template ([Templates and design](templates.md)). You never calculate totals yourself. 4. **Result:** a `RenderedInvoice` with the PDF, the XML, a file name and the validation result, which you save, download or mail ([Laravel integration](laravel.md)). Everything country-specific, such as German VAT rates, legal texts, XRechnung and the DIN 5008 letter layout, comes from a **country pack** ([Country packs](country-packs.md)); `quittio/quittio` includes the one for Germany. The optional **Managed layer** (not released yet) adds gapless numbering, an archive, cancellations and corrections as stored records ([Managed layer](managed.md)). ## German terms Quittio prints German invoices with German wording, and some German legal terms have no exact English equivalent. This documentation keeps the German term where it is the text on the invoice or the legal name, with an English explanation: | German | English | Note | |---|---|---| | Rechnung | Invoice | | | Gutschrift | Self-billed invoice | Issued by the buyer for the seller's supply (type code 389), not a refund | | Rechnungskorrektur | Invoice correction | Title of credit notes (type code 381) and of corrections issued with the German pack (corrected invoice, type code 384) | | Korrigierte Rechnung | Corrected invoice | Title of type code 384 outside the German correction policy | | Stornorechnung | Cancellation invoice | Cancels an issued invoice | | Anzahlungsrechnung, Abschlagsrechnung, Schlussrechnung | Advance payment invoice, partial invoice, final invoice | | | Angebot, Auftragsbestätigung, Lieferschein | Quote, order confirmation, delivery note | [Documents](documents.md) | | Zahlungserinnerung, Mahnung | Payment reminder, reminder | "1. Mahnung" is the first reminder after the payment reminder | | Leistungsdatum, Leistungszeitraum | Date of supply, period of supply | Mandatory on German invoices | | Skonto | Cash discount | | | Übertrag | Carried forward | Running total at a page break | | USt-IdNr. | VAT identification number | e.g. DE123456789 | | Steuernummer (St.-Nr.) | Tax number | Issued by the local tax office | | Kleinunternehmer | Small business | Charges no VAT (section 19 UStG) | | Steuerschuldnerschaft des Leistungsempfängers | Reverse charge | The buyer owes the VAT (section 13b UStG) | | Innergemeinschaftliche Lieferung | Intra-community supply | Tax-exempt supply of goods to a business in another EU country | | Leitweg-ID | Routing ID of a German public buyer | Goes into the buyer reference (BT-10) of an XRechnung | | UStG | German VAT Act (Umsatzsteuergesetz) | | | GoBD | German rules for digital bookkeeping and retention | | | KoSIT | Agency that publishes XRechnung and its validator | | | DIN 5008 | German standard for business letters | Address field and fold marks of the PDF | # Examples This page shows complete examples for common situations, from a first service invoice to reading an e-invoice you received. Find the situation closest to yours, copy the code and adapt it. If you have not rendered an invoice yet, start with [Getting started](getting-started.md). Terms such as ZUGFeRD, XRechnung or profile are explained in [E-invoicing basics](concepts.md). Every example runs in Quittio's test suite against the current version. The XML the examples produce passed the official validator of KoSIT, the German agency that publishes XRechnung. It checks the rules of EN 16931, XRechnung and Peppol BIS Billing 3.0 ([Rules and validation](concepts.md#rules-and-validation)). Each example starts with the situation, then shows the code, then explains the important lines with links to the reference. | Example | Situation and what it shows | |---|---| | [Service invoice](#service-invoice) | Hours and hosting for a German business: service period, cash discount, ZUGFeRD | | [Different VAT rates and a discount](#different-vat-rates-and-a-discount) | Goods at 7 % and 19 %: lines as arrays, a discount split per VAT rate | | [Online shop](#online-shop) | A consumer who paid at checkout: prices including VAT, a voucher | | [Small business](#small-business) | A seller who charges no VAT (section 19 UStG, the German VAT Act) and has no VAT identification number | | [Public sector](#public-sector) | A German public authority: XRechnung with Leitweg-ID (the buyer's routing ID), PDF copy | | [Advance payment and final invoice](#advance-payment-and-final-invoice) | A project paid in two parts: advance payment invoice, final invoice with deduction | | [Cancellation](#cancellation) | An issued invoice that was wrong: full and partial cancellation with `$result->cancel()` | | [Self-billing](#self-billing) | You write the invoice for your supplier: "Gutschrift" (self-billed invoice) | | [EU business: services](#eu-business-services) | A service for a business in Belgium: reverse charge (the customer owes the VAT), Peppol BIS | | [EU business: goods](#eu-business-goods) | Goods for a business in Austria: intra-community supply, deliver-to address | | [Export](#export) | A customer outside the EU, invoiced in US dollars | | [Foreign currency with German VAT](#foreign-currency-with-german-vat) | A German customer invoiced in US dollars: VAT stated in euro | | [Invoices for orders](#invoices-for-orders) | Orders in your Laravel application: Eloquent models, storing and mailing | | [Drafts and previews](#drafts-and-previews) | Invoices prepared over time: drafts and previews without the Managed layer | | [Testing](#testing) | Feature tests for the order flow: fake and validation in Pest | | [From quote to invoice](#from-quote-to-invoice) | A quote with an optional line, then the invoice from the quote | | [Payment reminder](#payment-reminder) | An overdue invoice: reminder with statutory default interest | | [Incoming e-invoices](#incoming-e-invoices) | An e-invoice from a supplier: validate and read an upload | | [Logo, colours and texts](#logo-colours-and-texts) | The design of a single invoice | | [Without Laravel](#without-laravel) | A plain PHP script | All examples use the Laravel facade and this seller in `config/quittio.php` ([Seller](configuration.md#seller)): ```php // config/quittio.php 'seller' => [ 'name' => 'Muster GmbH', 'vat_id' => 'DE123456789', 'address' => ['line1' => 'Hauptstraße 1', 'postcode' => '12345', 'city' => 'Musterstadt', 'country' => 'DE'], 'contact' => ['name' => 'Max Muster', 'phone' => '+49 30 1234567', 'email' => 'rechnung@muster.de'], 'electronic_address' => ['value' => 'rechnung@muster.de', 'scheme' => 'EM'], 'bank_accounts' => [['iban' => 'DE02120300000000202051', 'bic' => 'BYLADEM1001', 'name' => 'Muster GmbH']], ], ``` How the examples are written: - `Quittio::invoice()` starts an invoice with this seller and its bank account. `->render()` at the end of the chain calculates, validates and renders it ([Starting invoices](laravel.md#starting-invoices)). `Quittio::quote()`, `Quittio::paymentReminder()` and the other document methods work the same way. - The examples follow [one way to write it](getting-started.md#one-way-to-write-it). A line is its name with named values: `->line('Hosting', netPrice: '49.00', quantity: 12, unit: UnitCode::Month)`. Without `quantity` the quantity is 1. Without `vat` the line gets 19 %, the standard rate for a German seller ([Default VAT](building-invoices.md#default-vat)), so the examples set VAT only where it differs. - `Line::make('…', netPrice: …)->…` appears only where a line needs more than these values. The example says why. - Lines from your data appear as arrays or models, the customer as `Party::make()` or as your model, payment terms as `->dueInDays()` and `->cashDiscount()`. - Amounts are strings such as `'49.00'`, never floats. - `invoices` and `documents` are filesystem disks of your application (`config/filesystems.php`). - The seller is German, so the documents are in German. A comment such as `// 2026/Rechnung_RE-2026-0101.pdf` shows the German file name ("Rechnung" means invoice). `->locale('en')` switches texts and file names to English, see [EU business: services](#eu-business-services). - Without Laravel, the same builder works with the core instance ([Without Laravel](#without-laravel)). ## Invoices in Germany These examples invoice customers in Germany. Read the first one even if your case differs: it explains what all examples have in common, and the [Cancellation](#cancellation) and [Incoming e-invoices](#incoming-e-invoices) examples reuse it. ### Service invoice You bill a German business customer for a month of work: hours of software development and a monthly hosting fee. The example shows a service period, payment terms with a cash discount and the format Quittio chooses for German businesses. ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('RE-2026-0101') ->issueDate('2026-10-01') ->invoicingPeriod('2026-09-01', '2026-09-30') // service period instead of a delivery date ->buyerReference('PO-4711') // the reference your customer asked for (BT-10) ->buyer(Party::make('Kunde AG') ->vatId('DE987654321') ->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE') ->electronicAddress('ap@kunde.de', 'EM')) ->line('Software development', netPrice: '95.00', quantity: '62.5', unit: UnitCode::Hour, description: 'Customer portal, sprints 5 and 6') ->line('Hosting', netPrice: '49.00', unit: UnitCode::Month) ->dueInDays(30)->cashDiscount(days: 10, percent: '2.00') ->render(); // ZUGFeRD, profile EN 16931 $result->save('invoices', '2026/'); // 2026/Rechnung_RE-2026-0101.pdf ``` - Without a format, Quittio chooses one from the invoice ([Automatic choice](formats.md#automatic-choice)). For a German business it renders ZUGFeRD: a PDF/A-3 file (a PDF for archiving) with the e-invoice XML embedded. The XML follows the profile EN 16931 ([Formats](concepts.md#formats), [Profiles](concepts.md#profiles)). - German invoices must state when the service was provided. `invoicingPeriod()` states the service period (September) instead of a single delivery date. - Both lines get 19 % VAT by default. Hosting has no quantity, so it is 1 month. - Totals: 5,986.50 net, 1,137.44 VAT, 7,123.94 due on 31 October 2026 (30 days after the issue date). - `dueInDays()` and `cashDiscount()` become the payment terms. PDF and XML state them in German: "Zahlbar innerhalb von 30 Tagen ohne Abzug." (payable within 30 days without deduction) and "2 % Skonto bei Zahlung innerhalb von 10 Tagen (bis 11.10.2026)." (2 % cash discount for payment within 10 days, until 11 October 2026). See [Payment terms](building-invoices.md#payment-terms). - `save()` stores the file on the disk `invoices` below `2026/` and returns its path. The file name starts with the German title "Rechnung" (invoice). ### Different VAT rates and a discount You sell goods with different VAT rates: books at the reduced rate of 7 % and office supplies at the standard rate of 19 %. You grant a discount of 5 % on everything. The example passes the lines as arrays, as they might come from your database, and prints the VAT breakdown: ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('RE-2026-0102') ->issueDate('2026-10-05') ->deliveryDate('2026-10-02') ->buyer(Party::make('Kanzlei Recht & Partner') ->vatId('DE987654321') ->address('Marktplatz 2', '54321', 'Beispielstadt', 'DE')) ->lines([ ['name' => 'Handbook of VAT law', 'quantity' => [3, UnitCode::Piece], 'netPrice' => '89.00', 'vat' => VatPreset::Reduced], ['name' => 'Desk organiser', 'quantity' => [3, UnitCode::Piece], 'netPrice' => '24.90'], ]) ->discountPercent('5', 'Customer discount') // one allowance per VAT rate ->render(); foreach ($result->data()->vatBreakdown as $row) { echo $row->rate.' %: '.$row->taxableAmount.' net, '.$row->taxAmount." VAT\n"; } ``` ``` 7 %: EUR 253.65 net, EUR 17.76 VAT 19 %: EUR 70.96 net, EUR 13.48 VAT ``` - Each array key names a method of the line builder, and the value holds its arguments: `'quantity' => [3, UnitCode::Piece]` means 3 pieces ([Lines](building-invoices.md#lines)). - `VatPreset::Reduced` is a tax preset of the German country pack: the reduced rate of 7 % ([Tax presets](country-packs.md#tax-presets)). The desk organiser has no `vat`, so it gets 19 %. - VAT is calculated per VAT rate, never per line. So `discountPercent()` splits the discount of 17.09 into one allowance per rate, in proportion to the line sums: 13.35 at 7 % and 3.74 at 19 % ([Discount splitting](building-invoices.md#discount-splitting)). - For a discount on a single rate, use `allowance(VatCategory::StandardRate, '19', percent: '5', reason: '…')` instead ([Document level allowances and charges](building-invoices.md#document-level-allowances-and-charges)). ### Online shop You sell to consumers in an online shop. Your prices include VAT, and the customer pays at checkout, here with PayPal. The example shows prices including VAT, shipping, a voucher and an invoice that is already paid. ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\PaymentMeansCode; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('RE-2026-0103') ->issueDate('2026-10-06') ->deliveryDate('2026-10-06') ->buyer(Party::make('Erika Mustermann') ->address('Lindenweg 3', '10115', 'Berlin', 'DE') ->consumer()) // a consumer (B2C): PDF without XML ->line('T-shirt "Quittio"', priceInclVat: '19.99', quantity: 3) ->line('Shipping', priceInclVat: '4.90') ->line('Voucher WELCOME5', priceInclVat: '5.00', quantity: -1) ->payment(PaymentMeansCode::OnlinePaymentService, meansText: 'PayPal') ->prepaid('59.87') // paid at checkout ->render(); $result->format(); // "pdf" $result->data()->totals->grossTotal; // EUR 59.87 $result->data()->totals->amountDue; // EUR 0.00 ``` - `consumer()` marks the buyer as a consumer. Invoices to consumers need no e-invoice, so the automatic choice renders a PDF without XML ([Which format do I need?](concepts.md#which-format-do-i-need)). - `priceInclVat()` takes the price including VAT. The calculator derives the net prices, so that the invoice total is exactly the amount the customer paid: 3 × 19.99 + 4.90 − 5.00 = 59.87, of which 9.56 VAT. - In this gross price mode every line uses `priceInclVat()`. Shipping and vouchers are lines (the voucher with quantity −1), not charges or allowances ([Gross price mode](building-invoices.md#gross-price-mode)). - `payment()` states how the customer paid. `prepaid()` records the payment at checkout, so the amount due is 0.00 and the PDF shows no payment QR code. ### Small business You run a small business ("Kleinunternehmer") under section 19 UStG, the German VAT Act, and charge no VAT. You state this once, in the configuration. The seller in this example has a tax number but no VAT identification number: ```php // config/quittio.php of the small business 'seller' => [ 'name' => 'Lisa Klein Grafikdesign', 'tax_number' => '21/815/08150', 'identifiers' => [['value' => '21/815/08150', 'scheme' => null]], // seller identifier (BT-29): needed without a VAT identifier 'address' => ['line1' => 'Gartenstraße 7', 'postcode' => '12345', 'city' => 'Musterstadt', 'country' => 'DE'], 'electronic_address' => ['value' => 'hallo@lisaklein.de', 'scheme' => 'EM'], 'bank_accounts' => [['iban' => 'DE89370400440532013000', 'name' => 'Lisa Klein']], 'small_business' => true, // or QUITTIO_SELLER_SMALL_BUSINESS=true ], ``` Then you write every invoice as usual, without VAT on the lines: ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('2026-017') ->issueDate('2026-10-07') ->buyer(Party::make('Café Sonnenschein GmbH')->address('Am Markt 3', '12345', 'Musterstadt', 'DE')) ->line('Logo design', netPrice: '650.00', unit: UnitCode::LumpSum) ->line('Business card layout', netPrice: '55.00', quantity: 2, unit: UnitCode::Hour) ->dueInDays(14) ->render(); $result->data()->totals->amountDue; // EUR 760.00, no VAT ``` - `seller.small_business` makes `Quittio::invoice()` flag the invoice as a small business invoice (`Meta::FLAG_SMALL_BUSINESS`). Lines without `vat()` get `VatPreset::SmallBusiness`, and the German country pack rejects VAT on the invoice (rule `DE-USTG-09`). - Only an invoice with another seller than the configured one needs `->flag(Meta::FLAG_SMALL_BUSINESS)`. - Every line carries the exemption text "Steuerfreie Leistung nach § 19 Abs. 1 UStG (Steuerbefreiung für Kleinunternehmer)." (tax-exempt supply under section 19 (1) UStG, exemption for small businesses) in PDF and XML. - Invoices of a small business need no service date (section 34a UStDV, the German VAT implementing ordinance). They need the tax number or the VAT identification number ([Small businesses](country-packs.md#small-businesses)). - Without a VAT identification number, EN 16931 needs a seller identifier (BT-29, `seller.identifiers`) or a trade register number (BT-30, `seller.legal_registration`). Otherwise rendering fails with the rule `BR-CO-26` ([Rules and validation](concepts.md#rules-and-validation)). Here the tax number serves as the seller identifier. ### Public sector You invoice a German public authority, here a city administration. German public buyers require XRechnung, an e-invoice that is an XML file only. With the order they give you a Leitweg-ID, the routing ID of the buyer ([German terms](concepts.md#german-terms)). The example shows where the Leitweg-ID goes and how to add a readable PDF copy. ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('RE-2026-0104') ->issueDate('2026-10-08') ->deliveryDate('2026-10-02') ->buyerReference('04011000-12345-03') // Leitweg-ID from the order ->buyer(Party::make('Stadt Beispielstadt, Hauptamt') ->address('Rathausplatz 1', '54321', 'Beispielstadt', 'DE') ->electronicAddress('04011000-12345-03', '0204')) // scheme 0204: Leitweg-ID ->line('Website maintenance', netPrice: '85.00', quantity: 8, unit: UnitCode::Hour) ->dueInDays(30) ->render(['xrechnung-cii', 'pdf']); // XRechnung and a PDF copy $result->save('invoices', '2026/'); // 2026/Rechnung_RE-2026-0104.xml, the e-invoice $result->as('pdf')->save('invoices', '2026/'); // 2026/Rechnung_RE-2026-0104.pdf, a readable copy ``` - The Leitweg-ID goes into the buyer reference (BT-10). With a Leitweg-ID there, the automatic choice selects XRechnung in the CII syntax (`xrechnung-cii`) by itself ([Syntaxes](concepts.md#syntaxes-cii-and-ubl)). The buyer's electronic address is the Leitweg-ID as well, with the scheme `0204`. - `render()` without a format would render `xrechnung-cii` alone. The list `['xrechnung-cii', 'pdf']` renders both formats from the same calculation, like `->formats([...])`. The first format is the main result, and `as('pdf')` returns the PDF. The XML is the invoice, the PDF only a readable copy. - XRechnung requires the seller contact with name, phone and email, the electronic addresses of both parties and payment instructions. The seller configuration at the top of this page provides them ([XRechnung](formats.md#xrechnung)). - Quittio creates the file but does not send it. Upload it to the portal the authority names, or send it via Peppol. ### Advance payment and final invoice You agree on a project with an advance payment: you invoice part of the price when the order is placed and the rest after completion. The example shows the advance payment invoice and the final invoice that deducts it. ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\InvoiceTypeCode; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $customer = Party::make('Kunde AG') ->vatId('DE987654321') ->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE'); // Advance payment invoice when the order is placed $advance = Quittio::invoice() ->type(InvoiceTypeCode::PrepaymentInvoice) ->number('RE-2026-0090') ->issueDate('2026-09-01') ->invoicingPeriod('2026-09-01', '2026-10-31') // planned period of the service ->buyer($customer) ->line('Website relaunch, advance payment', netPrice: '1000.00', unit: UnitCode::LumpSum) ->dueInDays(14) ->render(); // Final invoice: the whole service with its whole VAT, minus the advance payment received $final = Quittio::invoice() ->number('RE-2026-0110') ->issueDate('2026-10-14') ->deliveryDate('2026-10-10') ->buyer($customer) ->line('Website relaunch', netPrice: '3000.00', unit: UnitCode::LumpSum) ->de()->deductAdvance($advance->data()) ->dueInDays(14) ->render(); $final->data()->totals->grossTotal; // EUR 3570.00 $final->data()->totals->prepaidAmount; // EUR 1190.00 $final->data()->totals->amountDue; // EUR 2380.00 ``` - `type(InvoiceTypeCode::PrepaymentInvoice)` sets the invoice type code 386 (BT-3, the kind of invoice). The PDF is titled "Anzahlungsrechnung" (advance payment invoice). - The advance payment invoice states the planned period of the service. Without a date, the German country pack warns (`DE-USTG-03`). - The final invoice states the whole service with its whole VAT. `deductAdvance()` refers to the advance payment invoice and deducts its gross amount ([Final invoices](country-packs.md#final-invoices-deductadvance)). - It also adds the note "Abzüglich Anzahlung gemäß Rechnung RE-2026-0090 vom 01.09.2026: Entgelt 1.000,00 € zzgl. 19 % Umsatzsteuer 190,00 € (§ 14 Abs. 5 Satz 2 UStG)." (less the advance payment of invoice RE-2026-0090 of 1 September 2026: net amount 1,000.00 € plus 19 % VAT 190.00 €, section 14 (5) sentence 2 UStG). - Call `deductAdvance()` once per advance payment received. It does not check whether the advance invoice was paid. ### Cancellation An issued invoice is never changed. If it is wrong, you issue a cancellation: a new document with its own number that refers to the original. The example cancels the [service invoice](#service-invoice) from above, first completely and then in part. `$result` is that invoice; `cancel()` renders the cancellation in the formats of the invoice. ```php // Cancel the whole invoice $cancellation = $result->cancel('ST-2026-0007', reason: 'Ordered twice'); $cancellation->filename(); // Stornorechnung_ST-2026-0007.pdf // Or cancel only part of it: 10 of the 62.5 hours of line 1 $partial = $result->cancel('ST-2026-0008', reason: 'Hours booked twice', lines: [ ['line' => '1', 'quantity' => '10'], ]); $partial->data()->totals->grossTotal; // EUR 1130.50 ``` - In Germany a cancellation is a credit note (type code 381) with positive amounts. It is titled "Stornorechnung" (cancellation invoice). - It refers to RE-2026-0101 and carries the note "Stornierung der Rechnung RE-2026-0101 vom 01.10.2026. Grund: Ordered twice" (cancellation of invoice RE-2026-0101 of 1 October 2026, reason: …). - The partial cancellation reverses 10 hours: 950.00 net plus 180.50 VAT. `['line' => '1', 'amount' => '100.00']` reduces a line by an amount instead, e.g. after a complaint. - For an invoice you stored as data, `Quittio::make(Quittio::cancellation($data, 'ST-2026-0007', reason: '…'))->render()` does the same ([Cancellations](laravel.md#cancellations)). - For several partial cancellations of one invoice, pass the earlier ones as `history:`, so that the VAT adds up exactly ([Cumulative VAT rounding](building-invoices.md#cumulative-vat-rounding)). The [Managed layer](managed.md#cancellations-and-corrections), the optional package `quittio/managed`, keeps this history itself. ### Self-billing With self-billing, the buyer writes the invoice instead of the seller, e.g. the commission statement for your sales agent. In Germany this document is called "Gutschrift" (self-billed invoice); it is not a refund. The example shows who is seller and who is buyer, and where the payment goes. ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\PaymentMeansCode; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->de()->selfBilled() // type code 389 and the note "Gutschrift" ->number('GS-2026-0012') ->issueDate('2026-10-05') ->invoicingPeriod('2026-09-01', '2026-09-30') ->seller(Party::make('Jonas Schreiber Handelsvertretung') // the supplier: your sales agent ->vatId('DE111111125') ->address('Lindenallee 12', '80331', 'München', 'DE')) ->buyer(Party::make('Muster GmbH') // you: you issue the document ->vatId('DE123456789') ->address('Hauptstraße 1', '12345', 'Musterstadt', 'DE')) ->line('Commission September 2026', netPrice: '840.00', unit: UnitCode::LumpSum) ->payment(PaymentMeansCode::SepaCreditTransfer, iban: 'DE89370400440532013000', accountName: 'Jonas Schreiber') ->render(); $result->filename(); // Gutschrift_GS-2026-0012.pdf ``` - The supplier stays the seller (BG-4) and you are the buyer, even though you issue the document. - `->seller()` replaces the seller of the configuration, and with it the configured bank account and small business setting. `payment()` sends the payment to the supplier's account. - `selfBilled()` sets type code 389 and the note "Gutschrift". The PDF is titled "Gutschrift" ([Self-billing](country-packs.md#self-billing-gutschrift)). - "Gutschrift" is used only for self-billing. Cancellations are titled "Stornorechnung" (cancellation invoice). ## Customers abroad For customers in other countries, the VAT treatment and the format the customer can receive decide how the invoice looks. ### EU business: services You provide a service to a business in another EU country, here IT consulting for a customer in Belgium. For such services the customer owes the VAT (reverse charge), so your invoice shows 0 % VAT. Belgian businesses receive e-invoices through Peppol, a network for delivering e-invoices, so the example writes Peppol BIS Billing 3.0 ([Formats](concepts.md#formats)). ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('RE-2026-0105') ->issueDate('2026-10-09') ->deliveryDate('2026-09-30') ->buyerReference('PO-2026-118') // Peppol requires a buyer reference (BT-10) or an order reference (BT-13) ->locale('en') // English texts, labels and file name ->seller(fn ($s) => $s->electronicAddress('DE123456789', '9930')) // Peppol participant ID ->buyer(Party::make('Klant BV') ->vatId('BE0123456749') ->address('Rue de la Loi 16', '1000', 'Brussels', 'BE') ->electronicAddress('0123456749', '0208')) ->defaultVat(VatPreset::ReverseChargeEu) // for every line of this invoice ->line('IT consulting', netPrice: '110.00', quantity: 16, unit: UnitCode::Hour) ->dueInDays(14) ->render('peppol'); $xml = $result->xml(); // hand it to your Peppol access point ``` - `defaultVat()` sets the VAT of every line of this invoice. `VatPreset::ReverseChargeEu` is VAT category AE with 0 % and the text "Reverse charge, the recipient is liable for VAT" (in German: "Steuerschuldnerschaft des Leistungsempfängers"). - The German country pack requires the VAT identification numbers of both parties (`DE-USTG-11`). It warns when the invoice is issued after the 15th of the month following the service (`DE-USTG-15`). - Peppol needs an electronic address (participant ID) for both parties and a buyer reference or purchase order reference ([Peppol BIS Billing 3.0](formats.md#peppol-bis-billing-30)). Here these are the German VAT number (scheme 9930) and the Belgian enterprise number (scheme 0208). - `->seller(fn ($s) => …)` adds the Peppol address to the configured seller. A `Party` instead of the closure would replace the seller. - `->locale('en')` makes texts, labels and the file name English: `Invoice_RE-2026-0105.xml`. - Quittio writes the XML but does not send it. Sending goes through a Peppol access point, a provider connected to the network. ### EU business: goods You deliver goods to a business in another EU country, here two pumps to a customer in Austria. This is an intra-community supply, which is exempt from German VAT. The example also shows a deliver-to address that differs from the buyer's address, and a line with an item number and a country of origin. ```php use Quittio\Core\Builder\Line; use Quittio\Core\Builder\Party; use Quittio\Core\Data\Party\Address; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('RE-2026-0106') ->issueDate('2026-10-09') ->deliveryDate('2026-10-08') ->buyer(Party::make('Kunde Handels GmbH') ->vatId('ATU12345675') ->address('Mariahilfer Straße 1', '1060', 'Wien', 'AT')) ->deliverTo('Kunde Handels GmbH, Lager', Address::make('Industriestraße 4', '4020', 'Linz', 'AT')) // the customer's warehouse ("Lager") // Item number and country of origin go beyond the named values: Line::make() ->line(Line::make('Industrial pump', netPrice: '1450.00', quantity: 2, unit: UnitCode::Piece, vat: VatPreset::IntraCommunitySupply) ->sellerId('P-200') ->originCountry('DE')) ->render(); ``` - The line needs an item number and a country of origin, so it uses `Line::make()`. It takes the same named values as `line()` and allows the other methods of the line builder. - `VatPreset::IntraCommunitySupply` sets VAT category K with 0 % and the text "Steuerfreie innergemeinschaftliche Lieferung (§ 4 Nr. 1b i. V. m. § 6a UStG)" (tax-exempt intra-community supply under section 4 no. 1b in conjunction with section 6a UStG). - For category K, EN 16931 requires the buyer's VAT identification number, the delivery date and the country of the deliver-to address (rules `BR-IC-02`, `BR-IC-11`, `BR-IC-12`). The German country pack warns after the 15th of the following month (`DE-USTG-15`). - The automatic choice renders ZUGFeRD ([Reverse charge, intra-community supplies and VAT identifiers](country-packs.md#reverse-charge-intra-community-supplies-and-vat-identifiers)). ### Export You sell to a customer outside the EU, here a measuring device for a customer in the United States, invoiced in US dollars. Exports are exempt from German VAT. ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\PaymentMeansCode; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('RE-2026-0107') ->issueDate('2026-10-12') ->deliveryDate('2026-10-12') ->currency('USD') ->buyer(Party::make('Customer Inc.')->address('1 Main Street', '10001', 'New York', 'US')) ->line('Measuring device', netPrice: '2400.00', unit: UnitCode::Piece, vat: VatPreset::Export) ->payment(PaymentMeansCode::CreditTransfer, iban: 'DE02120300000000202051', bic: 'BYLADEM1001') ->locale('en') ->render(); ``` - `VatPreset::Export` sets VAT category G with 0 % and, in English, the text "VAT-exempt export supply (section 4 no. 1a in conjunction with section 6 UStG)". - For a buyer outside the EU the automatic choice renders a PDF. Pass `->format('zugferd')` if the customer wants an e-invoice. - The account is given as credit transfer (payment means code 30), because a SEPA credit transfer (code 58) is for payments in euro. ### Foreign currency with German VAT You invoice a German customer in a foreign currency, here US dollars, with German VAT. Such an invoice also states the VAT in euro. The example sets the exchange rate and reads both VAT totals. ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\PaymentMeansCode; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('RE-2026-0108') ->issueDate('2026-10-12') ->deliveryDate('2026-10-12') ->currency('USD') ->taxCurrency('EUR', exchangeRate: '0.9234', rateDate: '2026-10-12') ->buyer(Party::make('Kunde AG') ->vatId('DE987654321') ->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE')) ->line('Software licence', netPrice: '399.00', quantity: 5) ->payment(PaymentMeansCode::CreditTransfer, iban: 'DE02120300000000202051', bic: 'BYLADEM1001') ->render(); $result->data()->totals->vatTotal; // USD 379.05 $result->data()->totals->vatTotalInTaxCurrency; // EUR 350.01 ``` - `taxCurrency()` sets the VAT accounting currency (BT-6) and the exchange rate. The calculator writes the VAT in euro (BT-111): 379.05 × 0.9234 = 350.01. - Without the rate the validation fails (`BR-53`). Without `taxCurrency()` the German country pack warns (`DE-USTG-10`). See [Currency and exchange rate](building-invoices.md#currency-and-exchange-rate). ## Laravel applications These examples connect Quittio to your application: models, stored drafts and tests. ### Invoices for orders Your application stores customers, orders and order items as Eloquent models. Instead of copying their fields into the builder for every invoice, the models provide the data themselves. The example shows the three models and the code that issues, stores and mails the invoice when an order ships. ```php use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Quittio\Core\Builder\LineBuilder; use Quittio\Core\Builder\PartyBuilder; use Quittio\Core\Contracts\ProvidesInvoiceData; use Quittio\Core\Contracts\ProvidesInvoiceLine; use Quittio\Core\Contracts\ProvidesParty; use Quittio\Core\Data\InvoiceData; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; class Customer extends Model implements ProvidesParty { public function fillParty(PartyBuilder $party): void { $party->name($this->company) ->vatId($this->vat_id) ->address($this->street, $this->postcode, $this->city, $this->country) ->electronicAddress($this->email, 'EM'); } } class OrderItem extends Model implements ProvidesInvoiceLine { protected function casts(): array { return ['unit_price' => 'decimal:2']; // a string such as "24.90", never a float } public function fillLine(LineBuilder $line): void { $line->name($this->product_name) ->sellerId($this->sku) ->quantity($this->quantity, UnitCode::Piece) ->netPrice($this->unit_price) ->vat(VatPreset::from($this->tax_preset)); // stored preset key, e.g. "de.reduced" } } class Order extends Model implements ProvidesInvoiceData { protected function casts(): array { return ['invoiced_at' => 'date', 'shipped_at' => 'date']; } public function customer(): BelongsTo { return $this->belongsTo(Customer::class); } public function items(): HasMany { return $this->hasMany(OrderItem::class); } public function toInvoiceData(): InvoiceData { return Quittio::invoice() ->number($this->invoice_number) ->issueDate($this->invoiced_at) ->deliveryDate($this->shipped_at) ->purchaseOrderReference($this->customer_reference) ->buyer($this->customer) ->lines($this->items) ->dueInDays(14) ->build(); } } ``` The three contracts of `Quittio\Core\Contracts` connect the models to the builder ([Application models](building-invoices.md#application-models)): - `Customer` implements `ProvidesParty`: `fillParty()` fills the buyer, so `->buyer($this->customer)` takes the customer. - `OrderItem` implements `ProvidesInvoiceLine`: `fillLine()` fills one line, so `->lines($this->items)` takes the order items. - `Order` implements `ProvidesInvoiceData`: `toInvoiceData()` builds the invoice, so `Quittio::make($order)` renders the invoice of an order. When the order ships, issue the invoice once and keep the file: ```php use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; use Quittio\Laravel\Facades\Quittio; // When the order ships: issue the invoice once, store it and send it $result = Quittio::make($order)->render(); $order->invoice_path = $result->save('invoices', 'orders/'); $order->save(); Mail::to($order->customer->email)->send(new InvoiceMail($result)); // Later downloads serve the stored file, so the invoice never changes return Storage::disk('invoices')->download($order->invoice_path); ``` - `InvoiceMail` is the mailable from [Mail attachments](laravel.md#mail-attachments); the rendered invoice is its attachment. - Rendering again later would build the invoice from the current model data. Serving the stored file keeps the issued invoice unchanged when, for example, the customer's address changes. - The invoice number comes from your number range. The [Managed layer](managed.md) assigns gapless numbers, stores the issued data and archives the files. - To render in the background: `Quittio::make($order)->queue()->save('invoices', 'orders/')` ([Queueing](laravel.md#queueing)). ### Drafts and previews Your users prepare an invoice over some time and want to see it before it is sent. Keep it as a draft: invoice data without a number that you store until you issue it. The example stores a draft as JSON, renders a preview, changes the draft and issues it. This needs no Managed layer. ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Core\Builder\Party; use Quittio\Core\Data\InvoiceData; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; // Create a draft and keep it as JSON, e.g. in a column of your model $draft = Quittio::invoice() ->buyer(Party::make('Kunde AG')->vatId('DE987654321')->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE')) ->line('Consulting', netPrice: '120.00', quantity: 10, unit: UnitCode::Hour) ->build(); $json = $draft->toJson(); // Preview: a PDF with watermark, without e-invoice XML; a draft has no number yet $preview = Quittio::make(InvoiceData::fromJson($json)) ->format('pdf') ->option('watermark', 'ENTWURF') // "Entwurf" means draft ->withoutValidation() ->render(); // Change the draft as often as needed $json = InvoiceBuilder::from(InvoiceData::fromJson($json)) ->updateLine('1', netPrice: '110.00') ->build() ->toJson(); // Issue it: number and dates, then render and store the final invoice $invoice = InvoiceBuilder::from(InvoiceData::fromJson($json)) ->number('RE-2026-0115') // the next number of your number range ->issueDate(today()) ->deliveryDate('2026-10-13') ->build(); $result = Quittio::make($invoice)->render(); $result->save('invoices', '2026/'); ``` - A watermark renders a plain PDF and never e-invoice XML, so a draft cannot pass as an invoice ([Drafts and watermarks](formats.md#drafts-and-watermarks)). - The preview runs without validation because a draft has no number yet. Return `$preview->stream()` to show it in the browser. - Issue a draft by setting the number and the dates. After that, never change the invoice; correct it with a [cancellation](#cancellation). - The [Managed layer](managed.md) adds drafts as database records, gapless numbering, frozen issued data, an archive with hashes, the history of cancellations and an audit log. ### Testing You want feature tests for the order flow from [Invoices for orders](#invoices-for-orders). The first test checks the flow with the fake: `Quittio::fake()` records every render call and skips validation, XML and PDF, so the test is fast. The second test runs the real validation on the invoice of an order with `check()`: ```php use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; use Quittio\Laravel\Facades\Quittio; use Quittio\Laravel\Rendering\RenderedInvoice; it('sends the invoice when an order ships', function () { Quittio::fake(); // no PDF, no XML, no validation: fast Mail::fake(); Storage::fake('invoices'); $order = Order::factory()->create(['invoice_number' => 'RE-2026-0130']); $this->post("/orders/{$order->id}/ship")->assertOk(); Quittio::assertRendered(fn (RenderedInvoice $r) => $r->data()->number === 'RE-2026-0130' && $r->format() === 'zugferd'); Mail::assertSent(InvoiceMail::class); }); it('builds a valid e-invoice for an order', function () { $order = Order::factory()->create(); $report = Quittio::make($order)->check(); // the real validation, without rendering expect($report->isValid())->toBeTrue() ->and($report->warnings())->toBe([]); }); ``` - `Order::factory()` is the model factory of your application. `InvoiceMail` and the route are from [Invoices for orders](#invoices-for-orders). - `assertRendered()` passes if at least one rendered invoice matches the callback ([Fake](testing.md#fake)). - `check()` is never faked. It validates like a render run, including the XML, without rendering a PDF ([Testing](testing.md)). ## Quotes and reminders Besides invoices, Quittio renders other business documents such as quotes and payment reminders ([Quotes and other documents](documents.md)). ### From quote to invoice You send the customer a quote first and invoice when they accept it. The example shows a quote with an optional line and an invoice that takes over the accepted quote. ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $quote = Quittio::quote() ->number('AN-2026-0031') ->issueDate('2026-10-01') ->validUntil('2026-10-31') ->buyer(Party::make('Kunde AG')->vatId('DE987654321')->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE')) ->line('Workshop "E-invoicing"', netPrice: '1200.00', unit: UnitCode::Day) ->optionalLine('Follow-up session', netPrice: '120.00', quantity: 4, unit: UnitCode::Hour); $quote->render()->save('documents', 'quotes/'); // quotes/Angebot_AN-2026-0031.pdf ("Angebot": quote) // The customer accepts: the invoice takes over parties, lines (without the optional line) and the bank account $result = Quittio::make(InvoiceBuilder::fromDocument($quote->build()) ->number('RE-2026-0140') ->issueDate('2026-11-03') ->deliveryDate('2026-10-28') ->dueInDays(14)) ->render(); ``` - The optional line is shown with its amounts. It is not counted in the totals and not taken over by `fromDocument()`. The invoice totals 1,428.00. - `Quittio::quote()` carries the bank account of the configuration, although quotes do not print it. The invoice takes it over ([From document to invoice](documents.md#from-document-to-invoice)). ### Payment reminder A customer has not paid an invoice by its due date. The example sends the first reminder ("1. Mahnung") and adds the statutory default interest for a business customer. ```php use Brick\Money\Money; use Quittio\Core\Builder\Party; use Quittio\Core\Support\Amount; use Quittio\Core\Support\Date; use Quittio\Country\DE\Dunning\DefaultInterest; use Quittio\Laravel\Facades\Quittio; $reminder = Quittio::paymentReminder(1) // first reminder ("1. Mahnung"), with seller and bank account ->number('M-2026-0005') ->issueDate('2026-10-15') ->buyer(Party::make('Kunde AG')->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE')) ->openItem('RE-2026-0042', '2026-08-03', '1190.00', dueDate: '2026-08-17') ->deadline('2026-10-29'); // Statutory default interest (section 288 BGB, the German Civil Code), split at every change of the base rate foreach ((new DefaultInterest)->calculate(Money::of('1190.00', 'EUR'), Date::parse('2026-08-18'), Date::parse('2026-10-15')) as $charge) { $reminder->interest(Amount::format($charge->base), $charge->ratePercent, $charge->from, $charge->until, $charge->description, $charge->dayCount); } $reminder->render()->save('documents', 'reminders/'); // reminders/1._Mahnung_M-2026-0005.pdf $reminder->build()->dunning->total('EUR'); // EUR 1209.89 ``` - `paymentReminder(1)` is the first formal reminder. Level 0 is a friendly payment reminder ("Zahlungserinnerung"), level 2 the second reminder, and so on ([Payment reminders](documents.md#payment-reminders)). - `openItem()` lists the unpaid invoice with its number, date, amount and due date. `deadline()` is the date by which the customer should pay. - `calculate()` takes the first day with interest and the day after the last one. Here the rate is 10.52 % a year: the base rate of 1.52 % plus 9 points for business debtors. From 18 August to 14 October 2026 this gives 19.89, so the reminder asks for 1,209.89. For consumers pass `consumer: true` (5 points). See [Default interest](country-packs.md#default-interest). - Fees and interest carry no VAT. - The PDF is saved as `1._Mahnung_M-2026-0005.pdf`, after its German title "1. Mahnung" (first reminder). ## Incoming e-invoices Your suppliers send you e-invoices, and you want to check and book them. The example is controller code that accepts an uploaded e-invoice, validates it and extracts the values for your accounting. ```php use Quittio\Laravel\Facades\Quittio; $content = $request->file('invoice')->get(); // uploaded PDF or XML $report = Quittio::validateFile($content); if (! $report->isValid()) { return back()->withErrors(array_map(fn ($m) => "[{$m->code}] {$m->message}", $report->errors())); } $read = Quittio::read($content); $invoice = $read->data; $booking = [ 'supplier' => $invoice->seller->name, 'supplier_vat_id' => $invoice->seller->vatId, 'number' => $invoice->number, 'issue_date' => $invoice->issueDate->toIsoString(), 'due_date' => $invoice->dueDate?->toIsoString(), 'amount_due' => (string) $invoice->totals?->amountDue->getAmount(), 'currency' => $invoice->currency, 'iban' => $invoice->paymentInstructions?->primaryCreditTransfer()?->accountId, 'profile' => $read->profile, // e.g. "en16931" or "xrechnung" ]; ``` For the ZUGFeRD invoice of the first example, `$booking` is: ```json {"supplier":"Muster GmbH","supplier_vat_id":"DE123456789","number":"RE-2026-0101","issue_date":"2026-10-01","due_date":"2026-10-31","amount_due":"7123.94","currency":"EUR","iban":"DE02120300000000202051","profile":"en16931"} ``` - `validateFile()` reads the file and runs all configured validation levels ([Validation](validation.md)). Each error has a rule code and a message, which the example passes back to the form. - `read()` returns the data as stated in the document. It reads XML in both syntaxes (CII and UBL, including XRechnung and Peppol) and PDFs with embedded XML the same way ([Syntaxes](concepts.md#syntaxes-cii-and-ubl), [Reading incoming invoices](reading.md)). - Amounts are never corrected silently: totals that differ from a recalculation are reported as `QUITTIO-DECLARED-TOTALS`. ## Logo, colours and texts You usually set your logo, colours and texts once in the configuration. The example shows how to set them for a single invoice. ```php use Quittio\Core\Builder\Party; use Quittio\Core\Data\Presentation\Colors; use Quittio\Core\Data\Presentation\Presentation; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() ->number('RE-2026-0150') ->deliveryDate('2026-10-14') ->buyer(Party::make('Kunde AG')->vatId('DE987654321')->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE')) ->line('Consulting', netPrice: '120.00', quantity: 4, unit: UnitCode::Hour) ->presentation(new Presentation( logo: storage_path('app/logo.png'), colors: new Colors(primary: '#0f766e'), introText: 'Thank you for your order. We invoice the following services:', closingText: 'Questions about this invoice? Call us at +49 30 1234567.', footerColumns: [ ['Muster GmbH', 'Hauptstraße 1', '12345 Musterstadt'], ['Managing director: Max Muster', 'Amtsgericht Musterstadt, HRB 12345'], // register court and register number ['Bank: Musterbank', 'IBAN DE02 1203 0000 0000 2020 51'], ], )) ->render(); ``` - The same values for all invoices go into the configuration: `pdf.logo`, `pdf.colors.primary`, `pdf.texts.intro`, `pdf.texts.closing` and `pdf.footer_columns` ([Configuration](configuration.md)). - Logos are PNG, JPEG or SVG. SVG logos in PDF/A need the PHP extension `imagick` ([Logos](templates.md#logos)). - The design is never part of the XML. ## Without Laravel You use Quittio in a PHP application without Laravel. The example is a complete script with the packages `quittio/core` and `quittio/country-de` (the German country pack). It renders an invoice, saves it and reads it back. ```php countries())); // lines without vat(): 19 % $invoice = InvoiceBuilder::make() ->number('RE-2026-0120') ->issueDate('2026-10-14') ->deliveryDate('2026-10-14') ->seller(Party::make('Muster GmbH')->vatId('DE123456789')->address('Hauptstraße 1', '12345', 'Musterstadt', 'DE')) ->buyer(Party::make('Kunde AG')->vatId('DE987654321')->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE')) ->line('Consulting', netPrice: '120.00', quantity: 4, unit: UnitCode::Hour) ->payment(PaymentMeansCode::SepaCreditTransfer, iban: 'DE02120300000000202051') ->dueInDays(14) ->build(); $result = $quittio->make($invoice)->render(); $path = $result->save(__DIR__.'/invoices/'.$result->filename()); // invoices/Rechnung_RE-2026-0120.pdf // Read it back, as the recipient would $read = $quittio->readFile($path); echo $read->data->totals->amountDue; // EUR 571.20 ``` - Without Laravel there is no configuration file and no facade. `InvoiceBuilder::make()` starts an empty invoice, so the seller and the bank account are set in code. - `useDefaultVat()` gives lines without `vat()` the default VAT of the seller's country pack (19 % in Germany), as the Laravel integration does. Without a default, such lines make `build()` fail. - `Quittio::create()` takes the configuration as an array ([Without Laravel](getting-started.md#without-laravel)). The instance offers `make()`, `cancellation()`, `document()`, `read()` and the other methods of the facade. # Building invoices This page is the reference for creating invoice data with Quittio's invoice builder: every builder method, VAT, payment, the calculation of totals, cancellations and the data model. Use it when you write your first real invoice and whenever you need an option that the tutorial does not show. If you have not rendered an invoice yet, start with [Getting started](getting-started.md); terms such as EN 16931, business terms and profiles are explained in [E-invoicing basics](concepts.md). The page has three parts: - **Basics:** the [overview](#overview) with the usual way to write each part of an invoice, the [conventions](#conventions) for amounts, dates and codes, and [creating and building](#creating-and-building) the data. - **The parts of an invoice**, in the order you usually need them: [number and type](#document-type-and-identification), [dates](#dates), [parties](#parties), [lines](#lines), [VAT](#vat), [payment](#payment), [references](#references), [notes](#notes), [attachments](#supporting-documents), [design and metadata](#presentation-and-metadata) and [your own models](#application-models). - **Advanced topics:** [foreign currencies](#currency-and-exchange-rate), [allowances and charges](#allowances-and-charges), [sub lines](#sub-lines), [prices including VAT](#gross-price-mode), [changing existing data](#changing-existing-data), [cancellations](#cancellations-and-corrections), [data of profile EXTENDED](#extensions), [macros](#macros), the [calculation](#calculation) and the [data model](#data-model). ## Overview `Quittio\Core\Builder\InvoiceBuilder` creates an `InvoiceData` object: the invoice as immutable data, following the European standard EN 16931 ([EN 16931 and business terms](concepts.md#en-16931-and-business-terms)). The builder is part of `quittio/core` and works with and without Laravel. Rendering turns the data into PDF and XML ([Formats and profiles](formats.md), [Laravel integration](laravel.md)). In Laravel, start with `Quittio::invoice()`. It returns the same builder, already filled with the seller, the first bank account and the small business setting of `config/quittio.php`, and the builder can render itself with `->render()` ([Starting invoices](laravel.md#starting-invoices)): ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\PaymentMeansCode; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; $invoice = Quittio::invoice() // seller and bank account from config/quittio.php ->number('RE-2026-0042') ->issueDate('2026-10-03') ->deliveryDate('2026-09-30') // date of supply, mandatory in Germany ->buyerReference('PO-4711') ->buyer(Party::make('Kunde AG') ->vatId('DE987654321') ->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE') ->electronicAddress('ap@kunde.de', 'EM')) ->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour) // default VAT: 19 % ->line('Handbook of VAT law', netPrice: '39.00', vat: VatPreset::Reduced) ->payment(PaymentMeansCode::SepaCreditTransfer, remittanceInformation: 'RE-2026-0042') // keeps the configured account ->dueInDays(14)->cashDiscount(days: 7, percent: '2.00') ->build(); // or ->render() for PDF and XML ``` Without Laravel, start with `InvoiceBuilder::make()` and set the seller yourself ([Seller and buyer](#seller-and-buyer)). Three things to know before you continue: - `build()` returns the data. In Laravel, `render()` renders it right away instead. - You never pass totals, VAT amounts or the VAT breakdown. Quittio calculates them when the invoice is rendered ([Calculation](#calculation)). - The builder only checks that the essential parts are there ([Creating and building](#creating-and-building)). Everything else, such as a missing invoice number, is reported by the [validation](validation.md) when the invoice is rendered. ### The usual forms Most parts of an invoice can be written in several ways. This documentation uses one usual form for each, the same as [Getting started](getting-started.md#one-way-to-write-it). The other forms exist for special cases; the linked sections describe them. | You need | Write | Details | |---|---|---| | A line with price, quantity, unit, VAT or description | `->line('Hosting', netPrice: '49.00', quantity: 12, unit: UnitCode::Month)`. Without `quantity` it is 1, without `vat` the default VAT applies (in Laravel 19 % for a German seller). | [Lines](#lines), [Default VAT](#default-vat) | | A line with more than that: item number, line discount, list price, period, sub lines, line group | `->line(Line::make('Pump', netPrice: '1450.00', quantity: 2)->sellerId('P-200'))`. `Line::make()` takes the same named values as `line()` and returns the line builder for the other methods. | [LineBuilder](#linebuilder) | | Many lines from your data | `->lines($order->items)` with models that implement `ProvidesInvoiceLine`, or `->lines($rows)` with arrays such as `['name' => 'Hosting', 'netPrice' => '49.00']` | [Lines](#lines), [Application models](#application-models) | | The customer | `->buyer($customer)` with a model that implements `ProvidesParty`, otherwise `->buyer(Party::make('Kunde AG')->address(…))` | [Seller and buyer](#seller-and-buyer) | | A VAT rate other than the default | `->line('Book', netPrice: '39.00', vat: VatPreset::Reduced)` | [VAT](#vat) | | Payment terms | `->dueInDays(14)->cashDiscount(days: 7, percent: '2.00')` | [Payment terms](#payment-terms) | | The result | `->render()` for PDF and XML, `->build()` for the data only, e.g. to store a draft | [Creating and building](#creating-and-building) | ## Conventions These rules apply to every method on this page. **Chaining.** Every builder method returns the builder, so calls can be chained. The builders are mutable; the built `InvoiceData` is not ([Immutability](#immutability)). **Numbers are strings.** Amounts, prices, quantities, percentages and rates are strings (`'95.00'`), integers or `Brick\Math` numbers. Floats are rejected with an `InvalidArgumentException` ("Floats are not accepted …"), with or without `declare(strict_types=1)`, because most decimal values have no exact float representation. - Methods typed `BigDecimal|int|float|string` accept a float only to reject it. Without `float` in the signature, PHP would turn `19.99` into `19` in files without strict types. - Parameters typed `string`, such as the amount of `allowance()`, receive a float as its string form. **Amounts have at most two decimals.** Allowance, charge, prepaid and rounding amounts and the base amounts of cash discounts have at most two decimals, in every currency. More decimals throw an `InvalidArgumentException`. Prices, quantities, percentages and rates keep any precision. **Dates.** Date parameters accept a `Quittio\Core\Support\Date`, a `DateTimeInterface` or a string: `YYYY-MM-DD`, `YYYYMMDD` or another notation that names a calendar date. Relative expressions such as `tomorrow` or `+14 days` are rejected. A `DateTimeInterface` is converted into a date in the time zone set with `timezone()`, so call `timezone()` first. `invoicingPeriod()` and `LineBuilder::period()` accept `Date` objects and strings only. **Codes.** Code parameters take the enum case (`UnitCode::Hour`) or the code as string (`'HUR'`). A string that is not a case of the enum is kept as `Quittio\Core\Codes\RawCode`, so every code of the official code lists can be used; the validation checks raw codes against the code lists. Exception: `attachment()` accepts only the MIME types of `MimeType`. **Calculated values.** Line net amounts (BT-131), the document totals (BG-22), the VAT breakdown (BG-23) and the due date derived from the payment terms are calculated when the invoice is rendered ([Calculation](#calculation)). There is no method to set them. **Terms used on this page:** - Numbers such as "buyer reference (BT-10)" or "seller postal address (BG-5)" are the business terms (BT) and business groups (BG) of EN 16931. You never type them in code; they help you match validation messages and the [field reference](field-reference.md) ([EN 16931 and business terms](concepts.md#en-16931-and-business-terms)). - Codes such as `BR-CO-26`, `BR-DE-1` or `QUITTIO-PROFORMA` name the rules that the validation checks ([Rules and validation](concepts.md#rules-and-validation)). - ZUGFeRD/Factur-X, XRechnung and Peppol are e-invoice formats ([Formats](concepts.md#formats)). CII and UBL are the two XML syntaxes they use ([Syntaxes: CII and UBL](concepts.md#syntaxes-cii-and-ubl)). A profile, such as EN 16931, EXTENDED or XRechnung, decides which fields and rules an XML file has ([Profiles](concepts.md#profiles)). - A country pack brings the tax presets, legal rules and texts of one country; `quittio/quittio` includes the German pack ([Country packs](country-packs.md)). - A **VAT group** is everything with the same VAT category and rate. The **VAT breakdown** (BG-23) has one entry per VAT group, with its taxable amount and its VAT. - round2(x) is x rounded half up to two decimals. ## Creating and building The tables of this page group the builder methods by topic and name their business terms. The API reference lists every method with its parameters. | Method | Description | |---|---| | `InvoiceBuilder::make()` | New, empty builder. Defaults: type code 380 (commercial invoice), currency `EUR`, EN 16931:2017. | | `InvoiceBuilder::from(InvoiceData $data)` | Builder with all values of existing data, e.g. to change a draft, a stored invoice or a read invoice ([Changing existing data](#changing-existing-data)). | | `Quittio::edit(InvoiceData $data)` | Like `from()`, but declared amounts are compared with the configured calculator and its policy ([Declared amounts](#declared-amounts)). `InvoiceBuilder::fromUsing($data, $calculator)` does the same with a calculator of your own. | | `InvoiceBuilder::fromDocument(DocumentData $document)` | Invoice from a quote, order confirmation, delivery note or proforma invoice. Parties, billable lines, document allowances and charges and payment data are taken over; number, issue date and notes are not. The number of an order confirmation becomes the sales order reference (BT-14), that of a delivery note the despatch advice reference (BT-16). See [From document to invoice](documents.md#from-document-to-invoice). | | `build(): InvoiceData` | Builds the data. Requires at least one line. | | `buildDraft(): InvoiceData` | Builds without requiring lines, e.g. for drafts or as the basis of payment reminders or account statements. Data without lines can be calculated (its line sum is zero), but it is not a valid invoice (BR-16). | `build()` and `buildDraft()` throw `Quittio\Core\Exceptions\InvalidInvoiceData` (an `InvalidArgumentException`) when an essential part is missing. The message names the business term, e.g. "Missing buyer postal address (BG-8).": | Missing | Business term | |---|---| | Seller, seller name, seller postal address | BG-4, BT-27, BG-5 | | Buyer, buyer name, buyer postal address | BG-7, BT-44, BG-8 | | Lines (`build()` only) | BG-25 | | Item name of a line | BT-153 | | VAT category of a line, unless the invoice has a [default VAT](#default-vat) | BG-30 | Everything else is reported by the [validation](validation.md) when the invoice is rendered or checked, e.g. a missing invoice number or a missing buyer reference that XRechnung requires. Without `issueDate()`, the issue date is today in the builder's time zone (`timezone()`, otherwise PHP's default time zone). ## Document type and identification Every invoice needs a number. The type code tells the recipient what kind of document it is. Its default, 380, is a normal commercial invoice, so most invoices only call `number()`. | Method | BT | Description | |---|---|---| | `number(string $number)` | BT-1 | Invoice number. The builder does not check it; the validation requires it. The [Managed layer](managed.md) assigns numbers itself. | | `type(InvoiceTypeCode\|RawCode\|string $type)` | BT-3 | Type code from the code list UNTDID 1001, default `InvoiceTypeCode::CommercialInvoice` (380). See [Type codes](#type-codes). | | `businessProcess(?string $process)` | BT-23 | Business process identifier. The profiles Peppol and XRechnung write `urn:fdc:peppol.eu:2017:poacc:billing:01:1.0` when it is not set. | | `standard(StandardVersion $version)` | BT-24 | Version of EN 16931: `StandardVersion::En16931_2017` (default) or `En16931_2026`. Only the profile EN 16931 supports 2026 (`urn:cen.eu:en16931:2026`); other profiles report `QUITTIO-STANDARD-VERSION`. | The specification identifier (BT-24) itself is not part of the data. It comes from the profile chosen when rendering ([Formats and profiles](formats.md#profiles)). ### Credit notes, corrected invoices and self-billing Set another type code when the document is not a normal invoice: ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Core\Enums\InvoiceTypeCode; use Quittio\Laravel\Facades\Quittio; // Credit note: positive amounts, type code 381, reference to the invoice Quittio::invoice()->type(InvoiceTypeCode::CreditNote)->precedingInvoice('RE-2026-0042', '2026-10-03'); // Corrected invoice (384) based on the original InvoiceBuilder::from($original) ->number('RE-2026-0050') ->issueDate('2026-10-20') ->type(InvoiceTypeCode::CorrectedInvoice) ->precedingInvoice($original->number, $original->issueDate) ->updateLine('2', netPrice: '80.00'); // Self-billing: the buyer issues the invoice, the supplier stays the seller (BG-4) Quittio::invoice()->type(InvoiceTypeCode::SelfBilledInvoice); ``` To cancel an issued invoice, build a cancellation with `Quittio::cancellation()` instead ([Cancellations and corrections](#cancellations-and-corrections)). The German pack adds `selfBilled()` for self-billing. It sets type code 389 and adds the note "Gutschrift" (self-billed invoice) with subject `REG`, which the German rules require ([Self-billing](country-packs.md#self-billing-gutschrift), [German terms](concepts.md#german-terms)). ### Type codes `InvoiceTypeCode` contains the type codes that EN 16931 allows: | Case | Code | Note | |---|---|---| | `CommercialInvoice` | 380 | Default. | | `CreditNote` | 381 | Credit note with positive amounts; UBL writes a `CreditNote` document. | | `CorrectedInvoice` | 384 | Reference the corrected invoice with `precedingInvoice()`. | | `SelfBilledInvoice` | 389 | Issued by the buyer (self-billing). | | `SelfBilledCreditNote` | 261 | Self-billed credit note. | | `SelfBilledDebitNote` | 527 | Self-billed debit note. | | `PartialInvoice` | 326 | Partial invoice. | | `PrepaymentInvoice` | 386 | Advance payment invoice. | | `PartialConstructionInvoice`, `PartialFinalConstructionInvoice`, `FinalConstructionInvoice` | 875, 876, 877 | Construction invoices. | | `DebitNote` | 383 | Debit note. | | `ProformaInvoice` | 325 | Rejected in the invoice pipeline (`QUITTIO-PROFORMA`); use `Quittio::proforma()` in Laravel or `DocumentBuilder::proforma()` ([Proforma invoices](documents.md#proforma-invoices)). | Further cases: `RequestForPayment` (71), `DebitNoteGoodsServices` (80), `CreditNoteGoodsServices` (81), `MeteredServicesInvoice` (82), `CreditNoteFinancialAdjustment` (83), `DebitNoteFinancialAdjustment` (84), `TaxNotification` (102), `InvoicingDataSheet` (130), `DirectPaymentValuation` (202), `ProvisionalPaymentValuation` (203), `PaymentValuation` (204), `InterimApplicationForPayment` (211), `FinalPaymentRequest` (218), `PaymentRequestForCompletedUnits` (219), `ConsolidatedCreditNote` (262), `PriceVariationInvoice` (295), `CreditNoteForPriceVariation` (296), `DelcredereCreditNote` (308), `CommercialInvoiceWithPackingList` (331), `CommissionNote` (382), `ConsolidatedInvoice` (385), `HireInvoice` (387), `TaxInvoice` (388), `DelcredereInvoice` (390), `FactoredInvoice` (393), `LeaseInvoice` (394), `ConsignmentInvoice` (395), `FactoredCreditNote` (396), `OcrPaymentCreditNote` (420), `DebitAdvice` (456), `ReversalOfDebit` (457), `ReversalOfCredit` (458), `ForwardersCreditNote` (532), `ForwardersInvoiceDiscrepancyReport` (553), `InsurersInvoice` (575), `ForwardersInvoice` (623), `PortChargesDocument` (633), `InvoiceInformationForAccounting` (751), `FreightInvoice` (780), `ClaimNotification` (817), `ConsularInvoice` (870), `CustomsInvoice` (935). `InvoiceTypeCode::isCreditNote()` is true for 81, 83, 261, 262, 296, 308, 381, 396, 420, 458 and 532. These are written as a `CreditNote` document in UBL. `isSelfBilled()` is true for 261, 389 and 527. The document title in PDF, HTML and file names follows the type code: - 380, 381, 384, 389, 325, 326, 386, 875, 876 and 877 have their own title. - Other credit notes use the title of 381, other self-billed documents that of 389. - Everything else is titled "Invoice" ("Rechnung" on a German invoice). `Presentation::$title` overrides the title. ## Dates An invoice has an issue date and usually the date or period of the delivery or service. German invoices must state the date of supply: a delivery date (`deliveryDate()`), an invoicing period (`invoicingPeriod()`) or a period on the lines (rule `DE-USTG-03`). | Method | BT | Description | |---|---|---| | `issueDate(Date\|DateTimeInterface\|string $date)` | BT-2 | Issue date, default today. | | `deliveryDate(Date\|DateTimeInterface\|string $date)` | BT-72 | Actual delivery or service date. Keeps the deliver-to data. | | `invoicingPeriod(Date\|string\|null $start, Date\|string\|null $end = null)` | BG-14 (BT-73, BT-74) | Invoicing or service period. One of the two dates may be `null`, not both. | | `dueDate(Date\|DateTimeInterface\|string $date)` | BT-9 | Due date. Without it, the calculator derives it from `dueInDays()`. Without payment terms, the configured default applies when rendering (`due_days`, Laravel `defaults.due_days`, default 14 there); never for credit notes. | | `taxPointDate(Date\|DateTimeInterface\|string $date)` | BT-7 | Date when VAT becomes chargeable, if it differs from the issue date. | | `taxPointDateCode(TaxPointDateCode $code)` | BT-8 | `TaxPointDateCode::IssueDate`, `DeliveryDate` or `PaymentDate` (written as 5, 29, 72 in CII and 3, 35, 432 in UBL). EN 16931 allows BT-7 or BT-8, not both (BR-CO-03). | | `timezone(string $timezone)` | – | Time zone for `DateTimeInterface` values and the default issue date. | Lines can carry their own period (`LineBuilder::period()`, BG-26). ## Parties Every invoice has a seller (your company) and a buyer (your customer). Further parties are optional: a payee, a tax representative of the seller and a deliver-to party. ### Seller and buyer In Laravel, the seller comes from `config/quittio.php` and you set only the buyer. Without Laravel, set both: ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Core\Builder\Party; $builder = InvoiceBuilder::make() ->seller(Party::make('Muster GmbH') ->vatId('DE123456789') ->address('Hauptstraße 1', '12345', 'Musterstadt', 'DE') ->contact('Max Muster', '+49 123 456789', 'rechnung@muster.de') ->electronicAddress('rechnung@muster.de', 'EM')) ->buyer(Party::make('Kunde AG') ->vatId('DE987654321') ->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE') ->electronicAddress('ap@kunde.de', 'EM')); ``` `seller()` and `buyer()` take one of four kinds of argument: | Argument | Effect | |---|---| | `Party::make('Kunde AG')->vatId(…)->address(…)` | A `PartyBuilder` (`Quittio\Core\Builder\Party` starts one). It replaces the party set before. The builder is copied, so it can serve as a template. | | An application model implementing `ProvidesParty`, e.g. `->buyer($customer)` | The model fills a new `PartyBuilder` ([Application models](#application-models)). It replaces the party set before. | | `SellerParty`, `BuyerParty` | A finished party, e.g. `Seller::fromConfig()` in Laravel. It replaces the party set before. | | A callable that receives a `PartyBuilder` | Continues the party set before, also a `SellerParty` or `BuyerParty` (e.g. from `Quittio::invoice()` in Laravel or from `InvoiceBuilder::from()`), so you can change single values. | A callable is the way to change the configured seller in Laravel, e.g. to add a contact person: ```php use Quittio\Core\Builder\PartyBuilder; use Quittio\Laravel\Facades\Quittio; Quittio::invoice() // seller from config/quittio.php ->seller(fn (PartyBuilder $s) => $s->contact('Erika Muster', '+49 123 4567', 'erika@muster.de')) ->buyer($customer); // ProvidesParty ``` **Replacing the configured seller.** `Quittio::invoice()` starts the builder with the defaults of a seller: the seller of the configuration or the current issuer of the Managed layer. It does this with `applyDefaults(callable $apply)`, and the builder remembers what these defaults set. If you then replace the seller with another one, those values are removed again: the bank account, the small business flag and the other defaults of that seller. Values that you set on the builder yourself stay. A callable keeps the seller and its defaults ([Starting invoices](laravel.md#starting-invoices)). `PartyBuilder` (`Quittio\Core\Builder\PartyBuilder`) has these methods: | Method | Seller | Buyer | Description | |---|---|---|---| | `name(string $name)` | BT-27 | BT-44 | Legal name. Required. | | `tradingName(?string $tradingName)` | BT-28 | BT-45 | Trading name. | | `vatId(?string $vatId)` | BT-31 | BT-48 | VAT identifier with country prefix, e.g. `DE123456789`; uppercased, spaces removed. | | `taxNumber(?string $taxNumber)` | BT-32 | – | Tax registration number (German "Steuernummer", the number issued by the local tax office); ignored for the buyer. | | `legalRegistration(?string $value, IdentifierScheme\|RawCode\|string\|null $scheme = null)` | BT-30 | BT-47 | Legal registration identifier, e.g. the commercial register number. | | `additionalLegalInfo(?string $info)` | BT-33 | – | Additional legal information (legal form, share capital …); ignored for the buyer. | | `identifier(string $value, IdentifierScheme\|RawCode\|string\|null $scheme = null)` | BT-29 | BT-46 | Adds a party identifier, e.g. a GLN (Global Location Number) or the customer number at the buyer. The seller keeps all identifiers, the buyer and the payee only the first. CII writes identifiers without scheme as `ram:ID`, with scheme as `ram:GlobalID`. | | `address(?string $line1, ?string $postcode, ?string $city, string $countryCode, ?string $line2 = null, ?string $line3 = null, ?string $subdivision = null)` | BG-5 | BG-8 | Postal address. The country code (ISO 3166-1 alpha-2, e.g. `DE`) is uppercased. Required. | | `postalAddress(Address $address)` | BG-5 | BG-8 | Postal address as object. | | `electronicAddress(string $value, ElectronicAddressScheme\|RawCode\|string $scheme = ElectronicAddressScheme::Email)` | BT-34 | BT-49 | Electronic address, e.g. an e-mail address, with its scheme (EAS code, e.g. `EM` for e-mail). Required by XRechnung and Peppol. Peppol needs a participant identifier, e.g. `9930` (German VAT number) or `0088` (GLN); `EM` is rejected there. | | `contact(?string $name = null, ?string $phone = null, ?string $email = null)` | BG-6 | BG-9 | Contact person, phone and e-mail. XRechnung requires all three for the seller. | | `consumer(bool $consumer = true)` | – | – | Marks the buyer as consumer (B2C). The automatic format choice then renders a PDF without XML, country packs apply their consumer rules, and templates get `buyer.isConsumer`. Not part of the XML; ignored for the seller. | | `fromModel(ProvidesParty $model)` | | | Lets an application model fill the builder ([Application models](#application-models)). | | `PartyBuilder::fromSeller(SellerParty $seller)`, `PartyBuilder::fromBuyer(BuyerParty $buyer)`, `PartyBuilder::fromPayee(PayeeParty $payee)` | | | Builder with the values of an existing party. | | `countryCode(): ?string` | | | Country code of the postal address set so far. | | `buildSeller(): SellerParty`, `buildBuyer(): BuyerParty`, `buildPayee(): PayeeParty` | | | Build the party. A missing name, or a missing address of seller or buyer, throws `InvalidInvoiceData`. | Without a VAT identifier, EN 16931 requires a seller identifier (BT-29) or a legal registration identifier (BT-30); otherwise rendering fails with BR-CO-26. Identifiers and electronic addresses take a scheme code. Use the enum case or pass any other code of the lists as string: - `IdentifierScheme` (identifier schemes of ISO 6523, the ICD codes) has the cases `Siren` (0002), `SwedishOrganisationNumber` (0007), `Siret` (0009), `Duns` (0060), `Gln` (0088), `DutchKvk` (0106), `Gtin` (0160), `DanishCvr` (0184), `DutchOin` (0190), `NorwegianOrganisationNumber` (0192), `LeitwegId` (0204) and `BelgianEnterpriseNumber` (0208). - `ElectronicAddressScheme` (EAS, the electronic address schemes) has `Email` (EM), `Siren` (0002), `SwedishOrganisationNumber` (0007), `Siret` (0009), `Duns` (0060), `Gln` (0088), `DutchKvk` (0106), `DanishCvr` (0184), `DutchOin` (0190), `NorwegianOrganisationNumber` (0192), `LeitwegId` (0204), `BelgianEnterpriseNumber` (0208), `AustrianVatNumber` (9914), `AustrianOrganisationNumber` (9915), `GermanVatNumber` (9930) and `FrenchVatNumber` (9957). ### Payee, tax representative and delivery | Method | BT | Description | |---|---|---| | `payee(PayeeParty\|PartyBuilder\|string\|callable $payee, ?string $identifier = null, ?string $legalRegistration = null, IdentifierScheme\|RawCode\|string\|null $identifierScheme = null, IdentifierScheme\|RawCode\|string\|null $legalRegistrationScheme = null)` | BG-10 (BT-59, BT-60, BT-61) | Payee, if different from the seller (e.g. a factoring company). See below. | | `taxRepresentative(string $name, string $vatId, Address $address)` | BG-11 (BT-62, BT-63, BG-12) | The seller's tax representative. | | `deliverTo(?string $recipientName, ?Address $address = null, ?Identifier $locationId = null)` | BG-13 (BT-70, BG-15, BT-71) | Deliver-to party, address and location identifier. Replaces these three values (also with `null`) and keeps the delivery date. | | `delivery(Delivery $delivery)` | BG-13 | Replaces the whole delivery information, including the delivery date. | `payee()` takes one of these: - the name, optionally with the identifier, the legal registration identifier and their schemes (e.g. `'0088'` for a GLN); - a `PayeeParty`; - a `PartyBuilder`, e.g. `Party::make('Factoring AG')->identifier(…)`; - a callable that receives a `PartyBuilder` (`name()`, `identifier()`, `legalRegistration()`). Like `seller()` and `buyer()`, it continues an earlier payee. Identifiers and schemes as arguments are allowed only next to a name; otherwise `payee()` throws an `InvalidArgumentException`. ```php use Quittio\Core\Data\Identifier; use Quittio\Core\Data\Party\Address; use Quittio\Core\Enums\IdentifierScheme; ->deliverTo('Kunde AG, Lager Nord', Address::make('Industriestraße 9', '54322', 'Beispielstadt', 'DE'), Identifier::of('4000001000005', IdentifierScheme::Gln)) ->taxRepresentative('Steuer Vertretung GmbH', 'DE111111125', new Address(countryCode: 'DE', line1: 'Ring 1', city: 'Berlin', postcode: '10115')) ``` `Address::make(?string $line1, ?string $postcode, ?string $city, string $countryCode)` creates an address with the most common fields. The constructor takes `countryCode`, `line1`, `line2`, `line3`, `city`, `postcode` and `subdivision`. `Identifier::of(string $value, IdentifierScheme|RawCode|string|null $scheme = null)` creates an identifier. ## Lines Lines (BG-25) are the goods and services you invoice. Every invoice needs at least one line. | `InvoiceBuilder` method | Description | |---|---| | `line(string\|LineBuilder\|ProvidesInvoiceLine\|array\|callable\|InvoiceLine $line, netPrice, quantity, unit, vat, description, priceInclVat)` | Adds a line (BG-25), in one of the forms below. | | `lines(iterable $lines)` | Adds several lines. Each entry may be any of the forms, e.g. an array of arrays or an Eloquent collection of order items. | | `updateLine(string $id, ?callable $change = null, netPrice, quantity, priceInclVat, name, description)` | Changes an existing line ([Changing existing data](#changing-existing-data)). | | `removeLine(string $id)` | Removes an existing line by its identifier (BT-126) ([Changing existing data](#changing-existing-data)). | A line can be given in these forms. Use the first one that fits: | Form | Use it when | Example | |---|---|---| | The name with named values | the line needs only price, quantity, unit, VAT or description (the usual case) | `->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour)` | | `Line::make()` (`Quittio\Core\Builder\Line`) | the line needs more: item number, line allowance or charge, list price, base quantity, period, sub lines, line group. It takes the same named values and returns the `LineBuilder` for the other methods. | `->line(Line::make('Web development', netPrice: '95.00')->sellerId('WD-1')->allowance(percent: '10', reason: 'Framework agreement'))` | | An application model implementing `ProvidesInvoiceLine` ([Application models](#application-models)) | the lines come from your models | `->lines($order->items)` | | An array whose keys are methods of the `LineBuilder` | the lines come as data, e.g. database rows or a JSON request | `->lines([['name' => 'Hosting', 'quantity' => [1, UnitCode::Month], 'netPrice' => '49.00']])` | | A callable that receives a new `LineBuilder` | rarely; the forms above cover the same | `->line(fn (LineBuilder $l) => $l->name('Travel expenses')->netPrice('120.00'))` | | A finished `InvoiceLine`, taken as it is | you copy a line of existing data | `->line($invoice->lines[0])` | The named arguments of `line()` and `Line::make()` are: | Argument | Meaning | |---|---| | `netPrice` | The price without VAT (BT-146). | | `priceInclVat` | The price including VAT ([Gross price mode](#gross-price-mode)). | | `quantity` | Default 1. | | `unit` | Default `UnitCode::Unit` (C62). | | `vat` | A tax preset or a category without rate. Default: the [default VAT](#default-vat) of the invoice. | | `description` | The item description. | The named arguments are allowed only together with a name; with another form, `line()` throws an `InvalidArgumentException`. A string is always the name of the line, never a callable. A `LineBuilder` is copied when it is added, so the same builder can serve as a template for further lines. ### Lines as arrays In an array, each key names a method of the `LineBuilder` and the value is its argument. The keys are `LineBuilder::ARRAY_KEYS`: `id`, `name`, `description`, `note`, `quantity`, `unit`, `netPrice`, `listPrice`, `priceInclVat`, `vat`, `period`, `allowance`, `charge`, `sellerId`, `buyerId`, `standardId`, `classification`, `originCountry`, `attribute`, `orderLineReference`, `buyerAccountingReference`, `objectId`, `group`, `highlight` and `showGrossPrice`. - A method with several arguments takes an array value, as list or by name: `'quantity' => [4, UnitCode::Hour]`, `'allowance' => ['percent' => '10', 'reason' => 'Early bird']`. - Null values are skipped, so database rows with empty columns can be passed. - An unknown key throws an `InvalidArgumentException`. - `Line::fromArray($array)` and `LineBuilder::fill($array)` do the same for a single builder. ```php use Quittio\Core\Builder\Line; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; $builder->lines([ Line::make('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour), // in a list: Line::make() ['name' => 'Hosting', 'quantity' => [1, UnitCode::Month], 'netPrice' => '49.00'], ['name' => 'Book', 'netPrice' => '39.00', 'vat' => VatPreset::Reduced], ]); ``` ### Line identifiers Every line has an identifier (BT-126). Partial cancellations, `updateLine()` and `removeLine()` refer to lines by it. - A line without `id()` gets its position (1, 2, 3 …). Identifiers that existing lines or other lines' `id()` already use are skipped, so identifiers stay unique after `from()` and `removeLine()`. Example: lines `A`, `B` with `id('1')` and `C` get `2`, `1` and `3`. - Sub lines get `{parent}.{n}`, e.g. `2.1`. - An identifier used twice, also by a sub line, makes `build()` throw `InvalidInvoiceData`. - Data from other sources (JSON, incoming invoices) gets the warning `QUITTIO-LINE-ID-DUPLICATE` instead, because EN 16931 does not require unique identifiers. ### LineBuilder `Quittio\Core\Builder\LineBuilder` is the builder that `Line::make()` returns: | Method | BT | Description | |---|---|---| | `id(string $id)` | BT-126 | Line identifier; default the position ([Line identifiers](#line-identifiers)). | | `name(string $name)` | BT-153 | Item name. Required. | | `description(?string $description)` | BT-154 | Item description. Printed as plain text; with the option `markdown` (Laravel `pdf.markdown`) as Markdown ([Templates](templates.md)). | | `note(?string $note)` | BT-127 | Line note. | | `quantity(BigDecimal\|int\|string $quantity, UnitCode\|RawCode\|string\|null $unit = null)` | BT-129, BT-130 | Invoiced quantity and optionally the unit; default 1. Negative quantities for returns and discounts. | | `unit(UnitCode\|RawCode\|string $unit)` | BT-130 | Unit (UN/ECE Recommendation 20/21), default `UnitCode::Unit` (C62). | | `netPrice(BigDecimal\|int\|string $price, BigDecimal\|int\|string\|null $baseQuantity = null, UnitCode\|RawCode\|string\|null $baseQuantityUnit = null)` | BT-146, BT-149, BT-150 | Net price per base quantity; the base quantity unit defaults to the line unit. Each call replaces the base quantity. | | `listPrice(BigDecimal\|int\|string $grossPrice, BigDecimal\|int\|string\|null $discount = null)` | BT-148, BT-147 | List (gross) price before price discount, and the discount per unit. Without `netPrice()` the net price is list price minus discount. | | `priceInclVat(BigDecimal\|int\|string $price)` | – | Price including VAT ([Gross price mode](#gross-price-mode)); the calculator derives the net price. | | `vat(TaxPreset\|VatCategory $presetOrCategory, BigDecimal\|int\|string\|null $rate = null, ?string $exemptionReason = null, VatExemptionReasonCode\|RawCode\|string\|null $exemptionReasonCode = null)` | BG-30 (BT-151, BT-152) | VAT treatment: a tax preset of a country pack, or category, rate and exemption reason ([VAT](#vat)). Without it, the line takes the [default VAT](#default-vat) of the invoice. | | `period(Date\|string\|null $start, Date\|string\|null $end = null)` | BG-26 (BT-134, BT-135) | Line period. | | `allowance(?string $amount = null, ?string $percent = null, ?string $reason = null, AllowanceReasonCode\|RawCode\|string\|null $reasonCode = null, ?string $baseAmount = null)` | BG-27 | Line allowance: amount or percentage; repeatable ([Line allowances and charges](#line-allowances-and-charges)). | | `charge(?string $amount = null, ?string $percent = null, ?string $reason = null, ChargeReasonCode\|RawCode\|string\|null $reasonCode = null, ?string $baseAmount = null)` | BG-28 | Line charge: amount or percentage; repeatable. | | `sellerId(?string $id)` | BT-155 | Seller's item number. | | `buyerId(?string $id)` | BT-156 | Buyer's item number. | | `standardId(string $value, IdentifierScheme\|RawCode\|string $scheme = IdentifierScheme::Gtin)` | BT-157 | Standard item identifier, default scheme GTIN (0160, the global trade item number). | | `classification(string $code, ItemClassificationScheme\|RawCode\|string $scheme, ?string $version = null)` | BT-158 | Item classification with list identifier and version; repeatable. | | `originCountry(?string $country)` | BT-159 | Country of origin (uppercased). | | `attribute(string $name, string $value)` | BG-32 (BT-160, BT-161) | Item attribute; repeatable. | | `orderLineReference(?string $reference)` | BT-132 | Line of the purchase order. | | `buyerAccountingReference(?string $reference)` | BT-133 | Buyer's accounting reference of the line. | | `objectId(string $value, string\|RawCode\|null $scheme = null)` | BT-128 | Invoiced object identifier of the line, with scheme. | | `subLine(string\|LineBuilder\|ProvidesInvoiceLine\|array\|callable $line, netPrice, quantity, unit, vat, description, priceInclVat)` | – | Adds a sub line, in the forms of `line()` except `InvoiceLine` ([Sub lines](#sub-lines)). | | `reference(DocumentReference $reference)` | – | Line reference to an order, contract, despatch or receiving advice or another document (profile EXTENDED, [Extensions](#extensions)). | | `extended(LineExtendedData $extended)` | – | Line data of profile EXTENDED ([Extensions](#extensions)). | | `group(string $groupId)` | – | Assigns the line to a line group of the PDF (`InvoiceBuilder::group()`, [Presentation and metadata](#presentation-and-metadata)). | | `highlight(bool $highlight = true)` | – | Highlights the line in the PDF (the default templates shade it and mark it in the secondary colour). | | `showGrossPrice(?bool $show = true)` | – | Prints the price including VAT below the net unit price of this line (`false`: not), whatever the presentation's `showGrossPrices` says; `null` follows the presentation. Only for lines with `priceInclVat()`. | | `fromModel(ProvidesInvoiceLine $model)` | – | Lets an application model fill the builder. | | `fill(array $values)` | – | Calls the methods named by the keys of the array ([Lines as arrays](#lines-as-arrays)). | | `build(string $currency, string $defaultId, ?string $locale = null, ?LineBuilder $vatDefault = null): InvoiceLine` | – | Builds the line; called by `InvoiceBuilder`, which passes the [default VAT](#default-vat) as `$vatDefault`. Use it to create an `InvoiceLine` on its own, e.g. `(new LineBuilder)->name('…')->…->build('EUR', '1')`. | `UnitCode` has the cases `Unit` (C62), `Piece` (H87), `Set` (SET), `Pair` (PR), `Dozen` (DZN), `LumpSum` (LS), `Second` (SEC), `Minute` (MIN), `Hour` (HUR), `Day` (DAY), `Week` (WEE), `Month` (MON), `Quarter` (QAN), `Year` (ANN), `Gram` (GRM), `Kilogram` (KGM), `Tonne` (TNE), `Millimetre` (MMT), `Centimetre` (CMT), `Metre` (MTR), `Kilometre` (KMT), `SquareMetre` (MTK), `CubicMetre` (MTQ), `Millilitre` (MLT), `Litre` (LTR), `KilowattHour` (KWH), `Kilowatt` (KWT), `Percent` (P1), `ServiceUnit` (E48), `Package` (XPK), `Box` (XBX) and `Pallet` (XPX). Every other code of the list can be passed as string. `ItemClassificationScheme` (code list UNTDID 7143) has `HarmonisedSystem` (HS), `Unspsc` (TST), `CustomsArticleNumber` (CV), `NationalProductGroup` (GN), `Isbn` (IB), `ProductServiceId` (MP) and `MutuallyDefined` (ZZZ). ### Prices Most lines need only `netPrice`. A price per base quantity (e.g. per 1000 pieces) or a list price with a discount needs `Line::make()`: ```php use Quittio\Core\Builder\Line; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; ->line(Line::make('Paper A4', quantity: 5000, unit: UnitCode::Piece, vat: VatPreset::Standard) ->netPrice('12.50', baseQuantity: 1000)) // 12.50 per 1000 sheets: 62.50 ->line(Line::make('Monitor', quantity: 2, vat: VatPreset::Standard) ->listPrice('299.00', discount: '30.00')) // net price 269.00 ``` The line amount before allowances and charges is round2(quantity × net price / base quantity). Prices must not be negative (BR-27); returns and discounts use a negative quantity. ## VAT Every line has a VAT category (BT-151) and, except for category O (not subject to VAT), a rate (BT-152). Usually you do not set them on every line. Lines without `vat()` take the [default VAT](#default-vat) of the invoice; in Laravel that is 19 % for a German seller. Set the VAT of a line when it differs from the default. Use a tax preset of a country pack or the category: ```php use Quittio\Core\Enums\VatCategory; use Quittio\Core\Enums\VatExemptionReasonCode; use Quittio\Country\DE\Tax\VatPreset; ->vat(VatPreset::Standard) // preset of the German pack ->vat(VatCategory::StandardRate, '19') ->vat(VatCategory::ReverseCharge, '0', 'Reverse charge', VatExemptionReasonCode::ReverseCharge) ->vat(VatCategory::Exempt, '0', 'Exempt according to Art. 132 VAT Directive', 'VATEX-EU-132') ->vat(VatCategory::NotSubjectToVat) // no rate ``` `vat()` is a method of the `LineBuilder`; `line()` and `Line::make()` take a preset or a category without rate as named argument `vat:`. A tax preset is a ready-made VAT treatment of a country pack: category, rate and, for exemptions, the reason text and code, e.g. `VatPreset::Reduced` for the German 7 % rate ([Tax presets](#tax-presets)). Reverse charge (category AE) means that the buyer, not the seller, owes the VAT. With a preset, category, rate, exemption reason and code come from the preset, and the other parameters are ignored. The preset key is kept on the line (`LineVat::$presetKey`). The exemption text of a preset is resolved in the locale of the invoice (`locale()`) when the line is built; the German presets use German without a locale. ### Default VAT Lines and sub lines without `vat()` take the default VAT of the invoice when it is built. The default is, in this order: 1. `defaultVat()` of the builder, with the arguments of `LineBuilder::vat()`, e.g. `->defaultVat(VatPreset::Reduced)` for a bookshop; 2. otherwise the application default, set once with `InvoiceBuilder::useDefaultVat(callable|TaxPreset|null $default)`. It is a preset, or a callable that receives the seller's country code (BT-40) and whether the invoice is flagged as small business invoice (`Meta::FLAG_SMALL_BUSINESS`), and returns a preset or `null`. `Quittio\Core\Tax\CountryDefaultVat` is such a callable: `new CountryDefaultVat(CountryRegistry $countries, ?TaxPreset $preset = null)`. - For normal invoices it returns `$preset`, otherwise the standard preset of the seller's country pack (`CountryDefaults::$vatPreset`, Germany `de.standard`). - For small business invoices it returns the small business preset of the pack (`CountryDefaults::$smallBusinessVatPreset`, Germany `de.small_business`), otherwise `$preset`. A small business ("Kleinunternehmer" in Germany) charges no VAT ([German terms](concepts.md#german-terms)). The Laravel integration installs it with the preset of `quittio.defaults.vat` ([Configuration](configuration.md#defaults)). So in Laravel, lines without `vat()` get 19 % for a German seller. Without Laravel, install it yourself: ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Core\Enums\UnitCode; use Quittio\Core\Tax\CountryDefaultVat; use Quittio\Country\DE\Tax\VatPreset; // Without Laravel, once at start-up InvoiceBuilder::useDefaultVat(new CountryDefaultVat($quittio->countries())); // or a preset: VatPreset::Standard $invoice = InvoiceBuilder::make() // ... ->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour) // 19 % ->line('Book', netPrice: '39.00', vat: VatPreset::Reduced) // 7 % ->build(); ``` The default only fills in what a line leaves open: the built data and the XML carry the VAT of every line, as EN 16931 requires. Without a default, a line without VAT makes `build()` fail (`InvalidInvoiceData`, BG-30). `useDefaultVat(null)` removes the application default. Documents (`DocumentBuilder`) use the same defaults and have `defaultVat()` as well. ### Categories | `VatCategory` | Code | Rate | VAT | Exemption reason (BT-120/BT-121) | Further requirements | |---|---|---|---|---|---| | `StandardRate` | S | greater than 0 (BR-S-05) | rate × taxable amount | not allowed (BR-S-10) | Seller VAT identifier, tax number or tax representative (BR-S-02). One VAT breakdown per rate. | | `ZeroRated` | Z | 0 (BR-Z-05) | 0 | not allowed (BR-Z-10) | As S (BR-Z-02). | | `Exempt` | E | 0 (BR-E-05) | 0 | required (BR-E-10) | As S (BR-E-02). | | `ReverseCharge` | AE | 0 (BR-AE-05) | 0 | required (BR-AE-10), e.g. `VATEX-EU-AE` | As S, and the buyer VAT identifier or legal registration identifier (BR-AE-02). | | `IntraCommunitySupply` | K | 0 (BR-IC-05) | 0 | required (BR-IC-10), e.g. `VATEX-EU-IC` | Seller VAT identifier or tax representative, and buyer VAT identifier (BR-IC-02); delivery date or invoicing period (BR-IC-11); deliver-to country (BR-IC-12). | | `Export` | G | 0 (BR-G-05) | 0 | required (BR-G-10), e.g. `VATEX-EU-G` | Seller VAT identifier or tax representative (BR-G-02). | | `NotSubjectToVat` | O | none (BR-O-05) | 0 | required (BR-O-10), e.g. `VATEX-EU-O` | No VAT identifier of seller, tax representative or buyer (BR-O-02); no other category in the invoice (BR-O-11). | | `CanaryIslands` | L | as given | rate × taxable amount | – | – | | `CeutaMelilla` | M | as given | rate × taxable amount | – | – | | `TransferredVatItaly` | B | as given | rate × taxable amount | – | – | - Categories Z, E, AE, K, G and O allow exactly one VAT breakdown each (BR-Z-01 …). - The calculator calculates VAT 0 for Z, E, AE, K, G and O, whatever rate is given. It writes the rate 0 to the VAT breakdown of a group whose lines have no rate (category O: no rate). The validation still requires the rate 0 on the lines (BR-Z-05 …). - The rules are part of the [validation](validation.md) (level L3). `VatCategory::requiresRate()`, `isZeroTax()` and `requiresExemptionReason()` expose the same properties. `VatExemptionReasonCode` (code list VATEX) has `ReverseCharge` (VATEX-EU-AE), `IntraCommunitySupply` (VATEX-EU-IC), `Export` (VATEX-EU-G), `NotSubjectToVat` (VATEX-EU-O), `TravelAgents` (VATEX-EU-D), `SecondHandGoods` (VATEX-EU-F), `WorksOfArt` (VATEX-EU-I), `CollectorsItems` (VATEX-EU-J), `Article79c` (VATEX-EU-79-C), `Article132` (VATEX-EU-132), `Article143` (VATEX-EU-143), `Article148` (VATEX-EU-148), `Article151` (VATEX-EU-151) and `Article309` (VATEX-EU-309). Other codes of the list can be passed as strings. ### Exemption reasons in the VAT breakdown You give the exemption reason per line, but the XML carries it per VAT breakdown entry (one per category and rate). The calculator therefore collects the reasons of the lines of each group, for the categories that require one (E, AE, K, G, O): - Different texts are joined with "; " in the order of the lines, so that no legally required note is lost. - A code is kept when all lines of the group have the same code. Otherwise it is left out, and the validation reports `QUITTIO-EXEMPTION-CONFLICT`. - The exemption reason of a document level allowance or charge is used only if no line of its group provides one, e.g. for an exempt charge on an otherwise standard rated invoice. ### Tax presets A tax preset implements `Quittio\Core\Tax\TaxPreset` (`key()`, `category()`, `rate()`, `exemptionReasonCode()`, `exemptionReason(?string $locale = null)`). Country packs provide their presets as `TaxPresetCollection` (`$pack->taxPresets()`, with `get(string $key)`, `has()`, `add()`, `count()` and iteration), so a stored preset key can be resolved again: ```php $preset = $quittio->countries()->get('DE')?->taxPresets()->get('de.standard'); ``` The German presets (`VatPreset::Standard`, `Reduced`, `SmallBusiness`, `ReverseChargeEu`, `VatPreset::exempt('§ 4 Nr. 14 UStG')` …) are listed in [Tax presets](country-packs.md#tax-presets); [Presets of your pack](country-packs.md#presets-of-your-pack) shows how a pack provides its own. Create your own presets with `CustomTaxPreset::make(string $key, VatCategory $category, BigDecimal|int|string|null $rate = null, VatExemptionReasonCode|RawCode|string|null $code = null, array|string|null $reason = null)`. Give the reason as text, or as an array of texts per locale (fallback `en`): ```php use Quittio\Core\Enums\VatCategory; use Quittio\Core\Tax\CustomTaxPreset; $exempt = CustomTaxPreset::make('acme.exempt_132', VatCategory::Exempt, '0', 'VATEX-EU-132', [ 'de' => 'Steuerfrei nach Art. 132 MwStSystRL', 'en' => 'Exempt according to Art. 132 VAT Directive', ]); ->line('Course', netPrice: '300.00', vat: $exempt) ``` ## Payment Payment data tells the buyer how and when to pay. It has three parts: the payment instructions (payment means and bank account), the payment terms (due date and cash discount) and amounts already paid. In Laravel, `Quittio::invoice()` already adds a SEPA credit transfer to the first bank account with an IBAN in `config/quittio.php`, so most invoices only add payment terms. ### Payment instructions | Method | BT | Description | |---|---|---| | `payment(PaymentMeansCode\|RawCode\|string $means, ?string $iban = null, ?string $bic = null, ?string $accountName = null, ?string $remittanceInformation = null, ?string $meansText = null)` | BG-16 (BT-81, BT-82, BT-83), BG-17 | Payment means, optionally with one account (IBAN uppercased, spaces removed), the remittance information (the payment reference the buyer should state) and a text for the means. | | `creditTransfer(string $accountId, ?string $bic = null, ?string $accountName = null)` | BG-17 (BT-84, BT-86, BT-85) | Adds a further account. CII writes an IBAN as `IBANID` and any other account identifier as `ProprietaryID`. Without payment instructions, it starts them with `SepaCreditTransfer` (58). | | `paymentCard(string $lastDigits, ?string $holderName = null)` | BG-18 (BT-87, BT-88) | Card data: only the last 4 to 6 digits (more throw an `InvalidArgumentException`). Without payment instructions, it starts them with `BankCard` (48). | | `directDebit(?string $mandateReference, ?string $creditorId = null, ?string $debitedIban = null)` | BG-19 (BT-89, BT-90, BT-91) | SEPA direct debit: mandate reference, creditor identifier, debited account. Without payment instructions, it starts them with `SepaDirectDebit` (59). | A second call of `payment()` replaces the payment instructions: - An account given in the call replaces all accounts. - Without an account, existing accounts are kept for the credit transfer codes 30, 42 and 58 (e.g. the seller's default account set by `Quittio::invoice()`), card data for 48, 54 and 55, and direct debit data for 49 and 59. Everything else is dropped. - The remittance information is kept unless a new one is given. `creditTransfer()`, `paymentCard()` and `directDebit()` add to existing instructions without changing the means code, so call `payment()` with the means first. ```php use Quittio\Core\Enums\PaymentMeansCode; // SEPA credit transfer with two accounts ->payment(PaymentMeansCode::SepaCreditTransfer, iban: 'DE02 1203 0000 0000 2020 51', bic: 'BYLADEM1001', accountName: 'Muster GmbH', remittanceInformation: 'RE-2026-0042') ->creditTransfer('AT61 1904 3002 3457 3201', 'BKAUATWW') // SEPA direct debit (replaces the seller's default account set by Quittio::invoice()) ->payment(PaymentMeansCode::SepaDirectDebit, remittanceInformation: 'RE-2026-0042') ->directDebit('MANDATE-2026-17', creditorId: 'DE98ZZZ09999999999', debitedIban: 'DE02120300000000202051') // Paid by credit card ->payment(PaymentMeansCode::CreditCard, meansText: 'Paid online') ->paymentCard('4242', 'Max Muster') ``` The payment means codes come from the code list UNTDID 4461: | `PaymentMeansCode` | Code | Data | |---|---|---| | `NotDefined` | 1 | – | | `Cash` | 10 | – | | `Cheque` | 20 | – | | `CreditTransfer` | 30 | account required (BR-61) | | `DebitTransfer` | 31 | – | | `PaymentToBankAccount` | 42 | account | | `BankCard` | 48 | card | | `DirectDebit` | 49 | direct debit | | `CreditCard` | 54 | card | | `DebitCard` | 55 | card | | `StandingAgreement` | 57 | – | | `SepaCreditTransfer` | 58 | account required (BR-61) | | `SepaDirectDebit` | 59 | direct debit; XRechnung requires creditor identifier and debited account (BR-DE-30, BR-DE-31) | | `OnlinePaymentService` | 68 | – | | `Clearing` | 97 | – | | `MutuallyDefined` | ZZZ | – | XRechnung requires payment instructions (BR-DE-1). Profile EXTENDED can carry further payment instructions (`ExtendedData::$additionalPaymentInstructions`, [Extensions](#extensions)). ### Payment terms Set the payment terms (BT-20) with these methods. Each continues the terms set before, also those of `InvoiceBuilder::from()` or of the seller's defaults: | Method | Description | |---|---| | `dueInDays(int $days)` | Payment target: the calculator sets the due date (BT-9) to issue date + days, unless `dueDate()` is set. | | `cashDiscount(int $days, string $percent, ?string $baseAmount = null)` | Cash discount ("Skonto") for payment within `$days` days after the issue date, optionally on a base amount. Repeatable for several tiers. The percentage is rounded to two decimals. | | `lateFee(?int $days = null, ?string $percent = null, ?string $text = null)` | Late payment terms. | | `paymentTerms(PaymentTerms\|string\|callable $terms)` | A text or a `PaymentTerms` object replaces the earlier terms, e.g. to start over. A callable receives the `PaymentTermsBuilder` (`text()` and the three methods above) and continues them. | ```php ->dueInDays(30) ->cashDiscount(days: 10, percent: '3.00') ->cashDiscount(days: 20, percent: '2.00', baseAmount: '1000.00') ``` `InvoiceBuilder::from($draft)->cashDiscount(days: 7, percent: '2.00')` adds a cash discount to the terms of the draft. A cash discount is an agreed reduction of the price, so it must be part of the XML, not only of the PDF. Quittio therefore writes the terms into the text BT-20, generated in the language of the rendering run: - Without `text()`, `dueInDays()` adds "Payable within 30 days without deduction." (German: "Zahlbar innerhalb von 30 Tagen ohne Abzug."). - Each cash discount adds a line such as "3.00 % cash discount for payment within 10 days (until 2026-10-13)." (German: "3 % Skonto bei Zahlung innerhalb von 10 Tagen (bis 13.10.2026)."). The late fee text follows. - XRechnung uses its structured notation instead (BR-DE-18): the texts first, then one line `#SKONTO#TAGE=10#PROZENT=3.00#` per cash discount (German for cash discount, days and percent), each closed by a line break. A base amount adds `BASISBETRAG=1000.00#`. - Profile EXTENDED additionally writes the cash discounts (one payment terms group per tier) and the late fee days and percentage as structured CII elements. ### Prepaid amounts and rounding | Method | BT | Description | |---|---|---| | `prepaid(BigDecimal\|int\|string $amount)` | BT-113 | Amount already paid, e.g. received advance payments; replaces an earlier value. | | `addPrepaid(BigDecimal\|int\|string $amount)` | BT-113 | Adds to the amount already paid, e.g. one of several advance payments. | | `rounding(BigDecimal\|int\|string $amount)` | BT-114 | Rounding amount, e.g. cash rounding to 0.05. Never use it to fix wrong totals. | The amount due is BT-115 = gross total − BT-113 + BT-114. For final invoices, the German pack offers `deductAdvance($advanceInvoice)`: it references the advance invoice and adds its gross amount to BT-113 ([Builder macros](country-packs.md#builder-macros)). ## References References link the invoice to other documents, e.g. the buyer's purchase order or a contract. All of them are optional in EN 16931. The one you need most often is the buyer reference (BT-10): XRechnung requires it, and for German public buyers it holds the Leitweg-ID, the routing ID of the authority ([German terms](concepts.md#german-terms)). | Method | BT | Description | |---|---|---| | `buyerReference(?string $reference)` | BT-10 | Buyer reference; required by XRechnung, for German public buyers the Leitweg-ID. | | `projectReference(string $id, ?string $name = null)` | BT-11 | Project reference. CII requires a name and writes the identifier when none is given. | | `contractReference(?string $reference)` | BT-12 | Contract reference. | | `purchaseOrderReference(?string $reference)` | BT-13 | Purchase order reference of the buyer. | | `salesOrderReference(?string $reference)` | BT-14 | Sales order reference of the seller. | | `receivingAdviceReference(?string $reference)` | BT-15 | Receiving advice reference. | | `despatchAdviceReference(?string $reference)` | BT-16 | Despatch advice (delivery note) reference. | | `tenderReference(?string $reference)` | BT-17 | Tender or lot reference. | | `invoicedObject(string $value, string\|RawCode\|null $scheme = null)` | BT-18 | Invoiced object identifier (e.g. a meter or subscription), with the scheme from the code list UNTDID 1153, written as `ReferenceTypeCode` (CII) or `schemeID` (UBL). | | `buyerAccountingReference(?string $reference)` | BT-19 | Buyer's accounting reference (cost centre). | | `precedingInvoice(string $number, Date\|DateTimeInterface\|string\|null $issueDate = null)` | BG-3 (BT-25, BT-26) | Reference to a preceding invoice; repeatable. CII carries only the first reference, UBL all of them. | Lines have their own references (`orderLineReference()`, `buyerAccountingReference()`, `objectId()`, see [LineBuilder](#linebuilder)). ## Notes `note(string $content, NoteSubjectCode|RawCode|string|null $subject = null)` adds an invoice note (BG-1): a free text (BT-22) with an optional subject code (BT-21). Call it several times for several notes. UBL writes a note with subject code as `#CODE#text`. | `NoteSubjectCode` | Code | |---|---| | `GeneralInformation` | AAI | | `PriceConditions` | AAK | | `LegalInformation` | ABL | | `Introduction` | ACY | | `PaymentDetail` | PMD | | `PaymentInformation` | PMT | | `RegulatoryInformation` | REG | | `SupplierRemarks` | SUR | | `TaxDeclaration` | TXD | Cancellations take over the notes with subject `REG` and `TXD` of the original ([Cancellations and corrections](#cancellations-and-corrections)). ## Supporting documents `attachment(string $id, ?string $content = null, MimeType|string|null $mimeType = null, ?string $filename = null, ?string $description = null, ?string $externalUri = null)` adds a supporting document (BG-24), e.g. a timesheet. It is repeatable. A supporting document is either embedded or a link: - Embedded: `$content` are the raw bytes; the XML carries them base64 encoded. An embedded document requires MIME type and file name (`InvalidArgumentException` otherwise). - Link: `$externalUri`. ```php use Quittio\Core\Enums\MimeType; ->attachment('Timesheet-09', file_get_contents($path), MimeType::Pdf, 'timesheet-2026-09.pdf', 'Timesheet September') ->attachment('Contract', externalUri: 'https://example.com/contracts/4711.pdf', description: 'Framework contract') ``` - `MimeType` has `Pdf` (application/pdf), `Png` (image/png), `Jpeg` (image/jpeg), `Csv` (text/csv), `Xlsx` (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) and `Ods` (application/vnd.oasis.opendocument.spreadsheet), the MIME codes EN 16931 allows. Another string fails with a `ValueError`. - Supporting documents are part of the XML from profile EN 16931 upwards, not in the profiles MINIMUM, BASIC WL and BASIC. They are not separate files in the PDF ([Formats and profiles](formats.md#embedded-files)). - XRechnung requires unique file names (BR-DE-22). ## Presentation and metadata These methods set the design of the PDF and HTML, the language of the invoice and data for templates and country packs. | Method | Description | |---|---| | `presentation(Presentation\|array\|callable $presentation)` | Design of this invoice in PDF and HTML: logo, colours, font, texts, footer, line groups, gross prices, payment QR code, watermark, paper, labels, title … A `Presentation` replaces it, an array changes single properties (`['introText' => '…']`), a callable receives the current `Presentation` (or an empty one) and returns the new one. See [Templates and design](templates.md). | | `group(string $id, string $title, bool $showSubtotal = true)` | Defines a line group of the PDF; lines join it with `LineBuilder::group($id)`. | | `locale(string $locale)` | Language of the invoice: labels, generated texts, exemption texts of presets. Rendering uses the locale of the call, then this one, then the configured locale, then the country pack default. | | `timezone(string $timezone)` | Time zone for dates (see [Dates](#dates)). | | `custom(string $key, mixed $value)` | Free data for templates (view model key `meta.custom`). | | `source(string $source)` | Origin of the data, e.g. `shop` (view model key `meta.source`). | | `flag(string $flag, bool $enabled = true)` | Sets or removes a flag that country packs evaluate, e.g. `Meta::FLAG_SMALL_BUSINESS` (`small_business`) or the German `Quittio\Country\DE\Support\Flags::SMALL_AMOUNT` (`de.small_amount`, see [Meta flags](country-packs.md#meta-flags)). | ```php use Quittio\Core\Builder\Line; use Quittio\Country\DE\Tax\VatPreset; ->presentation(['introText' => 'Thank you for your order.', 'showGrossPrices' => true]) ->group('hardware', 'Hardware') ->line(Line::make('Notebook', netPrice: '1200.00', vat: VatPreset::Standard)->group('hardware')) // group(): Line::make() ``` Quittio sets some flags itself; do not set them: `declared_amounts`, `declared_amounts_recalculated`, `cancellation_vat_aligned` and those with the prefixes `amounts_fingerprint:` and `declared_vat_in_tax_currency:` ([Declared amounts](#declared-amounts)). ## Application models Your own models can fill the builder, so a customer becomes the buyer and order items become lines. Implement one of three contracts in `Quittio\Core\Contracts`: | Contract | Method | Used by | |---|---|---| | `ProvidesParty` | `fillParty(PartyBuilder $party): void` | `InvoiceBuilder::buyer($model)` and `seller($model)`, `PartyBuilder::fromModel($model)`, e.g. a customer or user as buyer | | `ProvidesInvoiceLine` | `fillLine(LineBuilder $line): void` | `InvoiceBuilder::line($model)`, `lines($models)`, `LineBuilder::fromModel($model)` | | `ProvidesInvoiceData` | `toInvoiceData(): InvoiceData` | `Quittio::make($model)` (core and Laravel), `Invoice::draftFor($model)` and `HasInvoices::draftInvoice()` in the [Managed layer](managed.md) | ```php use Quittio\Core\Builder\LineBuilder; use Quittio\Core\Contracts\ProvidesInvoiceData; use Quittio\Core\Contracts\ProvidesInvoiceLine; use Quittio\Core\Data\InvoiceData; use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; class OrderItem extends Model implements ProvidesInvoiceLine { public function fillLine(LineBuilder $line): void { $line->id((string) $this->position) ->name($this->product_name) ->sellerId($this->sku) ->quantity((string) $this->quantity, UnitCode::Piece) ->netPrice($this->unit_price) // decimal string from the database ->vat(VatPreset::from($this->tax_preset)); // stored preset key, e.g. "de.standard" } } class Order extends Model implements ProvidesInvoiceData { public function toInvoiceData(): InvoiceData { return Quittio::invoice() ->number($this->invoice_number) ->buyer($this->customer) // ProvidesParty ->lines($this->items) ->build(); } } ``` `fillParty()` and `fillLine()` may set any value. `buyer($model)`, `seller($model)`, `line($model)` and `lines($models)` pass them a fresh builder. `PartyBuilder::fromModel()` and `LineBuilder::fromModel()` pass the builder they are called on, e.g. the builder of the party set before inside a `buyer()` callable. The invoice keeps a copy, so changing the model later does not change invoices that were already built. Store amounts as decimal strings (database `decimal` columns), not as floats. ## Currency and exchange rate Invoices are in euros by default. Set another invoice currency with `currency()`; all amounts of the invoice are then in that currency. If the VAT must also be stated in another currency, the VAT accounting currency (BT-6), set it with `taxCurrency()` together with the exchange rate. | Method | BT | Description | |---|---|---| | `currency(string $currency)` | BT-5 | Invoice currency (ISO 4217, uppercased), default `EUR`. All amounts of the invoice are in this currency. | | `taxCurrency(string $currency, BigDecimal\|int\|string\|null $exchangeRate = null, Date\|DateTimeInterface\|string\|null $rateDate = null)` | BT-6 | VAT accounting currency; with a rate, also sets `exchangeRate()`. | | `exchangeRate(BigDecimal\|int\|string $rate, Date\|DateTimeInterface\|string\|null $date = null)` | – | Units of the VAT accounting currency per unit of the invoice currency. | ```php ->currency('USD') ->taxCurrency('EUR', exchangeRate: '0.9234', rateDate: '2026-10-02') ``` The calculator then sets the VAT total in accounting currency: BT-111 = round2(BT-110 × rate), where BT-110 is the VAT total in the invoice currency. - Without a rate, BT-111 stays empty (except for a VAT total of zero) and the validation fails with BR-53. - BT-6 is written only if it differs from BT-5. - The rate itself appears in the XML only in profile EXTENDED (CII `TaxApplicableTradeCurrencyExchange`). Data read from XML has no exchange rate; see [Declared amounts](#declared-amounts) for how `from()` handles it. ## Allowances and charges An allowance reduces an amount, a charge increases it, e.g. a discount or a shipping fee. Both exist on two levels: - **Line allowances and charges** (BG-27, BG-28) change the net amount of one line. They need `Line::make()`. - **Document level allowances and charges** (BG-20, BG-21) belong to one VAT group of the whole invoice. For a discount on the whole invoice, `discountPercent()` and `discountAmount()` split it over the VAT groups for you. Each one has an amount, or a percentage of a base amount. EN 16931 requires a reason text or a reason code ([Reason codes](#reason-codes)). Invoices with prices including VAT cannot have allowances or charges ([Gross price mode](#gross-price-mode)). ### Line allowances and charges A line allowance or charge has an amount, or a percentage of a base amount. Without a base amount, the base is the line amount before allowances and charges (quantity × price); the calculator writes it to BT-137/BT-142. ```php use Quittio\Core\Builder\Line; use Quittio\Country\DE\Tax\VatPreset; // Line allowances and charges need Line::make() ->line(Line::make('Seminar', netPrice: '400.00', quantity: 3, vat: VatPreset::Standard) ->allowance(percent: '10', reason: 'Early bird') // 120.00 of 1200.00 ->charge('25.00', reason: 'Materials', reasonCode: 'ABK')) ``` ### Document level allowances and charges | Method | Description | |---|---| | `allowance(VatCategory $category, BigDecimal\|int\|string\|null $rate, ?string $amount = null, ?string $percent = null, ?string $reason = null, AllowanceReasonCode\|RawCode\|string\|null $reasonCode = null, ?string $baseAmount = null, ?string $exemptionReason = null, VatExemptionReasonCode\|RawCode\|string\|null $exemptionReasonCode = null)` | Document level allowance (BG-20: BT-92 to BT-98) for one VAT category and rate. | | `charge(VatCategory $category, BigDecimal\|int\|string\|null $rate, ?string $amount = null, ?string $percent = null, ?string $reason = null, ChargeReasonCode\|RawCode\|string\|null $reasonCode = null, ?string $baseAmount = null, ?string $exemptionReason = null, VatExemptionReasonCode\|RawCode\|string\|null $exemptionReasonCode = null)` | Document level charge (BG-21: BT-99 to BT-105). | | `discountPercent(BigDecimal\|int\|string $percent, ?string $reason = null, AllowanceReasonCode\|RawCode\|string\|null $reasonCode = AllowanceReasonCode::Discount)` | Percentage discount on all lines, split into one allowance per VAT group. | | `discountAmount(BigDecimal\|int\|string $amount, ?string $reason = null, AllowanceReasonCode\|RawCode\|string\|null $reasonCode = AllowanceReasonCode::Discount)` | Absolute discount, distributed over the VAT groups in proportion to their line sums. | A document level allowance or charge belongs to exactly one VAT group, given by category and rate. Without a base amount, the base is the sum of the line net amounts of its VAT group; the calculator writes it to BT-93/BT-100. Its exemption reason is used for the VAT breakdown as described in [Exemption reasons in the VAT breakdown](#exemption-reasons-in-the-vat-breakdown). ```php use Quittio\Core\Enums\ChargeReasonCode; use Quittio\Core\Enums\VatCategory; ->charge(VatCategory::StandardRate, '19', amount: '9.90', reason: 'Shipping', reasonCode: ChargeReasonCode::Freight) ->allowance(VatCategory::StandardRate, '19', percent: '5', reason: 'Loyalty discount') ->discountPercent('3', 'Framework agreement') // one allowance per VAT group ``` `discountPercent()` and `discountAmount()` split the discount when the invoice is built ([Discount splitting](#discount-splitting)): - Each VAT group gets one allowance, in proportion to the signed line sum of the group. A group with a negative sum (only returns) receives a negative part. - Rounding differences are distributed with the largest remainder method, so the parts add up exactly to the discount. - A part keeps percentage and base amount only if it equals round2(base × percentage / 100). Otherwise only the amount is written, because validators check that formula (e.g. PEPPOL-EN16931-R040). - A discount of 0 adds nothing. A discount on lines that add up to zero throws a `CalculationException`. ### Reason codes Use these codes for the `reasonCode` of allowances and charges on both levels. Other codes of the lists can be passed as strings. - `AllowanceReasonCode` (code list UNTDID 5189): `BonusForWorksAheadOfSchedule` (41), `OtherBonus` (42), `ManufacturersConsumerDiscount` (60), `DueToMilitaryStatus` (62), `DueToWorkAccident` (63), `SpecialAgreement` (64), `ProductionErrorDiscount` (65), `NewOutletDiscount` (66), `SampleDiscount` (67), `EndOfRangeDiscount` (68), `IncotermDiscount` (70), `PointOfSalesThresholdAllowance` (71), `MaterialSurchargeDeduction` (88), `Discount` (95), `SpecialRebate` (100), `FixedLongTerm` (102), `Temporary` (103), `Standard` (104), `YearlyTurnover` (105). - `ChargeReasonCode` (code list UNTDID 7161): `Advertising` (AA), `Telecommunication` (AAA), `Miscellaneous` (ABK), `AdditionalPackaging` (ABL), `OtherServices` (ADR), `PickUp` (ADT), `Delivery` (DL), `Freight` (FC), `Financing` (FI), `Labelling` (LA), `Packing` (PC), `SpecialHandling` (SH). ## Sub lines Sub lines divide a line into parts, e.g. a workstation that consists of a computer and a monitor. The parent line needs `Line::make()`. `subLine()` takes the same forms as `line()`, usually the name with named values: ```php use Quittio\Core\Builder\Line; use Quittio\Country\DE\Tax\VatPreset; ->line(Line::make('Workstation', vat: VatPreset::Standard) ->subLine('Computer', netPrice: '900.00', vat: VatPreset::Standard) ->subLine('Monitor', netPrice: '250.00', quantity: 2, vat: VatPreset::Standard)) ``` - A parent line without quantity has the quantity 1. Without its own price, it takes the sum of its sub lines as price. - Sub lines exist only in two profiles: EXTENDED (CII, with parent line identifier and line status `GROUP`/`DETAIL`) and the XRechnung extension (UBL only, `->format('xrechnung-ubl')->profile('xrechnung-extension')`). - The automatic format choice upgrades ZUGFeRD and CII to EXTENDED ([Automatic choice](formats.md#automatic-choice)). Other profiles reject sub lines (`QUITTIO-PROFILE-SUBLINES`). ## Gross price mode Shops and B2C invoices often work with prices including VAT. Give every line a `priceInclVat` instead of a `netPrice`. The calculator then derives the net prices so that the customer pays exactly the gross amount ([Gross price conversion](#gross-price-conversion)): ```php use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; Quittio::invoice() // ... ->line('T-shirt', priceInclVat: '19.99', quantity: 3, vat: VatPreset::Standard) ->line('Shipping', priceInclVat: '4.90', vat: VatPreset::Standard) ->line('Voucher', priceInclVat: '5.00', quantity: -1, vat: VatPreset::Standard) ->build(); ``` Restrictions: - All lines of the invoice use `priceInclVat()`. - Gross price lines have no line allowances, charges or sub lines. - The invoice has no document level allowances or charges. Model shipping and discounts as lines, a discount with a negative quantity. Violations throw a `CalculationException` when the invoice is calculated. To change a price later, `InvoiceLine::withPriceInclVat()` changes a gross price line and `withNetPrice()` a net price line. Each throws an `InvalidArgumentException` for a line of the other mode. ## Changing existing data `InvoiceBuilder::from(InvoiceData $data)` takes over every value of the data, including lines, document allowances and charges, payment data, presentation and metadata. Drafts, stored invoices and read invoices can then be changed with the normal builder methods. Existing lines are changed with `updateLine()` and removed with `removeLine()`: ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Core\Builder\PartyBuilder; use Quittio\Core\Data\Lines\InvoiceLine; use Quittio\Country\DE\Tax\VatPreset; $changed = InvoiceBuilder::from($draft) ->issueDate('2026-10-20') ->buyer(fn (PartyBuilder $b) => $b->contact('Lena Kunde', null, 'lena@kunde.de')) // changes the existing buyer ->updateLine('2', netPrice: '80.00', description: 'Price adjusted') ->updateLine('4', fn (InvoiceLine $line) => $line->with(['note' => 'Delivered in two parts'])) ->removeLine('3') ->line('Travel expenses', netPrice: '120.00', vat: VatPreset::Standard) ->build(); ``` `updateLine(string $id, ?callable $change = null, netPrice, quantity, priceInclVat, name, description)` changes a line in one of two ways: with the named values (`->updateLine('2', netPrice: '80.00', quantity: 3)`), or with a callable that receives the `InvoiceLine` and returns the changed one. `removeLine(string $id)` removes a line by its identifier (BT-126, [Line identifiers](#line-identifiers)). Both work only on lines that already exist as `InvoiceLine`: the lines of data passed to `InvoiceBuilder::from()` and lines added as `InvoiceLine`. Lines added in the other forms are built only in `build()`, so they cannot be changed or removed by identifier. An unknown identifier throws an `InvalidArgumentException`. What happens to the amounts: - Totals and the VAT breakdown are not taken over (except [declared amounts](#declared-amounts)); the result is calculated again when rendered. - Percentage document allowances and charges whose base is the line sum of their VAT group follow the lines when lines are added, changed or removed. So do discounts that `discountPercent()` or `discountAmount()` split over the VAT groups. They are recalculated or split again: a VAT group without lines keeps no allowance of 0.00, and a new VAT group gets its part. - `updateLine()` resets the line net amount and recalculates percentage line allowances and charges from the new line amount. A base amount that equalled the previous line amount follows the new one. - `InvoiceLine::withNetPrice()` drops list price and discount. It throws for a line with a price including VAT, and `withPriceInclVat()` throws for a line with a net price (the gross price mode applies to all lines). - `dueInDays()`, `cashDiscount()` and `lateFee()` continue the existing payment terms; a text passed to `paymentTerms()` replaces them. Seller and buyer callables continue the existing parties. ## Cancellations and corrections A cancellation ("Stornorechnung", cancellation invoice) is a new invoice with its own number. It reverses an issued invoice and refers to it (BG-3); the original is never changed. Build it with `cancellation()` of the `Quittio` instance or the Laravel facade. It returns an `InvoiceBuilder`: ```php use Quittio\Laravel\Facades\Quittio; $builder = $quittio->cancellation($original, 'ST-2026-0001', reason: 'Charged twice'); // core instance $builder = Quittio::cancellation($original, 'ST-2026-0001', reason: 'Charged twice'); // Laravel facade $result = $quittio->make($builder)->render(); ``` In Laravel, a rendered invoice can also be cancelled with `$result->cancel(…)` ([Getting started](getting-started.md#cancellation)). `cancellation(InvoiceData $original, string $number, ?string $reason = null, ?string $strategy = null, ?Date $issueDate = null, ?array $lines = null, ?CancellationHistory $history = null, ?string $title = null): InvoiceBuilder` | Parameter | Description | |---|---| | `$original` | The issued invoice, calculated (e.g. `RenderedInvoice::data()`, a stored snapshot or read data). A cancellation reverses its amounts exactly, also amounts another system rounded ([declared amounts](#declared-amounts)). | | `$number` | Number of the cancellation. | | `$reason` | Appended to the cancellation note ("Reason: …"). | | `$strategy` | `credit_note` or `negative_invoice` (`CorrectionPolicy::CANCELLATION_CREDIT_NOTE`, `CANCELLATION_NEGATIVE_INVOICE`, see [Sign strategies](#sign-strategies)). Default: the configured `corrections.cancellation_strategy`, then the country pack (Germany: `credit_note`). Another value throws an `InvalidArgumentException`. | | `$issueDate` | A `Date` (`Date::parse('2026-10-20')`), default today in the time zone of the original. | | `$lines` | Partial cancellation ([below](#partial-cancellations)); `null` cancels the whole invoice. | | `$history` | Earlier partial cancellations of the original, for the cumulative VAT rounding ([below](#cumulative-vat-rounding)). | | `$title` | Document title in PDF, HTML and file name (`Presentation::$title`), e.g. "Stornorechnung". Default: the title `cancellation` of the country pack's correction policy (`cancellation_self_billed` for self-billed originals); without one, the label of the type code. | The leading country pack of the original ([Country packs](country-packs.md)) provides the correction policy and the locale of the texts: the locale of the original, then the configured locale, then the pack default. Without a `Quittio` instance, `Cancellation::of(InvoiceData $original, string $number, string $strategy = 'credit_note', ?string $reason = null, ?Date $issueDate = null, ?string $locale = null, ?string $title = null, ?array $lines = null, ?CorrectionPolicy $policy = null, ?CancellationHistory $history = null)` (`Quittio\Core\Corrections\Cancellation`) does the same. Note the different parameter order, and that without `$title` or `$policy` the document title comes from the type code. The cancellation is a copy of the original with these changes: | Field | Value | |---|---| | Number, issue date | `$number`, `$issueDate` | | Type code | by strategy, see [Sign strategies](#sign-strategies) | | Preceding invoice (BG-3) | the original's number and issue date (replaces earlier references) | | Notes (BG-1) | "Stornierung der Rechnung RE-2026-0042 vom 03.10.2026." ("Cancellation of invoice … of …"; for self-billed originals "Stornierung der Gutschrift …", "Cancellation of the self-billed invoice …") with subject `AAI`, plus the reason. The original's notes with subject `REG` and `TXD` are kept, others dropped. | | Payment terms, due date | "Der Betrag wird mit offenen Forderungen verrechnet oder erstattet." ("The amount will be offset against open items or refunded."); no due date | | Lines, allowances, charges | all lines with document allowances and charges, or the selected lines | | Prepaid amount BT-113, rounding amount BT-114, third party payments | taken over by a full cancellation, so that the amounts due match; not by a partial one | | Title in PDF and file name | from the correction policy (Germany: "Stornorechnung", cancellation invoice; for self-billed originals "Storno-Gutschrift", cancellation of the self-billed invoice) | Everything else stays as in the original: parties, references, payment instructions, delivery, currency, attachments and presentation. The result is an `InvoiceBuilder`, so you can change it before rendering, e.g. `->note('…')` or `->payment(PaymentMeansCode::SepaCreditTransfer, iban: '…')` for the refund account. Changing lines or allowances recalculates the amounts. ### Sign strategies A cancellation reverses the amounts in one of two ways: as a credit note with positive amounts (`credit_note`) or as an invoice with negative amounts (`negative_invoice`). | Original | `credit_note` | `negative_invoice` | |---|---|---| | Invoice (380, 384, 326 …) | 381, positive amounts | 380, negative quantities and amounts | | Credit note (`isCreditNote()`) | 380, positive amounts | 380, positive amounts | | Self-billed invoice (389) or debit note (527) | 261, positive amounts | 389, negative quantities and amounts | | Self-billed credit note (261) | 389, positive amounts | 389, positive amounts | - A credit note is reversed by an invoice with the same positive amounts in both strategies; a second credit note would credit twice. - With `negative_invoice`, the quantities, line net amounts, line and document allowances and charges (amounts and base amounts), BT-113, BT-114 and third party payments are negated. Prices stay positive (BR-27). - Self-billed documents stay self-billed. - Both strategies are permitted in Germany. The German pack titles the document "Stornorechnung", never "Gutschrift" (in German a self-billed invoice), except for self-billing. - UBL writes a cancellation with `credit_note` as `CreditNote`. The XRechnung extension has no UBL credit note: use `negative_invoice` there. ### Partial cancellations Pass `lines:` to cancel only some lines, quantities or amounts: ```php $builder = $quittio->cancellation($original, 'ST-2026-0002', reason: 'Partial return', lines: [ ['line' => '3'], // the whole line ['line' => '1', 'quantity' => '1'], // 1 of 3 ['line' => '2', 'amount' => '50.00'], // an amount, e.g. a price reduction ]); ``` - Each entry names a line identifier (BT-126) once, with a `quantity`, an `amount` or neither (the whole line). Quantity and amount together throw an `InvalidArgumentException`, as do an unknown line or an empty list. - Quantities and amounts are magnitudes: a negative line (discount, return) keeps its sign and is reversed with it. - A quantity must be greater than 0 and at most the invoiced quantity. Line allowances and charges are scaled in proportion. Lines with sub lines can only be cancelled as a whole. - An amount becomes one unit (C62) at that net price, without allowances and charges. It must be greater than 0 and at most the line net amount, so the original must be calculated. For lines with a price including VAT, the amount includes VAT and is compared with the gross line amount. - Whole lines keep the original's declared net amounts; quantities and amounts are recalculated. - Invoices with document level allowances or charges cannot be cancelled partially, because these cannot be assigned to single lines reliably. Cancel fully and issue a new invoice. - Each part is rounded on its own: three cancellations of one unit of a line of 29.00 net for 3 units give 9.67 each. Cancel the remainder of a line as an amount (`['line' => '1', 'amount' => '9.66']`), so that the parts add up exactly to the line. The Managed layer tracks this per line ([Managed layer](managed.md#cancellations-and-corrections)). ### Cumulative VAT rounding VAT is rounded per document and VAT group. If each partial cancellation is rounded on its own, the parts together can differ from the original by a cent per part. Example: three parts of 9.67, 9.67 and 9.66 net at 19 % carry 3 × 1.84 = 5.52 VAT, the original 5.51. With a `CancellationHistory` of the earlier partial cancellations, the VAT is rounded cumulatively per VAT group: - S = taxable amount of the earlier cancellations + taxable amount of this one, - target = round2(S × rate / 100), or the original's VAT BT-117 when this cancellation closes the group (all its lines are cancelled completely afterwards), - VAT of this cancellation = target − VAT of the earlier cancellations. The three parts then carry 1.84, 1.83 and 1.84 = 5.51. - Every part differs by at most 0.01 from round2(own taxable amount × rate) and keeps the sign of its taxable amount. - The validation reports the difference as information `QUITTIO-CANCELLATION-VAT-ALIGNED`, and the cancellation note says "The VAT is aligned with the amounts already cancelled.". - For an original whose VAT another system rounded (BT-117 ≠ round2(BT-116 × rate)), the target uses its effective rate BT-117 / BT-116, which spreads the difference over the parts. This does not apply to groups with positive and negative lines. - In gross price mode, the taxable amounts follow the same way from the gross amounts cancelled so far, and the rounding amounts (BT-114) of all parts add up to the original's. - BT-111 in accounting currency is aligned the same way at the original's exchange rate. - Without a history, or for an original whose amounts are stale, the VAT is rounded per document. The Managed layer keeps the history itself. Without it, build the history from the cancellations as issued: ```php use Quittio\Core\Corrections\CancellationHistory; // $original: 3 × 10.00 with a line allowance of 1.00 at 19 %, 29.00 net, 5.51 VAT $history = new CancellationHistory; // no earlier partial cancellation $first = $quittio->make($quittio->cancellation($original, 'ST-1', lines: [['line' => '1', 'quantity' => '1']], history: $history))->render(); $history = $history->withCancellation($original, $first->data()); $second = $quittio->make($quittio->cancellation($original, 'ST-2', lines: [['line' => '1', 'quantity' => '1']], history: $history))->render(); $history = $history->withCancellation($original, $second->data()); // The last part cancels what is left of line 1 and closes the VAT group S|19.00 $history = new CancellationHistory($history->groups, closes: ['S|19.00']); $third = $quittio->make($quittio->cancellation($original, 'ST-3', lines: [['line' => '1', 'amount' => '9.66']], history: $history))->render(); ``` | `CancellationHistory` | Description | |---|---| | `new CancellationHistory(array $groups = [], array $closes = [])` | `$groups`: per VAT group key (`DeclaredAmounts::breakdownKey()`, e.g. `S\|19.00`, `S\|7.00`, `E\|0.00`, `O\|`) the sums of the earlier cancellations as `BigDecimal`: `taxable` (BT-116), `tax` (BT-117), `taxInTaxCurrency` (share of BT-111, null if unknown) and in gross price mode `gross` (null or missing if unknown), in the original's direction. `$closes`: keys of the VAT groups whose lines are all cancelled completely once this cancellation is issued, including groups closed earlier. | | `withCancellation(InvoiceData $original, InvoiceData $cancellation, array $closes = []): self` | The history after an issued (calculated) cancellation; `$closes` for the next one. | | `CancellationHistory::entries(InvoiceData $original, InvoiceData $cancellation)` | The VAT groups of an issued cancellation in the original's direction. | | `taxable(string $key)`, `tax(string $key)`, `gross(string $key)`, `closes(string $key)`, `taxInTaxCurrency()` | Values of the history. | | `align(InvoiceData $original, InvoiceData $cancellation)` | The aligned VAT breakdown and BT-111 of a calculated partial cancellation (used by `cancellation()`). | | `assertFits(InvoiceData $original, InvoiceData $cancellation, array $aligned)` | Throws an `InvalidArgumentException` for a history that does not fit the original: unknown VAT groups, more taxable amount than the original's lines, or a VAT more than 0.01 off or of the wrong sign. A history built with `withCancellation()` from the issued cancellations always fits. | | `matches(InvoiceData $original, InvoiceData $cancellation): bool` | Whether an issued cancellation carries the VAT this history gives it; false e.g. after another partial cancellation was issued meanwhile. | Amounts in the history are signed in the original's direction: a credit note that cancels an invoice and an invoice that cancels a credit note count with their amounts, a negative invoice with its amounts negated. ### Correction policy A country pack defines its defaults for cancellations and corrections in `Quittio\Core\Countries\CorrectionPolicy`: - `new CorrectionPolicy(string $cancellationStrategy = 'credit_note', string $correctionStrategy = 'cancel_and_reissue', array $titles = [])`, with the constants `CANCELLATION_CREDIT_NOTE`, `CANCELLATION_NEGATIVE_INVOICE`, `CORRECTION_CANCEL_AND_REISSUE` and `CORRECTION_CORRECTED_INVOICE`. - `$titles` maps a document kind to titles per locale (`['cancellation' => ['de' => 'Stornorechnung', 'en' => 'Cancellation invoice']]`). `title(string $kind, string $locale = 'en')` and `cancellationTitle(bool $selfBilled, string $locale = 'en')` read them; self-billed originals use `cancellation_self_billed` if defined. - The correction strategy is used by the Managed layer. In the core, correct an invoice by cancelling it and issuing a new one (`InvoiceBuilder::from($original)->number(…)`), or with a corrected invoice (type code 384, [Credit notes, corrected invoices and self-billing](#credit-notes-corrected-invoices-and-self-billing)). ## Extensions Profile EXTENDED of ZUGFeRD/Factur-X carries more data than EN 16931, e.g. further parties, delivery terms or instalments ([Profiles](concepts.md#profiles)). Add it with `extended(ExtendedData $extended)` and, for lines, `LineBuilder::extended(LineExtendedData $extended)`. - The data is written to CII in profile EXTENDED only. - When you use it, the automatic choice switches ZUGFeRD and CII to EXTENDED. Other profiles report `QUITTIO-PROFILE-EXTENDED-ONLY`. | `ExtendedData` (`Quittio\Core\Data\Extensions`) | Written as | |---|---| | `invoicer`, `invoicee` (`ExtendedParty`) | `InvoicerTradeParty`, `InvoiceeTradeParty` | | `ultimateCustomer` (`ExtendedParty`) | `ProductEndUserTradeParty` | | `shipFrom` (`ExtendedParty`) | `ShipFromTradeParty` | | `deliveryTermsCode` (string, e.g. an Incoterms code of the international delivery terms such as `FCA`) | `ApplicableTradeDeliveryTerms/DeliveryTypeCode` | | `additionalReferences` (list of `DocumentReference`) | `AdditionalReferencedDocument` with type code (default 916), name and date | | `installments` (list of `Installment(Date $dueDate, ?Money $amount = null, ?string $description = null)`) | one payment terms group per instalment with due date and partial amount | | `additionalPaymentInstructions` (list of `PaymentInstructions`) | further payment means | | `LineExtendedData` | Written as | |---|---| | `lineStatusReasonCode` (`LineExtendedData::STATUS_DETAIL`, `STATUS_GROUP`, `STATUS_INFORMATION`) | Line status; default `GROUP` for lines with sub lines and `DETAIL` for sub lines | | `shipTo` (`ExtendedParty`) | Ship-to party of the line | | `deliveryDate` (`Date`) | Delivery date of the line | | `includedProducts` (list of `IncludedProduct(string $name, ?BigDecimal $quantity = null, UnitCode\|RawCode\|null $unit = null, ?string $description = null, ?string $sellerId = null)`) | Included products | `ExtendedParty(string $name, ?Address $address = null, array $identifiers = [], ?string $vatId = null, ?Contact $contact = null)` describes a further party. A line `DocumentReference(string $id, string $kind = DocumentReference::TYPE_OTHER, ?string $lineId = null, ?Date $issueDate = null, ?RawCode $typeCode = null, ?string $description = null)` is added with `LineBuilder::reference()`. It is written by its kind: `TYPE_ORDER`, `TYPE_CONTRACT`, `TYPE_DESPATCH_ADVICE`, `TYPE_RECEIVING_ADVICE` or `TYPE_OTHER`. ```php use Quittio\Core\Data\Extensions\ExtendedData; use Quittio\Core\Data\Extensions\ExtendedParty; use Quittio\Core\Data\Extensions\Installment; use Quittio\Core\Data\Party\Address; use Quittio\Core\Support\Amount; use Quittio\Core\Support\Date; ->extended(new ExtendedData( ultimateCustomer: new ExtendedParty('Endkunde GmbH', Address::make('Weg 2', '12345', 'Musterstadt', 'DE')), deliveryTermsCode: 'FCA', installments: [ new Installment(Date::parse('2026-11-01'), Amount::of('500.00', 'EUR')), new Installment(Date::parse('2026-12-01'), Amount::of('690.00', 'EUR')), ], )) ``` ### XRechnung extension The XRechnung extension is a profile of XRechnung with additional fields, such as sub lines and third party payments. `xrechnungExtension(XRechnungExtensionData $extension)` adds third party payments (BG-DEX-09): `new XRechnungExtensionData([new ThirdPartyPayment(string $type, Money $amount, string $description)])`. - Third party payments are added to the amount due (BR-DEX-09). - They can only be transmitted in an XRechnung UBL invoice with profile `xrechnung-extension` (UBL `cac:PrepaidPayment`). CII, credit notes and other profiles fail with `QUITTIO-THIRD-PARTY-PAYMENTS`. ## Macros `InvoiceBuilder` and `LineBuilder` are macroable, so you can add your own methods: - `InvoiceBuilder::macro(string $name, callable $macro)` adds a method; `hasMacro(string $name)` and `flushMacros()` inspect and remove them. - The closure is bound to the builder, so `$this` and the builder's private properties are available. - `InvoiceBuilder` can also be extended: `make()`, `from()` and `fromDocument()` return `static`. Country packs add their helpers this way, e.g. `smallAmount()`, `selfBilled()` and `deductAdvance()` of the German pack ([Builder macros](country-packs.md#builder-macros)). ```php use Quittio\Core\Builder\InvoiceBuilder; InvoiceBuilder::macro('forProject', function (string $id, string $name): InvoiceBuilder { return $this->projectReference($id, $name)->note('Project: '.$name); }); $builder->forProject('P-17', 'Customer portal'); ``` ## Calculation `Quittio\Core\Calculation\En16931Calculator` calculates line net amounts, document allowances and charges, the VAT breakdown and the totals. Every render, check and prepare run calls it, so data from the builder never needs to be calculated by hand. To calculate without rendering, call it directly or use `prepare()` ([Formats and profiles](formats.md#rendering-in-the-core)): ```php use Quittio\Core\Calculation\En16931Calculator; $calculated = (new En16931Calculator)->calculate($invoice); $calculated->isCalculated(); // true $calculated->totals->amountDue; // Brick\Money\Money $calculated->vatBreakdown[0]->taxAmount; ``` Amounts are rounded half up to two decimals, only at the points listed below. VAT is calculated per VAT group (category and rate), never per line (BR-CO-17). Calculating calculated data again gives the same result. ### Line amounts | Step | Formula | |---|---| | Net price BT-146 | as given; otherwise list price BT-148 − price discount BT-147 | | Line amount before allowances and charges B | round2(quantity BT-129 × net price BT-146 / base quantity BT-149), the base quantity only if it is set and not 0 or 1 | | Line allowance BT-136 | as given; otherwise round2((base BT-137, default B) × percentage BT-138 / 100) | | Line charge BT-141 | as given; otherwise round2((base BT-142, default B) × percentage BT-143 / 100) | | Line net amount BT-131 | B − Σ line allowances + Σ line charges | A line with sub lines and without its own price takes the sum of the net amounts of its sub lines as line amount. The price is derived from it with up to ten decimals. `LineCalculator` (`calculate(InvoiceLine $line, string $currency)`) does this per line. Its static helpers are: - `baseAmount($quantity, $netPrice, $baseQuantity)`: B; - `exactAmount(InvoiceLine $line)`: the unrounded line amount that PEPPOL-EN16931-R120 checks; - `priceFor($amount, $quantity, $baseQuantity)`: the shortest net price, 2 to 10 decimals, for which the line formula yields an amount. ### Document totals and VAT breakdown | Step | Formula | |---|---| | Document allowance BT-92 / charge BT-99 | as given; otherwise round2((base BT-93/BT-100, default the line net sum of its VAT group) × percentage / 100) | | Taxable amount BT-116 per VAT group | Σ line net amounts − Σ allowances + Σ charges of the group | | VAT amount BT-117 per VAT group | round2(BT-116 × rate / 100); 0 for categories Z, E, AE, K, G and O | | Sum of line net amounts BT-106 | Σ BT-131 | | Sum of allowances BT-107, charges BT-108 | Σ document allowances, Σ document charges (empty if there are none) | | Total without VAT BT-109 | BT-106 − BT-107 + BT-108 | | VAT total BT-110 | Σ BT-117 | | VAT total in accounting currency BT-111 | round2(BT-110 × exchange rate), only if BT-6 differs from BT-5 | | Total with VAT BT-112 | BT-109 + BT-110 | | Amount due BT-115 | BT-112 − BT-113 + BT-114, plus third party payments of the XRechnung extension | | Due date BT-9 | issue date + `dueInDays()` if no due date is set | The VAT groups appear in the breakdown in the order of their first line. A document allowance or charge for a group without lines adds its own group. A rounding amount of 0 is left out. ### Gross price conversion `GrossPriceConverter` applies when a line has a price including VAT (`priceInclVat()`, [Gross price mode](#gross-price-mode)). Per VAT group: 1. G = Σ g_i with g_i = round2(quantity × price including VAT). 2. N = round2(G / (1 + rate / 100)), the taxable amount. 3. T = round2(N × rate / 100), the VAT (BR-CO-17). 4. Every line gets n_i = round2(g_i / (1 + rate / 100)); the cents N − Σ n_i go to the lines with the largest rounding remainders. A line keeps the sign of its gross amount (returns and vouchers stay negative). A cent never moves a line to zero or across zero while another line can take it. Net prices are derived with the precision needed for the line formula to yield n_i. 5. G − (N + T) of all groups becomes the rounding amount BT-114, so that the amount due equals the gross amount the customer saw. Example: 3 × 9.99 including 19 % gives G = 29.97, N = 25.18, T = 4.78, net price 8.393, BT-114 = 0.01 and an amount due of 29.97. `GrossPriceConverter::netOf(BigDecimal $gross, BigDecimal $rate)` is step 2. ### Discount splitting `AllowanceSplitter::splitAllowance(array $lines, string $currency, ?BigDecimal $percent, ?Money $amount, ?string $reason = null, AllowanceReasonCode|RawCode|null $reasonCode = null): list` implements `discountPercent()` and `discountAmount()`. The discount (the amount, or round2(line sum × percentage / 100)) is distributed over the VAT groups in proportion to their signed line sums with `LargestRemainder::distribute(BigDecimal $total, array $weights, int $scale = 2)`. `LargestRemainder::distribute()` floors every part and gives the remaining cents to the parts with the largest remainders (ties in the order of the weights). It returns parts that add up exactly to the total. Weights that are all zero share equally; weights that add up to zero throw a `CalculationException`. Example: a discount of 10.00 on 100.00 at 19 % and 50.00 at 7 % becomes 6.67 and 3.33. ### Declared amounts Data that Quittio did not calculate itself can carry amounts that another system rounded differently, e.g. line net amounts rounded per line or a VAT amount within the tolerance of BR-CO-17. Recalculating such data would change amounts that were already issued, and a cancellation would no longer reverse the original exactly. Quittio therefore keeps these declared amounts in two cases: - `InvoiceBuilder::from($data)` with data whose line net amounts or VAT breakdown the recalculation does not reproduce exactly (e.g. an invoice read from XML), as long as no amount-relevant input changes: lines, document allowances and charges, discounts and the currency. Changing a line, adding one or calling `allowance()` recalculates everything. - Cancellations of such data ([Cancellations and corrections](#cancellations-and-corrections)). The builder then sets `Meta::FLAG_DECLARED_AMOUNTS` with a fingerprint of the amount inputs (`DeclaredAmounts::fingerprint()`, an xxh128 hash over identifiers, quantities, prices, VAT groups, allowances, charges, net amounts, VAT breakdown and currency). The calculator keeps a declared amount within these tolerances: | Declared amount | Kept if | |---|---| | Line net amount BT-131 | within 0.02 of the unrounded line formula (`DeclaredAmounts::LINE_TOLERANCE`, as PEPPOL-EN16931-R120 tolerates it); a parent line may also state the sum of its sub lines | | VAT amount BT-117 | less than one currency unit from the recalculation (BR-CO-17); 0 for categories without VAT | | VAT total in accounting currency BT-111 | within 0.01 (`DeclaredAmounts::TAX_CURRENCY_TOLERANCE`) | - Amounts beyond these tolerances are recalculated, and the data gets `Meta::FLAG_DECLARED_AMOUNTS_RECALCULATED`. The validation reports it as `QUITTIO-DECLARED-AMOUNTS` (the document may not reverse the original exactly). - Data whose amount inputs no longer match the fingerprint, e.g. changed with `with()` or edited in stored JSON, is recalculated completely. - Data calculated by Quittio never needs declared amounts. The calculator also writes a fingerprint to every result, so `from()`, cancellations and the calculator never keep amounts that were changed afterwards. The calculation policy decides what happens with declared amounts (`calculation.declared_amounts` in Laravel, `calculation.declared_amounts` in `Quittio::create()`, or `new En16931Calculator(policy: new CalculationPolicy(DeclaredAmountsMode::Recalculate))`): | Mode | Declared amounts | |---|---| | `keep` (default) | kept within the tolerances above | | `recalculate` | always recalculated, reported as `QUITTIO-DECLARED-AMOUNTS`; the aligned VAT of partial cancellations is kept | | `reject` | kept within the tolerances, otherwise a `CalculationException` | The policy belongs to the run, not to the data: the same stored invoice gives other amounts under another policy. `InvoiceBuilder::from()` compares the declared amounts with the default calculator; `Quittio::edit()` and cancellations use the calculator of the instance. The helpers are public: `DeclaredAmounts::needed(InvoiceData $data)` (the recalculation does not reproduce the declared amounts), `isStale()` (changed after the amounts were calculated or declared), `isTrusted()` (flagged and fingerprint matches), `hasAmounts()` and `breakdownKey(VatCategory $category, ?BigDecimal $rate)` (VAT group key, e.g. `S|19.00`). Data read from XML has no exchange rate. `InvoiceData::withImpliedExchangeRate()` returns a copy with the rate BT-111 / BT-110 (ten decimals). `from()` takes it over, so that the recalculation reproduces BT-111. It drops the rate once amounts or the VAT accounting currency change, because the rate is only as precise as the declared amounts. Set the rate with `exchangeRate()` then, otherwise the validation fails with BR-53. ## Data model `Quittio\Core\Data\InvoiceData` is the root of the model. It is a superset of EN 16931 (versions 2017 and 2026) with the extensions of profile EXTENDED and the XRechnung extension. All data classes are `final readonly` with promoted constructor properties. The [field reference](field-reference.md) lists every field with business term, cardinality, profile level and XML paths. | Property | Type | BT | Set with | |---|---|---|---| | `number` | `string` | BT-1 | `number()` | | `issueDate` | `Date` | BT-2 | `issueDate()` | | `typeCode` | `InvoiceTypeCode\|RawCode` | BT-3 | `type()` | | `currency` | `string` | BT-5 | `currency()` | | `seller` | `SellerParty` | BG-4 | `seller()` | | `buyer` | `BuyerParty` | BG-7 | `buyer()` | | `lines` | `list` | BG-25 | `line()`, `lines()` | | `taxCurrency` | `?string` | BT-6 | `taxCurrency()` | | `taxPointDate`, `taxPointDateCode` | `?Date`, `TaxPointDateCode\|RawCode\|null` | BT-7, BT-8 | `taxPointDate()`, `taxPointDateCode()` | | `dueDate` | `?Date` | BT-9 | `dueDate()`, calculator | | `buyerReference`, `projectReference`, `contractReference`, `purchaseOrderReference`, `salesOrderReference`, `receivingAdviceReference`, `despatchAdviceReference`, `tenderReference`, `invoicedObject`, `buyerAccountingReference` | `?string`, `?ProjectReference`, `?Identifier` | BT-10 to BT-19 | [References](#references) | | `paymentTerms` | `?PaymentTerms` | BT-20 | `paymentTerms()` | | `notes` | `list` | BG-1 | `note()` | | `businessProcess` | `?string` | BT-23 | `businessProcess()` | | `precedingInvoices` | `list` | BG-3 | `precedingInvoice()` | | `payee` | `?PayeeParty` | BG-10 | `payee()` | | `sellerTaxRepresentative` | `?TaxRepresentativeParty` | BG-11 | `taxRepresentative()` | | `delivery` | `?Delivery` | BG-13 | `deliveryDate()`, `deliverTo()`, `delivery()` | | `invoicingPeriod` | `?Period` | BG-14 | `invoicingPeriod()` | | `paymentInstructions` | `?PaymentInstructions` | BG-16 | `payment()`, `creditTransfer()`, `paymentCard()`, `directDebit()` | | `allowances`, `charges` | `list`, `list` | BG-20, BG-21 | `allowance()`, `charge()`, `discountPercent()`, `discountAmount()` | | `totals` | `?Totals` | BG-22 | calculator | | `vatBreakdown` | `list` | BG-23 | calculator | | `attachments` | `list` | BG-24 | `attachment()` | | `prepaidAmount`, `roundingAmount` | `?Money` | input for BT-113, BT-114 | `prepaid()`, `addPrepaid()`, `rounding()` | | `extended` | `?ExtendedData` | profile EXTENDED | `extended()` | | `xrechnungExtension` | `?XRechnungExtensionData` | XRechnung extension | `xrechnungExtension()` | | `presentation` | `?Presentation` | PDF and HTML only | `presentation()`, `group()` | | `meta` | `Meta` | internal | `locale()`, `timezone()`, `custom()`, `source()`, `flag()`, `exchangeRate()` | | `standardVersion` | `StandardVersion` | internal | `standard()` | The parts live in sub namespaces of `Quittio\Core\Data`: | Namespace | Classes | |---|---| | `Party` | `SellerParty`, `BuyerParty`, `PayeeParty`, `TaxRepresentativeParty`, `Address`, `Contact`, `ElectronicAddress` | | `Lines` | `InvoiceLine`, `Item`, `Price`, `LineVat`, `Classification`, `ItemAttribute` | | `AllowanceCharge` | `DocumentAllowance`, `DocumentCharge` (base `DocumentAllowanceCharge`), `LineAllowance`, `LineCharge` (base `LineAllowanceCharge`) | | `Payment` | `PaymentInstructions`, `CreditTransfer`, `PaymentCard`, `DirectDebit`, `PaymentTerms`, `CashDiscount`, `PenaltyTerms` | | `Delivery` | `Delivery`, `Period` | | `References` | `PrecedingInvoiceReference`, `ProjectReference`, `DocumentReference` | | `Totals` | `Totals`, `VatBreakdown` | | `Attachments` | `Attachment` | | `Extensions` | `ExtendedData`, `ExtendedParty`, `Installment`, `IncludedProduct`, `LineExtendedData`, `XRechnungExtensionData`, `ThirdPartyPayment` | | `Presentation` | `Presentation`, `Colors`, `LineGroup`, `LinePresentation` | | (root) | `InvoiceData`, `Identifier`, `Note`, `Meta` | `Totals` has `lineNetSum` (BT-106), `allowanceSum` (BT-107), `chargeSum` (BT-108), `netTotal` (BT-109), `vatTotal` (BT-110), `vatTotalInTaxCurrency` (BT-111), `grossTotal` (BT-112), `prepaidAmount` (BT-113), `roundingAmount` (BT-114) and `amountDue` (BT-115), all `Brick\Money\Money`. A `VatBreakdown` has `taxableAmount` (BT-116), `taxAmount` (BT-117), `category` (BT-118), `rate` (BT-119), `exemptionReason` (BT-120) and `exemptionReasonCode` (BT-121). Types: amounts are `Money` with two decimals; prices, quantities and rates are `Brick\Math\BigDecimal`; dates are `Quittio\Core\Support\Date` (`toIsoString()`, `format()`, `addDays()`, `toCarbon()`, `isBefore()` …). ### Immutability Data objects never change. `with(array $changes)` returns a copy with the named properties replaced; an unknown property throws an `InvalidArgumentException`: ```php $copy = $invoice->with(['buyerReference' => '04011000-12345-03']); $copy = $invoice->withNumber('RE-2026-0043')->withIssueDate(Date::parse('2026-10-05')); $line = $invoice->lines[0]->with(['note' => 'Delivered in two parts']); ``` `with()` does not recalculate anything and checks nothing. For changes of amounts, prefer `InvoiceBuilder::from()`, so that allowances and discounts follow the lines ([Changing existing data](#changing-existing-data)). Changing amount inputs of calculated data with `with()` makes its amounts stale: the next calculation recalculates them ([Declared amounts](#declared-amounts)). Helpers of `InvoiceData`: `isCalculated()` (totals present), `typeCodeValue()` (BT-3 as string), `isCreditNote()`, `allLines()` (all lines including sub lines, depth first), `withImpliedExchangeRate()` and `InvoiceData::factory()` for test data ([Testing](testing.md)). Helpers of the parts: `InvoiceLine::withNetPrice()`, `withPriceInclVat()`, `hasSubLines()`, `Price::isGrossMode()`, `LineVat::groupKey()`, `DocumentAllowanceCharge::isCharge()` and `reasonCodeValue()`, `PaymentInstructions::primaryCreditTransfer()`, `CreditTransfer::isIban()`, `PaymentTerms::isEmpty()`, `Period::isValid()`, `Attachment::size()`, `Identifier::schemeValue()`, `Meta::hasFlag()`, `Presentation::isExplicit()`. ### Arrays and JSON `InvoiceData::toArray()`, `toJson(int $flags = 0)`, `InvoiceData::fromArray(array $data)` and `InvoiceData::fromJson(string $json)` convert the model losslessly, including totals, presentation and metadata: ```php $json = $invoice->toJson(JSON_PRETTY_PRINT); $same = InvoiceData::fromJson($json); ``` ```json { "number": "RE-1", "issueDate": "2026-10-03", "typeCode": "380", "currency": "EUR", "seller": { "name": "Muster GmbH", "address": { "countryCode": "DE", "line1": "Hauptstraße 1", "city": "Musterstadt", "postcode": "12345" }, "vatId": "DE123456789" }, "buyer": { "name": "Kunde AG", "address": { "countryCode": "DE", "line1": "Bahnhofstraße 5", "city": "Beispielstadt", "postcode": "54321" } }, "lines": [ { "id": "1", "quantity": "2", "unit": "HUR", "price": { "netPrice": "95.00" }, "vat": { "category": "S", "rate": "19" }, "item": { "name": "Beratung" } } ] } ``` - Keys are the property names. Values equal to the constructor default are left out and restored on the way back. - Amounts are strings with two decimals and without currency: `fromArray()` uses the invoice currency (BT-5), for BT-111 the VAT accounting currency (BT-6). Decimals are strings, dates `YYYY-MM-DD`, codes their value. The content of attachments is base64 encoded. - `fromArray()` rejects unknown fields, missing required values, floats and values of the wrong type with a `Quittio\Core\Exceptions\HydrationException` that names the path, e.g. `lines[0].price.netPrice: Floats are not accepted. Pass monetary values and quantities as strings.` A code that is not a case of its enum becomes a `RawCode` where the property allows one. - `toJson()` always uses `JSON_THROW_ON_ERROR`, `JSON_UNESCAPED_SLASHES` and `JSON_UNESCAPED_UNICODE`; pass further flags such as `JSON_PRETTY_PRINT`. The JSON Schema (draft 2020-12, `$id` `https://quittio.dev/schema/invoice.schema.json`) is shipped in `packages/core/resources/schema/invoice.schema.json` (`JsonSchemaGenerator::defaultPath()`). With the optional package `opis/json-schema`, `Quittio\Core\Serialization\JsonSchemaValidator` checks input before hydration: - `JsonSchemaValidator::available()` tells whether the package is installed. - `(new JsonSchemaValidator)->validate(string $json)` returns up to 20 errors as `path: message`, or an empty list if the input is valid. Without the package it throws a `RuntimeException`. ### Snapshots Stored invoices (the Managed layer, queued render jobs) use snapshots. A snapshot is `toSnapshot()`: `toArray()` plus the schema version `_version` (`InvoiceData::SNAPSHOT_VERSION`, currently 1). `InvoiceData::fromSnapshot(array $snapshot, ?SnapshotUpcasterChain $chain = null)` brings a snapshot of an older version to the current model before hydrating it; a snapshot without `_version` counts as version 1. Snapshots of issued invoices are never rewritten: upcasters run in memory when a snapshot is read. ```php use Quittio\Core\Serialization\Snapshots\SnapshotUpcaster; use Quittio\Core\Serialization\Snapshots\SnapshotUpcasterChain; final class SnapshotV1ToV2 implements SnapshotUpcaster { public function from(): int { return 1; // reads version 1, produces version 2 } public function upcast(array $snapshot): array { // rename or convert the fields that changed between the versions return $snapshot; } } SnapshotUpcasterChain::useDefault(new SnapshotUpcasterChain([new SnapshotV1ToV2])); ``` - `SnapshotUpcasterChain` (`register(SnapshotUpcaster $upcaster)`, `upcast(array $snapshot, int $from, int $to = InvoiceData::SNAPSHOT_VERSION)`) applies one upcaster per version step. - `SnapshotUpcasterChain::default()` is the shared chain of `fromSnapshot()`; `useDefault(?SnapshotUpcasterChain $chain)` replaces it. - A snapshot of a newer version than the installed package, an invalid version or a missing upcaster throw a `SnapshotVersionException`. - The current model is version 1, so no upcaster is needed yet; upcasters matter once a release changes the model. In Laravel, list the upcaster classes in `snapshots.upcasters` ([Configuration](configuration.md)). # Formats and profiles This page explains what Quittio produces from an invoice: a PDF with embedded XML, XML only, a plain PDF, HTML or JSON. It shows how Quittio chooses the output by itself, how you request a specific one, and lists every format and profile with its details. If the words format, profile, CII or UBL are new to you, read [Formats](concepts.md#formats) and [Profiles](concepts.md#profiles) in the E-invoicing basics first. Field numbers such as BT-10 are explained in [EN 16931 and business terms](concepts.md#en-16931-and-business-terms). Two settings decide the output of a render run: - The **format** decides what is produced: PDF, XML, HTML or JSON. For XML it also decides the syntax, CII or UBL ([Syntaxes](concepts.md#syntaxes-cii-and-ubl)). - The **profile** decides which fields and rules the XML follows, for example EN 16931, XRechnung, Peppol or a Factur-X level. Storage, downloads, queues and events are described in [Laravel integration](laravel.md), all configuration keys in [Configuration](configuration.md). ## You usually do not choose a format Without `format()`, Quittio chooses format and profile for each invoice. It uses data you provide anyway: the countries of seller and buyer, whether the buyer is a consumer, and the buyer reference. For a German seller: - A business customer gets ZUGFeRD: a PDF that people can read, with the XML for accounting software embedded (profile EN 16931). - A German public authority gets XRechnung as CII XML. Quittio recognises it by the Leitweg-ID (the routing ID of a German public buyer) in the buyer reference (BT-10). - A consumer gets a PDF without XML. So do a customer outside the EU and a small amount invoice up to 250 € that you mark with `->de()->smallAmount()`. [Which format do I need?](concepts.md#which-format-do-i-need) shows all cases in a table, [Automatic choice](#automatic-choice) the exact rules. The result tells you what Quittio chose: ```php use Quittio\Laravel\Facades\Quittio; $result = Quittio::make($invoice)->render(); // no format: automatic choice $result->format(); // e.g. "zugferd" $result->profile()?->key(); // e.g. "en16931" $prepared = Quittio::make($invoice)->prepare(); // the same decision, without rendering $prepared->format(); // e.g. "zugferd" $prepared->profile()?->key(); // e.g. "en16931" ``` ## Requesting a format When a customer requires a specific format, name it. Every format has a default profile, so you rarely need `profile()` as well. ```php use Quittio\Laravel\Facades\Quittio; // At the end of the builder chain $result = Quittio::invoice() ->buyer($customer) ->buyerReference('04011000-12345-03') // Leitweg-ID, required by XRechnung ->line('Consulting', netPrice: '120.00') ->render('xrechnung-cii'); // For invoice data you built before $result = Quittio::make($invoice)->format('xrechnung-cii')->render(); $result = Quittio::make($invoice)->format('zugferd')->profile('extended')->render(); // Several formats from one calculation; the first one is the main result $result = Quittio::make($invoice)->formats(['zugferd', 'xrechnung-cii'])->render(); $result->as('xrechnung-cii')->xml(); ``` - The format keys are listed in [Output formats](#output-formats). - Use `profile()` when a recipient asks for a Factur-X level such as `basic`, or for the XRechnung extension. Which profile applies without it is described in [Which profile is used](#which-profile-is-used). - To use one format for every invoice, set `defaults.format` in `config/quittio.php` (without Laravel: `format`). The default `null` means automatic choice ([Output configuration](#output-configuration)). - Without Laravel, the same methods exist on `$quittio->make($invoice)` ([Rendering in the core](#rendering-in-the-core)). ## Output formats These are the keys for `render()`, `format()` and `formats()`: | Format | Aliases | Syntax | Output | Profiles | Default profile | |---|---|---|---|---|---| | `zugferd` | `factur-x`, `facturx` | CII | PDF/A-3b with embedded XML (and the XML) | every CII profile that can be embedded: `minimum`, `basic-wl`, `basic`, `en16931`, `extended`, `xrechnung`, `xrechnung-extension` | `en16931` | | `xrechnung-cii` | `xrechnung` | CII | XML | `xrechnung`, `xrechnung-extension` | `xrechnung` | | `xrechnung-ubl` | – | UBL | XML | `xrechnung`, `xrechnung-extension` | `xrechnung` | | `cii` | – | CII | XML | every CII profile | `en16931` | | `ubl` | – | UBL | XML | every UBL profile: `en16931`, `peppol`, `xrechnung`, `xrechnung-extension` | `en16931` | | `peppol` | – | UBL | XML according to Peppol BIS Billing 3.0 | `peppol` | `peppol` | | `pdf` | – | – | PDF without XML (PDF/A-3b by default; optionally with embedded XML, see [PDF, HTML and JSON](#pdf-html-and-json)) | – | – | | `html` | – | – | the rendered template | – | – | | `json` | – | – | the calculated data model | – | – | PDF/A-3b is the archiving standard for PDF (PDF/A) in version 3, which allows embedded files, at conformance level b. Format and profile keys are case-insensitive. The profiles `xrechnung` and `xrechnung-extension` and the formats `xrechnung-cii` and `xrechnung-ubl` come with the German country pack (`Quittio\Country\DE\Formats\XRechnungFormats`); until 1.0 the core registers the two formats as well. Other country packs register their own formats the same way ([Writing your own country pack](country-packs.md#writing-your-own-country-pack)). MINIMUM and BASIC WL carry no invoice lines and do not comply with EN 16931. The validation warns about them with `QUITTIO-PROFILE-NOT-EINVOICE`; use them only when a recipient asks for them. ## Automatic choice Without a format, and without a configured `defaults.format`, the class `Quittio\Core\Formats\DefaultFormatResolver` decides. It first checks whether the invoice is **domestic**: seller and buyer lie in the same VAT territory, or the buyer has no country code. The VAT territory follows VAT law, not the map (`VatTerritory::sameVatTerritory()`). Monaco counts as France, and excluded territories such as Büsingen or Heligoland do not count as domestic. Then the first step that applies decides: 1. **Domestic invoice with a country rule.** The format rules of the leading country pack decide only what the country prescribes ([German format choice](country-packs.md#format-choice)). In Germany: - A valid Leitweg-ID in the buyer reference (BT-10), or a buyer electronic address with scheme `0204` → `xrechnung-cii` with profile `xrechnung`. - A small amount invoice (`smallAmount()`) between parties in Germany's domestic territory under VAT law (the "Inland" of the VAT Act, without Büsingen and Heligoland) → `pdf`. - Everything else goes on to the next steps. 2. **Buyer reachable via Peppol.** A buyer that is not a consumer and is reachable via Peppol → `peppol`. This step needs a [Peppol lookup](#peppol-lookup); without one it is skipped. 3. **Consumer** → `pdf`. 4. **Other domestic invoices** → `zugferd` with the configured profile (default `en16931`). 5. **Cross-border invoice.** The format rules of the buyer's country pack decide first. In Germany a Leitweg-ID leads to XRechnung, whatever the seller's country. A buyer in an excluded territory of the seller's country uses the seller's pack. Without a choice, an invoice within the EU VAT territory → `zugferd` with the configured profile. 6. **Seller or buyer outside the EU VAT territory** → `pdf`, e.g. a US seller invoicing a German business. `prepare()` shows the choice without rendering (see the [example above](#you-usually-do-not-choose-a-format)). ### Upgrade to profile EXTENDED Some data exists only in the profile EXTENDED: sub lines, `extended()` data and line references. When an invoice uses such data, the profile is upgraded: - The automatic choice `zugferd` or `cii` with another profile gets the profile `extended`. - The same applies to a CII format you request without `->profile()`, e.g. `->format('zugferd')`, and to a configured profile. It applies only to formats that allow `extended`. - A profile you request with `->profile()` is never changed. The validation reports the data it cannot carry. - With `auto_upgrade_profile = false` (Laravel `format_resolver.auto_upgrade_profile`), such an invoice fails with a `ConfigurationException` instead. XRechnung has no EXTENDED profile. `->format('xrechnung-cii')` keeps the profile `xrechnung`, and the validation reports the sub lines. To render XRechnung with sub lines, use `->format('xrechnung-ubl')->profile('xrechnung-extension')`. ### Peppol lookup Peppol is an international network for delivering e-invoices ([Formats](concepts.md#formats)). The default resolver chooses `peppol` only when a lookup says that the buyer is reachable there. The lookup is a closure that receives the `BuyerParty` and returns `true` or `false`. Quittio ships no lookup; pass your own resolver: ```php use Quittio\Core\Data\Party\BuyerParty; use Quittio\Core\Formats\DefaultFormatResolver; use Quittio\Core\Quittio; use Quittio\Country\DE\CountryDE; $quittio = Quittio::create($config, [new CountryDE], formatResolver: new DefaultFormatResolver( defaultProfile: 'en16931', autoUpgradeProfile: true, peppolLookup: fn (BuyerParty $buyer): bool => $directory->isRegistered($buyer->electronicAddress), )); ``` In Laravel, name an invokable class in `format_resolver.peppol_lookup`. It is resolved from the container, receives the `BuyerParty` and returns a bool. You can also set a callable at runtime; see [Peppol lookup](laravel.md#peppol-lookup) in the Laravel integration. Country rules come first, but they decide only what the country prescribes (in Germany: public buyers and small amount invoices). Domestic business invoices and cross-border invoices reach the lookup. ### Own resolver and country rules To replace the whole choice, write a resolver. It implements `Quittio\Core\Formats\FormatResolver` (`resolve(InvoiceData $invoice, ResolvedCountries $countries): FormatChoice`) and returns a `FormatChoice(string $format, ?string $profile = null, ?string $reason = null)`. A `null` profile uses the default profile of the format. Register it: - without Laravel with `Quittio::create(formatResolver: …)` or `$quittio->setFormatResolver(…)`, - in Laravel with `Quittio::extend('format_resolver', 'mine', fn ($app, array $config) => new MyResolver)` and `format_resolver.driver = 'mine'`. A country pack contributes its domestic and buyer rules through `formatRules()`. This is a list of `Quittio\Core\Formats\FormatRule` (`resolve(InvoiceData $invoice): ?FormatChoice`, `null` if the rule does not apply). The first rule that returns a choice wins ([Country packs](country-packs.md)). ## Profiles A profile is a set of fields and rules based on EN 16931 ([Profiles](concepts.md#profiles)). Every profile writes its specification identifier (BT-24) into the XML, so that the recipient knows which rules the invoice follows. Some profiles also set a default business process (BT-23), an identifier of the business context in which the invoice is exchanged. | Key | Name | Level | Syntax | Specification identifier (BT-24) | Business process (BT-23) default | Additionally required | |---|---|---|---|---|---|---| | `minimum` | Factur-X/ZUGFeRD MINIMUM | MINIMUM | CII | `urn:factur-x.eu:1p0:minimum` | – | – | | `basic-wl` | Factur-X/ZUGFeRD BASIC WL | BASIC WL | CII | `urn:factur-x.eu:1p0:basicwl` | – | – | | `basic` | Factur-X/ZUGFeRD BASIC | BASIC | CII | `urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic` | – | – | | `en16931` | EN 16931 | EN 16931 | CII, UBL | `urn:cen.eu:en16931:2017` (`…:2026` with `standard(StandardVersion::En16931_2026)`) | – | – | | `extended` | Factur-X/ZUGFeRD EXTENDED | EXTENDED | CII | `urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended` | – | – | | `peppol` | Peppol BIS Billing 3.0 | EN 16931 | UBL | `urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0` | `urn:fdc:peppol.eu:2017:poacc:billing:01:1.0` | Electronic addresses of seller and buyer (BT-34, BT-49); Peppol rules | | `xrechnung` | XRechnung 3.0 | EN 16931 | CII, UBL | `urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0` | `urn:fdc:peppol.eu:2017:poacc:billing:01:1.0` | Buyer reference (BT-10), electronic addresses of seller and buyer (BT-34, BT-49), seller contact (BG-6), payment instructions (BG-16); BR-DE rules | | `xrechnung-extension` | XRechnung 3.0 Extension | EN 16931 | CII, UBL | `urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0#conformant#urn:xeinkauf.de:kosit:extension:xrechnung_3.0` | as XRechnung | as XRechnung | Aliases: `basicwl` (basic-wl), `en-16931` and `comfort` (en16931), `peppol-bis-3` (peppol). The default business process is written only when the invoice sets none (`businessProcess()`). The registry is available as `$quittio->profiles()` (`get()`, `has()`, `all()`, `register()`, `alias()`, `bySpecificationId()`). ### Which profile is used The profile of a format is the first of these that applies: 1. The profile you request with `profile()`. 2. The configured profile (`profile`, Laravel `defaults.profile`), if it fits the format: the format allows it, it exists in the syntax of the format, and it can be embedded into the PDF where the format embeds XML. 3. The profile of the automatic choice. 4. The default profile of the format. 5. `en16931` (`Quittio::DEFAULT_PROFILE`). A configured profile therefore applies to `->format('zugferd')` and to formats chosen by a country rule as well. It is skipped where it does not fit, e.g. `basic` for `xrechnung-cii` or `peppol`. Without a configured profile (`null`, the default) the format or the automatic choice decides. The [upgrade to EXTENDED](#upgrade-to-profile-extended) comes on top. A run fails with a `ConfigurationException` for: - an unknown format, - a profile the format does not allow, - a profile that is not available in the syntax of the format (e.g. `peppol` in `zugferd`), - a profile that cannot be embedded into a PDF. An unknown profile throws an `InvalidArgumentException`. ### What each profile carries The writers leave out every field that the profile does not contain. The validation (level L2, see [Validation](validation.md)) reports such fields as warning `QUITTIO-PROFILE-NOT-ALLOWED`. A missing mandatory field of a profile is an error (`QUITTIO-PROFILE-REQUIRED`), and so is data of profile EXTENDED or of the XRechnung extension in another profile. The "Level" column of the [field reference](field-reference.md) names the lowest level of every field. | Level | Contains | Drops | |---|---|---| | MINIMUM | Number, issue date, type code, currency, buyer reference, purchase order reference, business process, seller name, legal registration, VAT identifier, tax number and country, buyer name and legal registration, total without VAT, VAT total, total with VAT, amount due | lines, addresses, VAT breakdown, payment data and everything else | | BASIC WL | MINIMUM plus postal addresses, identifiers and electronic addresses of seller and buyer, seller trading name, buyer VAT identifier, tax currency, tax point date, due date, contract, despatch advice and accounting references, payment terms text, notes, preceding invoice, payee, tax representative, delivery, invoicing period, payment means with IBAN and direct debit, document allowances and charges, VAT breakdown, all totals | invoice lines | | BASIC | BASIC WL plus lines: identifier, note, quantity and unit, prices (net, list price, discount, base quantity), VAT, item name and standard identifier, line period, line allowances and charges, line amount | item description, seller and buyer item numbers, classification, origin country, attributes, line object identifier, order line and accounting references, contacts, seller additional legal information, buyer trading name, supporting documents, project, sales order, receiving advice, tender and invoiced object references, payment means text, card, account name and BIC | | EN 16931 | everything of EN 16931 | data of profile EXTENDED | | EXTENDED | EN 16931 plus sub lines (with parent line identifier and line status), `ExtendedData` and `LineExtendedData`, the exchange rate, cash discounts and late fees as structured elements, several cash discount tiers, instalments, further payment instructions, line references, exemption reasons repeated on line and allowance level | – | The UBL profiles (`en16931`, `peppol`, `xrechnung`, `xrechnung-extension`) are on level EN 16931. UBL ignores `extended()` data and writes sub lines only for the XRechnung extension. ### Capabilities Profiles can implement marker interfaces in `Quittio\Core\Profiles\Capabilities` that change how the writers work: | Interface | Effect | Implemented by | |---|---|---| | `RequiresVatBreakdownRate` | The VAT breakdown always carries a rate, 0 for category O, outside the scope of VAT (BR-DE-14). | `xrechnung`, `xrechnung-extension` | | `StructuredCashDiscountText` | Cash discounts are written to the payment terms (BT-20) in the XRechnung notation `#SKONTO#TAGE=…#PROZENT=…#` ([Payment terms](building-invoices.md#payment-terms)). | `xrechnung`, `xrechnung-extension` | | `DescribesEmbeddedXml` | `xmpDescription()` and `xmpVersion()` replace the XMP metadata of the embedded XML (`Factur-X/ZUGFeRD Invoice`, `fx:Version` `1.0`), e.g. `XRechnung Invoice` and `3.0`. | `xrechnung`, `xrechnung-extension` | | `ExtendsCodeLists` | `additionalCodes(string $codeList)` adds codes to an official code list for the validation. | `xrechnung-extension` (identifier schemes `XR01` to `XR03` for the code lists ICD and EAS, the schemes of party identifiers and electronic addresses) | ### Own profiles and formats A profile implements `Quittio\Core\Profiles\Profile`. `AbstractProfile` provides the defaults: syntax CII, EN 16931:2017 only, embedded as `factur-x.xml` with AFRelationship `Alternative`, EN 16931 compliant, no additional rules. Register a profile in a country pack (`profiles()`) or with `$quittio->profiles()->register(new MyProfile)`. Register a format with `$quittio->formats()->register(new OutputFormat(string $key, ?Syntax $syntax = null, bool $pdf = false, bool $embedsXml = false, ?string $defaultProfile = null, array $profiles = [], bool $html = false, bool $json = false))` and an alias with `alias(string $alias, string $key)`. An empty `$profiles` allows every profile of the syntax. XML is written by one `XmlSerializer` per syntax (`syntax(): Syntax`, `serialize(InvoiceData $invoice, Profile $profile, SerializationOptions $options = new SerializationOptions): string`). `$quittio->addSerializer(…)` replaces the CII or UBL writer, in Laravel `Quittio::extend('serializer', …)`. ## ZUGFeRD and Factur-X ZUGFeRD (Germany) and Factur-X (France) are the same standard: a PDF for people with the CII XML embedded for software ([Formats](concepts.md#formats)). The format `zugferd` produces a PDF/A-3b with the CII XML as associated file, an embedded file that PDF/A-3 links to the document. mPDF, the PHP library that renders the PDF, writes it as PDF/A-3b (`PDFAversion` 3-B). The embedded XML is the same as `xml()` of the run, byte for byte. The PDF metadata carry: - the title "{title} {number}", e.g. "Rechnung RE-2026-0042" ("Rechnung" is the German document title "invoice"), - the seller as author, - "Quittio" as creator. The paper size comes from the presentation, then the option `paper` (Laravel `pdf.paper`), then the country pack (Germany: A4). ### Embedded files The name of the embedded file and its AFRelationship depend on the profile. The AFRelationship is a PDF/A-3 entry that says how the file relates to the document. The XMP metadata, the metadata block of the PDF, declare conformance level and version, so that software finds the invoice: | Profile | File name in the PDF | AFRelationship | XMP `fx:ConformanceLevel` | XMP `fx:Version` | |---|---|---|---|---| | BASIC, EN 16931, EXTENDED | `factur-x.xml` | `Alternative` | `BASIC`, `EN 16931`, `EXTENDED` | `1.0` | | MINIMUM, BASIC WL | `factur-x.xml` | `Data` | `MINIMUM`, `BASIC WL` | `1.0` | | XRechnung, XRechnung extension | `xrechnung.xml` | `Alternative` | `XRECHNUNG` | the XRechnung version, `3.0` | The embedded file has the MIME type `text/xml` and the description "Factur-X/ZUGFeRD Invoice" or "XRechnung Invoice". The XMP metadata contain the Factur-X extension schema (namespace `urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#`, prefix `fx`) and its PDF/A extension schema description. The schema carries: - `DocumentType` `INVOICE`, also for credit notes, - `DocumentFileName`, `Version` and `ConformanceLevel`. Peppol cannot be embedded. XRechnung can: `->format('zugferd')->profile('xrechnung')` gives a PDF with `xrechnung.xml` (ZUGFeRD profile XRECHNUNG). Supporting documents (`attachment()`, BG-24) are part of the XML, base64 encoded. They are not separate files in the PDF: the PDF carries exactly one associated file. ### Drafts and watermarks Drafts and previews carry a watermark (`Presentation::$watermark`, or the option `watermark`). They never produce e-invoice XML, so that a draft cannot look like an issued e-invoice. A run with a watermark and any format with XML throws a `ConfigurationException`. This covers ZUGFeRD/Factur-X, XRechnung, CII, UBL and Peppol, also next to `pdf` in `formats([...])`. Watermarked PDFs are plain PDF without embedded XML. They are not PDF/A, because PDF/A forbids the transparency of the watermark ([Watermark](templates.md#watermark)). ```php Quittio::make($draft)->format('pdf')->option('watermark', 'ENTWURF')->render(); // "Entwurf" = draft ``` ### PDF/A and logos PDF/A is produced for every PDF with embedded XML, and for `pdf` output when `pdf.pdfa` is `true`. PDF/A forbids the alpha channel (transparency) that mPDF writes. Logos in PDF/A documents are therefore re-encoded as RGB PNG on white. SVG logos are rasterised with the PHP extension `imagick` and must be self-contained. Without `imagick`, rendering an SVG logo fails with a `ConfigurationException` ([Logos](templates.md#logos)). ### PDF renderers The PDF comes from a `Quittio\Core\Pdf\PdfRenderer` (`render(RenderRequest $request): string`). Quittio ships three: - `MpdfRenderer`, the default, - `CallbackRenderer` for another HTML-to-PDF engine (Browsershot, Gotenberg …), - `PassthroughRenderer` for a finished PDF. How to configure and register them is described in [PDF renderers](templates.md#pdf-renderers). For the e-invoice, two fields of the request matter. `RenderRequest::$pdfa` asks for PDF/A-3b. `RenderRequest::$embedded` carries the XML as `EmbeddedXml` (`content`, `filename`, `relationship`, `conformanceLevel`, `description`, `documentType`, `version`). - A renderer whose `supportsEmbedding()` returns `true` handles both itself. - For a renderer that returns `false`, the core passes the result to `MpdfPostProcessor` whenever PDF/A or an embedded XML is requested. - `CallbackRenderer` and `PassthroughRenderer` return `true` and use their own post processor (default `MpdfPostProcessor`). It imports every page into a new PDF/A-3b and adds the output intent, the metadata, the XML as associated file and the Factur-X XMP metadata. So PDFs from other engines become ZUGFeRD. A result that does not start with `%PDF` throws a `PdfException`. ## XRechnung XRechnung 3.0 is the German standard for invoices to public authorities. It is a CIUS of EN 16931: a national specialisation with additional rules. The German country pack provides it ([XRechnung profiles](country-packs.md#xrechnung-profiles)) in both syntaxes: - as plain XML: `xrechnung-cii` (alias `xrechnung`) and `xrechnung-ubl`, - embedded in ZUGFeRD, as shown in [Embedded files](#embedded-files). The automatic choice uses `xrechnung-cii` for domestic German invoices whose buyer reference (BT-10) is a Leitweg-ID with valid check digits, or whose buyer electronic address has the scheme `0204`. XRechnung adds to EN 16931: - Required fields: the buyer reference (BT-10; for public buyers the Leitweg-ID), the electronic addresses of seller and buyer (BT-34, BT-49), a seller contact with name, phone and email (BG-6), payment instructions (BG-16), and city and postcode of seller and buyer. - Further XRechnung rules with the codes `BR-DE-…` ([XRechnung rules](country-packs.md#xrechnung-rules)). - The business process (BT-23) defaults to `urn:fdc:peppol.eu:2017:poacc:billing:01:1.0`. - Cash discounts in the payment terms (BT-20) use the notation `#SKONTO#TAGE=7#PROZENT=2.00#` (Skonto = cash discount, Tage = days, Prozent = percent; see [Payment terms](building-invoices.md#payment-terms)). - The VAT breakdown always carries a rate. The **XRechnung extension** (`->profile('xrechnung-extension')`, specification identifier `…#conformant#urn:xeinkauf.de:kosit:extension:xrechnung_3.0`) adds: - sub lines, in UBL only (`xrechnung-ubl`; CII fails with BR-DEX-15), - third party payments (`xrechnungExtension()`, BG-DEX-09), written as `cac:PrepaidPayment` and added to the amount due; only in UBL invoices, - the identifier schemes `XR01` to `XR03`. The extension is never chosen automatically; request it explicitly. It has no credit note in UBL (`DE-XRECHNUNG-EXT-01`): cancel such invoices with the strategy `negative_invoice`. ## Peppol BIS Billing 3.0 Peppol BIS Billing 3.0 is the invoice specification of the Peppol network. The format `peppol` writes UBL 2.1 according to it, with the profile `peppol`: - CustomizationID (the specification identifier, BT-24): `urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0`, - ProfileID (the business process, BT-23): `urn:fdc:peppol.eu:2017:poacc:billing:01:1.0`, unless the invoice sets BT-23. Seller and buyer need an electronic address (BT-34, BT-49), and the Peppol rules apply ([Validation](validation.md)). The electronic address is the Peppol participant identifier, so its scheme must be one Peppol accepts (`PEPPOL-EN16931-CL008`): e.g. `9930` (German VAT number) or `0088` (GLN), not `EM` (e-mail), which EN 16931 allows. Credit note type codes produce a `CreditNote` document. Quittio produces the document. Sending it through an access point, the service provider that connects you to the Peppol network, is not part of the core. ## CII and UBL The formats `cii` and `ubl` write plain XML in any profile of the syntax. Both writers require calculated data; the render run calculates. They write the elements in the order of the XSD, the XML schema of the syntax. **CII** (UN/CEFACT Cross Industry Invoice D16B, CEN/TS 16931-3-3, root `rsm:CrossIndustryInvoice`) is the syntax of ZUGFeRD/Factur-X and supports all Factur-X levels: - It carries only one preceding invoice reference (BG-3) on header level, so further references are dropped. - Party identifiers without scheme are written as `ram:ID`, with scheme as `ram:GlobalID`. - An account identifier that is no IBAN is written as `ProprietaryID`. - The tax point date is written with the first VAT breakdown. **UBL** (OASIS UBL 2.1, CEN/TS 16931-3-2) writes an `Invoice` document. For credit note type codes (`InvoiceTypeCode::isCreditNote()`) it writes a `CreditNote` document, whose element order differs from `Invoice`: - quantities as `CreditedQuantity`, - the due date (BT-9) in the first payment means; without payment instructions it is not written, - the project reference (BT-11) as additional document reference with type code 50, followed by the tender or lot reference (BT-17) as `OriginatorDocumentReference`. In both UBL documents: - A note with subject code is written as `#CODE#text`. - The creditor identifier of a direct debit (BT-90) is written as seller party identifier with scheme `SEPA`. - A sales order reference without purchase order reference is written with the order ID `NA`. - The payment card network is written as `NA`. Rules of both writers: - Amounts have exactly two decimals, prices and percentages at least two decimals, quantities no trailing zeros. The decimal separator is a dot. - The VAT accounting currency (BT-6) is written only when it differs from the invoice currency (BT-5). - Texts generated for the payment terms (BT-20) are in the run locale. `SerializationOptions(string $locale = 'en', bool $indent = true)` controls locale and indentation when you use a writer (`CiiWriter`, `UblWriter`) directly. ## PDF, HTML and JSON These formats are not e-invoices. The validation skips the profile field matrix (level L2) for them; the other levels run as usual. - `pdf` renders the template as PDF without e-invoice XML. It is PDF/A-3b unless `pdf.pdfa` is `false` or the run has a watermark. - With `pdf.b2c_embed_xml = true` (Laravel `format_resolver.b2c_embed_xml`), `pdf` embeds the CII XML as in ZUGFeRD, also for consumers. The XML uses the profile of the call or the configured profile (default EN 16931). The result then has `xml()` and `profile()`. Watermarked runs never embed XML. - `html` returns the rendered template as HTML (`html()`), e.g. for previews or mails. - `json` returns the calculated data model (`json()`). It is the same as `InvoiceData::toJson(JSON_PRETTY_PRINT)` of the calculated data ([Arrays and JSON](building-invoices.md#arrays-and-json)). ## Rendering in the core This section is the reference of a render call. The examples use the core class `Quittio\Core\Quittio` without Laravel. In Laravel, `Quittio::make()` returns the Laravel variant with the same methods plus `queue()`; its result can also be saved to a disk, downloaded and mailed ([Rendering invoices](laravel.md#rendering-invoices)). ```php use Quittio\Core\Quittio; use Quittio\Country\DE\CountryDE; $quittio = Quittio::create([], [new CountryDE]); $result = $quittio->make($invoice)->render(); // automatic choice $result = $quittio->make($invoice)->format('zugferd')->profile('extended')->render(); $result = $quittio->make($invoice)->formats(['zugferd', 'xrechnung-cii', 'json'])->render(); $result->as('xrechnung-cii')->xml(); ``` `make(InvoiceData|ProvidesInvoiceData|InvoiceBuilder $data)` returns a `Quittio\Core\Rendering\PendingInvoice`. A builder is built, and a `ProvidesInvoiceData` model is converted. | `PendingInvoice` method | Description | |---|---| | `format(string $format)` | One output format. Without it: the configured format (`format`, Laravel `defaults.format`), or the [automatic choice](#automatic-choice) if none is configured. | | `formats(array $formats)` | Several formats from one calculation and validation run, e.g. `['zugferd', 'xrechnung-cii']`; duplicates are removed. The first is the main result. | | `profile(Profile\|string $profile)` | Profile for all formats of the run (by key, alias or as object). | | `locale(string $locale)` | Language of PDF, HTML and generated XML texts. | | `country(string $country)` | Country pack that leads the run, instead of the seller's country (BT-40). | | `template(string $template)` | Template of PDF and HTML ([Templates and design](templates.md)). | | `presentation(Presentation $presentation)` | Design defaults for this run; the invoice's own values win. | | `option(string $key, mixed $value)` | Option for templates and validation rules, e.g. `watermark`, `paper`, `markdown`. | | `validate(bool $throwOnError = true)` | Validation mode of this run: errors throw, or (`false`) warnings only. | | `withoutValidation()` | Switches the validation off for this render run (not for `check()` and `prepare()`). | | `render(): RenderedInvoice` | Calculates, chooses format and profile, validates, writes the XML, renders template and PDF. | | `check(): ValidationResult` | Validates like a render run, without template and PDF, whatever the validation mode. This includes the XML levels, also for the XML that `pdf` embeds with `pdf.b2c_embed_xml`. Returns the result and does not throw for invalid data. | | `prepare(): PreparedInvoice` | Calculates and validates like `check()` without rendering. Returns the calculated data, the chosen formats with their profiles (`formats`, `format()`, `profile()`; `null` for formats that write or embed no XML), the leading and buyer country packs, the locale and the validation result. | | `data(): InvoiceData`, `options(): RenderOptions` | The data and the collected options. | ### Result `render()` returns a `Quittio\Core\Rendering\RenderedInvoice`: | Method | Description | |---|---| | `data(): InvoiceData` | The calculated data (totals, VAT breakdown, line amounts, due date). | | `format(): string`, `profile(): ?Profile` | Format key (aliases resolved, e.g. `zugferd` for `factur-x`) and profile; `null` for outputs without XML. | | `validation(): ValidationResult` | Validation result including warnings ([Validation](validation.md)). | | `as(string $format): RenderedInvoice` | The result of another format of the same run, by format key (not alias); `LogicException` for a format that was not rendered. | | `formats(): array` | Keys of all formats of the run. | | `pdf()`, `xml()`, `html()` | The outputs; `LogicException` if the format produced none. `hasPdf()`, `hasXml()` check first. | | `json(): string` | The calculated data as pretty-printed JSON (for every format). | | `contents(): string`, `extension(): string`, `mimeType(): string` | The main output (PDF, otherwise XML, HTML, JSON), its extension and MIME type. | | `filename(?string $extension = null): string` | File name of the main output, or with another extension. | | `hashes(): array` | SHA-256 hashes of PDF, XML and HTML (`['pdf' => …, 'xml' => …]`), e.g. for archiving. | | `save(string $path): string` | Writes the main output to a file, or into a directory (an existing one or a path ending in `/`) with `filename()`; creates missing directories and returns the path. Throws a `RuntimeException` when the file cannot be written. | ### File names File names follow the pattern `filename` (Laravel `pdf.filename`), default `{title}_{number}.{ext}`: | Placeholder | Value | |---|---| | `{title}` | Document title in the run locale, e.g. "Rechnung" (invoice), "Stornorechnung" (cancellation invoice), "Credit note" (`Presentation::$title` overrides it) | | `{number}` | Invoice number | | `{date}` | Issue date, `YYYY-MM-DD` | | `{format}` | Format key, e.g. `zugferd` | | `{ext}` | Extension of the output: `pdf`, `xml`, `html` or `json` | In title and number, characters other than letters, digits, `.`, `_` and `-` become `_`. A run of such characters becomes one `_`, and `_` at the start or end is removed. Example: `Rechnung_RE-2026-0042.pdf`; the XRechnung of the same run is `Rechnung_RE-2026-0042.xml`. ## Output configuration | `Quittio::create()` key | Laravel key | Default | Effect | |---|---|---|---| | `format` | `defaults.format` | `null` | Format of runs without `format()`; `null` = automatic choice (`Quittio::create()` also accepts `auto`). | | `profile` | `defaults.profile` | `null` | Default profile for every format that allows it; `null` = the profile of the format or of the automatic choice. | | `auto_upgrade_profile` | `format_resolver.auto_upgrade_profile` | `true` | Upgrade to EXTENDED when EXTENDED features are used. | | `pdf.b2c_embed_xml` | `format_resolver.b2c_embed_xml` | `false` | Embed the CII XML into `pdf` output. | | `pdf.pdfa` | `pdf.pdfa` | `true` | `pdf` output as PDF/A-3b (ZUGFeRD is always PDF/A). | | `filename` | `pdf.filename` | `{title}_{number}.{ext}` | [File names](#file-names). | | `locale`, `country` | `defaults.locale`, `defaults.country` | `null` | Locale of the run; country pack used when the seller has no country code. | | `options.paper` | `pdf.paper` | `null` | Paper size that wins over the country pack default. | | `temp_dir` | `pdf.cache_path` | system temp directory | Temporary files of mPDF and the logo cache. | | – | `format_resolver.driver` | `default` | Resolver registered with `Quittio::extend('format_resolver', …)`. | | `peppolLookup` of `DefaultFormatResolver` | `format_resolver.peppol_lookup` | `null` | [Peppol lookup](#peppol-lookup): invokable class (Laravel: resolved from the container) or callable that receives the `BuyerParty` and returns whether the buyer is reachable via Peppol. | | – | `pdf.renderer` | `mpdf` | Renderer registered with `Quittio::extend('renderer', …)`. | All keys of `config/quittio.php` are described in [Configuration](configuration.md). # Validation Quittio checks every invoice before it issues it: against the rules of EN 16931, of the profile (for example XRechnung or Peppol, see [Formats](concepts.md#formats)) and of the country (for example the German VAT Act). This page explains what happens when an invoice is invalid, how to read and handle the messages and how to configure the checks. It then describes the validation levels, the installation of the official external validators, all built-in rules and how to add your own. Rule codes such as `BR-CO-25` are explained in [Rules and validation](concepts.md#rules-and-validation), field numbers such as BT-10 in [EN 16931 and business terms](concepts.md#en-16931-and-business-terms). Common errors and their fixes are listed in [Troubleshooting](troubleshooting.md). ## Errors while rendering By default (validation mode `error`), an invoice with errors is not rendered: `render()` throws a `Quittio\Core\Validation\ValidationException`. Its message lists the first five errors, each with rule code, field and message, and the hint on the next line. For example, an XRechnung without buyer reference: ```text The invoice is not valid (2 errors): [QUITTIO-PROFILE-REQUIRED] BT-10: The field buyerReference (BT-10) is mandatory in profile XRechnung 3.0. Provide the field or choose a profile that does not require it. [BR-DE-15] BT-10: The buyer reference (BT-10) must be provided. Set the Leitweg-ID with ->buyerReference('04011000-12345-03'). ``` More than five errors end with "… and N more."; the full result is in `$e->result`. When only warnings block (with `block_on_warnings`), the message lists the blocking warnings instead. ### Reading a message Each finding starts with its rule code in brackets: - `[BR-DE-15]` is the rule code. Its prefix tells where the rule comes from: `BR-` EN 16931, `BR-DE-` XRechnung, `PEPPOL-` Peppol, `DE-USTG-` the German VAT Act (UStG), `QUITTIO-` Quittio itself. All built-in rules are listed in [Rule sets](#rule-sets). - `BT-10` is the field, here the buyer reference. The [Field reference](field-reference.md) shows where each field is in the data model. - The text is the message. The indented line below is the hint, usually the builder method that fixes the error. The hint of `BR-DE-15` sets the Leitweg-ID, the routing ID of a German public buyer ([German terms](concepts.md#german-terms)). One missing value can be reported by more than one rule. In the example, the field check of the profile (level L2, `QUITTIO-PROFILE-REQUIRED`) and the XRechnung rule `BR-DE-15` (level L3) both report the buyer reference. Setting it fixes both. ### Handling the exception ```php use Quittio\Core\Validation\ValidationException; use Quittio\Laravel\Facades\Quittio; try { $rendered = Quittio::make($invoice)->format('xrechnung-cii')->render(); } catch (ValidationException $e) { foreach ($e->result->errors() as $message) { report($message->code.' '.$message->message); } } ``` `$e->result` is a [`ValidationResult`](#the-result) with all findings. The Laravel integration dispatches two events ([Events](laravel.md#events)): - `InvoiceValidationFailed($invoice, $result)` before the exception is thrown, - `InvoiceValidated($invoice, $result)` after a successful run. This includes mode `warn`, with warnings or errors, but not mode `off`, where nothing is validated. Other exceptions of a render run: - `ConfigurationException`: unknown format, profile not supported by the format, watermark with e-invoice XML, invalid design values, - `Quittio\Core\View\MandatoryFieldMissing` from the [mandatory field guard](#mandatory-field-guard). All exceptions of the package implement `Quittio\Core\Exceptions\QuittioException`, so `catch (QuittioException $e)` catches every error of Quittio. Invalid arguments and runtime errors without a class of their own are `InvalidArgumentException`, `RuntimeException` or `LogicException` from `Quittio\Core\Exceptions`; they extend the PHP classes of the same name. ### Warnings Every finding has a severity: error, warning or info. Only errors stop the rendering. Warnings, for example a BIC with an invalid format (`QUITTIO-BIC`), are attached to the result: `$rendered->validation()->warnings()`. With `block_on_warnings` set to `true`, warnings block like errors ([Configuration](#configuration)). ### Checking without rendering While you build an invoice, `check()` instead of `render()` shows all findings at once. It validates like `render()`, renders no PDF, returns the `ValidationResult` and never throws for invalid data: ```php use Quittio\Laravel\Facades\Quittio; $report = Quittio::invoice()->buyer($customer)->line('Consulting', netPrice: '120.00')->check('xrechnung-cii'); $report = Quittio::make($invoice)->format('xrechnung-cii')->check(); $report->isValid(); $report->errors(); ``` Details are in [When validation runs](#when-validation-runs). ## Configuration The validation mode decides what happens with errors: | Mode | Behaviour | |---|---| | `error` (default) | Errors throw a `ValidationException`; nothing is rendered. | | `warn` | The run continues. `$rendered->validation()` holds all findings, errors included. | | `off` | Nothing is validated while rendering; the result is empty. `check()` and `prepare()` still validate. | Set it for the application with `validation.mode` (environment variable `QUITTIO_VALIDATION_MODE`), or for one call. ### Per call ```php Quittio::make($invoice)->validate()->render(); // mode error Quittio::make($invoice)->validate(false)->render(); // mode warn: errors are attached, nothing is thrown Quittio::make($invoice)->withoutValidation()->render(); // mode off: empty result (check() and prepare() still validate) Quittio::make($invoice)->option('max_decimals', 6)->render(); // option for the rules ``` ### Settings In Laravel (keys of `config/quittio.php`, see [Configuration](configuration.md#validation)). The levels L1 to L7 are described in [Validation levels](#validation-levels) below. | Key | Values | Default | |---|---|---| | `validation.mode` | `error` (throw), `warn` (attach the result only), `off` | `error` (`QUITTIO_VALIDATION_MODE`) | | `validation.levels` | `php` (L1, L2, L3, L7), `xsd` (L4), `kosit` (L5), `verapdf` (L6), or the name of a driver registered with `Quittio::extend('validator', …)` | `['php', 'xsd']` | | `validation.block_on_warnings` | `true`: warnings block like errors | `false` | | `validation.locale` | language of the messages: `null` = the language of the application, `'invoice'` = the language of the invoice, or a locale | `null` (`QUITTIO_VALIDATION_LOCALE`) | | `validation.mandatory_field_guard` | `exception`, `log`, `off`; `null` = `exception` outside production, `log` in production | `null` (`QUITTIO_MANDATORY_FIELD_GUARD`) | | `validation.rules` | single PHP rules switched off or re-rated by code, see [Switching rules off](#switching-rules-off-and-changing-their-severity) | nothing changed | | `validation.drivers.kosit` | see [KoSIT validator](#kosit-validator-l5) | | | `validation.drivers.verapdf` | see [veraPDF](#verapdf-l6) | | | `validation.drivers.vies` | see [VIES](#vat-identifiers-and-vies) | | Without Laravel, `Quittio::create()` takes the same settings. It uses the level names of the enum and separate switches for the external validators: ```php use Quittio\Core\Quittio; use Quittio\Country\DE\CountryDE; $quittio = Quittio::create([ 'validation' => [ 'mode' => 'error', // error, warn, off 'levels' => ['L1', 'L2', 'L3', 'L4', 'L7'], // default; L5 and L6 are switched on below 'block_on_warnings' => false, 'kosit' => ['enabled' => true, 'mode' => 'daemon', 'url' => 'http://127.0.0.1:8081'], 'verapdf' => ['enabled' => true, 'mode' => 'docker'], ], 'mandatory_fields' => 'exception', // exception, log, off 'options' => ['max_decimals' => 4], // options for rules, see below ], [new CountryDE]); ``` In `Quittio::create()`: - The PHP driver runs only the listed levels of L1, L2, L3 and L7, so L7 can be left out. - `L4` adds the XSD driver. - The KoSIT and veraPDF drivers are added only with `enabled => true`. Listing `L5` or `L6` alone has no effect. - An unknown mode throws a `ConfigurationException`. ### Options for rules Some rules read options. Set them per call with `->option()`, without Laravel also in `options`: | Option | Used by | Default | |---|---|---| | `max_decimals` | `QUITTIO-PRECISION`: more decimals in quantity or net price are a warning | `4` | | `attachment_max_bytes` | `QUITTIO-ATTACHMENT-SIZE`: limit per attached document | 10 MB | | `attachments_max_total_bytes` | `QUITTIO-ATTACHMENT-SIZE`: limit of all attachments | 15 MB | | `syntax` | Syntax of the XML that will be written (`Quittio\Core\Profiles\Syntax`); set by the run for every format with XML, used by `QUITTIO-THIRD-PARTY-PAYMENTS`, `QUITTIO-CREDIT-NOTE-DUE-DATE`, `BR-DEX-15` and `DE-XRECHNUNG-EXT-01` | from the format | ## Validation levels The checks are split into seven levels. Each level is checked by a driver. The default configuration runs L1 to L4 and L7 in PHP. L5 and L6 use the official external validators, which you install separately. | Level | What is checked | Driver | Stage | |---|---|---|---| | L1 | Structure and data formats of the data model (calculated, country and currency codes, decimals) | `php` | data | | L2 | Mandatory and allowed fields of the profile (field matrix) | `php` | data | | L3 | Business rules: EN 16931 (`BR-…`, `BR-CO-…`, `BR-CL-…`, VAT categories), package rules (`QUITTIO-…`), profile rules (Peppol, XRechnung) and country pack rules (`DE-USTG-…`) | `php` | data | | L4 | XML against the XSD (CII D16B, UBL 2.1) | `xsd` | xml | | L5 | XML against the official Schematron (KoSIT validator: EN 16931, XRechnung, Peppol) | `kosit` | xml | | L6 | PDF/A-3b conformance | `verapdf` | pdf | | L7 | Plausibility: IBAN, BIC, VAT identifier formats, e-mail addresses, dates, VAT category against the destination, Leitweg-ID, optional VIES check | `php` | data | Some terms of the table: - The XSD (XML Schema Definition) describes which elements an XML file of a syntax may contain. - The Schematron files are the official rules of EN 16931, XRechnung and Peppol, published as executable checks. The [KoSIT validator](#kosit-validator-l5) runs them. - PDF/A-3b is the archiving variant of PDF that may carry embedded files. [veraPDF](#verapdf-l6) checks it. - VIES is the EU service that confirms VAT identification numbers online ([VAT identifiers and VIES](#vat-identifiers-and-vies)). L1 to L3 and L7 run in PHP. L4 needs only `ext-dom`. L5 needs Java (or Docker), L6 needs veraPDF (or Docker). No other external validator (for example Mustang) is integrated; add one as [own driver](#own-rules-and-drivers). ## When validation runs A render run validates in three stages, per requested format: 1. **data** (L1, L2, L3, L7) on the calculated invoice, before any XML is written; 2. **xml** (L4, L5) on the generated XML, for formats with XML (and for `pdf` with embedded XML for consumers, `b2c_embed_xml`); 3. **pdf** (L6) on the rendered PDF. In mode `error`, the run stops after the first stage that has errors (or warnings with `block_on_warnings`). Data errors throw before XML is written, XML errors before the PDF is rendered. A driver runs only if it supports the subject: XML drivers need XML, veraPDF needs a PDF. The validation context holds: - the profile of the format, - the country packs: the seller's pack first, the buyer's pack for cross-border invoices, - the locale of the run: the language of the invoice, which rules use for its texts, - the language of the messages ([Messages and translations](#messages-and-translations)), - the options. Formats without XML (`pdf`, `html`, `json`) are no e-invoices: L2 (profile field matrix) is skipped for them. With several formats (`->formats([...])`), every format is validated with its own profile and syntax. `check()` and `prepare()` validate like a render run without template and PDF: ```php $result = Quittio::make($invoice)->format('xrechnung-cii')->check(); // ValidationResult $prepared = Quittio::make($invoice)->formats(['zugferd'])->prepare(); // PreparedInvoice: data, formats, country, buyerCountry, locale, validation ``` They are explicit validation requests: - They run the data and xml stages of every requested format with all configured drivers, whatever the validation mode (also `off` and `withoutValidation()`). The [Managed layer](managed.md) freezes invoices with `prepare()`. - They never throw a `ValidationException`. Configuration errors, such as an unsupported profile or an XML format for watermarked data, still throw. - They take the same decisions as `render()`. For `pdf` with `b2c_embed_xml` (and no watermark), the embedded CII XML is generated and validated in the same profile, so what is checked is what is rendered. - L6 does not run, because it needs the rendered PDF. ## The result `Quittio\Core\Validation\ValidationResult` (immutable, `JsonSerializable`): | Member | Returns | |---|---| | `messages` | `list` – all findings | | `levels` | `list` – the levels that were checked | | `isValid()` | `true` if there is no finding with severity `error`; warnings never make a result invalid | | `hasWarnings()` | whether there are warnings | | `errors()`, `warnings()`, `infos()` | findings of one severity | | `byLevel(ValidationLevel $level)` | findings of one level | | `has(string $code)` | whether a finding with this code exists | | `merge(ValidationResult $other)` | new result with the messages of both and the union of the levels | | `toArray()`, `jsonSerialize()` | `['valid' => bool, 'levels' => ['L1', …], 'messages' => [ … ]]` | | `toHtml()` | self-contained HTML fragment: status ("Valid", "Valid with warnings", "Invalid") and a table with severity, level, code, BT and message (with the hint); CSS classes `quittio-report`, `quittio-report-status`, `quittio-valid`, `quittio-invalid`, `quittio-{severity}` | `Quittio\Core\Validation\ValidationMessage` (readonly): | Property | Content | |---|---| | `severity` | `Severity::Error`, `Severity::Warning` or `Severity::Info` (`error`, `warning`, `info`) | | `code` | rule identifier, e.g. `BR-CO-25`, `BR-DE-15`, `DE-USTG-03`, `PEPPOL-EN16931-R010`, `QUITTIO-IBAN`, `XSD`, `PDFA-6.2.2-1` | | `message` | text in the language of the messages (`validation.locale`, see [Messages and translations](#messages-and-translations)), where a translation exists | | `level` | `ValidationLevel::L1` … `L7` | | `bt` | business term or group, e.g. `BT-10`, `BG-23` (nullable) | | `path` | path in the data model, e.g. `buyer.vatId`, `line 2.price.netPrice`, `vatBreakdown[0].taxAmount`; for XSD `line 12` (nullable) | | `hint` | solution hint from the message catalog (nullable) | | `xpath` | location in the XML (KoSIT findings, nullable) | `isError()` and `toArray()` are available on every message. Results of several drivers are merged by concatenation. A rule that is checked both in PHP (L3) and by the official Schematron (L5) can therefore appear twice, with the two levels. Within the PHP driver, identical findings (same code, path and message) are reported once. ## Messages and translations In Laravel, messages and hints are written in the language of the application (`app()->getLocale()`). A developer of an English application therefore reads English messages, also for German invoices. `validation.locale` sets `'invoice'` (the language of the invoice, as in the PDF) or a fixed locale. Without Laravel, the default is the language of the invoice. `Quittio::create(['validation' => ['locale' => 'en']])` sets a fixed locale, `'app'` the locale of the translator. Rules that check texts of the invoice, e.g. the reverse charge statement of `DE-USTG-06` (the note that the buyer owes the VAT), still use the language of the invoice. Messages come from the catalog `quittio::rules.{CODE}` with the entries `message` and `hint`; placeholders such as `:value` are replaced. The core ships the catalog in German and English (`packages/core/resources/lang/{de,en}/rules.php`). Other locales fall back to English, and rules without an entry use their English fallback text. Country packs bring their own namespace (`quittio-de::rules.DE-USTG-01`). To override messages in Laravel, publish the language files with `php artisan vendor:publish --tag=quittio-lang` and edit `lang/vendor/quittio/{locale}/rules.php`. Without Laravel, register a directory with `{locale}/rules.php` files; later paths override earlier ones: ```php use Quittio\Core\I18n\Translations; Translations::translator()->addPath(__DIR__.'/lang', 'quittio'); ``` Findings of the XSD validation, the KoSIT validator, veraPDF and the invoice reader are not translated: they carry the text of the tool or an English text. ## XML schema (L4) `Quittio\Core\Validation\Drivers\XsdValidator` validates against the bundled schemas (`packages/core/resources/xsd`): CII D16B for `CrossIndustryInvoice`, UBL 2.1 for `Invoice` and `CreditNote`. It needs no installation. All its findings are errors: | Code | Meaning | |---|---| | `XSD` | The XML does not match the schema; the path is `line N` | | `XSD-NOT-WELL-FORMED` | Unsafe or broken XML, see [reading](reading.md#limits-and-security) | | `XSD-UNKNOWN-ROOT` | No schema for the root element | Use it on its own with `(new XsdValidator)->validateXml($xml)` (returns `list`). ## KoSIT validator (L5) KoSIT, the German agency that publishes XRechnung, provides the official validator. The official Schematron rules are XSLT 2.0, which PHP cannot execute, so L5 uses the KoSIT validator (Java). It checks EN 16931 and XRechnung (CII and UBL) with the XRechnung validator configuration, and Peppol BIS Billing 3.0 with a second configuration. To use it: 1. Download the validator and its configurations ([Installation](#installation)). 2. Start it as a daemon, or let Quittio call it per document ([Modes](#modes)). 3. Add `kosit` to `validation.levels`. ### Installation ```bash php artisan quittio:schemas:download # into storage/quittio/validators php artisan quittio:schemas:download --path=/opt/validators --force --java=/usr/bin/java php artisan quittio:schemas:download --without-peppol ``` The command downloads into the target directory: - the validator `1.6.3` (standalone JAR from Maven Central) as `validator.jar`, - the XRechnung validator configuration `v2026-08-31` (XRechnung 3.0.2) into `xrechnung/`. It then compiles the Peppol BIS Billing 3.0 Schematron (`v3.0.20`, with SchXslt 1.10.1 and the Saxon bundled in the validator) into `peppol/PEPPOL-EN16931-UBL.xsl`. Compiling needs Java; skip it with `--without-peppol`. Existing files are kept unless `--force` is given. The PHP extension `zip` is required. Java 11 or newer is required to run the validator. At the end, the command prints the configuration to add and the command that starts the daemon on the port of `validation.drivers.kosit.url` (`-P `). Without Laravel use `Quittio\Core\Validation\Drivers\KositArtifacts`: - `download(string $target, bool $force = false)` returns `['jar' => …, 'scenarios' => …, 'repository' => …]`, - `compilePeppol(string $target, string $java = 'java', bool $force = false)` returns `['scenarios' => …, 'repository' => …]`. The scenario file `peppol-scenarios.xml` ships with the core (`KositArtifacts::peppolScenarios()`). ### Modes | Mode | How | Configuration | |---|---|---| | `daemon` (default, recommended) | HTTP POST of the XML (`Content-Type: application/xml`) to `url`; a running validator answers with its report | `url` | | `cli` | One JVM per document: `java -jar {jar} -s {scenarios} -r {repository} … -o {tmp} document.xml` | `jar`, `scenarios` and `repository` and/or `configurations`, `java` | Start a daemon with both configurations (paths as printed by the download command) on the port of the default URL `http://127.0.0.1:8081`. The validator itself listens on port 8080 unless it is started with `-P`: ```bash cd storage/quittio/validators java -jar validator.jar \ -s xrechnung/scenarios.xml -r xrechnung \ -s ../../../vendor/quittio/core/resources/kosit/peppol-scenarios.xml -r . \ -D -P 8081 ``` The repository of Quittio contains a Docker image with the same setup (`tools/docker/kosit/Dockerfile`). It listens on 8080 inside the container: ```bash docker build -t quittio/kosit -f tools/docker/kosit/Dockerfile . docker run --rm -p 8081:8080 quittio/kosit # maps it to the default port 8081 ``` With another port, set `QUITTIO_KOSIT_URL` (without Laravel `validation.kosit.url`) accordingly. Any endpoint that accepts the POST and returns a KoSIT report works. `validation.drivers.kosit`: | Key | Meaning | Default | |---|---|---| | `mode` | `daemon` or `cli` (other values throw `InvalidArgumentException`) | `daemon` | | `url` | daemon endpoint | `http://127.0.0.1:8081` (`QUITTIO_KOSIT_URL`) | | `jar` | path of `validator.jar` (cli) | `null` | | `scenarios`, `repository` | main configuration (cli) | `null` | | `configurations` | further configurations (cli), `[['scenarios' => …, 'repository' => …]]`, e.g. Peppol | `[]` | | `java` | Java binary (cli) | `java` | | `timeout` | seconds per document (HTTP timeout or process limit) | `30` | | `fail_on_unavailable` | an unreachable validator is an error (`true`) or a warning (`false`) | `true` | Add `kosit` to `validation.levels`. Without Laravel, the same keys go to `validation.kosit` with `enabled => true`; use `configurations` for all scenario sets there. ### Findings Every message of the report becomes a finding with: - the rule code of the report (`BR-DE-15`, `PEPPOL-EN16931-R010` …), - the severity of the report (`error`, `warning`, otherwise `info`), - the business term found in the text, - the XPath location. Findings of the schema step of the validator have the level L4, all others L5. A message of the schema step without a code of its own gets the code `KOSIT-XSD`. Additional codes: | Code | Severity | Meaning | |---|---|---| | `KOSIT-UNAVAILABLE` | error or warning (`fail_on_unavailable`) | No report: daemon unreachable or HTTP status other than 200, 406 or 422, JAR, configuration or Java missing, timeout | | `KOSIT-NO-SCENARIO` | warning | The configuration has no scenario for the document (e.g. Factur-X EXTENDED); only the XML schema was checked | | `KOSIT-REJECT` | error | The validator recommends rejection although no message is an error | | `KOSIT` | from the report | Message without code | The raw report of the last call is available from the driver instance with `lastReport()`, e.g. for archiving the test report. `KositReportParser::parse($report)` returns `['accepted' => bool, 'scenario' => ?string, 'messages' => list]`. ## veraPDF (L6) veraPDF is an open-source validator for PDF/A. `Quittio\Core\Validation\Drivers\VeraPdfValidator` checks the rendered PDF against PDF/A-3b (`--flavour 3b`, machine readable report, at most 5 failures per rule). It runs for formats that produce a PDF. ### Installing veraPDF veraPDF runs in one of two modes: | Mode | How | |---|---| | `cli` | local veraPDF installation (Java); the PDF is written to a temporary file and passed to `binary` | | `docker` | `docker run --rm -i {image}`; the PDF is passed via stdin | For mode `docker`, Docker is enough; fetch the image once with `docker pull verapdf/cli`. For mode `cli`, install veraPDF locally and make its `verapdf` command available (or set `binary`). Then add `verapdf` to `validation.levels`; without Laravel use `validation.verapdf` with `enabled => true`. `validation.drivers.verapdf` (all keys are in the published configuration): | Key | Meaning | Default | |---|---|---| | `mode` | `cli` or `docker` | `cli` in the Laravel configuration, `docker` without Laravel | | `binary` | veraPDF binary (cli) | `verapdf` | | `image` | Docker image (docker) | `verapdf/cli` | | `docker` | Docker binary (docker) | `docker` | | `timeout` | seconds | `120` | | `fail_on_unavailable` | an unavailable veraPDF is an error (`true`) or a warning (`false`) | `true` | Findings of veraPDF: | Code | Meaning | |---|---| | `PDFA-{clause}-{test}` | Failed rule, e.g. `PDFA-6.2.2-1`; the hint names specification, clause and description, the path the PDF object | | `VERAPDF-NOT-COMPLIANT` | Not compliant without a failed rule in the report | | `VERAPDF-NOT-PARSED` | veraPDF could not validate the file | | `VERAPDF-UNAVAILABLE` | veraPDF could not be run, e.g. `verapdf` or `docker` not installed. It is reported as a finding, not thrown: an error with `fail_on_unavailable` (in mode `error` the run then stops with a `ValidationException`), otherwise a warning | All findings of the external validators are merged into the result of the run like the PHP findings. ## Rule sets The PHP driver (`Quittio\Core\Validation\Drivers\PhpRulesDriver`, name `php`) runs these rule sets, filtered by the configured levels: | Rule set | Level | Codes | |---|---|---| | `StructureRules` | L1 | `QUITTIO-NOT-CALCULATED` (no totals), `QUITTIO-FORMAT-COUNTRY` (BT-40, BT-55 not two upper case letters), `QUITTIO-FORMAT-CURRENCY` (BT-5, BT-6 not three upper case letters), `QUITTIO-PRECISION` (warning, more decimals than `max_decimals`) | | `FieldMatrixRule` | L2 | `QUITTIO-PROFILE-REQUIRED` (error: mandatory field of the profile missing), `QUITTIO-PROFILE-NOT-ALLOWED` (warning: field is not part of the profile and is not written), `QUITTIO-PROFILE-EXTENDED-ONLY`, `QUITTIO-PROFILE-XRECHNUNG-EXTENSION-ONLY`, `QUITTIO-PROFILE-SUBLINES` (errors). The matrix comes from the mapping attributes of the data model ([field reference](field-reference.md)) plus the business terms the profile requires (XRechnung: BT-10, BT-34, BT-49, BG-6, BG-16); BT-23 is not required when the profile sets a default business process | | `En16931Rules` | L3 | `BR-02`, `BR-04` to `BR-07`, `BR-09`, `BR-11`, `BR-16` to `BR-18`, `BR-20`, `BR-21`, `BR-25`, `BR-27` to `BR-30`, `BR-33`, `BR-38`, `BR-42`, `BR-44`, `BR-48` to `BR-50`, `BR-52` to `BR-57`, `BR-61`, `BR-CO-03`, `BR-CO-09` to `BR-CO-18`, `BR-CO-25`, `BR-CO-26` | | `VatCategoryRules` | L3 | per category (`BR-S`, `BR-Z`, `BR-E`, `BR-AE`, `BR-IC`, `BR-G`, `BR-O`): `-01` breakdown present, `-02` party identifiers, `-05` rate, `-08` taxable amount, `-09` tax amount, `-10` exemption reason; plus `BR-IC-11`, `BR-IC-12`, `BR-O-11` | | `CodeListRules` | L3 | `BR-CL-01`, `-04` to `-08`, `-10`, `-11`, `-13`, `-14`, `-16`, `-19` to `-23`, `-25`, `-26` (official code lists) | | `QuittioRules` | L3 | package rules, see below | | `PlausibilityRules` | L7 | see [plausibility](#plausibility-l7) | | profile rules | L3 | Peppol BIS profile: `PEPPOL-EN16931-R001`, `-R002`, `-R003`, `-R005` (warning), `-R010`, `-R020`, `-CL008`, `-R040`, `-R041`, `-R042`, `-R061`, `-R120`, `-R121`, `-R130`; XRechnung profiles (country pack DE): `BR-DE-…`, `BR-DEX-15`, `DE-XRECHNUNG-EXT-01` | | country pack rules | L3, L7 | `rules()` of the seller's pack and `buyerCountryRules()` of the buyer's pack; Germany: `DE-USTG-01` … `DE-USTG-15` (L3), `DE-LEITWEG-ID`, `DE-LEITWEG-ID-SUSPECT`, `DE-TAX-NUMBER` (L7), see [Country packs](country-packs.md) | All EN 16931, code list, VAT category and Peppol rules report errors unless noted. The PHP rules cover the most frequent Schematron findings with readable messages. They do not replace the official Schematron (L5). ### Package rules (`QUITTIO-…`) | Code | Severity | Meaning | |---|---|---| | `QUITTIO-NEGATIVE-TOTAL` | warning | Commercial invoice (380) with a negative amount due; consider a credit note | | `QUITTIO-SUBLINES` | error | Net amount of a line differs from the sum of its sub lines | | `QUITTIO-LINE-ID-DUPLICATE` | warning | A line identifier (BT-126) is used more than once (data not built with `InvoiceBuilder`, which rejects duplicates) | | `QUITTIO-SUBLINE-VAT` | error | Sub line with another VAT category or rate than its parent line | | `QUITTIO-PRECEDING-INVOICES-CII` | warning | More than one preceding invoice (BG-3): CII transmits only the first | | `QUITTIO-EXEMPTION-CONFLICT` | warning | Lines of one VAT group with different exemption reason codes: the code (BT-121) is omitted, the texts are joined | | `QUITTIO-DECLARED-AMOUNTS` | warning | Declared amounts of an existing document were outside the tolerance and recalculated | | `QUITTIO-THIRD-PARTY-PAYMENTS` | error | Third party payments (BG-DEX-09) outside an XRechnung UBL invoice of the extension | | `QUITTIO-CREDIT-NOTE-DUE-DATE` | warning | UBL credit note with a due date (BT-9) but without payment instructions (BG-16): the UBL binding has no place for the date there, it is left out | | `QUITTIO-VAT-ROUNDING` | warning | VAT amount differs from taxable amount × rate within the tolerance of BR-CO-17 | | `QUITTIO-CANCELLATION-VAT-ALIGNED` | info | VAT of a partial cancellation aligned with the earlier ones (cumulative rounding) | | `QUITTIO-LINE-ROUNDING` | warning | Declared line net amount differs from quantity × price ± allowances and charges | | `QUITTIO-ATTACHMENT-FILENAME` | error | File name of an attached document used twice | | `QUITTIO-ATTACHMENT-SIZE` | error | Attachments larger than `attachment_max_bytes` or `attachments_max_total_bytes` | | `QUITTIO-INVALID-CHARACTERS` | error | A text contains control characters (e.g. a vertical tab from a spreadsheet cell) or invalid UTF-8, which XML cannot represent; XML writers refuse such data | | `QUITTIO-GENERIC-COUNTRY` | warning | No country pack for the seller's country: only EN 16931 is checked | | `QUITTIO-PROFILE-NOT-EINVOICE` | warning | Profile that is no EN 16931 e-invoice (MINIMUM, BASIC WL) | | `QUITTIO-STANDARD-VERSION` | error | The profile does not support the EN 16931 version of the invoice | | `QUITTIO-PROFORMA` | error | Type code 325 in the invoice pipeline; use `Quittio::proforma()` or `DocumentBuilder::proforma()` ([documents](documents.md#proforma-invoices)) | ### Plausibility (L7) | Code | Severity | Check | |---|---|---| | `QUITTIO-IBAN` | error | Check digits (ISO 13616, mod 97) of BT-84 and BT-91, for values that start like an IBAN | | `QUITTIO-BIC` | warning | Format of BT-86 | | `QUITTIO-VATID-FORMAT` | warning | Format of BT-31, BT-48 and BT-63 by country prefix (no check digits); unknown prefixes are not judged | | `QUITTIO-EMAIL` | error / warning | Electronic addresses with scheme `EM` (BT-34, BT-49: error) and contact e-mails (BT-43, BT-58: warning) | | `QUITTIO-DUE-DATE` | warning | Due date (BT-9) before the issue date | | `QUITTIO-VAT-K-DOMESTIC` | warning | Category K (intra-community supply) with a destination in the seller's member state | | `QUITTIO-VAT-K-OUTSIDE-EU` | warning | Category K with a destination outside the EU VAT territory | | `QUITTIO-VAT-G-INSIDE-EU` | warning | Category G (export) with a destination inside the EU VAT territory | | `QUITTIO-VIES` | error | VIES reports the VAT identifier as invalid (only with a VAT identifier verifier) | | `QUITTIO-VIES-UNAVAILABLE` | info | VIES could not answer | The destination is the deliver-to country (BT-80), otherwise the buyer's country (BT-55). The seller's member state comes from the prefix of BT-31 or BT-63, otherwise from BT-40. Territories outside the EU VAT area (e.g. Heligoland, the Canary Islands) are recognised by postcode. The helpers are public: `Quittio\Core\Validation\Plausibility\Iban::isValid($iban)`, `Iban::normalize($iban)` and `VatIdFormat::isValid($vatId)` (`null` for an unknown prefix). ### VAT identifiers and VIES VIES is the EU service that confirms VAT identification numbers online. The online check is off by default. In Laravel, switch it on with `validation.drivers.vies.enabled = true` (`timeout` in seconds, default 5; `cache_ttl` in seconds, default 86400). A `Quittio\Core\Validation\Plausibility\VatIdVerifier` bound in the container replaces the VIES client, e.g. in tests. Without Laravel, give the PHP driver a verifier: ```php use Quittio\Core\Validation\Drivers\PhpRulesDriver; use Quittio\Core\Validation\Plausibility\ViesVerifier; $drivers = $quittio->validators(); $quittio->clearValidators(); foreach ($drivers as $driver) { $quittio->addValidator($driver instanceof PhpRulesDriver ? $driver->withVatIdVerifier(new ViesVerifier(timeout: 5, cacheTtl: 86400)) : $driver); } ``` `ViesVerifier` uses the VIES REST interface: - It checks only identifiers of EU member states (Greece `EL`) and Northern Ireland (`XI`) whose format is valid. Others are reported as not supported and skipped. - Only the answers "valid" and "invalid" are results. They are cached per verifier instance. - Errors and unreachable services give `valid = null`, are not cached and never block (`QUITTIO-VIES-UNAVAILABLE`). `verify($vatId)` returns a `VatIdVerification` with `vatId`, `valid`, `name`, `address`, `error`, `supported` and `toArray()`. Own verifiers implement `VatIdVerifier::verify(string $vatId): VatIdVerification` and must not throw when a service is unavailable. ## Own rules and drivers A driver implements `Quittio\Core\Validation\ValidationDriver` (`name()`, `levels()`, `supports(ValidationSubject $subject)`, `validate(ValidationSubject $subject): ValidationResult`). Its levels decide the stage: L6 runs on the PDF, L4 or L5 on the XML, everything else on the data. The subject holds: - `invoice`: the calculated `InvoiceData`, - `context`: the `ValidationContext` with `profile`, `countryPacks`, `locale`, `options`, `option()`, `intOption()`, `leadingCountry()`, `hasCountry()`, `profileIs()`, `translator()`, - `xml`, `syntax` and `pdf`. Rules are easiest written as `CallbackRule`: `CallbackRule::l3()`, `CallbackRule::l7()` or `new CallbackRule($code, $level, $fallback, $check, $namespace)`. The closure receives the invoice and a `RuleReporter` with `error()`, `warning()` and `info()` (arguments `$bt`, `$path`, `$replace`) and `context()`: ```php use Quittio\Core\Data\InvoiceData; use Quittio\Core\Validation\Rules\CallbackRule; use Quittio\Core\Validation\Rules\RuleReporter; use Quittio\Core\Validation\{ValidationDriver, ValidationLevel, ValidationResult, ValidationSubject}; final class AcmeRules implements ValidationDriver { public function name(): string { return 'acme'; } public function levels(): array { return [ValidationLevel::L3]; } public function supports(ValidationSubject $subject): bool { return true; } public function validate(ValidationSubject $subject): ValidationResult { $rule = CallbackRule::l3('ACME-PO', 'Invoices to :value need a purchase order number (BT-13).', function (InvoiceData $i, RuleReporter $report): void { if ($i->purchaseOrderReference === null) { $report->error('BT-13', 'purchaseOrderReference', ['value' => $i->buyer->name]); } }, namespace: 'acme'); return new ValidationResult([...$rule->check($subject->invoice, $subject->context)], $this->levels()); } } ``` Register it in Laravel and add its name to the levels. The factory receives the container and `validation.drivers.acme`: ```php Quittio::extend('validator', 'acme', fn ($app, array $config) => new AcmeRules); // config/quittio.php: 'levels' => ['php', 'xsd', 'acme'] ``` Without Laravel: `$quittio->addValidator(new AcmeRules)`. The message of `ACME-PO` is looked up as `acme::rules.ACME-PO.message`, otherwise the fallback text is used. Register the catalog in Laravel with `loadTranslationsFrom(…, 'acme')`, in the core with `Translations::translator()->addPath($dir, 'acme')`. Other ways to add rules: - Implement `Quittio\Core\Validation\Rule` (`code()`, `level()`, `check(InvoiceData, ValidationContext): iterable`) and build the messages with `Messages::make()`. - Give such rules to `new PhpRulesDriver(additionalRules: [...])`. - Return them from a country pack (`rules()`, `buyerCountryRules()`) or a profile (`rules()`). Country packs may only add or tighten rules. `Quittio\Core\Validation\InvoiceValidator` runs a list of drivers on a `ValidationSubject` outside a render run. ## Switching rules off and changing their severity Every PHP rule is addressed by the code of its messages, e.g. `DE-USTG-15` or `QUITTIO-PRECISION`. `validation.rules` switches rules off, changes the severity of their findings or, with an own implementation, replaces them. A trailing `*` matches by prefix: ```php // config/quittio.php 'validation' => [ 'rules' => [ 'disabled' => ['QUITTIO-GENERIC-COUNTRY', 'DE-USTG-1*'], 'severity' => ['DE-USTG-08' => 'warning', 'QUITTIO-*' => 'info'], 'allow_official' => false, ], ], ``` - `disabled` drops the rules and every finding with a matching code. Rules that report several codes are covered too, e.g. `QUITTIO-PROFILE-NOT-ALLOWED` of the field matrix. - `severity` sets `error`, `warning` or `info`. An exact code wins over a prefix, a longer prefix over a shorter one. - A code that no rule has throws a `ConfigurationException` when the core is built, so typing errors do not switch nothing off silently. `$quittio->ruleCodes()` lists the known codes. - The rules of EN 16931 (`BR-…`) and Peppol (`PEPPOL-…`) are official: receivers and the KoSIT validator reject invoices that break them. Changing them needs `'allow_official' => true`. - The overrides change the PHP rules only (L1, L2, L3, L7). The findings of the XML schema, KoSIT and veraPDF are never changed. A rule of your own replaces a rule with the same code: ```php use Quittio\Core\Validation\Rules\CallbackRule; use Quittio\Core\Validation\Rules\RuleReporter; use Quittio\Laravel\Facades\Quittio; Quittio::extend('rule', 'DE-USTG-15', fn ($app) => CallbackRule::l7('DE-USTG-15', 'Own check of the service date.', function ($invoice, RuleReporter $report): void { // … }, namespace: 'quittio-de')); ``` Without Laravel, `Quittio::create()` reads the same keys under `validation.rules`, and `configureRules()` sets them on an instance: ```php use Quittio\Core\Validation\RuleOverrides; use Quittio\Core\Validation\Severity; $quittio->configureRules(new RuleOverrides( disabled: ['QUITTIO-GENERIC-COUNTRY'], severities: ['DE-USTG-08' => Severity::Warning], replacements: ['DE-USTG-15' => $ownRule], )); ``` ## Incoming files To check e-invoices you receive, validate the file itself: ```php $report = Quittio::validateFile($content); // content (bytes) of an XML or PDF e-invoice, never a path $report = Quittio::validatePath($path); // the same from a local file ``` Both read the file ([Reading incoming invoices](reading.md)) and run every configured driver that supports it, independent of the stage and of `validation.mode`. - They never throw a `ValidationException`. The reader exceptions (`UnsafeXmlException`, `PdfException`, `UnreadableInvoiceException`) are thrown. - The result starts with the reader's findings (level L1, `QUITTIO-DECLARED-TOTALS` L3). - The rules use the profile detected from the specification identifier (BT-24), otherwise the configured default profile. They also use the country packs of the parties and the syntax of the file. - XSD and KoSIT check the XML; for a PDF, the embedded XML. veraPDF runs only when the input was a PDF. - The read data is checked as declared, without recalculation. On the command line: ```bash php artisan quittio:validate invoice.pdf --profile=en16931 --kosit --verapdf --json ``` `--profile` fails the command when the detected profile differs. `--kosit` and `--verapdf` add the drivers for this call with the configured `validation.drivers.*` settings (see [Artisan commands](commands.md)). ## Mandatory field guard The template decides what the PDF shows. After rendering a PDF or HTML invoice, the guard checks that the template printed every mandatory value: invoice number and date, names and addresses, quantities and units, VAT rows, amount due, legal notes and the values the country pack requires. It does not run for [documents](documents.md). | Mode | Behaviour | |---|---| | `exception` | throws `Quittio\Core\View\MandatoryFieldMissing` (`$e->fields`: list of `MandatoryField` with `accessKey`, `description`, `legalBasis`) | | `log` | logs a warning (PSR-3 logger) and continues | | `off` | no check | Set the mode with `validation.mandatory_field_guard` (Laravel; default `exception` outside production, `log` in production) or `mandatory_fields` (`Quittio::create()`, default `exception`). In every mode except `off`, a missing value calls the listener of `$quittio->onMandatoryFieldsMissing(fn (InvoiceData $invoice, array $fields) => …)`. Laravel dispatches the event `Quittio\Laravel\Events\MandatoryFieldMissing($invoice, $fields)`. Which values count as printed is described in [Templates](templates.md#mandatory-field-guard). ## Germany: BMF error classes The German Federal Ministry of Finance (BMF) groups e-invoice errors into classes in its letter of 15 October 2025. The German country pack sorts findings into these classes: ```php use Quittio\Country\DE\CountryDE; use Quittio\Country\DE\Rules\BmfErrorClass; $classes = (new CountryDE)->errorClassification($result); // ['format' => [...], 'business_rule' => [...], 'content' => [...]] BmfErrorClass::of($message)->label('de'); // "Formatfehler (Rz. 6a)", the German label of "format error (marginal no. 6a)" ``` Errors and warnings (no infos) are grouped by class: | Class | Value | Findings | `voidsEInvoice()` | |---|---|---|---| | Format error (marginal no. 6a) | `format` | L1, L2, L4, L6 | `true` | | Business rule error (marginal no. 6b) | `business_rule` | L3, L5 | `false` | | Content error (marginal no. 35a) | `content` | every `DE-USTG-…` rule, L7 | `false` | `label(string $locale = 'de')` returns the German name, any other locale the English name. Classes without findings are not in the array. ## JSON input Invoice data can also arrive as JSON, e.g. from an API. Check it against the shipped schema (`packages/core/resources/schema/invoice.schema.json`, `JsonSchemaGenerator::defaultPath()`) before hydrating it. This needs the optional package `opis/json-schema`: ```php use Quittio\Core\Serialization\JsonSchemaValidator; use Quittio\Core\Data\InvoiceData; if (JsonSchemaValidator::available()) { $errors = (new JsonSchemaValidator)->validate($json); // list "path: message", at most 20; [] = valid } $invoice = InvoiceData::fromJson($json); ``` Without the package, `validate()` throws a `RuntimeException`; invalid JSON throws a `JsonException`. The schema checks the structure only. The business rules run when the invoice is rendered or checked. # Templates and design This page explains how Quittio prints an invoice or a [document](documents.md) (quote, reminder …) as PDF or HTML, and how you change its look. Read it when you want your own logo, colours, texts or layout. The first sections need no template knowledge; the [view model](#view-model) reference at the end is for writing own templates. The design only affects the PDF and HTML, never the e-invoice XML ([What an e-invoice is](concepts.md#what-an-e-invoice-is)). ## Design levels You can change the design on four levels. Start with level 1 and go further only when it is not enough. | Level | Use it when you want to | What you do | |---|---|---| | 1 – Configuration | Set logo, colours, font, texts, footer, sender line, letter layout, paper or QR code | Set `pdf.*` in `config/quittio.php`, or pass a [`Presentation`](#presentation) per invoice or per call ([Logo, colours and texts](#logo-colours-and-texts-level-1)) | | 2 – Partials | Change one part of the default template, e.g. the payment block | `php artisan vendor:publish --tag=quittio-views` and change single partials ([Changing single partials](#changing-single-partials-level-2)) | | 3 – Own template | Use a layout of your own | `php artisan make:quittio-template my-design`, then `->template('invoices.my-design.layout')` or `pdf.template` ([Own templates](#own-templates-level-3)) | | 4 – Own renderer | Create the PDF with another engine, e.g. Browsershot or Gotenberg | `Quittio::extend('renderer', …)` ([PDF renderers](#pdf-renderers)); PDF/A and the embedded XML stay with the package | PDF/A is the PDF variant for long-term archiving. A ZUGFeRD/Factur-X invoice is a PDF/A-3 file with the XML embedded ([Formats](concepts.md#formats)). Templates only display data. Quittio prepares totals, VAT, the texts of the payment terms, QR codes and labels in the [view model](#view-model) before the template runs. The same data also produces the XML. After rendering, the [mandatory field guard](#mandatory-field-guard) checks that the template printed every value an invoice must show. ## Default templates Quittio ships one default design in two template engines. Both print the same document. | 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()` without Laravel (template `default`); Laravel falls back to it for names that are no Blade view | The PDF is created by mPDF, a PHP library that converts the HTML of the template into a PDF ([PDF renderers](#pdf-renderers)). To see the result with your configuration, render the sample invoices with `php artisan quittio:preview` or open the preview route ([HTML output and preview](#html-output-and-preview)). ### Page layout The default templates print A4 pages ([Paper size](#paper-size)) with a left margin of 20 mm and a right margin of 19 mm, in the font Geist ([Fonts](#fonts)). The issuer is the party that sends the document: the seller, or the buyer on a self-billed invoice (German "Gutschrift", see [German terms](concepts.md#german-terms)). - **First page:** the logo (at most 14 mm high) or the issuer's name in the primary colour at the top left, 15 mm from the top edge. Below it is the address field at the position of the [letter layout](#letter-layouts): the sender line at its top and the recipient directly below. The invoice details (number, dates, references …) are on the right at the same height. Then follow the title, the line table, the totals with the gross total (or the amount due) in the primary colour, and the payment block. - **Following pages:** a header with the logo or name on the left, and the title with number, the recipient with customer number and the invoice date on the right. The line table continues below the header with its column heads. - **Every page:** the footer with up to four columns (address; phone and e-mail; register and legal information; VAT identifier and tax number) and the page number. Which elements use which colour is listed in [Design](#design). ### Carry-over across pages When the line table continues on the next page, the default templates print a running total at the break: - The last row of the page reads "Continued on page 2", "Carried forward" and the sum of the line amounts so far. The German output is "Fortsetzung auf Seite 2" (continued on page 2) and "Übertrag" (carried forward). - The first row of the next page reads "Brought forward from page 1" (German "Übertrag von Seite 1") with the same amount. The mPDF renderer does this. It lays the document out once. Only if the line rows land on more than one page, it lays the document out again with space reserved for the carry rows, finds the page breaks, splits the table there and inserts the rows. Single-page documents are rendered once. Documents without prices (delivery notes) get no carry-over. Own templates can use the same mechanism ([Carry-over in own templates](#carry-over-in-own-templates)). ## Logo, colours and texts (level 1) Most designs need only configuration. Set the defaults for all invoices in `config/quittio.php` ([Configuration](configuration.md)): ```php // config/quittio.php 'pdf' => [ 'logo' => storage_path('app/logo.png'), // PNG, JPEG or SVG 'colors' => ['primary' => '#0f766e', 'text' => '#15171c', 'muted' => '#5f6470', 'secondary' => null], 'font' => 'geist', // or 'inter', or a registered font 'texts' => ['salutation' => null, 'intro' => 'Thank you for your order.', 'closing' => 'Kind regards'], 'sender_line' => null, // null: generated from the seller 'footer_columns' => [], // []: generated from the seller ], ``` To change the design of one invoice, call `->presentation()` on the builder. The design is stored with the invoice data, so the invoice looks the same when it is rendered again. To change it for one render call only, e.g. for a second brand, call `->presentation()` on `Quittio::make()`: ```php use Quittio\Core\Data\Presentation\Colors; use Quittio\Core\Data\Presentation\Presentation; use Quittio\Laravel\Facades\Quittio; $invoice = Quittio::invoice() // ... ->presentation(['introText' => 'Thank you for your order.', 'letterLayout' => 'din5008a']) // this invoice ->build(); $result = Quittio::make($invoice) ->presentation(new Presentation(logo: storage_path('app/brand-b.png'), colors: new Colors(primary: '#7c3aed'))) // this call ->render(); ``` The invoice's own presentation wins over the presentation of the call, which wins over the configuration. The rules are in [Presentation](#presentation). | You want to change | Setting | Details | |---|---|---| | Logo | `pdf.logo`, `Presentation::$logo` | [Logos](#logos) | | Colours | `pdf.colors`, `Presentation::$colors` | [Presentation](#presentation), [Design](#design) | | Font | `pdf.font`, `pdf.fonts` | [Fonts](#fonts) | | Salutation, intro and closing text | `pdf.texts.*` | [Texts and Markdown](#texts-and-markdown) | | Footer columns and sender line | `pdf.footer_columns`, `pdf.sender_line` | [Presentation](#presentation), [Design](#design) | | Position of the address field, fold marks | `pdf.layout`, `pdf.fold_marks` | [Letter layouts](#letter-layouts) | | Paper size | `pdf.paper` | [Paper size](#paper-size) | | Payment QR code | `pdf.payment_qr.*` | [Payment QR codes](#payment-qr-codes) | | Single printed texts, e.g. "Amount due" | `Presentation::$labels`, language files | [Translations and locales](#translations-and-locales) | | Title of the document | `Presentation::$title` | [Presentation](#presentation) | | Draft marker | option `watermark` | [Watermark](#watermark) | ### 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](#selecting-a-template)) | `pdf.template` | | `logo` | `null` | Logo ([Logos](#logos)) | `pdf.logo` | | `colors` | `null` | `Colors(primary: '#2741d9', text: '#15171c', muted: '#5f6470', secondary: null)` | `pdf.colors.primary`, `text`, `muted`, `secondary` | | `font` | `null` (`geist`) | Font family ([Fonts](#fonts)) | `pdf.font` | | `senderLine` | `null` (generated) | Sender line above the address | `pdf.sender_line` | | `letterLayout` | `null` (country default) | [Letter layout](#letter-layouts) | `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>`, one list of lines per column | `pdf.footer_columns` | | `lineGroups` | `[]` | `list` (`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](#payment-qr-codes): `auto`, `girocode`, `swiss-qr-bill`, `none` | `pdf.payment_qr.type` | | `watermark` | `null` | [Watermark](#watermark) text | – (option `watermark`) | | `paper` | `A4` | [Paper size](#paper-size) | `pdf.paper` | | `labels` | `[]` | Label overrides, e.g. `['amount_due' => 'Total to pay']` | – | | `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 | – | BT-155 and the other BT numbers name the fields of the European standard EN 16931 ([Business terms](concepts.md#en-16931-and-business-terms)). Hints for single lines are `LinePresentation` objects (`groupId`, `highlight`, `showGrossPrice`). The line builder sets them: - `LineBuilder::group($groupId)` puts the line into a line group. - `highlight()` shades the line. - `showGrossPrice(?bool $show = true)` prints the price including VAT of this line, or with `false` not, whatever `showGrossPrices` says. `null` follows the presentation. ```php use Quittio\Core\Builder\Line; use Quittio\Core\Data\Presentation\Colors; use Quittio\Core\Data\Presentation\Presentation; use Quittio\Laravel\Facades\Quittio; $invoice = Quittio::invoice() // ... ->presentation(['logo' => storage_path('app/logo.png'), 'introText' => 'Thank you for your order.']) ->group('hardware', 'Hardware') // line group with subtotal ->line(Line::make('Router', netPrice: '89.00', quantity: 2)->group('hardware')) // group(): Line::make() ->build(); Quittio::make($invoice) ->presentation(new Presentation(colors: new Colors(primary: '#0f766e'), labels: ['amount_due' => 'Total to pay'])) ->render(); ``` **How presentations are merged.** The design of a run is built property by property from three layers: the invoice presentation over the presentation of the call (`->presentation(...)`) over the configured design. - A value that equals the default of an empty `Presentation` (`false`, `[]`, `'auto'`, `'A4'`, `null`) counts as not set and keeps the value of the layer 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`. Example: `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. **Checks.** Values are checked when rendering; invalid values throw a `ConfigurationException`: - colours must be hex values (`#2741d9` or `#fff`); - fonts must be 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.** Some design settings are render options: per call with `->option()`, without Laravel also in the `options` key of `Quittio::create()`. They are `fold_marks` (bool, default `true`), `markdown` (bool), `paper` (string), `watermark` (string), `girocode_amount` (`due`, `discounted`) and `logo_cache` (directory). ### Logos Set the logo with `pdf.logo` or `Presentation::$logo`. The default templates print it at the top left of the first page and in the header of the following pages. - Logos are local files, data URIs or `http`/`https` URLs 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 a `ConfigurationException`, 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), with a 10 s timeout and at most 3 redirects, each checked again. Quittio downloads 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 Laravel `temp_dir`/`logos`, default `sys_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 extension `imagick` (SVG coder only, 300 dpi but at most twice the logo size). Without `imagick`, an SVG logo fails with a `ConfigurationException` in PDF/A documents. - SVG logos must be self-contained: no `href` or `url()` 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](#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::$logo` or issuer branding from end users unless your application validates them (e.g. an uploaded file stored by the application). ### Fonts Two fonts are bundled (SIL Open Font License): | Family | Files | Script | |---|---|---| | `geist` (default, `pdf.font = 'geist'`) | Regular, Bold, Italic, Bold Italic; `geistmedium` with Medium and SemiBold | Latin and Cyrillic | | `inter` | Regular, Bold, Italic, Bold Italic | Latin, Cyrillic and Greek | Set `pdf.font = 'inter'` for Greek names or texts: mPDF prints no characters the font does not have. The font name is used as CSS `font-family` in the templates and as default font of mPDF. Register more fonts in the `pdf` section of `config/quittio.php`: ```php '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 Geist and Inter to `resources/fonts/quittio/geist` and `resources/fonts/quittio/inter`. - The Laravel renderer searches `pdf.font_directories`, then `resources/fonts/quittio` and its subdirectories automatically. 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 fonts and those of mPDF, so a published and edited font replaces the bundled one. - Without Laravel, pass the fonts to the renderer: `new MpdfRenderer(tempDir: …, fonts: [...], fontDirectories: [...], defaultFont: 'corporate')`. ### Letter layouts A letter layout fixes the address field on the first page, so that the recipient appears in the window of a window envelope. It is a `Quittio\Core\View\LetterLayout` with these values in mm (defaults in parentheses): `name`, `addressTop` (45), `addressLeft` (20), `addressWidth` (85), `addressHeight` (45), `senderLineHeight` (5), `foldMarks` (`[]`), `punchMark` (`null`) and `addressZoneTop` (17.7). `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 layouts after DIN 5008, the German standard for business letters: | Layout | Address field from the top | Fold marks | Punch mark | |---|---|---|---| | `din5008a` (form A) | 27 mm | 87 and 192 mm | 148.5 mm | | `din5008b` (form B, default) | 45 mm | 105 and 210 mm | 148.5 mm | | `free` | 45 mm (layout default) | none | none | The default templates print the sender line at the top of the address field and the recipient directly below it, both inside the window of a window envelope. DIN 5008 divides the address field into zones; own templates can use them (`design.addressZoneTop`, `design.noticeZoneHeight`, see [Design](#design)). Fold and punch marks are printed at the left edge unless `pdf.fold_marks` (option `fold_marks`) is `false`. See [Country packs](country-packs.md#din-5008-letter-layouts). ### Paper size The paper is the first of: 1. 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`; 2. the option `paper` (`pdf.paper`); 3. 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, because PDF/A does not allow the transparency of watermarks. - They 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 `` (alpha 0.08) and, for browsers, as rotated CSS overlay (`design.watermarkCss`). - The labels `draft` and `preview` can be used as text: "DRAFT" and "PREVIEW", in German "ENTWURF" (draft) and "VORSCHAU" (preview). ### Payment QR codes The default templates print a QR code next to the bank details that banking apps can scan to fill in the transfer. The type is set with `Presentation::$paymentQr` (`pdf.payment_qr.type`): | `paymentQr` | Generator | |---|---| | `auto` (default) | A registered generator (`setPaymentQrGenerator()`, Laravel `pdf.payment_qr.generator`). Otherwise what the country pack names in `CountryDefaults::$paymentQr`: `none` prints no code; for other values 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, the European QR code for SEPA credit transfers | | `swiss-qr-bill` | The generator registered with `setPaymentQrGenerator()` (Laravel: `pdf.payment_qr.generator`). Quittio ships no generator for the Swiss QR bill; without one, the render run fails with a `ConfigurationException` | | `none` | No code | No code is printed on refunds (credit notes, negative invoices) and on self-billed invoices. The EPC GiroCode (`EpcGiroCodeGenerator`, standard EPC069-12 version 002) is created when: - the currency is EUR, - the payment means is a (SEPA) credit transfer (code 30 or 58) with a valid IBAN, - and the amount due is between 0.01 and 999,999,999.99. Its 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); in total at most 331 bytes. `pdf.payment_qr.girocode_amount` (option `girocode_amount`) is `due` (the amount due) or `discounted` (the amount after the first cash discount). With `discounted`, `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 ([Payment reminders](documents.md#payment-reminders)). `payment.qrCode` in the view model 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. The templates therefore print them 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 and italic. HTML is shown as text, and links keep only their text. Formatting changes the displayed text compared with the XML. For example, `20*30*40 cm` is shown as "20*30*40 cm" with "30" in italics and without the asterisks. Keep the option 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 (language) of a run is the first of: 1. `->locale()` of the call, 2. the invoice's locale (`InvoiceBuilder::locale()`), 3. `defaults.locale` (`locale` in `Quittio::create()`), 4. 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 as `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`. ## 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. The preview route `/_quittio/preview` 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`). It shows them as HTML or, with `?format=pdf`, as PDF, with a "PREVIEW" watermark, the configured design and validation in mode `warn` (errors are reported, not thrown). - The route exists in the local environment, or with `quittio.preview.enabled = true`; outside `local` the gate `viewQuittioPreview` must allow the request ([Preview route](laravel.md#preview-route)). Its path is `quittio.preview.path`, its middleware `quittio.preview.middleware`. It is not registered while routes are cached. - `?template=` accepts the views that rendering accepts ([Selecting a template](#selecting-a-template)): `default`, the configured `pdf.template`, the views under `quittio::templates.` and the views of `pdf.templates`, never partials; the view must exist. - `?locale=` accepts a locale code. - Other values return 404. More in [Preview route](laravel.md#preview-route). On the command line: ```bash 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. ## Changing single partials (level 2) The Blade layout is split into partials, one per part of the page. To change one part, publish the views and edit the copy of that partial: ```bash 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. You can delete the copies you did not change; for those, Laravel uses the shipped partials, which follow package updates. The layout takes the partial prefix from the variable `$partials` (default `quittio::templates.default.partials`). | Partial | Content | |---|---| | `styles` | CSS: page margins (first and following pages), font, colours, address field position, watermark for browsers | | `header` | `` of the following pages: logo or issuer name, title with number, recipient with customer number, invoice date; and an empty header for the first page | | `footer` | `` with the footer columns and "Page {PAGENO} of {nbpg}" | | `marks` | Fold and punch marks at the left edge of the first page (option `fold_marks`, on by default) | | `sender-line` | Sender line at the top of the address field | | `recipient` | Address lines of the recipient | | `letterhead` | Logo or name of the issuer at the top left; 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 issuer (only when the footer does not show them), VAT identifier of the recipient ("Your VAT ID"; on a self-billed invoice also the seller's tax number, "Your tax number"), tax representative, deliver-to address | | `title` | Title (`

`) | | `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; a VAT column only with several VAT rates; the rows for the [carry-over](#carry-over-across-pages) | | `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. When the amount due is the gross total, it is printed once, as gross total | | `documents` | Packages, open items, fees, interest, totals and balance of [documents](documents.md) | | `tax-notes` | Exemption reasons and regulatory notes (`legalNotes`) | | `payment` | Payment terms and direct debit notice; next to them the bank details (payee, IBAN; BIC and reference; the account holder only when it is not the issuer) or the payout account, and the payment QR code | | `notes` | Other notes | | `attachments` | Attached documents (identifier and description) | | `closing` | Closing text, acceptance field of quotes | Partials read the [view model](#view-model). Keep printing every value a partial prints today unless you print it elsewhere: the [mandatory field guard](#mandatory-field-guard) reports missing mandatory values. ## Own templates (level 3) For a layout of your own, create a copy of the default template under your own name: ```bash php artisan make:quittio-template my-design # resources/views/invoices/my-design (--force overwrites) ``` The name may contain lower case letters, digits, `-` and `_`. The command copies the default layout and partials and points the layout at `invoices.my-design.partials`. Use the template per call with `->template('invoices.my-design.layout')` (allowed by the default `pdf.templates` entry `invoices.*`) or for every invoice with `pdf.template`. A template must follow some rules of mPDF ([mPDF rules for templates](#mpdf-rules-for-templates)) and must print every mandatory value ([Mandatory field guard](#mandatory-field-guard)). ### Selecting a template The template of a run is the first of: 1. `->template('…')` of the call; 2. `Presentation::$template` of the invoice's own presentation; 3. the `template` of the presentation passed to `->presentation()` of the call; 4. the `template` of the configured presentation; 5. the configured template. In Laravel this is the name `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](managed.md)), so they are restricted: - **Blade engine:** it renders `default`, the configured `pdf.template`, the shipped views under `quittio::templates.` (not their partials) and the views listed in `pdf.templates` (default `['invoices.*']`; an entry ending in `*` allows every view below that prefix, e.g. `mytheme::*`). A view whose name contains `.partials.` is never rendered as a template. Another existing view fails with an `InvalidArgumentException`. Names that are no Blade view are passed to the plain PHP engine. - **Plain PHP engine:** it accepts names only, no paths: segments of letters, digits, `_` and `-` separated by `/` (segments after the first may also contain `.`), no `..`, no absolute paths or stream wrappers. It resolves `{name}.php` inside its template directories (`template_paths` of `Quittio::create()`, then the shipped templates). An unknown or invalid name fails with an `InvalidArgumentException`. ### What a template receives A Blade template receives `$invoice`, the [view model](#view-model). A plain PHP template receives `$invoice` and `$e`, an escape function that returns HTML-safe text. Print values escaped: with `{{ }}` in Blade, with `$e(…)` in plain PHP. Only these prepared HTML values may be printed unescaped (`{!! !!}`): `lines[].description.html`, `notes[].html`, `texts.intro`, `texts.closing` and `design.watermarkCss`. ### Carry-over in own templates Own templates get the [carry-over](#carry-over-across-pages) with these markers (other renderers and browsers ignore them): ```html …… {{-- every line row; sub lines and group rows without --}}
``` - `` and `` enclose the rows of the line table. - Every line row carries its running total in `data-carry-over` (`lines[].carryOver` of `lineGroups`). - The `carry-out` comment holds the row printed at the end of a page, the `carry-in` comment the row at the start of the next page. - `{page}` is the next page in the carry-out row and the previous page in the carry-in row. `{amount}` is the `carryOver` of the last line row before the break. - The table needs a `` and fixed column widths, because each page gets a table of its own. ### Page numbers, headers and footers The default templates put the footer columns and "Page {PAGENO} of {nbpg}" into an `` that every page uses (`@page { footer: html_quittioFooter; }`). mPDF replaces `{PAGENO}` and `{nbpg}` with the page number and the number of pages. Headers work the same way with ``: the first page uses an empty header (`@page :first { header: html_quittioFirstHeader; }`), the following pages the header with logo, title and recipient. Up to four footer columns get a fixed width (43.8 mm each, the last 39.6 mm); more columns share the width. The page margins come from the view model: `design.pageMarginTop` for the first page and `design.continuationMarginTop` for the following pages ([Design](#design)). ### mPDF rules for templates mPDF supports only part of CSS. Follow these rules: - Layout with tables, not flexbox or grid. - Inside table cells, mPDF ignores `margin` and `padding` of 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: fixed` is measured from the page margins. The address field must be a direct child of ``. - Repeat table headers on every page via ``. - Headers and footers via `` / ``, page numbers with `{PAGENO}` and `{nbpg}`. - Use `page-break-inside: avoid` for the totals block. - Images as PNG or JPEG; fonts must be embeddable. ### Other template engines Other engines (Twig …) implement `Quittio\Core\Templates\TemplateEngine::render(string $template, InvoiceViewModel $invoice, array $data = []): string`. Pass them to `Quittio::create(templates: …)` or `$quittio->setTemplateEngine(…)`. ## PDF renderers A renderer turns the HTML of the template into the PDF (design level 4). | Renderer | Use | |---|---| | `MpdfRenderer` (default) | HTML to PDF/A-3b with mPDF in pure PHP; embeds the XML and writes the XMP metadata (the document description PDF/A requires) 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 | ```php use Quittio\Core\Pdf\Renderers\CallbackRenderer; use Quittio\Core\Pdf\RenderRequest; use Quittio\Laravel\Facades\Quittio; 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->setRenderer(…)`. A renderer implements `PdfRenderer` with `render(RenderRequest $request): string` and `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 (a PHP library that reads existing PDFs). The result is PDF/A only if the source PDF allows it (embedded fonts, no encryption, no JavaScript), so check it with [veraPDF](validation.md#verapdf-l6), a validator for PDF/A. 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. ## View model Templates receive `$invoice` (`Quittio\Core\View\InvoiceViewModel`) and nothing else. It holds every value of the invoice, already calculated and formatted for printing. - Nodes are read-only `ViewNode` objects. Use object syntax (`$invoice->seller->name`) or array syntax, loop over lists, and use `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()` returns the title. The tables name the EN 16931 field of a value in parentheses, e.g. invoice number (BT-1) or seller address (BG-5); see [Business terms](concepts.md#en-16931-and-business-terms). ### 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 (see below) | | `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 output: "Leistungsdatum" (date of supply) and "Leistungszeitraum" (period of supply) | | `deliveryDate` | BT-72 when an invoicing period is also set (otherwise `servicePeriod` shows it) | | `currency` | Invoice currency (BT-5) | | `document` | [document](#document) | | `documentInfo` | `null` for invoices, [documentInfo](#documentinfo) for documents | | `seller`, `buyer` | [Parties](#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) | | `lines` | All top-level [lines](#lines) | | `allowances`, `charges` | [Document allowances and charges](#allowances-and-charges) | | `vatRows` | [VAT breakdown](#vat-rows) | | `totals` | [Totals](#totals); `null` when not calculated | | `payment` | [Payment](#payment); `null` without payment instructions | | `paymentTerms` | [Payment terms](#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](#design) | | `meta` | `locale`, `custom` (values of `->custom()`), `source` | The `title` is the first of: 1. `Presentation::$title`; 2. for self-billed type codes: the title `self_billed` of the country pack's correction policy, which also holds the titles of cancellations and corrections. Germany: "Gutschrift"; in English "Self-billed invoice (Gutschrift)"; 3. the label of the type code (BT-3). | Label | English text | Used for | |---|---|---| | `title_380` | Invoice | Type code 380 | | `title_381` | Credit note | Type code 381 and other credit note codes | | `title_384` | Corrected invoice | Type code 384 | | `title_389` | Self-billed invoice | Type code 389 and other self-billed codes | | `title_325` | Proforma invoice | Type code 325 | | `title_326` | Partial invoice | Type code 326 | | `title_386` | Prepayment invoice | Type code 386 | | `title_875`, `title_876`, `title_877` | Partial construction invoice, Partial final construction invoice, Final construction invoice | Type codes 875, 876, 877 | | `title_default` | Invoice | Every other type code | ### document | Key | Content | |---|---| | `typeCode` | Type code (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](concepts.md#profiles) 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`, `street`, `additional`, `line3`, `postcode`, `city`, `subdivision`, `countryCode` and `country` (name in the locale). `lines` is formatted by the country pack's address formatter and starts with the name. The country is added in upper case when it differs from the issuer's country. 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) | | `carryOver` | Sum of the line net amounts up to this line in display order, only in `lineGroups[].lines`: the [carry-over](#carry-over-across-pages) when a page ends after the line | | `vatRate` | Rate (BT-152); `null` without rate and for margin schemes and travel agents (special VAT schemes without separate VAT) | | `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 a group (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) are discounts and surcharges on the whole document. They 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: "VAT 19%" when it is the only VAT row (its base is the net total printed above), otherwise "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 (exemption reason codes `VATEX-EU-D`, `-F`, `-I`, `-J`) | | `isSpecialScheme` | Margin scheme or travel agents | | `exemptionReason`, `exemptionReasonCode` | BT-120, BT-121 | Reverse charge means that the buyer owes the VAT ([German terms](concepts.md#german-terms)). Margin schemes and travel agents are special VAT schemes (sections 25 and 25a of the German VAT Act, UStG) that state no separate VAT. ### Totals | Key | Content | |---|---| | `lineNetSum`, `allowanceSum`, `chargeSum`, `netTotal`, `vatTotal`, `grossTotal`, `prepaid`, `rounding` | Document totals | | `vatTotalInTaxCurrency` | VAT in accounting currency (BT-111) | | `taxCurrencyNote` | "VAT in USD: … (rate … of …)" when an exchange rate is set | | `thirdPartyPayments` | List of `description`, `amount` (XRechnung extension) | | `amountDue` | Amount due | | `amountDueLabel` | "Amount due"; "Amount to be refunded" on credit notes and negative invoices; "Amount paid out" on self-billed invoices | | `dueEqualsGross` | The amount due is the gross total: nothing prepaid, rounded or paid by third parties, no refund and no payout | When `dueEqualsGross` is `true`, the default templates print the amount once, with the label `gross_total` ("Total incl. VAT"; German "Gesamtbetrag", total amount). 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 amount due (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](#payment-qr-codes) 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 `paymentTerms` is `null` without payment terms (BT-20) and due date. Otherwise it has: - `lines`: ready texts. These are the terms text; "Please transfer the amount by …" when the buyer pays by credit transfer, otherwise "Please pay the amount by …", on self-billed invoices "We will transfer the amount to your account by …"; one text per cash discount, such as "2% discount until 10/10/2026: 2,692.76 €"; and the late fee terms. - `discounts`: list of `days`, `percent`, `until`, `amount`, `text`. - `dueDate`. On credit notes, quotes, order confirmations 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 the issuer's name, the title, the header of the following pages and the gross total or amount due in the primary colour; section titles and the mark of highlighted lines in the secondary colour; the lines below the column heads and above the gross total or amount due in the text colour; secondary texts and labels in the muted colour | | `font` | Font family | | `fontMedium` | Family for the medium and semibold weights (item names, column heads, title, headings): `geistmedium` for Geist (normal = medium, bold = semibold); `null` for other fonts, then the templates use bold | | `logo` | Prepared logo ([Logos](#logos)) or `null` | | `letterLayout` | Name of the [letter layout](#letter-layouts) | | `addressTop`, `addressLeft`, `addressWidth`, `addressHeight` | Address field in mm from the top left page corner | | `pageMarginTop` | Top page margin of the first page in the default templates (15 mm): the letterhead starts here | | `continuationMarginTop` | Top page margin of the following pages (40 mm), below their header | | `letterheadHeight` | Space above the address field: `addressTop` − `pageMarginTop` | | `logoMaxHeight` | Height of the logo: 14 mm, less when the letterhead is lower (the logo ends 2 mm above the address field), at least 6 mm | | `addressZoneTop` | Start of the recipient zone (DIN 5008 "Anschriftzone") within the address field (17.7 mm), for own templates; the default templates print the recipient directly below the sender line | | `senderLineHeight` | Height of the sender line (5 mm) | | `noticeZoneHeight` | `addressZoneTop` − `senderLineHeight`: the DIN 5008 zone for additions and postal notes ("Zusatz- und Vermerkzone") above the sender line, for own templates | | `foldMarks`, `punchMark` | Mark positions in mm from the top; empty when the option `fold_marks` is `false` (default `true`) | | `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 and e-mail; legal registration and the parts of the additional legal information (split at " · " and line breaks); "VAT ID …" and "Tax no. …" of the issuer. The bank details are in the payment block | | `footerHasAddress` | The generated footer starts with the issuer's address | | `footerHasTaxIds` | The generated footer shows the issuer's VAT identifier or tax number (they count as printed for the [mandatory field guard](#mandatory-field-guard)); otherwise the default templates print them with the invoice details | | `showItemNumbers`, `showGrossPrices` | Presentation switches | ### documentInfo Only for [documents](documents.md): | 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`. ## Mandatory field guard Some values must appear on every invoice by law. After rendering a PDF or HTML invoice, the guard checks that the template printed them all: - number and issue date; - the full name of seller and buyer (`seller.name` or the address lines, which start with it; the trading name `displayName` alone is not enough) and their address (all address lines, or street, postcode and city); - name, quantity and unit (`unit.label`) of every line (in `lineGroups` or `lines`). Section 14 (4) no. 5 UStG requires "Menge und Art" (quantity and kind) of the goods; - label, or base and rate, and the VAT amount of every VAT row; - the cash discount terms (`paymentTerms.discounts[].text` or `paymentTerms.lines`) and the amount due; - every legal note (exemption reasons and regulatory notes, e.g. "Gutschrift" or the retention notice of section 14b UStG) and 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); the service date (`servicePeriod`, `deliveryDate` or `delivery.date`, or the period of every line); on self-billed invoices the title and the note "Gutschrift"; the retention notice of property related services; the [reverse charge](concepts.md#german-terms) or exemption note per VAT row; the VAT identifiers of buyer and seller for reverse charge and intra-community supplies (tax-exempt supplies of goods to a business in another EU country; see [Country packs](country-packs.md#mandatory-pdf-output)). How the guard decides whether a value was printed: - 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 (a single value, not a node), also in a condition such as `@if ($invoice->buyer->vatId)`, 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, and 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 a mandatory value 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](validation.md#mandatory-field-guard). # Quotes, delivery notes and other documents Besides invoices, a business sends other documents: quotes, order confirmations, delivery notes, proforma invoices, payment reminders and account statements. This page shows how to create them with Quittio. It assumes you know the invoice builder from [Getting started](getting-started.md). Documents share parties, lines, calculation, design and templates with invoices, but they are no invoices. They never produce e-invoice XML ([What an e-invoice is](concepts.md#what-an-e-invoice-is)), are not validated and are rendered as PDF or HTML only. | Type | `DocumentType` | Builder | Lines | Prices and totals | Payment block | |---|---|---|---|---|---| | Quote | `Quote` (`quote`) | `DocumentBuilder::quote()` | required | yes | none (no bank details, QR code or amount due) | | Order confirmation | `OrderConfirmation` (`order_confirmation`) | `orderConfirmation()` | required | yes | none | | Delivery note | `DeliveryNote` (`delivery_note`) | `deliveryNote()` | required | no | none | | Proforma invoice | `Proforma` (`proforma`) | `proforma()` | required | yes | payment terms, bank details, QR code | | Payment reminder | `PaymentReminder` (`payment_reminder`) | `paymentReminder(int $level = 0)` | not allowed | open items, fees, interest | bank details, QR code over the total | | Account statement | `AccountStatement` (`account_statement`) | `accountStatement()` | not allowed | open items, balance | bank details | The printed title depends on the locale. In English it is the name in the first column; reminders from level 1 on are titled "Reminder no. 1", "Reminder no. 2" … ([Payment reminders](#payment-reminders)). German documents are titled "Angebot" (quote), "Auftragsbestätigung" (order confirmation), "Lieferschein" (delivery note), "Proforma-Rechnung" (proforma invoice), "Zahlungserinnerung" (payment reminder), "1. Mahnung" (first reminder) and "Kontoauszug" (account statement). ## Creating a document In Laravel, start with the facade: `Quittio::quote()`, `Quittio::orderConfirmation()`, `Quittio::deliveryNote()`, `Quittio::proforma()`, `Quittio::paymentReminder(int $level = 0)` or `Quittio::accountStatement()`. Like `Quittio::invoice()`, these methods start the builder with the seller, the first bank account and the small business setting of `config/quittio.php` ([Starting invoices](laravel.md#starting-invoices)). `->render()` at the end of the chain renders the PDF. ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $quote = Quittio::quote() ->number('AN-2026-0007') ->issueDate('2026-10-06') ->buyer(Party::make('Kunde AG')->address('Weg 1', '54321', 'Beispielstadt', 'DE')) ->validUntil('2026-10-31') ->line('Consulting', netPrice: '120.00', quantity: 10, unit: UnitCode::Hour) ->optionalLine('Training', netPrice: '800.00', unit: UnitCode::Day) // shown, not in the totals ->alternativeLine('1', 'Senior consulting', netPrice: '150.00', quantity: 10, unit: UnitCode::Hour) // alternative to line 1 ->build(); Quittio::document($quote)->locale('de')->render()->download(); ``` This downloads a German quote as `Angebot_AN-2026-0007.pdf` ([File names](#file-names)). Use `->build()` to get the document data, e.g. to store it, and `Quittio::document()` to render it with more options ([Rendering](#rendering)). Without Laravel, start with the static methods of `DocumentBuilder` (`DocumentBuilder::quote()` …) and set the seller yourself with `->seller()`. `DocumentBuilder::make(DocumentType $type)` creates a builder for any type; reminders created this way have level 0. `DocumentType` has three methods: - `showsPrices()`: `false` for delivery notes; - `hasLineTotals()`: `true` for quotes, order confirmations and proformas; - `labelKey()`: the label key of the title, `document_{value}`. ## Quotes and order confirmations A quote offers goods or services at a price; an order confirmation confirms an order. Quotes print the validity date (`validUntil()`). They mark optional lines with "[Optional]" and alternative lines with "[Alternative to pos. 1]", and they end with an acceptance field ("Order placed:" with a signature line), where the customer can sign the quote. Optional and alternative lines get their line amounts, but they are excluded from the totals, the VAT breakdown and `InvoiceBuilder::fromDocument()`. Document discounts apply to the remaining lines. Quotes and order confirmations are no request to pay: the [view model](templates.md#view-model) has no payment block, no amount due and no due date text. Order confirmations reference the quote with `reference('AN-2026-0007', '2026-10-06')` and state the delivery date with `deliveryDate()`. ## Delivery notes A delivery note accompanies a shipment. It prints quantities without prices or totals. Lines get the net price 0 and the VAT category O (not subject to VAT) unless they set a price or VAT themselves, so they need only name, quantity and unit. This applies to lines given by name, as builder, array, model (`ProvidesInvoiceLine`) or callable. Finished `InvoiceLine` objects are taken as they are. ```php use Brick\Math\BigDecimal; use Quittio\Core\Documents\Package; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $note = Quittio::deliveryNote() ->number('LS-2026-0012') ->buyer($buyer) ->reference('AB-2026-0003') // the order confirmation ->deliveryDate('2026-10-02') ->serialisedLine('Router', ['SN-1001', 'SN-1002'], quantity: 2, unit: UnitCode::Unit) ->line('Cable', quantity: 5, unit: UnitCode::Metre) ->package(new Package('P1', type: 'Box', weightKg: BigDecimal::of('2.5'), dimensions: '30 × 20 × 10 cm', trackingNumber: '00340434161094042557')) ->build(); ``` `Package` has `id`, `type`, `weightKg` (`BigDecimal`), `dimensions` and `trackingNumber`. The template lists the packages, and the serial numbers below each line. ## Proforma invoices A proforma invoice announces the amount of a future invoice, e.g. for advance payment or customs. It is not an invoice for tax purposes. Build it with `Quittio::proforma()` (Laravel) or `DocumentBuilder::proforma()`. It is rendered like an invoice, with prices, totals, payment terms, bank details and QR code. Its title is "Proforma invoice", and the legal notes contain "This proforma invoice is not an invoice for tax purposes." It never carries e-invoice XML. Do not render a proforma as an invoice. An invoice with the type code (BT-3) 325 (`InvoiceTypeCode::ProformaInvoice`) fails validation with the rule `QUITTIO-PROFORMA` ([Rules and validation](concepts.md#rules-and-validation)), because e-invoice XML with VAT amounts would be booked as an invoice. ## Payment reminders A payment reminder asks the buyer to pay invoices that are overdue. The level counts the reminders: - Level 0 (default) is a friendly payment reminder, titled "Payment reminder" (German "Zahlungserinnerung"). - Level 1 and higher are formal reminders, titled "Reminder no. :level": "Reminder no. 1", "Reminder no. 2" … The German titles are "1. Mahnung" (first reminder), "2. Mahnung" (second reminder) … A negative level throws an `InvalidArgumentException`. ```php use Quittio\Laravel\Facades\Quittio; $reminder = Quittio::paymentReminder(1) // level 1: first reminder ->number('M-2026-0003') ->buyer($buyer) ->openItem('RE-2026-0042', '2026-08-01', '1190.00', dueDate: '2026-08-15', paid: '190.00') ->fee('Reminder costs', '2.50') ->interest('1000.00', '10.27', from: '2026-08-16', until: '2026-10-04') // until is exclusive: the last day with interest is 3 October ->deadline('2026-10-18') ->build(); $reminder->dunning->total('EUR'); // open items + fees + interest, no VAT $reminder->dunning->remittanceText(); // "RE-2026-0042" – open invoice numbers for the transfer ``` The template lists the open items (invoice number, date, due date, amount, paid, open), the fees, the interest lines and "Total to pay", then the deadline text ("Please pay the total amount by …"). The payment block uses the open invoice numbers as remittance information (the document number if none is open). The GiroCode, the QR code for the bank transfer ([Payment QR codes](templates.md#payment-qr-codes)), carries the total of the reminder. Rules: - Reminders have no lines, allowances or charges; the builder rejects them. The amount to pay is `Dunning::total()`: open items plus fees plus interest. - Fees and default interest carry no VAT. They are damages, not payment for a supply (section 1.3 (6) of the German VAT application decree, UStAE). The same applies to the flat rate for late payment of section 288 (5) of the German Civil Code (BGB). - Only actual costs may be claimed as reminder fees; Quittio has no default fee. The documents layer does not track which claims already carried a flat rate. - `interest()` takes the first day with interest (`from`, e.g. the day after the due date) and the day after the last day with interest (`until`, exclusive). `InterestCharge::lastDay()` is the last day with interest. The default description uses it: "Default interest 10.27% p.a. on 1,000.00 € from … to …". - Interest is simple interest, counted actual/actual: the days of each calendar year are divided by 365 or 366, so that a full year yields exactly the annual rate. Pass `dayCount: 365` or `dayCount: 360` for a fixed basis. Other values, and a period that ends before it starts, throw an `InvalidArgumentException`. The amount is rounded half up once. Classes of a reminder: - `Dunning` (`$document->dunning`): `level`, `openItems`, `deadline`, `fees` (`DunningFee`: `description`, `amount`), `interest`, `openTotal($currency)`, `feeTotal($currency)`, `interestTotal($currency)`, `total($currency)`, `openInvoiceNumbers()` (unsettled items, without duplicates) and `remittanceText(int $maxLength = 140)` (as many complete numbers as fit, comma separated). - `InterestCharge`: `base`, `ratePercent`, `from`, `until`, `description`, `amount` (a stated amount replaces the calculation), `dayCount`, `days()`, `lastDay()`, `total()`. - `OpenItem`: `invoiceNumber`, `invoiceDate`, `amount`, `dueDate`, `paid`, `description`, `open()` (amount minus paid; negative for overpaid items and credits) and `isSettled()` (nothing open). ### German default interest In Germany, a debtor in default owes interest at the statutory rate of section 288 BGB: the base rate of the Bundesbank plus 9 percentage points (businesses) or 5 points (consumers). The German country pack calculates it, with periods split at every change of the base rate. Add its charges to the reminder: ```php use Brick\Money\Money; use Quittio\Core\Support\Date; use Quittio\Country\DE\Dunning\DefaultInterest; use Quittio\Laravel\Facades\Quittio; $reminder = Quittio::paymentReminder(1) ->number('M-2026-0003') ->buyer($buyer) ->openItem('RE-2026-0042', '2026-06-01', '1000.00', dueDate: '2026-06-15'); // one charge per period with the same base rate; the end date is exclusive $reminder->interestCharge(...(new DefaultInterest)->calculate(Money::of('1000.00', 'EUR'), Date::parse('2026-06-16'), Date::parse('2026-10-05'), consumer: false)); ``` `interestCharge()` takes the charges with their day count (365 in Germany instead of actual/actual) and texts. For any country pack with a reference rate, `Quittio\Core\Dunning\DefaultInterest::forCountry($pack)` calculates the same with the pack's `referenceRates()` and `dunningLaw()`; the German class adds the German texts. The descriptions are German by default; any other locale, e.g. `calculate(…, locale: 'en')`, returns English ones. The base rate table, the flat rate and the conditions of default are described in [Country packs](country-packs.md#dunning-law). The country-neutral contract is `CountryPack::dunningLaw()` (`DunningLaw`: `b2bInterestPoints`, `b2cInterestPoints`, `b2bFlatRate`, `dayCount`, `consumerDefaultAfterDays`, `referenceRateName`). ## Account statements An account statement lists the invoices of a customer and the resulting balance. ```php use Quittio\Laravel\Facades\Quittio; $statement = Quittio::accountStatement() ->number('KA-2026-09') ->buyer($buyer) ->statementPeriod('2026-09-01', '2026-09-30') ->openingBalance('100.00') ->openItem('RE-2026-0050', '2026-09-10', '595.00', dueDate: '2026-09-24', paid: '595.00') ->openItem('RE-2026-0061', '2026-09-28', '238.00', dueDate: '2026-10-12') ->build(); $statement->statement->balance('EUR'); // 338.00 ``` The balance is the opening balance plus the open amounts (amount minus paid) of the listed items. The opening balance is the balance at the start of the period and already contains every invoice dated before it. Therefore: - with `openingBalance()`, only items dated within `statementPeriod()` are accepted; - without an opening balance, the statement is an open item list and may list older items; - items dated after the end of the period are always rejected; - without `statementPeriod()`, the period ends on the document date. Violations throw an `InvalidArgumentException`, as does a period that ends before it starts. `AccountStatement` (`$document->statement`) has `period`, `items`, `openingBalance`, `openItems()` (unsettled items) and `balance($currency)`. The template lists all items, the period, the opening balance and the balance; the payment block shows the bank details without QR code. ## From document to invoice When the customer accepts a quote or the goods are delivered, turn the document into an invoice: ```php use Quittio\Core\Builder\InvoiceBuilder; $invoice = InvoiceBuilder::fromDocument($quote) ->number('RE-2026-0100') ->issueDate('2026-11-16') ->deliveryDate('2026-11-15') ->build(); ``` `fromDocument()` starts an invoice from a quote, order confirmation, delivery note or proforma. - It takes over the parties, the billable lines (amounts are recalculated), document allowances and charges, payment terms and instructions, delivery, buyer reference, purchase order reference, presentation and locale. - Number and notes are not taken over. Set the number, and the issue date (otherwise today). - The number of an order confirmation becomes the sales order reference (BT-14), the number of a delivery note the despatch advice reference (BT-16). Quotes and proformas are not referenced. - Lines of a delivery note have no prices (net price 0, category O) and need prices and VAT before invoicing. The result is a normal `InvoiceBuilder`; validation and e-invoice formats apply when it is rendered. ## Rendering `Quittio::document()` renders a document with more options than `->render()` at the end of the builder chain: ```php use Quittio\Core\Data\Presentation\Presentation; use Quittio\Laravel\Facades\Quittio; $result = Quittio::document($quote) // DocumentData; in Laravel also a DocumentBuilder ->locale('en') ->country('DE') ->template('invoices.my-design.layout') ->presentation(new Presentation(title: 'Offer')) ->render(); $html = Quittio::document($quote)->htmlOnly()->render()->html(); ``` | `PendingDocument` method | Effect | |---|---| | `template(string $template)` | Template ([selection rules](templates.md#selecting-a-template)) | | `locale(string $locale)` | Locale; otherwise the document's locale, the configured locale, the country default | | `country(string $country)` | Country pack | | `presentation(Presentation $presentation)` | Design defaults of this call; the document's own presentation wins | | `htmlOnly()` | HTML instead of PDF | | `option(string $key, mixed $value)` | Output option of this call, e.g. `option('fold_marks', false)` ([output configuration](formats.md#output-configuration)) | | `render()` | `RenderedDocument` | | `queue(?string $connection = null, ?string $queue = null)` | Laravel: renders in a queued job, see below | Documents use the invoice templates and the same view model. The node `documentInfo` holds the document specific values ([view model](templates.md#documentinfo)), `document.kind` the type. The PDF is PDF/A-3b (the archivable PDF variant) when `pdf.pdfa` is set and no watermark is used, and it never contains embedded XML. Documents run no validation and no [mandatory field guard](templates.md#mandatory-field-guard). In Laravel they dispatch `DocumentRendering` and `DocumentRendered` ([Events](laravel.md#events)). `RenderedDocument` (core) has: - `data()`, `html()`, `hasPdf()`, `pdf()` (`LogicException` after `htmlOnly()`), `contents()` (PDF, otherwise HTML) and `mimeType()`; - `filename(?string $extension = null)`; - `save(string $path)`: a file, or a directory (an existing one or a path ending in `/`) for the generated file name; missing directories are created. The Laravel class adds `core()`, `download(?string $filename = null)`, `stream(?string $filename = null)` and `save(?string $disk = null, ?string $path = null)` (default disk `quittio.storage.disk`; a path ending in `/` gets the file name appended). It is `Responsable`, so a controller can return it as a download, and `Attachable` for mails ([Laravel integration](laravel.md)). ### Queueing In Laravel, `queue()->save()` renders the document in a queued job (`Quittio\Laravel\Jobs\RenderDocument`) and stores it on a disk: ```php use Quittio\Laravel\Facades\Quittio; Quittio::document($quote)->locale('de')->queue()->save('documents', 'quotes/'); ``` - The job carries the document as versioned JSON snapshot (`DocumentData::toSnapshot()`, `SNAPSHOT_VERSION`) and the render options as versioned JSON, never as serialised PHP objects. Jobs queued before an update are brought up to date by the classes in `quittio.snapshots.document_upcasters`. - Connection, queue and the dispatch after a database commit follow `quittio.queue.*`, as for invoices. - After storing, the job dispatches `QueuedDocumentStored` (result, disk and path); a job that failed for good dispatches `QueuedDocumentFailed`. Errors that another attempt cannot fix (invalid data, configuration) fail the job at once. `DocumentData::toArray()`, `fromArray()`, `toJson()`, `fromJson()`, `toSnapshot()` and `fromSnapshot()` store and restore documents outside the queue as well. ### File names The file name follows `pdf.filename` (default `{title}_{number}.{ext}`; `{format}` is the document type, `{date}` the document date). Characters other than letters, digits, `.`, `_` and `-` become `_`. Examples: | Document | English file name | German file name | |---|---|---| | Quote `AN-2026-0007` | `Quote_AN-2026-0007.pdf` | `Angebot_AN-2026-0007.pdf` ("Angebot": quote) | | Reminder level 1 `M-2026-0003` | `Reminder_no._1_M-2026-0003.pdf` | `1._Mahnung_M-2026-0003.pdf` ("1. Mahnung": first reminder) | ## Builder methods Parties, lines, notes, discounts and payment data work exactly like in the [invoice builder](building-invoices.md): - lines by name with named values (`->line('Consulting', netPrice: '120.00', quantity: 10, unit: UnitCode::Hour)`), as `Line::make()`, arrays, models or callables; - parties as `Party::make()`, models or callables; - lines without `vat()` get the [default VAT](building-invoices.md#default-vat). Dates accept `Date`, `DateTimeInterface` or a string; amounts are strings. | Method | Effect | |---|---| | `number(string $number)` | Document number | | `issueDate($date)` | Document date; default today (in PHP's default time zone, or the one set with `timezone()` through `tap()`) | | `currency(string $currency)` | Currency; default `EUR` | | `seller(SellerParty\|PartyBuilder\|ProvidesParty\|callable $seller)`, `buyer(BuyerParty\|PartyBuilder\|ProvidesParty\|callable $buyer)` | Parties ([Seller and buyer](building-invoices.md#seller-and-buyer)) | | `line(string\|InvoiceLine\|LineBuilder\|ProvidesInvoiceLine\|array\|callable $line, $netPrice = null, $quantity = null, $unit = null, $vat = null, ?string $description = null, $priceInclVat = null, ?DocumentLineOptions $options = null)` | Line ([Lines](building-invoices.md#lines)); the values are only allowed with a name | | `defaultVat(TaxPreset\|VatCategory $presetOrCategory, …)` | VAT of lines without `vat()` ([Default VAT](building-invoices.md#default-vat)) | | `lines(iterable $lines)` | Several lines | | `optionalLine(string\|InvoiceLine\|LineBuilder\|array\|callable $line, …)` | Optional quote line: shown with its amounts, not in the totals. Takes the same named values as `line()` | | `alternativeLine(string $alternativeTo, string\|InvoiceLine\|LineBuilder\|array\|callable $line, …)` | Alternative to the line with the identifier `$alternativeTo` (BT-126; lines are numbered `1`, `2`, … unless set with `LineBuilder::id()`); shown, not in the totals | | `serialisedLine(string\|InvoiceLine\|LineBuilder\|array\|callable $line, array $serialNumbers, …)` | Delivery note line with serial numbers | | `note(string $content, NoteSubjectCode\|RawCode\|string\|null $subject = null)` | Note (BT-22) with its subject code (BT-21) | | `discountPercent(BigDecimal\|int\|string $percent, ?string $reason = null, $reasonCode = AllowanceReasonCode::Discount)`, `discountAmount(BigDecimal\|int\|string $amount, ?string $reason = null, $reasonCode = AllowanceReasonCode::Discount)` | Document discount, on the lines that count towards the totals | | `dueInDays(int $days)`, `cashDiscount(int $days, string $percent, ?string $baseAmount = null)` | Payment target and cash discount ([Payment terms](building-invoices.md#payment-terms)) | | `paymentTerms(PaymentTerms\|string\|callable $terms)` | Payment terms as text or object | | `creditTransfer(string $iban, ?string $bic = null, ?string $accountName = null)` | Bank account | | `buyerReference(?string $reference)`, `purchaseOrderReference(?string $reference)` | Buyer's references | | `reference(string $number, $date = null)` | Preceding document, e.g. the quote of an order confirmation or the order of a delivery note | | `validUntil($date)` | Validity of a quote | | `deliveryDate($date)` | Delivery date | | `deliverTo(?string $recipientName, ?Address $address = null)` | Deliver-to party | | `package(Package $package)` | Package of a delivery note | | `openItem(string $invoiceNumber, $invoiceDate, string $amount, $dueDate = null, ?string $paid = null, ?string $description = null)` | Invoice of a reminder or statement | | `fee(string $description, BigDecimal\|int\|string $amount)` | Reminder fee | | `interest(string $base, BigDecimal\|string $ratePercent, $from, $until, ?string $description = null, ?int $dayCount = InterestCharge::ACTUAL_ACTUAL)` | Default interest of a reminder | | `interestCharge(InterestCharge ...$charges)` | Calculated interest charges, e.g. of `DefaultInterest::calculate()`, with their day count and texts | | `deadline($date)` | Payment deadline of a reminder | | `statementPeriod($start, $end)` | Period of an account statement | | `openingBalance(BigDecimal\|int\|string $amount)` | Opening balance of an account statement | | `presentation(Presentation\|array\|callable $presentation)` | Design of this document ([Templates](templates.md#presentation)) | | `locale(string $locale)` | Locale | | `custom(string $key, mixed $value)` | Custom value (`meta.custom` in the view model) | | `tap(callable $callback)` | Access to the underlying `InvoiceBuilder` | | `build(): DocumentData` | Builds the immutable document | `DocumentBuilder` is `Macroable`: add own methods with `DocumentBuilder::macro('name', fn () => …)`. `tap()` reaches every invoice builder method, but the document keeps only number, date, currency, parties, lines, allowances, charges, notes, buyer reference, purchase order reference, delivery, payment terms, payment instructions, presentation and meta. Other values set through `tap()` are dropped. `build()` throws an `InvalidArgumentException` when a quote, order confirmation, proforma or delivery note has no line, or when a reminder or account statement has lines, allowances or charges. ## DocumentData `build()` returns `Quittio\Core\Documents\DocumentData` (immutable; change values with `with()`). Its properties: `type`, `number`, `issueDate`, `currency`, `seller`, `buyer`, `lines`, `lineOptions` (`DocumentLineOptions` keyed by line identifier), `allowances`, `charges`, `notes`, `validUntil`, `reference`, `referenceDate`, `buyerReference`, `purchaseOrderReference`, `delivery`, `packages`, `dunning`, `statement`, `paymentTerms`, `paymentInstructions`, `totals`, `vatBreakdown`, `presentation` and `meta`. | Method | Returns | |---|---| | `options(string $lineId)` | `DocumentLineOptions` of a line (`optional`, `alternativeTo`, `serialNumbers`, `isBillable()`) | | `billableLines()` | Lines that count towards the totals (without optional and alternative lines) | | `isCalculated()` | Totals are set (or there is nothing to calculate) | | `toInvoiceDraft(bool $billableOnly = true)` | The document as `InvoiceData` for calculation and rendering; never use it as an invoice | Documents are calculated by `DocumentCalculator` with the invoice calculator when they are rendered. Every line gets its amounts; the totals and the VAT breakdown come from the billable lines. # Reading incoming invoices This page is about e-invoices you receive, for example from suppliers. Quittio reads XML files (CII and UBL, including XRechnung and Peppol) and PDFs with embedded XML (ZUGFeRD, Factur-X). It returns their content in the same data model that you use to build invoices, so you can take the values into your accounting. If these formats are new to you, see [Formats](concepts.md#formats), [Syntaxes](concepts.md#syntaxes-cii-and-ubl) and [Profiles](concepts.md#profiles) in the E-invoicing basics; field numbers such as BT-24 are explained in [EN 16931 and business terms](concepts.md#en-16931-and-business-terms). ## Reading a file ```php use Quittio\Laravel\Facades\Quittio; $read = Quittio::read($request->file('invoice')->get()); // content of an uploaded XML or PDF $read = Quittio::readFile('/path/to/invoice.pdf'); // or a local file $invoice = $read->data; // InvoiceData as stated in the document $invoice->number; // e.g. "RE-2026-0101" $invoice->seller->name; // the supplier $invoice->totals?->amountDue; // amount due as stated in the document $read->profile; // detected profile key, e.g. "en16931" or "xrechnung" $read->findings; // problems found while reading, e.g. QUITTIO-DECLARED-TOTALS $read->declaredTotals; // totals as stated in the document $read->embeddedFilename; // for PDFs: factur-x.xml, xrechnung.xml, zugferd-invoice.xml … ``` You get back a `ReadResult` ([The result](#the-result)): - `data` is an `InvoiceData` with the values as stated in the document. Amounts are never corrected. Totals that do not match the lines are reported as finding `QUITTIO-DECLARED-TOTALS` ([Declared amounts](#declared-amounts)). - `findings` lists problems found while reading, e.g. a missing issue date ([Findings](#findings)). - `profile` is the profile detected from the document, e.g. `xrechnung`. `read()` always treats its argument as the content of the document, never as a file name. You can pass request bodies (`$request->getContent()`) and uploads directly. `readFile()` reads a local file. It rejects paths with null bytes, missing or unreadable files, and files larger than the larger of the XML and PDF limits (`UnreadableInvoiceException`). Without Laravel, both methods exist on `Quittio\Core\Quittio` and use the limits of its configuration ([Limits and security](#limits-and-security)). A file that is no supported e-invoice throws an exception ([Errors](#errors)). ## Checking an incoming invoice Reading never validates the whole document. Before you book an invoice, check `$read->findings`, or validate the file: ```php use Quittio\Laravel\Facades\Quittio; $report = Quittio::validateFile($content); // content of an XML or PDF $report = Quittio::validatePath($path); // or a local file if (! $report->isValid()) { // reject the invoice, e.g. show $report->errors() } ``` `validateFile()` and `validatePath()` read the file and run all configured validation levels on it ([Incoming files](validation.md#incoming-files)). A complete controller that validates an upload and extracts the values for accounting is in [Incoming e-invoices](examples.md#incoming-e-invoices). ## On the command line ```bash php artisan quittio:read invoice.pdf # number, date, type, syntax and profile, parties, lines, totals, findings php artisan quittio:read invoice.xml --json # InvoiceData as JSON php artisan quittio:validate invoice.pdf # validation report ``` See [quittio:read](commands.md#quittioread) and [quittio:validate](commands.md#quittiovalidate). ## Supported documents | Input | Recognised by | Syntax | Content | |---|---|---|---| | CII XML | root `CrossIndustryInvoice` in `urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100` | `cii` | ZUGFeRD 2.x / Factur-X MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED; XRechnung CII including the extension | | UBL XML | root `Invoice` or `CreditNote` in the UBL 2.1 namespaces | `ubl` | EN 16931, XRechnung UBL including the extension, Peppol BIS Billing 3.0 | | PDF | content starts with `%PDF` (leading whitespace allowed) | of the embedded XML | ZUGFeRD 2.x / Factur-X, XRechnung embedded in a PDF | Everything else fails: - ZUGFeRD 1.0 (`urn:ferd:CrossIndustryDocument:invoice:1p0`) does not conform to EN 16931 and is rejected with a clear message. - Other root elements fail with "Unknown invoice format". - PDFs without embedded invoice fail with "The PDF does not contain an embedded e-invoice". The profile is detected from the specification identifier (BT-24). In CII it is `GuidelineSpecifiedDocumentContextParameter`, in UBL `cbc:CustomizationID`. Quittio compares it exactly with the identifiers of the installed profiles, for EN 16931:2017 and EN 16931:2026. Country pack profiles such as XRechnung count once the pack is registered. - An unknown identifier leaves `profile` `null` and is reported as information (`QUITTIO-READER-UNKNOWN-PROFILE`). - An identifier starting with `urn:cen.eu:en16931:2026` sets the standard version of the data to EN 16931:2026. ## Errors The reader throws an exception only when it cannot read the document at all: | Exception | When | |---|---| | `Quittio\Core\Xml\UnsafeXmlException` | The XML is too large, not well-formed, has a DOCTYPE, an unsupported encoding or exceeds a tag limit | | `Quittio\Core\Pdf\PdfException` | The PDF is too large, encrypted, malformed, exceeds a structure or decompression limit or uses an unsupported stream filter | | `Quittio\Core\Reading\UnreadableInvoiceException` | Unknown format, ZUGFeRD 1.0, a PDF without embedded invoice, a file that does not exist, is not readable or is too large | All three implement `Quittio\Core\Exceptions\QuittioException`. `validateFile()` and `validatePath()` throw the same exceptions. Everything the reader can read becomes a finding instead. ## The result `Quittio\Core\Reading\ReadResult` (readonly): | Property | Content | |---|---| | `data` | `InvoiceData` as stated: lines with their declared net amounts, the declared totals and VAT breakdown, all other business terms | | `syntax` | `cii` or `ubl` | | `specificationId` | BT-24 as written, or `null` | | `profile` | key of the detected profile (`en16931`, `extended`, `xrechnung`, `peppol` …) or `null` | | `declaredTotals` | `Totals` as stated (BT-106 to BT-115), `null` if the document has no monetary summation | | `declaredVatBreakdown` | `list` as stated (BG-23) | | `findings` | `list` (see [Findings](#findings)) | | `xml` | the XML that was read (for a PDF: the embedded XML) | | `raw` | the input: the PDF bytes, or the XML | | `embeddedFilename` | name of the embedded file for PDFs, otherwise `null` | `hasFindings()` tells whether there are findings. Extended data is read into the same data model as when building invoices ([field reference](field-reference.md)): Factur-X EXTENDED, sub lines from `ParentLineID` or `cac:SubInvoiceLine`, and XRechnung third party payments. ### Declared amounts Totals are never corrected silently. The reader keeps the amounts as stated and recalculates a copy from the lines. Every difference is reported as `QUITTIO-DECLARED-TOTALS` (warning, level L3). It compares: - the net amount of every line (BT-131), - the sum of line net amounts (BT-106), the total without VAT (BT-109), the VAT total (BT-110), the total with VAT (BT-112) and the amount due (BT-115). Documents without lines (Factur-X MINIMUM, BASIC WL) are not recalculated. MINIMUM does not transmit BT-106; it is derived from BT-109, BT-107 and BT-108 (`QUITTIO-READER-DERIVED`). EN 16931 carries the VAT exemption reason (BT-120, BT-121) only on the VAT breakdown. The reader copies it back to the lines, allowances and charges of the VAT group, so a recalculation keeps it. A read invoice can be the start of a builder (`InvoiceBuilder::from($read->data)`) or be cancelled ([Building invoices](building-invoices.md)). To keep its declared amounts there, the reader stamps a fingerprint of the amount inputs into the meta data (`$read->data->meta->amountsFingerprint()`). As long as lines, prices, allowances, charges, VAT breakdown and currency are unchanged, `InvoiceBuilder::from($read->data)` and cancellations of the read invoice keep: - declared line net amounts within 0.02 of quantity × price ± allowances and charges, - declared VAT amounts less than one currency unit from the recalculation, e.g. when another system rounded per line. Data changed before (e.g. with `with()`) no longer matches the fingerprint and is recalculated completely. `Quittio::make($read->data)` recalculates everything. The price base quantity (BT-149, BT-150) is read from the net price, or from the gross price if only the gross price states it. If both state different base quantities (CII), the gross price and discount are converted to the base quantity of the net price. With different units they are kept as stated, and a warning is reported. ## Findings The reader is tolerant: it reads as much of the document as possible and reports problems as findings instead of failing. Codes starting with `BR-` are the EN 16931 rules that the problem breaks ([Rules and validation](concepts.md#rules-and-validation)). Findings have level L1 unless noted. Errors mean that values are missing or could not be read as stated: | Code | Severity | Meaning | |---|---|---| | `BR-03` | error | The issue date (BT-2) is missing or unreadable. The placeholder `1970-01-01` is used, never the current date. | | `BR-05`, `BR-CL-04` | error | The invoice currency (BT-5) is missing or not an ISO 4217 code. The amounts are read in the currency of the amounts, otherwise in `XXX` (no currency). | | `QUITTIO-READER-CURRENCY` | error / warning | Amounts use another currency than BT-5 and are read as BT-5 (error); only the VAT total in accounting currency (BT-111) may differ. In CII a BT-110 without `currencyID` is read in the invoice currency (warning). | | `QUITTIO-READER-EXCHANGE-RATE` | warning | The exchange rate (CII) converts other currencies than BT-5 and BT-6. | | `QUITTIO-READER-DATE` | error | A date is not in the format of the syntax: CII format 102 (`YYYYMMDD`), UBL `YYYY-MM-DD` with an optional time zone. The date is not read. | | `QUITTIO-READER-NUMBER` | error | A number is not a decimal number (exponents like `1e3`, separators, more than 40 characters) or a payment term period is no whole number. The value is not read. | | `BR-DEC-…` | error | An amount whose business term has a BR-DEC rule has more than two decimals. It is rounded to two decimals. | | `QUITTIO-READER-DECIMALS` | warning | Another amount has more than two decimals and is rounded. | | `BR-12` … `BR-15`, `BR-45`, `BR-46` | error | A mandatory amount (BT-106, BT-109, BT-112, BT-115, BT-116, BT-117) is missing; 0.00 is used. Without the document totals (BG-22: CII `SpecifiedTradeSettlementHeaderMonetarySummation`, UBL `LegalMonetaryTotal`) `BR-12` (only with lines) to `BR-15` are reported and `data->totals` and `declaredTotals` stay `null`; `validateFile()` then reports these instead of `QUITTIO-NOT-CALCULATED`. | | `QUITTIO-READER-DERIVED` | info | BT-106 was derived for a document without lines. | | `BR-22`, `BR-23` | error | A line has no quantity (0 is used) or no unit. | | `QUITTIO-READER-SUBLINE-QUANTITY`, `QUITTIO-READER-SUBLINE-UNIT` | info | A sub line of the extension omits quantity or unit. | | `QUITTIO-READER-DUPLICATE-LINE` | warning | A line identifier is used more than once (CII). | | `QUITTIO-READER-PARENT-LINE` | warning / error | A sub line refers to a parent line that does not exist (warning), to itself, to a cycle or to a hierarchy deeper than 32 levels (error); it is read as a top-level line (CII). | | `QUITTIO-READER-BASE-QUANTITY` | info / warning | Net and gross price state different base quantities (CII): converted (info) or not convertible (warning). | | `QUITTIO-READER-VAT-CATEGORY` | error | Unknown VAT category; `S` is assumed. | | `QUITTIO-READER-UNKNOWN-CODE` | info / warning | A code that this package version does not know (unit, scheme, reason codes …) is kept as raw value (info); a VAT point date code outside the code list (warning). Codes of the official code lists without an enum case are read without finding. | | `QUITTIO-READER-UNIT` | warning | A payment terms period is not given in days or weeks (CII); weeks are converted to days. | | `QUITTIO-READER-INSTALLMENT` | warning | A partial payment (EXTENDED) without due date is skipped. | | `QUITTIO-READER-ATTACHMENT` | warning | An attached document has invalid Base64, an unsupported MIME code or no usable file name and is skipped. | | `QUITTIO-READER-UNKNOWN-PROFILE` | info | The specification identifier (BT-24) is not known to this installation. | | `QUITTIO-READER-PDF-CANDIDATES` | warning | The PDF embeds several invoice files with different content, or several XML files without a known name; the chosen file is named. | | `QUITTIO-READER-PDF-AF` | warning | The embedded file is not an associated file (`/AF`), as PDF/A-3 requires. | | `QUITTIO-READER-PDF-RELATIONSHIP` | warning | The `/AFRelationship` of the embedded file does not match the profile (`Alternative`, or `Data` for MINIMUM and BASIC WL). | | `QUITTIO-DECLARED-TOTALS` | warning (L3) | The declared amounts differ from a recalculation from the lines. | | `QUITTIO-READER-RECALCULATION` | warning (L3) | The invoice could not be recalculated for the comparison. | Unknown codes are errors in `validateFile()`, where the code list rules (`BR-CL-…`) run. ## PDF A ZUGFeRD or Factur-X PDF carries the invoice XML as embedded file ([Embedded files](formats.md#embedded-files)). The reader finds embedded files in the associated files (`/AF`) of the document catalog and in the name tree (`/Names /EmbeddedFiles`). This works also in PDFs with cross-reference streams, object streams, hybrid files and incremental updates. Damaged cross-reference data is rebuilt by scanning the file. The file is chosen in this order: 1. A known file name (`factur-x.xml`, `xrechnung.xml`, `zugferd-invoice.xml`, `ZUGFeRD-invoice.xml`, case-insensitive) before any other `.xml` file. 2. Among those, files referenced from `/AF` before files only in the name tree. 3. Then the order of the known names and of the document. Up to three further candidates with a known name are compared with the chosen file; different content is reported (`QUITTIO-READER-PDF-CANDIDATES`). Encrypted PDFs cannot be read. Streams must use `FlateDecode` (with PNG or TIFF predictors) or no filter. ## Limits and security Incoming files come from outside, so the reader limits what it accepts: | Protection | Limit | |---|---| | XML size | `quittio.reader.max_xml_bytes` (10 MB); without Laravel `reader.max_xml_bytes` | | PDF size | `quittio.reader.max_pdf_bytes` (25 MB) | | Decompressed size of every PDF stream | `quittio.reader.max_decompressed_bytes` (50 MB) | | DOCTYPE | Rejected (also an internal subset), so there are no external entities, no DTDs and no entity expansion; libxml runs without network access and without entity substitution | | Encoding | UTF-8, UTF-16, US-ASCII, ISO-8859-*, Windows-125*; UTF-32, UTF-7, EBCDIC and other encodings are rejected | | Start tags | at most 64 KB and 256 attributes per element, at most 256 namespace declarations in scope (checked before parsing) | | Numbers | decimal literals of at most 40 characters, no exponents | | PDF structure | at most 1,000,000 objects, 100 cross-reference sections, reference chains of 32, 100,000 objects per object stream, 100,000 objects when rebuilding, nesting depth 64; name tree depth 10 and 1,000 nodes, 1,000 file specifications, every node visited once | | Sub lines | hierarchies deeper than 32 levels are flattened (CII) | | File names | embedded file names and attachment file names are never used as paths: directory parts and control and format characters (e.g. a right-to-left override) are removed, `.`, `..` and empty names are rejected | Untrusted values are shortened in messages, and the currency of amounts is compared in PHP, never inside an XPath expression. The configuration keys are described in [Reader](configuration.md#reader). ## Own readers `Quittio\Core\Reading\InvoiceReader` can be used directly: ```php use Quittio\Core\Pdf\PdfEmbeddedFileExtractor; use Quittio\Core\Reading\InvoiceReader; $reader = new InvoiceReader( readers: null, // null = InvoiceReader::defaultReaders(): CII and UBL profiles: null, // null = ProfileRegistry::withDefaults() (no country pack profiles) pdfExtractor: new PdfEmbeddedFileExtractor(maxPdfBytes: 25 * 1024 * 1024, maxDecompressedBytes: 50 * 1024 * 1024), maxXmlBytes: 10 * 1024 * 1024, ); $read = $reader->withReader(new MyFormatReader)->readFile('/path/to/invoice.xml'); ``` A reader for another XML format implements `FormatReader` (`canRead(DOMDocument $document): bool`, `read(DOMDocument $document, string $xml): ReadResult`). `withReader()` returns a copy with the reader added after the default ones. Country packs provide readers for national formats with `CountryPack::readers()`. It returns new instances on every call, because readers keep the state of one document. `Quittio::read()`, `readFile()`, `validateFile()` and `validatePath()` try them after the CII and UBL readers, for every registered pack. The XSD driver (L4) knows only CII and UBL and reports other root elements (`XSD-UNKNOWN-ROOT`); add a validator for a national format with `addValidator()`. A PDF extractor implements one of two interfaces: - `EmbeddedXmlExtractor::extract(string $pdf): ?array` returns file name and XML, - `DiagnosingXmlExtractor::extractFile(string $pdf): ?ExtractedXml` returns `filename`, `xml`, `relationship` and `findings`. `PdfEmbeddedFileExtractor::names($pdf)` lists the names of all embedded files. # Laravel integration This page explains how Quittio works in a Laravel application: you start an invoice, render it, save, download or mail the result, render in the queue, react to events and adapt templates, translations and drivers. Read [Getting started](getting-started.md) first for a first invoice. The terms used here (format, profile, ZUGFeRD, XRechnung) are explained in [E-invoicing basics](concepts.md). `quittio/laravel` connects the country-neutral core with Laravel. It needs PHP 8.3 and Laravel 13. ```bash composer require quittio/quittio # core, Laravel integration and country pack Germany php artisan quittio:install # publishes config/quittio.php ``` Then set your company in `config/quittio.php` ([The keys you set first](configuration.md#the-keys-you-set-first)). ## Overview A typical invoice goes through these steps: ```php use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $result = Quittio::invoice() // 1. start: seller from config/quittio.php ->number('RE-2026-0042') ->deliveryDate('2026-09-30') // date of supply, mandatory in Germany ->buyer($customer) // a model that implements ProvidesParty ->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour) ->dueInDays(14) ->render(); // 2. render: format chosen automatically $result->save('invoices', '2026/'); // 3. save on a filesystem disk … return $result->download(); // … or download, or attach to a mail ``` | Step | What you use | Section | |---|---|---| | Start an invoice | `Quittio::invoice()` | [Starting invoices](#starting-invoices) | | Render it | `->render()`, or `Quittio::make($invoice)` for more options | [Rendering invoices](#rendering-invoices) | | Hand it out | `->save()`, `->download()`, `->stream()`, mail attachment | [Saving, downloading and mailing](#saving-downloading-and-mailing) | | Render later | `->queue()->save()` | [Queueing](#queueing) | | React to rendering | `InvoiceRendered` and other events | [Events](#events) | | Cancel an invoice | `$result->cancel()` | [Cancellations](#cancellations) | | Quotes, reminders and other documents | `Quittio::quote()` … | [Documents](#documents) | | Adapt the output | Blade templates, translations, drivers | [Blade templates](#blade-templates) and the sections after it | All calls go through the facade `Quittio\Laravel\Facades\Quittio`. Instead of the facade you can inject `Quittio\Laravel\QuittioManager`; both are the same singleton. Every method is listed in [Facade](#facade). ## Starting invoices `Quittio::invoice()` starts an invoice. It returns an `InvoiceBuilder` ([Building invoices](building-invoices.md)) that already has your company as seller, its first bank account and the small business setting of `config/quittio.php` ([Seller](configuration.md#seller)). You add the number, the customer, the lines and the payment terms. Then you end the chain: - `->render()` renders the invoice: PDF and XML, in a [`RenderedInvoice`](#renderedinvoice). - `->build()` returns the data only (an `InvoiceData`), e.g. to store a draft or to render it later with `Quittio::make()`. ```php use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $builder = Quittio::invoice() ->number('RE-2026-0042') ->deliveryDate('2026-09-30') // date of supply, mandatory in Germany ->buyer($customer) // a model that implements ProvidesParty ->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour) ->dueInDays(14); $result = $builder->render(); // format chosen automatically $result = $builder->render('xrechnung-cii'); // one format $result = $builder->render(['zugferd', 'xrechnung-cii']); // several formats; the first is the main result $report = $builder->check(); // validate only, no PDF $invoice = $builder->build(); // the data, not rendered ``` `render(string|array|null $format = null)` is the same as `Quittio::make($builder)->format($format)->render()`. For profile, locale, template or the queue, pass the builder to `Quittio::make()` ([Rendering invoices](#rendering-invoices)). `check(string|array|null $format = null)` validates in the same way, including the XML, but renders no PDF. It returns the `ValidationResult` and does not throw for invalid data ([Validation](validation.md)). The builder is a `Quittio\Laravel\Builder\RenderableInvoiceBuilder`, a subclass of `InvoiceBuilder`, so all builder methods and macros work. Documents start the same way, with the seller and bank account of the configuration. They end with `->build()` or `->render()` (PDF); see [Documents](#documents): | Method | Document | |---|---| | `Quittio::quote()` | Quote | | `Quittio::orderConfirmation()` | Order confirmation | | `Quittio::deliveryNote()` | Delivery note | | `Quittio::proforma()` | Proforma invoice | | `Quittio::paymentReminder(int $level = 0)` | Payment reminder (level 0) or reminder (level 1, 2 …) | | `Quittio::accountStatement()` | Account statement | ### Another seller The bank account and the small business setting belong to the seller of the configuration. So `->seller(...)` with another seller (`Party::make()`, a `SellerParty` or a model) removes everything the configuration set for its seller: the bank account and the small business flag. With the [Managed layer](#managed-layer) (the optional package that stores invoices in the database) it also removes the issuer's payment terms, design, locale, time zone and currency. What you set on the builder yourself stays, also when you set it before `->seller()`. A callable changes the configured seller instead and keeps its values: ```php use Quittio\Core\Builder\Party; use Quittio\Core\Data\Meta; use Quittio\Core\Enums\PaymentMeansCode; use Quittio\Laravel\Facades\Quittio; Quittio::invoice() ->seller(Party::make('Lisa Klein')->taxNumber('21/815/08150')->identifier('21/815/08150')->address('Gartenstraße 7', '12345', 'Musterstadt', 'DE')) ->payment(PaymentMeansCode::SepaCreditTransfer, iban: 'DE89370400440532013000') // Lisa's account ->flag(Meta::FLAG_SMALL_BUSINESS); // if she is a small business Quittio::invoice()->seller(fn ($s) => $s->contact('Max Muster')); // same seller, same defaults ``` ### Seller from the configuration `Quittio\Laravel\Seller` turns the `seller` section of `config/quittio.php` ([Configuration](configuration.md#seller)) into invoice data: | Method | Effect | |---|---| | `Seller::fromConfig(): SellerParty` | Builds the seller: name, VAT identifier, tax number, address, legal registration, identifiers, additional legal information, contact (when one of its values is set) and electronic address (when set) | | `Seller::apply(InvoiceBuilder $builder): InvoiceBuilder` | Sets the seller on the builder and adds a credit transfer with the first bank account that has an IBAN. With `seller.small_business`, it flags the invoice as small business invoice (`Meta::FLAG_SMALL_BUSINESS`). Returns the same builder. | `Quittio::invoice()` and the document methods use them through `ConfigInvoiceDefaults` (see below). Use them directly for builders you create yourself: ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Core\Documents\DocumentBuilder; use Quittio\Laravel\Seller; $invoice = Seller::apply(InvoiceBuilder::make())->number('RE-2026-0042')/* ... */->build(); $quote = DocumentBuilder::quote()->seller(Seller::fromConfig()); ``` With the Managed layer, issuer profiles hold the seller instead (`$issuer->builder()`). **Taking the seller from somewhere else.** `Quittio::invoice()` gets its seller from the binding of `Quittio\Laravel\Contracts\InvoiceDefaults` (`apply(InvoiceBuilder $builder): void`; for documents it is applied to their invoice builder). The default binding, `ConfigInvoiceDefaults`, calls `Seller::apply()`. The Managed layer binds the current issuer. Bind your own class to take the seller from elsewhere, e.g. from the current tenant: ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Laravel\Contracts\InvoiceDefaults; $this->app->bind(InvoiceDefaults::class, fn () => new class implements InvoiceDefaults { public function apply(InvoiceBuilder $builder): void { $builder->seller(tenant()->billingParty()); // a SellerParty, PartyBuilder or ProvidesParty } }); ``` ## Rendering invoices Rendering turns invoice data into the output files. `Quittio::make()` starts a render call for data you built before: an `InvoiceData`, an `InvoiceBuilder`, or a model that implements `ProvidesInvoiceData`. It offers the options that `->render()` on the builder does not have: ```php $result = Quittio::make($invoice)->render(); // format chosen automatically $result = Quittio::make($order)->format('zugferd')->profile('en16931')->locale('de')->render(); $result = Quittio::make($invoice)->formats(['zugferd', 'xrechnung-cii'])->render(); ``` A run does everything in one go: 1. It calculates the invoice: totals, VAT breakdown, and the due date from `defaults.due_days`. You never calculate totals yourself. 2. It chooses the format (the output, e.g. a ZUGFeRD PDF or an XRechnung XML, see [Formats](concepts.md#formats)) and the profile (how much of EN 16931 the XML contains, see [Profiles](concepts.md#profiles)). 3. It validates the invoice ([Validation per call](#validation-per-call)). 4. It writes the XML, renders the Blade template and produces the PDF/A (the archivable PDF standard) with the embedded XML. All formats of one call share the same calculation. ### PendingInvoice `Quittio::make()` returns a `Quittio\Laravel\Rendering\PendingInvoice`. It collects the options of one call; every setter returns the same object. | Method | Effect | |---|---| | `format(string $format)` | One output format: `zugferd` (`factur-x`, `facturx`), `xrechnung-cii` (`xrechnung`), `xrechnung-ubl`, `cii`, `ubl`, `peppol`, `pdf`, `html`, `json`. Replaces earlier formats. | | `formats(array $formats)` | Several formats from one calculation; duplicates are removed. The first is the main result. | | `profile(Profile\|string $profile)` | Profile key (`minimum`, `basic-wl`, `basic`, `en16931`, `extended`, `peppol`; `xrechnung` and `xrechnung-extension` with the German country pack; aliases such as `comfort`) or a `Profile` object. Every requested format must allow it, otherwise the run throws a `ConfigurationException`. | | `locale(string $locale)` | Locale of labels and formatting | | `country(string $country)` | Leading country pack (upper-cased), instead of the seller's country | | `template(string $template)` | Blade view or plain PHP template name; only allowed views (see [Blade templates](#blade-templates)) | | `presentation(Presentation $presentation)` | Design for this call, merged over the configured design property by property; the invoice's own presentation wins (see [Templates and design](templates.md)) | | `validate(bool $throwOnError = true)` | Validation mode `error` (`true`) or `warn` (`false`) for this call | | `withoutValidation()` | Validation mode `off` for this call | | `option(string $key, mixed $value)` | Option for the view model and the validation rules (see [Render options](#render-options)) | | `render()` | Renders: `RenderedInvoice` | | `check()` | Validates like `render()` without template and PDF: `ValidationResult` | | `prepare()` | Calculates and validates without rendering: `PreparedInvoice` | | `queue(?string $connection = null, ?string $queue = null)` | Renders in a queued job: `QueuedInvoice` (see [Queueing](#queueing)) | | `data()` | The invoice data passed to `make()` (not yet calculated) | | `options()` | The collected `Quittio\Core\Rendering\RenderOptions` | ### Fallback order Without an explicit value, each choice falls back in this order: | Choice | Order | |---|---| | Format | `format()`/`formats()` → `defaults.format` → automatic choice (when `defaults.format` is `null` or `'auto'`; see [Automatic choice](formats.md#automatic-choice)) | | Profile | `profile()` → `defaults.profile`, where the format allows it → profile of the automatic choice → default profile of the format. CII formats switch to EXTENDED (the profile with more fields than EN 16931) when the invoice uses EXTENDED features and `profile()` was not called (`format_resolver.auto_upgrade_profile`). | | Locale | `locale()` → invoice locale (`InvoiceBuilder::locale()`) → `defaults.locale` → country pack | | Country pack | `country()` → seller country (BT-40, see [business terms](concepts.md#en-16931-and-business-terms)) → `defaults.country` → generic EN 16931 pack | | Template | `template()` → `Presentation::$template` of the invoice → `template` of the presentation passed with `presentation()` → `pdf.template` | | Validation | `validate()`/`withoutValidation()` → `validation.mode` | ### Render options `option()` passes values to the view model and the validation rules. They override configured values of the same name: | Key | Value | Effect | |---|---|---| | `watermark` | string | Watermark text, e.g. `'DRAFT'`. Only for the formats `pdf` and `html`: with any format that produces e-invoice XML, the run throws a `ConfigurationException`. Watermarked PDFs are not PDF/A. | | `paper` | string | Paper size (like `pdf.paper`) | | `markdown` | bool | Line descriptions and notes as Markdown (like `pdf.markdown`) | | `fold_marks` | bool | Fold and punch marks (like `pdf.fold_marks`) | | `girocode_amount` | `'due'`\|`'discounted'` | Amount in the GiroCode, the payment QR code (like `pdf.payment_qr.girocode_amount`) | ### Validation per call Validation checks the invoice against the rules of EN 16931, the format and the country ([Rules and validation](concepts.md#rules-and-validation)). The validation mode decides what happens with errors: - In mode `error`, a run with errors throws `Quittio\Core\Validation\ValidationException`. With `validation.block_on_warnings`, warnings throw as well. The exception's `result` property holds the `ValidationResult`. - In mode `warn`, the result is attached and rendering continues. - In mode `off`, nothing is validated. ```php use Quittio\Core\Validation\ValidationException; use Quittio\Laravel\Facades\Quittio; try { $result = Quittio::make($invoice)->format('xrechnung-cii')->render(); } catch (ValidationException $e) { return back()->withErrors(array_map(fn ($m) => $m->message, $e->result->errors())); } $result = Quittio::make($invoice)->validate(false)->render(); // never throws on findings $result->validation()->warnings(); ``` `check()` always validates with the configured levels, whatever the mode, and never throws on findings. It does not render the template or the PDF, so the PDF/A check (level L6, veraPDF) does not run: ```php $report = Quittio::make($invoice)->format('xrechnung-cii')->check(); $report->isValid(); ``` `prepare()` returns a `Quittio\Core\Rendering\PreparedInvoice` with the decisions of a run: | Member | Content | |---|---| | `data` | Calculated `InvoiceData` | | `formats` | Format key => `Profile` (null for formats without XML) | | `country`, `buyerCountry` | Leading country pack and the buyer's pack (or null) | | `locale` | Chosen locale | | `validation` | `ValidationResult` of the data and XML levels | | `format()`, `profile()` | First format and its profile | Validation levels and messages: [Validation](validation.md). Common errors and their fixes: [Troubleshooting](troubleshooting.md). ### RenderedInvoice `render()` returns a `Quittio\Laravel\Rendering\RenderedInvoice` for the first format. It holds the files in memory. It implements `Illuminate\Contracts\Mail\Attachable` and `Illuminate\Contracts\Support\Responsable`, so you can attach it to a mail or return it from a controller. | Method | Returns | |---|---| | `data()` | Calculated `InvoiceData` (totals, VAT breakdown, due date) | | `format()` | Format key of this result | | `formats()` | All format keys of the run | | `as(string $format)` | The result of another format of the same run; a format that was not rendered throws a `LogicException` | | `profile()` | `Profile` of the XML, `null` for formats without XML | | `validation()` | `ValidationResult` of this format (warnings; in mode `warn` also errors) | | `hasPdf()`, `hasXml()` | Whether the format produced a PDF or XML | | `pdf()` | PDF bytes (`zugferd`, `pdf`); other formats throw a `LogicException` | | `xml()` | XML (`zugferd`, `xrechnung-cii`, `xrechnung-ubl`, `cii`, `ubl`, `peppol`, `pdf` with `b2c_embed_xml`); other formats throw | | `html()` | HTML (format `html` only); other formats throw | | `json()` | The calculated invoice as JSON (`InvoiceData::toJson()`), for every format | | `contents()` | Main output: PDF, otherwise XML, HTML or JSON | | `mimeType()` | `application/pdf`, `application/xml`, `text/html` or `application/json` | | `filename(?string $extension = null)` | File name from `pdf.filename`, e.g. `Rechnung_RE-2026-0042.pdf` ("Rechnung" is the German title, invoice); with `$extension` for another output, e.g. `filename('xml')` | | `hashes()` | SHA-256 of the outputs: `['pdf' => …, 'xml' => …, 'html' => …]` (only those produced) | | `download(?string $filename = null)` | HTTP response as attachment | | `stream(?string $filename = null)` | HTTP response for inline display in the browser | | `toResponse($request)` | Same as `download()`: return the result from a controller | | `toMailAttachment()` | `Illuminate\Mail\Attachment` with file name and MIME type | | `save(?string $disk = null, ?string $path = null)` | Stores the main output on a filesystem disk, returns the path; throws a `RuntimeException` when the disk does not store it | | `cancel(string $number, ?string $reason = null, ?array $lines = null, Date\|DateTimeInterface\|string\|null $issueDate = null, ?string $strategy = null, ?CancellationHistory $history = null)` | Builds the cancellation of this invoice with `Quittio::cancellation()` and renders it in the formats of this run; returns its `RenderedInvoice` ([Cancellations](#cancellations)) | | `core()` | The core result `Quittio\Core\Rendering\RenderedInvoice` (adds `extension()` and `save(string $localPath)` for local files) | ## Saving, downloading and mailing A rendered result is not stored anywhere yet. Save it on a disk, return it as an HTTP response or attach it to a mail. The same methods work for documents (`RenderedDocument`, see [Documents](#documents)). ### File names Results are named by the pattern `pdf.filename` (default `{title}_{number}.{ext}`). The placeholders are: - `{title}`: the document title in the render locale, e.g. "Rechnung" (invoice), "Stornorechnung" (cancellation invoice) or, in English, "Invoice", - `{number}`: the invoice number, - `{date}`: the issue date as `Y-m-d`, - `{format}`: the format key (for quotes and other documents: the document type), - `{ext}`: the extension (`pdf`, `xml`, `html`, `json`). Title and number are reduced to letters, digits, `.`, `_` and `-`. ```php // 'filename' => '{date}_{number}.{ext}' $result->filename(); // 2026-10-03_RE-2026-0042.pdf $result->as('xrechnung-cii')->filename(); // 2026-10-03_RE-2026-0042.xml ``` ### Responses Return the result from a controller to send it to the browser: ```php public function show(Invoice $invoice) { $result = Quittio::make($invoice)->format('zugferd')->render(); return $result; // download (Content-Disposition: attachment) // return $result->download('invoice.pdf'); // own file name // return $result->stream(); // inline, e.g. in a browser tab or iframe } ``` The response carries: - the content as body, - `Content-Type` from `mimeType()`, - `Content-Disposition` with an ASCII fallback name and the UTF-8 name (`filename*=`), - `Cache-Control: private, no-store`. The ASCII fallback transliterates: `Rechnung Über.pdf` becomes `Rechnung_Uber.pdf`, and `ß` becomes `ss`. Every other character outside letters, digits, `.`, `_` and `-` becomes one `_`. `stream()` sends the complete file inline; it is not a chunked `StreamedResponse`. ### Storage ```php $path = $result->save(); // quittio.storage.disk, file name of the result $path = $result->save('invoices', '2026/'); // directory (ends with "/"): 2026/Rechnung_RE-2026-0042.pdf $path = $result->as('xrechnung-cii')->save('invoices', '2026/RE-2026-0042.xml'); ``` `save()` stores `contents()` (the main output) with `Storage::disk($disk)->put()` and returns the path: - The disk defaults to `quittio.storage.disk`, and to the default disk when that is empty. - The path defaults to `filename()`. A path that ends with `/` is a directory; the file name is added. - Existing files are overwritten (unlike the archive of the Managed layer). For a local path without a disk, use `$result->core()->save('/var/invoices')`. ### Mail attachments `RenderedInvoice` and `RenderedDocument` are attachable. Return them from `attachments()` of a mailable; file name and MIME type come from the result: ```php use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; use Quittio\Laravel\Facades\Quittio; use Quittio\Laravel\Rendering\RenderedInvoice; final class InvoiceMail extends Mailable { public function __construct(private RenderedInvoice $invoice) {} public function envelope(): Envelope { return new Envelope(subject: 'Invoice '.$this->invoice->data()->number); } public function content(): Content { return new Content(view: 'mail.invoice'); } public function attachments(): array { return [$this->invoice]; // file name and MIME type of the result } } Mail::to($customer)->send(new InvoiceMail(Quittio::make($invoice)->format('zugferd')->render())); ``` `->attach($result)` works in `build()` and in notification mail messages as well. - For an XRechnung, attach the XML result (`$result->as('xrechnung-cii')`). If the recipient wants a readable PDF too, render a second format `pdf` in the same run and attach it as well. - For queued mailables, consider storing the file first (`save()`) and attaching it with `Attachment::fromStorageDisk()`, so that the file does not travel through the queue. The Managed layer ships its own mailable and delivery tracking (see [Managed layer](managed.md#delivery)). ## Queueing Render in a queued job when a request should not wait for the PDF, e.g. for many invoices at once. The job renders and saves the result on a disk: ```php $job = Quittio::make($invoice)->format('zugferd')->queue()->save('invoices', '2026/'); $job = Quittio::make($invoice)->queue('redis', 'invoices')->save(); ``` `queue()` returns a `Quittio\Laravel\Rendering\QueuedInvoice`. Its only method, `save(?string $disk = null, ?string $path = null)`, dispatches a `Quittio\Laravel\Jobs\RenderInvoice` job and returns it. | Detail | Behaviour | |---|---| | Connection, queue | Arguments of `queue()`, else `quittio.queue.connection` and `quittio.queue.queue`. The default queue is `quittio`: start a worker with `--queue=quittio,…`. | | Payload | The invoice as versioned JSON snapshot (`invoice`), the `RenderOptions` of the call as versioned JSON (`options`, see `RenderOptions::toArray()`; `renderOptions()` restores them), `disk` and `path` (public properties). A profile object travels by its key; options must be scalars, `null` or arrays of them. | | Dispatch | Inside a database transaction after the commit, and not at all after a rollback (`quittio.queue.after_commit`, default `true`) | | Execution | `handle()` renders with the stored options (events included), calls `save($disk, $path)` and dispatches `QueuedInvoiceStored`. A `null` disk is resolved when the job runs. | | Retries | `$tries = 3` with 10 and 60 seconds between the attempts, for errors that may pass (a disk that is not reachable, a KoSIT daemon that restarts). Errors that another attempt cannot fix fail the job at once: validation errors, invalid or unreadable data, configuration errors (`RenderInvoice::PERMANENT_ERRORS`). | | Failure | `failed()` dispatches `QueuedInvoiceFailed` with the job and the exception | Documents are queued the same way: `Quittio::document($quote)->queue()->save('documents', 'quotes/')` dispatches a `RenderDocument` job, which carries the document as versioned snapshot and dispatches `QueuedDocumentStored` or `QueuedDocumentFailed` ([Documents: queueing](documents.md#queueing)). | Upgrades | Snapshots of older data model versions are migrated by `quittio.snapshots.upcasters` ([Snapshots](configuration.md#snapshots)); `data()` returns the restored invoice. Jobs queued by Quittio 0.1.0 (options as PHP object) still run. | With the `sync` connection the job runs immediately. ## Events Events let you react to rendering, e.g. to log every invoice or to store its hashes. All events are in the namespace `Quittio\Laravel\Events`. They are dispatched by `QuittioManager::renderInvoice()`, i.e. by `PendingInvoice::render()`, by queued `RenderInvoice` jobs and by the rendering of the Managed layer. | Event | Dispatched | Payload | |---|---|---| | `InvoiceRendering` | Before the invoice is calculated, validated and rendered | `invoice` (`InvoiceData` as passed, not calculated), `options` (`RenderOptions`) | | `InvoiceValidated` | After all formats were rendered, before `InvoiceRendered`, when the run validated and validation did not block it: in mode `error` and in mode `warn`; not in mode `off` (nothing was validated) | `invoice` (calculated `InvoiceData`), `result` (`ValidationResult` of the main format; in mode `warn` it may contain errors, so check `isValid()`) | | `InvoiceValidationFailed` | When validation blocked the run (mode `error`); the `ValidationException` is thrown afterwards | `invoice` (`InvoiceData` as passed), `result` (`ValidationResult`) | | `InvoiceRendered` | After all requested formats were produced | `result` (`Quittio\Laravel\Rendering\RenderedInvoice`) | | `QueuedInvoiceStored` | After a queued `RenderInvoice` job stored the main output, after `InvoiceRendered` | `result` (`RenderedInvoice`), `disk` (of the job, `null` = configured disk), `path` (the stored path) | | `DocumentRendering` | Before a document (quote, reminder, …) is calculated and rendered | `document` (`DocumentData` as passed), `options` (`RenderOptions`) | | `DocumentRendered` | After a document was rendered | `result` (`Quittio\Laravel\Rendering\RenderedDocument`) | | `QueuedInvoiceFailed` | When a queued `RenderInvoice` job failed for good (last attempt, or at once for errors another attempt cannot fix) | `job` (`RenderInvoice`; `$job->data()` returns the invoice), `exception` | | `QueuedDocumentStored` | After a queued `RenderDocument` job stored the document | `result` (`RenderedDocument`), `disk`, `path` | | `QueuedDocumentFailed` | When a queued `RenderDocument` job failed for good | `job` (`RenderDocument`; `$job->data()` returns the document), `exception` | | `MandatoryFieldMissing` | When the template omitted mandatory values ([Mandatory field guard](validation.md#mandatory-field-guard)), in the guard modes `exception` (before the exception is thrown) and `log` | `invoice` (calculated `InvoiceData`), `fields` (list of `Quittio\Core\Countries\MandatoryField` with `accessKey`, `description`, `legalBasis`) | No events are dispatched for `check()`, `prepare()`, reading or validating files. `Quittio::fake()` dispatches the events of a real run ([Testing](testing.md#how-the-fake-works)). Queued listeners receive the event serialised: `InvoiceRendered`, `QueuedInvoiceStored` and `DocumentRendered` carry the rendered files, PDF bytes included, into the queue payload. For many or large invoices, listen synchronously and queue a job with the stored path instead. ```php use Illuminate\Support\Facades\Event; use Quittio\Laravel\Events\InvoiceRendered; Event::listen(function (InvoiceRendered $event) { Log::info('Invoice rendered', [ 'number' => $event->result->data()->number, 'format' => $event->result->format(), 'hashes' => $event->result->hashes(), ]); }); ``` The Managed layer dispatches further events for the lifecycle of stored invoices (see [Managed layer](managed.md#events)). ## Cancellations A cancellation reverses an issued invoice. It is a new invoice with its own number and a reference to the original ("Stornorechnung", cancellation invoice, on German invoices). ```php $result = Quittio::make($invoice)->render(); $cancellation = $result->cancel('ST-2026-0001', reason: 'Charged twice'); // rendered in the formats of $result // stored data: build the cancellation and render it $builder = Quittio::cancellation($original, 'ST-2026-0001', reason: 'Charged twice'); Quittio::make($builder)->render(); // partial cancellation of line 2 (one unit), with the earlier partial cancellations Quittio::cancellation($original, 'ST-2026-0002', lines: [['line' => '2', 'quantity' => 1]], history: $history); ``` `$result->cancel()` takes the arguments of `Quittio::cancellation()` (`reason`, `lines`, `issueDate`, `strategy`, `history`). It renders the cancellation in the formats of the invoice, so a cancellation of an XRechnung is an XRechnung again. The sign strategy decides how the amounts are written: `credit_note` is a credit note (type code 381) with positive amounts, `negative_invoice` an invoice (type code 380) with negative amounts. Both are permitted in Germany. - The sign strategy comes from the argument, then `corrections.cancellation_strategy`, then the country pack. - The locale comes from the original, then `defaults.locale`, then the country pack. Details: [Cancellation](getting-started.md#cancellation), [Cancellations and corrections](building-invoices.md#cancellations-and-corrections) and, for stored invoices, [Managed layer](managed.md#cancellations-and-corrections). ## Documents Quotes, order confirmations, delivery notes, proforma invoices, payment reminders and account statements are documents: PDFs without e-invoice XML. Start them with the methods of [Starting invoices](#starting-invoices). End the chain with `->render()`, or pass the builder to `Quittio::document()` for more options: ```php use Quittio\Core\Builder\Party; use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $quote = Quittio::quote()->number('AN-2026-0007')->issueDate('2026-10-01') ->buyer(Party::make('Kunde AG')->address('Weg 1', '54321', 'Beispielstadt', 'DE')) ->line('Consulting', netPrice: '100.00', quantity: 2, unit: UnitCode::Hour); return Quittio::document($quote)->locale('de')->render()->download(); ``` `Quittio::document()` returns a `Quittio\Laravel\Rendering\PendingDocument`: | Method | Effect | |---|---| | `template(string $template)` | Template (same allowlist as invoices) | | `locale(string $locale)` | Locale | | `country(string $country)` | Leading country pack | | `presentation(Presentation $presentation)` | Design for this call | | `htmlOnly()` | Render HTML only, no PDF | | `option(string $key, mixed $value)` | Output option of this call, e.g. `option('fold_marks', false)` | | `render()` | `RenderedDocument` | `PendingDocument` is `Macroable` like `PendingInvoice`. Documents never contain XML and are not validated. `RenderedDocument` offers: - `data()` (`DocumentData`), `hasPdf()` (`false` after `htmlOnly()`), `pdf()` (throws after `htmlOnly()`), `html()`, `contents()`, `mimeType()`, `filename(?string $extension = null)` and `core()`, - like invoices: `download()`, `stream()`, `toResponse()`, `toMailAttachment()` and `save()`. Document types and their data: [Quotes and other documents](documents.md). ## Blade templates The sections from here on describe how to adapt the output. The PDF is rendered from a Blade template. The core prepares the invoice for display (the view model: formatted amounts, labels, design values) and renders it with `Quittio\Laravel\Templates\BladeTemplateEngine`. A view receives one variable, `$invoice` (`Quittio\Core\View\InvoiceViewModel`, see [View model](templates.md#view-model)). | View | Use | |---|---| | `quittio::templates.default.layout` | Default template (`pdf.template`) | | `quittio::templates.default.partials.*` | Its partials (`letterhead`, `recipient`, `lines`, `totals`, `payment`, …) | | `invoices.{name}.layout` | Templates created by `make:quittio-template {name}` | Customise the design in levels (see [Templates and design](templates.md)): 1. the configuration (`pdf.*`, see [PDF and design](configuration.md#pdf-and-design)), 2. published partials: `php artisan vendor:publish --tag=quittio-views` copies them to `resources/views/vendor/quittio/templates`, which Laravel prefers over the package views, 3. an own template (`make:quittio-template`), 4. an own renderer ([Extensions](#extensions)). Template names may come from invoice data (`Presentation::$template`) or issuer branding. So only these views can be rendered: - `default`, which means the view of `pdf.template`, - the view configured in `pdf.template`, - the shipped views under `quittio::templates.` (not their partials), - the views of `pdf.templates` (default `['invoices.*']`). An entry ending in `*` allows every view below that prefix, e.g. `mytheme::*`. Views containing `.partials.` never qualify. Any other existing view fails with an `InvalidArgumentException`. A name that is no Blade view falls back to the plain PHP templates of the core. They resolve names inside their template directories only, never file paths. ```php // config/quittio.php 'pdf' => [ 'template' => 'invoices.acme.layout', // every invoice 'templates' => ['invoices.*', 'mytheme::*'], // selectable per call or per invoice ], Quittio::make($invoice)->template('mytheme::invoice')->render(); ``` ## Translations Invoices are printed in the language of the invoice, with Quittio's labels ("Rechnung", "Invoice" …). Labels, rule messages, code names and terms come from the core's language files `labels.php`, `rules.php`, `codes.php` and `terms.php`. They exist in German, English and French; French has no rule messages. They are in the translation namespace `quittio`. The core translates through Laravel's translator, so the usual overrides work: ```bash php artisan vendor:publish --tag=quittio-lang # lang/vendor/quittio/{locale}/*.php ``` A published file only needs the keys you change; Laravel merges it over the package file. ```php // lang/vendor/quittio/de/labels.php: "Bitte überweisen" (please transfer) instead of "Zahlbetrag" (amount due) return ['amount_due' => 'Bitte überweisen']; // or at runtime app('translator')->addLines(['labels.amount_due' => 'Bitte überweisen'], 'de', 'quittio'); ``` - Country packs add their own namespace `quittio-{code}`, e.g. `quittio-de` (`rules.php`, `tax.php`). Override it in `lang/vendor/quittio-de/{locale}/`. - The Managed layer uses `quittio-managed` (`--tag=quittio-managed-lang`). - Labels of a single invoice can be changed with `Presentation::$labels`. - Invoices are translated in the render locale, not in the application locale. ## Preview route The preview shows the sample invoices in the browser with your design, so you can check logo, colours and texts without creating an invoice. With `quittio.preview.enabled` (default: only in the `local` environment), the provider registers two routes under `quittio.preview.path`, with the middleware `quittio.preview.middleware`: | Route | Name | Response | |---|---|---| | `GET /_quittio/preview` | `quittio.preview.index` | List of the sample scenarios with HTML and PDF links | | `GET /_quittio/preview/{scenario}` | `quittio.preview.show` | The scenario as HTML, with `?format=pdf` as inline PDF | Query parameters of `show`: - `format`: `pdf`; anything else means HTML. - `template`: one of the views that rendering accepts (see [Blade templates](#blade-templates)): `default`, `pdf.template`, the shipped views under `quittio::templates.` and the views of `pdf.templates`, never partials. The view must exist. - `locale`: a locale code such as `en` or `de_CH`. Unknown scenarios, other or missing templates and invalid locales return 404, so no request can name a file or an arbitrary view. The preview renders the [sample scenarios](testing.md#scenarios) with the configured design, validation mode `warn` and a watermark ("PREVIEW", in German "VORSCHAU"). The locale defaults to the scenario's locale. The routes are not registered while routes are cached (`php artisan route:cache`). Requests are limited to `quittio.preview.throttle` (default 30 per minute; `null` = no limit). Outside the `local` environment every request is refused with 403 unless the gate `viewQuittioPreview` allows it. Define it, like Telescope's gate, in a service provider of your application: ```php use Illuminate\Support\Facades\Gate; Gate::define('viewQuittioPreview', fn (User $user) => $user->is_admin); ``` Add `auth` to `quittio.preview.middleware` (`['web', 'auth']`), so that guests are sent to the login instead of getting 403. On the command line, use `php artisan quittio:preview` (see [Artisan commands](commands.md#quittiopreview)). `Quittio\Laravel\Support\Preview` is the service behind route and command: | Method | Description | |---|---| | `scenarios(): array` | Names of the sample scenarios | | `allowsTemplate(string $template): bool` | Whether the preview may render this template: an existing view that rendering accepts (`BladeTemplateEngine::fromSettings()`), or `default` | | `allowsLocale(string $locale): bool` | Whether the locale code is well-formed | | `render(string $scenario, string $format = 'pdf', ?string $template = null, ?string $locale = null): RenderedInvoice` | Renders a scenario (`pdf` or `html`) with the watermark; invalid arguments throw an `InvalidArgumentException` | ## Extensions `Quittio::extend(string $type, string $name, Closure $factory)` registers a driver: your own PDF renderer, validator, XML writer or format resolver. - The factory receives the container and the driver's configuration array. - It must return an instance of the expected interface, otherwise building the core fails with a `ConfigurationException`. - Unknown types throw an `InvalidArgumentException`. - `extend()` forgets the built core, so call it in the `boot()` method of a service provider. | Type | Interface | Used when | Configuration passed | |---|---|---|---| | `renderer` | `Quittio\Core\Pdf\PdfRenderer` | `pdf.renderer` is `{name}` | `pdf.renderers.{name}` | | `validator` | `Quittio\Core\Validation\ValidationDriver` | `{name}` is listed in `validation.levels` | `validation.drivers.{name}` | | `serializer` | `Quittio\Core\Xml\XmlSerializer` | always; replaces the writer of its syntax | `serializers.{name}` | | `format_resolver` | `Quittio\Core\Formats\FormatResolver` | `format_resolver.driver` is `{name}` | `format_resolver` (including `peppol_lookup`) | ```php use Illuminate\Contracts\Container\Container; use Quittio\Core\Pdf\Renderers\CallbackRenderer; use Quittio\Laravel\Facades\Quittio; public function boot(): void { Quittio::extend('renderer', 'gotenberg', fn (Container $app, array $config) => new CallbackRenderer( fn (string $html) => $app->make(GotenbergClient::class)->pdf($html, $config), )); } // config/quittio.php: 'pdf' => ['renderer' => 'gotenberg', 'renderers' => ['gotenberg' => ['url' => '…']]] ``` - The callback of `Quittio\Core\Pdf\Renderers\CallbackRenderer` receives the HTML and the `RenderRequest` and returns any PDF. `CallbackRenderer` then turns it into PDF/A-3 and embeds the XML itself (design level 4 in [Templates and design](templates.md)). - A class implementing `PdfRenderer` directly receives the `RenderRequest` (`html`, `title`, `author`, `paper`, `pdfa`, `embedded`, `font`) and must produce the PDF/A and embed the file itself. - A validator driver returns its levels, decides with `supports()` which subjects it checks and returns a `ValidationResult` (see [Validation](validation.md)). ### Macros `QuittioManager` and `PendingInvoice` are macroable (`macro()`, `mixin()`, `hasMacro()`, `flushMacros()`). Use a macro for a combination of options you need often: ```php use Quittio\Laravel\Facades\Quittio; use Quittio\Laravel\Rendering\PendingInvoice; PendingInvoice::macro('forPublicBuyer', function () { /** @var PendingInvoice $this */ return $this->format('xrechnung-cii')->locale('de'); }); Quittio::make($invoice)->forPublicBuyer()->render(); ``` Builder macros are described in [Macros](building-invoices.md#macros). ## Peppol lookup Peppol is an international network for delivering e-invoices ([Formats](concepts.md#formats)). The default format resolver chooses the format `peppol` only with a lookup that tells whether a buyer is reachable via Peppol (see [Formats and profiles](formats.md#peppol-lookup)). Quittio ships no Peppol directory client, so you provide the lookup. Name an invokable class in `format_resolver.peppol_lookup`. It is resolved from the container, receives the `BuyerParty` and returns whether the buyer is reachable via Peppol: ```php use Illuminate\Support\Facades\Cache; use Quittio\Core\Data\Party\BuyerParty; final class PeppolDirectoryLookup { public function __construct(private PeppolDirectory $directory) {} public function __invoke(BuyerParty $buyer): bool { $address = $buyer->electronicAddress; // BT-49 if ($address === null) { return false; } $participant = $address->scheme->value.':'.$address->value; // e.g. 0208:0123456749 return Cache::remember('peppol:'.$participant, now()->addDay(), fn (): bool => $this->directory->isRegistered($participant)); } } // config/quittio.php: 'format_resolver' => ['peppol_lookup' => PeppolDirectoryLookup::class, …] ``` The lookup runs for every automatic choice with a business buyer that no domestic country rule decided (for German sellers: cross-border invoices), so cache its answers. A resolver registered with `Quittio::extend('format_resolver', …)` receives the value in its configuration and decides itself whether to use it. ## Logging The manager passes Laravel's logger (`Psr\Log\LoggerInterface`, the default log channel) to the core. The logger receives the warnings of the mandatory field guard in mode `log` (`validation.mandatory_field_guard`; the default in production). To route them elsewhere, listen for the `MandatoryFieldMissing` event, which is dispatched in the modes `log` and `exception`. ## Country packs Country packs bring the rules, tax presets and texts of a country. They are discovered from Composer metadata, listed in `quittio.countries` or registered with `Quittio::registerCountry()` (see [Configuration](configuration.md#country-packs)). Their translations, profiles, formats and rules are registered with them. Available packs and writing your own: [Country packs](country-packs.md). ## Managed layer `quittio/managed` stores invoices in the database and builds on this package: - It renders through the same `QuittioManager`, so configuration, templates, events and `Quittio::fake()` apply. - It reads `pdf.template` and `pdf.templates` to check issuer branding. - It uses `corrections.cancellation_strategy`. - It adds commands and implements two contracts of this package: | Contract | Used by | |---|---| | `Quittio\Laravel\Contracts\ManagedInstaller` | `php artisan quittio:install --managed` (migrations, first issuer profile) | | `Quittio\Laravel\Contracts\ManagedInvoiceRenderer` | `php artisan quittio:render {invoice}` (render a stored invoice again) | Issuer profiles replace the `seller` section, and the Managed archive uses its own disk and queue settings (`config/quittio-managed.php`). See [Managed layer](managed.md). ## Reference The tables below list the methods by purpose. The API reference has every public class and method with its parameters, generated from the code of this version. ### Facade ```php use Quittio\Laravel\Facades\Quittio; ``` Instead of the facade you can inject `Quittio\Laravel\QuittioManager`; both are the same singleton. | Method | Returns | Description | |---|---|---| | `make(InvoiceData\|ProvidesInvoiceData\|InvoiceBuilder $data)` | `PendingInvoice` | Starts a render call. A builder is built, a `ProvidesInvoiceData` model converted with `toInvoiceData()`. See [Rendering invoices](#rendering-invoices). | | `invoice()` | `RenderableInvoiceBuilder` | A new invoice with the seller of the configuration (or of the current issuer of the Managed layer); ends with `build()` or `render()` (see [Starting invoices](#starting-invoices)). | | `quote()`, `orderConfirmation()`, `deliveryNote()`, `proforma()`, `paymentReminder(int $level = 0)`, `accountStatement()` | `RenderableDocumentBuilder` | A new document with the seller of the configuration; ends with `build()` or `render()`. | | `document(DocumentData\|DocumentBuilder $document)` | `PendingDocument` | Starts a render call for a quote, order confirmation, delivery note, proforma invoice, payment reminder or account statement (see [Documents](#documents)). | | `cancellation(InvoiceData $original, string $number, ?string $reason = null, ?string $strategy = null, ?Date $issueDate = null, ?array $lines = null, ?CancellationHistory $history = null, ?string $title = null)` | `InvoiceBuilder` | Builder for the cancellation of an issued invoice; render it with `make()`. `$title` is the document title in the PDF, e.g. "Stornorechnung" (cancellation invoice); `null` = the title of the country pack's correction policy. See [Cancellations](#cancellations). | | `check(InvoiceData $invoice, RenderOptions $options)` | `ValidationResult` | Validates like a render run; the fluent form is `Quittio::make($invoice)->check()`. | | `prepare(InvoiceData $invoice, RenderOptions $options)` | `PreparedInvoice` | Calculates and validates without rendering; fluent form `->prepare()`. | | `read(string $content)` | `ReadResult` | Reads an incoming XML or PDF e-invoice from its content (bytes, never a path). See [Reading incoming invoices](reading.md). | | `readFile(string $path)` | `ReadResult` | The same from a file | | `validateFile(string $content)` | `ValidationResult` | Reads and validates an incoming XML or PDF (content) with all configured levels. See [Validation](validation.md#incoming-files). | | `validatePath(string $path)` | `ValidationResult` | The same from a file | | `renderInvoice(InvoiceData $invoice, RenderOptions $options)` | `RenderedInvoice` | The render run behind `PendingInvoice::render()`, with events | | `renderDocument(DocumentData $document, RenderOptions $options)` | `RenderedDocument` | The render run behind `PendingDocument::render()` | | `extend(string $type, string $name, Closure $factory)` | `QuittioManager` | Registers a renderer, validator, serializer or format resolver (see [Extensions](#extensions)) | | `registerCountry(CountryPack $pack)` | `QuittioManager` | Registers a country pack; it wins over configured and discovered packs with the same code | | `defaultVat(?string $sellerCountry, bool $smallBusiness)` | `?TaxPreset` | Default VAT of lines without `->vat()`: `defaults.vat`, otherwise the standard preset of the seller's country pack, for small businesses its small business preset ([Default VAT](building-invoices.md#default-vat)). The service provider installs it with `InvoiceBuilder::useDefaultVat()`. | | `countries()` | `CountryRegistry` | Active country packs: `all()`, `has($code)`, `get($code)`, `getOrGeneric($code)` | | `profiles()` | `ProfileRegistry` | Profiles of the core and the country packs: `all()`, `has($key)`, `get($key)`, `bySpecificationId($id)` | | `formats()` | `FormatRegistry` | Output formats: `all()`, `has($key)`, `get($key)` | | `settings()` | `Settings` | Typed access to `config('quittio')` (see [Settings](#settings)) | | `flush()` | `QuittioManager` | Forgets the built core; the next call builds it again. Configuration changes do not need it | | `core()` | `Quittio\Core\Quittio` | The core instance, e.g. `Quittio::core()->config()` or `->validators()` | | `coreWithLevels(array $levels)` | `Quittio\Core\Quittio` | A new core (the cached one stays unchanged) that validates with `validation.levels` plus `$levels`, e.g. `['kosit', 'verapdf']`, every driver with its settings of `validation.drivers.*`; used by `quittio:validate --kosit --verapdf` | | `macro()`, `mixin()`, `hasMacro()`, `flushMacros()` | – | Macros of the manager (see [Macros](#macros)) | | `fake()` | `QuittioFake` | Replaces rendering in tests (see [Testing](testing.md#fake)) | | `assertRendered()`, `assertRenderedCount()`, `assertNothingRendered()`, `assertDocumentRendered()`, `rendered()`, `documents()` | – | Assertions and recordings of the fake, only after `fake()` | The manager builds the core lazily, on the first call that needs it. It uses the configuration, the country packs, the Blade template engine, the PDF renderer, the logger and your extensions. ### Settings `Quittio::settings()` returns `Quittio\Laravel\Support\Settings`: typed read access to `config('quittio')` with dot notation. Wrong types throw a `ConfigurationException` naming the key. | Method | Returns | |---|---| | `get(string $key, mixed $default = null)` | Raw value | | `string(string $key, string $default)` | String; `null` and `''` give the default | | `nullableString(string $key)` | String or `null` | | `bool(string $key, bool $default)` | Boolean | | `int(string $key, int $default)` | Integer | | `array(string $key)` | Array (missing = `[]`) | | `strings(string $key)` | The string values of a list | | `section(string $key)` | `Settings` for a sub-array, e.g. `section('validation.drivers.kosit')` | ### Service provider The package registers itself through Laravel's package discovery (`composer.json`: `extra.laravel`): the provider `Quittio\Laravel\QuittioServiceProvider` and the alias `Quittio` for `Quittio\Laravel\Facades\Quittio`. With `"dont-discover": ["quittio/laravel"]` in your `composer.json`, register both yourself in `bootstrap/providers.php` and `config/app.php`. | Registered | Details | |---|---| | Configuration | `config/quittio.php` merged with the package defaults (see [Configuration](configuration.md)) | | `Quittio\Laravel\QuittioManager` | Singleton behind the facade, also available as `app('quittio')` | | `Quittio\Laravel\Contracts\InvoiceDefaults` | Bound to `ConfigInvoiceDefaults` unless already bound (see [Seller from the configuration](#seller-from-the-configuration)) | | `Quittio\Laravel\Support\Preview` | Singleton that renders the sample scenarios (see [Preview route](#preview-route)) | | `Quittio\Core\Serialization\Snapshots\SnapshotUpcasterChain` | Singleton with the upcasters of `quittio.snapshots.upcasters`; also the default chain of `InvoiceData::fromSnapshot()` | | Default VAT | `InvoiceBuilder::useDefaultVat()` with `Quittio::defaultVat()` (see [Default VAT](building-invoices.md#default-vat)) | | Views | Namespace `quittio` (`quittio::templates.default.layout`, its partials, `quittio::preview.index`) | | Translations | Namespace `quittio` with the labels and messages of the core; the core uses Laravel's translator (see [Translations](#translations)) | | Routes | The preview routes, when enabled | | Commands | `quittio:*` and `make:quittio-template` (console only, see [Artisan commands](commands.md)) | | Publish tags | `quittio-config`, `quittio-views`, `quittio-lang`, `quittio-fonts` (see [Artisan commands](commands.md#publish-tags)) | | `php artisan about` | Section "Quittio" with the active country packs and profiles | # Configuration This page describes `config/quittio.php`, the configuration file of the Laravel integration. When you install Quittio, read [The keys you set first](#the-keys-you-set-first): your company, your bank account and the defaults most applications change. The rest of the page is the reference of every key, in the order of the file. The terms used here (format, profile, BT numbers, rule codes) are explained in [E-invoicing basics](concepts.md). ## The keys you set first Publish the file once: ```bash php artisan quittio:install # config/quittio.php, with hints for the next steps php artisan vendor:publish --tag=quittio-config # the same file, without the hints ``` The options of the installer are described in [Artisan commands](commands.md#quittioinstall). ### Your company Every invoice names the company that issues it: the **seller**. `Quittio::invoice()` takes the seller from the `seller` section, so you set it once: ```php // config/quittio.php 'seller' => [ 'name' => env('QUITTIO_SELLER_NAME'), // legal name of the company 'vat_id' => env('QUITTIO_SELLER_VAT_ID'), // VAT identification number, e.g. DE123456789 'tax_number' => env('QUITTIO_SELLER_TAX_NUMBER'), // tax number from the tax office, e.g. 21/815/08150 'legal_registration' => 'HRB 12345', // commercial register number 'address' => [ 'line1' => 'Hauptstraße 1', // street and number 'line2' => null, 'postcode' => '12345', 'city' => 'Musterstadt', 'country' => 'DE', ], 'contact' => ['name' => 'Max Muster', 'phone' => '+49 30 1234567', 'email' => 'billing@muster.de'], 'electronic_address' => ['value' => 'billing@muster.de', 'scheme' => 'EM'], 'bank_accounts' => [ ['iban' => 'DE89370400440532013000', 'bic' => 'COBADEFFXXX', 'name' => 'Muster GmbH'], ], 'small_business' => (bool) env('QUITTIO_SELLER_SMALL_BUSINESS', false), // identifiers and additional_legal_info: see the reference below ], ``` Name, VAT identification number, tax number and the small business setting can also come from the environment: ```bash # .env QUITTIO_SELLER_NAME="Muster GmbH" QUITTIO_SELLER_VAT_ID=DE123456789 QUITTIO_SELLER_TAX_NUMBER=21/815/08150 QUITTIO_SELLER_SMALL_BUSINESS=false ``` The rules of the formats require some of these values: - Every invoice needs the name. German invoices also need street, postcode and city. - German invoices need the VAT identification number or the tax number. - Without a VAT identification number, EN 16931 (the European standard for e-invoices) needs another identifier of the seller: `identifiers` (BT-29) or `legal_registration` (BT-30). Otherwise rendering fails with the rule BR-CO-26. This concerns, for example, a small business with a tax number only. - XRechnung (the format for German public authorities) also needs the contact with name, phone and e-mail, and the electronic address. Peppol (an international network for delivering e-invoices) needs the electronic address as well. BT numbers are explained in [EN 16931 and business terms](concepts.md#en-16931-and-business-terms), rule codes such as BR-CO-26 in [Rules and validation](concepts.md#rules-and-validation). Every key of the section is listed in [Seller](#seller). ### Your bank account `Quittio::invoice()` puts the first entry of `seller.bank_accounts` that has an IBAN on every invoice, as payment by SEPA credit transfer. The PDF shows the IBAN and a payment QR code that banking apps can read (GiroCode in Germany). `bic` and `name` are optional. Further entries are ignored. ### Small businesses Set `seller.small_business` to `true` if you are a small business that charges no VAT (Kleinunternehmer, section 19 of the German VAT Act UStG). Invoices then carry no VAT. Lines without `->vat()` get the small business exemption with its legal note, and VAT on a line is rejected. See [Small businesses](country-packs.md#small-businesses). ### Defaults you may change The `defaults` section sets what an invoice uses when it does not set a value itself. These are the ones applications change most often: | Key | Default | Change it when | |---|---|---| | `defaults.due_days` | `14` | Your invoices are due after another number of days. `null` = no due date unless the invoice sets one. | | `defaults.vat` | `null` (19 % for a German seller) | Most of your lines have another VAT rate, e.g. `'de.reduced'` (7 %) for a bookshop | | `defaults.format` | `null` (automatic choice) | All your invoices need the same format, e.g. `'xrechnung-cii'` | | `defaults.locale` | `null` (German for a German seller) | Your invoices should be in another language, e.g. `'en'` | All keys of the section are described in [Defaults](#defaults). ### Design The `pdf` section sets the look of the PDF: logo, colours, font and texts. For example `'logo' => resource_path('images/logo.png')` and `'colors' => ['primary' => '#0f766e', …]`. See [PDF and design](#pdf-and-design) and [Templates and design](templates.md). Check the design with the sample invoices. They use sample data, not your seller: ```bash php artisan quittio:preview --scenario=simple ``` Then render a first invoice with your own data ([Getting started](getting-started.md#building-an-invoice)). ## How the configuration is read Without a published file, the package defaults apply. Laravel merges the package file with yours by top-level key. A section you leave out (for example `preview`) keeps the package defaults. Inside a section you keep, a missing key gets the value Quittio uses when nothing is set. For most keys that is the default listed on this page. These keys differ: | Key left out | Value | |---|---| | `defaults.country` | none: the seller's country decides; without it the only installed country pack, else the generic EN 16931 rules | | `defaults.due_days` | none: no due date unless the invoice sets one | | `pdf.cache_path` | `sys_get_temp_dir().'/quittio-mpdf'` | | `storage.disk` | the default disk of `config/filesystems.php` | | `queue.queue` | the default queue of the connection | The manager (a singleton) builds the core from the configuration when it is used for the first time and keeps it as long as the `quittio` configuration stays the same. A value changed at runtime (in tests, or per tenant) makes the next call build the core again, without further steps: ```php config()->set('quittio.pdf.colors.primary', '#0f766e'); Quittio::make($invoice)->render(); // uses the new colour ``` Under Octane the manager reads the configuration of the current request, not that of the worker's first request. `Quittio::flush()` still forgets the built core, e.g. after a bound class changed. Some values are process-wide rather than per application: the translator bridge, the default VAT for lines without VAT, the snapshot upcasters and the registered base rates of the German pack. `Quittio\Core\Quittio::resetState()` forgets them; the service providers set them again when the application boots. The test trait `InteractsWithQuittio` calls it after each test ([Testing](testing.md#assertions)). Some keys are read at other times: - `seller`, `defaults.vat`, `storage.disk` and `queue` are read on every call. - `preview` and `snapshots.upcasters` are read when the application boots. A value of the wrong type fails with a `ConfigurationException` that names the key, e.g. `quittio.pdf.pdfa must be a boolean.` The Managed layer has its own file, `config/quittio-managed.php`, with the database, issuers, archive, queue, numbering, corrections, delivery, audit log, retention and policies. Its keys are described in the [Managed layer](managed.md). Issuer profiles of the Managed layer replace the `seller` section below. ## Environment variables | Variable | Key | Default | |---|---|---| | `QUITTIO_DEFAULT_COUNTRY` | `defaults.country` | – (the only installed country pack) | | `QUITTIO_SELLER_NAME` | `seller.name` | – | | `QUITTIO_SELLER_VAT_ID` | `seller.vat_id` | – | | `QUITTIO_SELLER_TAX_NUMBER` | `seller.tax_number` | – | | `QUITTIO_SELLER_SMALL_BUSINESS` | `seller.small_business` | `false` | | `QUITTIO_DEFAULT_VAT` | `defaults.vat` | – (standard rate of the seller's country) | | `QUITTIO_VALIDATION_MODE` | `validation.mode` | `error` | | `QUITTIO_VALIDATION_LOCALE` | `validation.locale` | – (the language of the application) | | `QUITTIO_MANDATORY_FIELD_GUARD` | `validation.mandatory_field_guard` | – (`exception`, in production `log`) | | `QUITTIO_KOSIT_URL` | `validation.drivers.kosit.url` | `http://127.0.0.1:8081` (start the validator with `-P 8081`; its own default port is 8080) | | `QUITTIO_CANCELLATION_STRATEGY` | `corrections.cancellation_strategy` | – (country pack) | | `QUITTIO_DECLARED_AMOUNTS` | `calculation.declared_amounts` | `keep` | | `QUITTIO_STORAGE_DISK` | `storage.disk` | `local` | | `QUITTIO_PREVIEW_ENABLED` | `preview.enabled` | – (local environment only) | `QUITTIO_PREVIEW_ENABLED` must be `true` or `false`. Other values count as not set. Quittio 0.1.0 used shorter names for some variables: `QUITTIO_COUNTRY`, `QUITTIO_VALIDATION`, `QUITTIO_GUARD`, `QUITTIO_DISK` and `QUITTIO_PREVIEW`. They are still read when the new name is not set, until 1.0. ## Defaults Values used when an invoice or a render call does not set them itself. | Key | Type | Default | Effect | |---|---|---|---| | `defaults.country` | string\|null | `env('QUITTIO_DEFAULT_COUNTRY')` | Country pack used when the seller has no country code (BT-40), e.g. `'DE'`. Normally the seller's country decides; `->country()` on a render call wins as well. `null` = the only installed country pack if there is exactly one (`quittio/quittio` installs Germany). Without an installed pack for the code, only the generic EN 16931 rules apply. | | `defaults.profile` | string\|null | `null` | Profile of the e-invoice XML, i.e. how much of EN 16931 it contains ([Profiles](concepts.md#profiles)), e.g. `'en16931'`, `'extended'`, `'xrechnung'`. It applies to every format that allows it, also to `->format('zugferd')` and to formats chosen by a country rule. Formats that do not allow it ignore it (e.g. `basic` for XRechnung or Peppol). `null` = the profile of the automatic choice or the format's default (`zugferd`, `cii`, `ubl`: `en16931`; `xrechnung-cii`, `xrechnung-ubl`: `xrechnung`; `peppol`: `peppol`). `->profile()` on a render call always wins. See [Formats and profiles](formats.md). | | `defaults.format` | string\|null | `null` | Format of a render call without `->format()`. `null` or `'auto'` = automatic choice by the format resolver: XRechnung for public buyers with a Leitweg-ID (the routing ID of a German public buyer, see [German terms](concepts.md#german-terms)), PDF for consumers, ZUGFeRD for businesses ([Automatic choice](formats.md#automatic-choice)). Keys: `zugferd` (PDF with embedded XML; aliases `factur-x`, `facturx`), `xrechnung-cii` (alias `xrechnung`), `xrechnung-ubl`, `cii`, `ubl`, `peppol`, `pdf`, `html`, `json`. | | `defaults.locale` | string\|null | `null` | Language of the invoice (labels, generated texts, number and date formats), e.g. `'de'` or `'en'`, when neither `->locale()` on the render call nor the invoice (`InvoiceBuilder::locale()`) sets one. `null` = the default of the seller's country pack (Germany: `de`). | | `defaults.due_days` | int\|numeric string\|null | `14` | Due date (BT-9) = issue date + this number of days. It applies to invoices that set neither a due date nor payment terms (`->dueInDays()`), never to credit notes. `null` or `''` switches it off. Other values fail with a `ConfigurationException`. | | `snapshots.document_upcasters` | list | `[]` | The same for stored documents: `DocumentData::fromSnapshot()` and queued `RenderDocument` jobs. Documents have their own versions (`DocumentData::SNAPSHOT_VERSION`). | | `defaults.vat` | string\|null | `env('QUITTIO_DEFAULT_VAT')` | Tax preset of invoice lines without `->vat()`, by key, e.g. `'de.reduced'` for a bookshop. `null` = the standard rate of the seller's country pack (Germany: `de.standard`). Invoices of small businesses (`seller.small_business`) use the small business preset of the pack (`de.small_business`). An unknown key fails with a `ConfigurationException` when a line needs the default. See [Default VAT](building-invoices.md#default-vat). | ## Country packs A country pack brings the tax presets, legal rules and texts of one country ([How Quittio works](concepts.md#how-quittio-works)). `quittio/quittio` includes the pack for Germany. ```php 'countries' => [ // 'AT' => \App\Quittio\CountryAT::class, ], ``` Country packs are found in three ways, and all of them are combined: 1. **Package discovery**: a Composer package (or your root `composer.json`) lists its packs under `extra.quittio.country-packs`. `quittio/country-de` does this, so `composer require quittio/country-de` is enough. ```json "extra": { "quittio": { "country-packs": ["App\\Quittio\\CountryAT"] } } ``` The result is kept in `bootstrap/cache/quittio-countries.php` until Composer rewrites `vendor/composer/installed.json` or your `composer.json` changes, so requests do not read the package list. After editing `extra.quittio.country-packs` of the root `composer.json` by hand, the next request picks it up as well. 2. **Configuration**: `countries` lists further pack classes. The array keys are ignored; the pack's `code()` counts. Classes are resolved through the container. 3. **At runtime**: `Quittio::registerCountry(new CountryAT)`, e.g. in a service provider. It wins over the other two for the same country code and survives `Quittio::flush()`. Every pack must implement `Quittio\Core\Countries\CountryPack`, otherwise building the core fails with a `ConfigurationException`. For the same country code the last one wins, in this order: discovered packs, then `countries`, then `Quittio::registerCountry()`. So a subclass of `CountryDE` listed in `countries` replaces the discovered `CountryDE`. `php artisan quittio:countries` and `php artisan about` list the active packs. To write your own pack, see [Country packs](country-packs.md). ### Country pack Germany (`config/quittio-de.php`) The German pack has a small configuration of its own. Publish it with `php artisan vendor:publish --tag=quittio-de-config`. It holds the base rate (Basiszinssatz) under section 247 of the German Civil Code (BGB). `DefaultInterest` needs it to calculate the statutory interest on late payments, e.g. for payment reminders ([German default interest](documents.md#german-default-interest)). | Key | Type | Default | Effect | |---|---|---|---| | `base_rates` | array | `[]` | Base rates in addition to the table shipped with Quittio: ISO date (1 January or 1 July) => percent, e.g. `['2027-01-01' => '1.77']`. They win over the file below. | | `base_rate_file` | string\|null | `storage_path('app/quittio/base-rates.json')` | JSON file written by `php artisan quittio:base-rate:update` with the rates of the Deutsche Bundesbank. `null` = not used. | See [Country packs: base rate table](country-packs.md#base-rate-table) and [quittio:base-rate:update](commands.md#quittiobase-rateupdate). ## Seller Your company as it appears on every invoice. The section is used by `Quittio::invoice()`, the document methods (`Quittio::quote()` …), `Seller::fromConfig()` and `Seller::apply()` (see [Laravel integration](laravel.md#seller-from-the-configuration)). With `quittio/managed`, issuer profiles replace it. | Key | Type | Default | Effect | |---|---|---|---| | `seller.name` | string | `env('QUITTIO_SELLER_NAME')` | Legal name of the company (BT-27). Required. | | `seller.vat_id` | string\|null | `env('QUITTIO_SELLER_VAT_ID')` | VAT identification number with country prefix, e.g. `'DE123456789'` (BT-31) | | `seller.tax_number` | string\|null | `env('QUITTIO_SELLER_TAX_NUMBER')` | Tax number from the tax office, e.g. `'21/815/08150'` (BT-32). German invoices need the VAT identification number or the tax number. | | `seller.legal_registration` | string\|null | `null` | Commercial register number, e.g. `'HRB 12345'` (legal registration identifier, BT-30) | | `seller.identifiers` | list | `[]` | Further identifiers of the company (BT-29): `['value' => '…', 'scheme' => null]` (the scheme is optional) or plain strings, e.g. `[['value' => '4000001000005', 'scheme' => '0088']]` for a GLN (Global Location Number; `0088` is its code in the ISO 6523 list). Empty values are skipped. Without a VAT identification number, EN 16931 needs an identifier here or `legal_registration`, otherwise rendering fails with BR-CO-26. | | `seller.additional_legal_info` | string\|null | `null` | Further legal information printed on the invoice (BT-33), e.g. the court of registration and the managing directors | | `seller.address.line1` | string\|null | `null` | Street and number (BT-35). Required on German invoices. | | `seller.address.line2` | string\|null | `null` | Additional address line (BT-36) | | `seller.address.postcode` | string\|null | `null` | Postcode (BT-38). Required on German invoices. | | `seller.address.city` | string\|null | `null` | City (BT-37). Required on German invoices. | | `seller.address.country` | string\|null | `null` | Country code, ISO 3166-1 alpha-2 (BT-40), e.g. `'DE'`. It also selects the leading country pack. `null` = `defaults.country`, without it the code of the only installed country pack. | | `seller.contact.name` / `.phone` / `.email` | string\|null | `null` | Contact person (BG-6), added only when at least one value is set. XRechnung requires name, phone and e-mail. | | `seller.electronic_address.value` | string\|null | `null` | Electronic address of the seller (BT-34), added only when set. Required by XRechnung and Peppol. | | `seller.electronic_address.scheme` | string | `'EM'` | Scheme of the electronic address, a code of the EAS list (electronic address schemes): `EM` = e-mail address, `9930` = German VAT number (as Peppol participant ID). Peppol does not accept `EM` (`PEPPOL-EN16931-CL008`); set a participant ID such as `9930` when you send via Peppol | | `seller.bank_accounts` | list | `[['iban' => null, 'bic' => null, 'name' => null]]` | Bank accounts. `Quittio::invoice()` (`Seller::apply()`) adds a SEPA credit transfer with the first entry whose `iban` is not empty: the PDF shows the IBAN and a payment QR code. `bic` and `name` are optional. The other entries are ignored. | | `seller.small_business` | bool | `(bool) env('QUITTIO_SELLER_SMALL_BUSINESS', false)` | `Quittio::invoice()` (`Seller::apply()`) marks the invoice as an invoice of a small business (Germany: section 19 UStG, `Meta::FLAG_SMALL_BUSINESS`). Lines without `->vat()` get the small business preset of the country pack (Germany: `VatPreset::SmallBusiness`) with its legal note. Country packs reject VAT on the invoice; Germany also rejects the intra-community supply exemption, except for new vehicles. | ## PDF and design The look of the PDF and HTML output. The design of a single invoice and `->presentation()` on a render call override these values property by property. | Key | Type | Default | Effect | |---|---|---|---| | `pdf.renderer` | string | `'mpdf'` | PDF engine: `mpdf` (bundled) or a name registered with `Quittio::extend('renderer', …)`. An unknown name fails with a `ConfigurationException`. | | `pdf.template` | string | `'quittio::templates.default.layout'` | Blade view of the invoice layout. It is used for the template name `default`, i.e. for every render without `->template()` or `Presentation::$template`. Create your own copy of the default template with `php artisan make:quittio-template`, or name your own view. | | `pdf.templates` | list | `['invoices.*']` | Further views that `->template()`, `Presentation::$template` or issuer branding may select, besides `pdf.template` and the shipped `quittio::templates.*` views. `prefix*` allows every view below a prefix (`invoices.*`, `mytheme::*`). Partials (`.partials.`) are never selectable. Views not listed are refused. A missing key means `['invoices.*']`, `[]` allows no further views. See [Blade templates](laravel.md#blade-templates). | | `pdf.layout` | string\|null | `null` | Letter layout, i.e. the position of the address field: `free` (no norm), with the German pack also `din5008a` and `din5008b` (DIN 5008, the German standard for business letters). `null` = the default of the country pack (Germany: `din5008b`). An unknown layout fails with a `ConfigurationException`. See [Letter layouts](templates.md#letter-layouts). | | `pdf.paper` | string\|null | `null` | Paper size, e.g. `A4`, `Letter`. `null` = the default of the country pack (Germany: A4). A size set here wins over the country default, also `A4`. The invoice presentation and `->presentation()` win over it. | | `pdf.pdfa` | bool | `true` | Create plain PDFs (format `pdf`, quotes, reminders and other documents) as PDF/A-3b, the PDF standard for archiving. PDFs with embedded XML (ZUGFeRD/Factur-X) are always PDF/A-3b. PDFs with a watermark (drafts, previews) never are. | | `pdf.font` | string | `'geist'` | Default font family (plain family name). Geist (Latin, Cyrillic) and Inter (also Greek) are bundled, see [Fonts](templates.md#fonts). | | `pdf.fonts` | array | `[]` | Additional TrueType fonts for mPDF: lower-case family => `['R' => 'Regular.ttf', 'B' => 'Bold.ttf', 'I' => 'Italic.ttf', 'BI' => 'BoldItalic.ttf']`, files relative to `pdf.font_directories` | | `pdf.font_directories` | list | `[]` | Directories mPDF searches for the font files of `pdf.fonts`. `resources/fonts/quittio` and its subdirectories are added after the configured directories when they exist, so fonts placed there need no entry here. (`--tag=quittio-fonts` publishes Geist and Inter there, to `resources/fonts/quittio/geist` and `…/inter`.) mPDF takes a file from the first directory that has it: these directories come before the bundled fonts and those of mPDF. | | `pdf.logo` | string\|null | `null` | Logo: absolute path, `http(s)` URL or data URI of a PNG, JPEG or SVG. At most 10 MB and 25 megapixels; URLs only from public hosts. For PDF/A, logos are flattened to an RGB PNG; SVG logos then need the PHP extension `imagick`. Details in [Templates and design](templates.md#logos). | | `pdf.colors.primary` | string | `'#2741d9'` | Primary colour (hex): issuer name, title, gross total or amount due | | `pdf.colors.text` | string | `'#15171c'` | Text colour (hex) | | `pdf.colors.muted` | string | `'#5f6470'` | Colour of secondary text (hex): labels, descriptions, sender line, footer | | `pdf.colors.secondary` | string\|null | `null` | Accent colour (hex), e.g. section titles and the mark of highlighted lines (`design.secondaryColor` in templates). `null` = the primary colour. | | `pdf.sender_line` | string\|null | `null` | Small sender line above the address field. `null` = generated from the seller. | | `pdf.footer_columns` | list | `[]` | Footer columns, each a list of lines: `[['Muster GmbH', 'Hauptstraße 1'], ['IBAN DE02 …']]`. `[]` = footer generated from the seller data. | | `pdf.texts.salutation` | string\|null | `null` | Salutation above the intro text. `null` = none. | | `pdf.texts.intro` | string\|null | `null` | Text above the lines (Markdown, PDF only). `null` = none. | | `pdf.texts.closing` | string\|null | `null` | Text below the totals (Markdown, PDF only). `null` = none. | | `pdf.show_item_numbers` | bool | `false` | Print the seller's item number (BT-155) next to the item name | | `pdf.markdown` | bool | `false` | Render line descriptions and notes as Markdown instead of plain text. It is off by default: the same texts are in the e-invoice XML, where formatting characters such as `*` or `_` stay visible and can change the displayed text. | | `pdf.fold_marks` | bool | `true` | Print fold and punch marks for window envelopes, at the left edge of the first page | | `pdf.payment_qr.type` | string | `'auto'` | Payment QR code in the PDF. `auto`: the generator below, otherwise the code of the country pack (Germany: GiroCode, the EPC QR code for banking apps). `girocode`: the EPC QR code. `swiss-qr-bill`: needs `pdf.payment_qr.generator`; Quittio ships no Swiss QR bill generator, so without one rendering fails with a `ConfigurationException`. `none`: no code. Unknown values fail with a `ConfigurationException`. Refunds and self-billed invoices never get a code. | | `pdf.payment_qr.generator` | class-string\|null | `null` | Class implementing `Quittio\Core\Pdf\Qr\PaymentQrGenerator`, resolved from the container. It is used for `auto` before the country pack's generator and required for `swiss-qr-bill`. Another class fails with a `ConfigurationException`. | | `pdf.payment_qr.girocode_amount` | string | `'due'` | Amount in the GiroCode: `due` (amount due) or `discounted` (amount after the first cash discount; the PDF then notes until when it is valid) | | `pdf.filename` | string | `'{title}_{number}.{ext}'` | File name pattern of the results. Placeholders: `{title}` (document title in the language of the invoice, e.g. "Rechnung", German for invoice), `{number}`, `{date}` (issue date, `Y-m-d`), `{format}` (format key; the document type for quotes and other documents), `{ext}` (`pdf`, `xml`, `html`, `json`). Title and number are reduced to letters, digits, `.`, `_` and `-`. | | `pdf.cache_path` | string\|null | `storage_path('framework/cache/quittio')` | Temporary directory of mPDF. Prepared logos are cached in its `logos` subdirectory. `null` = `sys_get_temp_dir().'/quittio-mpdf'`. | | `pdf.renderers.mpdf.simple_tables` | bool | `false` | mPDF `simpleTables`: drops the borders of table cells and is not faster. Keep it off. | | `pdf.renderers.mpdf.pdfa_auto` | bool | `false` | mPDF `PDFAauto`: let mPDF correct PDF/A errors itself instead of failing | | `pdf.renderers.{name}` | array | – | Options passed to the factory of a renderer registered with `Quittio::extend('renderer', '{name}', …)` | The design keys (`logo`, `colors`, `font`, `sender_line`, `layout`, `texts`, `footer_columns`, `show_item_numbers`, `payment_qr.type`, `paper`) are design level 1 of [Templates and design](templates.md). The invoice presentation and `->presentation()` override them property by property. ## Validation Every invoice is checked before it is created: against EN 16931, the rules of the format (XRechnung, Peppol) and those of the country (Germany: UStG). See [Rules and validation](concepts.md#rules-and-validation) for the basics and [Validation](validation.md) for the details. | Key | Type | Default | Effect | |---|---|---|---| | `validation.mode` | string | `env('QUITTIO_VALIDATION_MODE', 'error')` | What happens with errors. `error`: throw a `ValidationException`. `warn`: create the invoice anyway and attach the result (`$result->validation()`). `off`: no validation while rendering. Per call: `->validate()`, `->validate(false)`, `->withoutValidation()`. | | `validation.levels` | list | `['php', 'xsd']` | Checks that run while rendering and in `validateFile()`: `php` (rules of EN 16931, format and country; levels L1–L3 and L7), `xsd` (XML schema, L4), `kosit` (the official KoSIT validator for XRechnung and Peppol, needs Java; L5), `verapdf` (PDF/A check, needs veraPDF; L6), or the name of a driver registered with `Quittio::extend('validator', …)`. Without `php`, the PHP rules do not run. | | `validation.block_on_warnings` | bool | `false` | In mode `error`, warnings block like errors | | `validation.locale` | string\|null | `env('QUITTIO_VALIDATION_LOCALE')` | Language of the validation messages and hints. `null` = the language of the application (`app()->getLocale()`), `'invoice'` = the language of the invoice, or a locale such as `'de'`. The invoice itself keeps its language. | | `validation.mandatory_field_guard` | string\|null | `env('QUITTIO_MANDATORY_FIELD_GUARD')` | Check that the PDF shows every legally required value (the template could leave one out): `exception`, `log` or `off`. `null` = `exception` outside production, `log` in production. See [Mandatory field guard](validation.md#mandatory-field-guard). | | `validation.rules.disabled` | list | `[]` | Codes of PHP rules to switch off, e.g. `['DE-USTG-15']`; a trailing `*` matches by prefix. See [Switching rules off](validation.md#switching-rules-off-and-changing-their-severity). | | `validation.rules.severity` | array | `[]` | Severity of the findings per code or prefix: `error`, `warning` or `info`, e.g. `['DE-USTG-08' => 'warning']` | | `validation.rules.allow_official` | bool | `false` | `true` allows changing the official EN 16931 and Peppol rules (`BR-…`, `PEPPOL-…`) | | `validation.drivers.kosit.mode` | string | `'daemon'` | `daemon` (a running validator via HTTP, fast) or `cli` (one Java call per file) | | `validation.drivers.kosit.url` | string | `env('QUITTIO_KOSIT_URL', 'http://127.0.0.1:8081')` | Address of the validator daemon. The validator listens on port 8080 unless started with `-P 8081` (or mapped, `docker run -p 8081:8080`). `quittio:schemas:download` prints a start command for the port of this URL. | | `validation.drivers.kosit.jar` | string\|null | `null` | Validator JAR (mode `cli`) | | `validation.drivers.kosit.scenarios` | string\|null | `null` | Scenario file of the main configuration (mode `cli`, e.g. XRechnung) | | `validation.drivers.kosit.repository` | string\|null | `null` | Repository directory of the main configuration (mode `cli`) | | `validation.drivers.kosit.configurations` | list | `[]` | Further scenario sets `[['scenarios' => …, 'repository' => …]]` (mode `cli`), e.g. Peppol BIS. `quittio:schemas:download` prints the values. | | `validation.drivers.kosit.java` | string | `'java'` | Java binary (mode `cli`) | | `validation.drivers.kosit.timeout` | int | `30` | Timeout in seconds | | `validation.drivers.kosit.fail_on_unavailable` | bool | `true` | `true` = an unreachable validator is an error (`KOSIT-UNAVAILABLE`), `false` = a warning | | `validation.drivers.verapdf.mode` | string | `'cli'` | `cli` (local `verapdf` binary) or `docker` (Docker image; the PDF is passed via stdin) | | `validation.drivers.verapdf.binary` | string | `'verapdf'` | veraPDF binary (mode `cli`) | | `validation.drivers.verapdf.image` | string | `'verapdf/cli'` | Docker image (mode `docker`) | | `validation.drivers.verapdf.docker` | string | `'docker'` | Docker binary (mode `docker`) | | `validation.drivers.verapdf.timeout` | int | `120` | Timeout in seconds | | `validation.drivers.verapdf.fail_on_unavailable` | bool | `true` | `true` = an unavailable veraPDF is an error, `false` = a warning | | `validation.drivers.vies.enabled` | bool | `false` | Check VAT identification numbers online with VIES, the VAT number service of the EU (`QUITTIO-VIES`, `QUITTIO-VIES-UNAVAILABLE`). A `Quittio\Core\Validation\Plausibility\VatIdVerifier` bound in the container replaces the VIES client (e.g. in tests). | | `validation.drivers.vies.cache_ttl` | int | `86400` | Seconds a VALID or INVALID answer is cached (in memory, per process) | | `validation.drivers.vies.timeout` | int | `5` | Timeout in seconds | | `validation.drivers.{name}` | array | – | Options passed to the factory of a validator registered with `Quittio::extend('validator', '{name}', …)` | The KoSIT and veraPDF drivers run only when `kosit` and `verapdf` are listed in `validation.levels`. `quittio:validate --kosit --verapdf` adds them for one file, with the same settings. Levels, messages and the official validators: see [Validation](validation.md). ## Corrections | Key | Type | Default | Effect | |---|---|---|---| | `corrections.cancellation_strategy` | string\|null | `env('QUITTIO_CANCELLATION_STRATEGY')` | How cancellations (`$result->cancel()`, `Quittio::cancellation()` and Managed cancellations) are written. `credit_note`: type code 381 with positive amounts. `negative_invoice`: type code 380 with negative amounts. `null` = the default of the country pack (Germany: `credit_note`; both are permitted). The `strategy:` argument wins. An unknown value fails with a `ConfigurationException`. See [Cancellations and corrections](building-invoices.md#cancellations-and-corrections). | ## Calculation | Key | Type | Default | Effect | |---|---|---|---| | `calculation.declared_amounts` | string | `env('QUITTIO_DECLARED_AMOUNTS', 'keep')` | What happens with amounts another system declared and rounded differently, in invoices continued with `Quittio::edit()` or `InvoiceBuilder::from()` and in their cancellations. `keep`: keep them within the tolerances of EN 16931. `recalculate`: always recalculate. `reject`: throw a `CalculationException` when they are outside the tolerances. Replaced amounts are reported as `QUITTIO-DECLARED-AMOUNTS`. See [Declared amounts](building-invoices.md#declared-amounts). | | `calculation.calculator` | string\|null | `null` | Class of an own calculator (`Quittio\Core\Calculation\TotalsCalculator`), resolved from the container. It calculates renders, cancellations, documents, read invoices and `Quittio::edit()`. `null` = the EN 16931 calculator with `declared_amounts`. | ## Snapshots A snapshot is an invoice stored as JSON together with the version of Quittio's data model. Queued render jobs and the Managed layer store invoices this way. | Key | Type | Default | Effect | |---|---|---|---| | `snapshots.upcasters` | list | `[]` | Classes (resolved through the container) or instances implementing `Quittio\Core\Serialization\Snapshots\SnapshotUpcaster`. They bring stored snapshots of older data model versions up to date when they are read: `InvoiceData::fromSnapshot()`, queued `RenderInvoice` jobs and the invoices of the Managed layer. Only needed after an update that changes the data model. Other values fail with a `ConfigurationException`. | An upcaster reads one version and returns the next one: ```php use Quittio\Core\Serialization\Snapshots\SnapshotUpcaster; final class RenameReferenceField implements SnapshotUpcaster { public function from(): int { return 1; // produces version 2 } public function upcast(array $snapshot): array // without the "_version" key { $snapshot['buyerReference'] ??= $snapshot['reference'] ?? null; unset($snapshot['reference']); return $snapshot; } } ``` ## Reader Limits for reading incoming e-invoices, as protection against oversized files. They apply to `Quittio::read()`, `readFile()`, `validateFile()`, `validatePath()`, `quittio:read` and `quittio:validate`. See [Reading incoming invoices](reading.md#limits-and-security). | Key | Type | Default | Effect | |---|---|---|---| | `reader.max_xml_bytes` | int | `10 * 1024 * 1024` | Largest XML file (bytes) | | `reader.max_pdf_bytes` | int | `25 * 1024 * 1024` | Largest PDF file (bytes) | | `reader.max_decompressed_bytes` | int | `50 * 1024 * 1024` | Largest decompressed content of the PDF streams (bytes) | ## Format resolver The format resolver makes the automatic choice of the format when a render call names none ([Automatic choice](formats.md#automatic-choice)). | Key | Type | Default | Effect | |---|---|---|---| | `format_resolver.driver` | string | `'default'` | `default` (the built-in choice) or the name of a resolver registered with `Quittio::extend('format_resolver', …)`. The factory receives the whole `format_resolver` array. An unknown name fails with a `ConfigurationException`. | | `format_resolver.peppol_lookup` | class-string\|null | `null` | Peppol lookup of the `default` resolver: an invokable class, resolved from the container, that receives the `Quittio\Core\Data\Party\BuyerParty` and returns `true` when the buyer receives invoices via Peppol (an international delivery network for e-invoices). The automatic choice then picks `peppol` for business buyers that no domestic country rule decided. See [Peppol lookup](laravel.md#peppol-lookup). At runtime a callable works as well. Anything else fails with a `ConfigurationException`. `null` = no lookup, the resolver never picks `peppol`. | | `format_resolver.b2c_embed_xml` | bool | `false` | `true` = embed the e-invoice XML in CII syntax (`factur-x.xml`, profile from `defaults.profile`) also into plain PDFs (format `pdf`, e.g. the automatic choice for consumers). These PDFs are then PDF/A-3b. | | `format_resolver.auto_upgrade_profile` | bool | `true` | When an invoice uses EXTENDED features (e.g. sub lines) and no profile was requested, CII formats switch to the profile EXTENDED, which has more fields than EN 16931. `false` = fail with a `ConfigurationException` instead. | Published files of earlier versions may contain a `networks` section (mail, Peppol, French platform). Nothing reads it, so remove it. The Peppol lookup above replaces `networks.peppol`. ## Storage, queue and preview | Key | Type | Default | Effect | |---|---|---|---| | `storage.disk` | string\|null | `env('QUITTIO_STORAGE_DISK', 'local')` | Filesystem disk (`config/filesystems.php`, e.g. `local` or `s3`) of `->save()` on rendered results and of queued renders without a disk. `null` or `''` = the default disk of `config/filesystems.php`. The archive of the Managed layer uses `quittio-managed.disk`. | | `queue.connection` | string\|null | `null` | Queue connection of `->queue()`. `null` = the default connection. | | `queue.queue` | string\|null | `'quittio'` | Queue name of `->queue()`. Workers must listen on it: `php artisan queue:work --queue=quittio,default`. | | `queue.after_commit` | bool | `true` | Inside a database transaction the job is dispatched after the commit, and not at all after a rollback. `false` = at once. | | `preview.enabled` | bool\|null | `env('QUITTIO_PREVIEW_ENABLED')` | `true` = register the browser preview of the sample invoices, `false` = never. `null` = only in the `local` environment. Never registered while routes are cached (`route:cache`). | | `preview.path` | string | `'_quittio/preview'` | URL prefix of the preview | | `preview.middleware` | list | `['web']` | Middleware of the preview routes, e.g. `['web', 'auth']` when you enable it outside `local`. Outside `local` the gate `viewQuittioPreview` must also allow the request ([Preview route](laravel.md#preview-route)). | | `preview.throttle` | string\|null | `'30,1'` | Rate limit of the preview as `attempts,minutes` (Laravel's `throttle` middleware); `null` = none | See [Saving, downloading and mailing](laravel.md#saving-downloading-and-mailing), [Queueing](laravel.md#queueing) and [Preview route](laravel.md#preview-route). ## Serializer extensions `serializers.{name}` (not in the published file) holds the options passed to the factory of a serializer registered with `Quittio::extend('serializer', '{name}', …)`. Registered serializers are always used. They replace the built-in XML writer of their syntax (CII or UBL, see [Syntaxes](concepts.md#syntaxes-cii-and-ubl)). ## Without Laravel The core takes the same settings as a `Quittio\Core\Config` object or as an array for `Quittio::create()` (`Config::fromArray()`). Some keys are shaped differently there, e.g. `validation.levels` as `L1` … `L7`, `pdf.b2c_embed_xml`, and `format` instead of `defaults.format`. See [Getting started](getting-started.md#without-laravel), which maps every core key to its Laravel key. # Artisan commands This page lists every Artisan command of Quittio and every publish tag for `vendor:publish`. You need it when you install Quittio, check the invoice design, validate e-invoice files (locally or in CI) and schedule recurring tasks. The commands are grouped by purpose: [Setup](#setup), [Preview and testing](#preview-and-testing), [Maintenance](#maintenance) and [Package development](#package-development). Terms such as XRechnung, Peppol, profile or KoSIT are explained in [E-invoicing basics](concepts.md). Three packages register commands: - `quittio/laravel` registers the general commands (console only). - `quittio/managed` adds the commands for stored invoices and extends `quittio:install` and `quittio:render`. - `quittio/country-de` adds `quittio:base-rate:update`. Every command returns exit code `0` on success and `1` on failure. The failure cases are listed per command, so CI jobs and the scheduler can react to them. ## Overview | Group | Command | Package | Purpose | |---|---|---|---| | Setup | [`quittio:install`](#quittioinstall) | laravel | Publish the configuration, optionally templates and fonts; `--managed` installs the Managed layer | | Setup | [`quittio:countries`](#quittiocountries) | laravel | Show the installed country packs | | Setup | [`make:quittio-template`](#makequittio-template) | laravel | Create a template from the default template | | Setup | [`quittio:schemas:download`](#quittioschemasdownload) | laravel | Download the KoSIT validator and the XRechnung configuration, compile the Peppol rules | | Preview and testing | [`quittio:preview`](#quittiopreview) | laravel | Render a sample invoice with the configured design | | Preview and testing | [`quittio:render`](#quittiorender) | laravel, managed | Render an invoice from a JSON file, or render a stored invoice again | | Preview and testing | [`quittio:validate`](#quittiovalidate) | laravel | Validate an XML or PDF e-invoice and print a report | | Preview and testing | [`quittio:read`](#quittioread) | laravel | Read an e-invoice and show its content | | Preview and testing | [`quittio:benchmark`](#quittiobenchmark) | laravel | Measure rendering performance | | Maintenance | [`quittio:verify`](#quittioverify) | managed | Check the hashes of the archived documents | | Maintenance | [`quittio:drafts:prune`](#quittiodraftsprune) | managed | Delete left-behind cancellation and correction drafts | | Maintenance | [`quittio:retention:check`](#quittioretentioncheck) | managed | List (and delete) invoices past their retention period | | Maintenance | [`quittio:sequence:show`](#quittiosequenceshow) | managed | Show the number sequences | | Maintenance | [`quittio:sequence:set`](#quittiosequenceset) | managed | Increase a number sequence | | Maintenance | [`quittio:base-rate:update`](#quittiobase-rateupdate) | country-de | Fetch the German base rate (Basiszinssatz, section 247 of the German Civil Code BGB) from the Deutsche Bundesbank | | Package development | [`quittio:codelists:update`](#quittiocodelistsupdate) | laravel | Regenerate the code lists | `php artisan about` shows a section "Quittio" with the active country packs and profiles. Commands that ask for confirmation (`quittio:sequence:set`, `quittio:retention:check --delete`, `quittio:drafts:prune`) treat a run without interaction as "no" and exit with `1`. Runs without interaction are `--no-interaction`, the scheduler and CI; pass `--force` there. ## Setup Run these commands when you install Quittio or change its setup. ### quittio:install ```bash php artisan quittio:install {--views} {--fonts} {--managed} {--force} ``` | Option | Effect | |---|---| | `--views` | Also publish the Blade templates (`quittio-views`) | | `--fonts` | Also publish the fonts (`quittio-fonts`, to `resources/fonts/quittio/geist` and `resources/fonts/quittio/inter`). The renderer searches `resources/fonts/quittio` and its subdirectories automatically. | | `--managed` | Also install the Managed layer (needs `quittio/managed`) | | `--force` | Overwrite existing files | The command publishes `config/quittio.php` (tag `quittio-config`) and the selected tags. Then it prints the next steps: set the seller, install a country pack, run the preview. It exits with `1` when `--managed` is given without `quittio/managed`. With `--managed`, the Managed installer then: 1. publishes `config/quittio-managed.php` (`quittio-managed-config`; overwritten with `--force`), 2. publishes the migrations (`quittio-managed-migrations`) unless they were published before. It detects them by file name, because `vendor:publish` gives migrations new timestamps. 3. asks "Run the migrations now?" (default yes) and runs `migrate --force`. When you decline, it prints the next steps and ends with exit code `0`. 4. asks for the first issuer profile, if none exists: key (`default`), company name (required), street and number, postcode, city, country (`DE`), VAT identifier, tax number (only without VAT identifier), e-mail address for invoices (contact and electronic address), IBAN, BIC (only with IBAN) and time zone. The time zone defaults to `app.timezone`, or to `Europe/Berlin` for Germany when that is UTC. The profile becomes the default issuer. Without interaction (`-n`), it runs the migrations without asking and skips the issuer profile. The immutability triggers are not published; publish them separately (see [Managed layer](managed.md#immutability)). It exits with `1` when the migrations or the database fail. ### quittio:countries ```bash php artisan quittio:countries ``` Lists the active country packs: code, class, profiles, number of rules and format rules, and the defaults (currency, locale, profile). Without a country pack, it warns that only EN 16931 rules are checked (exit code `0`). See [Country packs](country-packs.md). ### make:quittio-template ```bash php artisan make:quittio-template {name} {--force} ``` | Argument / option | Effect | |---|---| | `name` | Template name: lower-case letters, digits, `-` and `_`, starting with a letter or digit | | `--force` | Overwrite an existing template | Copies the default template (`layout.blade.php` and `partials/`) to `resources/views/invoices/{name}` and points the layout to its own partials. Use the new template per call with `->template('invoices.{name}.layout')` (allowed by the default `pdf.templates`), or for every invoice with `pdf.template`. An invalid name, or an existing template without `--force`, exits with `1`. See [Templates and design](templates.md). ### quittio:schemas:download ```bash php artisan quittio:schemas:download {--path=} {--force} {--without-peppol} {--java=java} ``` Installs the official validators for local checks and CI: the KoSIT validator (the validator for XRechnung and EN 16931, published by KoSIT, the agency behind XRechnung) and the rules of Peppol BIS Billing. Quittio uses them for the validation level L5 ([KoSIT validator](validation.md#kosit-validator-l5)). | Option | Default | Effect | |---|---|---| | `--path` | `storage/quittio/validators` | Target directory | | `--force` | – | Download and compile again, also when the files exist | | `--without-peppol` | – | Skip compiling the Peppol BIS Billing 3.0 rules | | `--java` | `java` | Java binary for compiling the Peppol rules | It downloads into the target directory: | File | Content | |---|---| | `validator.jar` | KoSIT validator 1.6.3 (standalone) | | `xrechnung/` | XRechnung validator configuration (release `v2026-08-31`, XRechnung 3.0.2), with `scenarios.xml` | | `peppol/PEPPOL-EN16931-UBL.xsl` | Peppol BIS Billing 3.0 rules (Schematron, a rule language for XML, `v3.0.20`), compiled with SchXslt 1.10.1 and the Saxon of the validator JAR | Every download is checked against the SHA-256 checksum pinned in `KositArtifacts::SHA256`; a file that does not match is discarded and the command fails. Existing files are kept unless `--force` is given. The command needs the PHP `zip` extension. Compiling the Peppol rules and running the validator need Java 11 or newer. Afterwards it prints: - the configuration for `validation.drivers.kosit` in mode `cli` (`jar`, `scenarios`, `repository` and, with Peppol, `configurations`), - a reminder to add `kosit` to `validation.levels`, - the command that starts the validator as a daemon for the default mode `daemon`, on the port of `validation.drivers.kosit.url`. The default URL is `http://127.0.0.1:8081`, so the command contains `-P 8081`; the validator's own default port is 8080. ```bash java -jar '…/validators/validator.jar' -s '…/validators/xrechnung/scenarios.xml' -r '…/validators/xrechnung' \ -s '…/peppol-scenarios.xml' -r '…/validators' -D -P 8081 ``` Download and compile errors exit with `1`. See [Validation](validation.md#kosit-validator-l5) and [Official validators in CI](testing.md#official-validators-in-ci). ## Preview and testing Use these commands to check the design, to render invoices from files and to check e-invoice files, e.g. in CI or when a customer sends you an invoice. ### quittio:preview ```bash php artisan quittio:preview {--scenario=simple} {--template=} {--format=pdf} {--locale=} {--output=} ``` Renders a sample invoice with your design (logo, colours, texts, template) into a file. The sample invoices use sample data, not your seller. | Option | Default | Effect | |---|---|---| | `--scenario` | `simple` | Sample scenario (see [Scenarios](testing.md#scenarios)) | | `--template` | configured | A view that rendering accepts: `default`, the configured `pdf.template`, a view under `quittio::templates.` or one of `pdf.templates`, e.g. `invoices.acme.layout` from `make:quittio-template`. No partials; the view must exist. | | `--format` | `pdf` | `pdf` or `html` | | `--locale` | scenario locale (`de`) | Locale of the labels | | `--output` | `storage/app/quittio/preview/{scenario}.{format}` | Target file; missing directories are created | The command renders the scenario with the configured design, validation mode `warn` and the watermark "PREVIEW" ("VORSCHAU" in German), like the [preview route](laravel.md#preview-route). Unknown scenarios, formats, templates or invalid locales exit with `1`. ### quittio:render ```bash php artisan quittio:render {invoice?} {--json=} {--format=} {--profile=} {--output=} ``` | Argument / option | Effect | |---|---| | `invoice` | Number, UUID or id of a stored invoice of the Managed layer (see below). Takes precedence over `--json`. | | `--json` | JSON file with invoice data in the format of `InvoiceData::toJson()` (see [Getting started](getting-started.md#json)) | | `--format` | Output format; default: `defaults.format`, else the automatic choice | | `--profile` | Profile | | `--output` | Directory (an existing one or a path ending in `/`; the file gets `filename()`) or target file; missing directories are created; default: the current directory. A file that cannot be written fails the command | **From a JSON file.** With `--json`, the command renders like `Quittio::make($data)->format(…)->profile(…)->render()`, with the configured validation mode. It writes the main output (`contents()`): the PDF of `zugferd` and `pdf`, the XML of the XML formats. It prints the number, format, profile and path, and the findings when there are warnings. Validation errors are printed as a table and exit with `1`. A missing `--json`, an unreadable file and any other error exit with `1` as well. **A stored invoice (Managed layer).** With `invoice` (and `quittio/managed` installed), the command renders an issued invoice whose documents are missing or failed: - It looks up a UUID first, then the number (a number used by several issuers must be given as UUID), then the numeric id. - When the current document version is complete, it reports that and exits with `0`. - Missing files are rendered as a new document version; existing files are never overwritten. The command lists the missing files and the new version. - Damaged files (hash mismatch) dispatch `ArchiveIntegrityViolation` and exit with `1`. Drafts and unknown invoices exit with `1` as well. Without `quittio/managed`, `invoice` exits with `1`. See [Rendering and archive](managed.md#rendering-and-archive). ### quittio:validate ```bash php artisan quittio:validate {file} {--profile=} {--kosit} {--verapdf} {--json} ``` Checks an e-invoice file against the rules, e.g. one you received or one your application produced. See [Validation](validation.md) for the levels L1 to L7. | Argument / option | Effect | |---|---| | `file` | XML or PDF file | | `--profile` | Expected profile (aliases allowed); a different profile fails | | `--kosit` | Also run the KoSIT validator (L5) with all settings of `quittio.validation.drivers.kosit` (`mode`, `url`, `jar`, `scenarios`, `repository`, `configurations`, `java`, `timeout`, `fail_on_unavailable`) | | `--verapdf` | Also run veraPDF, the PDF/A checker (L6), with all settings of `quittio.validation.drivers.verapdf` (`mode`, `binary`, `image`, `docker`, `timeout`, `fail_on_unavailable`) | | `--json` | Print the result as JSON instead of the report | The command reads the file and validates it with the configured levels plus `kosit` and `verapdf` (`Quittio::coreWithLevels()`): - The drivers are built exactly as in a render run. - A level already in `validation.levels` is not added twice. - The cached core of the application is not changed. - With `fail_on_unavailable` set to `false`, a validator that cannot be reached or started is reported as a warning (`KOSIT-UNAVAILABLE`, `VERAPDF-UNAVAILABLE`) and does not fail the command. The report shows invoice number, syntax, profile, the checked levels and a table of findings (severity, level, code, business term, message and hint). `--json` prints `{"valid": …, "levels": […], "messages": [{severity, code, message, level, bt, path, hint, xpath}], "profile": …, "expectedProfile": …, "profileMismatch": …}`; when the file cannot be checked, it prints `{"valid": false, "error": "…"}`. | Exit code | Meaning | |---|---| | `0` | The file has no errors and, with `--profile`, has the expected profile; warnings do not fail | | `1` | The file has errors or another profile than `--profile` | | `2` | The file could not be checked: missing or unreadable, not readable as e-invoice, or an unknown `--profile` | ### quittio:read ```bash php artisan quittio:read {file} {--json} ``` | Argument / option | Effect | |---|---| | `file` | XML or PDF file | | `--json` | Print the invoice data as JSON (`InvoiceData::toJson()`) | Shows the content of an e-invoice file: - number, issue date, type code, syntax and profile, - seller and buyer (with country), - the embedded file name of a PDF, - a table of the lines (position, item, quantity and unit, net amount, VAT category and rate), - net total, VAT and amount due (the totals declared in the file), - the findings of the reader. A missing or unreadable file exits with `1`. See [Reading incoming invoices](reading.md). ### quittio:benchmark ```bash php artisan quittio:benchmark {--count=20} {--scenario=simple} {--format=*} ``` | Option | Default | Effect | |---|---|---| | `--count` | `20` | Measured runs per format (at least 1) | | `--scenario` | `simple` | Sample scenario | | `--format` | `cii`, `zugferd` | Formats; repeat the option for several (`--format=cii --format=zugferd`) | Renders the scenario once per format to warm up fonts and caches. Then it renders `--count` times with validation mode `warn` and prints average, minimum, 95th percentile and maximum in milliseconds, plus the peak memory. An unknown scenario exits with `1`. ## Maintenance Recurring tasks. Most of them belong to the Managed layer, which stores invoices in the database ([Managed layer](managed.md)). Schedule them as shown in [Scheduling](#scheduling). ### quittio:verify Managed layer. ```bash php artisan quittio:verify {--from=} {--to=} {--issuer=} ``` | Option | Effect | |---|---| | `--from` | Issue date from (`Y-m-d`, inclusive) | | `--to` | Issue date to (`Y-m-d`, inclusive) | | `--issuer` | Only invoices of this issuer key | Checks every issued invoice of all issuers; the tenancy scope does not apply. The archived XML and PDF of the current document version must exist and match their SHA-256 hashes. Issued invoices whose documents were never archived (render status `pending`, `rendering` or `failed`) count as problems too. Every problem dispatches `ArchiveIntegrityViolation` (invoice, path, problem). The run writes the audit entry `verified` with the number of checked invoices and violations. Problems are printed as a table and exit with `1`. See [Rendering and archive](managed.md#rendering-and-archive). ### quittio:drafts:prune Managed layer. ```bash php artisan quittio:drafts:prune {--older-than=60} {--force} ``` | Option | Default | Effect | |---|---|---| | `--older-than` | `60` | Minimum age of cancellation drafts in minutes | | `--force` | – | Do not ask for confirmation | Deletes drafts that can never be issued: - cancellation drafts older than `--older-than` minutes. They were left behind (e.g. by a crash) while `cancel()` or `cancelPartially()` issued them. - correction drafts whose original is no longer in status `issued` or no longer exists. The command lists them (draft, kind, original, created), asks, and deletes each through the model (audit entry `deleted_draft`). A draft issued meanwhile is not touched. A non-numeric `--older-than` or a declined confirmation exits with `1`. See [Cancellations and corrections](managed.md#cancellations-and-corrections). ### quittio:retention:check Managed layer. ```bash php artisan quittio:retention:check {--delete} {--force} ``` | Option | Effect | |---|---| | `--delete` | Delete the expired invoices and their archived documents (asks for confirmation) | | `--force` | Do not ask for confirmation | Invoices must be kept for a legal retention period ([Retention and deletion](managed.md#retention-and-deletion)). The command lists the issued invoices past their retention period that are not on legal hold. An invoice counts as expired two days after `retention_until`. It dispatches `RetentionExpired` for each of them. It also reports archived files of deleted invoices that could not be deleted yet. Without `--delete` nothing changes. With `--delete`, it first retries the leftover files. Then it deletes by group: - A group is an invoice together with all invoices connected to it through cancellations, partial cancellations, corrections and replacements. - A group is deleted only when every member is expired and none is on legal hold. Otherwise the command names the invoice that keeps the group. - The rows of a group (relations, deliveries, referring drafts, invoices) are deleted in one transaction with audit entries, then all archived files of all document versions. Files that cannot be deleted are tried again by the next run. Failures of single groups are reported and the run continues (exit code `0`). A declined confirmation exits with `1`. See [Rendering and archive](managed.md#rendering-and-archive). ### quittio:sequence:show Managed layer. ```bash php artisan quittio:sequence:show {--issuer=} ``` | Option | Effect | |---|---| | `--issuer` | Only the sequences of this issuer key | Lists every number sequence with its next value and pattern, ordered by key. The key has the form `{issuer}:{sequence}:{period}`, e.g. `default:invoice:2026`. Sequences are created when the first invoice of a period is issued. See [Numbering](managed.md#numbering). ### quittio:sequence:set Managed layer. ```bash php artisan quittio:sequence:set {key} {value} {--force} ``` | Argument / option | Effect | |---|---| | `key` | Sequence key `{issuer}:{sequence}:{period}`, e.g. `default:invoice:2026` | | `value` | New next value, a positive integer greater than the current one | | `--force` | Do not ask for confirmation | Raises the next value of a sequence, e.g. when you migrate from another system. Lowering it is refused. A sequence that does not exist yet is created ahead of time, but only when the numbering would use it: - the issuer must exist, - the sequence must be `invoice`, or a kind with its own sequence for that issuer (`cancellation`, `correction`), - the period must match the reset mode (`2026` yearly, `2026-10` monthly, `all` never). The pattern comes from the configuration. The change runs in a transaction with a row lock and is written to the audit log. Invalid values or keys, a lower value and a declined confirmation exit with `1`. See [Numbering](managed.md#numbering). ### quittio:base-rate:update German country pack. ```bash php artisan quittio:base-rate:update [--dry-run] ``` | Option | Effect | |---|---| | `--dry-run` | Show the rates, write nothing | German default interest on late payments is the base rate (Basiszinssatz) under section 247 of the German Civil Code (BGB) plus a surcharge. The base rate changes on 1 January and 1 July. Quittio ships the rates up to its release; this command fetches the later ones, so that default interest (`DefaultInterest`) covers periods after the shipped table without a new release. The command fetches the base rate from the statistics API of the Deutsche Bundesbank (series `BBIN1.M.DE.BBK.BBKBAS2.EUR.ME`). It stores the rates that the shipped table does not contain, and values that differ from it, in `quittio-de.base_rate_file` (default `storage/app/quittio/base-rates.json`). The German country pack registers it (`GermanyServiceProvider`). It prints the latest rate and the rates after the shipped table. It warns when a value differs from the shipped table; the Bundesbank value is then used. It writes the file only when something changed. Exit code `1` when: - the API cannot be reached or returns no rates, - `quittio-de.base_rate_file` is empty (not with `--dry-run`), - the directory of the file cannot be created. See [Country packs: base rate table](country-packs.md#base-rate-table) and [Country pack Germany](configuration.md#country-pack-germany-configquittio-dephp). ## Package development ### quittio:codelists:update ```bash php artisan quittio:codelists:update {xsl?} {--target=} ``` | Argument / option | Default | Effect | |---|---|---| | `xsl` | `storage/quittio/validators/xrechnung/resources/cii/16b/xsl/EN16931-CII-validation.xsl` | EN 16931 CII Schematron XSL of the validator configuration | | `--target` | the code lists of `quittio/core` (`resources/codelists`) | Output directory | For the development of Quittio itself: it regenerates the JSON code lists (units, currencies, countries, VAT categories, VATEX, EAS and ICD schemes, payment means, …) from the official rules and prints the number of codes per list. In an application, pass `--target`; the default writes into the installed package. A missing XSL exits with `1` (run `quittio:schemas:download` first). ## Scheduling Schedule the maintenance commands in `routes/console.php`: ```php // routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('quittio:verify')->weekly()->onOneServer(); // exit code 1 on problems Schedule::command('quittio:drafts:prune --force')->daily()->onOneServer(); Schedule::command('quittio:retention:check')->monthly()->onOneServer(); // report and RetentionExpired events Schedule::command('quittio:base-rate:update')->weekly()->onOneServer(); // new base rate on 1 January and 1 July ``` | Command | Recommendation | |---|---| | `quittio:verify` | Daily or weekly. React to failures with `->onFailure(…)` or `->emailOutputOnFailure(…)` and listen for `ArchiveIntegrityViolation`. On large archives limit the range, e.g. `['--from' => now()->subDays(8)->toDateString()]`, and run a full check less often. | | `quittio:drafts:prune` | Daily with `--force` | | `quittio:retention:check` | Monthly without `--delete` for a report. Add `--delete --force` only when expired invoices should be deleted without review. | | `quittio:schemas:download` | Not scheduled: run it in deployment or CI when you use the KoSIT validator | | `quittio:base-rate:update` | Weekly (or daily in January and July): the new rate is stored as soon as the Bundesbank has published it | ## Publish tags Publish tags copy files of the packages into your application, so that you can change them: ```bash php artisan vendor:publish --tag=quittio-config ``` | Tag | Package | Publishes to | |---|---|---| | `quittio-config` | laravel | `config/quittio.php` ([Configuration](configuration.md)) | | `quittio-views` | laravel | `resources/views/vendor/quittio/templates` (default template and partials; they override the package views) | | `quittio-lang` | laravel | `lang/vendor/quittio` (labels, rule messages, codes, terms) | | `quittio-fonts` | laravel | `resources/fonts/quittio/geist` (Geist) and `resources/fonts/quittio/inter` (Inter). `resources/fonts/quittio` and its subdirectories are added to the font directories automatically, so further font files placed there only need an entry in `pdf.fonts`. | | `quittio-managed-config` | managed | `config/quittio-managed.php` | | `quittio-managed-migrations` | managed | `database/migrations` (tables of the Managed layer, with new timestamps) | | `quittio-managed-triggers` | managed | `database/migrations` (immutability triggers, recommended for production) | | `quittio-managed-views` | managed | `resources/views/vendor/quittio-managed` (mail view) | | `quittio-managed-lang` | managed | `lang/vendor/quittio-managed` (mail and invoice texts) | | `quittio-de-config` | country-de | `config/quittio-de.php` (base rates) | Publishing is only registered in the console. # Testing This page shows how to test code that creates invoices: fast feature tests with a fake, checks that your e-invoices are valid, test data and the official validators in CI. The examples use Pest; the same methods work in PHPUnit. | You want to test | Use | |---|---| | That your code renders an invoice, without the cost of a real run | [`Quittio::fake()`](#fake) | | That the invoice data is a valid e-invoice, without rendering | [`check()`](#validating-the-invoice-data) | | Storage, events, the queue or mail with real rendering | [Laravel fakes](#laravel-fakes) | | That a rendered PDF or XML is a valid e-invoice, or that the XML has not changed | [Assertions](#assertions) | | An invoice to test with, without writing a builder chain | [Factory](#factory) | | Realistic sample invoices: many lines, reverse charge, foreign currency … | [Scenarios](#scenarios) | | Your files with the official validators (KoSIT, veraPDF) | [Official validators in CI](#official-validators-in-ci) | | Models of the Managed layer | [Managed layer factories](#managed-layer-factories) | The factory and the scenarios are part of `quittio/core` and work without Laravel. The fake and the assertions need `quittio/laravel`. Terms such as ZUGFeRD, profile or reverse charge are explained in [E-invoicing basics](concepts.md). ## Fake The most common test checks that your code renders an invoice, e.g. when an order ships. `Quittio::fake()` replaces the rendering with a fast fake that records every render call. Afterwards you assert what was rendered: ```php use Quittio\Laravel\Facades\Quittio; use Quittio\Laravel\Rendering\RenderedInvoice; it('renders the invoice of an order', function () { $fake = Quittio::fake(); $this->post('/orders/42/invoice')->assertOk(); Quittio::assertRenderedCount(1); Quittio::assertRendered(fn (RenderedInvoice $r) => $r->format() === 'zugferd' && $r->data()->totals->amountDue->isEqualTo('1234.56')); expect($fake->rendered()[0]->data()->number)->toBe('RE-2026-0042'); }); ``` - `assertRenderedCount(1)` checks that exactly one invoice was rendered. - `assertRendered()` with a callback checks that at least one rendered invoice matches: here one in the format ZUGFeRD (`zugferd`, a PDF with embedded XML) with the expected amount due. The callback receives a `RenderedInvoice`; its `data()` holds the calculated invoice with totals. - `Quittio::fake()` returns the fake. `$fake->rendered()` lists the recorded results, in order. - The fake skips validation. To test that the invoice is a valid e-invoice, add a test with [`check()`](#validating-the-invoice-data). A complete example with an order, a mail and both kinds of test is [Testing](examples.md#testing) on the Examples page. ### How the fake works `Quittio::fake()` replaces the manager with a `Quittio\Laravel\Testing\QuittioFake` and returns it. It is swapped into the facade and into the container. So code that injects `QuittioManager`, queued `RenderInvoice` jobs and the Managed layer use the fake as well. The fake takes the decisions of a real run: it calls `prepare()` of the core without validators. Then it returns placeholder results instead of rendering. It skips validation, XML, template and PDF: | | Behaviour of the fake | |---|---| | Calculation | Real: totals, VAT breakdown and the due date of `defaults.due_days`, so `data()` holds the amounts | | Format | As in a real run: `format()`/`formats()`, then `defaults.format`, then the automatic choice (country rules, `format_resolver`, Peppol lookup). Every format of the call is recorded under the main result, so `formats()` and `as()` work. | | Profile | As in a real run: `profile()` with a key, alias or `Profile` object, `defaults.profile`, the profile of the automatic choice or of the format, the EXTENDED upgrade; `null` for formats without XML | | Errors | An unknown format or profile, or a profile the format does not allow, throws a `ConfigurationException` like a real run | | Validation | None: `validation()` is an empty `ValidationResult`, no `ValidationException` | | Outputs | Placeholders according to the format: XML ``, PDF `%PDF-1.7` with a comment, HTML `

{number}

`, JSON of the data | | File name | `Invoice_{number}.{ext}` | | Documents | Calculated; fake HTML and, unless `htmlOnly()`, a fake PDF; file name `{number}.{ext}` | | Events | As in a real run: `InvoiceRendering`, `InvoiceValidated` (with the empty result, not in mode `off`) and `InvoiceRendered`, so `Event::fake()` with `Event::assertDispatched(InvoiceRendered::class)` works | | Queue | `queue()->save()` records the job (`queued()`, `assertQueued()`) and dispatches it like a real run: `Queue::fake()` sees it, a `sync` queue renders it with the fake | Not faked: `check()`, `prepare()`, `read()`, `readFile()`, `validateFile()`, `validatePath()` and `cancellation()` run the real core. Responses, `save()` and mail attachments work on the fake results. The fake stays active until the application is rebuilt for the next test. ### Assertions of the fake These methods belong to the fake and are also callable through the facade (`Quittio::assertRendered()`): | Method | Asserts / returns | |---|---| | `assertRendered(?callable $callback = null)` | At least one invoice was rendered; with a callback, at least one `RenderedInvoice` for which it returns `true` | | `assertRenderedCount(int $count)` | Exactly `$count` invoices were rendered | | `assertNothingRendered()` | No invoice was rendered | | `assertDocumentRendered(?callable $callback = null)` | At least one document (quote, reminder, …) was rendered; with a callback, one `RenderedDocument` matches | | `rendered(): array` | The recorded `RenderedInvoice` results (the main format of each call), in order | | `documents(): array` | The recorded `RenderedDocument` results, in order | | `assertQueued(?callable $callback = null)` | At least one invoice was queued with `queue()->save()`; with a callback, one `RenderInvoice` job matches (`$job->data()` is the invoice, `$job->disk`, `$job->path`) | | `assertNothingQueued()` | No invoice and no document was queued | | `assertDocumentQueued(?callable $callback = null)` | At least one document was queued with `Quittio::document(…)->queue()->save()`; with a callback, one `RenderDocument` job matches (`$job->data()` is the document) | | `queued(): array` | The recorded `RenderInvoice` jobs, in order | Documents such as quotes and reminders ([Quotes and other documents](documents.md)) are recorded separately from invoices: ```php $fake = Quittio::fake(); Quittio::document($quote)->htmlOnly()->render(); expect($fake->documents())->toHaveCount(1) ->and($fake->documents()[0]->hasPdf())->toBeFalse(); ``` ## Validating the invoice data A test with the fake does not show whether the invoice is a valid e-invoice. `check()` runs the real validation, including the XML, without rendering a PDF. It is never faked, so it also works while the fake is active: ```php use Quittio\Laravel\Facades\Quittio; it('builds a valid e-invoice for an order', function () { $order = Order::factory()->create(); $report = Quittio::make($order)->check(); // the real validation, without rendering expect($report->isValid())->toBeTrue() ->and($report->warnings())->toBe([]); }); ``` - `Order` is a model of your application that implements `ProvidesInvoiceData` ([Invoices for orders](examples.md#invoices-for-orders)). Any invoice data works the same way: `Quittio::make($invoice)->check()`. - `check()` returns a `ValidationResult` and never throws a `ValidationException` for invalid data. `isValid()` is `true` without errors; `warnings()` lists the warnings ([The result](validation.md#the-result)). - It validates in the same formats and profiles as `render()` would. The PDF/A check (level L6) does not run, because it needs the rendered PDF ([When validation runs](validation.md#when-validation-runs)). - To validate a rendered PDF or XML file, use [`assertValidEInvoice()`](#assertions). ## Laravel fakes Without `Quittio::fake()` the real core renders. Then the usual Laravel fakes test the integration end to end: files on a disk, events and queued jobs. `InvoiceData::factory()` builds the test invoice ([Factory](#factory)): ```php use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Storage; use Quittio\Core\Data\InvoiceData; use Quittio\Laravel\Events\InvoiceRendered; use Quittio\Laravel\Events\InvoiceValidationFailed; use Quittio\Laravel\Facades\Quittio; use Quittio\Laravel\Jobs\RenderInvoice; it('stores the PDF', function () { Storage::fake('invoices'); Quittio::make(InvoiceData::factory()->make())->format('zugferd')->render()->save('invoices', '2026/'); Storage::disk('invoices')->assertExists('2026/Rechnung_RE-2026-0042.pdf'); }); it('dispatches the events', function () { Event::fake([InvoiceRendered::class, InvoiceValidationFailed::class]); Quittio::make(InvoiceData::factory()->make())->format('cii')->render(); Event::assertDispatched(InvoiceRendered::class, fn ($e) => $e->result->format() === 'cii'); Event::assertNotDispatched(InvoiceValidationFailed::class); }); it('queues the rendering', function () { Queue::fake(); Quittio::make(InvoiceData::factory()->make())->queue()->save('invoices', 'queued/'); Queue::assertPushedOn('quittio', RenderInvoice::class, fn (RenderInvoice $job) => $job->path === 'queued/'); }); ``` - The factory's invoice is German (locale `de`), so the file name starts with the German title "Rechnung" (invoice). - The events are described in [Events](laravel.md#events), the queue in [Queueing](laravel.md#queueing). `quittio` is the default queue name. - Rendered results are `Attachable`. So `Mail::fake()` with `Mail::assertSent(YourMail::class, fn ($mail) => $mail->hasAttachment($result))` checks mail attachments. - After changing `config('quittio.…')` inside a test, the next call uses the new configuration ([Configuration](configuration.md)). ## Assertions The trait `Quittio\Laravel\Testing\InteractsWithQuittio` adds two assertions for rendered files to a test case. `assertValidEInvoice()` checks that a PDF or XML is a valid e-invoice. `assertXmlMatchesSnapshot()` checks that the XML has not changed since you last accepted it. Add the trait to your tests: ```php // PHPUnit use Quittio\Laravel\Testing\InteractsWithQuittio; class InvoiceTest extends TestCase { use InteractsWithQuittio; } // Pest, in a single test file uses(InteractsWithQuittio::class); // Pest, for all tests of a directory (in tests/Pest.php) pest()->use(InteractsWithQuittio::class)->in('Feature'); ``` After each test the trait also resets the process-wide state of Quittio (`Quittio\Core\Quittio::resetState()`), e.g. base rates that a test registered. Then use the assertions on the outputs of a rendered invoice: ```php $result = Quittio::make(InvoiceData::factory()->make())->formats(['zugferd', 'xrechnung-ubl'])->render(); $this->assertValidEInvoice($result->pdf(), 'en16931'); $this->assertValidEInvoice($result->as('xrechnung-ubl')->xml(), 'xrechnung'); $this->assertXmlMatchesSnapshot($result->xml(), 'zugferd-default'); ``` | Method | Asserts | |---|---| | `assertValidEInvoice(string $pdfOrXml, ?string $profile = null)` | Reads the PDF or XML content and validates it with all configured levels (`quittio.validation.levels`, see [below](#official-validators-in-ci)). Fails with the code and message of every error; warnings do not fail. With `$profile`, the profile read from the file must be that profile (aliases are resolved, e.g. `comfort`). | | `assertXmlMatchesSnapshot(string $xml, ?string $name = null)` | Compares the canonicalised XML (whitespace between elements removed, re-indented) with a stored snapshot. The first run writes the snapshot and passes. Not well-formed XML fails. | The profile is the set of rules the file follows, e.g. `en16931` or `xrechnung` ([Profiles](concepts.md#profiles)). ### Snapshots A snapshot is a stored copy of the XML. Later runs compare the XML with it, so that an unintended change of the output fails the test. - Snapshots are stored in `__snapshots__/quittio/{name}.xml` next to the test file. - The name is reduced to letters, digits, `.`, `_` and `-`. Without a name it is `{TestClass}__{test name}`. Pest generates class and test names, so pass a name there. - To accept a changed XML, delete the snapshot file and run the test again. - Override the protected methods `quittioSnapshotDirectory()` and `quittioSnapshotName()` to change the location and the default name. ### Outside test case classes Outside test case classes, e.g. in Pest helper functions, `Quittio\Laravel\Testing\EInvoiceAssertions` provides the same assertions: ```php use Quittio\Laravel\Testing\EInvoiceAssertions; (new EInvoiceAssertions)->assertValidEInvoice($pdf, 'xrechnung'); (new EInvoiceAssertions(base_path('tests/__snapshots__')))->assertXmlMatchesSnapshot($xml, 'order-42'); ``` Its constructor takes the snapshot directory (default `tests/__snapshots__/quittio` below the working directory). Its default snapshot name is `xml`, so pass a name per snapshot. ## Factory Most tests need an invoice. `InvoiceData::factory()` (or `Quittio\Core\Testing\InvoiceFactory::new()`) builds one in a single line. The test invoices are deterministic: they contain no random values, so XML snapshots stay stable. Every method returns a new factory. ```php use Quittio\Core\Data\InvoiceData; InvoiceData::factory()->make(); // one line, 19 % InvoiceData::factory()->reverseCharge()->withLines(5)->make(); InvoiceData::factory()->intraCommunity()->calculated(); InvoiceData::factory()->b2c()->currency('USD')->number('X-1')->make(); InvoiceData::factory()->creditNote()->state(fn ($builder) => $builder->buyerReference('04011000-12345-03'))->make(); ``` The base invoice is the one of the scenarios (`Scenarios::base()`): - number `RE-2026-0042`, issue date 2026-10-03, delivery date 2026-09-30, locale `de`; - seller Muster GmbH (`DE123456789`, Musterstadt), buyer Kunde AG (`DE987654321`), buyer reference `PO-4711`; - payment terms 14 days and a SEPA credit transfer. With the German country pack the automatic choice renders these invoices as ZUGFeRD, and with `b2c()` as plain PDF. `withLines(3)` gives net 140.00, VAT 26.60 and an amount due of 166.60. | Method | Effect | |---|---| | `withLines(int $count)` | Number of lines (at least 1). Line *i*: "Item *i*", quantity *i* pieces, net price 10 × *i*, VAT 19 % (or the category of the VAT mode) | | `reverseCharge()` | Buyer Client SARL (Paris, `FR12345678901`), lines in category `AE` (reverse charge) with `VATEX-EU-AE` | | `intraCommunity()` | Buyer Cliente S.r.l. (Rome, `IT12345678901`), lines in category `K` (intra-community supply) with `VATEX-EU-IC` | | `export()` | Buyer Customer Inc. (New York, no VAT identifier), lines in category `G` (export) with `VATEX-EU-G` | | `b2c()` | Consumer buyer Erika Mustermann (Berlin), no buyer reference | | `currency(string $currency)` | Invoice currency; other than EUR adds the VAT accounting currency EUR (rate 0.9 of 2026-10-02) and a credit transfer | | `number(string $number)` | Invoice number (default `RE-2026-0042`) | | `creditNote()` | Type code 381 with the preceding invoice RE-2026-0001 of 2026-09-01 | | `state(callable $state)` | Calls `$state(InvoiceBuilder $builder)` for free changes; several states run in order | | `builder(): InvoiceBuilder` | The prepared builder | | `make(): InvoiceData` | Invoice data, not calculated (as passed to `Quittio::make()`) | | `calculated(?TotalsCalculator $calculator = null): InvoiceData` | Invoice data with totals and VAT breakdown, calculated with `$calculator` (default `En16931Calculator`) | `VATEX-EU-…` are the codes of the VAT exemption reasons ([Exemption reasons in the VAT breakdown](building-invoices.md#exemption-reasons-in-the-vat-breakdown)). ## Scenarios `Quittio\Core\Testing\Scenarios` holds realistic sample invoices: the ones of the preview, the visual tests and the official validation runs. Use them when a test needs special content, e.g. 60 lines for page breaks or sub lines. They are country neutral: they use VAT categories directly, not the tax presets of a country pack. ```php use Quittio\Core\Testing\Scenarios; use Quittio\Laravel\Facades\Quittio; $invoice = Scenarios::make('sub-lines')->number('T-1')->build(); $result = Quittio::make($invoice)->profile(Scenarios::profile('sub-lines'))->format('zugferd')->render(); ``` `php artisan quittio:preview --scenario=simple` renders a scenario with your template ([quittio:preview](commands.md#quittiopreview)). | Method | Returns | |---|---| | `Scenarios::names(): array` | All scenario names | | `Scenarios::make(string $name): InvoiceBuilder` | The builder of a scenario; an unknown name throws an `InvalidArgumentException` | | `Scenarios::profile(string $name): string` | Best profile: `extended` for `sub-lines`, `peppol` for `peppol`, otherwise `en16931` | | `Scenarios::base(): InvoiceBuilder` | Seller, buyer, dates and payment of all scenarios, without lines | Each scenario also has a static method (`Scenarios::simple()`, `manyLines()`, `longTexts()`, `mixedVat()`, `reverseCharge()`, `intraEu()`, `export()`, `cancellation()`, `subLines()`, `foreignCurrency()`, `b2cGross()`, `allowances()`, `attachments()`, `peppol()`). | Name | Content | |---|---| | `simple` | Two lines (24 h web development, monthly hosting with a period), 19 %, 2 % cash discount within 7 days | | `many-lines` | 60 lines with item numbers, 7 % and 19 % (page breaks) | | `long-texts` | Long note and description, item attributes | | `mixed-vat` | 7 %, 19 % and zero rated lines, 10 % document discount | | `reverse-charge` | French buyer, reverse charge (`AE`) | | `intra-eu` | Italian buyer, intra-community supply (`K`), delivery address, country of origin, HS code | | `export` | US buyer, export (`G`) | | `cancellation` | Credit note ST-2026-0007 cancelling RE-2026-0042 | | `sub-lines` | Line with three sub lines, invoicee and delivery terms (profile EXTENDED) | | `foreign-currency` | USD invoice with the VAT accounting currency EUR | | `b2c-gross` | Consumer buyer, gross prices, 7 % and 19 % | | `allowances` | Line allowance and charge, document charge and allowance, price base quantity, list price with discount | | `attachments` | Embedded CSV time sheet and an external document reference | | `peppol` | Belgian buyer, Peppol endpoints (`0208`, `9930`), reverse charge | ## Official validators in CI By default, rendering and `assertValidEInvoice()` use Quittio's PHP rules and the XSD schemas (levels `php` and `xsd`). Two official validators check more ([Rules and validation](concepts.md#rules-and-validation)): - the KoSIT validator (level L5) with the official Schematron rules of EN 16931, XRechnung and Peppol; KoSIT is the German agency that publishes XRechnung; - veraPDF (level L6), which checks that a PDF is a valid PDF/A file. They need Java and veraPDF. Run them in a separate test group, so that your fast tests do not need them: ```php use Quittio\Core\Data\InvoiceData; use Quittio\Core\Validation\Drivers\KositArtifacts; use Quittio\Laravel\Facades\Quittio; function useOfficialValidators(): void { $root = storage_path('quittio/validators'); // php artisan quittio:schemas:download config()->set('quittio.validation.levels', ['php', 'xsd', 'kosit', 'verapdf']); config()->set('quittio.validation.drivers.kosit', [ 'mode' => 'cli', 'jar' => $root.'/validator.jar', 'scenarios' => $root.'/xrechnung/scenarios.xml', 'repository' => $root.'/xrechnung', 'configurations' => [['scenarios' => KositArtifacts::peppolScenarios(), 'repository' => $root]], 'timeout' => 120, ]); config()->set('quittio.validation.drivers.verapdf', ['mode' => 'docker', 'image' => 'verapdf/cli']); } it('produces valid ZUGFeRD and XRechnung', function () { useOfficialValidators(); $invoice = InvoiceData::factory()->state(fn ($b) => $b->buyerReference('04011000-12345-03'))->make(); $result = Quittio::make($invoice)->formats(['zugferd', 'xrechnung-cii'])->render(); $this->assertValidEInvoice($result->pdf(), 'en16931'); $this->assertValidEInvoice($result->as('xrechnung-cii')->xml(), 'xrechnung'); })->group('official'); ``` - `php artisan quittio:schemas:download` downloads the validator and prints the values for `jar`, `scenarios`, `repository` and `configurations`. - In mode `cli` every validation starts a JVM (Java virtual machine). For many documents, run the validator as a daemon instead: `mode` `daemon` with `QUITTIO_KOSIT_URL` ([KoSIT validator](validation.md#kosit-validator-l5)). - The download command also prints the start command of the daemon, with `-P 8081` for the default URL `http://127.0.0.1:8081`. The validator's own default port is 8080. - An unreachable validator fails the validation with `KOSIT-UNAVAILABLE`, unless `fail_on_unavailable` is `false`. A GitHub Actions job for this group: ```yaml official-validation: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: php: '8.4' extensions: dom, libxml, mbstring, xmlwriter, zlib, intl, gd, bcmath, zip - uses: actions/setup-java@v6 with: distribution: temurin java-version: '21' - uses: actions/cache@v6 with: path: storage/quittio/validators key: quittio-validators-${{ hashFiles('composer.lock') }} - run: composer install --no-interaction --no-progress - run: php artisan quittio:schemas:download # keeps existing files - run: docker pull verapdf/cli - run: vendor/bin/pest --group=official ``` Exclude the group from the fast test job with `--exclude-group=official`. To check a single file by hand: `php artisan quittio:validate invoice.pdf --profile=en16931 --kosit --verapdf` ([Artisan commands](commands.md#quittiovalidate)). ## Managed layer factories The [Managed layer](managed.md) is the optional package `quittio/managed`: it stores invoices in your database, numbers and archives them. It ships Eloquent factories for its models (namespace `Quittio\Managed\Database\Factories`). Tests need the published Managed migrations (`quittio:install --managed`, see [Installation](managed.md#installation)) and a refreshed database. ```php use Quittio\Core\Data\InvoiceData; use Quittio\Managed\Models\Invoice; use Quittio\Managed\Models\Issuer; $issuer = Issuer::factory()->default()->create(); $invoice = Invoice::factory()->for($issuer)->create()->issue('2026-10-05'); $other = Issuer::factory()->company('Andere AG', 'DE111111111')->create(['key' => 'tenant-b']); $draft = Invoice::factory()->for($issuer)->data(InvoiceData::factory()->reverseCharge()->make())->create(); ``` `Issuer::factory()` (`IssuerFactory`) creates issuer profiles, the companies that issue invoices: | State | Attributes | |---|---| | (definition) | Key `issuer-{10 random characters}`; seller Muster GmbH with a random German VAT identifier (`DE` + 9 digits), Hauptstraße 1, 12345 Musterstadt, contact Max Muster, e-mail and electronic address `rechnung@muster.de`; country `DE`, time zone `Europe/Berlin`, bank account `DE02120300000000202051` / `BYLADEM1001`, defaults `locale` `de` and `due_days` 14; not the default issuer. Every issuer is a company of its own and gets its own number space. | | `default()` | Key `default`, default issuer, seller VAT identifier `DE123456789` | | `company(string $name, string $vatId)` | Another company: own name and VAT identifier, Marktplatz 2, 54321 Anderstadt, electronic address `rechnung@{slug of the name}.de` | `Invoice::factory()` (`InvoiceFactory`) creates drafts: | State | Attributes | |---|---| | (definition) | Draft (`status` draft, `kind` invoice) with the data of `InvoiceData::factory()->withLines(2)->make()` (number empty, totals not yet calculated), for a new issuer (`Issuer::factory()`) | | `data(InvoiceData $data)` | The columns of your invoice data instead | Issuing takes the number from the issuer's sequence. The documents are rendered in a queued job after the transaction. In tests, use `Queue::fake()`, or set `quittio-managed.queue.render_sync` to render in the same request. See [Managed layer](managed.md) for the lifecycle and [Testing against MySQL and PostgreSQL](managed.md#testing-against-mysql-and-postgresql). # Country packs A country pack holds everything in Quittio that differs between countries: VAT rates and exemption texts, national invoice rules and profiles, the formats a country prescribes, legal notices, letter layouts, numbering and retention periods, and the law on late payment. The core of Quittio stays country-neutral. Quittio ships a pack for Germany, and `quittio/quittio` installs it. Read this page when you invoice from Germany and need more than a standard invoice: an exemption or reverse charge, a small business invoice, XRechnung for a public buyer, a reminder with default interest or a letter layout after DIN 5008 (the German standard for business letters). Read it also when you want to support another country. If terms such as EN 16931, XRechnung, profile or `BR-DE` codes are new to you, read [E-invoicing basics](concepts.md) first. The page has two parts: | Part | For | Start with | |---|---|---| | **Using the German pack** | Everyone who invoices as a seller in Germany (most readers) | [Germany (`quittio/country-de`)](#germany-quittiocountry-de) | | **Writing a country pack** | Developers who add another country or change how a pack behaves | [How a pack is selected](#how-a-pack-is-selected), [Writing your own country pack](#writing-your-own-country-pack), [The `CountryPack` contract](#the-countrypack-contract) | The legal wording and references of the packs are proposals that must be confirmed by a tax adviser (see the legal review in the README). ## Germany (`quittio/country-de`) The German pack implements German VAT law (the VAT Act UStG and its implementing ordinance UStDV), XRechnung and German business letter conventions. Its class is `Quittio\Country\DE\CountryDE` (code `DE`, translation namespace `quittio-de`). - **Installation.** Laravel discovers the pack automatically, so there is nothing to register. Without Laravel, pass `new CountryDE` to `Quittio::create()` (see [Registering packs](#registering-packs)). The builder macro `de()` leads to the German special cases (`Quittio\Country\DE\Builder\GermanInvoice`), e.g. `->de()->smallAmount()`; it is available as soon as Composer loads the package. Each method returns the builder, so the chain continues there. The macros of version 0.2 directly on the builder (`->smallAmount()`, `->selfBilled()` …) still work, but are deprecated and report an `E_USER_DEPRECATED`. `stubs/InvoiceBuilder.stub.php` describes `de()` for IDEs and PHPStan. - **When it applies.** The pack leads every invoice whose seller is in Germany (seller country code `DE`). It then checks the German rules, chooses the format and provides the defaults: the German locale (unless the invoice or the configuration sets one), A4 paper and the DIN 5008 form B layout. For an invoice from another country to a business in Germany, only its format rules apply, so that German public buyers receive XRechnung. Details are in [How a pack is selected](#how-a-pack-is-selected). - **Legal references.** This page writes German legal references in English form: "section 14 (4) no. 1 UStG" stands for "§ 14 Abs. 4 Nr. 1 UStG". Other laws cited are the Civil Code (BGB), the Fiscal Code (AO) and the VAT application decree (UStAE). Values that the pack stores or prints in German keep their German form. German terms such as Gutschrift, Kleinunternehmer or Leitweg-ID are explained on first use and in the [glossary](concepts.md#german-terms). Common tasks: | You want to | Read | |---|---| | Set the VAT rate or an exemption on a line | [Tax presets](#tax-presets) | | Invoice as a small business without VAT (Kleinunternehmer) | [Small businesses](#small-businesses) | | Invoice a business in another EU country | [Reverse charge, intra-community supplies and VAT identifiers](#reverse-charge-intra-community-supplies-and-vat-identifiers) | | Know which format Quittio picks (ZUGFeRD, XRechnung, PDF) | [Format choice](#format-choice) | | Invoice a German public authority | [Format choice](#format-choice), [XRechnung profiles](#xrechnung-profiles), [XRechnung rules](#xrechnung-rules) | | Issue an invoice of up to 250 € with fewer details | [Small amount invoices](#small-amount-invoices) | | Deduct advance payments in a final invoice | [Final invoices](#final-invoices-deductadvance) | | Issue a self-billed invoice (Gutschrift) | [Self-billing](#self-billing-gutschrift) | | Cancel or correct an invoice | [Cancellations and corrections](#cancellations-and-corrections) | | Send a reminder with default interest | [Dunning law](#dunning-law), [Default interest](#default-interest) | | Choose the letter layout | [DIN 5008 letter layouts](#din-5008-letter-layouts) | | Look up a validation code such as `DE-USTG-03` or `BR-DE-15` | [UStG rules](#ustg-rules), [XRechnung rules](#xrechnung-rules) | Complete invoices for these cases are on [Examples](examples.md#invoices-in-germany). ### Tax presets A tax preset is a ready-made VAT treatment for an invoice line. It sets the VAT category (BT-151) and the rate, and for exempt supplies also the exemption reason code (BT-121) and text (BT-120). Use a preset instead of a bare category and rate: the line then carries the wording German law requires, and the German rules recognise the case. Lines without `vat:` take the [default VAT](building-invoices.md#default-vat); in Laravel that is 19 % for a German seller and `VatPreset::SmallBusiness` for a small business. The categories are the codes of EN 16931: `S` standard rate, `Z` zero rated, `E` exempt, `AE` reverse charge, `K` intra-community supply, `G` export outside the EU, `O` not subject to VAT (see [Categories](building-invoices.md#categories)). `Quittio\Country\DE\Tax\VatPreset` is a string-backed enum that implements `TaxPreset`. Apply it per line: ```php use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; $builder = Quittio::invoice() ->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour, vat: VatPreset::Standard) // an exemption without its own case: legal reference and VATEX code; // the invoice prints "Steuerfrei nach § 4 Nr. 14 UStG" (exempt under section 4 no. 14 UStG) ->line('Medical treatment', netPrice: '90.00', unit: UnitCode::Piece, vat: VatPreset::exempt('§ 4 Nr. 14 UStG', 'VATEX-EU-132-1C')); ``` | Case | Key | Category | Rate | Code (BT-121) | Use | |---|---|---|---|---|---| | `Standard` | `de.standard` | `S` | 19 (of the supply date) | – | Standard rate | | `Reduced` | `de.reduced` | `S` | 7 (of the supply date) | – | Reduced rate | | `ZeroRatedPv` | `de.zero_rated_pv` | `Z` | 0 | – | 0 % rate for photovoltaic systems (section 12 (3) UStG). The rate itself is the statement, so there is no exemption reason (BR-Z-10). | | `SmallBusiness` | `de.small_business` | `E` | 0 | – | Small business exemption (Kleinunternehmer, section 19 (1) UStG); see [Small businesses](#small-businesses) | | `ReverseChargeDomestic` | `de.reverse_charge_domestic` | `AE` | 0 | `VATEX-EU-AE` | Domestic reverse charge (section 13b UStG): the buyer in Germany owes the VAT | | `ReverseChargeEu` | `de.reverse_charge_eu` | `AE` | 0 | `VATEX-EU-AE` | Service to a business in another member state, which owes the VAT there. Both VAT identifiers are required (`DE-USTG-11`). Issue the invoice by the 15th of the following month (`DE-USTG-15`). | | `IntraCommunitySupply` | `de.intra_community_supply` | `K` | 0 | `VATEX-EU-IC` | VAT-exempt intra-community supply (section 4 no. 1b with section 6a UStG), also of new vehicles (`newVehicle()`). Not for small businesses, except new vehicles (`DE-USTG-14`). Issue the invoice by the 15th of the following month (`DE-USTG-15`). | | `TriangularTransaction` | `de.triangular_transaction` | `AE` | 0 | `VATEX-EU-AE` | Intra-community triangular transaction (section 25b UStG), in which the last customer owes the VAT; wording of section 14a (7) UStG. Both VAT identifiers are required. | | `Export` | `de.export` | `G` | 0 | `VATEX-EU-G` | VAT-exempt export supply (section 4 no. 1a with section 6 UStG) | | `NotTaxableInGermany` | `de.not_taxable` | `O` | none | `VATEX-EU-O` | Supply not taxable in Germany | | `MarginSecondHand` | `de.margin_second_hand` | `E` | 0 | `VATEX-EU-F` | Margin scheme, second-hand goods | | `MarginArt` | `de.margin_art` | `E` | 0 | `VATEX-EU-I` | Margin scheme, works of art | | `MarginAntiques` | `de.margin_antiques` | `E` | 0 | `VATEX-EU-J` | Margin scheme, collectors' items and antiques | | `TravelAgency` | `de.travel_agency` | `E` | 0 | `VATEX-EU-D` | Special scheme for travel agents | | `VatPreset::exempt(string $paragraph, $code = null)` | `de.exempt` | `E` | 0 | `$code` (enum, raw code or string) | Any other exemption, e.g. `§ 4 Nr. 14 UStG` | The codes in column BT-121 are VATEX codes: the standard codes for the reason of an exemption. `Standard` and `Reduced` take the rate that applied on the supply date of the line (`TaxTables::STANDARD_RATE`, `TaxTables::REDUCED_RATE`). The supply date is the end of the line period (BT-135), its start, the VAT point date (BT-7), the delivery date (BT-72), the end of the invoicing period (BT-74) or the issue date, in this order (`Quittio\Core\Tax\SupplyDate`). A delivery in August 2020 gets 16 % and 5 %, a correction of it issued in 2021 as well: ```php use Quittio\Core\Builder\InvoiceBuilder; use Quittio\Country\DE\Tax\VatPreset; InvoiceBuilder::make() ->issueDate('2021-01-20') ->deliveryDate('2020-08-15') ->line('Desk', netPrice: '400.00', vat: VatPreset::Standard); // 16 % ``` `VatPreset::Standard->rate()` returns the rate of today. The plausibility rule `DE-VAT-RATE` warns when a line of category `S` within the German VAT territory has a rate that applied neither as standard nor as reduced rate on its supply date. Average rates of agriculture and forestry (section 24 UStG) are a legitimate exception; switch the rule off for them with `validation.rules.disabled` ([Switching rules off](validation.md#switching-rules-off-and-changing-their-severity)). Up to version 0.2 the presets were called `Standard19` (`de.standard_19`) and `Reduced7` (`de.reduced_7`). These cases are deprecated and follow the dates as well; their keys stay readable: `taxPresets()->get('de.standard_19')` and `quittio.defaults.vat` find `Standard`, and lines of stored invoices keep their key. `VatPreset::fromKey($key)?->canonical()` turns an old key into the current case. `Standard`, `Reduced` and `ZeroRatedPv` carry no text, because EN 16931 forbids an exemption reason for these categories (BR-S-10, BR-Z-10). The other presets print an exemption reason (BT-120) in German or English, from `resources/lang/{de,en}/tax.php`: | Key | German | English | |---|---|---| | `de.small_business` | Steuerfreie Leistung nach § 19 Abs. 1 UStG (Steuerbefreiung für Kleinunternehmer). | VAT-exempt supply under section 19 (1) UStG (small business exemption). | | `de.reverse_charge_domestic` | Steuerschuldnerschaft des Leistungsempfängers (§ 13b UStG) | Reverse charge, the recipient is liable for VAT (section 13b UStG) | | `de.reverse_charge_eu` | Steuerschuldnerschaft des Leistungsempfängers | Reverse charge, the recipient is liable for VAT | | `de.intra_community_supply` | Steuerfreie innergemeinschaftliche Lieferung (§ 4 Nr. 1b i. V. m. § 6a UStG) | VAT-exempt intra-Community supply (section 4 no. 1b in conjunction with section 6a UStG) | | `de.triangular_transaction` | Innergemeinschaftliches Dreiecksgeschäft nach § 25b UStG; Steuerschuldnerschaft des letzten Abnehmers | Intra-Community triangular transaction (section 25b UStG); the last customer is liable for the VAT | | `de.export` | Steuerfreie Ausfuhrlieferung (§ 4 Nr. 1a i. V. m. § 6 UStG) | VAT-exempt export supply (section 4 no. 1a in conjunction with section 6 UStG) | | `de.not_taxable` | Nicht im Inland steuerbare Leistung | Service not taxable in Germany | | `de.margin_second_hand` | Gebrauchtgegenstände/Sonderregelung | Second-hand goods/special arrangements | | `de.margin_art` | Kunstgegenstände/Sonderregelung | Works of art/special arrangements | | `de.margin_antiques` | Sammlungsstücke und Antiquitäten/Sonderregelung | Collectors’ items and antiques/special arrangements | | `de.travel_agency` | Sonderregelung für Reisebüros | Special arrangements for travel agents | | `de.exempt` | Steuerfrei nach :paragraph | Exempt from VAT according to :paragraph | The English texts follow the terms of Directive 2006/112/EC ("exempt", "intra-Community supply", "Reverse charge"). The [legal texts](#legal-texts) of the same notices use the same wording. How presets behave: - **Language.** The text is taken in the invoice locale (`->locale()` on the builder) when the line is built. The enum presets and `VatPreset::exempt()` (a `Quittio\Country\DE\Tax\CustomVatPreset`) use German when the invoice has no locale. Other locales without a text fall back to English. - **Preset key.** The key stays on the line (`$line->vat->presetKey`). `DE-USTG-09`, `DE-USTG-11`, `DE-USTG-14`, `DE-USTG-15` and the small business detection rely on it. - **VAT breakdown.** Category, rate, code and text flow into the VAT breakdown (BG-23). When lines of one category and rate have different texts, the texts are joined in BT-120. The code (BT-121) is kept only when all codes agree; otherwise Quittio warns with `QUITTIO-EXEMPTION-CONFLICT`. - **PDF.** The PDF lists the BT-120 texts of all categories other than the standard rate in its legal notes (`legalNotes`). Margin scheme and travel agency rows show no VAT amount (section 14a (6) UStG). - **Allowances and charges.** Document level allowances and charges take a category and a rate, not a preset: `->charge(VatCategory::StandardRate, '19', amount: '10.00', reason: 'Shipping')`. - **Lookup by key**, e.g. from a database column: `VatPreset::from('de.standard')`, `(new CountryDE)->taxPresets()->get('de.standard')` or, across all installed packs, `$quittio->countries()->taxPreset('de.standard')`. `de.exempt` is not part of the collection. ### Small businesses A small business (Kleinunternehmer, section 19 UStG) charges no VAT. Its invoices state the exemption instead. Three steps make an invoice a correct small business invoice: 1. **Mark the seller as a small business.** Set `quittio.seller.small_business = true` (applied by `Quittio::invoice()`), the Managed issuer setting `tax_settings.small_business`, or `->flag(Meta::FLAG_SMALL_BUSINESS)`. `Flags::SMALL_BUSINESS` is the same flag (`small_business`). With the flag, a standard rated line, allowance or charge is an error (`DE-USTG-09`). The flag does not set presets or texts. 2. **Use `VatPreset::SmallBusiness` on every line.** Lines without `->vat()` get it automatically when the invoice has the small business flag and the default VAT comes from the country pack (`CountryDefaultVat`; in Laravel always, see [Default VAT](building-invoices.md#default-vat)). The preset's text (BT-120) appears in the PDF. 3. **Give a seller identifier if you have no VAT identifier.** Without a VAT identifier, EN 16931 needs a seller identifier (BT-29) or a legal registration (BT-30) (rule BR-CO-26). In Laravel set `quittio.seller.identifiers` or `legal_registration` (see [Configuration](configuration.md)). ```php use Quittio\Core\Enums\UnitCode; use Quittio\Laravel\Facades\Quittio; $builder = Quittio::invoice() // quittio.seller.small_business = true ->line('Consulting', netPrice: '80.00', unit: UnitCode::Hour); // VatPreset::SmallBusiness by default ``` What the pack checks: - An invoice whose lines all use the preset is a small business invoice under section 34a UStDV. It needs no service date (`DE-USTG-03`), and the PDF is not checked for one. The tax number or VAT identifier is still required (`DE-USTG-02`). - A small business cannot use the exemption for intra-community supplies (category `K`; `DE-USTG-14`, section 19 (1) sentence 2 UStG). The only exception is the intra-community supply of a new vehicle (`->de()->newVehicle()`, section 19 (6) sentence 1 UStG). A complete invoice is in [Examples: Small business](examples.md#small-business). ### Reverse charge, intra-community supplies and VAT identifiers With reverse charge (German: Steuerschuldnerschaft des Leistungsempfängers), the buyer owes the VAT instead of the seller. The invoice shows no VAT and must say why. An intra-community supply (innergemeinschaftliche Lieferung) is a VAT-exempt delivery of goods to a business in another EU member state. Use the presets `ReverseChargeDomestic`, `ReverseChargeEu`, `TriangularTransaction` and `IntraCommunitySupply` for these cases ([Tax presets](#tax-presets)). The pack then checks: - **Wording.** Reverse charge rows (category `AE`) must name "Steuerschuldnerschaft des Leistungsempfängers" (`DE-USTG-06`, warning). The texts of the reverse charge presets are accepted in every locale the pack ships, so English invoices with the presets pass as well. Your own texts must contain "Steuerschuldnerschaft". - **VAT identifiers.** Reverse charge in another member state and triangular transactions need the VAT identifiers of seller and buyer (`DE-USTG-11`). The rule does not apply to `ReverseChargeDomestic` (section 13b UStG). - **Deadline.** Invoices for intra-community supplies and for services in another member state (`ReverseChargeEu`) must be issued by the 15th of the month following the supply (`DE-USTG-15`, warning; section 14a (1) sentence 2 and (3) sentence 1 UStG). - **PDF.** The PDF must show the reverse charge or exemption note and the VAT identifiers (see [Mandatory PDF output](#mandatory-pdf-output)). - **Tax representative.** A tax representative of the seller (BG-11, `->taxRepresentative($name, $vatId, $address)`) satisfies `DE-USTG-02` and `BR-DE-16`. Its VAT identifier satisfies `DE-USTG-11` and the mandatory output. - **Number formats.** The tax number (Steuernummer, BT-32) of a seller in Germany is checked for format and check digit (`DE-TAX-NUMBER`, see [Tax number](#tax-number)). VAT identifiers are checked by the core plausibility rules: `QUITTIO-VATID-FORMAT` (for `DE` nine digits) and optionally `QUITTIO-VIES`, an online check against the EU's VIES service. The same rules warn about category `K` with a domestic or non-EU destination, and about `G` within the EU (see [Validation](validation.md#plausibility-l7)). Complete invoices are in [Examples: EU business: services](examples.md#eu-business-services), [EU business: goods](examples.md#eu-business-goods) and [Export](examples.md#export). ### Format choice Without `->format()`, Quittio chooses the format for each invoice ([Automatic choice](formats.md#automatic-choice)). The German pack adds only the cases in which German law prescribes a format. `Quittio\Country\DE\Formats\GermanFormatRule` returns a choice for these cases and null (no opinion) otherwise: | Condition | Format | Profile | |---|---|---| | The buyer reference (BT-10) is a Leitweg-ID with valid check digits, or the buyer's electronic address (BT-49) uses scheme `0204` (`GermanFormatRule::LEITWEG_SCHEME`) | `xrechnung-cii` | `xrechnung` | | A small amount invoice whose simplifications apply (`UstgRules::isSmallAmountInvoice()`), with seller and buyer in the German Inland (domestic territory under section 1 (2) UStG, i.e. without Büsingen and Heligoland; an address without country code counts as domestic) | `pdf` | – | | otherwise | – (null) | – | A Leitweg-ID is the routing ID of a German public buyer, such as an authority or a municipality. The buyer names it in the order, and it goes into the buyer reference (BT-10). Its last two digits are check digits. ```php use Quittio\Core\Builder\Party; use Quittio\Laravel\Facades\Quittio; $builder = Quittio::invoice() ->buyerReference('04011000-12345-03') // a Leitweg-ID: XRechnung ->buyer(Party::make('Stadt Beispielstadt') ->electronicAddress('04011000-12345-03', '0204')); // or the same ID as electronic address (scheme 0204) ``` - Order numbers shaped like a Leitweg-ID (e.g. `4711-01`) do not switch to XRechnung. A Leitweg-ID with a typo does not either; validation reports it as `DE-LEITWEG-ID-SUSPECT` (info, see [Leitweg-ID](#leitweg-id)). Correct it, set the `0204` endpoint as well, or choose the format explicitly (`->format('xrechnung-ubl')`). - An explicit `->format()` or a configured `quittio.defaults.format` always wins. Everything else follows the [general order of the resolver](formats.md#automatic-choice): - **Domestic business invoices:** the Peppol lookup of the resolver, if one is configured, then `zugferd` with the configured profile (`en16931` by default; EXTENDED when EXTENDED features are used). - **Consumers:** `pdf`. - **Cross-border invoices:** the e-invoicing obligation of section 14 (2) UStG covers only supplies between businesses established in Germany. Sellers outside the EU invoicing a German business get `pdf` from the resolver, sellers in other member states `zugferd`. Buyers in Büsingen or Heligoland are outside the EU VAT territory and get `pdf`. - **German public buyers** receive XRechnung whatever the seller's country: for cross-border invoices to a buyer in Germany, the rule runs as the rule of the buyer's pack. A complete invoice to a public buyer is in [Examples: Public sector](examples.md#public-sector). ### XRechnung profiles XRechnung is the German e-invoice standard for public buyers: EN 16931 with additional German rules, as an XML file in the CII or UBL syntax (see [Formats](concepts.md#formats)). The pack registers two profiles for it. XRechnung 3.0 is the one you normally use. The extension allows data that XRechnung 3.0 does not, such as sub lines. How Quittio writes XRechnung files is described in [Formats: XRechnung](formats.md#xrechnung). | | `XRechnung30` | `XRechnung30Extension` | |---|---|---| | Key, name | `xrechnung`, XRechnung 3.0 | `xrechnung-extension`, XRechnung 3.0 Extension | | Specification identifier (BT-24), which names the profile in the XML | `urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0` | `urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0#conformant#urn:xeinkauf.de:kosit:extension:xrechnung_3.0` | | Level, syntaxes | EN 16931; CII and UBL | as XRechnung 3.0; sub lines only in UBL | | Default business process (BT-23) | `urn:fdc:peppol.eu:2017:poacc:billing:01:1.0` | same | | Additionally required | buyer reference (BT-10), seller electronic address (BT-34), buyer electronic address (BT-49), seller contact (BG-6), payment instructions (BG-16) | same | | Embedded in a PDF | `xrechnung.xml`, XMP conformance level `XRECHNUNG`, `fx:Version` `3.0` | same | | Extras | VAT rate written in every breakdown, 0 for category `O` (BR-DE-14); cash discounts as `#SKONTO#` lines in the payment terms (BT-20); the [XRechnung rules](#xrechnung-rules) | sub lines, third party payments (BG-DEX-09, UBL invoices only), identifier schemes `XR01` to `XR03` in the ICD and EAS code lists | The profiles exist only when the pack is registered. Without it, the formats `xrechnung-cii` and `xrechnung-ubl` cannot be rendered, and incoming XRechnung files are reported as an unknown profile (`QUITTIO-READER-UNKNOWN-PROFILE`). ```php use Quittio\Laravel\Facades\Quittio; Quittio::make($invoice)->format('xrechnung-ubl')->render(); // XRechnung 3.0, UBL Quittio::make($invoice)->format('xrechnung-ubl')->profile('xrechnung-extension')->render(); // extension Quittio::make($invoice)->format('zugferd')->profile('xrechnung')->render(); // PDF/A-3 with xrechnung.xml ``` The automatic choice always uses `xrechnung`, also when the invoice carries extension data: the specification identifier never switches by itself. Choose `xrechnung-extension` explicitly for extension data. With `xrechnung`, third party payments fail with `QUITTIO-PROFILE-XRECHNUNG-EXTENSION-ONLY`. ### XRechnung rules `Quittio\Country\DE\Rules\XRechnungRules` (level L3) are the business rules of the XRechnung profiles, with codes `BR-DE-…`. They apply whenever the profile is `xrechnung` or `xrechnung-extension`, whatever the seller's country. The severity follows the flags of the official Schematron, the rule file of the official validator. Rules flagged `fatal` ("muss", must) are errors, rules flagged `warning` ("soll", should) are warnings, and `BR-DE-TMP-32` (flagged `information`) is an info. The KoSIT validator (the official XRechnung validator) remains authoritative (see [Validation](validation.md)). | Rule | Severity | Reported when | |---|---|---| | `BR-DE-1` | error | no payment instructions (BG-16) | | `BR-DE-2` | error | no seller contact (BG-6) | | `BR-DE-3`, `BR-DE-4` | error | seller city (BT-37) or postcode (BT-38) missing | | `BR-DE-5`, `BR-DE-6`, `BR-DE-7` | error | seller contact without name (BT-41), phone (BT-42) or e-mail (BT-43) | | `BR-DE-8`, `BR-DE-9` | error | buyer city (BT-52) or postcode (BT-53) missing | | `BR-DE-10`, `BR-DE-11` | error | a deliver-to address (BG-15) without city (BT-77) or postcode (BT-78) | | `BR-DE-15` | error | no buyer reference (BT-10); for public buyers this is the Leitweg-ID | | `BR-DE-16` | error | a category other than `O` is used and the seller has no VAT identifier (BT-31), tax number (BT-32) or tax representative (BG-11) | | `BR-DE-17` | warning | type code (BT-3) not one of 326, 380, 384, 389, 381, 875, 876, 877 | | `BR-DE-18` | error | a line of the payment terms text (BT-20) starts with `#` but does not match `#SKONTO#TAGE=n#PROZENT=n.nn#` (optionally `BASISBETRAG=n.nn#`) | | `BR-DE-19` | warning | SEPA credit transfer (58) with an invalid IBAN (BT-84) | | `BR-DE-20` | warning | SEPA direct debit (59) with an invalid debited IBAN (BT-91) | | `BR-DE-22` | error | attachment file names (BT-125-2) not unique | | `BR-DE-23-a`, `BR-DE-23-b` | error | credit transfer (30, 58) without credit transfer data (BG-17), or with card (BG-18) or direct debit (BG-19) data | | `BR-DE-24-a`, `BR-DE-24-b` | error | card payment (48, 54, 55) without card data (BG-18), or with BG-17 or BG-19 data | | `BR-DE-25-a`, `BR-DE-25-b` | error | direct debit (59) without direct debit data (BG-19), or with BG-17 or BG-18 data | | `BR-DE-26` | warning | corrected invoice (384) without preceding invoice reference (BG-3) | | `BR-DE-27` | warning | seller phone (BT-42) with fewer than three digits | | `BR-DE-28` | warning | seller e-mail (BT-43) without exactly one `@` surrounded by at least two characters | | `BR-DE-30`, `BR-DE-31` | error | direct debit (BG-19) without creditor identifier (BT-90) or debited account (BT-91) | | `BR-DE-TMP-32` | info (flag `information` in the Schematron) | no delivery date (BT-72), invoicing period (BG-14) or period on every line (BG-26) | | `BR-DEX-15` | error | sub lines in XRechnung CII (sub lines need the extension in UBL) | | `DE-XRECHNUNG-EXT-01` | error | a credit note in profile `xrechnung-extension` with syntax UBL; the official validator configuration has no scenario for it. Cancel with the strategy `negative_invoice` (type code 380, negative amounts). | `#SKONTO#` lines state a cash discount (Skonto) in a form software can read: `TAGE` is the number of days, `PROZENT` the percentage and `BASISBETRAG` the base amount. Quittio writes structured cash discounts itself (`#SKONTO#TAGE=7#PROZENT=2.00#`), so `BR-DE-18` only concerns lines you put into the payment terms text yourself. ### Small amount invoices German law allows a simplified invoice for small amounts (section 33 UStDV). `->de()->smallAmount()` marks an invoice of at most 250.00 EUR gross as such a small amount invoice; the limit of the issue date applies (`TaxTables::SMALL_AMOUNT_LIMIT`: 150.00 EUR from 2007, 250.00 EUR from 2017). As long as no reverse charge (`AE`) or intra-community supply (`K`) is involved, it relaxes: - `DE-USTG-01`: the buyer's address is not required (the seller's still is); - `DE-USTG-02`: no tax number or VAT identifier is required; - `DE-USTG-03`: no service date is required; - the mandatory PDF output for the tax number and the service date; - the format: `pdf` instead of `zugferd` when seller and buyer are in the German Inland. A Leitweg-ID still leads to XRechnung (see [Format choice](#format-choice)). `DE-USTG-08` enforces the limit. `DE-USTG-12` excludes reverse charge and intra-community supplies (section 33 sentence 3 UStDV): such invoices need all mandatory information and are not rendered as plain PDF. Possible distance sales (goods sent to a consumer in another member state, section 3c UStG) keep the simplifications, with a warning. ### Builder macros `Quittio\Country\DE\Builder\GermanInvoiceMacros` adds methods for German special cases to `InvoiceBuilder`, and so to `Quittio::invoice()`: | Macro | Use it for | Writes into the invoice | |---|---|---| | `smallAmount()` | an invoice of up to 250 € gross ([Small amount invoices](#small-amount-invoices)) | flag `de.small_amount` | | `propertyRelatedService(string $locale = 'de')` | a service in connection with a property for a non-business buyer, who must keep the invoice for two years | flag `de.property_related_service` and the note `LegalTexts::PROPERTY_RETENTION` (subject `REG`) | | `newVehicle(string $locale = 'de')` | the intra-community supply of a new vehicle | flag `de.new_vehicle` and the note `LegalTexts::NEW_VEHICLE` (subject `REG`). The vehicle details are your line attributes (`DE-USTG-07`). The vehicle lines use `VatPreset::IntraCommunitySupply` (`DE-USTG-13`), also for small businesses (`DE-USTG-14`). | | `selfBilled(string $locale = 'de')` | a self-billed invoice ([Self-billing](#self-billing-gutschrift)) | type code 389 and the note `LegalTexts::SELF_BILLING` (subject `REG`) | | `deductAdvance(InvoiceData $advance, string $locale = 'de')` | a final invoice that deducts an advance invoice ([Final invoices](#final-invoices-deductadvance)) | preceding invoice (BG-3), prepaid amount (BT-113) and the note `LegalTexts::ADVANCE_DEDUCTION` (subject `REG`) | | `consumerDefaultNotice(string $locale = 'de')` | the notice that a consumer is in default 30 days after the due date (section 286 (3) BGB) | the note `LegalTexts::CONSUMER_DEFAULT` (subject `PMT`) | The notes and their texts are listed in [Legal texts](#legal-texts). Macros are invisible to IDEs and static analysis. The pack ships a stub with their signatures, `stubs/InvoiceBuilder.stub.php`; it is never loaded at runtime. Add it to PHPStan (`vendor/quittio/quittio/packages/country-de/stubs/…` with the complete package): ```yaml parameters: stubFiles: - vendor/quittio/country-de/stubs/InvoiceBuilder.stub.php ``` - The `$locale` parameter selects the language of the note. It is independent of the invoice locale. - A locale with a region uses the text of its language (`en_GB` → English). Locales without a text of the pack get the German text (`LegalTexts::text()`). - Notes with subject `REG` (regulatory information) are listed in the PDF's legal notes (`legalNotes`). Notes with subject `PMT` (payment information) appear with the other notes. #### Final invoices (`deductAdvance`) A final invoice (Schlussrechnung, section 14 (5) sentence 2 UStG) states the full service with its full VAT and deducts the advance payments received. `->de()->deductAdvance($advance)` does this for one advance invoice: ```php use Quittio\Core\Enums\UnitCode; use Quittio\Country\DE\Tax\VatPreset; use Quittio\Laravel\Facades\Quittio; $advance = $advanceResult->data(); // the rendered advance invoice, already calculated $final = Quittio::invoice() // number, dates, buyer, payment … ->line('Website', netPrice: '3000.00', unit: UnitCode::Piece, vat: VatPreset::Standard) ->de()->deductAdvance($advance); ``` The advance invoice must be calculated. The data of a rendered invoice (`$result->data()`) already is; calculate data you built yourself with `(new En16931Calculator)->calculate($advanceInvoice)` (`Quittio\Core\Calculation\En16931Calculator`). With an advance invoice RE-2026-0040 of 1,000.00 EUR net plus 19 % VAT, the final invoice shows a gross total of 3,570.00 EUR, VAT 570.00 EUR, prepaid 1,190.00 EUR and an amount due of 2,380.00 EUR, with the note: > Abzüglich Anzahlung gemäß Rechnung RE-2026-0040 vom 01.09.2026: Entgelt 1.000,00 € zzgl. 19 % Umsatzsteuer 190,00 € (§ 14 Abs. 5 Satz 2 UStG). With `->de()->deductAdvance($advance, 'en')` the note is in English: "Less advance payment according to invoice RE-2026-0040 of 01/09/2026: net 1,000.00 € plus 19% VAT 190.00 € (section 14 (5) sentence 2 UStG)." - **Checks.** The advance invoice must be calculated (`totals` set), have a positive gross total and be in the currency of the final invoice. Otherwise an `InvalidArgumentException` is thrown. Set `->currency()` before calling the macro. - **Accepted documents.** Only documents that can be advance invoices are accepted (`GermanInvoiceMacros::ADVANCE_TYPES`): type codes 380 (commercial invoice), 386 (prepayment invoice), 326 (partial invoice), 875 (partial construction invoice) and 389 (self-billed invoice). Credit notes, debit notes, corrected invoices (384), final invoices (876, 877) and cancellations by negative invoice are rejected. - **Amount.** The deducted amount is the advance invoice's gross total plus its cash rounding (BT-114). - **Itemisation.** Every VAT row of the advance invoice is itemised. Standard rated rows read "Entgelt … zzgl. … Umsatzsteuer …" ("net … plus … VAT …", `LegalTexts::ADVANCE_DEDUCTION_TAXED`), other rows "Entgelt … ohne Umsatzsteuer" ("net … without VAT", `LegalTexts::ADVANCE_DEDUCTION_UNTAXED`). Rows are separated by semicolons. - **Language.** The whole note is in one language: the language of `$locale` if the pack has it (`de`, `en`), otherwise German. Amounts and dates are formatted in `$locale` when the note is in its language (e.g. `de_AT`). Otherwise they are formatted in the language of the note: `fr` gives the German note with German formatting. - **Several advance invoices.** Call the macro once per advance invoice; the prepaid amounts add up. - **Payment.** The macro does not check whether the advance invoice was paid. Deduct only payments you received. A complete example with the advance invoice is in [Examples: Advance payment and final invoice](examples.md#advance-payment-and-final-invoice). ### Self-billing (Gutschrift) In self-billing, the buyer issues the invoice for the seller's supply. German law requires the word "Gutschrift" on such an invoice (section 14 (4) no. 10 UStG). Quittio uses "Gutschrift" only for self-billing; commercial credit notes and cancellations carry other titles. - `->de()->selfBilled()` sets type code 389 and adds the note "Gutschrift" (English variant: "Gutschrift (self-billed invoice)"). - The PDF is titled "Gutschrift" in German and "Self-billed invoice (Gutschrift)" in English, and shows the buyer as issuer. - `DE-USTG-04` requires the label on every self-billed type code (389, 261, 527). - The mandatory output requires the title and the "Gutschrift" note in the PDF. A complete example is in [Examples: Self-billing](examples.md#self-billing). ### Cancellations and corrections `CountryDE::correctionPolicy()` sets how invoices of a German seller are cancelled and corrected (see [Building invoices: cancellations and corrections](building-invoices.md#cancellations-and-corrections)): - **Cancellation strategy `credit_note`:** a cancellation is a credit note (type code 381) with positive amounts. - **Correction strategy `cancel_and_reissue`:** the Managed layer corrects an invoice by cancelling it and issuing a new one. Both cancellation strategies are permitted in Germany. Change them with `quittio.corrections.cancellation_strategy` and `quittio-managed.correction.strategy`. For the XRechnung extension in UBL use `negative_invoice` (`DE-XRECHNUNG-EXT-01`). The policy also sets the document titles: | Kind | German | English | |---|---|---| | `cancellation` | Stornorechnung | Cancellation invoice | | `cancellation_self_billed` | Storno-Gutschrift | Cancellation of the self-billed invoice (Storno-Gutschrift) | | `correction` | Rechnungskorrektur | Corrected invoice | | `self_billed` | Gutschrift | Self-billed invoice (Gutschrift) | - `cancellation` and `cancellation_self_billed` title the documents of `Quittio::cancellation()`. - `correction` titles a Managed corrected invoice (384). - `self_billed` titles self-billed invoices (389, 261, 527) unless the presentation sets a title. Locales without a title of their own fall back to English ("Self-billed invoice (Gutschrift)"), which keeps the word "Gutschrift" required by section 14 (4) no. 10 UStG. ### Dunning law When a customer pays late, German law (section 288 BGB) gives the seller default interest (Verzugszinsen) and, from business debtors, a flat rate of 40 EUR. The pack provides these values and a calculator for the interest. Quittio's documents do not apply them by themselves: calculate the interest with [`DefaultInterest`](#default-interest) and pass the results to a payment reminder (see [Documents: payment reminders](documents.md#payment-reminders)). `CountryDE::dunningLaw()` returns these values: | Value | Germany | |---|---| | Interest for business debtors | base rate plus 9 percentage points | | Interest for consumers | base rate plus 5 percentage points | | Flat rate for business debtors | 40.00 EUR | | Day count | 365 | | Consumer default | 30 days after due date and receipt of the invoice, if the invoice points this out (`->de()->consumerDefaultNotice()`) | | Reference rate | "Basiszinssatz (§ 247 BGB)", the base rate of section 247 BGB | #### Default interest `Quittio\Country\DE\Dunning\DefaultInterest` calculates the default interest on an open amount: | Method | Result | |---|---| | `__construct(?BaseRateTable $rates = null, ?DunningLaw $law = null)` | Without a table, `BaseRates::table()` is used (shipped and registered rates). Without a law, `CountryDE::dunningLaw()` (points, flat rate and day count). | | `calculate(Money $amount, Date $from, Date $until, bool $consumer = false, string $locale = 'de'): list` | One charge per base rate period, from `$from` (first day with interest) to `$until` (exclusive). Rate = base rate + 9 points, or + 5 points with `$consumer`. Empty for a non-positive amount or when `$until` is not after `$from`. `InvalidArgumentException` for days the [base rate table](#base-rate-table) does not cover. | | `flatRate(string $currency = 'EUR'): ?Money` | The flat rate of 40.00 for business debtors (section 288 (5) BGB), once per claim | ```php use Brick\Money\Money; use Quittio\Core\Support\Amount; use Quittio\Core\Support\Date; use Quittio\Country\DE\Dunning\DefaultInterest; use Quittio\Laravel\Facades\Quittio; $interest = new DefaultInterest; $reminder = Quittio::paymentReminder(1) // first reminder ("1. Mahnung"); seller and bank account from the config ->number('M-2026-0003') ->buyer($buyer) ->openItem('RE-2026-0042', '2026-06-01', '1190.00', dueDate: '2026-06-15'); // first day with interest: the day after the due date; 2026-10-04 is the first day without interest foreach ($interest->calculate(Money::of('1190.00', 'EUR'), Date::parse('2026-06-16'), Date::parse('2026-10-04')) as $charge) { $reminder->interest(Amount::format($charge->base), $charge->ratePercent, $charge->from, $charge->until, $charge->description, dayCount: $charge->dayCount); } $reminder->fee('Pauschale nach § 288 Abs. 5 BGB', Amount::format($interest->flatRate())); // "flat rate under section 288 (5) BGB" ``` - **Descriptions.** They name the rate, the base rate, the points and the last day with interest. With the locale `de` (the default) a description reads "Verzugszinsen 10,27 % p. a. (Basiszinssatz 1,27 % + 9,00 Prozentpunkte) vom 16.06.2026 bis 30.06.2026". Any other locale gives "Default interest 10.27 % p.a. (base rate 1.27 % + 9.00 points) from 2026-06-16 to 2026-06-30". - **Day count.** The charges use the day count of the dunning law (`InterestCharge::$dayCount`; 365 for `CountryDE::dunningLaw()`, so a leap year yields 366/365 of the annual rate). Pass it to `interest()` as above (`dayCount: $charge->dayCount`); without it the reminder counts actual/actual. - **Periods.** A charge covers a whole period with the same base rate. Interest from 1 December 2025 to 28 February 2026 is one charge, although the table has an entry for 1 January 2026. - **Flat rate.** It is due only from business debtors, once per claim. The documents layer does not track which claims already carried it. - **Consumers.** For consumers, `->de()->consumerDefaultNotice()` adds the notice on default after 30 days to the invoice. A complete reminder is in [Examples: Payment reminder](examples.md#payment-reminder). #### Base rate table The base rate (Basiszinssatz, section 247 BGB) changes on 1 January and 1 July. `Quittio\Country\DE\Dunning\BaseRateTable` holds the rates. Quittio ships these (`BaseRateTable::PUBLISHED`): | Valid from | Rate in % | |---|---| | 2023-01-01 | 1.62 | | 2023-07-01 | 3.12 | | 2024-01-01 | 3.62 | | 2024-07-01 | 3.37 | | 2025-01-01 | 2.27 | | 2025-07-01 | 1.27 | | 2026-01-01 | 1.27 | | 2026-07-01 | 1.52 | Rates after the last entry are unknown until they are published. Interest for days from 1 January 2027 is therefore rejected instead of silently using the old rate. There are three ways to add new rates without a new Quittio release: 1. **Laravel configuration.** Add the rate to `config/quittio-de.php` (publish it with `php artisan vendor:publish --tag=quittio-de-config`; see [Configuration](configuration.md#country-pack-germany-configquittio-dephp)): ```php 'base_rates' => ['2027-01-01' => '1.77'], ``` 2. **Automatic update from the Deutsche Bundesbank (Laravel).** `php artisan quittio:base-rate:update` fetches the series `BBIN1.M.DE.BBK.BBKBAS2.EUR.ME` ("Basiszinssatz gemäß BGB", the base rate under the BGB) from the Bundesbank's statistics API ([command](commands.md#quittiobase-rateupdate)). - It stores the rates the shipped table does not contain in `quittio-de.base_rate_file` (default `storage/app/quittio/base-rates.json`), also those before 2023 for older claims. - It writes the file only when something changed; `--dry-run` shows the rates without writing. - A value that differs from the shipped table is reported, and the Bundesbank value is used. - Without a file (`base_rate_file` set to null) the command fails, except with `--dry-run`. Schedule it, e.g. weekly, so that the rate of 1 January and 1 July is there as soon as the Bundesbank has published it: ```php // routes/console.php Schedule::command('quittio:base-rate:update')->weekly(); ``` 3. **Without Laravel.** Call `BaseRates::register(['2027-01-01' => '1.77'])` once at start-up, fetch the rates yourself with `(new BundesbankBaseRates)->fetch()`, or use a table for one calculation: ```php $rates = (new BaseRateTable)->withRate('2027-01-01', '1.77'); $interest = new DefaultInterest($rates); ``` `DefaultInterest` without a table uses `BaseRates::table()`: the shipped table, then the rates of the file, then those of `quittio-de.base_rates`. The configuration wins for the same date. The service provider `Quittio\Country\DE\Laravel\GermanyServiceProvider` registers file and configuration at boot; Laravel discovers it automatically. `BaseRateTable`: | Method | Result | |---|---| | `__construct(array $rates = self::PUBLISHED)` | ISO date => rate in percent, sorted by date | | `withRate(Date\|string $from, string $rate): self` | new table with an additional or replaced rate | | `rateOn(Date $date): BigDecimal` | rate on a day; `InvalidArgumentException` before the first entry and from `validUntil()` on | | `periods(Date $from, Date $until): list` | periods with a constant rate (start inclusive, end exclusive), split where the rate changes. A table entry that keeps the rate (e.g. 2026-01-01) starts no new period. `InvalidArgumentException` if `$until` is after `validUntil()`. | | `lastChange(): Date` | date of the last entry, e.g. to warn when the table is outdated | | `validUntil(): Date` | first day without a known rate: the next 1 January or 1 July after the last entry (with the published table: 2027-01-01) | `Quittio\Country\DE\Dunning\BaseRates`: | Method | Result | |---|---| | `register(array $rates): void` | adds or replaces rates (ISO date => percent). Only 1 January and 1 July, and values such as `'1.77'` or `'-0.88'`; otherwise `InvalidArgumentException`. | | `table(): BaseRateTable` | the shipped table with the registered rates | | `registered(): array` / `reset(): void` | the registered rates; removes them (tests) | `Quittio\Country\DE\Dunning\BundesbankBaseRates`: | Method | Result | |---|---| | `__construct(?Closure $http = null, int $timeout = 15)` | `$http` receives URL and timeout and returns the response body (for tests or your own HTTP client); default `file_get_contents()` | | `fetch(string $since = '2002-01'): array` | the January and July values from the given month on, ISO date => percent; `RuntimeException` when the API cannot be reached or returns no rates | | `parse(string $csv): array` | reads the CSV download (`2026-07;1,52;…`) | ### DIN 5008 letter layouts DIN 5008 is the German standard for business letters. It fixes where the address field sits, so that the address shows in the window of a window envelope, and where the fold and punch marks are. `Quittio\Country\DE\Layouts\Din5008` provides two forms: form A (`din5008a`) with a low letterhead and form B (`din5008b`) with a higher one. Form B is the German default. `Din5008` has the methods `formA()`, `formB()` and `all()` (form A, form B and `free`). Values are in millimetres, as in DIN 5008:2020. They are unchanged since the 2011 edition, which enlarged the address field of both forms from 40 to 45 mm and moved the return address line (Rücksendeangabe) into the zone for additional information and notes (Zusatz- und Vermerkzone). | | `din5008a` (`Din5008::FORM_A`) | `din5008b` (`Din5008::FORM_B`) | |---|---|---| | Address field from top / left | 27 / 20 | 45 / 20 | | Address field width × height | 85 × 45 | 85 × 45 | | Zone for additional information and notes (Zusatz- und Vermerkzone), above the recipient | 17.7 (five lines, filled from the bottom); its last 5 mm hold the sender line (Rücksendeangabe) | same | | Recipient zone (Anschriftzone, six lines) | 27.3, from 44.7 | 27.3, from 62.7 | | Fold marks | 87 and 192 | 105 and 210 | | Punch mark | 148.5 | 148.5 | | Letterhead height in the default templates (top margin 15) | 12 | 30 | - The address field keeps its position when the invoice has a logo. - The default templates print the sender line at the top of the address field and the recipient directly below it, inside the envelope window. The zones above are for your own templates. - Select a layout per invoice with `->presentation(['letterLayout' => 'din5008a'])`, or for all invoices with `quittio.pdf.layout`. - Fold and punch marks are printed unless `quittio.pdf.fold_marks` (core option `fold_marks`) is `false`. See [Templates: letter layouts](templates.md#letter-layouts). ### UStG rules `Quittio\Country\DE\Rules\UstgRules::all()` (level L3) checks the mandatory content of an invoice under the German VAT Act (sections 14 (4) and 14a UStG) and the simplifications of sections 33 and 34a UStDV. The rules apply to every invoice of a German seller, in every format including a plain PDF. Messages and hints are in German and English. You rarely need this table up front: a failing rule names its code, the field and a hint. Look the code up here when the hint is not enough. | Rule | Severity (BT, path) | Reported when | Legal basis | Fix | |---|---|---|---|---| | `DE-USTG-01` | error (BG-5 `seller.address`, BG-8 `buyer.address`) | One message per party that names the missing fields. The seller lacks street (address line 1), postcode or city. Or the buyer lacks street or city, or a buyer in `DE` lacks the postcode. The buyer part is skipped for small amount invoices. | section 14 (4) no. 1 UStG | Add street, postcode and city for seller and buyer. | | `DE-USTG-02` | error (BT-31 `seller.vatId`) | The seller has neither a VAT identifier (BT-31) nor a tax number (BT-32), and there is no tax representative (BG-11). Skipped for small amount invoices. | section 14 (4) no. 2 UStG | `->vatId()` or `->taxNumber()` on the seller. | | `DE-USTG-03` | error (BT-72 `delivery.actualDeliveryDate`); warning for prepayment invoices (386) | No delivery date (BT-72), no invoicing period (BG-14), and not every line has a period (BG-26). Skipped for small amount invoices (section 33 UStDV) and small business invoices (section 34a UStDV). | section 14 (4) no. 6 UStG | `->deliveryDate()` (also when it equals the invoice date), `->invoicingPeriod()` or a `->period()` on every line. | | `DE-USTG-04` | error (BT-22 `notes`) | A self-billed type code (389, 261, 527), and no note contains "Gutschrift" or one of: self-billing, self billing, self-billed invoice, autofacturation, autofactura, autofatturazione, autofacturação, zelffacturering, samofakturowanie (case-insensitive). | section 14 (4) no. 10 UStG; other EU languages: section 14.5 (24) UStAE | `->de()->selfBilled()` adds the note. | | `DE-USTG-05` | error (BT-22 `notes`) | The flag `de.property_related_service` is set and no note contains "14b". | section 14 (4) no. 9 UStG | `->de()->propertyRelatedService()` adds the note. | | `DE-USTG-06` | warning per VAT row (BT-120 `vatBreakdown[i].exemptionReason`) | A VAT breakdown row of category `AE` whose exemption reason contains neither "Steuerschuldnerschaft" nor the text of one of the presets `ReverseChargeDomestic`, `ReverseChargeEu` and `TriangularTransaction`. The preset texts count in every locale the pack ships (`de`, `en`), the invoice locale and the validation locale (case-insensitive). | section 14a (1) and (5) UStG | Use `ReverseChargeDomestic`, `ReverseChargeEu` or `TriangularTransaction`. | | `DE-USTG-07` | error per line (BG-32 `lines[i].item.attributes`) | The flag `de.new_vehicle` is set and a line has no item attributes. | section 14a (4) UStG | `->attribute('Erstzulassung', …)` (first registration), `->attribute('Kilometerstand', …)` (mileage) on every line. | | `DE-USTG-08` | error (BT-112 `totals.grossTotal`) | The flag `de.small_amount` is set and the gross total exceeds 250.00 EUR. Other currencies count with their EUR equivalent: gross total × exchange rate of `->taxCurrency('EUR', $rate)`. Without that rate the rule always fails. | section 33 UStDV | Remove `->de()->smallAmount()` and issue a full invoice. | | `DE-USTG-09` | error (BT-151 `lines`, or BT-102 `charges` for document level entries) | A line uses `VatPreset::SmallBusiness` or the invoice has the small business flag, and a line (sub lines included) or a document level allowance or charge has category `S`. | section 19 UStG; VAT shown on charges counts too (section 14c (2) UStG) | `VatPreset::SmallBusiness` on every line, no standard rated charges. | | `DE-USTG-10` | warning (BT-6 `taxCurrency`) | The invoice currency is not EUR, the VAT total is not zero, and the VAT accounting currency (BT-6) is not EUR. | Article 230 VAT Directive, section 16 (6) UStG | `->taxCurrency('EUR', $rate, $date)`. | | `DE-USTG-11` | error (BT-31 `seller.vatId`, BT-48 `buyer.vatId`) | A line uses `ReverseChargeEu` or `TriangularTransaction`, or a line of category `AE` without preset has a buyer in the VAT territory of another member state (for services Monaco counts as France, and Northern Ireland does not count). Then the VAT identifiers of the seller (or of the tax representative) and of the buyer are required; each missing one is reported. A tax number is not sufficient. | section 14a (1) and (7) UStG | `->vatId()` for seller and buyer. | | `DE-USTG-12` | error (BT-151 `lines`); warning for possible distance sales | The flag `de.small_amount` is set and category `AE` or `K` is used (error). Or the buyer is a consumer and the destination lies in another member state (warning: a distance sale only if goods are transported there). The destination is the deliver-to address if it has a country, otherwise the buyer's address. | section 33 sentence 3 UStDV; section 3c UStG | Remove `->de()->smallAmount()` and issue a full invoice. | | `DE-USTG-13` | warning (BT-151 `lines`) | The flag `de.new_vehicle` is set and no line, allowance or charge uses category `K`. A warning, as the exemption may not apply in the individual case. | section 14a (4) UStG | `VatPreset::IntraCommunitySupply` on the vehicle lines, or remove `->de()->newVehicle()`. | | `DE-USTG-14` | error (BT-151 `lines`) | A line uses `VatPreset::SmallBusiness` or the invoice has the small business flag, a line, allowance or charge uses category `K`, and the flag `de.new_vehicle` is not set. | section 19 (1) sentence 2, (6) sentence 1 UStG | `VatPreset::SmallBusiness` on the lines; `->de()->newVehicle()` for the intra-community supply of a new vehicle. | | `DE-USTG-15` | warning (BT-2 `issueDate`) | A line, allowance or charge uses category `K`, or a line uses `ReverseChargeEu` (or category `AE` without preset for a buyer in another member state), and the invoice date is after the 15th of the month following the supply. The supply date is the latest of the delivery date (BT-72), the end of the invoicing period (BG-14) and the ends of the line periods (BG-26). Skipped without a supply date, for documents that refer to a preceding invoice (BG-3: cancellations, corrections, re-issued and final invoices) and for the type codes 381, 261, 384 and 386. A warning, as a late invoice stays valid. | section 14a (1) sentence 2 and (3) sentence 1 UStG | Issue these invoices by the 15th of the following month. | Helpers used by the rules and available to you: - `UstgRules::isSmallAmountInvoice($invoice)`: the invoice has the small amount flag and its simplifications apply (no category `AE` or `K`); - `UstgRules::isSmallBusinessInvoice($invoice)`: every line uses `VatPreset::SmallBusiness`; - `UstgRules::hasServiceDate($invoice)`: delivery date, invoicing period or a period on every line; - `UstgRules::issuingDeadline($invoice)`: the 15th of the month after the supply, or `null` without a supply date. #### Leitweg-ID `UstgRules::plausibility()` (level L7) checks buyer references (BT-10 `buyerReference`) that are shaped like a Leitweg-ID but have wrong check digits: | Rule | Severity | Reported when | |---|---|---| | `DE-LEITWEG-ID` | warning | The buyer is a public buyer: the profile is `xrechnung` or `xrechnung-extension`, or the buyer's electronic address (BT-49) uses scheme `0204`. Message: "The check digits of the Leitweg-ID ":value" (BT-10) are wrong." | | `DE-LEITWEG-ID-SUSPECT` | info | Any other buyer in Germany (country `DE` or none) that is not a consumer. Message: "The buyer reference ":value" (BT-10) is shaped like a Leitweg-ID, but its check digits are wrong. It is not treated as a Leitweg-ID, so the invoice is not issued as XRechnung automatically." | So a mistyped Leitweg-ID does not lead to ZUGFeRD unnoticed. An order number of the same shape (e.g. `4711-01`) gets the same info, which you may ignore. Neither finding changes the format. `Quittio\Country\DE\Support\LeitwegId`: | Method | Result | |---|---| | `looksLike(?string $value): bool` | the shape: a coarse address of 2 to 12 digits, an optional fine address of up to 30 letters or digits and two check digits, separated by hyphens | | `isValid(?string $value): bool` | shape and check digits (ISO 7064 Mod 97-10) are correct | | `checkDigits(string $address): string` | the two check digits for coarse and fine address without hyphens, e.g. `checkDigits('0401100012345')` → `03` | #### Tax number `UstgRules::plausibility()` also checks the tax number (Steuernummer, BT-32) of a seller in Germany. The local tax office issues this number; it is not the VAT identifier. | Rule | Severity | Reported when | |---|---|---| | `DE-TAX-NUMBER` | warning (BT-32 `seller.taxNumber`) | The seller's country is `DE`, and the tax number is not a valid 13-digit ELSTER number, a 12-digit federal number or the notation of a federal state, or its check digit is wrong. Also reported when a VAT identifier (`DE…`) was given as tax number. A Bavarian notation without the tax office number (`815/08152`) cannot be checked and is accepted. | The checks follow the document "Prüfung der Steuer- und Steueridentifikationsnummer" (checking tax numbers and tax identification numbers) of ELSTER, the online service of the German tax authorities, published by the Bavarian State Office for Taxes (Bayerisches Landesamt für Steuern): - **Format.** The 13-digit ELSTER format is `FFFF0BBBUUUUP` (NRW: `FFFF0BBBBUUUP`), with the 0 at the 5th position. - **Excluded districts.** 000, 998 and 999; below 100 in the Bavarian group; in NRW 0000, 0998 and 0999, and `UUUP` must be above 0009. - **Check digit.** Each state has its procedure: the modified 11 procedure (Rhineland-Palatinate), the 2 procedure (Baden-Württemberg, Hesse, Schleswig-Holstein), or the 11 procedure with the factors of the state (all others; Berlin with the variants A and B by tax office and district). - **Not checked:** whether the tax office exists. - **Notation without state.** A notation without the state (e.g. `93/815/08150`) is valid when it is valid for any state that uses this notation. `Quittio\Country\DE\Support\TaxNumber`: | Method | Result | |---|---| | `TaxNumber::parse(string $value, ?string $state = null): ?TaxNumber` | the valid number in any notation (`198/815/08152`, `9198081508152`, `919881508152`, `053 815 08158` …), or null. `$state` (ISO 3166-2:DE, e.g. `BY`) restricts a state notation to that state. | | `TaxNumber::validate(string $value, ?string $state = null): ?bool` | `true` valid, `false` invalid, `null` plausible but not checkable | | `TaxNumber::looksLikeVatId(string $value): bool` | a VAT identifier (`DE` + 9 digits) | | `->elster`, `->state`, `->formatted()` | 13-digit ELSTER format, state, notation on tax notices (e.g. `198/815/08152`, `400/8150/8159`) | ### Mandatory PDF output After rendering, the mandatory field guard checks that the PDF shows certain values (see [Templates: mandatory field guard](templates.md#mandatory-field-guard)). `CountryDE::mandatoryOutput()` adds these German values to the base fields. Paths without a value are skipped. The legal basis is the value Quittio reports, in German citation form. | Condition | Access key | Description | Legal basis | |---|---|---|---| | not a small amount invoice, and the seller has a VAT identifier or tax number, or a tax representative with VAT identifier | `seller.vatId\|seller.taxNumber\|taxRepresentative.vatId` | tax number or VAT identifier of the seller | § 14 Abs. 4 Nr. 2 UStG | | not a small amount or small business invoice, with BT-72 or BG-14 | `servicePeriod\|deliveryDate\|delivery.date` | date of the supply or service | § 14 Abs. 4 Nr. 6 UStG | | the same, without BT-72 and BG-14 | `servicePeriod\|lineGroups[*].lines[*].period\|lines[*].period` | date of the supply or service | § 14 Abs. 4 Nr. 6 UStG | | self-billed type code | `title`, and the path of the first note containing "Gutschrift" | label and note "Gutschrift" | § 14 Abs. 4 Nr. 10 UStG | | flag `de.property_related_service` | path of the retention note (its text or a note containing "14b") | retention notice for property related services | § 14 Abs. 4 Nr. 9 UStG | | each VAT row of category `AE` or `K` | `vatRows[i].label` or the legal note with its BT-120 text | reverse charge or exemption note | § 14a UStG | | category `AE` or `K` used | `buyer.vatId` | VAT identifier of the buyer | § 14a Abs. 1 und 3 UStG | | category `K` used, or `AE` with a buyer country different from the seller's | `seller.vatId\|taxRepresentative.vatId` | VAT identifier of the seller | § 14a Abs. 1 und 3 UStG | The default templates print all of them. ### Legal texts `Quittio\Country\DE\Texts\LegalTexts::catalog()` (also `CountryDE::legalTexts()`) holds the German notices in German and English. The texts live in the pack's language files `resources/lang/{de,en}/legal.php` (the exemption reasons in `tax.php`) and are read through the translator, so in Laravel `lang/vendor/quittio-de/{locale}/legal.php` overrides single texts. Its fallback locale is `de` (`LegalTexts::FALLBACK_LOCALE`). Builder macros add some of them; texts marked "–" are for your own notes. | Constant (key) | German | English | Added by | |---|---|---|---| | `SELF_BILLING` (`self_billing`) | Gutschrift | Gutschrift (self-billed invoice) | `selfBilled()` | | `PROPERTY_RETENTION` (`property_retention`) | Sie sind gesetzlich verpflichtet, diese Rechnung, einen Zahlungsbeleg oder eine andere beweiskräftige Unterlage zwei Jahre lang aufzubewahren (§ 14b Abs. 1 Satz 5 UStG). | You are required by law to keep this invoice, a payment receipt or other conclusive document for two years (section 14b (1) sentence 5 UStG). | `propertyRelatedService()` | | `REVERSE_CHARGE` (`reverse_charge`) | Steuerschuldnerschaft des Leistungsempfängers | Reverse charge, the recipient is liable for VAT | – (text of `VatPreset::ReverseChargeEu`) | | `INTRA_COMMUNITY_SUPPLY` (`intra_community_supply`) | Steuerfreie innergemeinschaftliche Lieferung (§ 4 Nr. 1b i. V. m. § 6a UStG) | VAT-exempt intra-Community supply (section 4 no. 1b in conjunction with section 6a UStG) | – (text of `VatPreset::IntraCommunitySupply`) | | `EXPORT` (`export`) | Steuerfreie Ausfuhrlieferung (§ 4 Nr. 1a i. V. m. § 6 UStG) | VAT-exempt export supply (section 4 no. 1a in conjunction with section 6 UStG) | – (text of `VatPreset::Export`) | | `SMALL_BUSINESS` (`small_business`) | Steuerfreie Leistung nach § 19 Abs. 1 UStG (Steuerbefreiung für Kleinunternehmer). | VAT-exempt supply under section 19 (1) UStG (small business exemption). | – (text of `VatPreset::SmallBusiness`) | | `PREPAYMENT` (`prepayment`) | Die Umsatzsteuer entsteht mit Vereinnahmung der Anzahlung. Die Leistung wird voraussichtlich am :date erbracht. | VAT becomes due on receipt of the prepayment. The service is expected to be performed on :date. | – | | `ADVANCE_DEDUCTION` (`advance_deduction`) | Abzüglich Anzahlung gemäß Rechnung :number vom :date: :amounts (§ 14 Abs. 5 Satz 2 UStG). | Less advance payment according to invoice :number of :date: :amounts (section 14 (5) sentence 2 UStG). | `deductAdvance()` | | `ADVANCE_DEDUCTION_TAXED` (`advance_deduction_taxed`) | Entgelt :net zzgl. :rate Umsatzsteuer :vat | net :net plus :rate VAT :vat | `deductAdvance()`, part of `:amounts` | | `ADVANCE_DEDUCTION_UNTAXED` (`advance_deduction_untaxed`) | Entgelt :net ohne Umsatzsteuer | net :net without VAT | `deductAdvance()`, part of `:amounts` | | `CONSUMER_DEFAULT` (`consumer_default`) | Sie kommen spätestens in Verzug, wenn Sie nicht innerhalb von 30 Tagen nach Fälligkeit und Zugang dieser Rechnung zahlen (§ 286 Abs. 3 BGB). | You will be in default at the latest if you do not pay within 30 days of the due date and receipt of this invoice (section 286 (3) BGB). | `consumerDefaultNotice()` | | `DEFAULT_INTEREST` (`default_interest`) | Verzugszinsen :rate % p. a. (Basiszinssatz :base_rate % + :points Prozentpunkte) vom :from bis :until | Default interest :rate % p.a. (base rate :base_rate % + :points points) from :from to :until | `DefaultInterest` (interest lines of payment reminders; German numbers and dates for every German locale) | | `NEW_VEHICLE` (`new_vehicle`) | Innergemeinschaftliche Lieferung eines neuen Fahrzeugs (§ 14a Abs. 4 UStG); die Fahrzeugmerkmale nach § 1b Abs. 2 und 3 UStG sind bei der Position angegeben. | Intra-Community supply of a new means of transport (section 14a (4) UStG); the vehicle details under section 1b (2) and (3) UStG are stated with the line. | `newVehicle()` | - `REVERSE_CHARGE`, `INTRA_COMMUNITY_SUPPLY`, `EXPORT` and `SMALL_BUSINESS` are the exemption reasons of the named [tax presets](#tax-presets), in every locale the pack ships (`CountryDE::locales()`). So a notice has one wording in BT-120 and in your notes, including overrides of `tax.php`. - No text is added automatically by validation or rendering. - `LegalTexts::text($key, $locale, $replace)` returns a text in the language of `$locale` (`en_GB` → English) or in German. `LegalTexts::localeOf($key, $locale)` names the catalog locale it uses. A text for your own note, here on a prepayment invoice (386), which states that VAT is due on receipt of the payment and names the expected date of the service: ```php use Quittio\Core\Enums\NoteSubjectCode; use Quittio\Country\DE\Texts\LegalTexts; $builder->type('386')->note( LegalTexts::catalog()->get(LegalTexts::PREPAYMENT, 'de', ['date' => '01.11.2026']), NoteSubjectCode::RegulatoryInformation, ); ``` Prepayment invoices (386) without a service date get a `DE-USTG-03` warning instead of an error; name the expected date as above. ### Meta flags The pack marks German special cases with meta flags on the invoice (`InvoiceData::$meta->flags`). The builder macros set them, and the rules read them. `Quittio\Country\DE\Support\Flags`: | Constant | Value | Set by | Effect | |---|---|---|---| | `SMALL_AMOUNT` | `de.small_amount` | `smallAmount()` | small amount invoice: relaxed rules, PDF format, `DE-USTG-08`, `DE-USTG-12` | | `PROPERTY_RELATED_SERVICE` | `de.property_related_service` | `propertyRelatedService()` | `DE-USTG-05`, mandatory output | | `NEW_VEHICLE` | `de.new_vehicle` | `newVehicle()` | `DE-USTG-07`, `DE-USTG-13`, `DE-USTG-14` (exception) | | `SMALL_BUSINESS` | `small_business` (`Meta::FLAG_SMALL_BUSINESS`) | seller configuration, Managed issuer, `->flag()` | `DE-USTG-09`, `DE-USTG-14` | | `SMALL_AMOUNT_LIMIT` | `250.00` | – | deprecated: the limit depends on the issue date, see `TaxTables::smallAmountLimitOn()` | ### Retention `CountryDE::retention()` returns `new RetentionRule(8, fromEndOfCalendarYear: true)`: invoices are kept for 8 years from the end of the calendar year of issue (section 14b UStG, section 147 AO as amended in 2025). An invoice issued on 3 October 2026 is kept until 31 December 2034. The Managed layer stores this date (`retention_until`); `quittio-managed.retention.years` may only lengthen it. ### BMF error classes In its letter of 15 October 2025, the German Federal Ministry of Finance (BMF) sorts errors in e-invoices into three classes. `Quittio\Country\DE\Rules\BmfErrorClass` maps Quittio's findings to these classes ("Rz." is the marginal number in the letter): | Case | Value | Findings | `label('de')` | `label('en')` | |---|---|---|---|---| | `Format` | `format` | levels L1, L2, L4, L6 | Formatfehler (Rz. 6a) | Format error (marginal no. 6a) | | `BusinessRule` | `business_rule` | levels L3, L5 | Geschäftsregelfehler (Rz. 6b) | Business rule error (marginal no. 6b) | | `Content` | `content` | every `DE-USTG-*` finding and level L7 | Inhaltsfehler (Rz. 35a) | Content error (marginal no. 35a) | - `BmfErrorClass::of(ValidationMessage $message)` classifies one finding. - `label()` returns German for `de` and English otherwise. - `voidsEInvoice()` is true for `Format` only: the document is then not an e-invoice but an "other invoice" (sonstige Rechnung). - `(new CountryDE)->errorClassification($result)` groups the errors and warnings of a `ValidationResult` by class value: ```php use Quittio\Country\DE\CountryDE; $classes = (new CountryDE)->errorClassification($result); // ['format' => [...], 'business_rule' => [...], 'content' => [...]] ``` The validation levels are described in [Validation](validation.md); see also [Validation: Germany: BMF error classes](validation.md#germany-bmf-error-classes). ### Translations The pack ships German and English catalogs in `resources/lang/{de,en}/`: - `rules.php`: message and hint of every rule (`BR-DE-*`, `BR-DEX-15`, `DE-XRECHNUNG-EXT-01`, `DE-USTG-*`, `DE-LEITWEG-ID`, `DE-LEITWEG-ID-SUSPECT`, `DE-TAX-NUMBER`); - `tax.php`: the exemption reason texts of the presets, including `de.exempt` for `VatPreset::exempt()` (placeholder `:paragraph`). They are registered as the namespace `quittio-de` (e.g. `quittio-de::rules.DE-USTG-03.message`). `CountryDE::registerTranslations()` registers them lazily, so presets work without a `Quittio` instance. Other locales fall back to English. With Laravel, Laravel's translator is used, so the usual override files apply: `lang/vendor/quittio-de/{locale}/rules.php` and `tax.php`, also for additional locales. `CountryDE::locales()` lists the locales the pack ships (the directories of `resources/lang`). Texts defined in PHP cannot be translated through files: - the legal texts (German and English, fallback German; the four notices taken from the presets follow `tax.php`); - the correction policy titles (fallback English); - the BMF labels and the default interest descriptions (German for `de`, English otherwise). ### The German pack's hooks For pack authors, this is how `CountryDE` fills the [`CountryPack` contract](#the-countrypack-contract): | Hook | Germany | |---|---| | `defaults()` | currency `EUR`, time zone `Europe/Berlin`, locale `de`, paper `A4`, letter layout `din5008b`, profile `en16931`, payment QR `girocode`, default VAT presets `de.standard` and `de.small_business` | | `profiles()` | [`XRechnung30`, `XRechnung30Extension`](#xrechnung-profiles) | | `taxPresets()` | every case of [`VatPreset`](#tax-presets) | | `rules()` | the [UStG rules](#ustg-rules) `DE-USTG-01` to `DE-USTG-15`, the [Leitweg-ID checks](#leitweg-id) `DE-LEITWEG-ID` and `DE-LEITWEG-ID-SUSPECT`, and the [tax number check](#tax-number) `DE-TAX-NUMBER` | | `buyerCountryRules()` | none | | `formatRules()` | [`GermanFormatRule`](#format-choice) | | `mandatoryOutput()` | [mandatory PDF output](#mandatory-pdf-output) | | `legalTexts()` | [`LegalTexts::catalog()`](#legal-texts) | | `numberingRules()` | pattern `RE-{Y}-{seq:4}`, reset `yearly`, resets `never`, `yearly`, `monthly`, gapless | | `retention()` | [8 years from the end of the calendar year](#retention) | | `dunningLaw()` | [section 288 BGB values](#dunning-law) | | `correctionPolicy()` | [credit note, cancel and reissue, German titles](#cancellations-and-corrections) | | `letterLayouts()` | [`din5008a`, `din5008b`, `free`](#din-5008-letter-layouts) | | `paymentQr()`, `addressFormatter()` | defaults: EPC GiroCode, international address format | | `translationPath()` | `resources/lang` of the package | | `errorClassification()` | [BMF error classes](#bmf-error-classes) (additional method) | ### Known limitations - The legal wording and references of presets, rules and texts are proposals that must be confirmed by a tax adviser (see the legal review in the README). The default correction strategy and the cancellation of self-billed invoices are under review as well. - Final invoices: only the EN 16931 variant (`deductAdvance()`) exists. The EXTENDED variant and the handling of unpaid advance invoices are open. - The shipped base rate table ends with 1 July 2026. Later rates come from `quittio-de.base_rates` or `php artisan quittio:base-rate:update` (see [Base rate table](#base-rate-table)). - Default dunning fees and flat rates are under review. The documents layer does not track the section 288 (5) BGB flat rate per claim. - Distance sales (section 3c UStG) cannot be told from the data; `DE-USTG-12` only warns about possible ones. - Tax numbers are checked for format and check digit, not against the list of tax offices. Territories without a distinct postcode cannot be detected. - The pack defines no buyer country rules, exporters (e.g. DATEV) or readers yet. ## How a pack is selected This and the following sections are for authors of country packs, and for readers who want to know how Quittio picks a pack. A country pack implements `Quittio\Core\Countries\CountryPack`, usually by extending `Quittio\Core\Countries\AbstractCountryPack`, which supplies neutral defaults for every hook (every method of the contract). A pack may only add or tighten rules; it never relaxes EN 16931. `DefaultCountryResolver` picks the packs for every run: render, check, prepare, documents, cancellations and the validation of incoming files. 1. **Leading pack (seller side).** Quittio takes the first of these values that is set: - the country passed to the run (`->country('AT')` on the pending invoice, `RenderOptions::$country`); - the seller's country code (BT-40); - the configured default country (`quittio.defaults.country`, core config key `country`); - otherwise `XX`. The registry returns the installed pack for that code, or a generic pack (`CountryGeneric`). 2. **Buyer pack.** If the buyer's country code (BT-55) is set and differs from the leading pack's code, the installed pack of the buyer's country is added. No generic pack is created for the buyer. ```php use Quittio\Laravel\Facades\Quittio; Quittio::make($invoice)->country('DE')->render(); // force the leading pack ``` What each side contributes: | Hook | Leading pack | Buyer pack (cross-border only) | |---|---|---| | `rules()` | applied | – | | `buyerCountryRules()` | – | applied | | `formatRules()` | domestic invoices | cross-border business invoices (see [automatic format choice](formats.md#automatic-choice)) | | `mandatoryOutput()` | merged with the base fields of the mandatory field guard | – | | `defaults()` (locale, paper, letter layout), `letterLayouts()`, `addressFormatter()`, `paymentQr()` | locale fallback and PDF | – | | `correctionPolicy()` | used for cancellations (`Quittio::cancellation()`) | – | | `translationPath()` | registered | registered | "Domestic" means that seller and buyer lie in the same VAT territory (see [`VatTerritory`](#vatterritory)), or that the buyer has no country code. A buyer in a territory excluded from the seller's VAT territory (e.g. Büsingen for Germany) is cross-border. Without a buyer pack of its own, the format rules of the leading pack still apply to it. Without a pack for the seller's country, `CountryGeneric` applies: EN 16931 rules only, international formatting and neutral defaults. Validation then reports the warning `QUITTIO-GENERIC-COUNTRY` (L3, BT-40): "No country pack is installed for the seller country :value. Only EN 16931 rules are checked; national mandatory information is not validated." `ResolvedCountries::isGeneric()` and `CountryGeneric::isGeneric()` tell this case apart. With the Managed layer, the issuer's country selects the pack for numbering and retention. The leading pack of an issued invoice is stored with it (`country_pack`) and used again for re-rendering and corrections (see [Managed layer](managed.md)). ## Registering packs A pack must be registered before Quittio can use it. The German pack registers itself in Laravel. ### Laravel Laravel finds packs in three places. All of them are instantiated through the container, so constructors may use dependency injection: 1. **Composer metadata.** Every installed package and the root `composer.json` of your application may declare packs. `quittio/country-de` declares itself this way, so `composer require quittio/country-de` (or `quittio/quittio`, which contains it) is all you need. Classes that do not exist are skipped. ```json "extra": { "quittio": { "country-packs": ["Acme\\CountryAT\\CountryAT"] } } ``` 2. **Configuration.** `quittio.countries` lists further pack classes; keys are ignored (see [Configuration](configuration.md#country-packs)). ```php 'countries' => [ 'AT' => \Acme\CountryAT\CountryAT::class, ], ``` 3. **At runtime.** Call `Quittio::registerCountry(new CountryAT)` (facade `Quittio\Laravel\Facades\Quittio`), e.g. in a service provider. Packs registered this way win over packs with the same code from the other two sources. A class from the first two sources that is not a country pack throws a `ConfigurationException`. Packs are keyed by `code()`. Of two packs with the same code, the one registered last wins. Discovered packs come first, then the configured classes, then packs registered at runtime. So a subclass of `CountryDE` in `quittio.countries` replaces the discovered `CountryDE`. `php artisan quittio:countries` lists the installed packs with code, class, profiles, number of rules and format rules, and the defaults (currency, locale, profile). Without any pack it prints a warning (see [Artisan commands](commands.md#quittiocountries)). ### Without Laravel ```php use Quittio\Core\Quittio; use Quittio\Country\DE\CountryDE; $quittio = Quittio::create(['country' => 'DE'], [new CountryDE]); $quittio->registerCountry(new CountryAT); // further packs at any time ``` Registering a pack does three things: - it adds the pack to the `CountryRegistry` under its upper-case code; - it registers the pack's `profiles()` in the profile registry, so that formats, `->profile('xrechnung')` and the reader know them; - it registers `translationPath()` as the translation namespace `quittio-{code}` (lower case, e.g. `quittio-de`). `$quittio->countries()` returns the registry: `has($code)`, `get($code)` (installed pack or null), `getOrGeneric($code)`, `all()` (array keyed by code) and `taxPreset($key)` (the preset with this key from any installed pack, or null). Codes are case-insensitive. ## Writing your own country pack 1. Create a Composer package that requires `quittio/core`. 2. Extend `AbstractCountryPack` and override only what differs. `code()` is the one required method. 3. Add tax presets, rules, format rules, mandatory output, layouts and texts as needed, and national output formats with `formats()` ([Formats of your pack](#formats-of-your-pack)). 4. Ship translations in `resources/lang/{locale}/rules.php` and return the directory from `translationPath()`. 5. Declare the pack in `composer.json` (`extra.quittio.country-packs`), so that the Laravel integration finds it. 6. Test it with example invoices and, where they exist, the official validation artefacts of the country. Have the legal content reviewed by a local tax adviser. A minimal pack (values for illustration only): ```php namespace Acme\CountryAT; use Quittio\Core\Countries\AbstractCountryPack; use Quittio\Core\Countries\CountryDefaults; use Quittio\Core\Countries\MandatoryField; use Quittio\Core\Countries\MandatoryFieldList; use Quittio\Core\Countries\RetentionRule; use Quittio\Core\Data\InvoiceData; use Quittio\Core\Enums\VatCategory; use Quittio\Core\Tax\CustomTaxPreset; use Quittio\Core\Tax\TaxPresetCollection; use Quittio\Core\Validation\Rules\CallbackRule; use Quittio\Core\Validation\Rules\Inspect; use Quittio\Core\Validation\Rules\RuleReporter; final class CountryAT extends AbstractCountryPack { public function code(): string { return 'AT'; } public function defaults(): CountryDefaults { return new CountryDefaults(currency: 'EUR', timezone: 'Europe/Vienna', locale: 'de'); } public function taxPresets(): TaxPresetCollection { return new TaxPresetCollection([ CustomTaxPreset::make('at.standard_20', VatCategory::StandardRate, '20'), ]); } public function rules(): array { return [ CallbackRule::l3('ACME-AT-01', 'The seller VAT identifier (BT-31) must be stated.', function (InvoiceData $i, RuleReporter $report): void { if (! Inspect::filled($i->seller->vatId)) { $report->error('BT-31', 'seller.vatId'); } }, 'quittio-at'), ]; } public function formatRules(): array { return [new ConsumerPdfRule]; } public function mandatoryOutput(InvoiceData $invoice): MandatoryFieldList { return new MandatoryFieldList([new MandatoryField('seller.vatId', 'VAT identifier of the seller')]); } public function retention(): RetentionRule { return new RetentionRule(7); } public function translationPath(): string { return dirname(__DIR__).'/resources/lang'; } } ``` The format rule of the example sends invoices to consumers as PDF: ```php use Quittio\Core\Data\InvoiceData; use Quittio\Core\Formats\FormatChoice; use Quittio\Core\Formats\FormatRegistry; use Quittio\Core\Formats\FormatRule; final class ConsumerPdfRule implements FormatRule { public function resolve(InvoiceData $invoice): ?FormatChoice { return $invoice->buyer->isConsumer ? new FormatChoice(FormatRegistry::PDF, null, 'Invoice to a consumer.') : null; // null: the next rule or the default choice decides } } ``` The German translation of the rule, in `resources/lang/de/rules.php`: ```php // "The seller's VAT identifier (UID, BT-31) must be stated." / "Set ->vatId() on the seller." return [ 'ACME-AT-01' => [ 'message' => 'Die UID des Verkäufers (BT-31) muss angegeben sein.', 'hint' => 'Setzen Sie ->vatId() beim Verkäufer.', ], ]; ``` ### Presets of your pack A preset implements `Quittio\Core\Tax\TaxPreset`: | Method | Returns | |---|---| | `key(): string` | stable key prefixed with the country code, e.g. `at.standard_20` | | `category(): VatCategory` | VAT category (BT-151) | | `rate(): ?BigDecimal` | rate in percent, null for category O | | `exemptionReasonCode(): VatExemptionReasonCode\|RawCode\|null` | VATEX code (BT-121) | | `exemptionReason(?string $locale = null): ?string` | exemption reason text (BT-120), shown in XML and PDF | - `CustomTaxPreset::make(string $key, VatCategory $category, BigDecimal|int|string|null $rate = null, VatExemptionReasonCode|RawCode|string|null $code = null, array|string|null $reason = null)` builds a preset ad hoc. `$reason` is a text or locale => text. A plain string counts as English; without a locale the English text is used. - For many presets a backed enum is convenient, as `Quittio\Country\DE\Tax\VatPreset` shows. - `TaxPresetCollection` offers `add()`, `has($key)`, `get($key)` (throws `InvalidArgumentException` for unknown keys), `count()` and iteration. A preset is applied per line with `->vat($preset)`. The line stores category, rate, reason, code and the preset key (`LineVat::$presetKey`), so that rules can recognise it (see [Building invoices: tax presets](building-invoices.md#tax-presets)). ### Rules of your pack A rule implements `Quittio\Core\Validation\Rule` (`code()`, `level()`, `check(InvoiceData, ValidationContext): iterable`). `CallbackRule::l3($code, $fallback, $check, $namespace)` (business rules) and `CallbackRule::l7(...)` (plausibility) cover most cases. The closure receives the invoice and a `RuleReporter`: - `error(?string $bt = null, ?string $path = null, array $replace = [])`, `warning(...)` and `info(...)` add a finding with business term and data path; - `context()` returns the `ValidationContext`: `profile`, `countryPacks` (leading pack first), `locale`, `option($key)` (e.g. `option('syntax')` with the `Syntax` that will be written), `profileIs(...$keys)`, `leadingCountry()`, `hasCountry($code)`. Messages: - They come from the catalog key `{namespace}::rules.{code}` with the entries `message` and `hint`, in the validation locale or English. Without an entry, the English `$fallback` is used. - `:name` placeholders are replaced from `$replace`. - Use the namespace `quittio-{code}` that registration gives your `translationPath()`. The rules of a pack run in every format, also for a plain PDF. Profile specific rules belong to a profile class (see [Formats and profiles](formats.md)). See [Validation](validation.md) for levels and results. ### Format rules of your pack `Quittio\Core\Formats\FormatRule::resolve(InvoiceData $invoice): ?FormatChoice` returns a `FormatChoice(string $format, ?string $profile = null, ?string $reason = null)` or null. - Rules run in order; the first non-null choice wins. - The resolver consults the leading pack for domestic invoices and the buyer's pack for cross-border invoices to businesses. - An explicit format always wins. - A choice of `zugferd` or `cii` is upgraded to EXTENDED when EXTENDED features are used. ### Formats of your pack `AbstractCountryPack` implements `Quittio\Core\Countries\Capabilities\ProvidesFormats` with empty defaults. `Quittio::registerCountry()` registers what a pack returns: - `formats()`: `list`, e.g. a national format with its syntax and allowed profiles; a format with the key of a registered one replaces it. - `formatAliases()`: further names, e.g. `['xrechnung' => 'xrechnung-cii']`. - `serializers()`: `list` for syntaxes the core does not write; a serializer replaces the one of its syntax. Germany brings XRechnung this way (`Quittio\Country\DE\Formats\XRechnungFormats`). ### Capabilities of your profiles Profiles declare what they can transmit with interfaces in `Quittio\Core\Profiles\Capabilities`, instead of the core checking profile keys: | Capability | Effect | |---|---| | `SupportsSubLines` | sub lines are allowed (no `QUITTIO-PROFILE-SUBLINES`) and the invoice is not upgraded to EXTENDED | | `SupportsExtensionFields` | `extensions()` names the extensions whose fields the profile carries, e.g. `['xrechnung']` for the fields of `XRechnungExtensionData` | | `SupportsThirdPartyPayments` | UBL invoices of the profile may carry third party payments (BG-DEX-09) | | `DescribesEmbeddedXml` | XMP description and version of the embedded XML | | `RequiresVatBreakdownRate`, `StructuredCashDiscountText`, `ExtendsCodeLists` | rate in every VAT breakdown, structured cash discount texts, additional codes | ### Builder macros of your pack A pack that offers builder helpers registers one macro named after its code, which returns an object with the helpers: `InvoiceBuilder::macro('at', fn () => new AustrianInvoice($this))`, used as `->at()->…`. Germany uses `->de()` ([`GermanInvoice`](#germany-quittiocountry-de)). Ship an IDE stub for the macro, as `packages/country-de/stubs/InvoiceBuilder.stub.php` does. ### Other hooks of your pack - `paymentQr()`: a `Quittio\Core\Pdf\Qr\PaymentQrGenerator` (`key()`, `supports(InvoiceData)`, `payload(InvoiceData, array $options = []): ?string`) for the QR mode `auto`. - `addressFormatter()`: a `Quittio\Core\View\AddressFormatter` (`format(Address $address, array $nameLines = [], ?string $senderCountry = null, ?string $locale = null): list`). - `profiles()`: national profiles, usually extending `Quittio\Core\Profiles\AbstractProfile`. They are registered with the pack. - `referenceRates()` (capability `ProvidesReferenceRates`): the reference interest rate of the late payment law, so that `Quittio\Core\Dunning\DefaultInterest::forCountry($pack)` calculates default interest ([DunningLaw](#dunninglaw)). ## The `CountryPack` contract Every method of `Quittio\Core\Countries\CountryPack`, its default in `AbstractCountryPack` and where Quittio uses it: | Method | Returns | Default in `AbstractCountryPack` | Used by | |---|---|---|---| | `code()` | ISO 3166-1 alpha-2 code, e.g. `DE` | – (abstract) | registry key, translation namespace `quittio-{code}` | | `defaults()` | [`CountryDefaults`](#countrydefaults) | `new CountryDefaults` | locale fallback, paper size, default letter layout | | `taxPresets()` | `TaxPresetCollection` | empty | lookup of presets by key; presets are never applied automatically | | `rules()` | `list` | `[]` | validation, when the pack leads | | `buyerCountryRules()` | `list` | `[]` | validation, when the pack is the buyer's pack of a cross-border invoice | | `profiles()` | `list` | `[]` | profile registry (national CIUS, i.e. national rule sets on top of EN 16931 such as XRechnung) | | `formatRules()` | `list` | `[]` | automatic format choice | | `mandatoryOutput(InvoiceData $invoice)` | [`MandatoryFieldList`](#mandatory-output) | empty | mandatory field guard (leading pack only) | | `legalTexts()` | [`LegalTextCatalog`](#legaltextcatalog) | empty | the pack's own helpers and your code; Quittio itself does not read it | | `numberingRules()` | [`NumberingRules`](#numberingrules) | `new NumberingRules` | Managed numbering | | `retention()` | [`RetentionRule`](#retentionrule) | `new RetentionRule(10)` | Managed retention | | `dunningLaw()` | [`DunningLaw`](#dunninglaw) | `new DunningLaw` | `Dunning\DefaultInterest::forCountry()`; reminders take fees and interest explicitly | | `correctionPolicy()` | [`CorrectionPolicy`](#correctionpolicy) | `new CorrectionPolicy` | `Quittio::cancellation()`, Managed corrections | | `paymentQr()` | `?PaymentQrGenerator` | `null` | payment QR code of the QR mode `auto`, after a generator registered with `setPaymentQrGenerator()` and unless `CountryDefaults::$paymentQr` is `none`; null falls back to the EPC GiroCode for `girocode` (see [`CountryDefaults`](#countrydefaults)) | | `addressFormatter()` | `AddressFormatter` | `InternationalAddressFormatter` | address lines of the view model | | `letterLayouts()` | `array` | `['free' => LetterLayout::free()]` | position of the address field and marks | | `exporters()` | `list` | `[]` | declared for accounting exports of the Managed layer (`Exporter::key()`); not read in this release | | `readers()` | `list` | `[]` | readers for national formats. `read()`, `readFile()` and `validateFile()` try them after the built-in CII and UBL readers, for every registered pack. | Optional capabilities (the interface gets new methods only in a major release, new abilities come as capabilities): | Capability | Methods | Default in `AbstractCountryPack` | Used by | |---|---|---|---| | `Capabilities\ProvidesFormats` | `formats()`, `formatAliases()`, `serializers()` | implemented, empty | `Quittio::registerCountry()` ([Formats of your pack](#formats-of-your-pack)) | | `Capabilities\ProvidesReferenceRates` | `referenceRates(): Dunning\ReferenceRates` | not implemented | `Dunning\DefaultInterest::forCountry()` | | `translationPath()` | `?string` | `null` | directory with `{locale}/{group}.php` files, registered as namespace `quittio-{code}` | ### CountryDefaults `new CountryDefaults(currency: 'EUR', timezone: 'UTC', locale: 'en', paper: 'A4', letterLayout: 'free', profile: 'en16931', requiredLocales: [], paymentQr: 'girocode', vatPreset: null, smallBusinessVatPreset: null)` | Property | Default | Effect | |---|---|---| | `locale` | `en` | Last fallback for the render and validation locale: run option, invoice locale (`->locale()`), `quittio.defaults.locale`, then this value. | | `paper` | `A4` | Paper size unless the presentation or `pdf.paper` sets one. | | `letterLayout` | `free` | Key of `letterLayouts()` used when the presentation sets none (`pdf.layout`). | | `currency`, `profile` | `EUR`, `en16931` | Shown by `quittio:countries`; not applied to invoices. | | `timezone`, `requiredLocales` | `UTC`, `[]` | Informational metadata for applications; not applied by the core. Set the time zone of dates with `->timezone()` on the builder. | | `paymentQr` | `girocode` | What the QR mode `auto` prints when no generator is registered with `setPaymentQrGenerator()`: `girocode` = the pack's `paymentQr()` generator, otherwise the EPC GiroCode; `none` = no code; another type (e.g. `swiss-qr-bill`) = the pack's generator, otherwise no code. See [Templates: payment QR codes](templates.md#payment-qr-codes). | | `vatPreset`, `smallBusinessVatPreset` | `null`, `null` | Keys of presets of `taxPresets()` for invoice lines without VAT treatment: the usual rate, and the rate of small business invoices (`Meta::FLAG_SMALL_BUSINESS`). Used by `CountryDefaultVat` for the seller's country ([Default VAT](building-invoices.md#default-vat)). Germany: `de.standard` and `de.small_business`. | ### CorrectionPolicy The default handling of cancellations and corrections (see [Managed: cancellations and corrections](managed.md#cancellations-and-corrections)). | Member | Meaning | |---|---| | `CANCELLATION_CREDIT_NOTE` = `credit_note` | cancel with type code 381 and positive amounts | | `CANCELLATION_NEGATIVE_INVOICE` = `negative_invoice` | cancel with type code 380 and negative amounts | | `CORRECTION_CANCEL_AND_REISSUE` = `cancel_and_reissue` | correct by cancelling and issuing a new invoice | | `CORRECTION_CORRECTED_INVOICE` = `corrected_invoice` | correct with a corrected invoice (type code 384) | | `__construct(string $cancellationStrategy = 'credit_note', string $correctionStrategy = 'cancel_and_reissue', array $titles = [])` | `$titles`: document kind => [locale => title] | | `title(string $kind, string $locale = 'en'): ?string` | title in the locale, else English, else null | | `cancellationTitle(bool $selfBilled, string $locale = 'en'): ?string` | `cancellation_self_billed` for self-billed originals if defined, otherwise `cancellation` | The kinds Quittio reads: - `cancellation` and `cancellation_self_billed`: the title of `Quittio::cancellation()`; - `correction`: the title of a Managed corrected invoice; - `self_billed`: the PDF/HTML title and file name of self-billed invoices, after a title set in the presentation. The sign strategy of a cancellation comes from the call, then `quittio.corrections.cancellation_strategy`, then the pack. The correction strategy comes from `quittio-managed.correction.strategy`, then the pack. ### DunningLaw Statutory late payment values; amounts and percentage points are decimal strings. | Property | Default | Meaning | |---|---|---| | `b2bInterestPoints` | `8.00` | points above the reference rate for business debtors | | `b2cInterestPoints` | `0.00` | points above the reference rate for consumers | | `b2bFlatRate` | `40.00` | flat rate for business debtors (null = none) | | `dayCount` | `365` | day count basis | | `consumerDefaultAfterDays` | `null` | days after which a consumer is in default | | `referenceRateName` | `null` | name of the reference rate | Quittio's documents do not read these values. `Quittio\Core\Dunning\DefaultInterest` calculates default interest from them and the reference rates of a pack that implements `Countries\Capabilities\ProvidesReferenceRates` (`referenceRates(): Dunning\ReferenceRates` with `rateOn()` and `periods()`): `DefaultInterest::forCountry($pack)->calculate($amount, $from, $until)`. Germany provides the Basiszinssatz this way ([`DefaultInterest`](#default-interest)); a pack of another country implements `ReferenceRates`, e.g. for the ECB main refinancing rate, on top of `Quittio\Core\Support\DatedValues`. ### NumberingRules Used by Managed numbering (see [Managed: numbering](managed.md#numbering)). | Property | Default | Meaning | |---|---|---| | `defaultPattern` | `INV-{Y}-{seq:4}` | pattern when neither the issuer nor the configuration sets one | | `defaultReset` | `yearly` | reset when none is configured | | `allowedResets` | `['never', 'yearly', 'monthly']` | other resets are rejected with a `ConfigurationException` | | `gapless` | `true` | declared; not read in this release (see [gapless numbers](managed.md#gapless-numbers)) | | `numberConsumedOnRejection` | `true` | declared; not read in this release | ### RetentionRule `new RetentionRule(int $years, bool $fromEndOfCalendarYear = true)`. `until(Date $issueDate): Date` returns the last day of the retention period: 31 December of the issue year plus `$years`, or, with `fromEndOfCalendarYear: false`, the issue date plus `$years`. The Managed layer stores this date with every issued invoice; `quittio-managed.retention.years` may only lengthen it. ### LegalTextCatalog `new LegalTextCatalog(array $texts = [], string $fallbackLocale = 'en')` with `$texts` as key => [locale => text]. - `has(string $key): bool` - `get(string $key, ?string $locale = null, array $replace = []): ?string`: the text in `$locale`, else in the fallback locale, else the first variant; null for unknown keys. `:name` placeholders are replaced from `$replace`. ### Mandatory output `mandatoryOutput()` names values the rendered PDF must show for one invoice, in addition to the base fields every PDF needs. After rendering, the mandatory field guard checks that the template printed them (see [Templates: mandatory field guard](templates.md#mandatory-field-guard)). `new MandatoryField(string $accessKey, string $description, ?string $legalBasis = null)`. The access key is a view model path: - `|` separates alternatives, `&` combines paths that must all be printed; - `[*]` stands for every index, e.g. `vatRows[*].amount`; - a path counts only where the view model has a value: null values and empty lists are not required, and a field whose alternatives all have no value is skipped. `MandatoryFieldList::merge()` combines two lists; for equal access keys the later field wins. `MandatoryFieldGuard::notePath($invoice, $matches)` returns the view model path of the first note that matches a callback (`legalNotes[i]` for regulatory, legal and tax notes, `notes[i].content|notes[i].html` otherwise), so that a pack can require a specific note. ### Letter layouts `letterLayouts()` returns `Quittio\Core\View\LetterLayout` objects by name. `CountryDefaults::$letterLayout` names the default; the presentation property `letterLayout` (Laravel: `pdf.layout`) selects another one. `free` is always available; an unknown name throws a `ConfigurationException`. Values are millimetres from the top left corner of the first page: | Property | Default | Meaning | |---|---|---| | `name` | – | layout key | | `addressTop`, `addressLeft` | 45.0, 20.0 | position of the address field | | `addressWidth`, `addressHeight` | 85.0, 45.0 | size of the address field | | `senderLineHeight` | 5.0 | height of the sender line above the recipient | | `addressZoneTop` | 17.7 | distance of the recipient address from the top of the field | | `foldMarks` | `[]` | distances of the fold marks from the top edge | | `punchMark` | null | distance of the punch mark | `letterheadHeight(float $pageMarginTop)` returns the space above the address field: `addressTop` minus the top page margin (15 mm on the first page in the default templates). See [Templates: letter layouts](templates.md#letter-layouts) for how the default templates use them; the German layouts are in [DIN 5008 letter layouts](#din-5008-letter-layouts). ### VatTerritory `Quittio\Core\Countries\VatTerritory` tells the EU VAT territory (Art. 5 to 7 of Directive 2006/112/EC) from a country code and a postcode. The format resolver, the German rules and the core plausibility checks use it; packs can use it in their own rules. | Method / constant | Result | |---|---| | `EU_MEMBER_STATES` | the 27 member states (Greece is `GR`) | | `NORTHERN_IRELAND` | `XI` | | `vatCountry(string $country): string` | the country whose VAT territory a country belongs to: `MC` → `FR`, `IM` → `GB`, `EL` → `GR` | | `isEuMemberState(string $country): bool` | member state after `vatCountry()` | | `excludedTerritory(string $country, ?string $postcode): ?string` | name of an excluded territory the address lies in, or null | | `isEuVatTerritory(string $country, ?string $postcode = null, bool $goods = true): bool` | address inside the EU VAT territory | | `memberState(string $country, ?string $postcode = null, bool $goods = true): ?string` | member state of the address (`XI` for Northern Ireland and goods), null outside | | `sameVatTerritory(string $country, ?string $postcode, string $otherCountry, ?string $otherPostcode): bool` | same VAT territory, e.g. `FR` and `MC`, but not `DE` and Büsingen | | `isNorthernIreland(string $country, ?string $postcode = null): bool` | `XI`, or `GB` with a `BT` postcode | | `memberStateOfVatId(string $vatId): ?string` | member state of a VAT identifier prefix (`EL` → `GR`, `XI` stays `XI`), null otherwise (also for the prefix `GR`) | Northern Ireland belongs to the territory for goods only (`$goods = false` excludes it). Territories that are excluded and recognised by postcode: - Germany: Büsingen am Hochrhein (78266) and Heligoland (27498); - Spain: the Canary Islands (35xxx, 38xxx), Ceuta (51xxx) and Melilla (52xxx); - Italy: Livigno (23041) and Campione d'Italia (22061); - Finland: the Åland Islands (22xxx); - Greece: Mount Athos (63086); - France: the overseas territories (97xxx, 981xx to 989xx; Monaco 980xx stays France). Territories without a distinct postcode (e.g. the part of Cyprus not controlled by the government) cannot be detected. # Troubleshooting This page lists the errors that people new to Quittio meet most often, with the cause and the fix. Most of them come from validation: Quittio checks every invoice against the rules of the e-invoicing standards before it renders it ([Rules and validation](concepts.md#rules-and-validation)). ## Reading an error message An invalid invoice stops the rendering with a `Quittio\Core\Validation\ValidationException`. Its message lists every error with the rule code, the field and a hint: ```text The invoice is not valid (2 errors): [BR-CO-25] BT-9: In case the amount due for payment (BT-115) is positive, either the payment due date (BT-9) or the payment terms (BT-20) shall be present. Add a payment target with ->dueInDays(14) or a due date with ->dueDate(). [DE-USTG-03] BT-72: The date of the supply or service must be stated: delivery date (BT-72), invoicing period (BG-14) or periods on all lines (BG-26) (section 14 (4) no. 6 UStG). Set ->deliveryDate(), even if it equals the invoice date, or ->invoicingPeriod(). ``` - `[BR-CO-25]` is the rule. The prefix tells where it comes from: `BR-` EN 16931, `BR-DE-` XRechnung, `DE-USTG-` the German VAT Act, `QUITTIO-` Quittio itself ([rule codes](concepts.md#rules-and-validation)). - `BT-9` is the field ([business terms](concepts.md#en-16931-and-business-terms)); the [Field reference](field-reference.md) shows where each field is in the data model. - The indented line is the hint: usually the builder method that fixes the error. Warnings do not stop the rendering; they are in `$result->validation()`. `->check()` instead of `->render()` validates without rendering and returns all findings without throwing, which helps while you build an invoice step by step ([Validation](validation.md)). The messages are in the language of your Laravel application (`app()->getLocale()`). `quittio.validation.locale` sets another language, `'invoice'` uses the language of the invoice ([Messages and translations](validation.md#messages-and-translations)). ## Errors while building These errors are thrown by `build()` or `render()` before any validation, as `Quittio\Core\Exceptions\InvalidInvoiceData` or `InvalidArgumentException`. ### "Floats are not accepted" ```text Floats are not accepted for amounts, prices, rates and quantities (19.99 given) … ``` Amounts, prices, rates and quantities must be strings or integers: `netPrice: '19.99'`, not `netPrice: 19.99`. A float such as 0.1 cannot be stored exactly, and invoices must be exact to the cent. Values from the database are usually strings already; cast decimals with `(string)`. ### "Missing VAT information of line 1 (BG-30)" The line has no VAT rate. In Laravel every line without `vat:` gets the default VAT (19 % for a German seller, `quittio.defaults.vat`). Without Laravel there is no default: set the VAT per line (`vat: VatPreset::Standard`), for the invoice with `->defaultVat(VatPreset::Standard)`, or once for the application with `InvoiceBuilder::useDefaultVat(…)` ([Default VAT](building-invoices.md#default-vat)). ### "Missing seller postal address (BG-5)" and other "Missing …" A required part of the invoice is missing entirely; the message names it and its field number. With `Quittio::invoice()` the seller comes from `config/quittio.php`: fill in `seller.address` (street, postcode, city, country) there ([Configuration](configuration.md)). For the buyer, call `->buyer(Party::make('…')->address(…))`. ## Validation errors ### Seller address incomplete (DE-USTG-01) The address of the seller or buyer lacks street, postcode, city or country; the message names the party and the missing fields. German invoices need the full address of both parties. For the seller, check `seller.address` in `config/quittio.php`. ### Seller identifier missing (BR-CO-26, BR-S-02, DE-USTG-02) The seller has neither a VAT identification number nor a tax number. Set `seller.vat_id` or `seller.tax_number` in `config/quittio.php` (or `->vatId()` / `->taxNumber()` on the seller). A small business with a tax number only also needs a seller identifier or a trade register number for EN 16931 (`seller.identifiers` or `seller.legal_registration`, rule `BR-CO-26`). ### Date of supply missing (DE-USTG-03) German invoices must state when the goods were delivered or the service was performed, even if it is the invoice date. Add `->deliveryDate('2026-09-30')`, an invoicing period, or a period on every line (`Line::make(…)->period(…)`). ### Payment due date missing (BR-CO-25) An invoice with an amount to pay needs a due date or payment terms: `->dueInDays(14)` or `->dueDate('2026-10-17')`. In Laravel `quittio.defaults.due_days` (default 14) adds a due date to every invoice without one, so the error appears without Laravel or when that key is `null`. ### XRechnung needs more fields (QUITTIO-PROFILE-REQUIRED, BR-DE-…) XRechnung (public buyers in Germany) requires fields that other formats leave optional. A typical first attempt reports several errors at once: | Field | Set with | |---|---| | Buyer reference, the Leitweg-ID of the authority (BT-10) | `->buyerReference('04011000-12345-03')` | | Seller contact: name, phone, e-mail (BG-6) | `seller.contact` in the configuration, or `->contact(…)` on the seller | | Electronic address of seller and buyer (BT-34, BT-49) | `seller.electronic_address` in the configuration; `->electronicAddress('ap@kunde.de', 'EM')` on the buyer | | Payment instructions (BG-16) | `seller.bank_accounts` in the configuration, or `->payment(…)` | See the [public sector example](examples.md#public-sector) and [German format choice](country-packs.md#format-choice). ### Peppol rejects the electronic address (PEPPOL-EN16931-CL008) Peppol delivers to the electronic address (BT-34, BT-49), so it must be a Peppol participant identifier. EN 16931 also allows an e-mail address (`EM`), Peppol does not, nor the other letter schemes and a few newer codes of the EAS list. Use the scheme under which the party is registered in Peppol, e.g. `->electronicAddress('DE123456789', '9930')` (German VAT number) or `->electronicAddress('4000001000005', '0088')` (GLN); for the seller set `seller.electronic_address` in the configuration. ZUGFeRD and XRechnung keep accepting `EM`. ### Small business with VAT (DE-USTG-09) The seller is marked as a small business (`seller.small_business`, German Kleinunternehmer under section 19 UStG), but a line carries VAT. Small businesses charge no VAT: with the small business setting, lines without `vat:` get the small business preset automatically; remove explicit `vat:` values from the lines. ## Rendering and PDF ### "The format … produces e-invoice XML and cannot be rendered with a watermark" Drafts and previews carry a watermark and must never pass as an e-invoice, so they cannot be rendered in a format with XML. Render drafts with `->render('pdf')` or `'html'` ([Watermark](templates.md#watermark)). ### "The template does not output mandatory information" An own template (or a changed partial) does not print a value the invoice must show, e.g. the VAT identifier or the date of supply; the message lists the missing values. Print them, or start from a copy of the default template (`php artisan make:quittio-template`). See [Mandatory field guard](templates.md#mandatory-field-guard). ### Logo errors - "The logo file … does not exist": the path in `pdf.logo` is relative or wrong; use an absolute path such as `storage_path('app/logo.png')`. - SVG logos in PDF/A documents (ZUGFeRD, XRechnung PDF) need the PHP extension `imagick`; use a PNG otherwise. - In HTML output a local logo path is not visible in a browser; use a URL or data URI there. Details: [Logos](templates.md#logos). ### Characters are missing in the PDF The default font Geist covers Latin and Cyrillic, not Greek. Set `pdf.font = 'inter'` for Greek names or texts, or register a font that has the characters ([Fonts](templates.md#fonts)). ### A long line item starts on the next page A table row is never split between two pages. When a line with a very long description does not fit below the invoice details, it moves to the next page as a whole. Shorten the description or move details to a note or an attachment. ### "Invalid colour" or "Invalid font name" Colours in `pdf.colors` must be hex values (`#2741d9`), fonts plain family names that are registered (`geist`, `inter` or an entry of `pdf.fonts`). ## External validators ### KOSIT-UNAVAILABLE, VERAPDF-UNAVAILABLE The KoSIT validator (official check of XRechnung and EN 16931) or veraPDF (PDF/A check) is enabled in `validation.levels` but could not run, usually because Java, Docker or the validator files are missing. By default (`fail_on_unavailable = true` in `validation.drivers.kosit` and `validation.drivers.verapdf`) this is an error and the rendering stops, so that an invoice is never sent unchecked by mistake. Install the validator as described in [Validation](validation.md#kosit-validator-l5), remove `kosit` or `verapdf` from `validation.levels`, or set `fail_on_unavailable` to `false` to get a warning instead. ## Still stuck? - Look at the invoice the way Quittio sees it: `Quittio::make($invoice)->prepare()` shows the chosen format, profile and all findings without rendering. - Render the sample invoices: `php artisan quittio:preview --scenario=simple` shows whether configuration and template work. - Validate an existing file: `php artisan quittio:validate invoice.xml` ([Artisan commands](commands.md)). - Compare with a complete example on [Examples](examples.md). - Questions about sending, the German e-invoicing obligation, Java or temporary files: [FAQ](faq.md). # FAQ Short answers to questions that come up before and after the first invoice, with links to the pages that explain the details. For error messages, see [Troubleshooting](troubleshooting.md). ## Does Quittio send invoices? No. Quittio creates, validates, reads and renders invoices; how they reach the buyer is up to your application: - **E-mail:** attach the result to a mailable; `RenderedInvoice` implements Laravel's `Attachable` ([Laravel integration](laravel.md)). - **Peppol:** the format `peppol` writes UBL according to Peppol BIS Billing 3.0. Sending it needs a Peppol access point, a service provider connected to the network; hand it the XML from `$result->xml()` ([Peppol BIS Billing 3.0](formats.md#peppol-bis-billing-30)). A Peppol lookup lets the automatic choice pick `peppol` for buyers that are reachable there ([Peppol lookup](formats.md#peppol-lookup)). - **Public buyers in Germany:** upload the XRechnung to the portal the authority names, or send it via Peppol ([Examples: Public sector](examples.md#public-sector)). ## Which format meets the German e-invoicing obligation? Since 1 January 2025 every business in Germany must be able to receive e-invoices. Sending them becomes mandatory step by step: the transitional rules of section 27 (38) UStG still allow other invoices for domestic business-to-business supplies until the end of 2026, and in 2027 for businesses whose total turnover of the previous year was at most 800,000 euros. From 2028 domestic business invoices must be e-invoices. Invoices to consumers and small amount invoices up to 250 euros are not affected ([E-invoicing basics](concepts.md)). An e-invoice in this sense is a structured format that conforms to EN 16931. With Quittio these are: | Format | Profile | |---|---| | `zugferd` / `factur-x` (PDF with embedded XML) | `en16931` (the default) or `extended`; not `minimum` or `basic-wl` | | `xrechnung-cii`, `xrechnung-ubl` | `xrechnung` (required for public buyers) | | `cii`, `ubl` | `en16931` | | `peppol` | `peppol` | A plain `pdf` is no e-invoice. MINIMUM and BASIC WL carry no lines and do not conform to EN 16931; the validation warns with `QUITTIO-PROFILE-NOT-EINVOICE`. The German country pack chooses `zugferd` for domestic business invoices, XRechnung for public buyers and `pdf` for consumers and small amount invoices ([German format choice](country-packs.md#format-choice)). This is a summary, not legal advice; the legal wording of the country pack is still under review (see the README of the repository). ## Do I need Java? Only for the official Schematron check (validation level L5) with the KoSIT validator, which runs on Java. Building, calculating, validating levels L1 to L4 and L7, writing XML, rendering PDF/A and reading invoices are pure PHP. The KoSIT validator can run as a daemon in another container or on another host, so the PHP application itself does not need Java ([KoSIT validator](validation.md#kosit-validator-l5)). The PDF/A check (L6) uses veraPDF, locally or through Docker. Both are off by default. ## Which exceptions does reading throw? Reading throws only when the file cannot be read as an invoice at all: `UnsafeXmlException` (too large, not well-formed, DOCTYPE, limits), `PdfException` (encrypted, malformed or too large PDF) and `UnreadableInvoiceException` (unknown format, ZUGFeRD 1.0, a PDF without embedded invoice, a missing file). All three implement `Quittio\Core\Exceptions\QuittioException`. Everything else, for example totals that differ from a recalculation, becomes a finding of the result ([Reading: Errors](reading.md#errors)). ```php use Quittio\Core\Exceptions\QuittioException; use Quittio\Laravel\Facades\Quittio; try { $read = Quittio::readFile($path); } catch (QuittioException $e) { // not an invoice Quittio can read: reject the upload } ``` ## Where does mPDF write temporary files? mPDF needs a writable temporary directory for fonts and images; Quittio also caches prepared logos in its `logos` subdirectory. In Laravel it is `pdf.cache_path` (default `storage/framework/cache/quittio`), without Laravel `temp_dir` of the configuration (default `sys_get_temp_dir()/quittio-mpdf`). On read-only file systems and in containers, point it to a writable volume ([Configuration](configuration.md)). ## How much time and memory does rendering take? It depends on the template, the number of lines and pages, logos and fonts. Measure it on your server with the sample invoices: ```bash php artisan quittio:benchmark --scenario=many-lines --count=20 ``` The command prints the average, minimum, 95th percentile and maximum time and the peak memory ([Artisan commands](commands.md)). Invoices over several pages take longer, because the line table is laid out again for the carry-over at each page break. For many invoices at once, render in the queue. ## What happens to queued invoices after an update? A queued render (`->queue()->save()`) stores the invoice as a versioned JSON snapshot and the render options as versioned JSON, not as PHP objects. When the data model changes between the dispatch and the run of the job, the configured upcasters bring the snapshot up to date (`quittio.snapshots.upcasters`), so the job renders the current model ([Queueing](laravel.md#queueing), [Snapshots](configuration.md#snapshots)). Queued documents (`Quittio::document($quote)->queue()->save()`) work the same way with their own snapshot version and `quittio.snapshots.document_upcasters` ([Documents: queueing](documents.md#queueing)). Let the queue run empty before an update anyway, as for any job whose code changes. # Versioning and compatibility Quittio follows [Semantic Versioning](https://semver.org). This page says what that promise covers, so you know what can change under you in an update and what cannot. ## Before 1.0 The current releases are 0.x. Until 1.0: - **Patch releases** (0.1.0 → 0.1.1) fix bugs and security issues. They do not remove or rename anything you use. A fix can still change behaviour that was wrong, e.g. reject data that produced invalid XML before. - **Minor releases** (0.1 → 0.2) may break the public API. Every break is listed in the [changelog](https://github.com/Quittio/quittio/blob/master/CHANGELOG.md) under "Changed" or "Removed", with what to do. - Where possible, an old name keeps working as a deprecated alias first (see [Deprecations](#deprecations)). From 1.0 on, only major releases break the public API. ## What the promise covers The public API is everything below that is not marked `@internal`: | Area | Covered | |---|---| | PHP classes | Public classes, methods, properties and constants in `Quittio\Core`, `Quittio\Laravel` and `Quittio\Country\DE`: builders, data objects, `Quittio\Core\Quittio`, the facade and `QuittioManager`, results, events, exceptions | | Contracts | Interfaces you implement, such as `CountryPack`, `ValidationDriver`, `Rule`, `PdfRenderer`, `TemplateEngine`, `XmlSerializer`, `FormatReader`, `FormatResolver`, `SnapshotUpcaster`, `PaymentQrGenerator`, `VatIdVerifier`, `InvoiceDefaults`. A method is added to an interface only in a major release; base classes such as `AbstractCountryPack` and `AbstractProfile` get new methods with a default implementation. New abilities of packs and profiles come as optional capability interfaces (`Countries\Capabilities\*`, `Profiles\Capabilities\*`), which are covered like the other contracts. | | Configuration | The keys of `config/quittio.php` and `config/quittio-de.php`, their meaning and defaults, and the environment variables. A renamed key or variable keeps working under its old name until the next major release. | | Rule codes | Codes of findings, both official (`BR-CO-10`, `BR-DE-1`, `PEPPOL-EN16931-R001`) and Quittio's (`QUITTIO-…`). A code keeps its meaning; its message text is not covered. Rule overrides (`validation.rules`, `Quittio::extend('rule', …)`) address rules by these codes. | | Events | Class names and payload properties of the events in `Quittio\Laravel\Events` | | Stored data | The snapshot formats of `InvoiceData::toSnapshot()` and `DocumentData::toSnapshot()` and the array format of `RenderOptions::toArray()`. A new version comes with an upcaster or a reader for the old one, so stored invoices and queued jobs stay readable. | | Commands | Names, arguments, options and exit codes of the Artisan commands | | Output | XML that conforms to the profile. The XML of the same data changes only when a standard or a profile changes, or to fix a conformance error. | ## What it does not cover - Classes and methods marked `@internal`, e.g. `XPathReader`, `XmlBuilder`, the PDF parser in `Pdf\Parsing`, `ConfigMapper`, `CountryPackDiscovery`, the preview controller and the command classes. Use the public contract instead: implement `FormatReader` instead of extending `XPathReader`, call the commands instead of their classes. - Private and protected members of final classes. - The markup and CSS of the shipped templates. Templates you published (`--tag=quittio-views`) are yours; a release lists template changes so you can merge them. - The bytes of rendered PDFs, the wording of messages and labels, and the exact layout. - New findings: a minor release may add rules. A new rule reports a warning unless a standard makes it an error; such errors are listed in the changelog. - Data that follows its publisher: code lists, base rates, KoSIT configurations and Peppol rules are updated when they change upstream. ## Deprecations A deprecated method, class, configuration key or environment variable - is marked `@deprecated` with its replacement, so your IDE and static analysis show it, - is listed under "Deprecated" in the changelog, - keeps working until the next major release; deprecations of 0.x releases are removed in 1.0 at the earliest. ## Supported versions | Dependency | Supported | |---|---| | PHP | 8.3 and newer | | Laravel | the current major release (13). A new Laravel major is supported in the next Quittio minor release, which then drops the previous one. | Security fixes are released for the latest minor release. # Field reference Generated from the mapping attributes of the data model (`php tools/docs/generate.php`). Do not edit by hand. Paths use the property names of `InvoiceData`; `[]` marks lists. "Level" is the lowest Factur-X/ZUGFeRD profile that carries the field at this path (a part is never written without its parent, e.g. the buyer address starts with BASIC WL). | Path | BT | Card. | Level | CII | UBL | Notes | |---|---|---|---|---|---|---| | `number` | BT-1 | 1 | MINIMUM | `rsm:ExchangedDocument/ram:ID` | `cbc:ID` | | | `issueDate` | BT-2 | 1 | MINIMUM | `rsm:ExchangedDocument/ram:IssueDateTime` | `cbc:IssueDate` | | | `typeCode` | BT-3 | 1 | MINIMUM | `rsm:ExchangedDocument/ram:TypeCode` | `cbc:InvoiceTypeCode \| cbc:CreditNoteTypeCode` | | | `currency` | BT-5 | 1 | MINIMUM | `ram:ApplicableHeaderTradeSettlement/ram:InvoiceCurrencyCode` | `cbc:DocumentCurrencyCode` | | | `seller` | BG-4 | 1 | MINIMUM | `ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty` | `cac:AccountingSupplierParty` | | | `seller.name` | BT-27 | 1 | MINIMUM | `ram:SellerTradeParty/ram:Name` | `cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName` | | | `seller.address` | BG-5 | 1 | MINIMUM | `ram:SellerTradeParty/ram:PostalTradeAddress` | `cac:AccountingSupplierParty/cac:Party/cac:PostalAddress` | | | `seller.address.countryCode` | BT-40 | 1 | MINIMUM | `ram:CountryID` | `cac:Country/cbc:IdentificationCode` | | | `seller.address.line1` | BT-35 | 0..1 | BASIC WL | `ram:LineOne` | `cbc:StreetName` | | | `seller.address.line2` | BT-36 | 0..1 | BASIC WL | `ram:LineTwo` | `cbc:AdditionalStreetName` | | | `seller.address.line3` | BT-162 | 0..1 | BASIC WL | `ram:LineThree` | `cac:AddressLine/cbc:Line` | | | `seller.address.city` | BT-37 | 0..1 | BASIC WL | `ram:CityName` | `cbc:CityName` | | | `seller.address.postcode` | BT-38 | 0..1 | BASIC WL | `ram:PostcodeCode` | `cbc:PostalZone` | | | `seller.address.subdivision` | BT-39 | 0..1 | BASIC WL | `ram:CountrySubDivisionName` | `cbc:CountrySubentity` | | | `seller.tradingName` | BT-28 | 0..1 | BASIC WL | `ram:SellerTradeParty/ram:SpecifiedLegalOrganization/ram:TradingBusinessName` | `cac:PartyName/cbc:Name` | | | `seller.identifiers[]` | BT-29 | 0..n | BASIC WL | `ram:SellerTradeParty/ram:ID \| ram:GlobalID` | `cac:PartyIdentification/cbc:ID` | | | `seller.identifiers[].value` | | | | | | | | `seller.identifiers[].scheme` | | | | | | | | `seller.legalRegistration` | BT-30 | 0..1 | MINIMUM | `ram:SellerTradeParty/ram:SpecifiedLegalOrganization/ram:ID` | `cac:PartyLegalEntity/cbc:CompanyID` | | | `seller.legalRegistration.value` | | | | | | | | `seller.legalRegistration.scheme` | | | | | | | | `seller.vatId` | BT-31 | 0..1 | MINIMUM | `ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]` | `cac:PartyTaxScheme[cac:TaxScheme/cbc:ID="VAT"]/cbc:CompanyID` | | | `seller.taxNumber` | BT-32 | 0..1 | MINIMUM | `ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]` | `cac:PartyTaxScheme[cac:TaxScheme/cbc:ID!="VAT"]/cbc:CompanyID` | | | `seller.additionalLegalInfo` | BT-33 | 0..1 | EN 16931 | `ram:SellerTradeParty/ram:Description` | `cac:PartyLegalEntity/cbc:CompanyLegalForm` | | | `seller.electronicAddress` | BT-34 | 0..1 | BASIC WL | `ram:SellerTradeParty/ram:URIUniversalCommunication/ram:URIID` | `cbc:EndpointID` | required in xrechnung, peppol | | `seller.electronicAddress.value` | | | | | | | | `seller.electronicAddress.scheme` | | | | | | | | `seller.contact` | BG-6 | 0..1 | EN 16931 | `ram:SellerTradeParty/ram:DefinedTradeContact` | `cac:Contact` | required in xrechnung | | `seller.contact.name` | BT-41 | 0..1 | EN 16931 | `ram:PersonName` | `cbc:Name` | | | `seller.contact.phone` | BT-42 | 0..1 | EN 16931 | `ram:TelephoneUniversalCommunication/ram:CompleteNumber` | `cbc:Telephone` | | | `seller.contact.email` | BT-43 | 0..1 | EN 16931 | `ram:EmailURIUniversalCommunication/ram:URIID` | `cbc:ElectronicMail` | | | `buyer` | BG-7 | 1 | MINIMUM | `ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty` | `cac:AccountingCustomerParty` | | | `buyer.name` | BT-44 | 1 | MINIMUM | `ram:BuyerTradeParty/ram:Name` | `cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName` | | | `buyer.address` | BG-8 | 1 | BASIC WL | `ram:BuyerTradeParty/ram:PostalTradeAddress` | `cac:AccountingCustomerParty/cac:Party/cac:PostalAddress` | | | `buyer.address.countryCode` | BT-55 | 1 | BASIC WL | `ram:CountryID` | `cac:Country/cbc:IdentificationCode` | | | `buyer.address.line1` | BT-50 | 0..1 | BASIC WL | `ram:LineOne` | `cbc:StreetName` | | | `buyer.address.line2` | BT-51 | 0..1 | BASIC WL | `ram:LineTwo` | `cbc:AdditionalStreetName` | | | `buyer.address.line3` | BT-163 | 0..1 | BASIC WL | `ram:LineThree` | `cac:AddressLine/cbc:Line` | | | `buyer.address.city` | BT-52 | 0..1 | BASIC WL | `ram:CityName` | `cbc:CityName` | | | `buyer.address.postcode` | BT-53 | 0..1 | BASIC WL | `ram:PostcodeCode` | `cbc:PostalZone` | | | `buyer.address.subdivision` | BT-54 | 0..1 | BASIC WL | `ram:CountrySubDivisionName` | `cbc:CountrySubentity` | | | `buyer.tradingName` | BT-45 | 0..1 | EN 16931 | `ram:BuyerTradeParty/ram:SpecifiedLegalOrganization/ram:TradingBusinessName` | `cac:PartyName/cbc:Name` | | | `buyer.identifier` | BT-46 | 0..1 | BASIC WL | `ram:BuyerTradeParty/ram:ID \| ram:GlobalID` | `cac:PartyIdentification/cbc:ID` | | | `buyer.identifier.value` | | | | | | | | `buyer.identifier.scheme` | | | | | | | | `buyer.legalRegistration` | BT-47 | 0..1 | MINIMUM | `ram:BuyerTradeParty/ram:SpecifiedLegalOrganization/ram:ID` | `cac:PartyLegalEntity/cbc:CompanyID` | | | `buyer.legalRegistration.value` | | | | | | | | `buyer.legalRegistration.scheme` | | | | | | | | `buyer.vatId` | BT-48 | 0..1 | BASIC WL | `ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]` | `cac:PartyTaxScheme/cbc:CompanyID` | | | `buyer.electronicAddress` | BT-49 | 0..1 | BASIC WL | `ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID` | `cbc:EndpointID` | required in xrechnung, peppol | | `buyer.electronicAddress.value` | | | | | | | | `buyer.electronicAddress.scheme` | | | | | | | | `buyer.contact` | BG-9 | 0..1 | EN 16931 | `ram:BuyerTradeParty/ram:DefinedTradeContact` | `cac:Contact` | | | `buyer.contact.name` | BT-56 | 0..1 | EN 16931 | `ram:PersonName` | `cbc:Name` | | | `buyer.contact.phone` | BT-57 | 0..1 | EN 16931 | `ram:TelephoneUniversalCommunication/ram:CompleteNumber` | `cbc:Telephone` | | | `buyer.contact.email` | BT-58 | 0..1 | EN 16931 | `ram:EmailURIUniversalCommunication/ram:URIID` | `cbc:ElectronicMail` | | | `buyer.isConsumer` | | | | | | internal | | `lines[]` | BG-25 | 1..n | BASIC | `rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem` | `cac:InvoiceLine \| cac:CreditNoteLine` | | | `lines[].id` | BT-126 | 1 | BASIC | `ram:AssociatedDocumentLineDocument/ram:LineID` | `cbc:ID` | | | `lines[].quantity` | BT-129 | 1 | BASIC | `ram:SpecifiedLineTradeDelivery/ram:BilledQuantity` | `cbc:InvoicedQuantity \| cbc:CreditedQuantity` | | | `lines[].unit` | BT-130 | 1 | BASIC | `ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode` | `cbc:InvoicedQuantity/@unitCode \| cbc:CreditedQuantity/@unitCode` | | | `lines[].price` | BG-29 | 1 | BASIC | `ram:SpecifiedLineTradeAgreement` | `cac:Price` | | | `lines[].price.netPrice` | BT-146 | 1 | BASIC | `ram:NetPriceProductTradePrice/ram:ChargeAmount` | `cac:Price/cbc:PriceAmount` | | | `lines[].price.discount` | BT-147 | 0..1 | BASIC | `ram:GrossPriceProductTradePrice/ram:AppliedTradeAllowanceCharge/ram:ActualAmount` | `cac:Price/cac:AllowanceCharge/cbc:Amount` | | | `lines[].price.grossPrice` | BT-148 | 0..1 | BASIC | `ram:GrossPriceProductTradePrice/ram:ChargeAmount` | `cac:Price/cac:AllowanceCharge/cbc:BaseAmount` | | | `lines[].price.baseQuantity` | BT-149 | 0..1 | BASIC | `ram:NetPriceProductTradePrice/ram:BasisQuantity` | `cac:Price/cbc:BaseQuantity` | | | `lines[].price.baseQuantityUnit` | BT-150 | 0..1 | BASIC | `ram:NetPriceProductTradePrice/ram:BasisQuantity/@unitCode` | `cac:Price/cbc:BaseQuantity/@unitCode` | | | `lines[].price.priceInclVat` | | | | | | internal | | `lines[].vat` | BG-30 | 1 | BASIC | `ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax` | `cac:Item/cac:ClassifiedTaxCategory` | | | `lines[].vat.category` | BT-151 | 1 | BASIC | `ram:ApplicableTradeTax/ram:CategoryCode` | `cac:ClassifiedTaxCategory/cbc:ID` | | | `lines[].vat.rate` | BT-152 | 0..1 | BASIC | `ram:ApplicableTradeTax/ram:RateApplicablePercent` | `cac:ClassifiedTaxCategory/cbc:Percent` | | | `lines[].vat.exemptionReason` | | | | | | internal | | `lines[].vat.exemptionReasonCode` | | | | | | internal | | `lines[].vat.presetKey` | | | | | | internal | | `lines[].item` | BG-31 | 1 | BASIC | `ram:SpecifiedTradeProduct` | `cac:Item` | | | `lines[].item.name` | BT-153 | 1 | BASIC | `ram:SpecifiedTradeProduct/ram:Name` | `cac:Item/cbc:Name` | | | `lines[].item.description` | BT-154 | 0..1 | EN 16931 | `ram:SpecifiedTradeProduct/ram:Description` | `cac:Item/cbc:Description` | | | `lines[].item.sellerId` | BT-155 | 0..1 | EN 16931 | `ram:SpecifiedTradeProduct/ram:SellerAssignedID` | `cac:Item/cac:SellersItemIdentification/cbc:ID` | | | `lines[].item.buyerId` | BT-156 | 0..1 | EN 16931 | `ram:SpecifiedTradeProduct/ram:BuyerAssignedID` | `cac:Item/cac:BuyersItemIdentification/cbc:ID` | | | `lines[].item.standardId` | BT-157 | 0..1 | BASIC | `ram:SpecifiedTradeProduct/ram:GlobalID` | `cac:Item/cac:StandardItemIdentification/cbc:ID` | | | `lines[].item.standardId.value` | | | | | | | | `lines[].item.standardId.scheme` | | | | | | | | `lines[].item.classifications[]` | BT-158 | 0..n | EN 16931 | `ram:SpecifiedTradeProduct/ram:DesignatedProductClassification` | `cac:Item/cac:CommodityClassification` | | | `lines[].item.classifications[].code` | | | | | | | | `lines[].item.classifications[].listId` | | | | | | | | `lines[].item.classifications[].listVersion` | | | | | | | | `lines[].item.originCountry` | BT-159 | 0..1 | EN 16931 | `ram:SpecifiedTradeProduct/ram:OriginTradeCountry/ram:ID` | `cac:Item/cac:OriginCountry/cbc:IdentificationCode` | | | `lines[].item.attributes[]` | BG-32 | 0..n | EN 16931 | `ram:SpecifiedTradeProduct/ram:ApplicableProductCharacteristic` | `cac:Item/cac:AdditionalItemProperty` | | | `lines[].item.attributes[].name` | BT-160 | 1 | EN 16931 | `ram:ApplicableProductCharacteristic/ram:Description` | `cac:AdditionalItemProperty/cbc:Name` | | | `lines[].item.attributes[].value` | BT-161 | 1 | EN 16931 | `ram:ApplicableProductCharacteristic/ram:Value` | `cac:AdditionalItemProperty/cbc:Value` | | | `lines[].note` | BT-127 | 0..1 | BASIC | `ram:AssociatedDocumentLineDocument/ram:IncludedNote/ram:Content` | `cbc:Note` | | | `lines[].objectId` | BT-128 | 0..1 | EN 16931 | `ram:SpecifiedLineTradeSettlement/ram:AdditionalReferencedDocument[ram:TypeCode="130"]` | `cac:DocumentReference[cbc:DocumentTypeCode="130"]/cbc:ID` | | | `lines[].objectId.value` | | | | | | | | `lines[].objectId.scheme` | | | | | | | | `lines[].netAmount` | BT-131 | 1 | BASIC | `ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount` | `cbc:LineExtensionAmount` | calculated | | `lines[].orderLineReference` | BT-132 | 0..1 | EN 16931 | `ram:SpecifiedLineTradeAgreement/ram:BuyerOrderReferencedDocument/ram:LineID` | `cac:OrderLineReference/cbc:LineID` | | | `lines[].buyerAccountingReference` | BT-133 | 0..1 | EN 16931 | `ram:SpecifiedLineTradeSettlement/ram:ReceivableSpecifiedTradeAccountingAccount/ram:ID` | `cbc:AccountingCost` | | | `lines[].period` | BG-26 | 0..1 | BASIC | `ram:SpecifiedLineTradeSettlement/ram:BillingSpecifiedPeriod` | `cac:InvoicePeriod` | | | `lines[].period.start` | BT-134 | | | | | | | `lines[].period.end` | BT-135 | | | | | | | `lines[].allowances[]` | BG-27 | 0..n | BASIC | `ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeAllowanceCharge` | `cac:AllowanceCharge` | | | `lines[].allowances[].amount` | BT-136 | 1 | BASIC | `ram:SpecifiedTradeAllowanceCharge/ram:ActualAmount` | `cac:AllowanceCharge/cbc:Amount` | | | `lines[].allowances[].baseAmount` | BT-137 | 0..1 | BASIC | `ram:BasisAmount` | `cbc:BaseAmount` | | | `lines[].allowances[].percent` | BT-138 | 0..1 | BASIC | `ram:CalculationPercent` | `cbc:MultiplierFactorNumeric` | | | `lines[].allowances[].reason` | BT-139 | 0..1 | BASIC | `ram:Reason` | `cbc:AllowanceChargeReason` | | | `lines[].allowances[].reasonCode` | BT-140 | 0..1 | BASIC | `ram:ReasonCode` | `cbc:AllowanceChargeReasonCode` | | | `lines[].charges[]` | BG-28 | 0..n | BASIC | `ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeAllowanceCharge` | `cac:AllowanceCharge` | | | `lines[].charges[].amount` | BT-141 | 1 | BASIC | `ram:SpecifiedTradeAllowanceCharge/ram:ActualAmount` | `cac:AllowanceCharge/cbc:Amount` | | | `lines[].charges[].baseAmount` | BT-142 | 0..1 | BASIC | `ram:BasisAmount` | `cbc:BaseAmount` | | | `lines[].charges[].percent` | BT-143 | 0..1 | BASIC | `ram:CalculationPercent` | `cbc:MultiplierFactorNumeric` | | | `lines[].charges[].reason` | BT-144 | 0..1 | BASIC | `ram:Reason` | `cbc:AllowanceChargeReason` | | | `lines[].charges[].reasonCode` | BT-145 | 0..1 | BASIC | `ram:ReasonCode` | `cbc:AllowanceChargeReasonCode` | | | `lines[].subLines[]` | | | | | | internal | | `lines[].lineReferences[]` | | | | | | EXTENDED only | | `lines[].lineReferences[].id` | | | | | | | | `lines[].lineReferences[].kind` | | | | | | | | `lines[].lineReferences[].lineId` | | | | | | | | `lines[].lineReferences[].issueDate` | | | | | | | | `lines[].lineReferences[].typeCode` | | | | | | | | `lines[].lineReferences[].description` | | | | | | | | `lines[].extended` | | | | | | EXTENDED only | | `lines[].extended.lineStatusReasonCode` | | | | | | EXTENDED only | | `lines[].extended.shipTo` | | | | | | EXTENDED only | | `lines[].extended.shipTo.name` | | | | | | | | `lines[].extended.shipTo.address` | | | | | | | | `lines[].extended.shipTo.address.countryCode` | BT-40 | 1 | EXTENDED | `ram:CountryID` | `cac:Country/cbc:IdentificationCode` | | | `lines[].extended.shipTo.address.line1` | BT-35 | 0..1 | EXTENDED | `ram:LineOne` | `cbc:StreetName` | | | `lines[].extended.shipTo.address.line2` | BT-36 | 0..1 | EXTENDED | `ram:LineTwo` | `cbc:AdditionalStreetName` | | | `lines[].extended.shipTo.address.line3` | BT-162 | 0..1 | EXTENDED | `ram:LineThree` | `cac:AddressLine/cbc:Line` | | | `lines[].extended.shipTo.address.city` | BT-37 | 0..1 | EXTENDED | `ram:CityName` | `cbc:CityName` | | | `lines[].extended.shipTo.address.postcode` | BT-38 | 0..1 | EXTENDED | `ram:PostcodeCode` | `cbc:PostalZone` | | | `lines[].extended.shipTo.address.subdivision` | BT-39 | 0..1 | EXTENDED | `ram:CountrySubDivisionName` | `cbc:CountrySubentity` | | | `lines[].extended.shipTo.identifiers[]` | | | | | | | | `lines[].extended.shipTo.identifiers[].value` | | | | | | | | `lines[].extended.shipTo.identifiers[].scheme` | | | | | | | | `lines[].extended.shipTo.vatId` | | | | | | | | `lines[].extended.shipTo.contact` | | | | | | | | `lines[].extended.shipTo.contact.name` | BT-41 | 0..1 | EXTENDED | `ram:PersonName` | `cbc:Name` | | | `lines[].extended.shipTo.contact.phone` | BT-42 | 0..1 | EXTENDED | `ram:TelephoneUniversalCommunication/ram:CompleteNumber` | `cbc:Telephone` | | | `lines[].extended.shipTo.contact.email` | BT-43 | 0..1 | EXTENDED | `ram:EmailURIUniversalCommunication/ram:URIID` | `cbc:ElectronicMail` | | | `lines[].extended.deliveryDate` | | | | | | EXTENDED only | | `lines[].extended.includedProducts[]` | | | | | | EXTENDED only | | `lines[].extended.includedProducts[].name` | | | | | | | | `lines[].extended.includedProducts[].quantity` | | | | | | | | `lines[].extended.includedProducts[].unit` | | | | | | | | `lines[].extended.includedProducts[].description` | | | | | | | | `lines[].extended.includedProducts[].sellerId` | | | | | | | | `lines[].presentation` | | | | | | PDF only | | `lines[].presentation.groupId` | | | | | | | | `lines[].presentation.highlight` | | | | | | | | `lines[].presentation.showGrossPrice` | | | | | | | | `taxCurrency` | BT-6 | 0..1 | BASIC WL | `ram:ApplicableHeaderTradeSettlement/ram:TaxCurrencyCode` | `cbc:TaxCurrencyCode` | | | `taxPointDate` | BT-7 | 0..1 | BASIC WL | `ram:ApplicableTradeTax/ram:TaxPointDate` | `cbc:TaxPointDate` | | | `taxPointDateCode` | BT-8 | 0..1 | BASIC WL | `ram:ApplicableTradeTax/ram:DueDateTypeCode` | `cac:InvoicePeriod/cbc:DescriptionCode` | | | `dueDate` | BT-9 | 0..1 | BASIC WL | `ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime` | `cbc:DueDate (CreditNote: cac:PaymentMeans/cbc:PaymentDueDate)` | | | `buyerReference` | BT-10 | 0..1 | MINIMUM | `ram:ApplicableHeaderTradeAgreement/ram:BuyerReference` | `cbc:BuyerReference` | required in xrechnung | | `projectReference` | BT-11 | 0..1 | EN 16931 | `ram:ApplicableHeaderTradeAgreement/ram:SpecifiedProcuringProject` | `cac:ProjectReference/cbc:ID` | | | `projectReference.id` | | | | | | | | `projectReference.name` | | | | | | | | `contractReference` | BT-12 | 0..1 | BASIC WL | `ram:ContractReferencedDocument/ram:IssuerAssignedID` | `cac:ContractDocumentReference/cbc:ID` | | | `purchaseOrderReference` | BT-13 | 0..1 | MINIMUM | `ram:BuyerOrderReferencedDocument/ram:IssuerAssignedID` | `cac:OrderReference/cbc:ID` | | | `salesOrderReference` | BT-14 | 0..1 | EN 16931 | `ram:SellerOrderReferencedDocument/ram:IssuerAssignedID` | `cac:OrderReference/cbc:SalesOrderID` | | | `receivingAdviceReference` | BT-15 | 0..1 | EN 16931 | `ram:ReceivingAdviceReferencedDocument/ram:IssuerAssignedID` | `cac:ReceiptDocumentReference/cbc:ID` | | | `despatchAdviceReference` | BT-16 | 0..1 | BASIC WL | `ram:DespatchAdviceReferencedDocument/ram:IssuerAssignedID` | `cac:DespatchDocumentReference/cbc:ID` | | | `tenderReference` | BT-17 | 0..1 | EN 16931 | `ram:AdditionalReferencedDocument[ram:TypeCode="50"]/ram:IssuerAssignedID` | `cac:OriginatorDocumentReference/cbc:ID` | | | `invoicedObject` | BT-18 | 0..1 | EN 16931 | `ram:AdditionalReferencedDocument[ram:TypeCode="130"]/ram:IssuerAssignedID` | `cac:AdditionalDocumentReference[cbc:DocumentTypeCode="130"]/cbc:ID` | | | `invoicedObject.value` | | | | | | | | `invoicedObject.scheme` | | | | | | | | `buyerAccountingReference` | BT-19 | 0..1 | BASIC WL | `ram:ReceivableSpecifiedTradeAccountingAccount/ram:ID` | `cbc:AccountingCost` | | | `paymentTerms` | BT-20 | 0..1 | BASIC WL | `ram:SpecifiedTradePaymentTerms` | `cac:PaymentTerms` | | | `paymentTerms.text` | BT-20 | 0..1 | BASIC WL | `ram:SpecifiedTradePaymentTerms/ram:Description` | `cac:PaymentTerms/cbc:Note` | | | `paymentTerms.dueDays` | | | | | | internal | | `paymentTerms.discounts[]` | | | | | | internal | | `paymentTerms.discounts[].days` | | | | | | | | `paymentTerms.discounts[].percent` | | | | | | | | `paymentTerms.discounts[].baseAmount` | | | | | | | | `paymentTerms.lateFeeTerms` | | | | | | internal | | `paymentTerms.lateFeeTerms.days` | | | | | | | | `paymentTerms.lateFeeTerms.percent` | | | | | | | | `paymentTerms.lateFeeTerms.text` | | | | | | | | `notes[]` | BG-1 | 0..n | BASIC WL | `rsm:ExchangedDocument/ram:IncludedNote` | `cbc:Note` | | | `notes[].content` | BT-22 | 1 | BASIC WL | `ram:IncludedNote/ram:Content` | `cbc:Note` | | | `notes[].subjectCode` | BT-21 | 0..1 | BASIC WL | `ram:IncludedNote/ram:SubjectCode` | `cbc:Note (prefix #CODE#)` | | | `businessProcess` | BT-23 | 0..1 | MINIMUM | `ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID` | `cbc:ProfileID` | | | `precedingInvoices[]` | BG-3 | 0..n | BASIC WL | `ram:InvoiceReferencedDocument` | `cac:BillingReference` | | | `precedingInvoices[].number` | BT-25 | 1 | BASIC WL | `ram:InvoiceReferencedDocument/ram:IssuerAssignedID` | `cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID` | | | `precedingInvoices[].issueDate` | BT-26 | 0..1 | BASIC WL | `ram:InvoiceReferencedDocument/ram:FormattedIssueDateTime` | `cac:BillingReference/cac:InvoiceDocumentReference/cbc:IssueDate` | | | `payee` | BG-10 | 0..1 | BASIC WL | `ram:PayeeTradeParty` | `cac:PayeeParty` | | | `payee.name` | BT-59 | 1 | BASIC WL | `ram:PayeeTradeParty/ram:Name` | `cac:PayeeParty/cac:PartyName/cbc:Name` | | | `payee.identifier` | BT-60 | 0..1 | BASIC WL | `ram:PayeeTradeParty/ram:ID \| ram:GlobalID` | `cac:PayeeParty/cac:PartyIdentification/cbc:ID` | | | `payee.identifier.value` | | | | | | | | `payee.identifier.scheme` | | | | | | | | `payee.legalRegistration` | BT-61 | 0..1 | BASIC WL | `ram:PayeeTradeParty/ram:SpecifiedLegalOrganization/ram:ID` | `cac:PayeeParty/cac:PartyLegalEntity/cbc:CompanyID` | | | `payee.legalRegistration.value` | | | | | | | | `payee.legalRegistration.scheme` | | | | | | | | `sellerTaxRepresentative` | BG-11 | 0..1 | BASIC WL | `ram:SellerTaxRepresentativeTradeParty` | `cac:TaxRepresentativeParty` | | | `sellerTaxRepresentative.name` | BT-62 | 1 | BASIC WL | `ram:SellerTaxRepresentativeTradeParty/ram:Name` | `cac:TaxRepresentativeParty/cac:PartyName/cbc:Name` | | | `sellerTaxRepresentative.vatId` | BT-63 | 1 | BASIC WL | `ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]` | `cac:PartyTaxScheme/cbc:CompanyID` | | | `sellerTaxRepresentative.address` | BG-12 | 1 | BASIC WL | `ram:PostalTradeAddress` | `cac:PostalAddress` | | | `sellerTaxRepresentative.address.countryCode` | BT-69 | 1 | BASIC WL | `ram:CountryID` | `cac:Country/cbc:IdentificationCode` | | | `sellerTaxRepresentative.address.line1` | BT-64 | 0..1 | BASIC WL | `ram:LineOne` | `cbc:StreetName` | | | `sellerTaxRepresentative.address.line2` | BT-65 | 0..1 | BASIC WL | `ram:LineTwo` | `cbc:AdditionalStreetName` | | | `sellerTaxRepresentative.address.line3` | BT-164 | 0..1 | BASIC WL | `ram:LineThree` | `cac:AddressLine/cbc:Line` | | | `sellerTaxRepresentative.address.city` | BT-66 | 0..1 | BASIC WL | `ram:CityName` | `cbc:CityName` | | | `sellerTaxRepresentative.address.postcode` | BT-67 | 0..1 | BASIC WL | `ram:PostcodeCode` | `cbc:PostalZone` | | | `sellerTaxRepresentative.address.subdivision` | BT-68 | 0..1 | BASIC WL | `ram:CountrySubDivisionName` | `cbc:CountrySubentity` | | | `delivery` | BG-13 | 0..1 | BASIC WL | `ram:ApplicableHeaderTradeDelivery` | `cac:Delivery` | | | `delivery.recipientName` | BT-70 | 0..1 | BASIC WL | `ram:ShipToTradeParty/ram:Name` | `cac:Delivery/cac:DeliveryParty/cac:PartyName/cbc:Name` | | | `delivery.locationId` | BT-71 | 0..1 | BASIC WL | `ram:ShipToTradeParty/ram:ID \| ram:GlobalID` | `cac:Delivery/cac:DeliveryLocation/cbc:ID` | | | `delivery.locationId.value` | | | | | | | | `delivery.locationId.scheme` | | | | | | | | `delivery.actualDeliveryDate` | BT-72 | 0..1 | BASIC WL | `ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime` | `cac:Delivery/cbc:ActualDeliveryDate` | | | `delivery.address` | BG-15 | 0..1 | BASIC WL | `ram:ShipToTradeParty/ram:PostalTradeAddress` | `cac:Delivery/cac:DeliveryLocation/cac:Address` | | | `delivery.address.countryCode` | BT-80 | 1 | BASIC WL | `ram:CountryID` | `cac:Country/cbc:IdentificationCode` | | | `delivery.address.line1` | BT-75 | 0..1 | BASIC WL | `ram:LineOne` | `cbc:StreetName` | | | `delivery.address.line2` | BT-76 | 0..1 | BASIC WL | `ram:LineTwo` | `cbc:AdditionalStreetName` | | | `delivery.address.line3` | BT-165 | 0..1 | BASIC WL | `ram:LineThree` | `cac:AddressLine/cbc:Line` | | | `delivery.address.city` | BT-77 | 0..1 | BASIC WL | `ram:CityName` | `cbc:CityName` | | | `delivery.address.postcode` | BT-78 | 0..1 | BASIC WL | `ram:PostcodeCode` | `cbc:PostalZone` | | | `delivery.address.subdivision` | BT-79 | 0..1 | BASIC WL | `ram:CountrySubDivisionName` | `cbc:CountrySubentity` | | | `invoicingPeriod` | BG-14 | 0..1 | BASIC WL | `ram:BillingSpecifiedPeriod` | `cac:InvoicePeriod` | | | `invoicingPeriod.start` | BT-73 | | | | | | | `invoicingPeriod.end` | BT-74 | | | | | | | `paymentInstructions` | BG-16 | 0..1 | BASIC WL | `ram:SpecifiedTradeSettlementPaymentMeans` | `cac:PaymentMeans` | required in xrechnung | | `paymentInstructions.meansCode` | BT-81 | 1 | BASIC WL | `ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode` | `cac:PaymentMeans/cbc:PaymentMeansCode` | | | `paymentInstructions.meansText` | BT-82 | 0..1 | EN 16931 | `ram:SpecifiedTradeSettlementPaymentMeans/ram:Information` | `cac:PaymentMeans/cbc:PaymentMeansCode/@name` | | | `paymentInstructions.remittanceInformation` | BT-83 | 0..1 | BASIC WL | `ram:PaymentReference` | `cac:PaymentMeans/cbc:PaymentID` | | | `paymentInstructions.creditTransfers[]` | BG-17 | 0..n | BASIC WL | `ram:SpecifiedTradeSettlementPaymentMeans` | `cac:PaymentMeans` | | | `paymentInstructions.creditTransfers[].accountId` | BT-84 | 1 | BASIC WL | `ram:PayeePartyCreditorFinancialAccount/ram:IBANID \| ram:ProprietaryID` | `cac:PayeeFinancialAccount/cbc:ID` | | | `paymentInstructions.creditTransfers[].accountName` | BT-85 | 0..1 | EN 16931 | `ram:PayeePartyCreditorFinancialAccount/ram:AccountName` | `cac:PayeeFinancialAccount/cbc:Name` | | | `paymentInstructions.creditTransfers[].serviceProviderId` | BT-86 | 0..1 | EN 16931 | `ram:PayeeSpecifiedCreditorFinancialInstitution/ram:BICID` | `cac:PayeeFinancialAccount/cac:FinancialInstitutionBranch/cbc:ID` | | | `paymentInstructions.card` | BG-18 | 0..1 | EN 16931 | `ram:SpecifiedTradeSettlementPaymentMeans/ram:ApplicableTradeSettlementFinancialCard` | `cac:PaymentMeans/cac:CardAccount` | | | `paymentInstructions.card.primaryAccountNumber` | BT-87 | 1 | EN 16931 | `ram:ApplicableTradeSettlementFinancialCard/ram:ID` | `cac:CardAccount/cbc:PrimaryAccountNumberID` | | | `paymentInstructions.card.holderName` | BT-88 | 0..1 | EN 16931 | `ram:ApplicableTradeSettlementFinancialCard/ram:CardholderName` | `cac:CardAccount/cbc:HolderName` | | | `paymentInstructions.directDebit` | BG-19 | 0..1 | BASIC WL | `ram:SpecifiedTradeSettlementPaymentMeans/ram:PayerPartyDebtorFinancialAccount` | `cac:PaymentMeans/cac:PaymentMandate` | | | `paymentInstructions.directDebit.mandateReference` | BT-89 | 0..1 | BASIC WL | `ram:SpecifiedTradePaymentTerms/ram:DirectDebitMandateID` | `cac:PaymentMandate/cbc:ID` | | | `paymentInstructions.directDebit.creditorId` | BT-90 | 0..1 | BASIC WL | `ram:CreditorReferenceID` | `cac:AccountingSupplierParty/cac:Party/cac:PartyIdentification/cbc:ID[@schemeID="SEPA"]` | | | `paymentInstructions.directDebit.debitedAccountId` | BT-91 | 0..1 | BASIC WL | `ram:PayerPartyDebtorFinancialAccount/ram:IBANID` | `cac:PaymentMandate/cac:PayerFinancialAccount/cbc:ID` | | | `allowances[]` | BG-20 | 0..n | BASIC WL | `ram:SpecifiedTradeAllowanceCharge[ram:ChargeIndicator/udt:Indicator="false"]` | `cac:AllowanceCharge[cbc:ChargeIndicator="false"]` | | | `allowances[].amount` | BT-92 | 1 | BASIC WL | `ram:SpecifiedTradeAllowanceCharge/ram:ActualAmount` | `cac:AllowanceCharge/cbc:Amount` | | | `allowances[].vatCategory` | BT-95 | 1 | BASIC WL | `ram:CategoryTradeTax/ram:CategoryCode` | `cac:TaxCategory/cbc:ID` | | | `allowances[].vatRate` | BT-96 | 0..1 | BASIC WL | `ram:CategoryTradeTax/ram:RateApplicablePercent` | `cac:TaxCategory/cbc:Percent` | | | `allowances[].baseAmount` | BT-93 | 0..1 | BASIC WL | `ram:BasisAmount` | `cbc:BaseAmount` | | | `allowances[].percent` | BT-94 | 0..1 | BASIC WL | `ram:CalculationPercent` | `cbc:MultiplierFactorNumeric` | | | `allowances[].reason` | BT-97 | 0..1 | BASIC WL | `ram:Reason` | `cbc:AllowanceChargeReason` | | | `allowances[].reasonCode` | BT-98 | 0..1 | BASIC WL | `ram:ReasonCode` | `cbc:AllowanceChargeReasonCode` | | | `allowances[].exemptionReason` | | | | | | internal | | `allowances[].exemptionReasonCode` | | | | | | internal | | `charges[]` | BG-21 | 0..n | BASIC WL | `ram:SpecifiedTradeAllowanceCharge[ram:ChargeIndicator/udt:Indicator="true"]` | `cac:AllowanceCharge[cbc:ChargeIndicator="true"]` | | | `charges[].amount` | BT-99 | 1 | BASIC WL | `ram:SpecifiedTradeAllowanceCharge/ram:ActualAmount` | `cac:AllowanceCharge/cbc:Amount` | | | `charges[].vatCategory` | BT-102 | 1 | BASIC WL | `ram:CategoryTradeTax/ram:CategoryCode` | `cac:TaxCategory/cbc:ID` | | | `charges[].vatRate` | BT-103 | 0..1 | BASIC WL | `ram:CategoryTradeTax/ram:RateApplicablePercent` | `cac:TaxCategory/cbc:Percent` | | | `charges[].baseAmount` | BT-100 | 0..1 | BASIC WL | `ram:BasisAmount` | `cbc:BaseAmount` | | | `charges[].percent` | BT-101 | 0..1 | BASIC WL | `ram:CalculationPercent` | `cbc:MultiplierFactorNumeric` | | | `charges[].reason` | BT-104 | 0..1 | BASIC WL | `ram:Reason` | `cbc:AllowanceChargeReason` | | | `charges[].reasonCode` | BT-105 | 0..1 | BASIC WL | `ram:ReasonCode` | `cbc:AllowanceChargeReasonCode` | | | `charges[].exemptionReason` | | | | | | internal | | `charges[].exemptionReasonCode` | | | | | | internal | | `totals` | BG-22 | 1 | MINIMUM | `ram:SpecifiedTradeSettlementHeaderMonetarySummation` | `cac:LegalMonetaryTotal` | calculated | | `totals.lineNetSum` | BT-106 | 1 | BASIC WL | `ram:LineTotalAmount` | `cac:LegalMonetaryTotal/cbc:LineExtensionAmount` | calculated | | `totals.netTotal` | BT-109 | 1 | MINIMUM | `ram:TaxBasisTotalAmount` | `cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount` | calculated | | `totals.grossTotal` | BT-112 | 1 | MINIMUM | `ram:GrandTotalAmount` | `cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount` | calculated | | `totals.amountDue` | BT-115 | 1 | MINIMUM | `ram:DuePayableAmount` | `cac:LegalMonetaryTotal/cbc:PayableAmount` | calculated | | `totals.allowanceSum` | BT-107 | 0..1 | BASIC WL | `ram:AllowanceTotalAmount` | `cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount` | calculated | | `totals.chargeSum` | BT-108 | 0..1 | BASIC WL | `ram:ChargeTotalAmount` | `cac:LegalMonetaryTotal/cbc:ChargeTotalAmount` | calculated | | `totals.vatTotal` | BT-110 | 0..1 | MINIMUM | `ram:TaxTotalAmount (currencyID = BT-5)` | `cac:TaxTotal/cbc:TaxAmount` | calculated | | `totals.vatTotalInTaxCurrency` | BT-111 | 0..1 | BASIC WL | `ram:TaxTotalAmount (currencyID = BT-6)` | `cac:TaxTotal/cbc:TaxAmount` | calculated | | `totals.prepaidAmount` | BT-113 | 0..1 | BASIC WL | `ram:TotalPrepaidAmount` | `cac:LegalMonetaryTotal/cbc:PrepaidAmount` | | | `totals.roundingAmount` | BT-114 | 0..1 | BASIC WL | `ram:RoundingAmount` | `cac:LegalMonetaryTotal/cbc:PayableRoundingAmount` | | | `vatBreakdown[]` | BG-23 | 1..n | BASIC WL | `ram:ApplicableTradeTax` | `cac:TaxTotal/cac:TaxSubtotal` | calculated | | `vatBreakdown[].taxableAmount` | BT-116 | 1 | BASIC WL | `ram:ApplicableTradeTax/ram:BasisAmount` | `cac:TaxSubtotal/cbc:TaxableAmount` | calculated | | `vatBreakdown[].taxAmount` | BT-117 | 1 | BASIC WL | `ram:ApplicableTradeTax/ram:CalculatedAmount` | `cac:TaxSubtotal/cbc:TaxAmount` | calculated | | `vatBreakdown[].category` | BT-118 | 1 | BASIC WL | `ram:ApplicableTradeTax/ram:CategoryCode` | `cac:TaxSubtotal/cac:TaxCategory/cbc:ID` | calculated | | `vatBreakdown[].rate` | BT-119 | 0..1 | BASIC WL | `ram:ApplicableTradeTax/ram:RateApplicablePercent` | `cac:TaxSubtotal/cac:TaxCategory/cbc:Percent` | calculated | | `vatBreakdown[].exemptionReason` | BT-120 | 0..1 | BASIC WL | `ram:ApplicableTradeTax/ram:ExemptionReason` | `cac:TaxSubtotal/cac:TaxCategory/cbc:TaxExemptionReason` | | | `vatBreakdown[].exemptionReasonCode` | BT-121 | 0..1 | BASIC WL | `ram:ApplicableTradeTax/ram:ExemptionReasonCode` | `cac:TaxSubtotal/cac:TaxCategory/cbc:TaxExemptionReasonCode` | | | `attachments[]` | BG-24 | 0..n | EN 16931 | `ram:AdditionalReferencedDocument[ram:TypeCode="916"]` | `cac:AdditionalDocumentReference` | | | `attachments[].id` | BT-122 | 1 | EN 16931 | `ram:AdditionalReferencedDocument/ram:IssuerAssignedID` | `cac:AdditionalDocumentReference/cbc:ID` | | | `attachments[].description` | BT-123 | 0..1 | EN 16931 | `ram:AdditionalReferencedDocument/ram:Name` | `cac:AdditionalDocumentReference/cbc:DocumentDescription` | | | `attachments[].externalUri` | BT-124 | 0..1 | EN 16931 | `ram:AdditionalReferencedDocument/ram:URIID` | `cac:AdditionalDocumentReference/cac:Attachment/cac:ExternalReference/cbc:URI` | | | `attachments[].content` | BT-125 | 0..1 | EN 16931 | `ram:AdditionalReferencedDocument/ram:AttachmentBinaryObject` | `cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject` | | | `attachments[].mimeType` | BT-125-1 | 0..1 | EN 16931 | `ram:AttachmentBinaryObject/@mimeCode` | `cbc:EmbeddedDocumentBinaryObject/@mimeCode` | | | `attachments[].filename` | BT-125-2 | 0..1 | EN 16931 | `ram:AttachmentBinaryObject/@filename` | `cbc:EmbeddedDocumentBinaryObject/@filename` | | | `prepaidAmount` | | | | | | | | `roundingAmount` | | | | | | | | `extended` | | | | | | EXTENDED only | | `extended.invoicer` | | | | | | EXTENDED only | | `extended.invoicer.name` | | | | | | | | `extended.invoicer.address` | | | | | | | | `extended.invoicer.address.countryCode` | BT-40 | 1 | EXTENDED | `ram:CountryID` | `cac:Country/cbc:IdentificationCode` | | | `extended.invoicer.address.line1` | BT-35 | 0..1 | EXTENDED | `ram:LineOne` | `cbc:StreetName` | | | `extended.invoicer.address.line2` | BT-36 | 0..1 | EXTENDED | `ram:LineTwo` | `cbc:AdditionalStreetName` | | | `extended.invoicer.address.line3` | BT-162 | 0..1 | EXTENDED | `ram:LineThree` | `cac:AddressLine/cbc:Line` | | | `extended.invoicer.address.city` | BT-37 | 0..1 | EXTENDED | `ram:CityName` | `cbc:CityName` | | | `extended.invoicer.address.postcode` | BT-38 | 0..1 | EXTENDED | `ram:PostcodeCode` | `cbc:PostalZone` | | | `extended.invoicer.address.subdivision` | BT-39 | 0..1 | EXTENDED | `ram:CountrySubDivisionName` | `cbc:CountrySubentity` | | | `extended.invoicer.identifiers[]` | | | | | | | | `extended.invoicer.identifiers[].value` | | | | | | | | `extended.invoicer.identifiers[].scheme` | | | | | | | | `extended.invoicer.vatId` | | | | | | | | `extended.invoicer.contact` | | | | | | | | `extended.invoicer.contact.name` | BT-41 | 0..1 | EXTENDED | `ram:PersonName` | `cbc:Name` | | | `extended.invoicer.contact.phone` | BT-42 | 0..1 | EXTENDED | `ram:TelephoneUniversalCommunication/ram:CompleteNumber` | `cbc:Telephone` | | | `extended.invoicer.contact.email` | BT-43 | 0..1 | EXTENDED | `ram:EmailURIUniversalCommunication/ram:URIID` | `cbc:ElectronicMail` | | | `extended.invoicee` | | | | | | EXTENDED only | | `extended.invoicee.name` | | | | | | | | `extended.invoicee.address` | | | | | | | | `extended.invoicee.address.countryCode` | BT-40 | 1 | EXTENDED | `ram:CountryID` | `cac:Country/cbc:IdentificationCode` | | | `extended.invoicee.address.line1` | BT-35 | 0..1 | EXTENDED | `ram:LineOne` | `cbc:StreetName` | | | `extended.invoicee.address.line2` | BT-36 | 0..1 | EXTENDED | `ram:LineTwo` | `cbc:AdditionalStreetName` | | | `extended.invoicee.address.line3` | BT-162 | 0..1 | EXTENDED | `ram:LineThree` | `cac:AddressLine/cbc:Line` | | | `extended.invoicee.address.city` | BT-37 | 0..1 | EXTENDED | `ram:CityName` | `cbc:CityName` | | | `extended.invoicee.address.postcode` | BT-38 | 0..1 | EXTENDED | `ram:PostcodeCode` | `cbc:PostalZone` | | | `extended.invoicee.address.subdivision` | BT-39 | 0..1 | EXTENDED | `ram:CountrySubDivisionName` | `cbc:CountrySubentity` | | | `extended.invoicee.identifiers[]` | | | | | | | | `extended.invoicee.identifiers[].value` | | | | | | | | `extended.invoicee.identifiers[].scheme` | | | | | | | | `extended.invoicee.vatId` | | | | | | | | `extended.invoicee.contact` | | | | | | | | `extended.invoicee.contact.name` | BT-41 | 0..1 | EXTENDED | `ram:PersonName` | `cbc:Name` | | | `extended.invoicee.contact.phone` | BT-42 | 0..1 | EXTENDED | `ram:TelephoneUniversalCommunication/ram:CompleteNumber` | `cbc:Telephone` | | | `extended.invoicee.contact.email` | BT-43 | 0..1 | EXTENDED | `ram:EmailURIUniversalCommunication/ram:URIID` | `cbc:ElectronicMail` | | | `extended.ultimateCustomer` | | | | | | EXTENDED only | | `extended.ultimateCustomer.name` | | | | | | | | `extended.ultimateCustomer.address` | | | | | | | | `extended.ultimateCustomer.address.countryCode` | BT-40 | 1 | EXTENDED | `ram:CountryID` | `cac:Country/cbc:IdentificationCode` | | | `extended.ultimateCustomer.address.line1` | BT-35 | 0..1 | EXTENDED | `ram:LineOne` | `cbc:StreetName` | | | `extended.ultimateCustomer.address.line2` | BT-36 | 0..1 | EXTENDED | `ram:LineTwo` | `cbc:AdditionalStreetName` | | | `extended.ultimateCustomer.address.line3` | BT-162 | 0..1 | EXTENDED | `ram:LineThree` | `cac:AddressLine/cbc:Line` | | | `extended.ultimateCustomer.address.city` | BT-37 | 0..1 | EXTENDED | `ram:CityName` | `cbc:CityName` | | | `extended.ultimateCustomer.address.postcode` | BT-38 | 0..1 | EXTENDED | `ram:PostcodeCode` | `cbc:PostalZone` | | | `extended.ultimateCustomer.address.subdivision` | BT-39 | 0..1 | EXTENDED | `ram:CountrySubDivisionName` | `cbc:CountrySubentity` | | | `extended.ultimateCustomer.identifiers[]` | | | | | | | | `extended.ultimateCustomer.identifiers[].value` | | | | | | | | `extended.ultimateCustomer.identifiers[].scheme` | | | | | | | | `extended.ultimateCustomer.vatId` | | | | | | | | `extended.ultimateCustomer.contact` | | | | | | | | `extended.ultimateCustomer.contact.name` | BT-41 | 0..1 | EXTENDED | `ram:PersonName` | `cbc:Name` | | | `extended.ultimateCustomer.contact.phone` | BT-42 | 0..1 | EXTENDED | `ram:TelephoneUniversalCommunication/ram:CompleteNumber` | `cbc:Telephone` | | | `extended.ultimateCustomer.contact.email` | BT-43 | 0..1 | EXTENDED | `ram:EmailURIUniversalCommunication/ram:URIID` | `cbc:ElectronicMail` | | | `extended.shipFrom` | | | | | | EXTENDED only | | `extended.shipFrom.name` | | | | | | | | `extended.shipFrom.address` | | | | | | | | `extended.shipFrom.address.countryCode` | BT-40 | 1 | EXTENDED | `ram:CountryID` | `cac:Country/cbc:IdentificationCode` | | | `extended.shipFrom.address.line1` | BT-35 | 0..1 | EXTENDED | `ram:LineOne` | `cbc:StreetName` | | | `extended.shipFrom.address.line2` | BT-36 | 0..1 | EXTENDED | `ram:LineTwo` | `cbc:AdditionalStreetName` | | | `extended.shipFrom.address.line3` | BT-162 | 0..1 | EXTENDED | `ram:LineThree` | `cac:AddressLine/cbc:Line` | | | `extended.shipFrom.address.city` | BT-37 | 0..1 | EXTENDED | `ram:CityName` | `cbc:CityName` | | | `extended.shipFrom.address.postcode` | BT-38 | 0..1 | EXTENDED | `ram:PostcodeCode` | `cbc:PostalZone` | | | `extended.shipFrom.address.subdivision` | BT-39 | 0..1 | EXTENDED | `ram:CountrySubDivisionName` | `cbc:CountrySubentity` | | | `extended.shipFrom.identifiers[]` | | | | | | | | `extended.shipFrom.identifiers[].value` | | | | | | | | `extended.shipFrom.identifiers[].scheme` | | | | | | | | `extended.shipFrom.vatId` | | | | | | | | `extended.shipFrom.contact` | | | | | | | | `extended.shipFrom.contact.name` | BT-41 | 0..1 | EXTENDED | `ram:PersonName` | `cbc:Name` | | | `extended.shipFrom.contact.phone` | BT-42 | 0..1 | EXTENDED | `ram:TelephoneUniversalCommunication/ram:CompleteNumber` | `cbc:Telephone` | | | `extended.shipFrom.contact.email` | BT-43 | 0..1 | EXTENDED | `ram:EmailURIUniversalCommunication/ram:URIID` | `cbc:ElectronicMail` | | | `extended.deliveryTermsCode` | | | | | | EXTENDED only | | `extended.additionalReferences[]` | | | | | | EXTENDED only | | `extended.additionalReferences[].id` | | | | | | | | `extended.additionalReferences[].kind` | | | | | | | | `extended.additionalReferences[].lineId` | | | | | | | | `extended.additionalReferences[].issueDate` | | | | | | | | `extended.additionalReferences[].typeCode` | | | | | | | | `extended.additionalReferences[].description` | | | | | | | | `extended.installments[]` | | | | | | EXTENDED only | | `extended.installments[].dueDate` | | | | | | | | `extended.installments[].amount` | | | | | | | | `extended.installments[].description` | | | | | | | | `extended.additionalPaymentInstructions[]` | | | | | | EXTENDED only | | `extended.additionalPaymentInstructions[].meansCode` | BT-81 | 1 | EXTENDED | `ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode` | `cac:PaymentMeans/cbc:PaymentMeansCode` | | | `extended.additionalPaymentInstructions[].meansText` | BT-82 | 0..1 | EXTENDED | `ram:SpecifiedTradeSettlementPaymentMeans/ram:Information` | `cac:PaymentMeans/cbc:PaymentMeansCode/@name` | | | `extended.additionalPaymentInstructions[].remittanceInformation` | BT-83 | 0..1 | EXTENDED | `ram:PaymentReference` | `cac:PaymentMeans/cbc:PaymentID` | | | `extended.additionalPaymentInstructions[].creditTransfers[]` | BG-17 | 0..n | EXTENDED | `ram:SpecifiedTradeSettlementPaymentMeans` | `cac:PaymentMeans` | | | `extended.additionalPaymentInstructions[].creditTransfers[].accountId` | BT-84 | 1 | EXTENDED | `ram:PayeePartyCreditorFinancialAccount/ram:IBANID \| ram:ProprietaryID` | `cac:PayeeFinancialAccount/cbc:ID` | | | `extended.additionalPaymentInstructions[].creditTransfers[].accountName` | BT-85 | 0..1 | EXTENDED | `ram:PayeePartyCreditorFinancialAccount/ram:AccountName` | `cac:PayeeFinancialAccount/cbc:Name` | | | `extended.additionalPaymentInstructions[].creditTransfers[].serviceProviderId` | BT-86 | 0..1 | EXTENDED | `ram:PayeeSpecifiedCreditorFinancialInstitution/ram:BICID` | `cac:PayeeFinancialAccount/cac:FinancialInstitutionBranch/cbc:ID` | | | `extended.additionalPaymentInstructions[].card` | BG-18 | 0..1 | EXTENDED | `ram:SpecifiedTradeSettlementPaymentMeans/ram:ApplicableTradeSettlementFinancialCard` | `cac:PaymentMeans/cac:CardAccount` | | | `extended.additionalPaymentInstructions[].card.primaryAccountNumber` | BT-87 | 1 | EXTENDED | `ram:ApplicableTradeSettlementFinancialCard/ram:ID` | `cac:CardAccount/cbc:PrimaryAccountNumberID` | | | `extended.additionalPaymentInstructions[].card.holderName` | BT-88 | 0..1 | EXTENDED | `ram:ApplicableTradeSettlementFinancialCard/ram:CardholderName` | `cac:CardAccount/cbc:HolderName` | | | `extended.additionalPaymentInstructions[].directDebit` | BG-19 | 0..1 | EXTENDED | `ram:SpecifiedTradeSettlementPaymentMeans/ram:PayerPartyDebtorFinancialAccount` | `cac:PaymentMeans/cac:PaymentMandate` | | | `extended.additionalPaymentInstructions[].directDebit.mandateReference` | BT-89 | 0..1 | EXTENDED | `ram:SpecifiedTradePaymentTerms/ram:DirectDebitMandateID` | `cac:PaymentMandate/cbc:ID` | | | `extended.additionalPaymentInstructions[].directDebit.creditorId` | BT-90 | 0..1 | EXTENDED | `ram:CreditorReferenceID` | `cac:AccountingSupplierParty/cac:Party/cac:PartyIdentification/cbc:ID[@schemeID="SEPA"]` | | | `extended.additionalPaymentInstructions[].directDebit.debitedAccountId` | BT-91 | 0..1 | EXTENDED | `ram:PayerPartyDebtorFinancialAccount/ram:IBANID` | `cac:PaymentMandate/cac:PayerFinancialAccount/cbc:ID` | | | `xrechnungExtension` | | | | | | XRechnung extension only | | `xrechnungExtension.thirdPartyPayments[]` | | | | | | XRechnung extension only | | `xrechnungExtension.thirdPartyPayments[].type` | | | | | | | | `xrechnungExtension.thirdPartyPayments[].amount` | | | | | | | | `xrechnungExtension.thirdPartyPayments[].description` | | | | | | | | `presentation` | | | | | | PDF only | | `presentation.template` | | | | | | PDF only | | `presentation.logo` | | | | | | PDF only | | `presentation.colors` | | | | | | PDF only | | `presentation.colors.primary` | | | | | | | | `presentation.colors.text` | | | | | | | | `presentation.colors.muted` | | | | | | | | `presentation.colors.secondary` | | | | | | | | `presentation.font` | | | | | | PDF only | | `presentation.senderLine` | | | | | | PDF only | | `presentation.letterLayout` | | | | | | PDF only | | `presentation.salutation` | | | | | | PDF only | | `presentation.introText` | | | | | | PDF only | | `presentation.closingText` | | | | | | PDF only | | `presentation.footerColumns[]` | | | | | | PDF only | | `presentation.lineGroups[]` | | | | | | PDF only | | `presentation.lineGroups[].id` | | | | | | | | `presentation.lineGroups[].title` | | | | | | | | `presentation.lineGroups[].showSubtotal` | | | | | | | | `presentation.showGrossPrices` | | | | | | PDF only | | `presentation.showItemNumbers` | | | | | | PDF only | | `presentation.paymentQr` | | | | | | PDF only | | `presentation.watermark` | | | | | | PDF only | | `presentation.paper` | | | | | | PDF only | | `presentation.labels[]` | | | | | | PDF only | | `presentation.title` | | | | | | PDF only | | `presentation.explicit[]` | | | | | | PDF only | | `meta` | | | | | | internal | | `meta.locale` | | | | | | internal | | `meta.timezone` | | | | | | internal | | `meta.exchangeRate` | | | | | | internal | | `meta.exchangeRateDate` | | | | | | internal | | `meta.custom[]` | | | | | | internal | | `meta.source` | | | | | | internal | | `meta.flags[]` | | | | | | internal | | `standardVersion` | | | | | | internal | # AI assistants This page shows how coding assistants such as Claude Code, Cursor or GitHub Copilot can read this documentation, so that they write code for the current version of Quittio. There are three ways: plain Markdown files, an MCP server, and the guidelines and the skill that Quittio ships for [Laravel Boost](https://laravel.com/docs/boost). You can use any of them alone. ## Markdown and llms.txt Every page of this documentation is also available as plain Markdown: add `.md` to its address, e.g. [`https://docs.quittio.dev/getting-started.md`](https://docs.quittio.dev/getting-started.md). Two files list or contain all pages, in the [llms.txt](https://llmstxt.org) format for language models: | File | Content | |---|---| | [`/llms.txt`](https://docs.quittio.dev/llms.txt) | Index of all pages with a short description | | [`/llms-full.txt`](https://docs.quittio.dev/llms-full.txt) | All pages in one file | The files at the root describe the latest release. The development version and older versions have their own files under their path, e.g. `/next/llms.txt` and `/0.2/llms.txt` ([version switcher](https://docs.quittio.dev/next/)). The preview of the unreleased [Managed layer](managed.md) is left out of the Markdown files, both lists and the MCP server below, so that assistants do not write code for a package that cannot be installed yet. ## MCP server `https://docs.quittio.dev/mcp` is a read-only [MCP](https://modelcontextprotocol.io) server for this documentation. MCP (Model Context Protocol) is a standard that lets an assistant call the tools of a server. The server uses the Streamable HTTP transport and needs no authentication. It answers with the documentation of the latest release, the version at the root of the website. | Tool | Arguments | Returns | |---|---|---| | `search_docs` | `query`, optional `limit` (1–20, default 5) | The best matching sections with page, anchor, URL and an excerpt | | `get_page` | `page` (name such as `getting-started`, a title or a URL), optional `section` (anchor or heading) | The page or section as Markdown; long pages without a section return their outline | | `list_pages` | – | All pages with name, title, URL and description | ### Connecting an assistant Claude Code: ```bash claude mcp add --transport http quittio-docs https://docs.quittio.dev/mcp ``` Cursor (`.cursor/mcp.json`): ```json { "mcpServers": { "quittio-docs": { "url": "https://docs.quittio.dev/mcp" } } } ``` VS Code with GitHub Copilot (`.vscode/mcp.json`): ```json { "servers": { "quittio-docs": { "type": "http", "url": "https://docs.quittio.dev/mcp" } } } ``` Claude Desktop and claude.ai: Settings → Connectors → Add custom connector, URL `https://docs.quittio.dev/mcp`. ## Laravel Boost [Laravel Boost](https://laravel.com/docs/boost) loads AI guidelines and skills from the packages of a Laravel application. Guidelines are short rules the assistant always receives; a skill holds longer instructions that are loaded when a task needs them. Quittio ships both, in `quittio/quittio` and in `quittio/laravel`: | Path in the package | Content | |---|---| | `resources/boost/guidelines/core.blade.php` | Short guidelines that are always loaded: what Quittio is, the main rules (builder, tax presets, strings for amounts), a rendering example | | `resources/boost/skills/quittio-development/SKILL.md` | The skill `quittio-development`, loaded when an assistant works with invoices: building, rendering, validating, cancelling, reading and testing | Boost only reads packages that the application requires directly in its `composer.json`. After installing or updating Quittio, run: ```bash php artisan boost:install ``` Both files point the assistant to this documentation and to the [MCP server](#mcp-server) for details.