Skip to main content

Invoices and e-invoicing

Every invoice is persisted as an immutable InvoiceRecord — its number, its buyer snapshot (name, address, VAT id, frozen at finalization as §14 UStG requires) and its line and tax breakdown. That stored row is what the e-invoicing renderers read; nothing needs to be populated by hand.

Which side issues it depends on the driver, and the stored row is the same either way. Where the provider finalizes invoices, a webhook effect persists what it finalized. Where this package runs the cycle it issues the document itself, from the order it just priced. The renderers, the numbering and the buyer snapshot read one shape, so an install that changes driver keeps one archive rather than two.

When money goes back, the correction is persisted the same way and linked to the invoice it corrects — a provider credit note (credit_note.created) on one path, a document this package issues on the other. DATEV books it as a Haben (credit), and the writer renders it as an EN 16931 correcting document referencing the original invoice; amounts stay positive, the document type carries the correcting meaning. Which type depends on what the correction claims: a full cancellation of the original is 381 (EN 16931 names it "Credit note"), a partial correction that leaves the original standing is 384 ("Corrected invoice"). A correction carries that choice from the moment it is built and keeps it through to the rendered document — it is not derived from the amounts, because a correction for the full sum can still be an amendment. See Accounting and DATEV for the booking side.

Render a stored invoice as an EN 16931 document, or export a batch to DATEV:

// XRechnung — the standalone UBL syntax (the EInvoice default), for B2G / Leitweg-ID:
$ubl = app(Pushery\Billing\Contracts\EInvoice::class)->render($invoice);

// ZUGFeRD / Factur-X — the UN/CEFACT CII syntax a hybrid PDF/A-3 embeds:
$cii = app(Pushery\Billing\Invoicing\ZugferdCiiInvoice::class)->render($invoice);

$csv = app(Pushery\Billing\Invoicing\DatevExport::class)
->export($invoices, $from, $to);

Both syntaxes are built from one normalized invoice model, so a standard sale, an intra-EU reverse charge, a correction (type 381 or 384) or a multi-rate split can never drift between them. For the reverse-charge rules that govern when a zero rate is applied, see Taxes.

An amount collected on behalf of another party is stated, not taxed. A line whose tax_rate is null is an amount the issuer collected in the name and on behalf of another party, such as the goods on the document of an intermediated sale beside the platform's fee. Both syntaxes state it in category E with the exemption code VATEX-EU-79-C (Article 79(c) of the VAT Directive) and the reason "Collected in the name and on behalf of another party", in a band of its own. The page prints a dash where a rate would stand and states the amount apart, between the figures the rate and the tax are stated over and the total, so a short receipt states its rate over the issuer's supply alone. EN 16931 has no category of its own for such an amount, and O admits no other category beside it (BR-O-11) while the issuer's own supply is taxed. On a document that is exempt, margin-taxed or outside the scope of the tax as a whole, the amount joins that document's one band, and the page states nothing apart. A line that names no rate at all is read as 0 %.

A paid invoice, or a credit note whose money went back, states its whole amount as already paid, so nothing is due on it. Any other document states its full amount as due, and when: the due_at it records, or the day it was issued where it records none.

Set your company details under config('billing.company') and your DATEV account numbers under config('billing.datev').

XRechnung asks for two things beyond the address, on every document. A seller contact with a contact point, a telephone number and an email address (contact_name, contact_phone, contact_email), and payment instructions, which the package writes as a SEPA credit transfer to the account in iban, with the bank in bic. A document without them is refused by the KoSIT validator, so a recipient that validates, as German public bodies do, rejects it. The package writes neither without your values: a missing account leaves the payment instructions out rather than stating an account nobody gave. Each document keeps the values it was issued with, so changing them later changes only the documents issued after.

// config/billing.php
'company' => [
// name, address, vat_id, ... as before
'contact_name' => 'Buchhaltung',
'contact_phone' => '+49 30 1234567',
'contact_email' => '[email protected]',
'iban' => 'DE02120300000000202051',
'bic' => 'BYLADEM1001',
],

The hybrid ZUGFeRD / Factur-X PDF/A-3​

To ship the hybrid ZUGFeRD / Factur-X document — the CII XML embedded in a human-readable PDF/A-3 — Invoicing\ZugferdPdfInvoice::embed() merges the XML into your own rendered invoice PDF and returns the PDF/A-3 bytes:

// $yourInvoicePdf is your app's own rendered invoice (binary content) — the human-readable page the
// structured XML is embedded into. Pass your own document, never untrusted input.
$pdf = app(Pushery\Billing\Invoicing\ZugferdPdfInvoice::class)
->embed($invoice, $yourInvoicePdf);

A conformant PDF/A-3 needs a real PDF toolchain the lean core does not carry, so this one helper is opt-in: composer require horstoeko/zugferd (a suggest dependency). Without it embed() throws a clear MissingPdfEmbedder instead of fataling — and you can still ship the CII or XRechnung XML on its own, which needs no PDF library.


← Back to the documentation index