@fin.cx/skr 3.0.0 → 3.2.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/skr.api.d.ts +34 -0
- package/dist_ts/skr.api.js +59 -1
- package/dist_ts/skr.opos.d.ts +80 -0
- package/dist_ts/skr.opos.js +182 -0
- package/dist_ts/skr.ustva.d.ts +31 -0
- package/dist_ts/skr.ustva.js +99 -0
- package/package.json +2 -1
- package/readme.plan.md +60 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +13 -0
- package/ts/skr.api.ts +96 -0
- package/ts/skr.opos.ts +258 -0
- package/ts/skr.ustva.ts +151 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPOS (Offene-Posten-Buchhaltung): open-item accounting over the v2 ledger.
|
|
3
|
+
*
|
|
4
|
+
* Open items are DERIVED from the hashed journal — no separate bookkeeping
|
|
5
|
+
* state that could drift. A line on a personal account (debtors 10000-69999,
|
|
6
|
+
* creditors 70000-99999) belongs to the open item keyed by
|
|
7
|
+
* (account, belegfeld1); the item is open while its signed sum is non-zero.
|
|
8
|
+
* Settling an item is ordinary posting (core.bookBankPayment + JournalPoster),
|
|
9
|
+
* so Festschreibung and the audit chain apply unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Only dunning metadata lives outside the ledger (OposMeta collection) —
|
|
12
|
+
* process state, not bookkeeping.
|
|
13
|
+
*/
|
|
14
|
+
import * as core from './core/index.js';
|
|
15
|
+
import { getDbSync } from './skr.database.js';
|
|
16
|
+
import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
|
|
17
|
+
export const OPOS_META_COLLECTION = 'OposMeta';
|
|
18
|
+
function kindOf(accountNumber, policy) {
|
|
19
|
+
if (core.isDebtorAccount(policy, accountNumber))
|
|
20
|
+
return 'debtor';
|
|
21
|
+
if (core.isCreditorAccount(policy, accountNumber))
|
|
22
|
+
return 'creditor';
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
async function loadPersonalLines(skrType, policy, accountNumber, asOf) {
|
|
26
|
+
const match = {
|
|
27
|
+
skrType,
|
|
28
|
+
schemaVersion: 2,
|
|
29
|
+
status: { $in: ['posted', 'finalized'] },
|
|
30
|
+
};
|
|
31
|
+
if (asOf)
|
|
32
|
+
match.date = { $lte: asOf };
|
|
33
|
+
if (accountNumber)
|
|
34
|
+
match['lines.accountNumber'] = accountNumber;
|
|
35
|
+
const docs = await getDbSync()
|
|
36
|
+
.mongoDb.collection(JOURNAL_ENTRY_COLLECTION)
|
|
37
|
+
.find(match)
|
|
38
|
+
.sort({ date: 1, sequenceNumber: 1 })
|
|
39
|
+
.toArray();
|
|
40
|
+
const rows = [];
|
|
41
|
+
for (const doc of docs) {
|
|
42
|
+
for (const line of (doc.lines ?? [])) {
|
|
43
|
+
const account = line.accountNumber;
|
|
44
|
+
if (!kindOf(account, policy))
|
|
45
|
+
continue;
|
|
46
|
+
if (accountNumber && account !== accountNumber)
|
|
47
|
+
continue;
|
|
48
|
+
rows.push({
|
|
49
|
+
entryId: doc.id,
|
|
50
|
+
accountNumber: account,
|
|
51
|
+
belegfeld1: line.belegfeld1 || doc.reference || '',
|
|
52
|
+
side: line.side,
|
|
53
|
+
amountCents: line.amountCents ?? 0,
|
|
54
|
+
date: doc.date,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Derive open items. Debtors: claims are debits, settlements credits;
|
|
62
|
+
* creditors inverse. Items with openCents === 0 are settled and omitted
|
|
63
|
+
* unless includeSettled is set.
|
|
64
|
+
*/
|
|
65
|
+
export async function listOpenItems(skrType, policy, options) {
|
|
66
|
+
const rows = await loadPersonalLines(skrType, policy, options?.accountNumber, options?.asOf);
|
|
67
|
+
const groups = new Map();
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const kind = kindOf(row.accountNumber, policy);
|
|
70
|
+
if (options?.kind && kind !== options.kind)
|
|
71
|
+
continue;
|
|
72
|
+
const key = `${row.accountNumber}:${row.belegfeld1}`;
|
|
73
|
+
let item = groups.get(key);
|
|
74
|
+
if (!item) {
|
|
75
|
+
item = {
|
|
76
|
+
accountNumber: row.accountNumber,
|
|
77
|
+
kind,
|
|
78
|
+
reference: row.belegfeld1 ?? '',
|
|
79
|
+
invoiceCents: 0,
|
|
80
|
+
clearedCents: 0,
|
|
81
|
+
openCents: 0,
|
|
82
|
+
firstDate: row.date.toISOString().slice(0, 10),
|
|
83
|
+
lastDate: row.date.toISOString().slice(0, 10),
|
|
84
|
+
entryIds: [],
|
|
85
|
+
dunningLevel: 0,
|
|
86
|
+
};
|
|
87
|
+
groups.set(key, item);
|
|
88
|
+
}
|
|
89
|
+
const isClaim = (kind === 'debtor') === (row.side === 'debit');
|
|
90
|
+
if (isClaim)
|
|
91
|
+
item.invoiceCents += row.amountCents;
|
|
92
|
+
else
|
|
93
|
+
item.clearedCents += row.amountCents;
|
|
94
|
+
item.lastDate = row.date.toISOString().slice(0, 10);
|
|
95
|
+
if (!item.entryIds.includes(row.entryId))
|
|
96
|
+
item.entryIds.push(row.entryId);
|
|
97
|
+
}
|
|
98
|
+
const meta = await getDbSync().mongoDb.collection(OPOS_META_COLLECTION).find({ skrType }).toArray();
|
|
99
|
+
const metaByKey = new Map(meta.map((m) => [`${m.accountNumber}:${m.reference}`, m]));
|
|
100
|
+
const items = [];
|
|
101
|
+
for (const item of groups.values()) {
|
|
102
|
+
item.openCents = item.invoiceCents - item.clearedCents;
|
|
103
|
+
const extra = metaByKey.get(`${item.accountNumber}:${item.reference}`);
|
|
104
|
+
if (extra) {
|
|
105
|
+
item.dunningLevel = extra.dunningLevel ?? 0;
|
|
106
|
+
item.dueDate = extra.dueDate;
|
|
107
|
+
}
|
|
108
|
+
if (item.openCents !== 0 || options?.includeSettled)
|
|
109
|
+
items.push(item);
|
|
110
|
+
}
|
|
111
|
+
return items.sort((a, b) => a.accountNumber.localeCompare(b.accountNumber) || a.firstDate.localeCompare(b.firstDate));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Aging list relative to asOf, bucketed by days since dueDate (falling back
|
|
115
|
+
* to the item's first posting date).
|
|
116
|
+
*/
|
|
117
|
+
export async function getAging(skrType, policy, kind, asOf, bucketDays = [30, 60, 90]) {
|
|
118
|
+
const items = await listOpenItems(skrType, policy, { kind, asOf });
|
|
119
|
+
const buckets = [
|
|
120
|
+
...bucketDays.map((days, index) => ({
|
|
121
|
+
label: index === 0 ? `0-${days}` : `${bucketDays[index - 1] + 1}-${days}`,
|
|
122
|
+
maxAgeDays: days,
|
|
123
|
+
items: [],
|
|
124
|
+
totalCents: 0,
|
|
125
|
+
})),
|
|
126
|
+
{ label: `> ${bucketDays[bucketDays.length - 1]}`, maxAgeDays: null, items: [], totalCents: 0 },
|
|
127
|
+
];
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
const anchor = item.dueDate ?? item.firstDate;
|
|
130
|
+
const ageDays = Math.max(0, Math.floor((asOf.getTime() - new Date(anchor).getTime()) / 86_400_000));
|
|
131
|
+
const bucket = buckets.find((candidate) => candidate.maxAgeDays !== null && ageDays <= candidate.maxAgeDays) ??
|
|
132
|
+
buckets[buckets.length - 1];
|
|
133
|
+
bucket.items.push(item);
|
|
134
|
+
bucket.totalCents += item.openCents;
|
|
135
|
+
}
|
|
136
|
+
return buckets;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Subledger control totals: the aggregated personal-account balances that a
|
|
140
|
+
* balance sheet reports as Forderungen (B.II.1) / Verbindlichkeiten (C.4)
|
|
141
|
+
* aus Lieferungen und Leistungen. Control accounts themselves are blocked for
|
|
142
|
+
* posting, so these aggregates ARE the control balances.
|
|
143
|
+
*/
|
|
144
|
+
export async function getControlTotals(skrType, policy, asOf) {
|
|
145
|
+
const rows = await loadPersonalLines(skrType, policy, undefined, asOf);
|
|
146
|
+
let debtorsCents = 0;
|
|
147
|
+
let creditorsCents = 0;
|
|
148
|
+
const debtorAccounts = new Set();
|
|
149
|
+
const creditorAccounts = new Set();
|
|
150
|
+
for (const row of rows) {
|
|
151
|
+
const signed = row.side === 'debit' ? row.amountCents : -row.amountCents;
|
|
152
|
+
if (kindOf(row.accountNumber, policy) === 'debtor') {
|
|
153
|
+
debtorsCents += signed;
|
|
154
|
+
debtorAccounts.add(row.accountNumber);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
creditorsCents -= signed; // creditors carry credit balances positive
|
|
158
|
+
creditorAccounts.add(row.accountNumber);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
debtorsCents,
|
|
163
|
+
creditorsCents,
|
|
164
|
+
debtorAccounts: debtorAccounts.size,
|
|
165
|
+
creditorAccounts: creditorAccounts.size,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Dunning metadata (process state, not bookkeeping — letters are out of
|
|
170
|
+
* scope; the level/due-date state machine is in).
|
|
171
|
+
*/
|
|
172
|
+
export async function setOposMeta(params) {
|
|
173
|
+
const update = { updatedAt: new Date() };
|
|
174
|
+
if (params.dunningLevel !== undefined)
|
|
175
|
+
update.dunningLevel = params.dunningLevel;
|
|
176
|
+
if (params.dueDate !== undefined)
|
|
177
|
+
update.dueDate = params.dueDate;
|
|
178
|
+
await getDbSync()
|
|
179
|
+
.mongoDb.collection(OPOS_META_COLLECTION)
|
|
180
|
+
.updateOne({ skrType: params.skrType, accountNumber: params.accountNumber, reference: params.reference }, { $set: update }, { upsert: true });
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2tyLm9wb3MuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9za3Iub3Bvcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7O0dBWUc7QUFDSCxPQUFPLEtBQUssSUFBSSxNQUFNLGlCQUFpQixDQUFDO0FBQ3hDLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUM5QyxPQUFPLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUczRCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxVQUFVLENBQUM7QUFvQy9DLFNBQVMsTUFBTSxDQUFDLGFBQXFCLEVBQUUsTUFBMkI7SUFDaEUsSUFBSSxJQUFJLENBQUMsZUFBZSxDQUFDLE1BQU0sRUFBRSxhQUFhLENBQUM7UUFBRSxPQUFPLFFBQVEsQ0FBQztJQUNqRSxJQUFJLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLEVBQUUsYUFBYSxDQUFDO1FBQUUsT0FBTyxVQUFVLENBQUM7SUFDckUsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDO0FBV0QsS0FBSyxVQUFVLGlCQUFpQixDQUM5QixPQUFpQixFQUNqQixNQUEyQixFQUMzQixhQUFzQixFQUN0QixJQUFXO0lBRVgsTUFBTSxLQUFLLEdBQTRCO1FBQ3JDLE9BQU87UUFDUCxhQUFhLEVBQUUsQ0FBQztRQUNoQixNQUFNLEVBQUUsRUFBRSxHQUFHLEVBQUUsQ0FBQyxRQUFRLEVBQUUsV0FBVyxDQUFDLEVBQUU7S0FDekMsQ0FBQztJQUNGLElBQUksSUFBSTtRQUFFLEtBQUssQ0FBQyxJQUFJLEdBQUcsRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLENBQUM7SUFDdEMsSUFBSSxhQUFhO1FBQUUsS0FBSyxDQUFDLHFCQUFxQixDQUFDLEdBQUcsYUFBYSxDQUFDO0lBRWhFLE1BQU0sSUFBSSxHQUFHLE1BQU0sU0FBUyxFQUFFO1NBQzNCLE9BQU8sQ0FBQyxVQUFVLENBQUMsd0JBQXdCLENB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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IAccountPolicy } from './core/index.js';
|
|
2
|
+
import type { TSKRType } from './skr.types.js';
|
|
3
|
+
export interface IUstvaMovement {
|
|
4
|
+
accountNumber: string;
|
|
5
|
+
/** debitCents - creditCents over the period */
|
|
6
|
+
signedCents: number;
|
|
7
|
+
}
|
|
8
|
+
export interface IUstvaScenarioBases {
|
|
9
|
+
/** igE 19 % net bases (Kz 89) */
|
|
10
|
+
intraEuAcquisitionStandardCents: number;
|
|
11
|
+
/** igE 7 % net bases (Kz 93) */
|
|
12
|
+
intraEuAcquisitionReducedCents: number;
|
|
13
|
+
/**
|
|
14
|
+
* §13b bases (Kz 46). The scenario tag does not distinguish EU services
|
|
15
|
+
* from other §13b cases — everything lands here; reclassify to Kz 84
|
|
16
|
+
* manually if a non-EU-services §13b case ever occurs.
|
|
17
|
+
*/
|
|
18
|
+
reverseChargeServicesEuCents: number;
|
|
19
|
+
}
|
|
20
|
+
export interface IUstvaPeriodData {
|
|
21
|
+
movements: IUstvaMovement[];
|
|
22
|
+
scenarioBases: IUstvaScenarioBases;
|
|
23
|
+
/** tagged igE/§13b entries examined */
|
|
24
|
+
taggedEntryCount: number;
|
|
25
|
+
/** extraction anomalies — review before filing */
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
export declare function getUstvaPeriodData(skrType: TSKRType, policy: IAccountPolicy, options: {
|
|
29
|
+
dateFrom: Date;
|
|
30
|
+
dateTo: Date;
|
|
31
|
+
}): Promise<IUstvaPeriodData>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UStVA period extraction: one period's account movements plus the EXACT
|
|
3
|
+
* scenario Bemessungsgrundlagen (igE / §13b) taken from taxScenario-tagged
|
|
4
|
+
* journal entries. The result feeds @fin.cx/tax `computeUstva()` directly —
|
|
5
|
+
* movements as `IAccountMovementInput[]`, scenarioBases as
|
|
6
|
+
* `IScenarioBaseInput`.
|
|
7
|
+
*
|
|
8
|
+
* Base extraction per tagged entry: sum of (debit - credit) over lines that
|
|
9
|
+
* sit neither on a policy VAT account nor on a personal account (>= 10000).
|
|
10
|
+
* For the recipe shapes that is exactly the net expense line; Stornos flip
|
|
11
|
+
* sides and cancel. Every extracted base is cross-checked against the
|
|
12
|
+
* entry's own input-VAT line — disagreement lands in `warnings`.
|
|
13
|
+
*/
|
|
14
|
+
import { vatFromNetCents } from '@fin.cx/calculation';
|
|
15
|
+
import { getDbSync } from './skr.database.js';
|
|
16
|
+
import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
|
|
17
|
+
import { aggregateTrialBalance } from './skr.balances.js';
|
|
18
|
+
function isPersonalAccount(accountNumber) {
|
|
19
|
+
const numeric = Number.parseInt(accountNumber, 10);
|
|
20
|
+
return numeric >= 10000 && numeric <= 99999;
|
|
21
|
+
}
|
|
22
|
+
export async function getUstvaPeriodData(skrType, policy, options) {
|
|
23
|
+
const trialBalance = await aggregateTrialBalance(skrType, options);
|
|
24
|
+
const movements = trialBalance.accounts
|
|
25
|
+
.filter((account) => account.balanceCents !== 0)
|
|
26
|
+
.map((account) => ({
|
|
27
|
+
accountNumber: account.accountNumber,
|
|
28
|
+
signedCents: account.balanceCents,
|
|
29
|
+
}));
|
|
30
|
+
const vatAccounts = new Set(Object.values(policy.vat));
|
|
31
|
+
const warnings = [];
|
|
32
|
+
const scenarioBases = {
|
|
33
|
+
intraEuAcquisitionStandardCents: 0,
|
|
34
|
+
intraEuAcquisitionReducedCents: 0,
|
|
35
|
+
reverseChargeServicesEuCents: 0,
|
|
36
|
+
};
|
|
37
|
+
const collection = getDbSync().mongoDb.collection(JOURNAL_ENTRY_COLLECTION);
|
|
38
|
+
const tagged = (await collection
|
|
39
|
+
.find({
|
|
40
|
+
schemaVersion: 2,
|
|
41
|
+
skrType,
|
|
42
|
+
status: { $in: ['posted', 'finalized'] },
|
|
43
|
+
taxScenario: { $in: ['intra_eu_acquisition', 'reverse_charge_13b'] },
|
|
44
|
+
date: { $gte: options.dateFrom, $lte: options.dateTo },
|
|
45
|
+
}, { projection: { sequenceNumber: 1, journalNumber: 1, taxScenario: 1, lines: 1 } })
|
|
46
|
+
.toArray());
|
|
47
|
+
for (const entry of tagged) {
|
|
48
|
+
const label = entry.journalNumber ?? `seq ${entry.sequenceNumber}`;
|
|
49
|
+
const inputVatAccount = entry.taxScenario === 'intra_eu_acquisition'
|
|
50
|
+
? policy.vat.intraEuAcqInput
|
|
51
|
+
: policy.vat.reverseChargeInput;
|
|
52
|
+
let baseCents = 0;
|
|
53
|
+
let inputVatCents = 0;
|
|
54
|
+
for (const line of entry.lines) {
|
|
55
|
+
const amount = line.amountCents ?? 0;
|
|
56
|
+
const signed = line.side === 'debit' ? amount : -amount;
|
|
57
|
+
if (line.accountNumber === inputVatAccount) {
|
|
58
|
+
inputVatCents += signed;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (vatAccounts.has(line.accountNumber))
|
|
62
|
+
continue;
|
|
63
|
+
if (isPersonalAccount(line.accountNumber))
|
|
64
|
+
continue;
|
|
65
|
+
baseCents += signed;
|
|
66
|
+
}
|
|
67
|
+
if (baseCents === 0 && inputVatCents !== 0) {
|
|
68
|
+
warnings.push(`entry ${label}: no base extractable (counterparty not a personal account?) — VAT line is ${inputVatCents} cents`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (baseCents === 0)
|
|
72
|
+
continue;
|
|
73
|
+
// rate detection from the entry's own VAT line (19 % vs 7 %)
|
|
74
|
+
const absBase = Math.abs(baseCents);
|
|
75
|
+
const absVat = Math.abs(inputVatCents);
|
|
76
|
+
const reduced = Math.abs(absVat * 100 - 7 * absBase) < Math.abs(absVat * 100 - 19 * absBase);
|
|
77
|
+
const rate = reduced ? 7 : 19;
|
|
78
|
+
const expectedVat = vatFromNetCents(baseCents, rate);
|
|
79
|
+
if (Math.abs(inputVatCents - expectedVat) > 2) {
|
|
80
|
+
warnings.push(`entry ${label}: base ${baseCents} cents at ${rate} % expects VAT ${expectedVat}, ledger line says ${inputVatCents}`);
|
|
81
|
+
}
|
|
82
|
+
if (entry.taxScenario === 'intra_eu_acquisition') {
|
|
83
|
+
if (reduced)
|
|
84
|
+
scenarioBases.intraEuAcquisitionReducedCents += baseCents;
|
|
85
|
+
else
|
|
86
|
+
scenarioBases.intraEuAcquisitionStandardCents += baseCents;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
scenarioBases.reverseChargeServicesEuCents += baseCents;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
movements,
|
|
94
|
+
scenarioBases,
|
|
95
|
+
taggedEntryCount: tagged.length,
|
|
96
|
+
warnings,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2tyLnVzdHZhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvc2tyLnVzdHZhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7R0FZRztBQUNILE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUN0RCxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDOUMsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDM0QsT0FBTyxFQUFFLHFCQUFxQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUF1QzFELFNBQVMsaUJBQWlCLENBQUMsYUFBcUI7SUFDOUMsTUFBTSxPQUFPLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxhQUFhLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDbkQsT0FBTyxPQUFPLElBQUksS0FBSyxJQUFJLE9BQU8sSUFBSSxLQUFLLENBQUM7QUFDOUMsQ0FBQztBQUVELE1BQU0sQ0FBQyxLQUFLLFVBQVUsa0JBQWtCLENBQ3RDLE9BQWlCLEVBQ2pCLE1BQXNCLEVBQ3RCLE9BQXlDO0lBRXpDLE1BQU0sWUFBWSxHQUFHLE1BQU0scUJBQXFCLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ25FLE1BQU0sU0FBUyxHQUFxQixZQUFZLENBQUMsUUFBUTtTQUN0RCxNQUFNLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDLE9BQU8sQ0FBQyxZQUFZLEtBQUssQ0FBQyxDQUFDO1NBQy9DLEdBQUcsQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUMsQ0FBQztRQUNqQixhQUFhLEVBQUUsT0FBTyxDQUFDLGFBQWE7UUFDcEMsV0FBVyxFQUFFLE9BQU8sQ0FBQyxZQUFZO0tBQ2xDLENBQUMsQ0FBQyxDQUFDO0lBRU4sTUFBTSxXQUFXLEdBQUcsSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsR0FBRyxD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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fin.cx/skr",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "SKR03 and SKR04 German accounting standards for double-entry bookkeeping",
|
|
5
5
|
"main": "dist_ts/index.js",
|
|
6
6
|
"typings": "dist_ts/index.d.ts",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@fin.cx/chartdata": "^2.1.0",
|
|
38
|
+
"@fin.cx/tax": "^1.1.0",
|
|
38
39
|
"@git.zone/tsbuild": "^4.4.2",
|
|
39
40
|
"@git.zone/tsrun": "^2.0.5",
|
|
40
41
|
"@git.zone/tstest": "^3.6.6",
|
package/readme.plan.md
CHANGED
|
@@ -102,6 +102,19 @@ allowed BU keys, account→UStVA-Kennzahl mappings, scenario Kennzahlen table
|
|
|
102
102
|
(89/93, 46/47, 84/85, 41, 43, 81, 86, 60, 66/61/67/62), coverage introspection,
|
|
103
103
|
provenance per record, reproducible seed pipeline in tools/.
|
|
104
104
|
**RELEASED as @fin.cx/chartdata v1.1.0** (2026-07-09, verdaccio + npmjs).
|
|
105
|
+
**A1 REBASE COMPLETE (2026-07-09):** chartdata v2.1.0 = curated oracle-verified
|
|
106
|
+
datasets (91 SKR03 / 78 SKR04 accounts, verification-only oracle, no foreign
|
|
107
|
+
strings); skr v3.0.0 aligned: charts GENERATED from chartdata
|
|
108
|
+
(tools/generate-chart-data.ts), policies corrected (expense default 4900/6300,
|
|
109
|
+
SKR04 controls 1200/3300, bank 1800/Kasse 1600), all 98 tests remapped to real
|
|
110
|
+
DATEV numbers and green. A2 HGB §266/§275 mappings DONE
|
|
111
|
+
(chartdata v2.2.0: every non-statistical account carries hgb266Position or
|
|
112
|
+
hgb275Position, validated against the statute Gliederung trees in
|
|
113
|
+
chartdata.hgb.ts and verified against the oracle's position tags — 11
|
|
114
|
+
documented modeling exceptions where German textbook treatment differs, e.g.
|
|
115
|
+
USt liabilities in Passiva C.8). NEXT: A3 E-Bilanz taxonomy; D1 can now build
|
|
116
|
+
the §266/§275 Gliederung from chartdata.
|
|
117
|
+
|
|
105
118
|
**A1 cross-diff done (2026-07-09):** Odoo l10n_de oracle proves the skr-v1 seed
|
|
106
119
|
charts are an invented hybrid (~50/125 SKR03 + ~66/130 SKR04 divergences);
|
|
107
120
|
ALL fiscal A1 additions verified clean. Rate-label fixes released (v1.1.1:
|
|
@@ -132,9 +145,30 @@ auto/review/unmatched decisions to skr recipes in the consuming app.
|
|
|
132
145
|
|
|
133
146
|
B1 `@fin.cx/camt` (CAMT.052/053/054; MT940 = legacy) + `@fin.cx/bankrec` (canonical
|
|
134
147
|
decimal bank txn with SEPA refs, adapters, balance-chain continuity, confidence-scored
|
|
135
|
-
matching, auto-post above threshold, review queue below).
|
|
148
|
+
matching, auto-post above threshold, review queue below).
|
|
149
|
+
|
|
150
|
+
**B2 status 2026-07-09: RELEASED in skr v3.1.0** — open items DERIVED from the
|
|
151
|
+
hashed v2 ledger (no drifting side-state): grouped by (personal account,
|
|
152
|
+
belegfeld1), partial settlements book only the paid amount, full settlements
|
|
153
|
+
with Skonto+VSt correction via the core recipe (ordinary audited postings),
|
|
154
|
+
aging buckets, dunning metadata (OposMeta, process state only), control totals
|
|
155
|
+
= aggregated personal ranges (feed HGB B.II.1/C.4 in D1). Dunning LETTERS out
|
|
156
|
+
of scope as planned. A2 also DONE (chartdata v2.2.0 HGB mappings).
|
|
157
|
+
|
|
158
|
+
B2 original scope: OPOS inside skr (ts/opos/:
|
|
136
159
|
masters, open items, clearItems with Skonto+VAT correction, aging, dunning states;
|
|
137
|
-
invariant: subledger == control account).
|
|
160
|
+
invariant: subledger == control account).
|
|
161
|
+
|
|
162
|
+
**B3 status 2026-07-09: RELEASED as @fin.cx/assets v1.1.0** — pure-function
|
|
163
|
+
Anlagenbuchhaltung: exact-cent AfA schedules (linear + degressive with switch,
|
|
164
|
+
pro rata temporis), GWG immediate/Sammelposten, year-keyed policy data (GWG
|
|
165
|
+
thresholds, degressive windows incl. Investitionsbooster 2025, BMF useful-life
|
|
166
|
+
suggestions), disposals with Buchgewinn/-verlust, §284(3) Anlagespiegel (gross
|
|
167
|
+
method, tie-outs tested), ledger-agnostic posting proposals for skr recipes.
|
|
168
|
+
12/12 tests. Remaining M-B: A3 taxonomy, then D1 consumes Anlagespiegel +
|
|
169
|
+
getControlTotals + chartdata HGB mappings.
|
|
170
|
+
|
|
171
|
+
B3 original scope: `@fin.cx/assets` (AfA schedules from BMF
|
|
138
172
|
tables, GWG/degressive as year-keyed policy, Anlagespiegel → D1). B1 before live UStVA;
|
|
139
173
|
B2/B3 may trail.
|
|
140
174
|
|
|
@@ -146,8 +180,32 @@ version-compat matrix + checksum gate). Kennzahl computation in `@fin.cx/tax` (V
|
|
|
146
180
|
ships early). C1 bridge → C2 UStVA 3-month parallel-run (3 consecutive zero/explained-diff
|
|
147
181
|
months required) → C2' live + E1 → C3 DFV/ZM/UStJE.
|
|
148
182
|
|
|
183
|
+
**UStVA Kennzahl module RELEASED 2026-07-09 as @fin.cx/tax v1.1.0** —
|
|
184
|
+
`computeUstva()`: pure aggregation of period account movements into UStVA
|
|
185
|
+
Kennzahlen via chartdata mappings (81/86/41/43/48/60 bases, 66/61/67/62 taxes),
|
|
186
|
+
form rounding done right (Bemessungsgrundlagen truncated to full euros on the
|
|
187
|
+
period sum per Kennzahl; taxes cent-exact; output tax = form computation from
|
|
188
|
+
truncated bases), igE/§13b bases from exact `scenarioBases` (skr taxScenario
|
|
189
|
+
tags) with `(derived)` fallback from output-tax movements, Kz 47/85 split
|
|
190
|
+
preserving the ledger total, Kz 83 Zahllast + Kz 39 Sondervorauszahlung,
|
|
191
|
+
ledger-vs-form reconciliation with tolerance (default 5 EUR) and loud `issues`
|
|
192
|
+
(unmapped revenue, unknown revenue-class accounts, undifferentiated output
|
|
193
|
+
VAT). Soll/Ist is the CALLER's period selection; `taxationMethod` recorded on
|
|
194
|
+
the result — open question #3 still needs answering before first filing.
|
|
195
|
+
12/12 tests. Remaining C2: skr-side glue (movements + scenario base extraction
|
|
196
|
+
per period), then parallel-run diffing once ELSTER access exists.
|
|
197
|
+
|
|
149
198
|
### M-D — Year-end (`@fin.cx/jahresabschluss` + `@fin.cx/tax`, Q1–Q2 2027)
|
|
150
199
|
|
|
200
|
+
**D1 CORE RELEASED 2026-07-09 as @fin.cx/jahresabschluss v1.1.0** (built early
|
|
201
|
+
— all inputs existed): §266 Bilanz + §275 GKV GuV from chartdata mappings and
|
|
202
|
+
exact-cent balances (loud failure on unmapped balances; Jahresergebnis derived
|
|
203
|
+
twice and tied out on Passiva A.V; personal accounts auto-roll to B.II.1/C.4),
|
|
204
|
+
§267/§267a size classes as year-keyed data (post-2024 thresholds + two-year
|
|
205
|
+
rule), condensed statements for klein/kleinst. 10/10 tests. Remaining D1:
|
|
206
|
+
Anhang skeleton, Anlagespiegel integration glue, real-data validation
|
|
207
|
+
("re-file the past" needs prior-year books).
|
|
208
|
+
|
|
151
209
|
D1 §266/§275 Gliederung + §267 size classes + Anhang skeleton + Anlagespiegel. D3 tax
|
|
152
210
|
engine (§4(5) EStG, §8b KStG, GewSt §8/§9 add-backs, loss carryforwards, KSt 15%+Soli,
|
|
153
211
|
GewSt Hebesatz Bremen 460%; provisions posted back to skr). D2 E-Bilanz XBRL writer
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/index.ts
CHANGED
|
@@ -19,6 +19,19 @@ export type {
|
|
|
19
19
|
ITrialBalanceCents,
|
|
20
20
|
IBalanceQueryOptions,
|
|
21
21
|
} from './skr.balances.js';
|
|
22
|
+
export {
|
|
23
|
+
listOpenItems,
|
|
24
|
+
getAging,
|
|
25
|
+
getControlTotals,
|
|
26
|
+
setOposMeta,
|
|
27
|
+
} from './skr.opos.js';
|
|
28
|
+
export type { IOpenItem, IAgingBucket, IControlTotals, TOposKind } from './skr.opos.js';
|
|
29
|
+
export { getUstvaPeriodData } from './skr.ustva.js';
|
|
30
|
+
export type {
|
|
31
|
+
IUstvaPeriodData,
|
|
32
|
+
IUstvaMovement,
|
|
33
|
+
IUstvaScenarioBases,
|
|
34
|
+
} from './skr.ustva.js';
|
|
22
35
|
export { verifyHashChain } from './skr.verify.js';
|
|
23
36
|
export type { IChainVerificationResult } from './skr.verify.js';
|
|
24
37
|
export { migrateToV2 } from './skr.migrate.js';
|
package/ts/skr.api.ts
CHANGED
|
@@ -1026,6 +1026,102 @@ export class SkrApi {
|
|
|
1026
1026
|
return migrateToV2();
|
|
1027
1027
|
}
|
|
1028
1028
|
|
|
1029
|
+
// ========== OPOS (open items) ==========
|
|
1030
|
+
|
|
1031
|
+
public async listOpenItems(options?: {
|
|
1032
|
+
kind?: import('./skr.opos.js').TOposKind;
|
|
1033
|
+
accountNumber?: string;
|
|
1034
|
+
asOf?: Date;
|
|
1035
|
+
includeSettled?: boolean;
|
|
1036
|
+
}): Promise<import('./skr.opos.js').IOpenItem[]> {
|
|
1037
|
+
this.ensureInitialized();
|
|
1038
|
+
const { listOpenItems } = await import('./skr.opos.js');
|
|
1039
|
+
return listOpenItems(this.currentSKRType!, this.getDefaultPolicy(), options);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
public async getOposAging(
|
|
1043
|
+
kind: import('./skr.opos.js').TOposKind,
|
|
1044
|
+
asOf: Date,
|
|
1045
|
+
bucketDays?: number[],
|
|
1046
|
+
): Promise<import('./skr.opos.js').IAgingBucket[]> {
|
|
1047
|
+
this.ensureInitialized();
|
|
1048
|
+
const { getAging } = await import('./skr.opos.js');
|
|
1049
|
+
return getAging(this.currentSKRType!, this.getDefaultPolicy(), kind, asOf, bucketDays);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
public async getControlTotals(asOf?: Date): Promise<import('./skr.opos.js').IControlTotals> {
|
|
1053
|
+
this.ensureInitialized();
|
|
1054
|
+
const { getControlTotals } = await import('./skr.opos.js');
|
|
1055
|
+
return getControlTotals(this.currentSKRType!, this.getDefaultPolicy(), asOf);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ========== UStVA extraction ==========
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* One period's movements + exact igE/§13b scenario bases, shaped for
|
|
1062
|
+
* @fin.cx/tax `computeUstva()`. Review `warnings` before filing.
|
|
1063
|
+
*/
|
|
1064
|
+
public async getUstvaPeriodData(
|
|
1065
|
+
dateFrom: Date,
|
|
1066
|
+
dateTo: Date,
|
|
1067
|
+
): Promise<import('./skr.ustva.js').IUstvaPeriodData> {
|
|
1068
|
+
this.ensureInitialized();
|
|
1069
|
+
const { getUstvaPeriodData } = await import('./skr.ustva.js');
|
|
1070
|
+
return getUstvaPeriodData(this.currentSKRType!, this.getDefaultPolicy(), {
|
|
1071
|
+
dateFrom,
|
|
1072
|
+
dateTo,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
public async setDunning(params: {
|
|
1077
|
+
accountNumber: string;
|
|
1078
|
+
reference: string;
|
|
1079
|
+
dunningLevel?: number;
|
|
1080
|
+
dueDate?: string;
|
|
1081
|
+
}): Promise<void> {
|
|
1082
|
+
this.ensureInitialized();
|
|
1083
|
+
const { setOposMeta } = await import('./skr.opos.js');
|
|
1084
|
+
return setOposMeta({ skrType: this.currentSKRType!, ...params });
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Settle an open item with a bank payment (Skonto and small-difference
|
|
1089
|
+
* write-offs via the core recipe); ordinary v2 posting, fully audited.
|
|
1090
|
+
*/
|
|
1091
|
+
public async settleOpenItem(params: {
|
|
1092
|
+
item: import('./skr.opos.js').IOpenItem;
|
|
1093
|
+
paidCents: number;
|
|
1094
|
+
date: Date;
|
|
1095
|
+
bankAccount?: string;
|
|
1096
|
+
skonto?: { ratePercent: number };
|
|
1097
|
+
diffToleranceCents?: number;
|
|
1098
|
+
description?: string;
|
|
1099
|
+
}): Promise<JournalEntry> {
|
|
1100
|
+
this.ensureInitialized();
|
|
1101
|
+
// a plain underpayment (no Skonto, beyond tolerance) is a PARTIAL
|
|
1102
|
+
// settlement: book only the paid amount and leave the rest open
|
|
1103
|
+
const shortfall = params.item.openCents - params.paidCents;
|
|
1104
|
+
const isPartial =
|
|
1105
|
+
!params.skonto && shortfall > (params.diffToleranceCents ?? 0);
|
|
1106
|
+
const draft = core.bookBankPayment(
|
|
1107
|
+
{
|
|
1108
|
+
date: params.date,
|
|
1109
|
+
direction: params.item.kind === 'debtor' ? 'incoming' : 'outgoing',
|
|
1110
|
+
bankAccount: params.bankAccount,
|
|
1111
|
+
counterpartyAccount: params.item.accountNumber,
|
|
1112
|
+
expectedCents: isPartial ? params.paidCents : params.item.openCents,
|
|
1113
|
+
paidCents: params.paidCents,
|
|
1114
|
+
skonto: params.skonto,
|
|
1115
|
+
diffToleranceCents: params.diffToleranceCents,
|
|
1116
|
+
description:
|
|
1117
|
+
params.description ?? `Ausgleich ${params.item.reference || params.item.accountNumber}`,
|
|
1118
|
+
reference: params.item.reference || undefined,
|
|
1119
|
+
},
|
|
1120
|
+
this.getDefaultPolicy(),
|
|
1121
|
+
);
|
|
1122
|
+
return this.journalPoster.postDraft(draft, { policy: this.getDefaultPolicy() });
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1029
1125
|
/**
|
|
1030
1126
|
* Get current SKR type
|
|
1031
1127
|
*/
|