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.

Maintainers

Package info

github.com/justinholtweb/craft-datevz

Type:craft-plugin

pkg:composer/justinholtweb/craft-datevz

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

5.0.0 2026-08-24 17:00 UTC

This package is auto-updated.

Last update: 2026-08-24 18:50:44 UTC


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. Belegdatum carries 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.