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, GoBD1. 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
2. Legal Basis
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):- 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):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 — nurLeistungsdatum 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 wennpaymentDate < 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:- 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_schluesselenthä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)
- CONA ruft
deriveBuSchluessel()mit dem Order-Steuersatz auf (Shopify liefert taxRate bei Bestellung) - Erlöskonto wird analog aus der BU-Config aufgelöst
- Werte sind in 99% identisch zur späteren SR (gleiche Ware, gleicher Steuersatz)
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:- Anzahlungskonto festlegen: z.B. 1718 (Erhaltene Anzahlungen)
- Buchungsfall einstellen: “Erhaltene Anzahlungen ohne Abschlagsrechnung”
- Buchungstext für Auflösung konfigurieren
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
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 FunktionderiveBuSchluessel() in @cona/core:
isEUCountry()-Logik aus @cona/utils.
6.5 BU-Konfiguration in DATEV-Settings
Statt Posting-Matrix-Rules: einfache Mapping-Tabelle in den DATEV Export Settings:6.6 Auswirkungen auf bestehende Systeme
6.7 Vorteil für Revenue Recognition
Mitbu_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)
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, nurLeistungsdatum 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.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)
8.4 Payment After Fulfillment (Kauf auf Rechnung)
Zahlung kommt NACH Fulfillment. Kein Anzahlungssachverhalt:8.5 Fulfillment in geschlossener Buchungsperiode
- Warnung loggen: “Revenue für Fulfillment vom 31.03. wird in April gebucht (März geschlossen)”
- CONA bucht in die nächste offene Periode
Leistungsdatumwird trotzdem auf das tatsächliche Fulfillment-Datum gesetzt (31.03.)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: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 togeneral_ledgertable - Migration: Backfill existing entries from
dimensions-JSON →bu_schluesselcolumn - Migration: Remove BU key from
dimensions-JSON (cleanup)
- Create
packages/core/src/general_ledger/derive-bu-schluessel.ts— zentrale Ableitungsfunktion -
createJournalEntry(): BU automatisch setzen viaderiveBuSchluessel(taxRate, country, buConfig) -
process-dimensions.ts: BU-Dimension aus Rule-Verarbeitung ausnehmen -
consolidate-entries.ts:generateDimensionKey()umbu_schluessel-Spalte erweitern (statt aus dimensions-JSON)
- Add
buSchluesselConfigtodatevExportSettingsSchemain@cona/types - Remove
"BU"fromdatevDimensionTypeenum (KOST1/KOST2/Kost-Menge bleiben) - Default
buSchluesselConfiginposting-matrix-seed.tssetzen statt BU-Dimension + Matrix
-
entry-transformer.ts(Temporal + Webapp):entry.bu_schluesseldirekt lesen -
consolidated-transformer.ts(Temporal + Webapp): gleiche Vereinfachung
- 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)
SetLeistungsdatum (#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_datefield togeneral_ledger(nullableDateTime) - Add
fulfillment_datefield todocumentstable (nullableDateTime)
- Extend Shopify Orders GraphQL query to include fulfillment data (
fulfillments.createdAt) - Set
service_dateon revenue GL entries: usefulfillment.createdAtwhen available, fall back toorder.processedAt - Set
fulfillment_dateon the document when a fulfillment exists
-
Leistungsdatum(#115): emitentry.service_datein TTMMJJJJ format when present -
Datum Zuord. Steuerperiode(#116): always set together with Leistungsdatum
- Show
Leistungsdatumin the GL detail view
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_modeto Shopify integration settings ("order_date"|"fulfillment_date") - Toggle in Shopify-Integration-Settings UI: “Umsatzrealisierung: Bestelldatum / Versanddatum”
- 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
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 inentry-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)
- Primär:
-
Erlöskonto (Anzahlungen)(#101) auf AG-Buchungen:- Primär:
sr_entry.credit_account.account_nr(Gegenkonto der SR) - Fallback: Erlöskonto aus
buConfig.taxRateMappings
- Primär:
- Hinweis-Banner: “Steuerberater muss Anzahlungsfunktion in DATEV einmalig konfigurieren”
10. Was sich NICHT ändert (Abgrenzung)
11. Risks & Mitigations
12. Resolved Questions
-
Fulfillment-Datum:
fulfillment.createdAt(Versanddatum) wird verwendet. NichtdeliveryStatus === "delivered", da die meisten Shops kein Carrier-Tracking haben. - 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.
- Accounting Period Auto-Creation: Ja, Buchungsperioden werden automatisch erstellt wie bei allen anderen Vorgängen auch.
-
Consolidated Entries + AG/SR: Bereits gelöst durch die bestehende Konsolidierungslogik. In
consolidate-entries.tswerden 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. - 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.