@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,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking recipes — pure builders: (input, policy) => IJournalDraft.
|
|
3
|
+
* No persistence, no chart lookups; amounts are integer cents and every
|
|
4
|
+
* fractional computation goes through @fin.cx/calculation.
|
|
5
|
+
*/
|
|
6
|
+
import * as plugins from './core.plugins.js';
|
|
7
|
+
import type {
|
|
8
|
+
IJournalDraft,
|
|
9
|
+
IJournalLineDraft,
|
|
10
|
+
TCents,
|
|
11
|
+
TTaxScenario,
|
|
12
|
+
} from './core.types.js';
|
|
13
|
+
import type { IAccountPolicy } from './core.accountpolicy.js';
|
|
14
|
+
import { isAutomatikAccount } from './core.accountpolicy.js';
|
|
15
|
+
import { suggestBuKey } from './core.bukeys.js';
|
|
16
|
+
|
|
17
|
+
const { splitGrossCents, vatFromNetCents } = plugins.calculation;
|
|
18
|
+
|
|
19
|
+
export interface IVendorInvoiceInput {
|
|
20
|
+
date: Date;
|
|
21
|
+
/** creditor personal account (70000-99999) */
|
|
22
|
+
vendorAccount: string;
|
|
23
|
+
expenseAccount?: string;
|
|
24
|
+
/** invoice total that becomes payable (for §13b / igE this equals net) */
|
|
25
|
+
grossCents: TCents;
|
|
26
|
+
vatRatePercent?: number;
|
|
27
|
+
scenario?: TTaxScenario;
|
|
28
|
+
invoiceNumber?: string;
|
|
29
|
+
description: string;
|
|
30
|
+
costCenter1?: string;
|
|
31
|
+
docLink?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ICustomerInvoiceInput {
|
|
35
|
+
date: Date;
|
|
36
|
+
/** debtor personal account (10000-69999) */
|
|
37
|
+
customerAccount: string;
|
|
38
|
+
revenueAccount?: string;
|
|
39
|
+
grossCents: TCents;
|
|
40
|
+
vatRatePercent?: number;
|
|
41
|
+
scenario?: TTaxScenario;
|
|
42
|
+
invoiceNumber?: string;
|
|
43
|
+
description: string;
|
|
44
|
+
costCenter1?: string;
|
|
45
|
+
docLink?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface IBankPaymentInput {
|
|
49
|
+
date: Date;
|
|
50
|
+
direction: 'outgoing' | 'incoming';
|
|
51
|
+
bankAccount?: string;
|
|
52
|
+
/** personal debtor/creditor account being settled */
|
|
53
|
+
counterpartyAccount: string;
|
|
54
|
+
/** open gross amount being settled */
|
|
55
|
+
expectedCents: TCents;
|
|
56
|
+
/** amount that actually moved on the bank account */
|
|
57
|
+
paidCents: TCents;
|
|
58
|
+
/** book the difference as Skonto with VAT correction at this rate */
|
|
59
|
+
skonto?: { ratePercent: number };
|
|
60
|
+
/** write off differences up to this size to rounding-diff accounts (default 0) */
|
|
61
|
+
diffToleranceCents?: TCents;
|
|
62
|
+
description: string;
|
|
63
|
+
reference?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface IInternalTransferInput {
|
|
67
|
+
date: Date;
|
|
68
|
+
fromBankAccount: string;
|
|
69
|
+
toBankAccount: string;
|
|
70
|
+
amountCents: TCents;
|
|
71
|
+
description: string;
|
|
72
|
+
reference?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ISuspenseInput {
|
|
76
|
+
date: Date;
|
|
77
|
+
direction: 'incoming' | 'outgoing';
|
|
78
|
+
amountCents: TCents;
|
|
79
|
+
bankAccount?: string;
|
|
80
|
+
description: string;
|
|
81
|
+
reference?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ISuspenseResolutionInput {
|
|
85
|
+
date: Date;
|
|
86
|
+
direction: 'incoming' | 'outgoing';
|
|
87
|
+
amountCents: TCents;
|
|
88
|
+
/** the account the suspense amount really belongs to */
|
|
89
|
+
targetAccount: string;
|
|
90
|
+
description: string;
|
|
91
|
+
reference?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface IFxDifferenceInput {
|
|
95
|
+
kind: 'payable' | 'receivable';
|
|
96
|
+
/** EUR value at booking date */
|
|
97
|
+
bookedCents: TCents;
|
|
98
|
+
/** EUR value actually settled */
|
|
99
|
+
settledCents: TCents;
|
|
100
|
+
counterAccount: string;
|
|
101
|
+
buchungstext: string;
|
|
102
|
+
belegfeld1?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface ILineSpec {
|
|
106
|
+
accountNumber: string;
|
|
107
|
+
counterAccount: string;
|
|
108
|
+
side: 'debit' | 'credit';
|
|
109
|
+
amountCents: TCents;
|
|
110
|
+
buKey?: IJournalLineDraft['buKey'];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function makeDraft(
|
|
114
|
+
base: {
|
|
115
|
+
date: Date;
|
|
116
|
+
description: string;
|
|
117
|
+
reference?: string;
|
|
118
|
+
scenario?: TTaxScenario;
|
|
119
|
+
belegfeld1?: string;
|
|
120
|
+
costCenter1?: string;
|
|
121
|
+
docLink?: string;
|
|
122
|
+
},
|
|
123
|
+
policy: IAccountPolicy,
|
|
124
|
+
lines: ILineSpec[],
|
|
125
|
+
): IJournalDraft {
|
|
126
|
+
return {
|
|
127
|
+
date: base.date,
|
|
128
|
+
description: base.description,
|
|
129
|
+
reference: base.reference,
|
|
130
|
+
skrType: policy.skrType,
|
|
131
|
+
currency: 'EUR',
|
|
132
|
+
taxScenario: base.scenario,
|
|
133
|
+
lines: lines
|
|
134
|
+
.filter((line) => line.amountCents !== 0)
|
|
135
|
+
.map((line) => ({
|
|
136
|
+
accountNumber: line.accountNumber,
|
|
137
|
+
counterAccount: line.counterAccount,
|
|
138
|
+
side: line.side,
|
|
139
|
+
amountCents: line.amountCents,
|
|
140
|
+
buKey: line.buKey ?? '',
|
|
141
|
+
buchungstext: base.description,
|
|
142
|
+
belegfeld1: base.belegfeld1,
|
|
143
|
+
costCenter1: base.costCenter1,
|
|
144
|
+
docLink: base.docLink,
|
|
145
|
+
})),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function assertPositive(amountCents: TCents, label: string): void {
|
|
150
|
+
if (!Number.isSafeInteger(amountCents) || amountCents <= 0) {
|
|
151
|
+
throw new Error(`${label} must be a positive integer cent amount, got ${amountCents}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Eingangsrechnung (vendor invoice).
|
|
157
|
+
* domestic: expense net + Vorsteuer + creditor gross.
|
|
158
|
+
* §13b / igE: expense net + paired input/output VAT + creditor net.
|
|
159
|
+
* tax-free/import: expense gross + creditor gross.
|
|
160
|
+
*/
|
|
161
|
+
export function bookVendorInvoice(
|
|
162
|
+
input: IVendorInvoiceInput,
|
|
163
|
+
policy: IAccountPolicy,
|
|
164
|
+
): IJournalDraft {
|
|
165
|
+
assertPositive(input.grossCents, 'grossCents');
|
|
166
|
+
const scenario = input.scenario ?? 'domestic_standard';
|
|
167
|
+
const rate =
|
|
168
|
+
input.vatRatePercent ?? (scenario === 'domestic_reduced' ? 7 : scenario === 'domestic_tax_free' ? 0 : 19);
|
|
169
|
+
const expenseAccount = input.expenseAccount ?? policy.expenseDefault;
|
|
170
|
+
const base = {
|
|
171
|
+
date: input.date,
|
|
172
|
+
description: input.description,
|
|
173
|
+
reference: input.invoiceNumber,
|
|
174
|
+
scenario,
|
|
175
|
+
belegfeld1: input.invoiceNumber,
|
|
176
|
+
costCenter1: input.costCenter1,
|
|
177
|
+
docLink: input.docLink,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const expenseIsAutomatik = isAutomatikAccount(policy, expenseAccount);
|
|
181
|
+
|
|
182
|
+
if (scenario === 'domestic_standard' || scenario === 'domestic_reduced') {
|
|
183
|
+
const { netCents, vatCents } = splitGrossCents(input.grossCents, rate);
|
|
184
|
+
const vatAccount = rate === 7 ? policy.vat.inputReduced : policy.vat.inputStandard;
|
|
185
|
+
return makeDraft(base, policy, [
|
|
186
|
+
{
|
|
187
|
+
accountNumber: expenseAccount,
|
|
188
|
+
counterAccount: input.vendorAccount,
|
|
189
|
+
side: 'debit',
|
|
190
|
+
amountCents: netCents,
|
|
191
|
+
buKey: suggestBuKey({
|
|
192
|
+
direction: 'inbound',
|
|
193
|
+
scenario,
|
|
194
|
+
ratePercent: rate,
|
|
195
|
+
targetIsAutomatik: expenseIsAutomatik,
|
|
196
|
+
hasExplicitVatLines: vatCents > 0,
|
|
197
|
+
}),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
accountNumber: vatAccount,
|
|
201
|
+
counterAccount: input.vendorAccount,
|
|
202
|
+
side: 'debit',
|
|
203
|
+
amountCents: vatCents,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
accountNumber: input.vendorAccount,
|
|
207
|
+
counterAccount: expenseAccount,
|
|
208
|
+
side: 'credit',
|
|
209
|
+
amountCents: input.grossCents,
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (scenario === 'reverse_charge_13b' || scenario === 'intra_eu_acquisition') {
|
|
215
|
+
// invoice total is net; German VAT is self-assessed in paired lines
|
|
216
|
+
const vatCents = vatFromNetCents(input.grossCents, rate);
|
|
217
|
+
const inputVatAccount =
|
|
218
|
+
scenario === 'reverse_charge_13b' ? policy.vat.reverseChargeInput : policy.vat.intraEuAcqInput;
|
|
219
|
+
const outputVatAccount =
|
|
220
|
+
scenario === 'reverse_charge_13b'
|
|
221
|
+
? policy.vat.reverseChargeOutput
|
|
222
|
+
: policy.vat.intraEuAcqOutput;
|
|
223
|
+
return makeDraft(base, policy, [
|
|
224
|
+
{
|
|
225
|
+
accountNumber: expenseAccount,
|
|
226
|
+
counterAccount: input.vendorAccount,
|
|
227
|
+
side: 'debit',
|
|
228
|
+
amountCents: input.grossCents,
|
|
229
|
+
buKey: suggestBuKey({
|
|
230
|
+
direction: 'inbound',
|
|
231
|
+
scenario,
|
|
232
|
+
ratePercent: rate,
|
|
233
|
+
targetIsAutomatik: expenseIsAutomatik,
|
|
234
|
+
hasExplicitVatLines: vatCents > 0,
|
|
235
|
+
}),
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
accountNumber: inputVatAccount,
|
|
239
|
+
counterAccount: outputVatAccount,
|
|
240
|
+
side: 'debit',
|
|
241
|
+
amountCents: vatCents,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
accountNumber: outputVatAccount,
|
|
245
|
+
counterAccount: inputVatAccount,
|
|
246
|
+
side: 'credit',
|
|
247
|
+
amountCents: vatCents,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
accountNumber: input.vendorAccount,
|
|
251
|
+
counterAccount: expenseAccount,
|
|
252
|
+
side: 'credit',
|
|
253
|
+
amountCents: input.grossCents,
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// domestic_tax_free, export_third_country (import without German VAT),
|
|
259
|
+
// intra_eu_supply is not a vendor scenario but degrades to the same shape
|
|
260
|
+
return makeDraft(base, policy, [
|
|
261
|
+
{
|
|
262
|
+
accountNumber: expenseAccount,
|
|
263
|
+
counterAccount: input.vendorAccount,
|
|
264
|
+
side: 'debit',
|
|
265
|
+
amountCents: input.grossCents,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
accountNumber: input.vendorAccount,
|
|
269
|
+
counterAccount: expenseAccount,
|
|
270
|
+
side: 'credit',
|
|
271
|
+
amountCents: input.grossCents,
|
|
272
|
+
},
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Ausgangsrechnung (customer invoice).
|
|
278
|
+
*/
|
|
279
|
+
export function bookCustomerInvoice(
|
|
280
|
+
input: ICustomerInvoiceInput,
|
|
281
|
+
policy: IAccountPolicy,
|
|
282
|
+
): IJournalDraft {
|
|
283
|
+
assertPositive(input.grossCents, 'grossCents');
|
|
284
|
+
const scenario = input.scenario ?? 'domestic_standard';
|
|
285
|
+
const rate =
|
|
286
|
+
input.vatRatePercent ?? (scenario === 'domestic_reduced' ? 7 : scenario === 'domestic_standard' ? 19 : 0);
|
|
287
|
+
const base = {
|
|
288
|
+
date: input.date,
|
|
289
|
+
description: input.description,
|
|
290
|
+
reference: input.invoiceNumber,
|
|
291
|
+
scenario,
|
|
292
|
+
belegfeld1: input.invoiceNumber,
|
|
293
|
+
costCenter1: input.costCenter1,
|
|
294
|
+
docLink: input.docLink,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (scenario === 'domestic_standard' || scenario === 'domestic_reduced') {
|
|
298
|
+
const revenueAccount =
|
|
299
|
+
input.revenueAccount ??
|
|
300
|
+
(rate === 7 ? policy.revenue.domesticReduced : policy.revenue.domesticStandard);
|
|
301
|
+
const revenueIsAutomatik = isAutomatikAccount(policy, revenueAccount);
|
|
302
|
+
const { netCents, vatCents } = splitGrossCents(input.grossCents, rate);
|
|
303
|
+
const vatAccount = rate === 7 ? policy.vat.outputReduced : policy.vat.outputStandard;
|
|
304
|
+
return makeDraft(base, policy, [
|
|
305
|
+
{
|
|
306
|
+
accountNumber: input.customerAccount,
|
|
307
|
+
counterAccount: revenueAccount,
|
|
308
|
+
side: 'debit',
|
|
309
|
+
amountCents: input.grossCents,
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
accountNumber: revenueAccount,
|
|
313
|
+
counterAccount: input.customerAccount,
|
|
314
|
+
side: 'credit',
|
|
315
|
+
amountCents: netCents,
|
|
316
|
+
buKey: suggestBuKey({
|
|
317
|
+
direction: 'outbound',
|
|
318
|
+
scenario,
|
|
319
|
+
ratePercent: rate,
|
|
320
|
+
targetIsAutomatik: revenueIsAutomatik,
|
|
321
|
+
hasExplicitVatLines: vatCents > 0,
|
|
322
|
+
}),
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
accountNumber: vatAccount,
|
|
326
|
+
counterAccount: input.customerAccount,
|
|
327
|
+
side: 'credit',
|
|
328
|
+
amountCents: vatCents,
|
|
329
|
+
},
|
|
330
|
+
]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let revenueAccount = input.revenueAccount;
|
|
334
|
+
if (!revenueAccount) {
|
|
335
|
+
if (scenario === 'intra_eu_supply') revenueAccount = policy.revenue.intraEuSupply;
|
|
336
|
+
else if (scenario === 'export_third_country') revenueAccount = policy.revenue.export;
|
|
337
|
+
else if (scenario === 'reverse_charge_13b') revenueAccount = policy.revenue.reverseCharge;
|
|
338
|
+
else {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Scenario '${scenario}' requires an explicit revenueAccount — no policy default exists`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const revenueIsAutomatik = isAutomatikAccount(policy, revenueAccount);
|
|
345
|
+
return makeDraft(base, policy, [
|
|
346
|
+
{
|
|
347
|
+
accountNumber: input.customerAccount,
|
|
348
|
+
counterAccount: revenueAccount,
|
|
349
|
+
side: 'debit',
|
|
350
|
+
amountCents: input.grossCents,
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
accountNumber: revenueAccount,
|
|
354
|
+
counterAccount: input.customerAccount,
|
|
355
|
+
side: 'credit',
|
|
356
|
+
amountCents: input.grossCents,
|
|
357
|
+
buKey: suggestBuKey({
|
|
358
|
+
direction: 'outbound',
|
|
359
|
+
scenario,
|
|
360
|
+
ratePercent: 0,
|
|
361
|
+
targetIsAutomatik: revenueIsAutomatik,
|
|
362
|
+
hasExplicitVatLines: false,
|
|
363
|
+
}),
|
|
364
|
+
},
|
|
365
|
+
]);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Bank payment clearing a debtor/creditor open amount, with Skonto
|
|
370
|
+
* (incl. VAT correction) or small-difference write-off.
|
|
371
|
+
*/
|
|
372
|
+
export function bookBankPayment(input: IBankPaymentInput, policy: IAccountPolicy): IJournalDraft {
|
|
373
|
+
assertPositive(input.expectedCents, 'expectedCents');
|
|
374
|
+
assertPositive(input.paidCents, 'paidCents');
|
|
375
|
+
const bank = input.bankAccount ?? policy.bank;
|
|
376
|
+
const tolerance = input.diffToleranceCents ?? 0;
|
|
377
|
+
const base = {
|
|
378
|
+
date: input.date,
|
|
379
|
+
description: input.description,
|
|
380
|
+
reference: input.reference,
|
|
381
|
+
belegfeld1: input.reference,
|
|
382
|
+
};
|
|
383
|
+
const diff = input.expectedCents - input.paidCents;
|
|
384
|
+
const lines: ILineSpec[] = [];
|
|
385
|
+
|
|
386
|
+
if (input.direction === 'outgoing') {
|
|
387
|
+
lines.push(
|
|
388
|
+
{
|
|
389
|
+
accountNumber: input.counterpartyAccount,
|
|
390
|
+
counterAccount: bank,
|
|
391
|
+
side: 'debit',
|
|
392
|
+
amountCents: input.expectedCents,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
accountNumber: bank,
|
|
396
|
+
counterAccount: input.counterpartyAccount,
|
|
397
|
+
side: 'credit',
|
|
398
|
+
amountCents: input.paidCents,
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
if (diff > 0) {
|
|
402
|
+
if (input.skonto) {
|
|
403
|
+
const rate = input.skonto.ratePercent;
|
|
404
|
+
const { netCents, vatCents } = splitGrossCents(diff, rate);
|
|
405
|
+
const skontoAccount =
|
|
406
|
+
rate === 7 ? policy.skonto.receivedReduced : policy.skonto.receivedStandard;
|
|
407
|
+
const vatAccount = rate === 7 ? policy.vat.inputReduced : policy.vat.inputStandard;
|
|
408
|
+
lines.push({
|
|
409
|
+
accountNumber: skontoAccount,
|
|
410
|
+
counterAccount: input.counterpartyAccount,
|
|
411
|
+
side: 'credit',
|
|
412
|
+
amountCents: netCents,
|
|
413
|
+
buKey: vatCents > 0 ? '40' : '',
|
|
414
|
+
});
|
|
415
|
+
if (vatCents > 0) {
|
|
416
|
+
lines.push({
|
|
417
|
+
accountNumber: vatAccount,
|
|
418
|
+
counterAccount: input.counterpartyAccount,
|
|
419
|
+
side: 'credit',
|
|
420
|
+
amountCents: vatCents,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
} else if (diff <= tolerance) {
|
|
424
|
+
lines.push({
|
|
425
|
+
accountNumber: policy.roundingDiff.gain,
|
|
426
|
+
counterAccount: input.counterpartyAccount,
|
|
427
|
+
side: 'credit',
|
|
428
|
+
amountCents: diff,
|
|
429
|
+
});
|
|
430
|
+
} else {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Underpayment of ${diff} cents exceeds tolerance ${tolerance} — pass skonto or resolve explicitly`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
} else if (diff < 0) {
|
|
436
|
+
const overpaid = -diff;
|
|
437
|
+
if (overpaid <= tolerance) {
|
|
438
|
+
lines.push({
|
|
439
|
+
accountNumber: policy.roundingDiff.loss,
|
|
440
|
+
counterAccount: input.counterpartyAccount,
|
|
441
|
+
side: 'debit',
|
|
442
|
+
amountCents: overpaid,
|
|
443
|
+
});
|
|
444
|
+
} else {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Overpayment of ${overpaid} cents exceeds tolerance ${tolerance} — resolve explicitly`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
lines.push(
|
|
452
|
+
{
|
|
453
|
+
accountNumber: bank,
|
|
454
|
+
counterAccount: input.counterpartyAccount,
|
|
455
|
+
side: 'debit',
|
|
456
|
+
amountCents: input.paidCents,
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
accountNumber: input.counterpartyAccount,
|
|
460
|
+
counterAccount: bank,
|
|
461
|
+
side: 'credit',
|
|
462
|
+
amountCents: input.expectedCents,
|
|
463
|
+
},
|
|
464
|
+
);
|
|
465
|
+
if (diff > 0) {
|
|
466
|
+
if (input.skonto) {
|
|
467
|
+
const rate = input.skonto.ratePercent;
|
|
468
|
+
const { netCents, vatCents } = splitGrossCents(diff, rate);
|
|
469
|
+
const skontoAccount =
|
|
470
|
+
rate === 7 ? policy.skonto.grantedReduced : policy.skonto.grantedStandard;
|
|
471
|
+
const vatAccount = rate === 7 ? policy.vat.outputReduced : policy.vat.outputStandard;
|
|
472
|
+
lines.push({
|
|
473
|
+
accountNumber: skontoAccount,
|
|
474
|
+
counterAccount: input.counterpartyAccount,
|
|
475
|
+
side: 'debit',
|
|
476
|
+
amountCents: netCents,
|
|
477
|
+
buKey: vatCents > 0 ? '40' : '',
|
|
478
|
+
});
|
|
479
|
+
if (vatCents > 0) {
|
|
480
|
+
lines.push({
|
|
481
|
+
accountNumber: vatAccount,
|
|
482
|
+
counterAccount: input.counterpartyAccount,
|
|
483
|
+
side: 'debit',
|
|
484
|
+
amountCents: vatCents,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
} else if (diff <= tolerance) {
|
|
488
|
+
lines.push({
|
|
489
|
+
accountNumber: policy.roundingDiff.loss,
|
|
490
|
+
counterAccount: input.counterpartyAccount,
|
|
491
|
+
side: 'debit',
|
|
492
|
+
amountCents: diff,
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Short payment of ${diff} cents exceeds tolerance ${tolerance} — pass skonto or resolve explicitly`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
} else if (diff < 0) {
|
|
500
|
+
const overReceived = -diff;
|
|
501
|
+
if (overReceived <= tolerance) {
|
|
502
|
+
lines.push({
|
|
503
|
+
accountNumber: policy.roundingDiff.gain,
|
|
504
|
+
counterAccount: input.counterpartyAccount,
|
|
505
|
+
side: 'credit',
|
|
506
|
+
amountCents: overReceived,
|
|
507
|
+
});
|
|
508
|
+
} else {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`Over-receipt of ${overReceived} cents exceeds tolerance ${tolerance} — resolve explicitly`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return makeDraft(base, policy, lines);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Bank-to-bank transfer via Geldtransit — two drafts, one per bank statement,
|
|
521
|
+
* so each side reconciles independently.
|
|
522
|
+
*/
|
|
523
|
+
export function bookInternalTransfer(
|
|
524
|
+
input: IInternalTransferInput,
|
|
525
|
+
policy: IAccountPolicy,
|
|
526
|
+
): [IJournalDraft, IJournalDraft] {
|
|
527
|
+
assertPositive(input.amountCents, 'amountCents');
|
|
528
|
+
const base = {
|
|
529
|
+
date: input.date,
|
|
530
|
+
description: input.description,
|
|
531
|
+
reference: input.reference,
|
|
532
|
+
belegfeld1: input.reference,
|
|
533
|
+
};
|
|
534
|
+
const outgoing = makeDraft(base, policy, [
|
|
535
|
+
{
|
|
536
|
+
accountNumber: policy.geldtransit,
|
|
537
|
+
counterAccount: input.fromBankAccount,
|
|
538
|
+
side: 'debit',
|
|
539
|
+
amountCents: input.amountCents,
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
accountNumber: input.fromBankAccount,
|
|
543
|
+
counterAccount: policy.geldtransit,
|
|
544
|
+
side: 'credit',
|
|
545
|
+
amountCents: input.amountCents,
|
|
546
|
+
},
|
|
547
|
+
]);
|
|
548
|
+
const incoming = makeDraft(base, policy, [
|
|
549
|
+
{
|
|
550
|
+
accountNumber: input.toBankAccount,
|
|
551
|
+
counterAccount: policy.geldtransit,
|
|
552
|
+
side: 'debit',
|
|
553
|
+
amountCents: input.amountCents,
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
accountNumber: policy.geldtransit,
|
|
557
|
+
counterAccount: input.toBankAccount,
|
|
558
|
+
side: 'credit',
|
|
559
|
+
amountCents: input.amountCents,
|
|
560
|
+
},
|
|
561
|
+
]);
|
|
562
|
+
return [outgoing, incoming];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Park an unclear bank movement on the clearing account.
|
|
567
|
+
*/
|
|
568
|
+
export function bookToSuspense(input: ISuspenseInput, policy: IAccountPolicy): IJournalDraft {
|
|
569
|
+
assertPositive(input.amountCents, 'amountCents');
|
|
570
|
+
const bank = input.bankAccount ?? policy.bank;
|
|
571
|
+
const base = {
|
|
572
|
+
date: input.date,
|
|
573
|
+
description: input.description,
|
|
574
|
+
reference: input.reference,
|
|
575
|
+
belegfeld1: input.reference,
|
|
576
|
+
};
|
|
577
|
+
if (input.direction === 'incoming') {
|
|
578
|
+
return makeDraft(base, policy, [
|
|
579
|
+
{ accountNumber: bank, counterAccount: policy.clearing, side: 'debit', amountCents: input.amountCents },
|
|
580
|
+
{ accountNumber: policy.clearing, counterAccount: bank, side: 'credit', amountCents: input.amountCents },
|
|
581
|
+
]);
|
|
582
|
+
}
|
|
583
|
+
return makeDraft(base, policy, [
|
|
584
|
+
{ accountNumber: policy.clearing, counterAccount: bank, side: 'debit', amountCents: input.amountCents },
|
|
585
|
+
{ accountNumber: bank, counterAccount: policy.clearing, side: 'credit', amountCents: input.amountCents },
|
|
586
|
+
]);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Move a parked suspense amount to its real account once clarified.
|
|
591
|
+
*/
|
|
592
|
+
export function resolveSuspense(
|
|
593
|
+
input: ISuspenseResolutionInput,
|
|
594
|
+
policy: IAccountPolicy,
|
|
595
|
+
): IJournalDraft {
|
|
596
|
+
assertPositive(input.amountCents, 'amountCents');
|
|
597
|
+
const base = {
|
|
598
|
+
date: input.date,
|
|
599
|
+
description: input.description,
|
|
600
|
+
reference: input.reference,
|
|
601
|
+
belegfeld1: input.reference,
|
|
602
|
+
};
|
|
603
|
+
if (input.direction === 'incoming') {
|
|
604
|
+
return makeDraft(base, policy, [
|
|
605
|
+
{
|
|
606
|
+
accountNumber: policy.clearing,
|
|
607
|
+
counterAccount: input.targetAccount,
|
|
608
|
+
side: 'debit',
|
|
609
|
+
amountCents: input.amountCents,
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
accountNumber: input.targetAccount,
|
|
613
|
+
counterAccount: policy.clearing,
|
|
614
|
+
side: 'credit',
|
|
615
|
+
amountCents: input.amountCents,
|
|
616
|
+
},
|
|
617
|
+
]);
|
|
618
|
+
}
|
|
619
|
+
return makeDraft(base, policy, [
|
|
620
|
+
{
|
|
621
|
+
accountNumber: input.targetAccount,
|
|
622
|
+
counterAccount: policy.clearing,
|
|
623
|
+
side: 'debit',
|
|
624
|
+
amountCents: input.amountCents,
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
accountNumber: policy.clearing,
|
|
628
|
+
counterAccount: input.targetAccount,
|
|
629
|
+
side: 'credit',
|
|
630
|
+
amountCents: input.amountCents,
|
|
631
|
+
},
|
|
632
|
+
]);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Realized FX difference lines for a settlement whose EUR value moved between
|
|
637
|
+
* booking and payment. Returns lines to embed into the settlement draft
|
|
638
|
+
* (empty array when there is no difference).
|
|
639
|
+
*/
|
|
640
|
+
export function fxDifferenceLines(
|
|
641
|
+
input: IFxDifferenceInput,
|
|
642
|
+
policy: IAccountPolicy,
|
|
643
|
+
): IJournalLineDraft[] {
|
|
644
|
+
const diff = input.settledCents - input.bookedCents;
|
|
645
|
+
if (diff === 0) return [];
|
|
646
|
+
const isGain = input.kind === 'payable' ? diff < 0 : diff > 0;
|
|
647
|
+
const amountCents = Math.abs(diff);
|
|
648
|
+
return [
|
|
649
|
+
{
|
|
650
|
+
accountNumber: isGain ? policy.fx.gain : policy.fx.loss,
|
|
651
|
+
counterAccount: input.counterAccount,
|
|
652
|
+
side: isGain ? 'credit' : 'debit',
|
|
653
|
+
amountCents,
|
|
654
|
+
buKey: '',
|
|
655
|
+
buchungstext: input.buchungstext,
|
|
656
|
+
belegfeld1: input.belegfeld1,
|
|
657
|
+
},
|
|
658
|
+
];
|
|
659
|
+
}
|