E-invoices
An e-invoice is the invoice as a structured XML file that accounting software imports by itself, instead of a PDF somebody types in again. KahunaCart writes EN 16931, the European e-invoice standard, in UBL 2.1, laid out the way Peppol BIS Billing 3.0 does it. That's what most European accounting packages, Peppol access points and public portals read. This page is for store owners who sell to businesses that ask for one, and for developers who want to change the file or add another format.
E-invoices are off as shipped, so a store that doesn't need them never sees them. They need KahunaCart 1.6.14 or later.
Switch them on
Go to KahunaCart → Settings → Accounts and turn on E-invoices in the E-invoices section, under the invoice settings. Then fill in what an e-invoice needs that a paper invoice doesn't:
- Your address in parts. Street, city and postcode, because an e-invoice can't read the free-text address block printed on the PDF.
- Country. The two-letter code of the country you're registered in, such as
DE. Left empty, it uses the checkout's default country. - E-invoice email. Where customers send e-invoices to you. Left empty, it uses your support email, then the address store emails come from.
Your company name and VAT number come from the invoice settings you already have, so the e-invoice and the PDF always name the same seller. The rest is optional: a Peppol ID (left empty, your VAT number is used under your country's Peppol scheme, which is right for most stores), your company registration number, an IBAN and BIC printed as the way to pay an unpaid invoice, the default unit lines are counted in (C62, meaning "one"), and the days to pay.
Once it's on:
- Each order in the admin has an E-invoice button beside Download.
- Each credit note has an XML button beside its PDF, which downloads a UBL credit note that refers back to the original invoice.
- The order list's bulk bar adds XML (ZIP), with one file per selected order.
- An order that can't be written as an e-invoice shows No e-invoice where the button would be, with the reason in its tooltip. The reasons are listed below.
The amounts come from the same rows the invoice PDF prints, so the two always agree. Each line, discount, shipping charge and fee goes in the VAT category of the tax that was actually charged on it, line amounts are net of VAT even on a store whose prices include tax, and gift cards, store credit and deposits count as already paid. Nothing is recalculated.
Let customers download it
Customers Can Download (einvoice.customer_link, off as shipped) adds a Download e-invoice link to the customer's order page and printable invoice, beside Download PDF, so a business buyer can import the invoice into their own accounting software without asking you for it. It only appears on an order that can be written as an e-invoice. With more than one format, there's a link for each, named by format.
The link is signed the same way as the invoice link, and every theme gets it without an update. The account's order list doesn't carry it, because deciding whether each order can be written means building each one's document. It's one click away on each order's page.
VAT categories
Every line on an e-invoice carries one of the EN 16931 VAT categories, and accounting software uses it to book the VAT. While e-invoices are on, the tax rate editor shows E-invoice category. Most rates can stay on Automatic:
| When | Category |
|---|---|
| The rate charged tax | Standard rated (S) |
| No tax, on an order sold without tax to a buyer with a VAT number | Reverse charge (AE) |
| No tax, to a buyer outside your VAT area with no VAT number | Export (G) |
| No tax, and the store has no VAT number and charged no tax at all | Not subject to VAT (O) |
| No tax, on an order sold without tax for another reason | Exempt (E), with your reverse-charge wording as the reason |
| No tax otherwise, such as a 0% rate | Zero rated (Z) |
Pin a category by hand for a rate that needs a fixed one, such as books exempt under a national rule, and type that rule in Reason no VAT was charged. The category is saved with each order's tax when the order is placed, so changing it later doesn't rewrite old orders.
A rate that charges tax can only be Standard rated, and a rate marked Standard rated that charged nothing is refused. That way a mistake shows up as a reason in the admin, not as a failed import at your accountant's.
See Tax for setting up the rates themselves.
What can't be exported
Some orders can't be described honestly in EN 16931. Rather than writing a file that won't import or books the wrong VAT, KahunaCart refuses them with one sentence saying why:
- An order that hasn't completed yet.
- An order sold through a merchant of record, such as Polar. The provider is the seller there, so its tax invoice is the one that counts.
- A line charged more than one tax, such as Canadian GST plus QST, or a US state and county rate. EN 16931 allows one VAT rate per line.
- A tax rate with a price threshold, which splits a line by its price.
- An order with VAT on it from a store with no VAT number in the invoice settings.
- A partial refund of an order with more than one VAT rate, because the credit note's tax can't be split between the rates exactly. A refund of a single-rate order, or of a whole order, is fine.
- An order whose VAT doesn't work out from its own amounts. Every document is checked against the EN 16931 sums before it's written.
- A currency with three decimals, such as the Kuwaiti dinar. EN 16931 amounts have two at most.
Selling through more than one company
When each currency sells through its own company (see Selling through more than one company), a company's entry can carry its own einvoice settings, which replace the store's for orders in that currency:
currencies:
sellers:
EUR:
einvoice:
street: 'Narva mnt 5'
city: 'Tallinn'
postcode: '10117'
country: EE
email: '[email protected]'
Change the template
The UBL file is drawn by a Twig template, kahunacart-einvoice-ubl.xml.twig. To change it, copy it from the plugin's templates/ folder into your theme's templates/ folder and edit it there. It's handed doc, the document with every amount already worked out and formatted in the currency's decimals, so the template only places values and never calculates anything.
Two things to keep:
- The
<?xml ?>declaration must be the very first thing it writes. A Twig comment above it is fine, since Twig drops it, but a blank line or an XML comment above it makes the file invalid. The output is checked as XML before it's handed out, so an invalid template means every download answers with an error rather than a broken file. - Escape what you print. The shipped template escapes everything for XML whatever the site's autoescape setting. Keep that in your copy.
The override is used everywhere the file is written: the customer's download, the admin's buttons, the bulk ZIP and the API. Because it lives in the theme, switching themes or updating one that you edited in place drops it. To keep a template whatever the theme, use a child theme, or put it in a small site plugin that adds its own templates/ folder in onTwigTemplatePaths.
Add your own format
A format that is one template needs no plugin. Put the template in your theme and name it in user/config/plugins/kahunacart.yaml:
einvoice:
formats:
- id: ee
label: 'Estonian e-arve'
template: 'kahunacart-einvoice-ee.xml.twig'
It then appears beside UBL in the admin, on the customer link, in the API and on the download route. The template gets the same doc as the UBL one, so the UBL template is a good starting point to copy. Each entry can also set extension (default xml), mime (default application/xml) and credit_notes (default true; set it to false for a format that has no credit notes).
For developers
An add-on format. A format that needs more than a template, such as Factur-X (a PDF/A with the XML inside it), XRechnung, or sending through Peppol or a government portal, comes from an add-on. It listens to onKahunaCartInvoiceFormats and calls add() on the event's formats with an object implementing Invoice\EInvoice\EInvoiceFormat:
| Method | Answers |
|---|---|
id() |
A short id for the URL: lowercase letters, digits, - and _ |
label() |
What the admin calls it |
extension(), mimeType() |
The file's extension and Content-Type |
supports(EInvoice $document) |
Null when the document can be written, otherwise one plain sentence saying why not |
render(EInvoice $document) |
The file's contents |
The first format registered under an id keeps it, so core's ubl can't be replaced by accident. EInvoice carries type (invoice or credit_note), data (what the UBL template prints) and facts (the order, its lines and adjustments, the seller, the buyer and what was paid). A document core has already refused never reaches supports().
The download route. The storefront serves the file at {base}/order/{hash}/einvoice/{format}, behind a signed link of its own that lasts as long as the invoice link (security.invoice_link_days), or to the order's signed-in owner or an admin. The links reach the order page and invoice templates as einvoice_links, a list of id, label and url, so a theme that draws its own order page can place them.
The API and MCP. get_order carries einvoices: each format with its label, whether this order can be exported in it, the reason when it can't, and the signed url when it can. list_credit_notes carries the same for each note. GET /kahunacart/orders/{id}/einvoice/{format} (get_order_einvoice) and GET /kahunacart/orders/{id}/credit-notes/{cnid}/einvoice/{format} (get_credit_note_einvoice) answer the file itself as JSON, with filename, mime_type and content. A tax rate's category is einvoice_category and einvoice_exemption on the tax zone API, and a product's own unit is einvoice_unit on the products API and as an import column, such as H87 for a piece or HUR for an hour.
Checking a file. The plugin's tests check every document against the OASIS UBL 2.1 schema and the EN 16931 sums, and its sample files pass the official EN 16931 and Peppol schematrons. An invoice to a consumer, addressed by email, is valid EN 16931 but gets one known Peppol finding, because Peppol has no address for a consumer: such a file is for accounting software, not for sending over Peppol.