justinholtweb / craft-datevz
DATEV bookkeeping export for Craft Commerce — turn orders, payments and refunds into an importable DATEV-Format Buchungsstapel with German VAT, OSS and reverse-charge handled.
Package info
github.com/justinholtweb/craft-datevz
Type:craft-plugin
pkg:composer/justinholtweb/craft-datevz
Requires
- php: ^8.2
- ext-iconv: *
- ext-json: *
- craftcms/cms: ^5.3.0
- craftcms/commerce: ^5.0.0
README
DATEV bookkeeping export for Craft Commerce 5. Your orders come out as a Buchungsstapel your Steuerberater can import, with German VAT, OSS and reverse charge already worked out — and a preview that shows every posting before a file exists.
Datevz never guesses quietly. If an order cannot be booked — no account mapped for a product, a batch that straddles two fiscal years, postings that do not add up to the order total — it says so, names the order, and refuses to write the file. A DATEV import is not a good place to find that out.
Requirements
- Craft CMS 5.3+
- Craft Commerce 5.0+
- PHP 8.2+
Installation
composer require justinholtweb/craft-datevz php craft plugin/install datevz
Then open Settings → Plugins → Datevz and fill in the four things that come from the tax practice: the Beraternummer, the Mandantennummer, the Sachkontenlänge, and which chart of accounts (SKR03 or SKR04) the client is on. Everything else has a working default.
What it costs
$99, and $79 a year to keep receiving updates. One edition — everything below is in it. There is no feature you discover you needed at month-end and cannot have.
| Buchungsstapel export (DATEV-Format, v12/v13) | ✅ |
| SKR03 / SKR04 account presets | ✅ |
| Revenue split per tax rate, per account | ✅ |
| Reverse charge, export and One Stop Shop treatment | ✅ |
| Account mapping rules — by SKU, product type, category, shipping method | ✅ |
| Per-customer debtor accounts + Debitoren master data export | ✅ |
| Payment postings per gateway, with fees | ✅ |
| Refunds as credit notes | ✅ |
| Preview before exporting, with per-order problems | ✅ |
| Reconciliation against the order total | ✅ |
| Batch archive with checksums | ✅ |
| Log of every export, download and refusal | ✅ |
| Panel on Commerce's order screen | ✅ |
Console commands and craft.datevz.* Twig API |
✅ |
What a posting looks like
One order, €100 of goods at 19%:
119,00;"S";"EUR";;;;1400;8400;;1503;"RE-1042";"8556";;"Shop RE-1042";…
Gross against the debtor in Soll, revenue in Gegenkonto, Belegdatum as TTMM, invoice number in
Belegfeld 1. The revenue account is an Automatikkonto, so it carries its own tax rate and the
BU-Schlüssel column stays empty — which is what DATEV expects, and why the presets choose those
accounts.
The direction is never in the amount. Umsatz is always positive and unsigned; a credit is a
posting with H in the Soll/Haben column. A negative Umsatz is a rejected import, not a refund.
VAT treatment
Datevz decides how an order is treated from where it is going and who the customer is:
| Situation | Treatment | Books to (SKR03) |
|---|---|---|
| Ships within Germany | Domestic | 8400 / 8300 by rate |
| EU consumer, OSS off | Domestic | 8400 / 8300 |
| EU consumer, OSS on | Distance sale | the account you map for that country |
| EU business with a valid VAT ID for its own country | Reverse charge | 8125 |
| Outside the EU | Export | 8120 |
The VAT ID comes from the address's Organization Tax ID, which is where Craft 5 keeps it. It is checked for shape, not with VIES: a checkout must not depend on a foreign service being up, and by the time an order is being booked the validity of the number at the time of sale is a matter for the invoice.
What Datevz will not do is override the treatment because of how much tax Commerce charged. An export carrying 19%, or a domestic sale carrying none, is a problem in the shop's tax configuration — so it is reported, not papered over.
Accounts
The presets are a starting point, not advice. A chart of accounts belongs to the practice, and two shops on the same SKR can legitimately book to different accounts, so every number is editable and the preset buttons never overwrite one you have already set.
| SKR03 | SKR04 | |
|---|---|---|
| Revenue, standard rate | 8400 | 4400 |
| Revenue, reduced rate | 8300 | 4300 |
| Intra-community supply | 8125 | 4125 |
| Export | 8120 | 4120 |
| Discounts granted | 8700 | 4700 |
| Gateway fees | 4970 | 6855 |
| Money (Geldtransit) | 1360 | 1460 |
| Collective debtor | 1400 | 1200 |
Mapping rules beat all of it: match a SKU (BOOK-* works), a product type, a category or
a shipping method, and send it to an account of its own. First match wins.
Debtors
Two modes. Collective books every receivable to one account — right for a shop with thousands of one-off buyers. Per customer allocates a personal account from a range you choose, keyed to the customer and falling back to their email address, so somebody who ordered as a guest and later registered keeps one number. Those accounts can be exported as a Debitoren/Kreditoren master-data file for the practice to import.
Debtor accounts are always one digit longer than the Sachkonten. Datevz checks that before it writes anything; getting it wrong is the most common reason an otherwise fine batch is refused.
Payments and refunds
- Each successful transaction books the money in against the debtor, on the account mapped for its gateway (a Geldtransit account is the usual choice).
- A gateway fee is booked only when the gateway recorded one on the transaction. Nothing is estimated.
- A refund is two postings, not one: the money goes back, and — unless you turn it off — the revenue is reversed the way a credit note reverses it, in the same proportions and off the same accounts. Posting only the money movement leaves the debtor account permanently out of balance.
Exporting
Datevz → Export: pick a period, preview, then create the batch. The preview and the export run the same code, so what you read is what the file contains.
An order that has been exported is left out of the next batch. Delete a batch and its orders are released again — the archive is the record of what went, so deleting it is the only honest way to say "that never happened".
Every batch is stored whole, with a sha256 of the delivered file, so months later you can prove that the copy the practice holds is the copy Datevz produced.
From the command line
php craft datevz/export/run --from=2026-01-01 --to=2026-01-31 php craft datevz/export/run --from=2026-01-01 --to=2026-01-31 --dryRun php craft datevz/export/run --from=2026-01-01 --to=2026-01-31 --path=/tmp/january.csv php craft datevz/export/list php craft datevz/debtors/export php craft datevz/log/prune
A dry run exits non-zero when it finds errors, so a monthly cron job can fail loudly instead of mailing a broken batch.
Twig
{% set state = craft.datevz.orderState(order) %}
{% if craft.datevz.isExported(order) %}
Booked on {{ state.dateExported|date('d.m.Y') }} in batch {{ state.batchId }}
{% endif %}
{% set set = craft.datevz.postings(order) %}
{{ craft.datevz.treatmentLabel(set.treatment) }}
{% for posting in set.postings %}
{{ posting.account }} / {{ posting.contraAccount }} {{ posting.debitCredit }} {{ posting.amount }}
{% endfor %}
Nothing in the Twig API writes: rendering a template is not a place to allocate a debtor number or mark an order exported.
What Datevz checks before it writes
- The practice numbers are present, and the accounts are the right length.
- Every bucket of revenue has an account and, where it needs one, a BU-Schlüssel.
- The postings add up to the order total. A cent or two of inclusive-tax rounding can go to a rounding account if you configure one; anything more is an error.
- The batch stays inside one fiscal year.
Belegdatumcarries only a day and a month — the year comes from the header — so a batch that straddles a year-end books half its postings into the wrong one, silently, with every row individually valid. - No two orders share a document number.
- Every order is in the currency the batch declares.
- Every row is exactly as wide as the format version in the header.
Encoding
Files are written Windows-1252, which is what DATEV's own exports use. If your customers have names that do not survive that trip, switch to UTF-8 with BOM, which current DATEV releases accept. The stored document is always UTF-8; the conversion happens when the file is delivered.
A note on the format
The column layout lives in one place — src/format/Fields.php — as an ordered list, because
everything about a DATEV file is positional. Datevz writes Buchungsstapel format version 13
(125 columns) by default and can write version 12 (121 columns). If a practice's DATEV expects a
different release, switch the version in the settings; if a column has moved, that array is the
only thing to change.
License
Proprietary. See LICENSE.md.