Skip to main content

RFC: Revenue Recognition by Delivery Date (Erlösrealisation nach Versanddatum)

Status: Draft Author: CONA Engineering Created: 2026-04-02 Updated: 2026-04-08 Strategy: B — DATEV-gestützte Anzahlungsautomatik Related: HGB § 252 Abs. 1 Nr. 4 (Realisationsprinzip), § 13 Abs. 1 Nr. 1a UStG, GoBD

1. Motivation

A customer wants to recognize revenue not at order/invoice time, but at shipment date (Versanddatum / Fulfillment date). This is the correct approach under HGB’s Realisationsprinzip, which requires revenue recognition at the point of Gefahrenübergang (transfer of risk). For B2C e-commerce (Verbrauchsgüterkauf), § 475 Abs. 2 BGB places the Gefahrenübergang at delivery to the consumer. We use the fulfillment date as a pragmatic proxy accepted by most Steuerberater.

Current Model

Target Model

Design Decision: Strategie B (DATEV-Automatik)

CONA is not the leading accounting system — it delivers booking entries to DATEV, which is the system of record. Therefore:
  • CONA’s internal GL keeps its current structure (Bank ↔ Sammeldebitor ↔ Erlöse)
  • CONA does NOT book to “Erhaltene Anzahlungen” (1718) or generate Auflösungsbuchungen
  • The DATEV export enriches bookings with Anzahlungsfelder (Buchungstyp, Auftragsnummer, USt-Schlüssel (Anzahlungen), Erlöskonto (Anzahlungen))
  • DATEV Kanzlei-Rechnungswesen uses its built-in Anzahlungsfunktion to handle USt-Korrektur, 1718-Umbuchung, and Auflösung automatically
Prerequisite: Der Steuerberater richtet einmalig die Anzahlungsfunktion in DATEV ein (Stapeleigenschaften → Anzahlungen → Konten und Buchungsfall konfigurieren).
Critical: When payment is received before delivery, VAT is already due at payment time (§ 13 Abs. 1 Nr. 1a S. 4 UStG). DATEV handles this automatically via the Anzahlungsfelder — CONA does not need to book separate USt entries.

3. Vorher / Nachher — EXTF-Export am konkreten Beispiel

Szenario: Bestellung #1042, bezahlt am 28.03.2026, versendet am 02.04.2026. Brutto 1.190,00 € (netto 1.000 €, 19% USt). Shopify-Transaktions-ID: 98765.

3.1 VORHER — Feature AUS (Umsatzrealisierung nach Bestelldatum)

Beide Buchungssätze landen im März-Stapel mit Belegdatum 28.03.: Buchung 1 — Zahlung (März-Stapel):
Buchung 2 — Erlös (März-Stapel):
Ergebnis in DATEV:
  • März-UStVA: 190 € USt (aus Erlösbuchung)
  • Sammeldebitor: OPOS ausgeziffert über Belegfeld 1
  • Erlös: im März verbucht

3.2 NACHHER — Feature AN (Umsatzrealisierung nach Versanddatum)

Die Zahlung bleibt im März-Stapel, der Erlös wandert in den April-Stapel. Zusätzlich werden die Anzahlungsfelder und das Leistungsdatum gesetzt: Buchung 1 — Zahlung (März-Stapel):
Buchung 2 — Erlös (April-Stapel):

3.3 Was DATEV daraus macht

Zusammengefasst: An den Buchungssätzen in CONA ändert sich fast nichts — gleiche Konten, gleiche Beträge, gleiche GID. Nur das Datum der Erlösbuchung verschiebt sich, und es kommen 4 zusätzliche DATEV-Felder hinzu (Buchungstyp, USt-Schlüssel Anz., Erlöskonto Anz., Leistungsdatum + Datum Zuord. Steuerperiode), damit DATEV die USt periodengerecht zuordnen kann.

3.4 Wann werden die Anzahlungsfelder NICHT gesetzt?


4. Booking Model in CONA

4.1 Principle: Minimal Changes

CONA’s internal GL structure stays identical to today. The only change is the timing of when the revenue entry is created:

4.2 CONA GL Entries

Entry #1 — Payment Received (Day 1, e.g. March 28)

Entry #2 — Revenue at Fulfillment (Day 2, e.g. April 2)

Reconciliation on Sammeldebitor (1400)

4.3 Same-Day Scenario (Payment + Fulfillment am selben Tag)

Wenn Zahlung und Versand am selben Tag, ist es exakt wie das heutige Modell. Kein Buchungstyp AG/SR nötig im DATEV-Export — nur Leistungsdatum wird zusätzlich gesetzt.

4.4 Was sich in CONA ändert / nicht ändert


5. DATEV Export — Die Anzahlungsfelder

5.1 Relevante DATEV-Felder

Die DATEV EXTF-Spezifikation enthält einen Anzahlungsblock (Felder #95-#101), den wir bisher nicht befüllen:

5.2 Buchungstyp-Werte

5.3 DATEV-Export je Buchung

Payment-Buchung (Buchungstyp “AG”)

Wird gesetzt wenn paymentDate < fulfillmentDate:
Was DATEV damit macht: Beim Import erkennt DATEV den Buchungstyp “AG” und speichert den Vorgang als offene Anzahlung. Die USt auf die Anzahlung wird automatisch in die korrekte UStVA-Periode gebucht (§ 13 Abs. 1 Nr. 1a S. 4 UStG).

Revenue-Buchung (Buchungstyp “SR”)

Was DATEV damit macht: Beim Import erkennt DATEV anhand der gleichen Auftragsnummer die Zugehörigkeit zur Anzahlung “AG”. DATEV erzeugt automatisch die Auflösungsbuchung (Erh. Anz. an Sammeldebitor), korrigiert die Anzahlungs-USt und bucht die endgültige USt der Schlussrechnung.

5.4 Herkunft von USt-Schlüssel und Erlöskonto auf der AG-Buchung

Problem: Die Payment-Buchung (Bank → Sammeldebitor) hat keinen BU-Schlüssel und kein Erlöskonto — das sind Eigenschaften der noch nicht existierenden Revenue-Buchung. DATEV braucht die Werte aber beim Import der AG-Buchung. Lösung: Durch die BU-Schlüssel-Vereinfachung (s. Section 6) ist der BU direkt als DB-Spalte auf dem GL-Entry verfügbar:
Warum das funktioniert:
  • Der DATEV-Export wird typischerweise nach Periodenabschluss ausgelöst (z.B. März-Export Anfang April)
  • Zu diesem Zeitpunkt existiert die Revenue-Buchung (02.04.) bereits in CONAs GL
  • sr_entry.bu_schluessel enthält den korrekten BU direkt als Spalte — kein JSON-Parsing, kein Dimension-Mapping
  • Fallback: deriveBuSchluessel() (gleiche Funktion wie bei Journal-Entry-Erstellung, s. 6.4)
Edge Case — Fulfillment steht noch aus: Wenn der Nutzer den März-Export vor dem Fulfillment macht, gibt es noch keine SR-Buchung. Dann:
  1. CONA ruft deriveBuSchluessel() mit dem Order-Steuersatz auf (Shopify liefert taxRate bei Bestellung)
  2. Erlöskonto wird analog aus der BU-Config aufgelöst
  3. Werte sind in 99% identisch zur späteren SR (gleiche Ware, gleicher Steuersatz)
SKR03 — Standard-Erlöskonten und BU-Schlüssel (Referenz):
Hinweis zu Automatikkonten: 8400 und 8300 sind Automatikkonten (AM) in DATEV. Wenn das Gegenkonto ein Automatikkonto ist, berechnet DATEV die USt automatisch. Der BU-Schlüssel 9 bzw. 14 dient als zusätzliche Absicherung.

5.5 Automatische DATEV-Buchungen (transparent für CONA)

DATEV erzeugt intern bei Import der SR-Buchung mit passender Auftragsnummer:

5.6 DATEV-Einrichtung durch Steuerberater (einmalig)

In DATEV Kanzlei-Rechnungswesen → Buchungsstapel → Eigenschaften → Anzahlungen:
  1. Anzahlungskonto festlegen: z.B. 1718 (Erhaltene Anzahlungen)
  2. Buchungsfall einstellen: “Erhaltene Anzahlungen ohne Abschlagsrechnung”
  3. Buchungstext für Auflösung konfigurieren
Diese Einrichtung erfolgt einmalig pro Mandant und muss nicht wiederholt werden.

5.7 Hinweis zu Feld #115 Leistungsdatum

Aus der DATEV-Spezifikation:
“Beim Import des Leistungsdatums muss das Feld Datum Zuord. Steuerperiode (#116) gefüllt sein. Der Einsatz des Leistungsdatums muss in Absprache mit dem Steuerberater erfolgen.”
Beide Felder (#115 + #116) werden daher immer zusammen gesetzt. Datum Zuord. Steuerperiode steuert, in welche UStVA-Periode die Buchung fällt.

6. BU-Schlüssel — Vereinfachung des Steuerschlüssel-Systems

6.1 Motivation

Aktuell wird der BU-Schlüssel über eine vollständige Posting-Matrix (Tax Code (BU) Matrix) mit Rules bestimmt, als gl_dimensions-Wert im dimensions-JSON des GL-Eintrags gespeichert, und beim DATEV-Export über ein Dimension-Mapping aufgelöst. Diese Architektur ist für den BU-Schlüssel overengineered:
  • Der BU-Schlüssel ist deterministisch aus Steuersatz + Lieferland ableitbar
  • 95%+ der Nutzer haben identische BU-Rules (19% → 9, 7% → 14, ig. Lieferung → 3)
  • Die Posting-Matrix-Konfiguration für BU ist unnötig komplex
  • Für die AG-Buchungen im Revenue-Recognition-Feature brauchen wir den BU direkt als DB-Spalte, nicht versteckt im JSON

6.2 Architektur-Entscheidung

6.3 Neue DB-Spalte

Die dimensions-JSON-Spalte bleibt bestehen für KOST1, KOST2 und Kost-Menge — nur der BU-Schlüssel wird herausgezogen.

6.4 Automatische BU-Ableitung

Neue Funktion deriveBuSchluessel() in @cona/core:
Die Funktion nutzt die bestehende isEUCountry()-Logik aus @cona/utils.

6.5 BU-Konfiguration in DATEV-Settings

Statt Posting-Matrix-Rules: einfache Mapping-Tabelle in den DATEV Export Settings:
Der Nutzer kann die BU-Werte in den DATEV-Settings anpassen — aber die meisten werden die Defaults behalten.

6.6 Auswirkungen auf bestehende Systeme

6.7 Vorteil für Revenue Recognition

Mit bu_schluessel als eigene Spalte wird die AG/SR-Logik im DATEV-Export deutlich einfacher:
  • USt-Schlüssel (Anzahlungen) auf der AG-Buchung = sr_entry.bu_schluessel (direkt lesbar)
  • Fallback wenn noch kein SR existiert = deriveBuSchluessel(order.taxRate, ...) (gleiche Funktion)
  • Kein Dimension-Mapping-Lookup mehr nötig, keine UUID-Auflösung, kein JSON-Parsing

7. GID / Auszifferung

7.1 Keine Änderung an der Reconciliation-Logik

Die bestehende GID-basierte Auszifferung auf dem Sammeldebitor bleibt identisch:

7.2 GID-Quelle (wie bisher)

Der GID wird aus der Shopify-Transaktion abgeleitet — verfügbar bei Payment UND bei Fulfillment.

7.3 Split Payments

Für Bestellungen mit mehreren Zahlungsmethoden erzeugt die bestehende Multi-GID-Allocation proportionale GL-Einträge. Das funktioniert unverändert:

8. Edge Cases

8.1 Same-Day (Zahlung + Versand am selben Tag)

Kein Buchungstyp AG/SR nötig. Standard-Buchung wie bisher, nur Leistungsdatum wird gesetzt. Optional können wir trotzdem die Auftragsnummer setzen — schadet nicht, löst aber keine Anzahlungsautomatik aus wenn kein AG vorhanden ist.

8.2 Partial Fulfillment (Teillieferung)

Shopify unterstützt mehrere Fulfillments pro Order. Jedes Fulfillment enthält eine Teilmenge der Line Items.
Im DATEV-Export erhalten beide SR-Buchungen die gleiche Auftragsnummer. DATEV löst die Anzahlung bei der ersten Schlussrechnung anteilig auf und den Rest bei der zweiten. Auszifferung in CONA: Auf dem Sammeldebitor stehen Payment (-80 €) und zwei Revenue-Buchungen (+50 €, +30 €). Der Reconciliation Engine erkennt über den autoDifferenceThreshold die Teilzahlungen.

8.3 Cancelled Order (Stornierung ohne Versand)

Im DATEV-Export: Die Payment-Buchung hatte Buchungstyp “AG”. Die Refund-Buchung bekommt keinen speziellen Buchungstyp — DATEV erkennt anhand der Auftragsnummer, dass die Anzahlung zurückgezahlt wurde.

8.4 Payment After Fulfillment (Kauf auf Rechnung)

Zahlung kommt NACH Fulfillment. Kein Anzahlungssachverhalt:
Die Erkennung: Wenn zum Zeitpunkt des Revenue-Booking bereits Zahlung vorhanden ist → AG/SR. Wenn Fulfillment vor Zahlung → kein Buchungstyp nötig.

8.5 Fulfillment in geschlossener Buchungsperiode

  1. Warnung loggen: “Revenue für Fulfillment vom 31.03. wird in April gebucht (März geschlossen)”
  2. CONA bucht in die nächste offene Periode
  3. Leistungsdatum wird trotzdem auf das tatsächliche Fulfillment-Datum gesetzt (31.03.)
  4. Datum Zuord. Steuerperiode = tatsächliches Fulfillment-Datum → DATEV ordnet die USt korrekt zu

8.6 Refund After Revenue Recognition

Standard-Refund-Handling. Die Gutschrift referenziert die Original-Rechnung:
GID der Refund-Transaktion verknüpft die Einträge für die Auszifferung.

9. Implementation Plan

Each phase is independently shippable and delivers value on its own. Later phases build on earlier ones but are not blocked by them unless noted.

Phase 1: BU-Schlüssel Foundation (mittel, s. Section 6)

Refactor the BU-Schlüssel from a dimension-JSON value into a first-class DB column. No user-facing behaviour change — purely internal simplification that unblocks everything else. Data Model:
  • Add bu_schluessel String? column to general_ledger table
  • Migration: Backfill existing entries from dimensions-JSON → bu_schluessel column
  • Migration: Remove BU key from dimensions-JSON (cleanup)
Core Logic:
  • Create packages/core/src/general_ledger/derive-bu-schluessel.ts — zentrale Ableitungsfunktion
  • createJournalEntry(): BU automatisch setzen via deriveBuSchluessel(taxRate, country, buConfig)
  • process-dimensions.ts: BU-Dimension aus Rule-Verarbeitung ausnehmen
  • consolidate-entries.ts: generateDimensionKey() um bu_schluessel-Spalte erweitern (statt aus dimensions-JSON)
Settings & Types:
  • Add buSchluesselConfig to datevExportSettingsSchema in @cona/types
  • Remove "BU" from datevDimensionType enum (KOST1/KOST2/Kost-Menge bleiben)
  • Default buSchluesselConfig in posting-matrix-seed.ts setzen statt BU-Dimension + Matrix
DATEV Export:
  • entry-transformer.ts (Temporal + Webapp): entry.bu_schluessel direkt lesen
  • consolidated-transformer.ts (Temporal + Webapp): gleiche Vereinfachung
UI:
  • DATEV-Settings-Dialog: “BU-Schlüssel” Tab mit einfacher Mapping-Tabelle
  • "BU" aus Dimension-Mapping-Optionen entfernen
  • Posting-Matrix: “Tax Code (BU)” Matrix nicht mehr für neue Gruppen erstellen

Phase 2: Leistungsdatum — always set service date (klein)

Set Leistungsdatum (#115) and Datum Zuord. Steuerperiode (#116) on every revenue booking in the DATEV export. This is legally required (UStDV § 31 Abs. 4) and valuable independent of any revenue-timing change. No toggle needed — this is unconditionally correct. Data Model:
  • Add service_date field to general_ledger (nullable DateTime)
  • Add fulfillment_date field to documents table (nullable DateTime)
Shopify Adapter:
  • Extend Shopify Orders GraphQL query to include fulfillment data (fulfillments.createdAt)
  • Set service_date on revenue GL entries: use fulfillment.createdAt when available, fall back to order.processedAt
  • Set fulfillment_date on the document when a fulfillment exists
DATEV Export:
  • Leistungsdatum (#115): emit entry.service_date in TTMMJJJJ format when present
  • Datum Zuord. Steuerperiode (#116): always set together with Leistungsdatum
UI:
  • Show Leistungsdatum in the GL detail view
Shippable independently: Yes. Revenue is still booked at order date — only the DATEV export gains the Leistungsdatum fields. Existing bookings without service_date export exactly as before (fields left empty).

Phase 3: Revenue Recognition by Fulfillment Date (mittel)

Shift revenue booking from order date to fulfillment date. Opt-in per Shopify integration. Depends on: Phase 2 (service_date column + fulfillment data from Shopify) Settings:
  • Add revenue_recognition_mode to Shopify integration settings ("order_date" | "fulfillment_date")
  • Toggle in Shopify-Integration-Settings UI: “Umsatzrealisierung: Bestelldatum / Versanddatum”
Shopify Adapter:
  • When revenue_recognition_mode = "fulfillment_date":
    • Payment booking: weiterhin bei Zahlungseingang (wie bisher)
    • Sales Invoice: erst bei Fulfillment erzeugen, mit date = fulfillment.createdAt
    • service_date = fulfillment.createdAt (already wired from Phase 2)
  • Handle partial fulfillments: proportionale Revenue-Buchungen je Fulfillment
Shippable independently: Yes. Revenue moves to the correct period. DATEV export already emits Leistungsdatum from Phase 2. The AG/SR Anzahlungsfelder are not yet set — the Steuerberater can still process the export, just without the automatic Anzahlungsauflösung in DATEV.

Phase 4: DATEV Anzahlungsfelder — AG/SR for cross-month bookings (klein)

Enrich the DATEV export with Buchungstyp AG/SR and Anzahlungsfelder so DATEV can automatically handle prepayment reclassification and VAT correction across periods. Depends on: Phase 1 (bu_schluessel column) + Phase 3 (revenue timing shift) DATEV Export — Änderungen in entry-transformer.ts:
  • Auftragsnummer (#95) mit Shopify Order-Nr. befüllen
  • Buchungstyp (#96) setzen — nur wenn Zahlung vor Fulfillment UND unterschiedliche Kalendermonate:
    • "AG" für Payment-Buchungen (Erhaltene Anzahlung)
    • "SR" für Revenue-Buchungen (Schlussrechnung)
  • USt-Schlüssel (Anzahlungen) (#97) auf AG-Buchungen:
    • Primär: sr_entry.bu_schluessel (direkt aus DB-Spalte, dank Phase 1)
    • Fallback: deriveBuSchluessel(order.taxRate, country, buConfig)
  • Erlöskonto (Anzahlungen) (#101) auf AG-Buchungen:
    • Primär: sr_entry.credit_account.account_nr (Gegenkonto der SR)
    • Fallback: Erlöskonto aus buConfig.taxRateMappings
UI:
  • Hinweis-Banner: “Steuerberater muss Anzahlungsfunktion in DATEV einmalig konfigurieren”
Shippable independently: Yes, but only meaningful when Phase 3 is active (otherwise payment and revenue are always on the same date → no cross-month scenario). Can be shipped together with Phase 3 or as a fast follow-up.

10. Was sich NICHT ändert (Abgrenzung)


11. Risks & Mitigations


12. Resolved Questions

  1. Fulfillment-Datum: fulfillment.createdAt (Versanddatum) wird verwendet. Nicht deliveryStatus === "delivered", da die meisten Shops kein Carrier-Tracking haben.
  2. Multi-Currency: Es wird der FX-Kurs vom jeweiligen Buchungstag genommen. Für die Payment-Buchung der Kurs vom Zahlungstag, für die Revenue-Buchung der Kurs vom Fulfillment-Tag. Daraus entstehende Währungsdifferenzen werden wie gewohnt ausgebucht.
  3. Accounting Period Auto-Creation: Ja, Buchungsperioden werden automatisch erstellt wie bei allen anderen Vorgängen auch.
  4. Consolidated Entries + AG/SR: Bereits gelöst durch die bestehende Konsolidierungslogik. In consolidate-entries.ts werden Reconciliation Groups, die mehrere Perioden umspannen, automatisch als “problematic” markiert und von der Konsolidierung ausgeschlossen. Da bei monatsübergreifenden Vorgängen Payment (März) und Revenue (April) über den gleichen GID verknüpft sind, erkennt die Engine die periodenübergreifende Recon Group und exportiert die Einträge einzeln — genau das Verhalten, das wir für AG/SR brauchen. Keine Code-Änderung nötig.
  5. DATEV Anzahlungs-Verhalten bei Konsolidierung: Siehe #4 — monatsübergreifende Buchungen werden bereits nicht konsolidiert. Bei Same-Month-Vorgängen (kein AG/SR nötig) können Buchungen weiterhin konsolidiert werden, da dort kein Buchungstyp gesetzt wird.