@kreiseck/kasseneck-api 0.22.0 → 0.25.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/CHANGELOG.md +50 -0
- package/README.md +32 -0
- package/dist/cjs/printing/escpos.d.ts +0 -4
- package/dist/cjs/printing/escpos.js +26 -32
- package/dist/cjs/rechnung/index.d.ts +1 -0
- package/dist/cjs/rechnung/index.js +12 -1
- package/dist/cjs/rechnung/rechnen.d.ts +123 -0
- package/dist/cjs/rechnung/rechnen.js +327 -0
- package/dist/cjs/rechnung/summen.d.ts +3 -2
- package/dist/cjs/rechnung/summen.js +3 -10
- package/dist/cjs/rechnung/vertrag.d.ts +1 -1
- package/dist/cjs/rechnung/vertrag.js +2 -0
- package/dist/esm/printing/escpos.d.ts +0 -4
- package/dist/esm/printing/escpos.js +26 -32
- package/dist/esm/rechnung/index.d.ts +1 -0
- package/dist/esm/rechnung/index.js +1 -0
- package/dist/esm/rechnung/rechnen.d.ts +123 -0
- package/dist/esm/rechnung/rechnen.js +316 -0
- package/dist/esm/rechnung/summen.d.ts +3 -2
- package/dist/esm/rechnung/summen.js +3 -9
- package/dist/esm/rechnung/vertrag.d.ts +1 -1
- package/dist/esm/rechnung/vertrag.js +2 -0
- package/fixtures/hobex-hps-codes.json +1 -1
- package/fixtures/kasse-texte.json +1 -1
- package/fixtures/oberflaeche.json +4 -2
- package/fixtures/position-aus-euro.json +107 -0
- package/fixtures/rechnung-api.schema.json +4 -2
- package/fixtures/rechnung-rechnen-zufall.json +8451 -0
- package/fixtures/rechnung-rechnen.json +304 -0
- package/fixtures/rechnung-texte.json +1 -1
- package/package.json +13 -2
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Der Rechenkern der Rechnung — exakt, in ganzen Zahlen.
|
|
4
|
+
*
|
|
5
|
+
* Eine Zeile ist ein Bruch: Preis (µ€) × Menge (Tausendstel) × Rabattanteil
|
|
6
|
+
* (Hundertstel-Prozent). Multipliziert man die Skalen, ist die kleinste Einheit
|
|
7
|
+
* 10⁻¹⁷ Cent — das passt in keine Gleitkommazahl und in keine 53-Bit-Ganzzahl,
|
|
8
|
+
* also rechnet der Kern mit BigInt. Gerundet wird genau EINMAL je USt-Satz, auf
|
|
9
|
+
* dem Bruch, kaufmaennisch (halbe Einheit vom Nullpunkt weg).
|
|
10
|
+
*
|
|
11
|
+
* Warum das so streng ist: Vier Umsetzungen (Server, dieses Paket, Dart, Panel)
|
|
12
|
+
* mussten bisher dieselbe Reihenfolge von Gleitkomma-Schritten nachbauen, damit
|
|
13
|
+
* Grenzfaelle gleich runden. Mit Ganzzahlen gibt es keine Reihenfolge mehr, an
|
|
14
|
+
* der etwas auseinanderlaufen koennte.
|
|
15
|
+
*
|
|
16
|
+
* Dieser Unterpfad ist rein: kein Transport, kein Zugangsschluessel, keine
|
|
17
|
+
* Abhaengigkeit ausser Typen. Er wird im Browser gebuendelt (Panel) und im
|
|
18
|
+
* Server geladen.
|
|
19
|
+
*
|
|
20
|
+
* Spec: docs/specs/2026-09-17-rechnung-ganzzahlen-design.md § 5.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.RechenFehler = exports.STEUERFREIE_FAELLE = exports.BETRAG_GRENZE_CENTS = void 0;
|
|
24
|
+
exports.rund = rund;
|
|
25
|
+
exports.rechnungRechnen = rechnungRechnen;
|
|
26
|
+
exports.positionAusEuro = positionAusEuro;
|
|
27
|
+
exports.anteiligerPreis = anteiligerPreis;
|
|
28
|
+
exports.satzSchluessel = satzSchluessel;
|
|
29
|
+
exports.satzText = satzText;
|
|
30
|
+
exports.preisText = preisText;
|
|
31
|
+
/** Skala des Zeilenbruchs: 10⁻¹⁷ Cent. */
|
|
32
|
+
const E = 10n ** 17n;
|
|
33
|
+
/** Hoechster Betrag je Zeile und je Rechnung: 999.999.999,99 €. */
|
|
34
|
+
exports.BETRAG_GRENZE_CENTS = 99_999_999_999;
|
|
35
|
+
/** Steuerfaelle, in denen die Rechnung keine Steuer ausweist: jede Zeile zaehlt zu 0 %. */
|
|
36
|
+
exports.STEUERFREIE_FAELLE = Object.freeze([
|
|
37
|
+
'smallBusiness',
|
|
38
|
+
'reverseCharge',
|
|
39
|
+
'igLieferung',
|
|
40
|
+
'exportThirdCountry',
|
|
41
|
+
'domesticReverseCharge',
|
|
42
|
+
'outsideScope',
|
|
43
|
+
]);
|
|
44
|
+
/** Fehler des Kerns — mit Code, Feld und Index, damit die API daraus einen Feldfehler machen kann. */
|
|
45
|
+
class RechenFehler extends Error {
|
|
46
|
+
code;
|
|
47
|
+
feld;
|
|
48
|
+
index;
|
|
49
|
+
constructor(code, nachricht, feld, index) {
|
|
50
|
+
super(nachricht);
|
|
51
|
+
this.name = 'RechenFehler';
|
|
52
|
+
this.code = code;
|
|
53
|
+
this.feld = feld;
|
|
54
|
+
this.index = index;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.RechenFehler = RechenFehler;
|
|
58
|
+
/**
|
|
59
|
+
* `a / b` kaufmaennisch gerundet, halbe Einheit vom Nullpunkt weg; `b` > 0.
|
|
60
|
+
*
|
|
61
|
+
* Ohne Gleitkomma: `(2·|a| + b) / (2·b)` ist genau dann eins groesser, wenn der
|
|
62
|
+
* Rest mindestens die halbe Einheit betraegt.
|
|
63
|
+
*/
|
|
64
|
+
function rund(a, b) {
|
|
65
|
+
const negativ = a < 0n;
|
|
66
|
+
const betrag = negativ ? -a : a;
|
|
67
|
+
const ganz = (2n * betrag + b) / (2n * b);
|
|
68
|
+
return negativ ? -ganz : ganz;
|
|
69
|
+
}
|
|
70
|
+
const GRENZEN = {
|
|
71
|
+
unitPriceMicros: [0, 1_000_000_000_000],
|
|
72
|
+
quantityMilli: [-1_000_000_000_000, 1_000_000_000_000],
|
|
73
|
+
discountBp: [0, 10_000],
|
|
74
|
+
vatRateBp: [0, 10_000],
|
|
75
|
+
};
|
|
76
|
+
function ganzzahl(wert, feld, index) {
|
|
77
|
+
if (typeof wert !== 'number' || !Number.isInteger(wert)) {
|
|
78
|
+
throw new RechenFehler('kein_ganzzahlwert', `items[${index}].${feld} muss eine ganze Zahl sein`, feld, index);
|
|
79
|
+
}
|
|
80
|
+
const [min, max] = GRENZEN[feld];
|
|
81
|
+
if (wert < min || wert > max) {
|
|
82
|
+
throw new RechenFehler('ausserhalb', `items[${index}].${feld} liegt ausserhalb von ${min} … ${max}`, feld, index);
|
|
83
|
+
}
|
|
84
|
+
return wert;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Verteilt die Zeilenbetraege eines Satzes so, dass sie genau die Satzsumme
|
|
88
|
+
* ergeben. Startwert ist die kaufmaennisch gerundete Zeile; der Rest geht
|
|
89
|
+
* einzeln an die Zeilen, bei denen das Runden am meisten weggenommen (bzw.
|
|
90
|
+
* zugegeben) hat. Verglichen wird auf dem Bruch, nie in Gleitkomma; bei
|
|
91
|
+
* Gleichstand bekommt die fruehere Zeile den Cent. Eine Zeile ueber 0 bekommt
|
|
92
|
+
* nie einen Cent — sonst stuende auf dem Blatt ein Betrag, den die Position
|
|
93
|
+
* nicht hat.
|
|
94
|
+
*/
|
|
95
|
+
function verteile(gruppe, feld, faktor, nenner, ziel) {
|
|
96
|
+
const werte = gruppe.map((z) => {
|
|
97
|
+
const zaehler = z.L * faktor;
|
|
98
|
+
return { z, zaehler, wert: rund(zaehler, nenner) };
|
|
99
|
+
});
|
|
100
|
+
let rest = ziel - werte.reduce((s, w) => s + w.wert, 0n);
|
|
101
|
+
if (rest !== 0n) {
|
|
102
|
+
const schritt = rest > 0n ? 1n : -1n;
|
|
103
|
+
const kandidaten = werte
|
|
104
|
+
.filter((w) => w.zaehler !== 0n)
|
|
105
|
+
.sort((a, b) => {
|
|
106
|
+
const da = (a.zaehler - a.wert * nenner) * schritt;
|
|
107
|
+
const db = (b.zaehler - b.wert * nenner) * schritt;
|
|
108
|
+
if (da !== db)
|
|
109
|
+
return db > da ? 1 : -1;
|
|
110
|
+
return a.z.index - b.z.index;
|
|
111
|
+
});
|
|
112
|
+
for (let k = 0; rest !== 0n && kandidaten.length > 0; k = (k + 1) % kandidaten.length) {
|
|
113
|
+
kandidaten[k].wert += schritt;
|
|
114
|
+
rest -= schritt;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const w of werte)
|
|
118
|
+
w.z[feld] = Number(w.wert);
|
|
119
|
+
}
|
|
120
|
+
function rechnungRechnen(positionen, optionen) {
|
|
121
|
+
const steuerfrei = exports.STEUERFREIE_FAELLE.includes(optionen.taxScheme ?? 'normal');
|
|
122
|
+
const bruttoPreise = optionen.priceMode === 'gross' && !steuerfrei;
|
|
123
|
+
const grenze = BigInt(exports.BETRAG_GRENZE_CENTS) * E;
|
|
124
|
+
const zeilen = positionen.map((p, index) => {
|
|
125
|
+
const preis = BigInt(ganzzahl(p.unitPriceMicros, 'unitPriceMicros', index));
|
|
126
|
+
const menge = BigInt(ganzzahl(p.quantityMilli, 'quantityMilli', index));
|
|
127
|
+
const rabatt = BigInt(ganzzahl(p.discountBp ?? 0, 'discountBp', index));
|
|
128
|
+
const satz = ganzzahl(p.vatRateBp ?? 0, 'vatRateBp', index);
|
|
129
|
+
const L = preis * menge * (10000n - rabatt) * 1000000n;
|
|
130
|
+
if ((L < 0n ? -L : L) > grenze) {
|
|
131
|
+
throw new RechenFehler('amount_too_large', `items[${index}] uebersteigt ${exports.BETRAG_GRENZE_CENTS} Cent`, 'unitPriceMicros', index);
|
|
132
|
+
}
|
|
133
|
+
return { index, rateBp: steuerfrei ? 0 : satz, L, netCents: 0, grossCents: 0 };
|
|
134
|
+
});
|
|
135
|
+
const jeSatz = new Map();
|
|
136
|
+
for (const z of zeilen) {
|
|
137
|
+
const gruppe = jeSatz.get(z.rateBp);
|
|
138
|
+
if (gruppe)
|
|
139
|
+
gruppe.push(z);
|
|
140
|
+
else
|
|
141
|
+
jeSatz.set(z.rateBp, [z]);
|
|
142
|
+
}
|
|
143
|
+
const byRate = [];
|
|
144
|
+
for (const [rateBp, gruppe] of jeSatz) {
|
|
145
|
+
const r = BigInt(rateBp);
|
|
146
|
+
const S = gruppe.reduce((s, z) => s + z.L, 0n);
|
|
147
|
+
let netCents;
|
|
148
|
+
let vatCents;
|
|
149
|
+
let grossCents;
|
|
150
|
+
if (bruttoPreise) {
|
|
151
|
+
grossCents = rund(S, E);
|
|
152
|
+
netCents = rund(grossCents * 10000n, 10000n + r);
|
|
153
|
+
vatCents = grossCents - netCents;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
netCents = rund(S, E);
|
|
157
|
+
vatCents = rund(S * r, E * 10000n);
|
|
158
|
+
grossCents = netCents + vatCents;
|
|
159
|
+
}
|
|
160
|
+
if (bruttoPreise) {
|
|
161
|
+
verteile(gruppe, 'netCents', 10000n, E * (10000n + r), netCents);
|
|
162
|
+
verteile(gruppe, 'grossCents', 1n, E, grossCents);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
verteile(gruppe, 'netCents', 1n, E, netCents);
|
|
166
|
+
verteile(gruppe, 'grossCents', 10000n + r, E * 10000n, grossCents);
|
|
167
|
+
}
|
|
168
|
+
byRate.push({
|
|
169
|
+
rateBp,
|
|
170
|
+
netCents: Number(netCents),
|
|
171
|
+
vatCents: Number(vatCents),
|
|
172
|
+
grossCents: Number(grossCents),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
byRate.sort((a, b) => b.rateBp - a.rateBp);
|
|
176
|
+
const summe = (feld) => byRate.reduce((s, r) => s + r[feld], 0);
|
|
177
|
+
const ergebnis = {
|
|
178
|
+
netCents: summe('netCents'),
|
|
179
|
+
vatCents: summe('vatCents'),
|
|
180
|
+
grossCents: summe('grossCents'),
|
|
181
|
+
byRate,
|
|
182
|
+
lines: zeilen.map((z) => ({ netCents: z.netCents, grossCents: z.grossCents, rateBp: z.rateBp })),
|
|
183
|
+
};
|
|
184
|
+
for (const feld of ['netCents', 'vatCents', 'grossCents']) {
|
|
185
|
+
if (Math.abs(ergebnis[feld]) > exports.BETRAG_GRENZE_CENTS) {
|
|
186
|
+
throw new RechenFehler('amount_too_large', `Die Rechnung uebersteigt ${exports.BETRAG_GRENZE_CENTS} Cent (${feld})`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return ergebnis;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Wandelt eine Zahl ueber ihren kuerzesten Dezimaltext in eine Ganzzahl mit
|
|
193
|
+
* `stellen` Nachkommastellen. Kein Gleitkomma-Rechnen: `0.1 * 1000` ist
|
|
194
|
+
* 100.00000000000001, `String(0.1)` dagegen genau "0.1". Mehr Stellen als
|
|
195
|
+
* erlaubt ergeben `null` — auch Gleitkomma-Rauschen wie 0.30000000000000004,
|
|
196
|
+
* das mit 17 Stellen dasteht.
|
|
197
|
+
*/
|
|
198
|
+
function ganzAusDezimaltext(wert, stellen) {
|
|
199
|
+
if (typeof wert !== 'number' || !Number.isFinite(wert))
|
|
200
|
+
return null;
|
|
201
|
+
const treffer = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(String(wert));
|
|
202
|
+
if (!treffer)
|
|
203
|
+
return null;
|
|
204
|
+
const [, vorzeichen = '', ganzTeil = '0', bruchTeil = '', hoch = '0'] = treffer;
|
|
205
|
+
const verschiebung = Number(hoch);
|
|
206
|
+
let ziffern = `${ganzTeil}${bruchTeil}`;
|
|
207
|
+
let nachkomma = bruchTeil.length - verschiebung;
|
|
208
|
+
if (nachkomma < 0) {
|
|
209
|
+
ziffern += '0'.repeat(-nachkomma);
|
|
210
|
+
nachkomma = 0;
|
|
211
|
+
}
|
|
212
|
+
if (nachkomma > stellen) {
|
|
213
|
+
// Nur echte Nullen am Ende duerfen wegfallen (1.20 bei 1 Stelle).
|
|
214
|
+
const zuviel = nachkomma - stellen;
|
|
215
|
+
if (!/^0+$/.test(ziffern.slice(-zuviel)))
|
|
216
|
+
return null;
|
|
217
|
+
ziffern = ziffern.slice(0, -zuviel);
|
|
218
|
+
nachkomma = stellen;
|
|
219
|
+
}
|
|
220
|
+
const skaliert = `${ziffern}${'0'.repeat(stellen - nachkomma)}`;
|
|
221
|
+
const zahl = Number(`${vorzeichen}${skaliert}`);
|
|
222
|
+
return Number.isSafeInteger(zahl) ? zahl : null;
|
|
223
|
+
}
|
|
224
|
+
const EURO_FELDER = ['unitPrice', 'quantity', 'vatRate', 'discountPct'];
|
|
225
|
+
/**
|
|
226
|
+
* Euro-Position → Ganzzahl-Form, verlustfrei oder mit Grund abgelehnt. Die
|
|
227
|
+
* EINE Stelle, die das tut: Panel (beim Lesen), SEPA-Lauf, Umrechnungsskript
|
|
228
|
+
* und das Festschreiben benutzen alle diese Funktion, damit niemand eine
|
|
229
|
+
* zweite Auslegung baut.
|
|
230
|
+
*
|
|
231
|
+
* Eine Position, die schon `unitPriceMicros` traegt, kommt unveraendert
|
|
232
|
+
* zurueck — die Funktion ist die Formweiche, nicht eine blinde Umrechnung.
|
|
233
|
+
*
|
|
234
|
+
* Fehlende Menge und fehlender Satz gelten als 0, weil der Server bisher so
|
|
235
|
+
* abgerechnet hat. Ein fehlender Preis dagegen ist NIE 0: sonst wuerde eine
|
|
236
|
+
* Position, der nur der Preis fehlt, still zur 0-€-Zeile.
|
|
237
|
+
*/
|
|
238
|
+
function positionAusEuro(item) {
|
|
239
|
+
if (typeof item.unitPriceMicros === 'number') {
|
|
240
|
+
return { ok: true, position: item };
|
|
241
|
+
}
|
|
242
|
+
const roh = item.unitPrice;
|
|
243
|
+
if (typeof roh !== 'number' || !Number.isFinite(roh)) {
|
|
244
|
+
return { ok: false, feld: 'unitPrice', grund: 'kein_zahlwert' };
|
|
245
|
+
}
|
|
246
|
+
const micros = ganzAusDezimaltext(Math.abs(roh), 6);
|
|
247
|
+
if (micros === null)
|
|
248
|
+
return { ok: false, feld: 'unitPrice', grund: 'nachkommastellen' };
|
|
249
|
+
if (micros > GRENZEN.unitPriceMicros[1])
|
|
250
|
+
return { ok: false, feld: 'unitPrice', grund: 'ausserhalb' };
|
|
251
|
+
const zahl = (feld, stellen, max) => {
|
|
252
|
+
const w = item[feld];
|
|
253
|
+
if (w === undefined || w === null)
|
|
254
|
+
return { ok: true, wert: 0 };
|
|
255
|
+
if (typeof w !== 'number' || !Number.isFinite(w))
|
|
256
|
+
return { ok: false, grund: 'kein_zahlwert' };
|
|
257
|
+
const ganz = ganzAusDezimaltext(w, stellen);
|
|
258
|
+
if (ganz === null)
|
|
259
|
+
return { ok: false, grund: 'nachkommastellen' };
|
|
260
|
+
if (ganz < (feld === 'quantity' ? -max : 0) || ganz > max)
|
|
261
|
+
return { ok: false, grund: 'ausserhalb' };
|
|
262
|
+
return { ok: true, wert: ganz };
|
|
263
|
+
};
|
|
264
|
+
const menge = zahl('quantity', 3, GRENZEN.quantityMilli[1]);
|
|
265
|
+
if (!menge.ok)
|
|
266
|
+
return { ok: false, feld: 'quantity', grund: menge.grund };
|
|
267
|
+
const satz = zahl('vatRate', 2, 10_000);
|
|
268
|
+
if (!satz.ok)
|
|
269
|
+
return { ok: false, feld: 'vatRate', grund: satz.grund };
|
|
270
|
+
const rabatt = zahl('discountPct', 2, 10_000);
|
|
271
|
+
if (!rabatt.ok)
|
|
272
|
+
return { ok: false, feld: 'discountPct', grund: rabatt.grund };
|
|
273
|
+
const rest = { ...item };
|
|
274
|
+
for (const feld of EURO_FELDER)
|
|
275
|
+
delete rest[feld];
|
|
276
|
+
return {
|
|
277
|
+
ok: true,
|
|
278
|
+
position: {
|
|
279
|
+
...rest,
|
|
280
|
+
unitPriceMicros: micros,
|
|
281
|
+
quantityMilli: roh < 0 ? -menge.wert : menge.wert,
|
|
282
|
+
discountBp: rabatt.wert,
|
|
283
|
+
vatRateBp: satz.wert,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Anteiliger Preis fuer einen Teilzeitraum (erste Rechnung eines Abos).
|
|
289
|
+
* War der Ursprungspreis centgenau, bleibt auch der Anteil centgenau — sonst
|
|
290
|
+
* stuende auf der ersten Rechnung ein Preis mit sechs Stellen, wo bisher zwei
|
|
291
|
+
* standen.
|
|
292
|
+
*/
|
|
293
|
+
function anteiligerPreis(unitPriceMicros, monate, intervall) {
|
|
294
|
+
const micros = BigInt(unitPriceMicros);
|
|
295
|
+
const anteil = rund(micros * BigInt(monate), BigInt(intervall));
|
|
296
|
+
if (unitPriceMicros % 10_000 !== 0)
|
|
297
|
+
return Number(anteil);
|
|
298
|
+
return Number(rund(anteil, 10000n) * 10000n);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Schluessel eines USt-Satzes in den gespeicherten Maps (`creditedCents.byRate`,
|
|
302
|
+
* `invoice_stats.vatByRate`): der Prozenttext mit Punkt, genau wie
|
|
303
|
+
* `String(satz)` ihn bisher gebildet hat. NICHT fuer die Anzeige — dafuer gibt
|
|
304
|
+
* es `satzText`.
|
|
305
|
+
*/
|
|
306
|
+
function satzSchluessel(rateBp) {
|
|
307
|
+
return String(rateBp / 100);
|
|
308
|
+
}
|
|
309
|
+
/** Ein USt-Satz fuer die Anzeige: oesterreichisch mit Komma, ohne nachlaufende Nullen. */
|
|
310
|
+
function satzText(rateBp) {
|
|
311
|
+
return satzSchluessel(rateBp).replace('.', ',');
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Ein Einzelpreis fuer die Anzeige: mindestens zwei, hoechstens sechs
|
|
315
|
+
* Nachkommastellen, Tausenderpunkt, Komma als Trennzeichen — in jeder Sprache
|
|
316
|
+
* oesterreichisch, wie die Betraege auf dem Blatt.
|
|
317
|
+
*/
|
|
318
|
+
function preisText(unitPriceMicros) {
|
|
319
|
+
const negativ = unitPriceMicros < 0;
|
|
320
|
+
const betrag = Math.abs(unitPriceMicros);
|
|
321
|
+
const ganz = Math.trunc(betrag / 1_000_000);
|
|
322
|
+
let bruch = String(betrag % 1_000_000).padStart(6, '0');
|
|
323
|
+
while (bruch.length > 2 && bruch.endsWith('0'))
|
|
324
|
+
bruch = bruch.slice(0, -1);
|
|
325
|
+
const mitPunkten = String(ganz).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
326
|
+
return `${negativ ? '-' : ''}${mitPunkten},${bruch}`;
|
|
327
|
+
}
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { InvoiceTotals } from './typen.js';
|
|
22
22
|
import type { PriceMode, TaxScheme } from './vertrag.js';
|
|
23
|
+
import { STEUERFREIE_FAELLE } from './rechnen.js';
|
|
24
|
+
/** Aus dem Kern uebernommen, damit Kern und Huelle nie auseinanderlaufen. */
|
|
25
|
+
export { STEUERFREIE_FAELLE };
|
|
23
26
|
/** Was fuer die Summe zaehlt — `InvoiceItemInput` passt unveraendert. */
|
|
24
27
|
export interface SummenPosition {
|
|
25
28
|
quantity: number;
|
|
@@ -27,8 +30,6 @@ export interface SummenPosition {
|
|
|
27
30
|
vatRate: number;
|
|
28
31
|
discountPct?: number;
|
|
29
32
|
}
|
|
30
|
-
/** Steuerfaelle, in denen die Rechnung keine Steuer ausweist: jede Zeile zaehlt zu 0 %. */
|
|
31
|
-
export declare const STEUERFREIE_FAELLE: readonly TaxScheme[];
|
|
32
33
|
/**
|
|
33
34
|
* Netto, USt und Brutto einer Rechnung in Cent, je Satz absteigend — dieselbe
|
|
34
35
|
* Form wie `invoice.totals` in den Antworten.
|
|
@@ -22,15 +22,8 @@
|
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.STEUERFREIE_FAELLE = void 0;
|
|
24
24
|
exports.rechnungSummen = rechnungSummen;
|
|
25
|
-
|
|
26
|
-
exports.STEUERFREIE_FAELLE
|
|
27
|
-
'smallBusiness',
|
|
28
|
-
'reverseCharge',
|
|
29
|
-
'igLieferung',
|
|
30
|
-
'exportThirdCountry',
|
|
31
|
-
'domesticReverseCharge',
|
|
32
|
-
'outsideScope',
|
|
33
|
-
]);
|
|
25
|
+
const rechnen_js_1 = require("./rechnen.js");
|
|
26
|
+
Object.defineProperty(exports, "STEUERFREIE_FAELLE", { enumerable: true, get: function () { return rechnen_js_1.STEUERFREIE_FAELLE; } });
|
|
34
27
|
/** Auf Cent gerundete Euro — dieselbe Rundung wie am Server (halber Cent vom Nullpunkt weg). */
|
|
35
28
|
function euroRund(n) {
|
|
36
29
|
return ((n < 0 ? -1 : 1) * Math.round((Math.abs(n) + Number.EPSILON) * 100)) / 100;
|
|
@@ -51,7 +44,7 @@ function centRund(x) {
|
|
|
51
44
|
* ausweist.
|
|
52
45
|
*/
|
|
53
46
|
function rechnungSummen(items, priceMode, taxScheme = 'normal') {
|
|
54
|
-
const steuerfrei =
|
|
47
|
+
const steuerfrei = rechnen_js_1.STEUERFREIE_FAELLE.includes(taxScheme);
|
|
55
48
|
const bruttoPreise = priceMode === 'gross' && !steuerfrei;
|
|
56
49
|
// Ungerundete Zeilen je Satz, in Euro wie am Server.
|
|
57
50
|
const jeSatz = new Map();
|
|
@@ -24,7 +24,7 @@ export type RechnungAufruf = (typeof RECHNUNG_AUFRUFE)[number];
|
|
|
24
24
|
* Stabile Fehlercodes. Das Fremdsystem verzweigt am Code, der Text darf sich
|
|
25
25
|
* aendern. `validation` traegt zusaetzlich `errors: [{ field, message }]`.
|
|
26
26
|
*/
|
|
27
|
-
export declare const INVOICE_ERROR_CODES: readonly ["validation", "idempotency_conflict", "module_inactive", "customer_not_found", "customer_exists", "short_code_taken", "short_code_immutable", "recipient_required", "invoice_requirements_missing", "invoice_not_found", "invoice_ambiguous", "not_cancellable", "partial_credit_exists", "credit_exceeds_invoice", "einvoice_incomplete", "invoice_api_not_enabled", "invoice_setup_incomplete", "language_not_allowed", "brand_not_found", "not_payable", "payment_exceeds_invoice", "tax_scheme_mismatch", "vat_rate_not_in_country", "reverse_charge_reason_required", "reverse_charge_threshold", "mixed_supply_not_allowed", "oss_not_enabled"];
|
|
27
|
+
export declare const INVOICE_ERROR_CODES: readonly ["validation", "idempotency_conflict", "module_inactive", "customer_not_found", "customer_exists", "short_code_taken", "short_code_immutable", "recipient_required", "invoice_requirements_missing", "invoice_not_found", "invoice_ambiguous", "not_cancellable", "partial_credit_exists", "credit_exceeds_invoice", "einvoice_incomplete", "invoice_api_not_enabled", "invoice_setup_incomplete", "language_not_allowed", "brand_not_found", "not_payable", "payment_exceeds_invoice", "tax_scheme_mismatch", "vat_rate_not_in_country", "reverse_charge_reason_required", "reverse_charge_threshold", "mixed_supply_not_allowed", "oss_not_enabled", "einvoice_unavailable", "amount_too_large"];
|
|
28
28
|
export type InvoiceErrorCode = (typeof INVOICE_ERROR_CODES)[number];
|
|
29
29
|
/** Gruende einer Gutschrift. Der Server druckt den deutschen Anzeigetext. */
|
|
30
30
|
export declare const CREDIT_NOTE_REASONS: readonly ["cancellation", "price_reduction", "return", "incorrect_invoice", "other"];
|
|
@@ -69,6 +69,8 @@ exports.INVOICE_ERROR_CODES = [
|
|
|
69
69
|
'reverse_charge_threshold',
|
|
70
70
|
'mixed_supply_not_allowed',
|
|
71
71
|
'oss_not_enabled',
|
|
72
|
+
'einvoice_unavailable', // zu dieser Rechnung entsteht keine E-Rechnung, Grund in `reason`
|
|
73
|
+
'amount_too_large', // Betrag ueber der Grenze des Ganzzahlkerns; erst ab dessen Umstieg gesendet
|
|
72
74
|
];
|
|
73
75
|
/** Gruende einer Gutschrift. Der Server druckt den deutschen Anzeigetext. */
|
|
74
76
|
exports.CREDIT_NOTE_REASONS = [
|
|
@@ -208,10 +208,6 @@ export declare function escPosSetGlobalFont(doc: EscPosDocument, font: PosFont |
|
|
|
208
208
|
* weglaesst, schaltet fett also ab.
|
|
209
209
|
*/
|
|
210
210
|
export declare function escPosSetStyles(doc: EscPosDocument, styles?: PosStyles): void;
|
|
211
|
-
/**
|
|
212
|
-
* Interner Textdruck: absolute Position setzen (`ESC $`), Stil senden, Bytes
|
|
213
|
-
* ausgeben. [colInd]/[colWidth] beschreiben die Spalte im Zwoelftel-Raster.
|
|
214
|
-
*/
|
|
215
211
|
/**
|
|
216
212
|
* Wortweiser Umbruch von (kodierten) Text-Bytes auf hoechstens `max` Zeichen
|
|
217
213
|
* je Zeile: geschnitten wird am letzten Leerzeichen innerhalb der Grenze
|
|
@@ -319,10 +319,6 @@ export function escPosSetStyles(doc, styles = {}) {
|
|
|
319
319
|
alt.codeTable = codepage;
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
|
-
/**
|
|
323
|
-
* Interner Textdruck: absolute Position setzen (`ESC $`), Stil senden, Bytes
|
|
324
|
-
* ausgeben. [colInd]/[colWidth] beschreiben die Spalte im Zwoelftel-Raster.
|
|
325
|
-
*/
|
|
326
322
|
/**
|
|
327
323
|
* Wortweiser Umbruch von (kodierten) Text-Bytes auf hoechstens `max` Zeichen
|
|
328
324
|
* je Zeile: geschnitten wird am letzten Leerzeichen innerhalb der Grenze
|
|
@@ -424,27 +420,38 @@ export function wortzeilenText(text, max) {
|
|
|
424
420
|
out.push(rest.replace(/\u00a0/g, ' '));
|
|
425
421
|
return out;
|
|
426
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Interner Textdruck: Stil senden (Ausrichtung zuerst), absolute Position
|
|
425
|
+
* setzen (`ESC $`, ausser bei einer vollen Zeile), Bytes ausgeben. [colInd]/
|
|
426
|
+
* [colWidth] beschreiben die Spalte im Zwoelftel-Raster.
|
|
427
|
+
*/
|
|
427
428
|
function textIntern(doc, textBytes, optionen = {}) {
|
|
428
429
|
const stile = vollstaendigeStile(optionen.styles);
|
|
429
430
|
const colInd = optionen.colInd ?? 0;
|
|
430
431
|
const colWidth = optionen.colWidth ?? 12;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
432
|
+
// Ausrichtung vor Position -- `ESC a` gilt nur am Zeilenanfang, nach `ESC $`
|
|
433
|
+
// verwirft der Drucker ihn. Zwilling von `_text` in generator.dart.
|
|
434
|
+
escPosSetStyles(doc, optionen.styles);
|
|
435
|
+
// Eine volle Zeile beginnt am linken Rand; ein Positionsbefehl dafuer waere
|
|
436
|
+
// nicht nur ueberfluessig, er entwertet die Ausrichtung.
|
|
437
|
+
if (!(colInd === 0 && colWidth === 12)) {
|
|
438
|
+
const breiteJeZeichen = zeichenBreite(doc, stile);
|
|
439
|
+
let von = spaltenPosition(doc, colInd);
|
|
440
|
+
if (colWidth !== 12) {
|
|
441
|
+
const bis = spaltenPosition(doc, colInd + colWidth) - doc.spaceBetweenRows;
|
|
442
|
+
const textLaenge = textBytes.length * breiteJeZeichen;
|
|
443
|
+
if (stile.align === 'right') {
|
|
444
|
+
von = bis - textLaenge;
|
|
445
|
+
}
|
|
446
|
+
else if (stile.align === 'center') {
|
|
447
|
+
von = von + (bis - von) / 2 - textLaenge / 2;
|
|
448
|
+
}
|
|
449
|
+
if (von < 0)
|
|
450
|
+
von = 0;
|
|
441
451
|
}
|
|
442
|
-
|
|
443
|
-
|
|
452
|
+
const position = runden(von);
|
|
453
|
+
anhaengen(doc, [...C_POS, position & 0xff, (position >> 8) & 0xff]);
|
|
444
454
|
}
|
|
445
|
-
const position = runden(von);
|
|
446
|
-
anhaengen(doc, [...C_POS, position & 0xff, (position >> 8) & 0xff]);
|
|
447
|
-
escPosSetStyles(doc, optionen.styles);
|
|
448
455
|
anhaengen(doc, textBytes);
|
|
449
456
|
}
|
|
450
457
|
/**
|
|
@@ -681,13 +688,6 @@ export function escPosQrCode(doc, text, options = {}) {
|
|
|
681
688
|
}
|
|
682
689
|
if (drucken)
|
|
683
690
|
anhaengen(doc, qrCodeBytes(text, qrOptionen));
|
|
684
|
-
// Ausrichtung SOFORT zuruecksetzen: `ESC a` gilt nur am Zeilenanfang. Die
|
|
685
|
-
// Spalten-Positionierung (`ESC $`) schickt ihren Reset sonst mitten in der
|
|
686
|
-
// Zeile — Epson ignoriert ihn, und alles nach dem QR rueckte nach rechts
|
|
687
|
-
// (belegt am TM-T20, 2026-08-20).
|
|
688
|
-
if ((options.align ?? 'center') !== 'left') {
|
|
689
|
-
escPosSetStyles(doc, { align: 'left' });
|
|
690
|
-
}
|
|
691
691
|
}
|
|
692
692
|
/**
|
|
693
693
|
* Der Bildweg: ein fertiges QR-Raster als Rasterbild (`GS v 0`) — der
|
|
@@ -745,9 +745,6 @@ export function escPosQrRaster(doc, matrix, options = {}) {
|
|
|
745
745
|
anhaengen(doc, rasterBildKopf(breite, breite));
|
|
746
746
|
anhaengen(doc, daten);
|
|
747
747
|
anhaengen(doc, [ZEILENUMBRUCH]);
|
|
748
|
-
if ((options.align ?? 'center') !== 'left') {
|
|
749
|
-
escPosSetStyles(doc, { align: 'left' });
|
|
750
|
-
}
|
|
751
748
|
}
|
|
752
749
|
/**
|
|
753
750
|
* Punkte je Modul, mit denen `escPosQrRaster` ein Raster dieser Groesse auf
|
|
@@ -820,7 +817,4 @@ export function escPosRasterBild(doc, bild, options = {}) {
|
|
|
820
817
|
escPosSetStyles(doc, { align: options.align ?? 'center' });
|
|
821
818
|
anhaengen(doc, rasterBildKopf(bild.breite, bild.hoehe));
|
|
822
819
|
anhaengen(doc, daten);
|
|
823
|
-
if ((options.align ?? 'center') !== 'left') {
|
|
824
|
-
escPosSetStyles(doc, { align: 'left' });
|
|
825
|
-
}
|
|
826
820
|
}
|
|
@@ -17,4 +17,5 @@ export { cancelInvoice, createCreditNote, createCustomer, getCustomer, getInvoic
|
|
|
17
17
|
export type { Brand, CancelInvoiceRequest, CancelResult, CreditNoteRequest, CreditNoteResult, Customer, CustomerInput, CustomerPage, CustomerSearch, EInvoiceStatus, Invoice, InvoiceDetail, InvoiceItem, InvoiceItemInput, InvoiceListQuery, InvoicePage, InvoiceRecipient, InvoiceSetupGap, InvoiceSetupStatus, InvoiceTotals, InvoiceNotice, InvoicePayment, InvoicePreview, InvoiceRateTotals, IssueInvoiceRequest, IssueResult, PaymentInput, PreviewResult, RecordPaymentRequest, RecordPaymentResult, } from './typen.js';
|
|
18
18
|
export { RECHNUNG_TEXTE, rechnungText, type RechnungTextSchluessel } from './texte.js';
|
|
19
19
|
export { rechnungSummen, STEUERFREIE_FAELLE, type SummenPosition } from './summen.js';
|
|
20
|
+
export { anteiligerPreis, BETRAG_GRENZE_CENTS, positionAusEuro, preisText, RechenFehler, rechnungRechnen, rund, satzSchluessel, satzText, type RechenErgebnis, type RechenFehlerCode, type RechenOptionen, type RechenPosition, type SatzSumme, type Umwandlung, type UmwandlungsGrund, type ZeilenBetrag, } from './rechnen.js';
|
|
20
21
|
export { RECHNUNG_VERTRAG_VERSION, RECHNUNG_AUFRUFE, INVOICE_ERROR_CODES, CREDIT_NOTE_REASONS, TAX_SCHEMES, PRICE_MODES, VAT_RATES, CUSTOMER_TYPES, INVOICE_LIST_STATUS, DOC_TYPES, EINVOICE_FORMATS, INVOICE_LANGUAGES, INVOICE_PAYMENT_METHODS, ITEM_KINDS, REVERSE_CHARGE_REASONS, INVOICE_NOTICE_CODES, INVOICE_UNITS, RECHNUNG_EINHEITEN_CODES, INVOICE_SETUP_REQUIREMENTS, KUNDE_FELDER, POSITION_FELDER, RECHNUNG_ANFRAGEN, RECHNUNG_GENAU_EINS, RECHNUNG_MINDESTENS_EINS, type RechnungAufruf, type InvoiceErrorCode, type CreditNoteReason, type TaxScheme, type PriceMode, type VatRatePercent, type CustomerType, type InvoiceListStatus, type DocType, type EInvoiceFormat, type InvoiceLanguage, type InvoicePaymentMethod, type ItemKind, type ReverseChargeReason, type InvoiceNoticeCode, type InvoiceUnit, type InvoiceSetupRequirement, type Format, type Feld, } from './vertrag.js';
|
|
@@ -16,4 +16,5 @@ export { istRechnungFehler, rechnungFehlerCode, rechnungFeldFehler, } from './fe
|
|
|
16
16
|
export { cancelInvoice, createCreditNote, createCustomer, getCustomer, getInvoice, getInvoicePdf, getInvoiceSetupStatus, getInvoiceXml, issueInvoice, listBrands, listInvoices, previewInvoice, recordInvoicePayment, searchCustomers, updateCustomer, } from './endpunkte.js';
|
|
17
17
|
export { RECHNUNG_TEXTE, rechnungText } from './texte.js';
|
|
18
18
|
export { rechnungSummen, STEUERFREIE_FAELLE } from './summen.js';
|
|
19
|
+
export { anteiligerPreis, BETRAG_GRENZE_CENTS, positionAusEuro, preisText, RechenFehler, rechnungRechnen, rund, satzSchluessel, satzText, } from './rechnen.js';
|
|
19
20
|
export { RECHNUNG_VERTRAG_VERSION, RECHNUNG_AUFRUFE, INVOICE_ERROR_CODES, CREDIT_NOTE_REASONS, TAX_SCHEMES, PRICE_MODES, VAT_RATES, CUSTOMER_TYPES, INVOICE_LIST_STATUS, DOC_TYPES, EINVOICE_FORMATS, INVOICE_LANGUAGES, INVOICE_PAYMENT_METHODS, ITEM_KINDS, REVERSE_CHARGE_REASONS, INVOICE_NOTICE_CODES, INVOICE_UNITS, RECHNUNG_EINHEITEN_CODES, INVOICE_SETUP_REQUIREMENTS, KUNDE_FELDER, POSITION_FELDER, RECHNUNG_ANFRAGEN, RECHNUNG_GENAU_EINS, RECHNUNG_MINDESTENS_EINS, } from './vertrag.js';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Der Rechenkern der Rechnung — exakt, in ganzen Zahlen.
|
|
3
|
+
*
|
|
4
|
+
* Eine Zeile ist ein Bruch: Preis (µ€) × Menge (Tausendstel) × Rabattanteil
|
|
5
|
+
* (Hundertstel-Prozent). Multipliziert man die Skalen, ist die kleinste Einheit
|
|
6
|
+
* 10⁻¹⁷ Cent — das passt in keine Gleitkommazahl und in keine 53-Bit-Ganzzahl,
|
|
7
|
+
* also rechnet der Kern mit BigInt. Gerundet wird genau EINMAL je USt-Satz, auf
|
|
8
|
+
* dem Bruch, kaufmaennisch (halbe Einheit vom Nullpunkt weg).
|
|
9
|
+
*
|
|
10
|
+
* Warum das so streng ist: Vier Umsetzungen (Server, dieses Paket, Dart, Panel)
|
|
11
|
+
* mussten bisher dieselbe Reihenfolge von Gleitkomma-Schritten nachbauen, damit
|
|
12
|
+
* Grenzfaelle gleich runden. Mit Ganzzahlen gibt es keine Reihenfolge mehr, an
|
|
13
|
+
* der etwas auseinanderlaufen koennte.
|
|
14
|
+
*
|
|
15
|
+
* Dieser Unterpfad ist rein: kein Transport, kein Zugangsschluessel, keine
|
|
16
|
+
* Abhaengigkeit ausser Typen. Er wird im Browser gebuendelt (Panel) und im
|
|
17
|
+
* Server geladen.
|
|
18
|
+
*
|
|
19
|
+
* Spec: docs/specs/2026-09-17-rechnung-ganzzahlen-design.md § 5.
|
|
20
|
+
*/
|
|
21
|
+
import type { PriceMode, TaxScheme } from './vertrag.js';
|
|
22
|
+
/** Hoechster Betrag je Zeile und je Rechnung: 999.999.999,99 €. */
|
|
23
|
+
export declare const BETRAG_GRENZE_CENTS = 99999999999;
|
|
24
|
+
/** Steuerfaelle, in denen die Rechnung keine Steuer ausweist: jede Zeile zaehlt zu 0 %. */
|
|
25
|
+
export declare const STEUERFREIE_FAELLE: readonly TaxScheme[];
|
|
26
|
+
/** Eine Position in gespeicherter Form — alle Werte ganzzahlig. */
|
|
27
|
+
export interface RechenPosition {
|
|
28
|
+
/** Einzelpreis in Millionstel Euro, 0 … 10¹². */
|
|
29
|
+
unitPriceMicros: number;
|
|
30
|
+
/** Menge in Tausendstel; negativ = Abzugszeile, 0 = Textzeile. */
|
|
31
|
+
quantityMilli: number;
|
|
32
|
+
/** Rabatt in Hundertstel-Prozent, 0 … 10000 (ohne Angabe 0). */
|
|
33
|
+
discountBp?: number;
|
|
34
|
+
/** USt-Satz in Hundertstel-Prozent, 0 … 10000 (ohne Angabe 0). */
|
|
35
|
+
vatRateBp?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface RechenOptionen {
|
|
38
|
+
priceMode: PriceMode;
|
|
39
|
+
/** Ohne Angabe `normal`. */
|
|
40
|
+
taxScheme?: TaxScheme;
|
|
41
|
+
}
|
|
42
|
+
export interface SatzSumme {
|
|
43
|
+
rateBp: number;
|
|
44
|
+
netCents: number;
|
|
45
|
+
vatCents: number;
|
|
46
|
+
grossCents: number;
|
|
47
|
+
}
|
|
48
|
+
export interface ZeilenBetrag {
|
|
49
|
+
netCents: number;
|
|
50
|
+
grossCents: number;
|
|
51
|
+
/** Der Satz, mit dem die Zeile gerechnet wurde (bei steuerfreiem Fall 0). */
|
|
52
|
+
rateBp: number;
|
|
53
|
+
}
|
|
54
|
+
export interface RechenErgebnis {
|
|
55
|
+
netCents: number;
|
|
56
|
+
vatCents: number;
|
|
57
|
+
grossCents: number;
|
|
58
|
+
/** Absteigend nach `rateBp`. */
|
|
59
|
+
byRate: SatzSumme[];
|
|
60
|
+
/** Je Position, in der Reihenfolge der Eingabe. */
|
|
61
|
+
lines: ZeilenBetrag[];
|
|
62
|
+
}
|
|
63
|
+
export type RechenFehlerCode = 'amount_too_large' | 'kein_ganzzahlwert' | 'ausserhalb';
|
|
64
|
+
/** Fehler des Kerns — mit Code, Feld und Index, damit die API daraus einen Feldfehler machen kann. */
|
|
65
|
+
export declare class RechenFehler extends Error {
|
|
66
|
+
readonly code: RechenFehlerCode;
|
|
67
|
+
readonly feld?: string;
|
|
68
|
+
readonly index?: number;
|
|
69
|
+
constructor(code: RechenFehlerCode, nachricht: string, feld?: string, index?: number);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `a / b` kaufmaennisch gerundet, halbe Einheit vom Nullpunkt weg; `b` > 0.
|
|
73
|
+
*
|
|
74
|
+
* Ohne Gleitkomma: `(2·|a| + b) / (2·b)` ist genau dann eins groesser, wenn der
|
|
75
|
+
* Rest mindestens die halbe Einheit betraegt.
|
|
76
|
+
*/
|
|
77
|
+
export declare function rund(a: bigint, b: bigint): bigint;
|
|
78
|
+
export declare function rechnungRechnen(positionen: readonly RechenPosition[], optionen: RechenOptionen): RechenErgebnis;
|
|
79
|
+
export type UmwandlungsGrund = 'kein_zahlwert' | 'nachkommastellen' | 'ausserhalb';
|
|
80
|
+
export type Umwandlung = {
|
|
81
|
+
ok: true;
|
|
82
|
+
position: RechenPosition & Record<string, unknown>;
|
|
83
|
+
} | {
|
|
84
|
+
ok: false;
|
|
85
|
+
feld: 'unitPrice' | 'quantity' | 'vatRate' | 'discountPct';
|
|
86
|
+
grund: UmwandlungsGrund;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Euro-Position → Ganzzahl-Form, verlustfrei oder mit Grund abgelehnt. Die
|
|
90
|
+
* EINE Stelle, die das tut: Panel (beim Lesen), SEPA-Lauf, Umrechnungsskript
|
|
91
|
+
* und das Festschreiben benutzen alle diese Funktion, damit niemand eine
|
|
92
|
+
* zweite Auslegung baut.
|
|
93
|
+
*
|
|
94
|
+
* Eine Position, die schon `unitPriceMicros` traegt, kommt unveraendert
|
|
95
|
+
* zurueck — die Funktion ist die Formweiche, nicht eine blinde Umrechnung.
|
|
96
|
+
*
|
|
97
|
+
* Fehlende Menge und fehlender Satz gelten als 0, weil der Server bisher so
|
|
98
|
+
* abgerechnet hat. Ein fehlender Preis dagegen ist NIE 0: sonst wuerde eine
|
|
99
|
+
* Position, der nur der Preis fehlt, still zur 0-€-Zeile.
|
|
100
|
+
*/
|
|
101
|
+
export declare function positionAusEuro(item: Record<string, unknown>): Umwandlung;
|
|
102
|
+
/**
|
|
103
|
+
* Anteiliger Preis fuer einen Teilzeitraum (erste Rechnung eines Abos).
|
|
104
|
+
* War der Ursprungspreis centgenau, bleibt auch der Anteil centgenau — sonst
|
|
105
|
+
* stuende auf der ersten Rechnung ein Preis mit sechs Stellen, wo bisher zwei
|
|
106
|
+
* standen.
|
|
107
|
+
*/
|
|
108
|
+
export declare function anteiligerPreis(unitPriceMicros: number, monate: number, intervall: number): number;
|
|
109
|
+
/**
|
|
110
|
+
* Schluessel eines USt-Satzes in den gespeicherten Maps (`creditedCents.byRate`,
|
|
111
|
+
* `invoice_stats.vatByRate`): der Prozenttext mit Punkt, genau wie
|
|
112
|
+
* `String(satz)` ihn bisher gebildet hat. NICHT fuer die Anzeige — dafuer gibt
|
|
113
|
+
* es `satzText`.
|
|
114
|
+
*/
|
|
115
|
+
export declare function satzSchluessel(rateBp: number): string;
|
|
116
|
+
/** Ein USt-Satz fuer die Anzeige: oesterreichisch mit Komma, ohne nachlaufende Nullen. */
|
|
117
|
+
export declare function satzText(rateBp: number): string;
|
|
118
|
+
/**
|
|
119
|
+
* Ein Einzelpreis fuer die Anzeige: mindestens zwei, hoechstens sechs
|
|
120
|
+
* Nachkommastellen, Tausenderpunkt, Komma als Trennzeichen — in jeder Sprache
|
|
121
|
+
* oesterreichisch, wie die Betraege auf dem Blatt.
|
|
122
|
+
*/
|
|
123
|
+
export declare function preisText(unitPriceMicros: number): string;
|