@fin.cx/skr 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +33 -6
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/core.accountpolicy.d.ts +67 -0
- package/dist_ts/core/core.accountpolicy.js +91 -0
- package/dist_ts/core/core.bukeys.d.ts +38 -0
- package/dist_ts/core/core.bukeys.js +117 -0
- package/dist_ts/core/core.datev.captions.d.ts +7 -0
- package/dist_ts/core/core.datev.captions.js +134 -0
- package/dist_ts/core/core.datev.d.ts +47 -0
- package/dist_ts/core/core.datev.js +129 -0
- package/dist_ts/core/core.hash.d.ts +27 -0
- package/dist_ts/core/core.hash.js +72 -0
- package/dist_ts/core/core.plugins.d.ts +3 -0
- package/dist_ts/core/core.plugins.js +9 -0
- package/dist_ts/core/core.recipes.d.ts +118 -0
- package/dist_ts/core/core.recipes.js +504 -0
- package/dist_ts/core/core.taxscenario.d.ts +27 -0
- package/dist_ts/core/core.taxscenario.js +85 -0
- package/dist_ts/core/core.types.d.ts +48 -0
- package/dist_ts/core/core.types.js +2 -0
- package/dist_ts/core/core.validate.d.ts +17 -0
- package/dist_ts/core/core.validate.js +89 -0
- package/dist_ts/core/index.d.ts +15 -0
- package/dist_ts/core/index.js +16 -0
- package/dist_ts/index.d.ts +15 -0
- package/dist_ts/index.js +10 -1
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +4 -2
- package/dist_ts/skr.api.d.ts +91 -22
- package/dist_ts/skr.api.js +357 -42
- package/dist_ts/skr.balances.d.ts +28 -0
- package/dist_ts/skr.balances.js +96 -0
- package/dist_ts/skr.classes.journalentry.d.ts +19 -3
- package/dist_ts/skr.classes.journalentry.js +67 -4
- package/dist_ts/skr.classes.reports.js +49 -102
- package/dist_ts/skr.database.d.ts +2 -0
- package/dist_ts/skr.database.js +1 -1
- package/dist_ts/skr.errors.d.ts +18 -0
- package/dist_ts/skr.errors.js +34 -0
- package/dist_ts/skr.export.js +4 -7
- package/dist_ts/skr.invoice.adapter.d.ts +15 -1
- package/dist_ts/skr.invoice.adapter.js +253 -41
- package/dist_ts/skr.invoice.booking.js +14 -28
- package/dist_ts/skr.migrate.d.ts +20 -0
- package/dist_ts/skr.migrate.js +223 -0
- package/dist_ts/skr.period.d.ts +47 -0
- package/dist_ts/skr.period.js +95 -0
- package/dist_ts/skr.posting.d.ts +74 -0
- package/dist_ts/skr.posting.js +209 -0
- package/dist_ts/skr.security.d.ts +6 -9
- package/dist_ts/skr.security.js +16 -89
- package/dist_ts/skr.sequence.d.ts +32 -0
- package/dist_ts/skr.sequence.js +51 -0
- package/dist_ts/skr.txview.d.ts +46 -0
- package/dist_ts/skr.txview.js +235 -0
- package/dist_ts/skr.types.d.ts +35 -1
- package/dist_ts/skr.verify.d.ts +13 -0
- package/dist_ts/skr.verify.js +84 -0
- package/package.json +21 -24
- package/readme.hints.md +37 -0
- package/readme.md +94 -1
- package/readme.plan.md +180 -243
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/core.accountpolicy.ts +154 -0
- package/ts/core/core.bukeys.ts +146 -0
- package/ts/core/core.datev.captions.ts +133 -0
- package/ts/core/core.datev.ts +174 -0
- package/ts/core/core.hash.ts +76 -0
- package/ts/core/core.plugins.ts +9 -0
- package/ts/core/core.recipes.ts +659 -0
- package/ts/core/core.taxscenario.ts +156 -0
- package/ts/core/core.types.ts +74 -0
- package/ts/core/core.validate.ts +119 -0
- package/ts/core/index.ts +15 -0
- package/ts/index.ts +31 -0
- package/ts/plugins.ts +4 -0
- package/ts/skr.api.ts +432 -46
- package/ts/skr.balances.ts +127 -0
- package/ts/skr.classes.journalentry.ts +54 -5
- package/ts/skr.classes.reports.ts +54 -126
- package/ts/skr.database.ts +3 -0
- package/ts/skr.errors.ts +44 -0
- package/ts/skr.export.ts +3 -6
- package/ts/skr.invoice.adapter.ts +291 -45
- package/ts/skr.invoice.booking.ts +22 -34
- package/ts/skr.migrate.ts +268 -0
- package/ts/skr.period.ts +139 -0
- package/ts/skr.posting.ts +273 -0
- package/ts/skr.security.ts +18 -93
- package/ts/skr.sequence.ts +89 -0
- package/ts/skr.txview.ts +268 -0
- package/ts/skr.types.ts +37 -1
- package/ts/skr.verify.ts +99 -0
- /package/{license.md → license} +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DATEV BU-Schlüssel (Steuerschlüssel) — single source of truth.
|
|
3
|
+
*
|
|
4
|
+
* Provenance (2026-07-09): cross-checked against multiple independent German
|
|
5
|
+
* bookkeeping sources. Key semantics that v1 (skr.postingkeys.ts) and the
|
|
6
|
+
* finance.plus reference implementation got wrong are corrected here:
|
|
7
|
+
* - 18/19 are steuerpflichtiger innergemeinschaftlicher Erwerb (§1a UStG)
|
|
8
|
+
* at 7%/19% — NOT generic "Vorsteuer ig Lieferungen".
|
|
9
|
+
* - 91/94 are Leistungsempfänger schuldet die Steuer (§13b UStG) at 7%/19%.
|
|
10
|
+
* The official DATEV Steuerschlüssel-Tabelle (Hilfe-Center Dokument 0904313)
|
|
11
|
+
* is portal-gated; verification against it plus one manual DATEV import per
|
|
12
|
+
* release candidate is the final gate (see readme.plan.md, M0 Phase 0).
|
|
13
|
+
*/
|
|
14
|
+
import type { TBuKey, TTaxScenario } from './core.types.js';
|
|
15
|
+
|
|
16
|
+
export interface IBuKeyRule {
|
|
17
|
+
key: TBuKey;
|
|
18
|
+
description: string;
|
|
19
|
+
taxType: 'none' | 'ust' | 'vst' | 'vst_ust' | 'automatik_aufhebung';
|
|
20
|
+
ratePercent: number | null;
|
|
21
|
+
scenarios: TTaxScenario[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const BU_KEY_RULES: Record<Exclude<TBuKey, ''>, IBuKeyRule> = {
|
|
25
|
+
'1': {
|
|
26
|
+
key: '1',
|
|
27
|
+
description: 'Umsatzsteuerfrei (§4 UStG)',
|
|
28
|
+
taxType: 'ust',
|
|
29
|
+
ratePercent: 0,
|
|
30
|
+
scenarios: ['domestic_tax_free', 'intra_eu_supply', 'export_third_country'],
|
|
31
|
+
},
|
|
32
|
+
'2': {
|
|
33
|
+
key: '2',
|
|
34
|
+
description: 'Umsatzsteuer 7 %',
|
|
35
|
+
taxType: 'ust',
|
|
36
|
+
ratePercent: 7,
|
|
37
|
+
scenarios: ['domestic_reduced'],
|
|
38
|
+
},
|
|
39
|
+
'3': {
|
|
40
|
+
key: '3',
|
|
41
|
+
description: 'Umsatzsteuer 19 %',
|
|
42
|
+
taxType: 'ust',
|
|
43
|
+
ratePercent: 19,
|
|
44
|
+
scenarios: ['domestic_standard'],
|
|
45
|
+
},
|
|
46
|
+
'8': {
|
|
47
|
+
key: '8',
|
|
48
|
+
description: 'Vorsteuer 7 %',
|
|
49
|
+
taxType: 'vst',
|
|
50
|
+
ratePercent: 7,
|
|
51
|
+
scenarios: ['domestic_reduced'],
|
|
52
|
+
},
|
|
53
|
+
'9': {
|
|
54
|
+
key: '9',
|
|
55
|
+
description: 'Vorsteuer 19 %',
|
|
56
|
+
taxType: 'vst',
|
|
57
|
+
ratePercent: 19,
|
|
58
|
+
scenarios: ['domestic_standard'],
|
|
59
|
+
},
|
|
60
|
+
'18': {
|
|
61
|
+
key: '18',
|
|
62
|
+
description: 'Steuerpflichtiger innergemeinschaftlicher Erwerb 7 % (§1a UStG)',
|
|
63
|
+
taxType: 'vst_ust',
|
|
64
|
+
ratePercent: 7,
|
|
65
|
+
scenarios: ['intra_eu_acquisition'],
|
|
66
|
+
},
|
|
67
|
+
'19': {
|
|
68
|
+
key: '19',
|
|
69
|
+
description: 'Steuerpflichtiger innergemeinschaftlicher Erwerb 19 % (§1a UStG)',
|
|
70
|
+
taxType: 'vst_ust',
|
|
71
|
+
ratePercent: 19,
|
|
72
|
+
scenarios: ['intra_eu_acquisition'],
|
|
73
|
+
},
|
|
74
|
+
'40': {
|
|
75
|
+
key: '40',
|
|
76
|
+
description: 'Aufhebung der Automatik',
|
|
77
|
+
taxType: 'automatik_aufhebung',
|
|
78
|
+
ratePercent: null,
|
|
79
|
+
scenarios: [
|
|
80
|
+
'domestic_standard',
|
|
81
|
+
'domestic_reduced',
|
|
82
|
+
'domestic_tax_free',
|
|
83
|
+
'intra_eu_supply',
|
|
84
|
+
'intra_eu_acquisition',
|
|
85
|
+
'reverse_charge_13b',
|
|
86
|
+
'export_third_country',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
'91': {
|
|
90
|
+
key: '91',
|
|
91
|
+
description: 'Leistungsempfänger schuldet die Steuer 7 % (§13b UStG)',
|
|
92
|
+
taxType: 'vst_ust',
|
|
93
|
+
ratePercent: 7,
|
|
94
|
+
scenarios: ['reverse_charge_13b'],
|
|
95
|
+
},
|
|
96
|
+
'94': {
|
|
97
|
+
key: '94',
|
|
98
|
+
description: 'Leistungsempfänger schuldet die Steuer 19 % (§13b UStG)',
|
|
99
|
+
taxType: 'vst_ust',
|
|
100
|
+
ratePercent: 19,
|
|
101
|
+
scenarios: ['reverse_charge_13b'],
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export function isKnownBuKey(value: string): value is TBuKey {
|
|
106
|
+
return value === '' || Object.prototype.hasOwnProperty.call(BU_KEY_RULES, value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getBuKeyRule(key: TBuKey): IBuKeyRule | null {
|
|
110
|
+
if (key === '') return null;
|
|
111
|
+
return BU_KEY_RULES[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Suggest the BU key for the expense/revenue line of a booking.
|
|
116
|
+
*
|
|
117
|
+
* targetIsAutomatik: Automatikkonten compute tax themselves on DATEV import.
|
|
118
|
+
* When the entry carries explicit VAT lines, the automatism must be cancelled
|
|
119
|
+
* with key 40; a plain automatik line without explicit VAT gets no key.
|
|
120
|
+
*/
|
|
121
|
+
export function suggestBuKey(params: {
|
|
122
|
+
direction: 'inbound' | 'outbound';
|
|
123
|
+
scenario: TTaxScenario;
|
|
124
|
+
ratePercent: number;
|
|
125
|
+
targetIsAutomatik: boolean;
|
|
126
|
+
hasExplicitVatLines: boolean;
|
|
127
|
+
}): TBuKey {
|
|
128
|
+
if (params.targetIsAutomatik) {
|
|
129
|
+
return params.hasExplicitVatLines ? '40' : '';
|
|
130
|
+
}
|
|
131
|
+
const reduced = params.ratePercent === 7;
|
|
132
|
+
switch (params.scenario) {
|
|
133
|
+
case 'domestic_standard':
|
|
134
|
+
case 'domestic_reduced':
|
|
135
|
+
if (params.direction === 'inbound') return reduced ? '8' : '9';
|
|
136
|
+
return reduced ? '2' : '3';
|
|
137
|
+
case 'intra_eu_acquisition':
|
|
138
|
+
return reduced ? '18' : '19';
|
|
139
|
+
case 'reverse_charge_13b':
|
|
140
|
+
return params.direction === 'inbound' ? (reduced ? '91' : '94') : '1';
|
|
141
|
+
case 'domestic_tax_free':
|
|
142
|
+
case 'intra_eu_supply':
|
|
143
|
+
case 'export_third_country':
|
|
144
|
+
return params.direction === 'outbound' ? '1' : '';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DATEV Buchungsstapel (EXTF 700, Formatversion 13) column captions — 125 fields.
|
|
3
|
+
* Extracted 1:1 from a DATEV-importable reference file; do not edit by hand.
|
|
4
|
+
* Field 114 (index 113) is Festschreibung. The physical file must be written
|
|
5
|
+
* as CP1252 with CRLF line endings (IO-layer concern, not core's).
|
|
6
|
+
*/
|
|
7
|
+
export const DATEV_COLUMN_CAPTIONS: string[] = [
|
|
8
|
+
"Umsatz (ohne Soll/Haben-Kz)",
|
|
9
|
+
"Soll/Haben-Kennzeichen",
|
|
10
|
+
"WKZ Umsatz",
|
|
11
|
+
"Kurs",
|
|
12
|
+
"Basisumsatz",
|
|
13
|
+
"WKZ Basisumsatz",
|
|
14
|
+
"Konto",
|
|
15
|
+
"Gegenkonto (ohne BU-Schlüssel)",
|
|
16
|
+
"BU-Schlüssel",
|
|
17
|
+
"Belegdatum",
|
|
18
|
+
"Belegfeld 1",
|
|
19
|
+
"Belegfeld 2",
|
|
20
|
+
"Skonto",
|
|
21
|
+
"Buchungstext",
|
|
22
|
+
"Postensperre",
|
|
23
|
+
"Diverse Adressnummer",
|
|
24
|
+
"Geschäftspartnerbank",
|
|
25
|
+
"Sachverhalt",
|
|
26
|
+
"Zinssperre",
|
|
27
|
+
"Beleglink",
|
|
28
|
+
"Beleginfo – Art 1",
|
|
29
|
+
"Beleginfo – Inhalt 1",
|
|
30
|
+
"Beleginfo – Art 2",
|
|
31
|
+
"Beleginfo – Inhalt 2",
|
|
32
|
+
"Beleginfo – Art 3",
|
|
33
|
+
"Beleginfo – Inhalt 3",
|
|
34
|
+
"Beleginfo – Art 4",
|
|
35
|
+
"Beleginfo – Inhalt 4",
|
|
36
|
+
"Beleginfo – Art 5",
|
|
37
|
+
"Beleginfo – Inhalt 5",
|
|
38
|
+
"Beleginfo – Art 6",
|
|
39
|
+
"Beleginfo – Inhalt 6",
|
|
40
|
+
"Beleginfo – Art 7",
|
|
41
|
+
"Beleginfo – Inhalt 7",
|
|
42
|
+
"Beleginfo – Art 8",
|
|
43
|
+
"Beleginfo – Inhalt 8",
|
|
44
|
+
"KOST1 – Kostenstelle",
|
|
45
|
+
"KOST2 – Kostenstelle",
|
|
46
|
+
"Kost Menge",
|
|
47
|
+
"EU-Land u. USt-IdNr.",
|
|
48
|
+
"EU-Steuersatz",
|
|
49
|
+
"Abw. Versteuerungsart",
|
|
50
|
+
"Sachverhalt L+L",
|
|
51
|
+
"Funktionsergänzung L+L",
|
|
52
|
+
"BU 49 Hauptfunktionstyp",
|
|
53
|
+
"BU 49 Hauptfunktionsnummer",
|
|
54
|
+
"BU 49 Funktionsergänzung",
|
|
55
|
+
"Zusatzinformation – Art 1",
|
|
56
|
+
"Zusatzinformation – Inhalt 1",
|
|
57
|
+
"Zusatzinformation – Art 2",
|
|
58
|
+
"Zusatzinformation – Inhalt 2",
|
|
59
|
+
"Zusatzinformation – Art 3",
|
|
60
|
+
"Zusatzinformation – Inhalt 3",
|
|
61
|
+
"Zusatzinformation – Art 4",
|
|
62
|
+
"Zusatzinformation – Inhalt 4",
|
|
63
|
+
"Zusatzinformation – Art 5",
|
|
64
|
+
"Zusatzinformation – Inhalt 5",
|
|
65
|
+
"Zusatzinformation – Art 6",
|
|
66
|
+
"Zusatzinformation – Inhalt 6",
|
|
67
|
+
"Zusatzinformation – Art 7",
|
|
68
|
+
"Zusatzinformation – Inhalt 7",
|
|
69
|
+
"Zusatzinformation – Art 8",
|
|
70
|
+
"Zusatzinformation – Inhalt 8",
|
|
71
|
+
"Zusatzinformation – Art 9",
|
|
72
|
+
"Zusatzinformation – Inhalt 9",
|
|
73
|
+
"Zusatzinformation – Art 10",
|
|
74
|
+
"Zusatzinformation – Inhalt 10",
|
|
75
|
+
"Zusatzinformation – Art 11",
|
|
76
|
+
"Zusatzinformation – Inhalt 11",
|
|
77
|
+
"Zusatzinformation – Art 12",
|
|
78
|
+
"Zusatzinformation – Inhalt 12",
|
|
79
|
+
"Zusatzinformation – Art 13",
|
|
80
|
+
"Zusatzinformation – Inhalt 13",
|
|
81
|
+
"Zusatzinformation – Art 14",
|
|
82
|
+
"Zusatzinformation – Inhalt 14",
|
|
83
|
+
"Zusatzinformation – Art 15",
|
|
84
|
+
"Zusatzinformation – Inhalt 15",
|
|
85
|
+
"Zusatzinformation – Art 16",
|
|
86
|
+
"Zusatzinformation – Inhalt 16",
|
|
87
|
+
"Zusatzinformation – Art 17",
|
|
88
|
+
"Zusatzinformation – Inhalt 17",
|
|
89
|
+
"Zusatzinformation – Art 18",
|
|
90
|
+
"Zusatzinformation – Inhalt 18",
|
|
91
|
+
"Zusatzinformation – Art 19",
|
|
92
|
+
"Zusatzinformation – Inhalt 19",
|
|
93
|
+
"Zusatzinformation – Art 20",
|
|
94
|
+
"Zusatzinformation – Inhalt 20",
|
|
95
|
+
"Stück",
|
|
96
|
+
"Gewicht",
|
|
97
|
+
"Zahlweise",
|
|
98
|
+
"Forderungsart",
|
|
99
|
+
"Veranlagungsjahr",
|
|
100
|
+
"Zugeordnete Fälligkeit",
|
|
101
|
+
"Skontotyp",
|
|
102
|
+
"Auftragsnummer",
|
|
103
|
+
"Buchungstyp",
|
|
104
|
+
"USt-Schlüssel (Anzahlungen)",
|
|
105
|
+
"EU-Mitgliedstaat (Anzahlungen)",
|
|
106
|
+
"Sachverhalt L+L (Anzahlungen)",
|
|
107
|
+
"EU-Steuersatz (Anzahlungen)",
|
|
108
|
+
"Erlöskonto (Anzahlungen)",
|
|
109
|
+
"Herkunft-Kz",
|
|
110
|
+
"Leerfeld",
|
|
111
|
+
"KOST-Datum",
|
|
112
|
+
"SEPA-Mandatsreferenz",
|
|
113
|
+
"Skontosperre",
|
|
114
|
+
"Gesellschaftername",
|
|
115
|
+
"Beteiligtennummer",
|
|
116
|
+
"Identifikationsnummer",
|
|
117
|
+
"Zeichnernummer",
|
|
118
|
+
"Postensperre bis",
|
|
119
|
+
"Bezeichnung",
|
|
120
|
+
"Kennzeichen",
|
|
121
|
+
"Festschreibung",
|
|
122
|
+
"Leistungsdatum",
|
|
123
|
+
"Datum Zuord.",
|
|
124
|
+
"Fälligkeit",
|
|
125
|
+
"Generalumkehr",
|
|
126
|
+
"Steuersatz",
|
|
127
|
+
"Land",
|
|
128
|
+
"Abrechnungsreferent",
|
|
129
|
+
"BVV-Position",
|
|
130
|
+
"EU-Mitgliedstaat u. UStID (Ursprung)",
|
|
131
|
+
"EU-Steuersatz (Ursprung)",
|
|
132
|
+
"Abw. Skontokonto",
|
|
133
|
+
];
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DATEV Buchungsstapel (EXTF 700, Formatversion 13) row model and writer.
|
|
3
|
+
*
|
|
4
|
+
* Layout verified against a DATEV-importable reference file (2026-07-09):
|
|
5
|
+
* 125 data columns, 31 header fields, decimal-comma amounts, Belegdatum TTMM,
|
|
6
|
+
* text fields quoted, field 114 = Festschreibung. The writer emits a CSV
|
|
7
|
+
* STRING with CRLF; persisting it as CP1252 is the IO layer's job.
|
|
8
|
+
* One manual import into real DATEV per release candidate remains the final
|
|
9
|
+
* conformance gate (readme.plan.md, M0).
|
|
10
|
+
*/
|
|
11
|
+
import * as plugins from './core.plugins.js';
|
|
12
|
+
import type { IJournalDraft, TBuKey, TCents } from './core.types.js';
|
|
13
|
+
import { DATEV_COLUMN_CAPTIONS } from './core.datev.captions.js';
|
|
14
|
+
import { DATEV_MAX_BELEGFELD, DATEV_MAX_BUCHUNGSTEXT } from './core.validate.js';
|
|
15
|
+
|
|
16
|
+
const { centsToString } = plugins.calculation;
|
|
17
|
+
|
|
18
|
+
export const DATEV_COLUMN_COUNT = 125;
|
|
19
|
+
export const DATEV_HEADER_FIELD_COUNT = 31;
|
|
20
|
+
|
|
21
|
+
export interface IDatevBuchungsstapelRow {
|
|
22
|
+
umsatzCents: TCents;
|
|
23
|
+
sollHaben: 'S' | 'H';
|
|
24
|
+
konto: string;
|
|
25
|
+
gegenKonto: string;
|
|
26
|
+
buSchluessel: TBuKey;
|
|
27
|
+
belegDatum: Date;
|
|
28
|
+
belegfeld1?: string;
|
|
29
|
+
belegfeld2?: string;
|
|
30
|
+
buchungstext: string;
|
|
31
|
+
belegLink?: string;
|
|
32
|
+
kost1?: string;
|
|
33
|
+
kost2?: string;
|
|
34
|
+
festschreibung?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface IDatevExtfHeaderOptions {
|
|
38
|
+
/** deterministic input — core never reads the clock */
|
|
39
|
+
createdAt: Date;
|
|
40
|
+
consultantNumber: number;
|
|
41
|
+
clientNumber: number;
|
|
42
|
+
fiscalYearStart: Date;
|
|
43
|
+
dateFrom: Date;
|
|
44
|
+
dateTo: Date;
|
|
45
|
+
label: string;
|
|
46
|
+
festschreibung: boolean;
|
|
47
|
+
accountLength?: number;
|
|
48
|
+
/** 2-char producer id in the header */
|
|
49
|
+
origin?: string;
|
|
50
|
+
exportedBy?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pad(value: number, width: number): string {
|
|
54
|
+
return String(value).padStart(width, '0');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatYyyymmdd(date: Date): string {
|
|
58
|
+
return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1, 2)}${pad(date.getUTCDate(), 2)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatTtmm(date: Date): string {
|
|
62
|
+
return `${pad(date.getUTCDate(), 2)}${pad(date.getUTCMonth() + 1, 2)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatTimestamp(date: Date): string {
|
|
66
|
+
return (
|
|
67
|
+
formatYyyymmdd(date) +
|
|
68
|
+
pad(date.getUTCHours(), 2) +
|
|
69
|
+
pad(date.getUTCMinutes(), 2) +
|
|
70
|
+
pad(date.getUTCSeconds(), 2) +
|
|
71
|
+
pad(date.getUTCMilliseconds(), 3)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatAmount(cents: TCents): string {
|
|
76
|
+
return centsToString(cents).replace('.', ',').replace('-', '-');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function quote(value: string, maxLength?: number): string {
|
|
80
|
+
let sanitized = value.replace(/"/g, "'");
|
|
81
|
+
if (maxLength && sanitized.length > maxLength) {
|
|
82
|
+
sanitized = sanitized.slice(0, maxLength);
|
|
83
|
+
}
|
|
84
|
+
return `"${sanitized}"`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Flatten journal drafts into DATEV rows — one row per line, with the line's
|
|
89
|
+
* explicit counterAccount as Gegenkonto (no pair reconstruction).
|
|
90
|
+
*/
|
|
91
|
+
export function journalDraftsToDatevRows(
|
|
92
|
+
drafts: IJournalDraft[],
|
|
93
|
+
options?: { festschreibung?: boolean },
|
|
94
|
+
): IDatevBuchungsstapelRow[] {
|
|
95
|
+
const rows: IDatevBuchungsstapelRow[] = [];
|
|
96
|
+
for (const draft of drafts) {
|
|
97
|
+
for (const line of draft.lines) {
|
|
98
|
+
rows.push({
|
|
99
|
+
umsatzCents: line.amountCents,
|
|
100
|
+
sollHaben: line.side === 'debit' ? 'S' : 'H',
|
|
101
|
+
konto: line.accountNumber,
|
|
102
|
+
gegenKonto: line.counterAccount,
|
|
103
|
+
buSchluessel: line.buKey,
|
|
104
|
+
belegDatum: draft.date,
|
|
105
|
+
belegfeld1: line.belegfeld1 ?? draft.reference,
|
|
106
|
+
belegfeld2: line.belegfeld2,
|
|
107
|
+
buchungstext: line.buchungstext || draft.description,
|
|
108
|
+
belegLink: line.docLink,
|
|
109
|
+
kost1: line.costCenter1,
|
|
110
|
+
kost2: line.costCenter2,
|
|
111
|
+
festschreibung: options?.festschreibung,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return rows;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function rowToFields(row: IDatevBuchungsstapelRow): string[] {
|
|
119
|
+
const fields = new Array<string>(DATEV_COLUMN_COUNT).fill('');
|
|
120
|
+
fields[0] = formatAmount(row.umsatzCents); // 1 Umsatz
|
|
121
|
+
fields[1] = quote(row.sollHaben); // 2 Soll/Haben-Kennzeichen
|
|
122
|
+
fields[6] = row.konto; // 7 Konto
|
|
123
|
+
fields[7] = row.gegenKonto; // 8 Gegenkonto
|
|
124
|
+
if (row.buSchluessel !== '') fields[8] = quote(row.buSchluessel); // 9 BU-Schlüssel
|
|
125
|
+
fields[9] = formatTtmm(row.belegDatum); // 10 Belegdatum
|
|
126
|
+
if (row.belegfeld1) fields[10] = quote(row.belegfeld1, DATEV_MAX_BELEGFELD); // 11
|
|
127
|
+
if (row.belegfeld2) fields[11] = quote(row.belegfeld2, DATEV_MAX_BELEGFELD); // 12
|
|
128
|
+
fields[13] = quote(row.buchungstext, DATEV_MAX_BUCHUNGSTEXT); // 14 Buchungstext
|
|
129
|
+
if (row.belegLink) fields[19] = quote(row.belegLink); // 20 Beleglink
|
|
130
|
+
if (row.kost1) fields[36] = quote(row.kost1); // 37 KOST1
|
|
131
|
+
if (row.kost2) fields[37] = quote(row.kost2); // 38 KOST2
|
|
132
|
+
if (row.festschreibung !== undefined) fields[113] = row.festschreibung ? '1' : '0'; // 114
|
|
133
|
+
return fields;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildHeaderLine(header: IDatevExtfHeaderOptions): string {
|
|
137
|
+
const fields = new Array<string>(DATEV_HEADER_FIELD_COUNT).fill('');
|
|
138
|
+
fields[0] = '"EXTF"';
|
|
139
|
+
fields[1] = '700';
|
|
140
|
+
fields[2] = '21';
|
|
141
|
+
fields[3] = '"Buchungsstapel"';
|
|
142
|
+
fields[4] = '13';
|
|
143
|
+
fields[5] = formatTimestamp(header.createdAt);
|
|
144
|
+
fields[7] = quote(header.origin ?? 'SK');
|
|
145
|
+
fields[8] = quote(header.exportedBy ?? 'fin.cx/skr');
|
|
146
|
+
fields[10] = String(header.consultantNumber);
|
|
147
|
+
fields[11] = String(header.clientNumber);
|
|
148
|
+
fields[12] = formatYyyymmdd(header.fiscalYearStart);
|
|
149
|
+
fields[13] = String(header.accountLength ?? 4);
|
|
150
|
+
fields[14] = formatYyyymmdd(header.dateFrom);
|
|
151
|
+
fields[15] = formatYyyymmdd(header.dateTo);
|
|
152
|
+
fields[16] = quote(header.label, 30);
|
|
153
|
+
fields[18] = '1'; // Buchungstyp: Finanzbuchführung
|
|
154
|
+
fields[20] = header.festschreibung ? '1' : '0';
|
|
155
|
+
fields[21] = '"EUR"';
|
|
156
|
+
return fields.join(';');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Produce the complete EXTF file content (header, caption row, data rows).
|
|
161
|
+
*/
|
|
162
|
+
export function writeExtfBuchungsstapel(
|
|
163
|
+
rows: IDatevBuchungsstapelRow[],
|
|
164
|
+
header: IDatevExtfHeaderOptions,
|
|
165
|
+
): { csv: string; filename: string } {
|
|
166
|
+
const lines: string[] = [
|
|
167
|
+
buildHeaderLine(header),
|
|
168
|
+
DATEV_COLUMN_CAPTIONS.join(';'),
|
|
169
|
+
...rows.map((row) => rowToFields(row).join(';')),
|
|
170
|
+
];
|
|
171
|
+
const csv = lines.join('\r\n') + '\r\n';
|
|
172
|
+
const filename = `EXTF_Buchungsstapel_${formatYyyymmdd(header.dateFrom)}_${formatYyyymmdd(header.dateTo)}.csv`;
|
|
173
|
+
return { csv, filename };
|
|
174
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical serialization + SHA-256 chaining for the GoBD audit trail.
|
|
3
|
+
* Pure functions — the persistence layer decides what goes into the chain;
|
|
4
|
+
* this module guarantees the representation is stable across releases.
|
|
5
|
+
*/
|
|
6
|
+
import * as plugins from './core.plugins.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deterministic string form of a value: object keys sorted recursively,
|
|
10
|
+
* Dates as ISO strings, undefined properties omitted, array order preserved.
|
|
11
|
+
* Non-integer numbers are rejected — monetary values must be integer cents.
|
|
12
|
+
*/
|
|
13
|
+
export function canonicalize(value: unknown): string {
|
|
14
|
+
return JSON.stringify(sortValue(value));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sortValue(value: unknown): unknown {
|
|
18
|
+
if (value === null || value === undefined) return value ?? null;
|
|
19
|
+
if (value instanceof Date) return value.toISOString();
|
|
20
|
+
if (Array.isArray(value)) return value.map((entry) => sortValue(entry));
|
|
21
|
+
if (typeof value === 'number') {
|
|
22
|
+
if (!Number.isFinite(value)) throw new Error(`Cannot canonicalize non-finite number ${value}`);
|
|
23
|
+
if (!Number.isInteger(value)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Cannot canonicalize non-integer number ${value} — monetary values must be integer cents`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === 'object') {
|
|
31
|
+
const sorted: Record<string, unknown> = {};
|
|
32
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
33
|
+
const entry = (value as Record<string, unknown>)[key];
|
|
34
|
+
if (entry !== undefined) sorted[key] = sortValue(entry);
|
|
35
|
+
}
|
|
36
|
+
return sorted;
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hash of one chain entry: SHA-256 over predecessor hash + canonical payload.
|
|
43
|
+
*/
|
|
44
|
+
export function entryHash(prevHash: string, payload: unknown): string {
|
|
45
|
+
return plugins.sha256FromStringSync(`${prevHash}\n${canonicalize(payload)}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Chain genesis per ledger scope (one chain per skrType + fiscal year).
|
|
50
|
+
*/
|
|
51
|
+
export function genesisHash(skrType: string, fiscalYear: number): string {
|
|
52
|
+
return plugins.sha256FromStringSync(`GENESIS:${skrType}:${fiscalYear}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Walk a sequence of already-canonicalized entries and verify the chain.
|
|
57
|
+
* Returns the first mismatch or null when intact.
|
|
58
|
+
*/
|
|
59
|
+
export function verifyChain(
|
|
60
|
+
entries: Array<{ sequenceNumber: number; prevHash: string; entryHash: string; payload: unknown }>,
|
|
61
|
+
genesis: string,
|
|
62
|
+
): { index: number; reason: string } | null {
|
|
63
|
+
let expectedPrev = genesis;
|
|
64
|
+
for (let index = 0; index < entries.length; index++) {
|
|
65
|
+
const entry = entries[index];
|
|
66
|
+
if (entry.prevHash !== expectedPrev) {
|
|
67
|
+
return { index, reason: `prevHash mismatch at sequence ${entry.sequenceNumber}` };
|
|
68
|
+
}
|
|
69
|
+
const recomputed = entryHash(entry.prevHash, entry.payload);
|
|
70
|
+
if (recomputed !== entry.entryHash) {
|
|
71
|
+
return { index, reason: `entryHash mismatch at sequence ${entry.sequenceNumber}` };
|
|
72
|
+
}
|
|
73
|
+
expectedPrev = entry.entryHash;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The stateless core imports ONLY pure-computation dependencies.
|
|
2
|
+
// No smartdata, no filesystem, no network, no crypto certs — this module must
|
|
3
|
+
// stay usable without MongoDB or any runtime environment beyond JS itself.
|
|
4
|
+
// smarthash is permitted as the sole exception for SHA-256 string hashing
|
|
5
|
+
// (pure computation, needed for the GoBD hash chain).
|
|
6
|
+
import * as calculation from '@fin.cx/calculation';
|
|
7
|
+
import { sha256FromStringSync } from '@push.rocks/smarthash';
|
|
8
|
+
|
|
9
|
+
export { calculation, sha256FromStringSync };
|