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 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)
Writing a country pack Developers who add another country or change how a pack behaves How a pack is selected, Writing your own country pack, 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). 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.
  • 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.

Common tasks:

You want to Read
Set the VAT rate or an exemption on a line Tax presets
Invoice as a small business without VAT (Kleinunternehmer) Small businesses
Invoice a business in another EU country Reverse charge, intra-community supplies and VAT identifiers
Know which format Quittio picks (ZUGFeRD, XRechnung, PDF) Format choice
Invoice a German public authority Format choice, XRechnung profiles, XRechnung rules
Issue an invoice of up to 250 € with fewer details Small amount invoices
Deduct advance payments in a final invoice Final invoices
Issue a self-billed invoice (Gutschrift) Self-billing
Cancel or correct an invoice Cancellations and corrections
Send a reminder with default interest Dunning law, Default interest
Choose the letter layout DIN 5008 letter layouts
Look up a validation code such as DE-USTG-03 or BR-DE-15 UStG rules, XRechnung rules

Complete invoices for these cases are on Examples.

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; 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).

Quittio\Country\DE\Tax\VatPreset is a string-backed enum that implements TaxPreset. Apply it per line:

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
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:

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).

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 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). 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).
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.

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). 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).
  • 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). 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).

Complete invoices are in Examples: EU business: services, EU business: goods and Export.

Format choice

Without ->format(), Quittio chooses the format for each invoice (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.

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). 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:

  • 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.

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). 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.

  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 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).

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).

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).

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) 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) 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) 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.

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):

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:

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.

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.

Cancellations and corrections

CountryDE::correctionPolicy() sets how invoices of a German seller are cancelled and corrected (see Building invoices: 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 and pass the results to a payment reminder (see Documents: 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<InterestCharge> 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 does not cover.
flatRate(string $currency = 'EUR'): ?Money The flat rate of 40.00 for business debtors (section 288 (5) BGB), once per claim
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.

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):

    '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).

    • 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:

    // 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:

    $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<array{from: Date, until: Date, rate: BigDecimal}> 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.

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). 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.

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, 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:

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:
use Quittio\Country\DE\CountryDE;

$classes = (new CountryDE)->errorClassification($result);   // ['format' => [...], 'business_rule' => [...], 'content' => [...]]

The validation levels are described in Validation; see also Validation: 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:

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
taxPresets() every case of VatPreset
rules() the UStG rules DE-USTG-01 to DE-USTG-15, the Leitweg-ID checks DE-LEITWEG-ID and DE-LEITWEG-ID-SUSPECT, and the tax number check DE-TAX-NUMBER
buyerCountryRules() none
formatRules() GermanFormatRule
mandatoryOutput() mandatory PDF output
legalTexts() LegalTexts::catalog()
numberingRules() pattern RE-{Y}-{seq:4}, reset yearly, resets never, yearly, monthly, gapless
retention() 8 years from the end of the calendar year
dunningLaw() section 288 BGB values
correctionPolicy() credit note, cancel and reissue, German titles
letterLayouts() din5008a, din5008b, free
paymentQr(), addressFormatter() defaults: EPC GiroCode, international address format
translationPath() resources/lang of the package
errorClassification() 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).
  • 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.

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)
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), 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).

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.

    "extra": { "quittio": { "country-packs": ["Acme\\CountryAT\\CountryAT"] } }
    
  2. Configuration. quittio.countries lists further pack classes; keys are ignored (see Configuration).

    '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).

Without Laravel

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).
  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):

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:

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:

// "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).

Rules of your pack

A rule implements Quittio\Core\Validation\Rule (code(), level(), check(InvoiceData, ValidationContext): iterable<ValidationMessage>). 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). See Validation 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<OutputFormat>, 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<XmlSerializer> 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). 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<string>).
  • 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).

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 new CountryDefaults locale fallback, paper size, default letter layout
taxPresets() TaxPresetCollection empty lookup of presets by key; presets are never applied automatically
rules() list<Rule> [] validation, when the pack leads
buyerCountryRules() list<Rule> [] validation, when the pack is the buyer’s pack of a cross-border invoice
profiles() list<Profile> [] profile registry (national CIUS, i.e. national rule sets on top of EN 16931 such as XRechnung)
formatRules() list<FormatRule> [] automatic format choice
mandatoryOutput(InvoiceData $invoice) MandatoryFieldList empty mandatory field guard (leading pack only)
legalTexts() LegalTextCatalog empty the pack’s own helpers and your code; Quittio itself does not read it
numberingRules() NumberingRules new NumberingRules Managed numbering
retention() RetentionRule new RetentionRule(10) Managed retention
dunningLaw() DunningLaw new DunningLaw Dunning\DefaultInterest::forCountry(); reminders take fees and interest explicitly
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)
addressFormatter() AddressFormatter InternationalAddressFormatter address lines of the view model
letterLayouts() array<string, LetterLayout> ['free' => LetterLayout::free()] position of the address field and marks
exporters() list<Exporter> [] declared for accounting exports of the Managed layer (Exporter::key()); not read in this release
readers() list<FormatReader> [] 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)
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.
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). Germany: de.standard and de.small_business.

CorrectionPolicy

The default handling of cancellations and corrections (see Managed: 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); 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).

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)
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).

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 for how the default templates use them; the German layouts are in 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.