Zum Inhalt springen

Changelog

Neuerungen Version für Version, direkt aus dem CHANGELOG.md auf GitHub. Die Einträge sind auf Englisch.

0.7.1

Aktuell

Release auf GitHub

Behoben

  • validateFile() and validatePath() no longer report XSD-UNKNOWN-ROOT for files that a reader of a country pack reads, e.g. ebInterface: the XSD level checks CII and UBL only.
  • Payment reminders of a Swiss seller get the payment part and receipt of the QR bill with the reminder total; SwissQrBillGenerator::slip() reads the values from the code. A payment QR code other than a GiroCode is captioned with the new label payment_qr ("QR-Code zum Bezahlen", "Scan to pay") instead of "GiroCode".
  • A seller in a country without a pack of its own takes the pack of the country whose VAT territory it belongs to: Monaco the French, Liechtenstein the Swiss pack (VatTerritory::vatCountry() maps LI to CH). FR-CGI-01 and FR-CGI-08 ask no business in Monaco for a SIREN.
  • PartyBuilder::vatId() keeps the space before the note of the VAT registration of a Swiss UID number ("CHE-123.456.789 MWST"), so invoices and documents print it in this form.

0.7.0

Release auf GitHub

The country packs for Austria, Belgium, Switzerland and France are published as separate packages. Their legal wording, rules and defaults were checked against the law and the official sources of October 2026, but have not been reviewed by a tax adviser yet; check the mentions and rates of your invoices before you rely on them.

Neu

  • Country packs for Austria (quittio/country-at), Belgium (quittio/country-be), Switzerland (quittio/country-ch) and France (quittio/country-fr), published as read-only split repositories next to quittio/country-de and not part of quittio/quittio. Their legal wording, rules and defaults are not reviewed by a tax adviser yet. The documentation has a page for each of them.
  • DunningLaw::$b2cFixedRate and $b2bFixedRate: fixed default interest rates for consumers and businesses instead of the reference rate plus points (e.g. 4 % for consumers in Austria, section 1000 (1) ABGB; 5 % in Switzerland, Art. 104 OR). DefaultInterest charges them for the whole time; DefaultInterest::forCountry() accepts a pack without reference rates when both rates are fixed (DunningLaw::hasFixedRates()).
  • Payment slips: a payment QR code generator that implements Pdf\Qr\ProvidesPaymentSlip returns a PaymentSlip, which the default templates (plain PHP and Blade, partial payment-slip) print after the payment part of the Swiss QR bill: receipt and payment part at the bottom of a page of its own, the code at 46 × 46 mm without quiet zone. The view model has it as payment.qrCode.slip.
  • Liberation Sans (SIL Open Font License) is bundled as font family liberationsans, for the payment slip.
  • QrImage::svg(), png() and dataUri() take border: false for a code without quiet zone.
  • A regional locale such as de_CH takes the texts of its language (de) where it has none of its own, before English, in the core translator and the Laravel translator bridge; numbers and dates keep the regional format.
  • Dutch labels, code names and payment terms texts (nl), e.g. for invoices from Flanders.

Behoben

  • The payment QR code type swiss-qr-bill prints the QR bill of the country pack Switzerland when no generator is registered, instead of failing with a ConfigurationException; the same holds for any type that the pack's generator has.
  • CorrectionPolicy::title() takes the title of the language of a regional locale, so the cancellation of a Swiss invoice in de_CH is titled "Gutschrift" and that of a Belgian one in nl_BE "Creditnota" instead of the English title.
  • CustomTaxPreset::exemptionReason() takes the text of the language of a regional locale (fr for fr_BE, de for de_CH) before the English one, so an exemption from VatPreset::exempt() of a country pack is printed in the language of an invoice with a regional locale.
  • The mandatory field guard counts the register number (BT-30) as printed when the default templates show it in the footer, as it does for the tax identifiers.
  • The Peppol rules treat the business process (BT-23) as the official rules do: an empty one is reported by PEPPOL-EN16931-R001 and -R007, one of only whitespace by -R007 and -R008 instead of -R001.
  • PEPPOL-EN16931-R041 and PEPPOL-EN16931-R042 report the path of the allowance or charge (allowances[0].baseAmount, charges[0].percent). Without it, an allowance and a charge with the same error were reported as one finding.
  • CI ran every job on PHP 8.5, so PHP 8.3 and 8.4 were not tested; the jobs now use the PHP version they name, and PHPStan analyses for PHP 8.3 to 8.5.

0.6.0

Release auf GitHub

The legal wording, rules and defaults of the country pack Germany were reviewed against the UStG, the UStDV, the UStAE, the BMF letters of 15 October 2025 and 15 October 2026 and the case law, and confirmed by a tax adviser in October 2026. The changes below follow from that review. Quittio remains software, not tax advice; see the legal review and the disclaimer in the README.

Neu

  • DE-USTG-16 (warning): an invoice to a business, or for a service in connection with a property, issued more than six months after the supply (section 14 (2) sentence 2 UStG). Cancellations, corrections, prepayment invoices and re-issued invoices are skipped.
  • DE-USTG-17 (warning): a prepayment invoice (386) with an amount already paid (BT-113) but no VAT point date (BT-7). The date of receipt must be stated if it differs from the issue date (section 14 (4) sentence 1 no. 6 UStG).
  • ->de()->serviceDateOpen() for prepayment invoices whose date of the service is not agreed yet: flag de.service_date_open and the note LegalTexts::SERVICE_DATE_OPEN ("Der Zeitpunkt der Leistung ist noch nicht vereinbart."). DE-USTG-03 no longer warns about the missing date then (section 14.8 (4) sentence 5 UStAE).
  • ->de()->smallBusinessId('DE123456789EX') for small businesses that state their small business identification number (suffix EX) instead of the tax number (section 34a sentence 1 no. 2 UStDV). It sets the number as tax number (BT-32) and the small business flag. VatIdFormat::isSmallBusinessId() recognises such numbers.
  • QUITTIO-SME-ID (info): a small business identification number (suffix EX) in a VAT identifier field (BT-31, BT-48, BT-63). It is no VAT identifier (Article 284 (3) VAT Directive) and cannot be checked in VIES.
  • UstgRules::supplyDate(): the supply date that DE-USTG-08, DE-USTG-15 and DE-USTG-16 use.
  • Tax presets can require an invoice note: presets that implement Quittio\Core\Tax\StatesInvoiceNote give a text (invoiceNote()), which the builder adds once as note (BT-22, subject REG) when a line uses the preset, unless the invoice has a note with the same text. The PDF lists such a text once, also when it is the exemption reason (BT-120) as well.
  • The German margin scheme, travel agency and triangular transaction presets state their text of section 14a (6) and (7) UStG also as invoice note, as the mapping table of KoSIT and FeRD assigns these statements to the invoice note (BG-1). VatPreset::ZeroRatedPv adds the note "Steuersatz 0 % nach § 12 Abs. 3 UStG." (LegalTexts::ZERO_RATED_PV), which category Z cannot carry as exemption reason.
  • VatPreset::BusinessTransfer (de.business_transfer): transfer of a business as a whole (Geschäftsveräußerung im Ganzen, section 1 (1a) UStG), category O with VATEX-EU-O and the text "Nicht steuerbare Geschäftsveräußerung im Ganzen (§ 1 Abs. 1a UStG).". Like every category O invoice it states tax numbers instead of VAT identifiers (BR-O-02).
  • ->de()->discountAgreement('der Bonusvereinbarung vom 01.01.2026') refers to agreed bonuses or rebates whose amount is not known yet (section 14 (4) no. 7 UStG) with the note LegalTexts::DISCOUNT_AGREEMENT.
  • Quittio::correction($original, $number, reason: …) (core and Laravel facade, Corrections\Correction::of()) starts a corrected invoice (384) that replaces the original: a copy with the new number, BG-3 and the note "Diese Rechnung berichtigt und ersetzt die Rechnung … vom …" (label correction_note), titled by the policy (correction, Germany: "Rechnungskorrektur"). Credit notes and self-billed documents are rejected. The new rule DE-USTG-18 (warning) reports a 384 without such a note (section 31 (5) UStDV, section 15.2a (7) UStAE).
  • Quittio::priceReduction($original, $number, PriceReductionReason::CashDiscount, percent: '2') (core and Laravel facade, Corrections\PriceReduction::of()) builds a document on the reduction of the consideration (section 17 UStG): a credit note (381) with one line per reduced VAT group, BG-3, the flag price_reduction and a note that names reason, net amount and VAT per rate. The reduction is a percentage, a net amount (one VAT group) or net amounts per VAT group ('S:19' => '50.00'). The German policy titles it "Entgeltminderung" (title price_reduction).
  • CorrectionPolicy takes a strategyResolver that chooses the cancellation strategy per original (cancellationStrategyFor()); Quittio::cancellation() uses it when neither the argument nor the configuration sets one. The German pack cancels self-billed invoices with a Leitweg-ID (sent as XRechnung) by a negative self-billed invoice (389) instead of 261, which XRechnung does not list (BR-DE-17).
  • Bonus statements (DocumentType::BonusStatement, DocumentBuilder::bonusStatement(), Quittio::bonusStatement() in Laravel): the reduction of the consideration of the supplies of a period, e.g. an annual bonus, per VAT rate (section 17 (4) UStG). bonusPeriod(), bonusAgreement() and bonusInvoices() set period, agreement and invoices (DocumentData::$bonus, BonusStatement); bonus() and bonusOnRevenue() add one reduction per VAT rate as a line. The PDF names period and agreement (label bonus_statement_note, with "§ 17 Abs. 4 UStG" from the German pack), names reductions instead of prices, VAT and amounts ("Minderung", "Minderung USt. 19 %", "Bonus gesamt (brutto)") and shows no bank details or amount due. Reductions for intra-community supplies and exports state "Minderung des Entgelts für steuerfreie innergemeinschaftliche Lieferungen" or "… Ausfuhrlieferungen" instead of the exemption text of a supply.
  • The PDF prints the text of a special scheme once when it is also part of the joined exemption reason (BT-120) of a mixed exempt group; the note (BT-22) stays in the XML.
  • Documents have plausibility notices (DocumentData::notices(), RenderedDocument::notices() in core and Laravel; the render run logs them as warnings): QUITTIO-DUNNING-FLAT-RATE-FEES for a reminder to a business that claims the flat rate and reminder fees, which count towards it (section 288 (5) sentence 3 BGB).
  • DE-USTG-19 (info): a final invoice deducts an advance settled by self-billing (389) but is no self-billed invoice itself (flag de.self_billed_advance, set by deductAdvance()).
  • ->de()->advance($expectedService, receivedOn: …) builds a prepayment invoice (386): the note "Anzahlungsrechnung. Die Leistung wird voraussichtlich am … / im … erbracht." with the expected date or month ("2027-03"), or without one the note that the date is not agreed yet. A date of receipt is stated as VAT point date (BT-7) and in the note "Die Anzahlung wurde am … vereinnahmt." (LegalTexts::PREPAYMENT_MONTH, PREPAYMENT_RECEIVED, flag de.expected_service_date).
  • ->de()->deductAdvance($advance, received: …) deducts only the amount received: a part is distributed over the VAT rows of the advance invoice and the VAT taken out of each part (section 14.8 (5) UStAE); GermanInvoice::RECEIVED_IN_FULL deducts the whole advance. Without received: the whole advance is deducted as before, and the new warning DE-USTG-20 asks to confirm the amount (flag de.advance_receipt_unconfirmed).
  • DocumentBuilder::flatRate($claim, $amount = '40.00') adds the flat rate for recovery costs of a late payment (Article 6 Directive 2011/7/EU, section 288 (5) BGB) for one claim, as DunningFee of the new kind DunningFeeKind::FlatRate with the claim (DunningFee::$kind, $claim). Building a reminder rejects a second flat rate of the same claim and a flat rate for a consumer.
  • Country packs can complete an invoice before it is calculated, validated and rendered (Countries\Capabilities\CompletesInvoices::complete()), with their settings from Config::$countrySettings (core: countries.{code}, Laravel: quittio.countries.{code}).
  • The German pack leaves out the VAT identifiers of seller and buyer on invoices of category O only (not subject to VAT), which EN 16931 forbids (BR-O-02), when the seller's tax number is given and there is no tax representative: flag de.vat_ids_omitted, info DE-CATEGORY-O-01. Otherwise the info DE-CATEGORY-O-02 names what is missing. Setting quittio.countries.de.omit_vat_ids_for_category_o (default true).
  • The German pack adds the default notice of section 286 (3) BGB (LegalTexts::CONSUMER_DEFAULT, subject PMT) to every invoice for a consumer that does not have one: without it the 30 days do not apply to consumers. Switch it off with quittio.countries.de.consumer_default_notice = false.
  • The German pack checks the e-invoice obligation of section 14 (1) and section 27 (38) UStG when a run produces no e-invoice (plain PDF, MINIMUM, BASIC WL): DE-EINVOICE-01 (error) when an e-invoice is required (supplies from 2028, of 2027 with a prior-year turnover above 800,000 EUR, or after the end of the transition), DE-EINVOICE-02 (warning) while the transition still allows an other invoice, as PDF only with the buyer's consent. Not for consumers, buyers or sellers outside the Inland, small amount invoices up to 250 EUR, small businesses, tickets (->de()->ticket()), supplies exempt under section 4 no. 8 to 29 UStG only, cancellations and corrections, nor for drafts. Prepayment invoices count with their issue date. New setting quittio.countries.de.prior_year_turnover_above_800k (default false). Both findings are BMF format errors.
  • A PDF or HTML rendered in one run with an XML-only e-invoice (e.g. ['xrechnung-cii', 'pdf']) states in its legal notes that it is a readable copy: "Lesbare Darstellung der E-Rechnung (XRechnung 3.0) Nr. … – inhaltsgleiches Mehrstück. Maßgeblich ist ausschließlich die XML-Datei." (label einvoice_visualization). The render option visualization_notice = false leaves it out.
  • tools/validate-fixtures.php writes 13 further German documents (small business with identification number, prepayment invoice, final invoices with full, partial and remainder deduction, self-billed invoice and both of its cancellations, corrected invoice, price reduction, business transfer, photovoltaic, margin scheme) in EN 16931 and XRechnung, CII and UBL, and as ZUGFeRD PDF, so that KoSIT and veraPDF check them in CI.
  • The validation context carries whether the run produces an e-invoice (option e_invoice) and the settings of the country packs (option countries).
  • ->de()->deductAdvance($advance, style: GermanInvoice::REMAINDER) deducts the advance as negative lines per VAT rate instead of the prepaid amount (remainder invoice, section 14.8 (11) UStAE), with the note LegalTexts::ADVANCE_DEDUCTION_REMAINDER. The default PREPAID stays (BT-113 with the itemisation in BT-22, accepted by the BMF letter of 15 October 2026 until 30 June 2030).

Geändert

  • DE-USTG-06 (missing "Steuerschuldnerschaft des Leistungsempfängers") is an error instead of a warning: the statement is mandatory (section 14a (1) sentence 1 and (5) sentence 1 UStG). The buyer owes the VAT all the same.
  • DE-USTG-11 no longer requires a VAT identifier of a small business seller for reverse charge services in another member state and triangular transactions (section 19 (1) sentence 2 UStG). The buyer's VAT identifier is still required.
  • Small business invoices (section 34a UStDV) are recognised by the small business flag as well: UstgRules::isSmallBusinessInvoice() is true when the flag is set or a line uses VatPreset::SmallBusiness (before: every line). DE-USTG-03 and the mandatory PDF output skip the service date for them, as DE-USTG-09 and DE-USTG-14 already did.
  • DE-USTG-15 checks final invoices (BG-3 with a prepaid amount BT-113): on intra-community supplies advance payments trigger no VAT, so the final invoice has the deadline. Re-issued invoices with BG-3 and no prepaid amount are still skipped.
  • DE-USTG-08 takes the small amount limit of the supply date instead of the issue date (the raise to 250 EUR applies to supplies from 1 January 2017), and the issue date only without a supply date.
  • DE-USTG-04 (missing "Gutschrift") is a warning when the XML is written: in an e-invoice the type code labels a self-billed invoice (section 14.5 (24) sentence 1 UStAE). It stays an error otherwise.
  • QUITTIO-VATID-FORMAT no longer reports small business identification numbers, and DE-TAX-NUMBER accepts them as tax number of a small business.
  • German default interest counts actual/actual: CountryDE::dunningLaw()->dayCount is null instead of 365, so the days of a leap year are divided by 366 (as the statutory conventions of the annex to section 16 PAngV do); a full leap year yields exactly the annual rate. DunningLaw::$dayCount accepts null.
  • LegalTexts::PREPAYMENT reads "Anzahlungsrechnung. Die Leistung wird voraussichtlich am :date erbracht." ("Advance invoice. The service is expected to be performed on :date."): the sentence that VAT becomes due on receipt is not required and was dropped.
  • The PDF and HTML recognise margin scheme and travel agency lines by their own exemption code: their lines show no rate also when the exempt VAT group lost its code because other exemptions share it (QUITTIO-EXEMPTION-CONFLICT), and a group of different special schemes is shown as special scheme. An exempt group that mixes a special scheme with other exemptions is shown in two rows of vatRows ("Sonderregelung, kein gesonderter Steuerausweis: …" and "Umsatzsteuerfrei … auf …"), or in one row without rate and VAT amount (vat_row_exempt_special_scheme) when an exempt document level allowance or charge makes the parts unknown. ViewModelFactory::mixedExemptGroup() and vatRowIndexes() tell templates and country packs which row shows which VAT breakdown row.
  • The readable copy notice of a PDF next to an XRechnung is the last of the legalNotes, so that the note paths of MandatoryFieldGuard::notePath() stay valid.
  • The text of VatPreset::NotTaxableInGermany names the place of supply abroad: "Nicht im Inland steuerbare Leistung (Leistungsort im Ausland)" ("Service not taxable in Germany (place of supply abroad)").
  • The note of a partial cancellation whose VAT is aligned with the earlier ones (cancellation_vat_aligned) explains the rounding: "Rundungsausgleich: Die Umsatzsteuer dieses Teilstornos ist so bemessen, dass alle Teilstornos zur Rechnung RE-… zusammen deren Umsatzsteuer exakt berichtigen." The label takes the placeholder :number; overridden labels without it keep working.

0.5.0

Release auf GitHub

Neu

  • MimeType::Xml (application/xml) for supporting documents (BT-125-1) in the profile xrechnung-extension, which allows XML attachments (BR-DEX-01). The PHP rules now check the MIME code with BR-CL-24: other profiles report MimeType::Xml as error, as the official Schematron does (BR-CL-24, in Peppol also PEPPOL-EN16931-CL001); the extension accepts it through ExtendsCodeLists::additionalCodes().

Geändert

  • The VAT rows of the PDF and HTML name IGIC (category L, Canary Islands) and IPSI (category M, Ceuta and Melilla) instead of VAT, e.g. "7 % IGIC auf 50,00 €" or "7% IGIC on 50.00 €" (labels vat_row_igic and vat_row_ipsi). Rows of the other categories are unchanged.
  • The package rules QUITTIO-DECLARED-AMOUNTS, QUITTIO-VAT-ROUNDING and QUITTIO-CANCELLATION-VAT-ALIGNED read the declared amounts state through the internal Calculation\DeclaredAmountsState like the calculator, the builder, the reader and the cancellations. The results are the same.

0.4.0

Release auf GitHub

Neu

  • Typed view model: every node that templates receive has a class in Quittio\Core\View\Model (PartyView, AddressView, LineView, VatRowView, TotalsView, PaymentView, DesignView, DocumentInfoView …; lists are ViewList<T>, e.g. ViewList<LineView>) with its keys as typed properties, so IDEs and PHPStan know that $invoice->totals->amountDue is a ?FormattedValue. The nodes are ViewNode objects over the same data: published Blade and PHP templates, array syntax, toArray() and the mandatory field guard work unchanged (Typed nodes). The compatibility promise now names the keys and types of the view model.
  • The PHP rules check the XRechnung extension like the official validator: BR-DEX-02 (warning), BR-DEX-04 to BR-DEX-10 and BR-DEX-12 to BR-DEX-14 for identifier schemes with XR01 to XR03, the amount due with third party payments and the third party payments themselves. In the profile xrechnung-extension they replace BR-CL-10, -11, -21, -25, -26 and, in UBL, BR-CO-16 and QUITTIO-SUBLINES, so a violation is reported once. Profiles declare such replacements with the capability ReplacesRules.
  • The Peppol profile checks all Peppol BIS Billing 3.0 rules that the data of an invoice can break, with readable messages instead of only the official validation: the format of the business process (PEPPOL-EN16931-R007), values of only whitespace (-R008), net price against gross price and discount (-R046), the sign of BT-111 (-R055), line periods within the invoicing period (-R110, -R111), currencies and dates (-CL007, -F001), the type codes of business process 01 (-P0100, -P0101), the type codes 326 and 384 outside Germany (-P0112), VATEX codes with the wrong VAT category (-P0104 to -P0111) and the format and check digits of GLN, Norwegian, Danish, Belgian, Italian, Swedish and Australian identifiers (PEPPOL-COMMON-R040 to -R053). The code lists CodeList::PEPPOL_CURRENCIES, PEPPOL_INVOICE_TYPES and PEPPOL_CREDIT_NOTE_TYPES are generated from the Peppol rules with CodeListGenerator::generatePeppol() (CodeListGenerator::PEPPOL_LISTS). The rules the UBL writer always satisfies are listed in the validation docs (Peppol rules).
  • The PHP validation checks the VAT categories L (IGIC, Canary Islands: BR-AF-01, -02, -05, -08, -09, -10), M (IPSI, Ceuta and Melilla: BR-AG-01, -02, -05, -08, -09, -10) and B (split payment, Italy: BR-B-01, BR-B-02). Like S, L and M allow one VAT breakdown per rate with VAT; their rate may also be 0. The warning QUITTIO-IGIC-ZERO-RATE-CII reports IGIC with the rate 0 in CII, which the official CII Schematron rejects.
  • XRechnung CVD (Clean Vehicles Directive) in the German pack: the profile xrechnung-cvd (XRechnung30Cvd, specification identifier …#compliant#urn:xeinkauf.de:kosit:xrechnung:cvd_0.9) for xrechnung-cii, xrechnung-ubl and zugferd, with the rules BR-DE-CVD-01 to BR-DE-CVD-06-b: contract (BT-12) and tender or lot reference (BT-17) are required, and vehicle lines carry the category as item classification with scheme CVD (BT-158) and the item attribute cva (BT-160, BT-161). The profile accepts the scheme CVD in the code list check BR-CL-13.

Geändert

  • The Laravel settings of the German pack live under quittio.countries.de (config('quittio.countries.de.base_rates'), ….base_rate_file) instead of the configuration quittio-de. php artisan vendor:publish --tag=quittio-de-config publishes them to config/quittio/countries/de.php, which Laravel loads as quittio.countries.de; they can also be set in config/quittio.php under 'countries' => ['de' => […]]. quittio:install publishes the tag quittio-{code}-config of every installed country pack with config/quittio.php. GermanyServiceProvider no longer fills quittio-de with defaults: code that reads config('quittio-de.…') reads quittio.countries.de. Lower-case codes under quittio.countries hold the settings of a pack, the convention for further packs (quittio.countries.{code}); a pack class listed under 'de' is moved to a numeric key.
  • The calculator, the builder, the reader and the cancellations read and write the declared amounts flags together as one typed state (the internal Calculation\DeclaredAmountsState) instead of single flags. The flags stay the stored format with the same strings, so stored data of earlier versions is read unchanged; they are now always written after the other flags in a fixed order (declared_amounts, declared_amounts_recalculated, cancellation_vat_aligned, declared_vat_in_tax_currency:, amounts_fingerprint: last), and duplicates or an unreadable declared_vat_in_tax_currency: value are dropped when data is calculated again.
  • ViewModelFactory builds the view model with one internal factory per section (header, parties, references, lines, allowances and charges, VAT breakdown, totals, payment, notes, design, documents; View\Internal\Sections). The view model is the same as before.

Veraltet

  • The configuration quittio-de (config/quittio-de.php): move its keys to quittio.countries.de and delete the file. Until 1.0 it is still read: its keys fill those that quittio.countries.de leaves out or keeps at the default, and the provider reports an E_USER_DEPRECATED on every start while it is set (The old file).

Behoben

  • The KoSIT validator (L5) reports a rule with the level the validator scenario gives it (customLevel), as the validator itself does when it decides on acceptance. Before, the level of the Schematron was used, so e.g. BR-CO-16 in an XRechnung extension invoice with third party payments was reported as error although the validator accepted the document.
  • The official validation in CI now checks PDF/A-3b with veraPDF: the job pulls the verapdf/cli image (pinned by digest), and the veraPDF tests fail instead of being skipped when the image is missing in CI. tools/validate-fixtures.sh also renders every EN 16931 scenario as ZUGFeRD PDF and checks the PDFs with veraPDF; before, it generated no PDFs, and its veraPDF call would have received no files.

0.3.0

Release auf GitHub

Neu

  • Single PHP rules can be switched off, re-rated or replaced by their code: validation.rules.disabled, validation.rules.severity and Quittio::extend('rule', …) in Laravel, RuleOverrides with Quittio::configureRules() in the core. Official EN 16931 and Peppol rules need allow_official; unknown codes are refused. Quittio::ruleCodes() lists the known codes.
  • The calculation policy decides what happens with declared amounts of other systems: calculation.declared_amounts (keep, recalculate, reject), CalculationPolicy and DeclaredAmountsMode in the core. An own calculator is configured with calculation.calculator or passed to Quittio::create(calculator: …); Quittio::calculator() returns it.
  • Quittio::edit() (core and Laravel) and InvoiceBuilder::fromUsing() continue existing invoice data with the calculator of the instance; cancellations and the document calculator of QuittioFake use it as well. InvoiceFactory::calculated() takes a calculator.
  • Tax presets whose rate depends on the date (Quittio\Core\Tax\DatedTaxPreset) are resolved with the supply date of each line (SupplyDate: line period, VAT point date, delivery date, invoicing period, issue date). DatedValues holds values that change on dates; TaxPresetCollection::alias() finds presets under further keys.
  • VatPreset::Standard (de.standard) and VatPreset::Reduced (de.reduced) take the German rate of the supply date (TaxTables), e.g. 16 % and 5 % from July to December 2020. The plausibility rule DE-VAT-RATE warns about rates that applied neither as standard nor as reduced rate on the supply date. The small amount limit of DE-USTG-08 is that of the issue date (150.00 EUR before 2017).
  • Country packs register output formats, aliases and serializers (Countries\Capabilities\ProvidesFormats, implemented by AbstractCountryPack); the German pack brings XRechnung (XRechnungFormats). Profiles declare sub lines, extension fields and third party payments with capabilities (SupportsSubLines, SupportsExtensionFields, SupportsThirdPartyPayments) instead of the core checking XRechnung keys.
  • Default interest for every country pack with a reference rate: Quittio\Core\Dunning\DefaultInterest (forCountry($pack)), the contract Dunning\ReferenceRates and the capability ProvidesReferenceRates; the German pack provides its base rates (BaseRateTable implements ReferenceRates) and its DefaultInterest uses the core calculation. DocumentBuilder::interestCharge() adds calculated charges with their day count and texts.
  • Documents can be queued in Laravel: Quittio::document($quote)->queue()->save($disk, $path) with the job RenderDocument, the events QueuedDocumentStored and QueuedDocumentFailed and QuittioManager::queueDocument(); QuittioFake records them (assertDocumentQueued(), queuedDocuments()). Documents are stored as versioned snapshots (DocumentData::toSnapshot(), fromSnapshot(), toArray(), toJson() …) with their own upcasters (quittio.snapshots.document_upcasters, SnapshotUpcasterChain::documentDefault()); Mapping\Attributes\MapOf keeps keyed arrays.
  • The German special cases are reached with the builder macro de(): ->de()->smallAmount(), ->de()->selfBilled(), ->de()->deductAdvance($advance) … (GermanInvoice).

Geändert

  • Lines without VAT treatment of a German seller get the preset key de.standard instead of de.standard_19, and quittio.defaults.vat = 'de.reduced_7' gives de.reduced. Applications that compare preset keys use VatPreset::fromKey($key)?->canonical(). Supplies from 2020-07-01 to 2020-12-31 get 16 % and 5 %.
  • CiiWriter, UblWriter, CiiReader and UblReader keep no state between calls: each call works with its own internal worker, so one instance can be used reentrantly. The readers no longer extend the internal XPathReader and therefore no longer expose its constants.
  • The render run resolves the logo of the presentation before it builds the view model (ViewContext::$logo, ResolvedLogo), so the view layer does no I/O; ViewModelFactory keeps no state between calls.
  • The documentation website is versioned: the root shows the latest release, /next/ the development version and /X.Y/ older minor versions, with a version switcher (docs/_versioning/build.mjs). Every version has a generated API reference under api/ (phpDocumentor, tools/docs/phpdoc.sh); the builder traits under Builder\Concerns are no longer @internal, so their methods appear there.
  • Pull requests opened in the read-only split repositories quittio/core, quittio/laravel and quittio/country-de are closed automatically with a pointer to the monorepo.

Veraltet

  • VatPreset::Standard19 and VatPreset::Reduced7: use VatPreset::Standard and VatPreset::Reduced; the keys de.standard_19 and de.reduced_7 stay readable. Flags::SMALL_AMOUNT_LIMIT: use TaxTables::smallAmountLimitOn().
  • The builder macros smallAmount(), propertyRelatedService(), newVehicle(), selfBilled(), deductAdvance() and consumerDefaultNotice() directly on the builder: use ->de()->…. GermanInvoiceMacros::ADVANCE_TYPES: use GermanInvoice::ADVANCE_TYPES. FormatRegistry::XRECHNUNG_CII and XRECHNUNG_UBL: use XRechnungFormats::CII and UBL of the German pack, which registers the formats; the core keeps them until 1.0.

Behoben

  • Peppol invoices with an electronic address scheme that Peppol does not accept, e.g. EM (e-mail), are rejected with the new rule PEPPOL-EN16931-CL008 instead of passing the PHP rules and failing the official Peppol validation. The allowed schemes are the code list CodeList::PEPPOL_EAS (peppol-eas-codes.json, from the Peppol BIS rules 3.0.20); tools/codelists/update.php regenerates it with CodeListGenerator::generatePeppol().

0.2.0

Release auf GitHub

Neu

  • The legal texts of the German pack (LegalTexts) and the text of default interest come from its language files (resources/lang/{de,en}/legal.php), so lang/vendor/quittio-de overrides them in Laravel; LegalTexts::DEFAULT_INTEREST is new.
  • quittio/country-de ships stubs/InvoiceBuilder.stub.php, which describes its builder macros (smallAmount(), selfBilled(), deductAdvance(), …) for IDEs and PHPStan.
  • Tests: generated round trips through CII and UBL, fuzzing of the XML, PDF and array input, and tests for formats, mapping, serialisation, the PDF parser, mail attachments, a second boot and the escaping of the Blade templates.
  • Documentation: Versioning and compatibility with the backwards compatibility promise; ARCHITECTURE.md, decision records in docs/adr/, RELEASING.md, a reworked CONTRIBUTING.md, an FAQ page; issue and pull request templates, CODEOWNERS and response times in SECURITY.md. Classes outside the public API (XPathReader, XmlBuilder, Pdf\Parsing, CarryOver, ConfigMapper, the command and preview classes, …) are marked @internal.
  • Documents (quotes, reminders, …) dispatch DocumentRendering and DocumentRendered in Laravel; PendingDocument has option() (core and Laravel), data(), options() and is Macroable (Laravel).
  • Quittio::setPostProcessor() replaces the PDF/A and XML embedding for renderers that cannot embed. Profiles describe their embedded XML with the capability DescribesEmbeddedXml (XMP description and fx:Version) instead of the file name deciding it.
  • Amounts of prepaid(), addPrepaid(), rounding(), discountPercent(), discountAmount() and of the document builder's fee() and openingBalance() accept BigDecimal, int or string like line prices (floats are rejected). DocumentBuilder::note(), discountPercent() and discountAmount() take the same subject and reason codes as the invoice builder.
  • QuittioFake dispatches the events of a real run (InvoiceRendering, InvoiceValidated, InvoiceRendered) and records queued invoices: assertQueued(), assertNothingQueued(), queued(). Its decisions follow configuration changes without Quittio::flush().
  • Events QueuedInvoiceStored (a queued job stored the file, with disk and path) and QueuedInvoiceFailed (a queued job failed for good).
  • Quittio\Core\Quittio::resetState() forgets the process-wide state (translator bridge, default snapshot upcasters, application default VAT, base rates of the German pack); extensions register their own state with onResetState(). The test trait InteractsWithQuittio calls it after each test.

Geändert

  • README and documentation mark the Managed layer quittio/managed as not released yet; its documentation page is a preview and is left out of llms.txt, llms-full.txt and the MCP index.
  • The documentation is built and link-checked on pull requests; every PHP code block of the documentation must parse.
  • quittio/core, quittio/laravel and quittio/country-de declare their test dependencies and ship a phpunit.xml.dist, so their split repositories run composer test on their own.
  • quittio/quittio registers the Quittio facade alias and suggests ext-intl, ext-zip and opis/json-schema like the single packages.
  • Development: Composer scripts test:fast, test:official, test:visual, test:coverage, test:mutation, docs:generate, codelists:update and ci; a weekly CI run with composer audit, line coverage and mutation testing; PHPStan also checks tools/*.php and reports calls of deprecated code.
  • Visual baselines are written only with QUITTIO_UPDATE_BASELINES=1; a missing baseline fails the test. The default Blade partials that print pre-escaped HTML carry a warning comment.
  • Queued RenderInvoice jobs store the render options as versioned JSON (RenderOptions::toArray()/fromArray()) instead of a PHP-serialised object, so an update of Quittio no longer breaks jobs in the queue; jobs queued by 0.1.0 still run. Errors that another attempt cannot fix (validation, invalid data, configuration) fail the job at once instead of three times; other errors are retried after 10 and 60 seconds. Inside a database transaction the job is dispatched after the commit (quittio.queue.after_commit).
  • Every exception that Quittio throws implements QuittioException: invalid arguments and runtime errors are now Quittio\Core\Exceptions\InvalidArgumentException, RuntimeException or LogicException, which extend the PHP classes of the same name, so existing catch blocks keep working.
  • Country pack discovery keeps its result in bootstrap/cache/quittio-countries.php (and per process) until Composer changes the installed packages, instead of reading vendor/composer/installed.json whenever the core is built.
  • The Laravel configuration no longer defaults to Germany: defaults.country (QUITTIO_COUNTRY) and seller.address.country are null, which means the only installed country pack (Germany with quittio/quittio), so existing applications keep their behaviour. With several packs, set defaults.country or the seller's country. Quittio::registerCountry() makes the next call build the core again.
  • The Laravel manager builds a new core when the quittio configuration changes, so Quittio::flush() is no longer needed after config()->set(). Under Octane it reads the configuration of the current request instead of the application that created the singleton; so does the default VAT of lines without VAT.

Veraltet

  • Environment variables are named after their configuration keys: QUITTIO_DEFAULT_COUNTRY, QUITTIO_VALIDATION_MODE, QUITTIO_MANDATORY_FIELD_GUARD, QUITTIO_STORAGE_DISK and QUITTIO_PREVIEW_ENABLED. The names of 0.1.0 (QUITTIO_COUNTRY, QUITTIO_VALIDATION, QUITTIO_GUARD, QUITTIO_DISK, QUITTIO_PREVIEW) are still read when the new one is not set, until 1.0.
  • The methods of Quittio\Core\Quittio that change the instance are named after what they do: addSerializer(), addValidator(), clearValidators(), setRenderer(), setTemplateEngine(), setPaymentQrGenerator() and setFormatResolver(). The with…() names, which elsewhere in Quittio return a changed copy, remain as deprecated aliases until 1.0.

Sicherheit

  • quittio:schemas:download checks the KoSIT validator, the XRechnung configuration, the Peppol rules and SchXslt against pinned SHA-256 checksums before it unpacks or runs them; so do the download scripts in tools/.
  • Decimal strings are only accepted in plain notation (Decimal::of(), builders, InvoiceData::fromArray()/fromJson(), snapshots): an exponent such as 1e999999999 was expanded to a billion digits and exhausted the memory. Other notations throw a NumberFormatException (a HydrationException for array and JSON input).
  • The preview route refuses requests outside the local environment unless the gate viewQuittioPreview allows them; before, QUITTIO_PREVIEW=true made it public behind the web middleware only. Requests are limited to 30 per minute (quittio.preview.throttle). QuittioServiceProvider::registerPreviewRoutes() is no longer public.

Behoben

  • Default interest of payment reminders was described in English for German locales with a region such as de_AT.
  • InvoiceData::fromJson() threw PHP's JsonException for JSON that cannot be parsed (empty, too deep, invalid UTF-8); it now throws a HydrationException. JsonSchemaValidator::validate() reports such input as a finding.
  • DE-USTG-08 names the limit of Flags::SMALL_AMOUNT_LIMIT (250.00 EUR) instead of a second, hard-coded value. The English fallback messages of four UStG rules matched their translations no longer; a test now compares every rule's fallback with lang/en/rules.php and requires a German message.
  • The field reference documents the UBL credit note path of BT-129 and BT-130 (cbc:CreditedQuantity) and no longer shows pseudo-XPath for BT-110 and BT-111. A test now evaluates every documented CII and UBL path against the XML of the sample invoices and of the official XRechnung test suite.
  • A custom TotalsCalculator was bypassed by the VAT alignment of partial cancellations; Cancellation::of() and DeclaredAmounts::needed() accept the calculator, and Quittio::cancellation() passes its own.
  • Texts with control characters (e.g. a vertical tab copied from a spreadsheet cell) or invalid UTF-8 produced XML that was not well-formed and was rejected by every receiver. The new rule QUITTIO-INVALID-CHARACTERS names the field, and the CII and UBL writers refuse such data with an InvalidInvoiceData exception.
  • A UBL credit note without payment instructions (BG-16) left out its due date (BT-9) silently: the UBL binding transmits it only in cac:PaymentMeans. The new warning QUITTIO-CREDIT-NOTE-DUE-DATE points this out; the field reference names the credit note path.
  • A percentage document allowance or charge without base amount whose VAT category and rate matched no invoice line became 0.00 silently (data from JSON, readers or with()); the calculator now throws a CalculationException. Drafts without any lines still calculate with a zero base.
  • Reading an invoice and the check for declared amounts no longer turn errors such as a TypeError of a custom TotalsCalculator into the warning QUITTIO-READER-RECALCULATION; only exceptions are reported.
  • The post-processing of PDFs from renderers that cannot embed (PDF/A and XML) uses the configured temp_dir instead of the system temporary directory.
  • quittio:validate: an unknown --profile printed a stack trace; files that cannot be checked (missing, not an e-invoice, unknown profile) now exit with 2 instead of 1, so scripts can tell them from invalid files; with --json such errors are printed as {"valid": false, "error": …} and the result names expectedProfile and profileMismatch.
  • Failed writes are reported with a clear message: quittio:render, quittio:preview and quittio:base-rate:update exit with 1, RenderedInvoice::save() of the Laravel package throws a RuntimeException when the disk does not store the file. save() of the core results and quittio:render --output treat a path ending in / as directory and create missing directories (RenderedDocument::save() did not create them).
  • The CII and UBL readers accept every xs:boolean spelling of ChargeIndicator (1/0, surrounding white space). Before, such document allowances and charges were dropped and line charges were read as allowances; a missing or invalid indicator is now reported as QUITTIO-READER-CHARGE-INDICATOR. The documented CII and UBL paths of BG-20 and BG-21 are valid XPath.

0.1.0

Release auf GitHub

First release.

Neu

  • Monorepo quittio/quittio (the complete package) with the read-only splits quittio/core, quittio/laravel and quittio/country-de.
  • EN 16931 data model with mapping attributes (BT, cardinality, profile level, CII and UBL paths), builders, JSON conversion, generated JSON schema and field reference.
  • Calculation according to EN 16931 including document allowances split by VAT rate, gross prices, foreign currency and third party payments.
  • Profiles MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, Peppol BIS 3; XRechnung 3.0 and the XRechnung extension in the country pack Germany.
  • Validation levels L1 to L7: PHP rules (EN 16931, Peppol, BR-DE, UStG), XSD, KoSIT validator (daemon and CLI), veraPDF (CLI and Docker), plausibility checks.
  • CII and UBL writers and readers with protection against XXE, entity expansion and oversized input; PDF reader for embedded invoices with decompression limits.
  • ZUGFeRD/Factur-X PDF/A-3b with mPDF, XMP metadata and associated files; post processing of foreign PDFs; EPC GiroCode.
  • View model with formatted and raw values, mandatory field guard, plain PHP and Blade default templates with partials, DIN 5008 letter layouts.
  • Format resolver, entry point Quittio\Core\Quittio and Laravel facade with several formats per call, storage, downloads, mail attachments, queue, events, preview route and Artisan commands.
  • Quotes, order confirmations, delivery notes, proforma invoices, payment reminders and account statements.
  • Country pack Germany: tax presets, UStG rules, format rules, legal texts, BMF error classes, numbering, retention, dunning law with base rate table, correction policy, builder helpers.
  • Testing helpers: fake, assertions, invoice factory, sample scenarios, visual regression (identical baselines on macOS and the Linux CI image).
  • quittio:schemas:download compiles the Peppol BIS Billing 3.0 rules; KoSIT configuration accepts several scenario sets.
  • Base rates up to 1 July 2026; small business invoices according to section 34a UStDV.
  • Full cancellations (Quittio::cancellation()) with configurable sign strategy: type code 381 with positive amounts (default) or 380 with negative amounts (corrections.cancellation_strategy).
  • Quittio::prepare() / PendingInvoice::prepare(): calculation, format and profile choice and validation without rendering.
  • InvoiceBuilder::updateLine(), removeLine() and InvoiceLine::withNetPrice().
  • Complete reference documentation: new pages Building invoices, Laravel integration and Configuration; every builder method, config key (both packages), command, event, exception, tax preset and rule is documented, the Managed layer page covers models, numbering, archive, immutability, delivery, audit log, authorisation and retention in full.
  • Documentation website built from docs/ with Jekyll and Just the Docs (navigation, search), deployed to Cloudflare by .github/workflows/docs.yml; the Markdown files stay the only source.
  • Country pack Germany: tax preset VatPreset::TriangularTransaction for intra-community triangular transactions (section 25b UStG, wording of section 14a (7) UStG).
  • Country pack Germany: rule DE-USTG-11 requires the VAT identifiers of seller and buyer for reverse charge services in another member state and for triangular transactions (section 14a (1) and (7) UStG); the tax number or a legal registration identifier is not sufficient there.
  • Country pack Germany: ->deductAdvance($advanceInvoice) for final invoices (section 14 (5) sentence 2 UStG): references the paid advance invoice, adds it to BT-113 and itemises its net amounts and VAT per rate in a note. InvoiceBuilder::addPrepaid() adds to BT-113.
  • quittio.seller.small_business now takes effect: Seller::apply() marks the invoice (Meta::FLAG_SMALL_BUSINESS) and the German pack rejects VAT on it (DE-USTG-09).
  • Official KoSIT test for every German tax preset in XRechnung CII and UBL; the XRechnung golden-file tests run in the official CI job; CI validates every package composer.json.
  • Laravel: format_resolver.peppol_lookup (invokable class or callable receiving the BuyerParty) for the automatic Peppol choice; Quittio::coreWithLevels(); RenderedDocument::hasPdf(); Quittio::fake()->documents().
  • Core: LineBuilder::showGrossPrice(); InvoiceBuilder::payee() accepts a PayeeParty, identifier schemes or a PartyBuilder callable; Quittio::cancellation() takes an optional $title; country pack readers (CountryPack::readers()) are used by read() and validateFile().
  • Country pack Germany: info DE-LEITWEG-ID-SUSPECT for buyer references shaped like a Leitweg-ID with wrong check digits; warning DE-USTG-13 when newVehicle() is used without a line of category K.
  • Country pack Germany: the base rate (§ 247 BGB) can be updated without a release: quittio-de.base_rates in the new configuration config/quittio-de.php, php artisan quittio:base-rate:update fetching it from the statistics API of the Deutsche Bundesbank (BundesbankBaseRates), or BaseRates::register(); DefaultInterest uses these rates by default. The pack has a Laravel service provider of its own (GermanyServiceProvider, discovered automatically).
  • Country pack Germany: tax numbers (BT-32) of German sellers are checked for format and check digit according to the ELSTER procedures of every federal state (warning DE-TAX-NUMBER); TaxNumber parses the ELSTER, federal and state notations.
  • Country pack Germany: rule DE-USTG-14 rejects intra-community supplies (category K) of small businesses except for new vehicles (section 19 (1) sentence 2 and (6) sentence 1 UStG); warning DE-USTG-15 for invoices on intra-community supplies and on services in another member state issued after the 15th of the month following the supply (section 14a (1) sentence 2 and (3) sentence 1 UStG); UstgRules::issuingDeadline().
  • Documentation for AI assistants: llms.txt, llms-full.txt and every page as Markdown on the website, a read-only MCP server at https://docs.quittio.dev/mcp (tools search_docs, get_page, list_pages) and Laravel Boost guidelines and the skill quittio-development in quittio/quittio and quittio/laravel.
  • Core: shorter invoice code. Lines can be given as name with named values (->line('Consulting', netPrice: '120.00', quantity: 4, unit: UnitCode::Hour), also vat, description and priceInclVat; the same in Line::make(), subLine() and the document builder), as Line::make('Consulting', …)->… or as arrays whose keys are the LineBuilder methods (LineBuilder::ARRAY_KEYS, fill(), Line::fromArray()), parties as Party::make('Kunde AG')->… or as application models: ->buyer($customer) for models implementing ProvidesParty. subLine() and the document builder accept the same forms.
  • Core: default VAT for lines without vat(): InvoiceBuilder::defaultVat() and DocumentBuilder::defaultVat() per invoice, InvoiceBuilder::useDefaultVat() for the application, CountryDefaultVat (configured preset, otherwise the standard preset of the seller's country pack, for small business invoices its small business preset), CountryDefaults::$vatPreset and $smallBusinessVatPreset (Germany: de.standard_19, de.small_business), CountryRegistry::taxPreset().
  • Core: InvoiceBuilder::applyDefaults() remembers the defaults of a seller; seller() with another seller removes them again, except values the caller set. Quittio::invoice() and the document methods use it, so another seller gets neither the bank account nor the small business flag (nor, with quittio/managed, the payment terms, design, locale, time zone and currency) of the configured seller.
  • validation.locale: the language of validation messages, independent of the invoice language. In Laravel the default is the language of the application (app()->getLocale()), 'invoice' restores the language of the invoice; without Laravel the default stays the invoice language, 'app' uses the translator's locale. ValidationContext::messageLocale().
  • Laravel: Quittio::invoice()->…->check() validates without rendering.
  • Core: fewer callbacks. dueInDays(), cashDiscount() and lateFee() on the invoice and document builder replace paymentTerms(fn ($t) => …); presentation() takes an array of changes; updateLine('2', netPrice: '80.00', quantity: 3) takes named values; payee() takes a PartyBuilder. The callbacks keep working.
  • Laravel: QUITTIO_SELLER_SMALL_BUSINESS for quittio.seller.small_business.
  • Laravel: config/quittio.php explains every key; pdf.colors.secondary is listed (default null, as before).
  • PDF: carry-over across pages. When the line table continues on the next page, the page ends with "Fortsetzung auf Seite 2 · Übertrag" and the sum of the lines so far, the next page starts with "Übertrag von Seite 1" (English "Continued on page 2 · Carried forward", "Brought forward from page 1"). The mPDF renderer lays a document out again only when the lines span pages; own templates enable it with markers (templates), view model lineGroups[].lines[].carryOver. Following pages get a header with logo or name, title and number, recipient, customer number and date.
  • Fonts: Geist (SIL Open Font License) is bundled and the default font, with the family geistmedium (Medium, SemiBold) for medium weights; Inter stays bundled (pdf.font = 'inter', also for Greek).
  • View model: design.fontMedium, design.continuationMarginTop, design.logoMaxHeight, design.footerHasTaxIds, totals.dueEqualsGross; labels payment, transfer_until, remittance_short, vat_row_single, your_vat_id, your_tax_number, tax_number_short, carry_over, carry_over_from, continued_on.
  • Documentation for first-time users: new pages E-invoicing basics (EN 16931, CII and UBL, formats, profiles, rule codes, which format you need, German terms) and Troubleshooting (common errors with cause and fix); Getting started shows the minimal configuration of your company and explains a first invoice line by line; every page starts with what it is for, explains its terms and is ordered from the common case to the reference; German text in the English documentation is translated or explained.
  • Laravel: quittio.defaults.vat (tax preset of lines without ->vat(), default the standard rate of the seller's country) and RenderedInvoice::cancel($number, reason: …), which renders the cancellation in the formats of the invoice; Quittio::defaultVat().
  • Laravel: Quittio::invoice() starts an invoice with the seller, bank account and small business setting of the configuration and renders it with ->render() (optionally a format or a list of formats); Quittio::quote(), orderConfirmation(), deliveryNote(), proforma(), paymentReminder() and accountStatement() start documents the same way. The seller comes from the binding of Quittio\Laravel\Contracts\InvoiceDefaults (default ConfigInvoiceDefaults; quittio/managed binds the current issuer).
  • Documentation page Examples: complete examples from service invoices, small businesses, XRechnung and customers abroad to cancellations, Laravel orders, drafts, reminders and incoming invoices; a test runs the code of the page, and another one checks that every code block of the documentation imports the classes it uses.

Geändert

  • Country pack Germany: DE-USTG-01 names the party and the missing address fields ("Die Anschrift des leistenden Unternehmers (Verkäufer, BG-5) ist unvollständig, es fehlt: Straße und Hausnummer, Postleitzahl, Ort") and says where to fill them in. The message of ValidationException contains the hint of every listed finding.
  • Core: macros of InvoiceBuilder, LineBuilder and DocumentBuilder run in the scope of the builder class, so they reach its state also on subclasses.
  • Core: a line without quantity() has the quantity 1 instead of failing with BT-129.
  • Repository layout: the root package is quittio/quittio (it replaces quittio/core, quittio/laravel and quittio/country-de); the meta package was removed; the packages are split into read-only repositories on every push; the commercial quittio/managed lives in its own repository.
  • Country pack Germany: the format rule only decides for public buyers (Leitweg-ID with valid check digits or endpoint 0204: XRechnung, also for sellers abroad) and domestic small amount invoices (PDF); consumers, the Peppol lookup, domestic B2B (ZUGFeRD with the configured profile), sellers outside Germany and buyers in Büsingen or Heligoland are left to the format resolver.
  • check() and prepare() always validate (whatever validation.mode or withoutValidation() say), validate the CII XML that pdf embeds with b2c_embed_xml exactly as render() writes it, and refuse XML formats for watermarked data like render().
  • Laravel: InvoiceValidated is no longer dispatched in validation mode off; the unused networks config section was removed; Quittio::fake() picks format and profile like a real run (defaults.format, automatic choice, profile keys, all formats with as()), honours htmlOnly() and throws for unknown formats or profiles.
  • English tax and legal texts of the German pack use the terms of Directive 2006/112/EC; LegalTexts notices take the preset texts, so one notice has one wording.
  • Font directories configured for the mPDF renderer take precedence over the bundled fonts and the mPDF fonts; fonts published with --tag=quittio-fonts are found without configuration.
  • New design of the default templates (Blade and plain PHP): logo or issuer name at the top left; the sender line at the top of the address field and the recipient directly below it (the DIN 5008 zones stay available to own templates); the invoice details on the right; the line table with a rule below the column heads, item names in medium weight, a VAT column only for several rates; totals with "USt. 19 %" for a single rate and the gross total (or the amount due) large in the primary colour; payment terms, bank details and payment QR code side by side; a footer in four columns (address; phone and e-mail; register and legal information; VAT identifier and tax number, which leave the invoice details) with the page number centred. New default colours #2741d9, #15171c and #5f6470, top margin of the first page 15 mm, fold and punch marks on by default (pdf.fold_marks), the bank details are no longer repeated in the footer, the account holder is printed only when it is not the issuer. German labels: "Beschreibung", "Betrag", "Zahlung", "GiroCode", "2 % Skonto bis 10.10.2026: 2.692,76 €", "Bitte bis zum 17.10.2026 überweisen." for credit transfers, "Ihre USt-IdNr."; service periods within one year read "01.10.–31.10.2026".

Behoben

  • Laravel: a published config/quittio.php without validation.levels keeps the default checks (php, xsd) instead of running none; only an empty list switches them off.
  • The hint of rule BR-CO-25 names ->dueInDays(14) instead of the callback form; a validation message with one finding reads "(1 error)".
  • Documentation: facts corrected against the code, among them the order in which the profile is chosen, the template of a presentation passed to ->presentation(), the signatures of line(), subLine() and the document line methods, the day count in the default interest examples, BundesbankBaseRates::fetch() (an instance method), the check of German tax numbers (DE-TAX-NUMBER) and that an unavailable veraPDF stops the rendering by default.
  • Amounts in locales with a multibyte grouping separator (French) were garbled: the digits were grouped by reversing the string bytes including the separator. ICU's narrow no-break space is now replaced by a no-break space, which every bundled font has.
  • Legal references: self-billed invoices are section 14 (2) sentence 5 UStG (was sentence 2); DE-USTG-10 names its basis (Article 230 VAT Directive, section 16 (6) UStG).
  • quittio:validate --kosit/--verapdf uses every driver setting (java, docker, timeout, fail_on_unavailable) and no longer changes the cached core; defaults.format = 'auto' is accepted in Laravel and as issuer default; quittio:preview and the preview route accept the views of pdf.templates; the facade docblock lists every manager and fake method; the ASCII fallback file name of downloads transliterates characters ("Ü" becomes "U").
  • Profile validation no longer warns about postal addresses in BASIC WL and BASIC; address fields carry their Factur-X levels and the business terms of their context (e.g. BT-55 for the buyer country). Data of EXTENDED and of the XRechnung extension, and fields below a field that is not allowed, are reported once.
  • Reading a CII or UBL invoice without document totals reports BR-12 to BR-15 instead of QUITTIO-NOT-CALCULATED.
  • In the view model of documents (quotes, reminders …) document.typeCode is null and document.type names the document type; the default templates highlight lines marked with highlight() and use colors.secondary; CountryDefaults::$paymentQr selects the QR code of the mode auto; self-billed invoices take their title from the country pack's correction policy.
  • paymentTerms() with a callable continues existing terms, also after InvoiceBuilder::from().
  • Country pack Germany: DE-USTG-06 accepts the pack's reverse charge texts in every shipped language; newVehicle() adds the note of section 14a (4) UStG; deductAdvance() writes the whole note in one language and rejects documents that cannot be advance invoices (credit notes, corrections, negative totals); VatPreset::exempt() defaults to German like the enum presets; DefaultInterest uses the day count of the dunning law (365) and BaseRateTable::periods() merges periods with the same rate.
  • Floats as amounts, prices, rates and quantities are rejected with an InvalidArgumentException also in files without declare(strict_types=1); before, PHP turned netPrice(19.99) into 19 for parameters typed BigDecimal|int|string. The same applies to InvoiceLine::withNetPrice()/withPriceInclVat(), CustomTaxPreset::make() and partial cancellations of the Managed layer.
  • UBL credit notes write the tender or lot reference (BT-17) as OriginatorDocumentReference in the element order of CreditNote; it was dropped before.
  • Default line identifiers skip identifiers set with id() (a line without id() and one with id('1') both got 1); identifiers used twice make build() throw InvalidInvoiceData, and data from other sources gets the warning QUITTIO-LINE-ID-DUPLICATE.
  • The configured profile (defaults.profile) applies to every format that allows it, also to ->format('zugferd') and formats chosen by a country rule; Config::$profile defaults to null (format or automatic choice decides). XRechnung CII with sub lines reports the sub lines in the validation instead of failing with "does not support profile extended".
  • The template of the presentation passed to ->presentation() and of the configured presentation selects the template (after ->template() and the invoice's own presentation).
  • Laravel: country packs listed in quittio.countries replace discovered packs with the same code (e.g. a subclass of CountryDE).
  • swiss-qr-bill without a generator fails with a ConfigurationException instead of printing no code; generators are registered with Quittio::withPaymentQrGenerator() or quittio.pdf.payment_qr.generator. PDF renderers whose supportsEmbedding() is false get PDF/A and the embedded XML from MpdfPostProcessor; CallbackRenderer and PassthroughRenderer report true.
  • A missing java, verapdf or docker binary is reported as KOSIT-UNAVAILABLE/VERAPDF-UNAVAILABLE instead of aborting the render run with an ErrorException in Laravel.
  • Country pack Germany: invoices with VatPreset::ZeroRatedPv (section 12 (3) UStG) failed BR-Z-10, because the preset sent an exemption reason; the zero rate is now stated as the rate only.
  • Country pack Germany: order numbers shaped like a Leitweg-ID (e.g. 4711-01) no longer switch the format to XRechnung; only a Leitweg-ID with valid check digits or a 0204 endpoint does, and the check digit warning is limited to public buyers.
  • Country pack Germany: "Gutschrift" is required for all self-billed type codes (261, 389, 527), not only 389.
  • Country pack Germany: default interest texts named the first day of the next period as end date; they now name the last day with interest.
  • Country pack Germany: the small amount simplifications (section 33 UStDV) no longer apply to reverse charge, intra-community supplies and distance sales (section 33 sentence 3 UStDV, rule DE-USTG-12); such invoices need all mandatory information and are not rendered as plain PDF.
  • Country pack Germany: DE-USTG-09 also rejects VAT on document level charges of small businesses; DE-USTG-01 requires the postcode of German buyers; DE-USTG-11 also applies to reverse charge lines without a preset when the buyer is in another member state.
  • Country pack Germany: ->deductAdvance() rejects advance invoices in another currency and deducts the amount received (including a cash rounding BT-114).
  • Country pack Germany: the base rate table rejects dates from the next scheduled change (1 January or 1 July) after the last known rate instead of silently using the old rate (BaseRateTable::validUntil()); the § 288 (5) BGB flat rate uses the money context of the documents.
  • quittio.storage.disk is used as the default disk of save().
  • Package dependencies: quittio/core requires ext-gd (needed by mPDF) and ext-ctype, quittio/country-de declares Brick, quittio/laravel and quittio/managed require laravel/framework (they use Illuminate\Foundation); the autoload prefix of quittio/laravel is Quittio\Laravel\.
  • Calculation: exemption reasons are only written into VAT breakdowns of categories that allow them (E, AE, K, G, O); S and Z breakdowns no longer fail BR-S-10/BR-Z-10.
  • Calculation: gross prices and document allowances are distributed with signed weights, so vouchers, returns and negative lines keep their sign and correct net amounts; a negative gross price is rejected.
  • Calculation: document allowances and charges accept an exemption reason and code for VAT groups they create alone; sub lines with a VAT category or rate other than their parent are rejected (QUITTIO-SUBLINE-VAT).
  • XML: BT-6 is omitted when it equals the invoice currency (BR-53 in CII); MINIMUM writes only the seller country; a warning reports preceding invoices (BG-3) that CII cannot carry (QUITTIO-PRECEDING-INVOICES-CII); XRechnung BT-20 contains free text, due days and late fee terms before the #SKONTO# lines.
  • Reading: exemption reasons of the VAT breakdown are copied to the lines, a declared BT-111 survives recalculation, all VATEX codes of the code list are accepted.
  • PDF: watermarked drafts and previews never embed e-invoice XML (an embedded format with a watermark throws), watermarked documents and HTML show the watermark.
  • PDF: the mandatory field guard only counts printed values (not count() or loops) and checks regulatory notes in legalNotes; the German pack uses the keys servicePeriod, line periods and taxRepresentative.vatId.
  • PDF: the seller's tax representative (BG-11), payee (BG-10), delivery address (BG-13) and price base quantity (BT-149) are printed; every cash discount tier shows its own amount; credit notes show "Erstattungsbetrag" without bank details; titles follow the type code family (self-billed → "Gutschrift", credit notes → "Rechnungskorrektur", 325 → "Proforma-Rechnung", 386 → "Anzahlungsrechnung"); commercial credit notes are no longer labelled "Gutschrift" in the code lists.
  • PDF: the DIN 5008 address field keeps its position with a logo; VAT rows are labelled per category; split allowances name their VAT rate; quotes and order confirmations show no payment block or GiroCode; the EPC payload respects the 331 byte limit; the render locale wins over the invoice locale.
  • Security: template names from requests or options are never file paths (PhpTemplateEngine resolves names only inside the configured template directories); the preview route accepts only the default, the configured and the package templates.
  • PDF/A: logos are re-encoded as RGB PNG on white (logos with transparency or above 600×200 px made every ZUGFeRD render fail), SVG logos are rasterised (Imagick) or rejected; the GiroCode PNG is compressed.
  • PDF: the mandatory field guard checks every line (name, quantity), every VAT row, every legal note, the cash discount terms, both buyer name and address, the service date (BT-72/BG-14, otherwise every line period) and the full tax representative; null values no longer count as printed.
  • PDF: self-billed invoices show the buyer as issuer and the seller as recipient, with payout wording and without GiroCode; margin scheme and travel agency rows state no VAT amount (section 14a (6) UStG); descriptions and notes are plain text by default (Markdown opt-in, it may change the displayed text); a watermark together with any XML format throws; per-call presentations merge over the configured one; payment reminders encode the total and the open invoice numbers in the GiroCode; DIN 5008: the recipient starts in the Anschriftzone.
  • XMP: fx:Version is the XRechnung version for the XRECHNUNG conformance level, the XRechnung extension declares XRECHNUNG.
  • XML (EXTENDED): exemption reasons on line and allowance/charge level, one payment terms group per cash discount tier, the exchange rate; German BT-20 free text uses German number and date formats.
  • Builder: payment() replaces the payment instructions instead of accumulating accounts (duplicate IBAN and lost BT-83 in foreign currency invoices, BR-DE-25-b on direct debit); percentage allowances and charges follow their base after line changes; InvoiceLine::withPriceInclVat(), price modes cannot be mixed.
  • Cancellations: read foreign currency invoices keep BT-111 (implied exchange rate), XRechnung third-party payments are reversed in full cancellations only, gross-price lines can be cancelled by amount, self-billed documents stay self-billed (389 ↔ 261).
  • Laravel: seller.identifiers (BT-29) for sellers without VAT identifier; explicit pdf.paper wins over the country default; unknown letter layouts and QR types throw; queued renders travel as snapshots through the upcaster chain; removed the unused defaults.currency, defaults.timezone and defaults.standard.
  • Reading: own PDF parser with cross-reference streams, object streams and incremental updates (PDFs from PDFBox, iText, Acrobat or Word could not be read); name tree loops, exponent numbers, oversized tags and attribute floods are rejected; strict dates (never replaced by today, BR-03 for a missing issue date); no default currency (BR-05), BT-110 only in the invoice currency, BT-149 from the gross price, BR-CL-06 for BT-8, BR-DEC as errors, sanitised attachment names, embedded files chosen via /AF with findings for ambiguous PDFs, a specific message for ZUGFeRD 1.0; MINIMUM and BASIC WL documents without lines no longer trigger line based rules.
  • Reading API: read() and validateFile() take the content only, never a path; readFile() and validatePath() read files.
  • Documents: proforma invoices (325) are rejected in the invoice pipeline (QUITTIO-PROFORMA, use DocumentBuilder::proforma()); payment reminders and account statements take no lines, the reminder total is open items + fees + interest without VAT and the GiroCode uses it with the open invoice numbers; account statements respect their period; interest uses actual/actual per calendar year and the text names the last interest day; level 1 reminders are titled "1. Mahnung".
  • Countries: the buyer's country pack also applies for sellers outside the EU (e.g. XRechnung for German public buyers); VatTerritory knows the EU VAT territory (XI for goods, Monaco, Isle of Man, Büsingen, Helgoland and other excluded areas); VIES is only queried for EU member states and XI, and unavailable answers are not treated as invalid; L7 warnings for intra-community supplies and exports to the wrong destination.
  • Partial cancellations reverse taxable amounts and VAT exactly: per VAT group the VAT of a cancellation is round2(cumulative cancelled taxable amount × rate) minus the VAT already cancelled, the closing cancellation takes the original's VAT; in gross price mode the net amounts are aligned the same way (cumulative gross / (1 + rate)), so BT-116 and BT-117 per group and BT-109 to BT-115 add up exactly over every sequence; each document stays within one cent of its own rounding and keeps the sign of its taxable amount (BR-CO-17); the same for BT-111 in foreign currency invoices; originals of other systems with their own rounding are aligned with their effective rate. Managed stores the cancelled amounts per group (vat on invoice relations), recomputes them under lock and refuses a cancellation that raced with another one; core users can pass a CancellationHistory, which is validated against the original. Aligned documents carry the info QUITTIO-CANCELLATION-VAT-ALIGNED and a note sentence when a group's VAT differs from its own rounding; drafts keep a declared VAT breakdown only for aligned cancellations.
  • Reading PDFs: the reconstruction of damaged files scans one object at a time and stops above 100,000 objects, PNG predictors only process the data present, the catalogue search no longer caches every object (memory and CPU exhaustion with small crafted files).
  • PDF: invoices with line groups (->group()) failed to render under Laravel (mPDF warning with packTableData); refunds and self-billed invoices no longer announce a direct debit; SVG logos are read only with the SVG coder at a bounded resolution, logos above 10 MB or 25 megapixels are rejected; the GiroCode beneficiary falls back to the seller name; partials cannot be previewed as templates.
  • Reading XML: encodings that hide the markup from the pre-scan (UTF-7, EBCDIC, UTF-32) and DOCTYPEs are rejected before libxml parses them, namespace declarations in scope are limited (CPU exhaustion with libxml < 2.12); BT-6 is no longer interpolated into XPath (injection of BT-111); relative date strings ("tomorrow 2026", "+2000 days") are rejected also in the builder; cash discount periods in weeks; bidi control characters are stripped from attachment names.
  • Calculation: different exemption reasons in one VAT group are joined in BT-120 (e.g. § 4 Nr. 14 UStG together with a margin scheme), BT-121 is only kept when the codes agree (QUITTIO-EXEMPTION-CONFLICT warning); percentage document allowances follow removed lines; line ids no longer collide after removeLine() and line().
  • Cancellations of self-billed invoices are titled "Storno-Gutschrift" with the note "Stornierung der Gutschrift …" (CorrectionPolicy::cancellationTitle(), title key cancellation_self_billed).
  • Country pack Germany: the § 3c distance sale exclusion of small amount invoices uses the VAT territory and the deliver-to address and only warns; "Gutschrift" may be given in another official EU language (UStAE 14.5 Abs. 24); small amount invoices in a foreign currency are compared by their EUR equivalent.
  • PDF: remote logos are fetched by Quittio only from public addresses (checked per resolved IP and redirect, connected to the checked IP), mPDF never fetches URLs; Blade templates are restricted to an allowlist (quittio.pdf.templates, default invoices.*); the guard requires the unit and the seller's legal name; refunds of negative invoices are shown as positive amounts; Presentation::$explicit allows per-invoice resets.
  • Cancellations and unchanged InvoiceBuilder::from() keep the declared line and VAT amounts of read invoices within the EN 16931 rounding tolerance, so foreign invoices using other rounding are reversed exactly (QUITTIO-DECLARED-AMOUNTS warns when amounts had to be recalculated); an exchange rate implied from BT-111 is dropped once amounts change; amount discounts split across VAT rates are re-split after line changes; third-party payments (BG-DEX-09) are rejected where the syntax cannot carry them (QUITTIO-THIRD-PARTY-PAYMENTS).
  • Validation knows the XML syntax that is written, so syntax specific rules apply (e.g. BR-DEX-15 for XRechnung CII); UBL writes BT-82 and the credit note due date only once with several accounts (UBL-SR-45/46).
  • PDF: mPDF cannot fetch anything over HTTP (deny-all client), logo values are strictly classified (no backslashes, protocol-relative URLs or surrounding whitespace; data URIs strictly decoded and re-encoded; SVG entity declarations refused in every mode); IPv6 site-local addresses are refused for remote logos.
  • Declared amounts: only set when the recalculation does not reproduce them, line net amounts kept only within 0.02 of the unrounded formula (QUITTIO-LINE-ROUNDING), and only while a fingerprint of the amount inputs matches (also for read data edited before from()), so edits outside the builder never keep stale amounts; categories without VAT never keep a declared VAT amount; PEPPOL-EN16931-R120 uses the official tolerance of 0.02.
  • Country pack Germany: DE-XRECHNUNG-EXT-01 rejects credit notes of the XRechnung extension in UBL (no validator scenario; use the negative_invoice strategy).
  • pdf.paper and the VIES verification (validation.drivers.vies.enabled) take effect; file names of XML-only formats no longer contain a raw translation key.
  • Watermarks (draft and preview PDFs) were never printed: mPDF needs showWatermarkText, and PDF/A forbids their transparency. Watermarked PDFs without embedded XML are now rendered without PDF/A.