@kreiseck/kasseneck-api 1.11.0 → 1.12.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 +21 -0
- package/README.md +2 -1
- package/dist/cjs/client/aufrufe.d.ts +4 -4
- package/dist/cjs/client/aufrufe.js +15 -0
- package/dist/cjs/client/receipts.d.ts +5 -0
- package/dist/cjs/client/receipts.js +65 -6
- package/dist/cjs/enums/voucher-action.d.ts +2 -0
- package/dist/cjs/enums/voucher-action.js +2 -0
- package/dist/cjs/enums/voucher-type.d.ts +10 -0
- package/dist/cjs/enums/voucher-type.js +10 -0
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +9 -3
- package/dist/cjs/models/index.d.ts +3 -3
- package/dist/cjs/models/index.js +7 -2
- package/dist/cjs/models/receipt-errors.d.ts +1 -1
- package/dist/cjs/models/receipt-errors.js +2 -0
- package/dist/cjs/models/receipt-item.d.ts +11 -3
- package/dist/cjs/models/receipt-item.js +22 -1
- package/dist/cjs/models/receipt.d.ts +31 -1
- package/dist/cjs/models/receipt.js +45 -5
- package/dist/cjs/models/voucher.d.ts +57 -4
- package/dist/cjs/models/voucher.js +111 -3
- package/dist/cjs/partner/fehler.d.ts +1 -1
- package/dist/cjs/partner/fehler.js +2 -0
- package/dist/cjs/partner/index.d.ts +1 -1
- package/dist/cjs/partner/typen.d.ts +7 -1
- package/dist/cjs/partner/typen.js +3 -0
- package/dist/cjs/pos/client.d.ts +2 -1
- package/dist/cjs/pos/client.js +3 -1
- package/dist/cjs/pos/gutschein.d.ts +90 -0
- package/dist/cjs/pos/gutschein.js +164 -0
- package/dist/cjs/pos/index.d.ts +3 -2
- package/dist/cjs/pos/index.js +13 -2
- package/dist/cjs/pos/settings.d.ts +3 -0
- package/dist/cjs/pos/settings.js +12 -10
- package/dist/cjs/pos/texte.d.ts +156 -0
- package/dist/cjs/pos/texte.js +72 -1
- package/dist/cjs/react/index.js +8 -2
- package/dist/cjs/receipt/due.d.ts +83 -2
- package/dist/cjs/receipt/due.js +202 -36
- package/dist/cjs/receipt/grid.js +13 -1
- package/dist/cjs/receipt/gutschein-plan.d.ts +104 -0
- package/dist/cjs/receipt/gutschein-plan.js +360 -0
- package/dist/cjs/receipt/index.d.ts +1 -1
- package/dist/cjs/receipt/index.js +3 -0
- package/dist/cjs/receipt/layout.js +186 -10
- package/dist/cjs/register/pairing.d.ts +21 -1
- package/dist/cjs/register/pairing.js +17 -0
- package/dist/cjs/stored/bon-layout.d.ts +8 -0
- package/dist/cjs/stored/bon-layout.js +23 -0
- package/dist/cjs/stored/draht.d.ts +3 -1
- package/dist/cjs/stored/draht.js +53 -6
- package/dist/cjs/stored/einstellungen.js +2 -1
- package/dist/cjs/stored/gutscheine.d.ts +3 -0
- package/dist/cjs/stored/gutscheine.js +60 -0
- package/dist/cjs/stored/index.js +2 -17
- package/dist/cjs/stored/vokabular.d.ts +6 -4
- package/dist/cjs/stored/vokabular.js +48 -2
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client/aufrufe.d.ts +4 -4
- package/dist/esm/client/aufrufe.js +15 -0
- package/dist/esm/client/receipts.d.ts +5 -0
- package/dist/esm/client/receipts.js +66 -8
- package/dist/esm/enums/voucher-action.d.ts +2 -0
- package/dist/esm/enums/voucher-action.js +2 -0
- package/dist/esm/enums/voucher-type.d.ts +10 -0
- package/dist/esm/enums/voucher-type.js +10 -0
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +3 -3
- package/dist/esm/models/index.d.ts +3 -3
- package/dist/esm/models/index.js +3 -3
- package/dist/esm/models/receipt-errors.d.ts +1 -1
- package/dist/esm/models/receipt-errors.js +2 -0
- package/dist/esm/models/receipt-item.d.ts +11 -3
- package/dist/esm/models/receipt-item.js +20 -1
- package/dist/esm/models/receipt.d.ts +31 -1
- package/dist/esm/models/receipt.js +44 -5
- package/dist/esm/models/voucher.d.ts +57 -4
- package/dist/esm/models/voucher.js +108 -3
- package/dist/esm/partner/fehler.d.ts +1 -1
- package/dist/esm/partner/fehler.js +2 -0
- package/dist/esm/partner/index.d.ts +1 -1
- package/dist/esm/partner/typen.d.ts +7 -1
- package/dist/esm/partner/typen.js +3 -0
- package/dist/esm/pos/client.d.ts +2 -1
- package/dist/esm/pos/client.js +3 -1
- package/dist/esm/pos/gutschein.d.ts +90 -0
- package/dist/esm/pos/gutschein.js +158 -0
- package/dist/esm/pos/index.d.ts +3 -2
- package/dist/esm/pos/index.js +4 -2
- package/dist/esm/pos/settings.d.ts +3 -0
- package/dist/esm/pos/settings.js +2 -0
- package/dist/esm/pos/texte.d.ts +156 -0
- package/dist/esm/pos/texte.js +70 -0
- package/dist/esm/react/index.js +8 -2
- package/dist/esm/receipt/due.d.ts +83 -2
- package/dist/esm/receipt/due.js +198 -34
- package/dist/esm/receipt/grid.js +13 -1
- package/dist/esm/receipt/gutschein-plan.d.ts +104 -0
- package/dist/esm/receipt/gutschein-plan.js +352 -0
- package/dist/esm/receipt/index.d.ts +1 -1
- package/dist/esm/receipt/index.js +1 -1
- package/dist/esm/receipt/layout.js +187 -11
- package/dist/esm/register/pairing.d.ts +21 -1
- package/dist/esm/register/pairing.js +16 -0
- package/dist/esm/stored/bon-layout.d.ts +8 -0
- package/dist/esm/stored/bon-layout.js +20 -0
- package/dist/esm/stored/draht.d.ts +3 -1
- package/dist/esm/stored/draht.js +52 -6
- package/dist/esm/stored/einstellungen.js +3 -2
- package/dist/esm/stored/gutscheine.d.ts +3 -0
- package/dist/esm/stored/gutscheine.js +56 -0
- package/dist/esm/stored/index.js +2 -17
- package/dist/esm/stored/vokabular.d.ts +6 -4
- package/dist/esm/stored/vokabular.js +48 -2
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/fixtures/expected/voucher-cancellation.grid32.txt +30 -0
- package/fixtures/expected/voucher-cancellation.grid48.txt +29 -0
- package/fixtures/expected/voucher-cancellation.lines.json +252 -0
- package/fixtures/expected/voucher-cancellation.sheet32.json +206 -0
- package/fixtures/expected/voucher-cancellation.sheet48.json +200 -0
- package/fixtures/expected/voucher-multi-purpose-redeem.grid32.txt +25 -0
- package/fixtures/expected/voucher-multi-purpose-redeem.grid48.txt +25 -0
- package/fixtures/expected/voucher-multi-purpose-redeem.lines.json +259 -0
- package/fixtures/expected/voucher-multi-purpose-redeem.sheet32.json +176 -0
- package/fixtures/expected/voucher-multi-purpose-redeem.sheet48.json +176 -0
- package/fixtures/expected/voucher-multi-purpose-sale.grid32.txt +24 -0
- package/fixtures/expected/voucher-multi-purpose-sale.grid48.txt +23 -0
- package/fixtures/expected/voucher-multi-purpose-sale.lines.json +216 -0
- package/fixtures/expected/voucher-multi-purpose-sale.sheet32.json +170 -0
- package/fixtures/expected/voucher-multi-purpose-sale.sheet48.json +164 -0
- package/fixtures/expected/voucher-payout.grid32.txt +23 -0
- package/fixtures/expected/voucher-payout.grid48.txt +22 -0
- package/fixtures/expected/voucher-payout.lines.json +216 -0
- package/fixtures/expected/voucher-payout.sheet32.json +164 -0
- package/fixtures/expected/voucher-payout.sheet48.json +158 -0
- package/fixtures/expected/voucher-service-redeem.grid32.txt +26 -0
- package/fixtures/expected/voucher-service-redeem.grid48.txt +25 -0
- package/fixtures/expected/voucher-service-redeem.lines.json +261 -0
- package/fixtures/expected/voucher-service-redeem.sheet32.json +182 -0
- package/fixtures/expected/voucher-service-redeem.sheet48.json +176 -0
- package/fixtures/expected/voucher-single-purpose-redeem-exempt.grid32.txt +30 -0
- package/fixtures/expected/voucher-single-purpose-redeem-exempt.grid48.txt +29 -0
- package/fixtures/expected/voucher-single-purpose-redeem-exempt.lines.json +331 -0
- package/fixtures/expected/voucher-single-purpose-redeem-exempt.sheet32.json +206 -0
- package/fixtures/expected/voucher-single-purpose-redeem-exempt.sheet48.json +200 -0
- package/fixtures/expected/voucher-single-purpose-redeem.grid32.txt +25 -0
- package/fixtures/expected/voucher-single-purpose-redeem.grid48.txt +24 -0
- package/fixtures/expected/voucher-single-purpose-redeem.lines.json +246 -0
- package/fixtures/expected/voucher-single-purpose-redeem.sheet32.json +176 -0
- package/fixtures/expected/voucher-single-purpose-redeem.sheet48.json +170 -0
- package/fixtures/expected/voucher-single-purpose-sale.grid32.txt +26 -0
- package/fixtures/expected/voucher-single-purpose-sale.grid48.txt +24 -0
- package/fixtures/expected/voucher-single-purpose-sale.lines.json +225 -0
- package/fixtures/expected/voucher-single-purpose-sale.sheet32.json +182 -0
- package/fixtures/expected/voucher-single-purpose-sale.sheet48.json +170 -0
- package/fixtures/hobex-hps-codes.json +1 -1
- package/fixtures/invoice-api.schema.json +1 -1
- package/fixtures/invoice-texts.json +1 -1
- package/fixtures/manifest.json +64 -0
- package/fixtures/pos-texts.json +170 -1
- package/fixtures/receipt-due-generated.json +1 -1
- package/fixtures/receipt-due-voucher-generated.json +494 -0
- package/fixtures/receipts/voucher-cancellation.json +76 -0
- package/fixtures/receipts/voucher-multi-purpose-redeem.json +62 -0
- package/fixtures/receipts/voucher-multi-purpose-sale.json +63 -0
- package/fixtures/receipts/voucher-payout.json +65 -0
- package/fixtures/receipts/voucher-service-redeem.json +82 -0
- package/fixtures/receipts/voucher-single-purpose-redeem-exempt.json +86 -0
- package/fixtures/receipts/voucher-single-purpose-redeem.json +71 -0
- package/fixtures/receipts/voucher-single-purpose-sale.json +88 -0
- package/fixtures/surface.json +35 -5
- package/fixtures/v3/antworten/belege.json +3405 -3
- package/fixtures/v3/antworten/belegmail.json +5 -2
- package/fixtures/v3/antworten/kasse-belege.json +3423 -3
- package/fixtures/v3/antworten/kasse.json +621 -14
- package/fixtures/v3/antworten/lager.json +1 -1
- package/fixtures/v3/antworten/rechnungen.json +5 -2
- package/fixtures/v3/antworten/storno.json +214 -3
- package/fixtures/v3/stored/belege.json +5 -2
- package/fixtures/v3/stored/kasse.json +225 -4
- package/fixtures/v3/stored/rechnungen.json +5 -2
- package/fixtures/v3/v3-vokabular.json +1375 -23
- package/fixtures/v3/zahlbetrag-faelle.json +293 -3
- package/package.json +4 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { VoucherCode } from '@kreiseck/validator';
|
|
2
|
+
import { isNewFormVoucher, voucherIsValid, voucherServerFields } from '../models/voucher.js';
|
|
3
|
+
/**
|
|
4
|
+
* Stichtag des 0-%-Topfs fuer Mehrzweck-Verkaeufe (Backend
|
|
5
|
+
* `gutschein-core.NULLTOPF_AB`). Solange er in der Zukunft liegt, zaehlt ein
|
|
6
|
+
* Mehrzweck-Verkauf als Fluss; ab dem Tag als Position im 0-%-Satz. Der
|
|
7
|
+
* Zahlbetrag bleibt gleich, Toepfe und Fluss verschieben sich.
|
|
8
|
+
*/
|
|
9
|
+
export const VOUCHER_ZERO_RATE_BUCKET_FROM = '2099-12-31';
|
|
10
|
+
const KINDS = ['voucher', 'voucher_float'];
|
|
11
|
+
const ARTEN = ['multi_purpose', 'single_purpose', 'service', 'promotion'];
|
|
12
|
+
/** `vat-buckets.bucketFuerSatz`. */
|
|
13
|
+
export function topfFuerSatz(vat) {
|
|
14
|
+
switch (vat) {
|
|
15
|
+
case 20: return 'amountRateStandard';
|
|
16
|
+
case 10: return 'amountRateReduced1';
|
|
17
|
+
case 13: return 'amountRateReduced2';
|
|
18
|
+
case 0: return 'amountRateZero';
|
|
19
|
+
case 19: return 'amountRateSpecial';
|
|
20
|
+
case 4.9: return 'amountRateSpecial';
|
|
21
|
+
default: return 'amountRatOthers';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const TOPF_REIHENFOLGE = ['amountRateStandard', 'amountRateReduced1', 'amountRateReduced2', 'amountRateZero', 'amountRateSpecial', 'amountRatOthers'];
|
|
25
|
+
function istGutscheinZeile(p) {
|
|
26
|
+
return p.kind !== undefined && KINDS.includes(p.kind);
|
|
27
|
+
}
|
|
28
|
+
/** Position, die ein Gutschein decken darf: weder Trinkgeld noch Gutscheinzeile. */
|
|
29
|
+
export function istDeckbar(p) {
|
|
30
|
+
return p.tip == null && p.kind !== 'tip' && !istGutscheinZeile(p);
|
|
31
|
+
}
|
|
32
|
+
/** Wie beleg-toepfe: Euro in Positionsreihenfolge aufsummieren, erst die Summe auf Cent runden. */
|
|
33
|
+
export function centSumme(liste) {
|
|
34
|
+
return Math.round(liste.reduce((s, it) => s + it.priceOne * it.amount, 0) * 100);
|
|
35
|
+
}
|
|
36
|
+
/** `gutschein-core.satzBpAusVat`. */
|
|
37
|
+
function satzBp(vat) {
|
|
38
|
+
return Math.round(Number(vat) * 100);
|
|
39
|
+
}
|
|
40
|
+
function position(satzBpWert, cents, kind) {
|
|
41
|
+
return { amount: 1, priceOne: cents / 100, vat: satzBpWert / 100, tip: null, kind };
|
|
42
|
+
}
|
|
43
|
+
/** Code in Normalform (tolerant eingelesen, Pruefzeichen stimmt) oder null. */
|
|
44
|
+
function normalerCode(code) {
|
|
45
|
+
if (typeof code !== 'string')
|
|
46
|
+
return null;
|
|
47
|
+
const v = VoucherCode.validate(code);
|
|
48
|
+
return v.ok ? v.normalized : null;
|
|
49
|
+
}
|
|
50
|
+
/** Ist der Eintrag ein Wunsch der neuen Form (nicht vom Server gelesen)? */
|
|
51
|
+
function istWunsch(v) {
|
|
52
|
+
return isNewFormVoucher(v) && voucherServerFields(v).length === 0;
|
|
53
|
+
}
|
|
54
|
+
/** Ein gelesener Eintrag (Serverfelder gesetzt): nur Wert und Rabatt tragen Geld ausserhalb der Positionen. */
|
|
55
|
+
function istGelesen(v) {
|
|
56
|
+
return voucherServerFields(v).length > 0;
|
|
57
|
+
}
|
|
58
|
+
const STATUS_FEHLER = {
|
|
59
|
+
used_up: 'voucher_used_up', paid_out: 'voucher_used_up', cancelled: 'voucher_cancelled', blocked: 'voucher_blocked', expired: 'voucher_expired',
|
|
60
|
+
};
|
|
61
|
+
/** Die Saetze, auf die `kuTeile` verteilt: je Satz `centSumme` der deckbaren Positionen, nur positive. */
|
|
62
|
+
export function kuSaetze(kunde) {
|
|
63
|
+
const jeSatz = new Map();
|
|
64
|
+
for (const it of kunde) {
|
|
65
|
+
if (!istDeckbar(it))
|
|
66
|
+
continue;
|
|
67
|
+
const s = satzBp(it.vat);
|
|
68
|
+
if (!jeSatz.has(s))
|
|
69
|
+
jeSatz.set(s, []);
|
|
70
|
+
jeSatz.get(s).push(it);
|
|
71
|
+
}
|
|
72
|
+
return [...jeSatz].map(([s, liste]) => ({ satzBp: s, basis: centSumme(liste) })).filter((x) => x.basis > 0);
|
|
73
|
+
}
|
|
74
|
+
/** `gutschein-core.kuTeile`: anteilig zur Positionssumme je Satz, groesster Rest, Topf-Reihenfolge bei Gleichstand. */
|
|
75
|
+
function kuTeile(kunde, betragCents) {
|
|
76
|
+
const jeSatz = new Map();
|
|
77
|
+
for (const it of kunde) {
|
|
78
|
+
if (!istDeckbar(it))
|
|
79
|
+
continue;
|
|
80
|
+
const s = satzBp(it.vat);
|
|
81
|
+
if (!jeSatz.has(s))
|
|
82
|
+
jeSatz.set(s, []);
|
|
83
|
+
jeSatz.get(s).push(it);
|
|
84
|
+
}
|
|
85
|
+
const rang = (s) => TOPF_REIHENFOLGE.indexOf(topfFuerSatz(s / 100));
|
|
86
|
+
const saetze = [...jeSatz]
|
|
87
|
+
.map(([s, liste]) => ({ satzBp: s, basis: centSumme(liste), cents: 0, rest: 0 }))
|
|
88
|
+
.filter((s) => s.basis > 0)
|
|
89
|
+
.sort((a, b) => (rang(a.satzBp) - rang(b.satzBp)) || (b.satzBp - a.satzBp));
|
|
90
|
+
const summe = saetze.reduce((s, x) => s + x.basis, 0);
|
|
91
|
+
if (summe <= 0 || betragCents > summe)
|
|
92
|
+
return null;
|
|
93
|
+
for (const s of saetze) {
|
|
94
|
+
s.cents = Math.floor((betragCents * s.basis) / summe);
|
|
95
|
+
s.rest = (betragCents * s.basis) % summe;
|
|
96
|
+
}
|
|
97
|
+
let offen = betragCents - saetze.reduce((s, x) => s + x.cents, 0);
|
|
98
|
+
const nachRest = saetze.map((s, i) => ({ s, i })).sort((a, b) => (b.s.rest - a.s.rest) || (a.i - b.i));
|
|
99
|
+
for (let k = 0; offen > 0; k++, offen--)
|
|
100
|
+
nachRest[k].s.cents += 1;
|
|
101
|
+
return saetze.filter((s) => s.cents > 0).map((s) => ({ satzBp: s.satzBp, cents: s.cents }));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Wuensche und gelesene Eintraege in Positionen und Belegeintraege, wie
|
|
105
|
+
* `plane` sie baut. `kunde` sind Ware und Trinkgeld in innerer Form.
|
|
106
|
+
*/
|
|
107
|
+
export function planeGutscheine(kunde, vouchers, art, o) {
|
|
108
|
+
const nein = (fehler, text) => ({ fehler, text });
|
|
109
|
+
const storno = art === 'cancellation';
|
|
110
|
+
const wuensche = vouchers.filter(istWunsch);
|
|
111
|
+
const zusatz = [];
|
|
112
|
+
const gutscheine = [];
|
|
113
|
+
// Wert und Rabatt der Altform bzw. gelesener Belege: Euro aus ganzen Cent.
|
|
114
|
+
const altEintrag = (v, i) => {
|
|
115
|
+
if (!Number.isSafeInteger(v.valueCents))
|
|
116
|
+
return nein('voucher_invalid', `Gutschein ${i + 1} hat keinen ganzen Cent-Wert.`);
|
|
117
|
+
return { action: String(v.action), type: String(v.type), value: Math.round(v.valueCents) / 100, ...(v.zeroRateBucket === true ? { imNullTopf: true } : {}) };
|
|
118
|
+
};
|
|
119
|
+
if (wuensche.length === 0) {
|
|
120
|
+
const nulltopf = !storno && o.today >= (o.zeroRateBucketFrom ?? VOUCHER_ZERO_RATE_BUCKET_FROM);
|
|
121
|
+
// `gutschein-transaktion.brauchtPlan`: ab dem Stichtag laeuft plane auch fuer einen Altform-Verkauf.
|
|
122
|
+
const mitPlan = nulltopf && vouchers.some((v) => !istGelesen(v) && v.action === 'sell' && v.type === 'value');
|
|
123
|
+
if (mitPlan && kunde.some(istGutscheinZeile))
|
|
124
|
+
return nein('voucher_invalid', 'Gutschein-Positionen setzt nur der Server.');
|
|
125
|
+
for (const [i, v] of vouchers.entries()) {
|
|
126
|
+
// Gelesene Eintraege der neuen Form ohne Wert/Rabatt: ihr Geld steht in den Positionen.
|
|
127
|
+
if (istGelesen(v) && v.type !== 'value' && v.type !== 'promo')
|
|
128
|
+
continue;
|
|
129
|
+
const g = altEintrag(v, i);
|
|
130
|
+
if ('fehler' in g)
|
|
131
|
+
return g;
|
|
132
|
+
// Altform-Verkauf ab dem Stichtag: Betrag in den 0-%-Topf.
|
|
133
|
+
if (nulltopf && !istGelesen(v) && g.action === 'sell' && g.type === 'value') {
|
|
134
|
+
zusatz.push(position(0, Math.round(v.valueCents), 'voucher_float'));
|
|
135
|
+
g.imNullTopf = true;
|
|
136
|
+
}
|
|
137
|
+
gutscheine.push(g);
|
|
138
|
+
}
|
|
139
|
+
return { zusatz, gutscheine, auszahlungCents: 0, mitPlan, minusJeTopf: new Map() };
|
|
140
|
+
}
|
|
141
|
+
// Ab hier: mindestens ein Wunsch der neuen Form (createReceipt fuehrt `plane` aus).
|
|
142
|
+
if (storno)
|
|
143
|
+
return nein('voucher_invalid', 'Gutscheine der neuen Form gibt es am Storno nur gespiegelt vom Server.');
|
|
144
|
+
if (art === 'training')
|
|
145
|
+
return nein('voucher_invalid', 'Gutscheine der neuen Form gibt es nicht auf Schulungsbelegen.');
|
|
146
|
+
if (kunde.some(istGutscheinZeile))
|
|
147
|
+
return nein('voucher_invalid', 'Gutschein-Positionen setzt nur der Server.');
|
|
148
|
+
for (const [i, v] of vouchers.entries()) {
|
|
149
|
+
if (istGelesen(v))
|
|
150
|
+
return nein('voucher_invalid', `Gutschein ${i + 1}: Serverfelder an einem Wunsch.`);
|
|
151
|
+
if (isNewFormVoucher(v) && !voucherIsValid(v))
|
|
152
|
+
return nein('voucher_invalid', `Gutschein ${i + 1} ist unvollstaendig.`);
|
|
153
|
+
}
|
|
154
|
+
// Kein Gutschein mit Gutschein bezahlen, sobald einer der beiden die neue Form hat.
|
|
155
|
+
const handel = vouchers.filter((v) => v.action === 'sell' || v.action === 'redeem');
|
|
156
|
+
if (handel.some((v) => v.action === 'sell') && handel.some((v) => v.action === 'redeem') && handel.some(isNewFormVoucher))
|
|
157
|
+
return nein('voucher_invalid', 'Gutscheine verkaufen und einloesen gehen nicht am selben Beleg.');
|
|
158
|
+
if (wuensche.some((v) => v.type === 'promotion') && vouchers.length > 1)
|
|
159
|
+
return nein('voucher_invalid', 'Ein Aktionsgutschein steht allein am Beleg.');
|
|
160
|
+
if (wuensche.some((v) => v.action === 'payout') && vouchers.some((v) => !isNewFormVoucher(v) && v.type === 'promo')) {
|
|
161
|
+
return nein('voucher_invalid', 'Rabatt und Auszahlung gehen nicht am selben Beleg.');
|
|
162
|
+
}
|
|
163
|
+
const codes = wuensche.filter((v) => v.action !== 'sell').map((v) => normalerCode(v.code));
|
|
164
|
+
if (codes.some((c) => c == null))
|
|
165
|
+
return nein('voucher_invalid', 'Ein Gutscheincode ist ungueltig.');
|
|
166
|
+
if (new Set(codes).size !== codes.length)
|
|
167
|
+
return nein('voucher_invalid', 'Derselbe Gutschein steht zweimal am Beleg.');
|
|
168
|
+
const ku = o.smallBusiness === true;
|
|
169
|
+
const nulltopf = o.today >= (o.zeroRateBucketFrom ?? VOUCHER_ZERO_RATE_BUCKET_FROM);
|
|
170
|
+
const minusJeTopf = new Map();
|
|
171
|
+
const minus = (s, c) => {
|
|
172
|
+
const topf = topfFuerSatz(s / 100);
|
|
173
|
+
minusJeTopf.set(topf, (minusJeTopf.get(topf) ?? 0) + c);
|
|
174
|
+
};
|
|
175
|
+
const belegt = new Set();
|
|
176
|
+
let auszahlungCents = 0;
|
|
177
|
+
const einstellung = (gewollt) => {
|
|
178
|
+
const e = o.voucherSettings;
|
|
179
|
+
// Der Server verkauft nur mit bestaetigter Einstellung (`voucher_settings_missing`), auch Mehrzweck.
|
|
180
|
+
if (e == null || e.valueVoucherType == null) {
|
|
181
|
+
return nein('voucher_settings_missing', 'Die Gutschein-Einstellung der Kasse fehlt (voucherSettings).');
|
|
182
|
+
}
|
|
183
|
+
return e.valueVoucherType === gewollt ? null : nein('voucher_invalid', 'Die Gutschein-Einstellung der Kasse verkauft eine andere Art.');
|
|
184
|
+
};
|
|
185
|
+
const pruefung = (v, i) => {
|
|
186
|
+
const code = normalerCode(v.code);
|
|
187
|
+
const info = (o.voucherInfo ?? []).find((x) => normalerCode(x.code) === code);
|
|
188
|
+
if (info == null)
|
|
189
|
+
return nein('voucher_not_found', `Gutschein ${i + 1}: keine Pruefung zum Code (checkVoucher).`);
|
|
190
|
+
if (info.check.type !== v.type)
|
|
191
|
+
return nein('voucher_invalid', `Gutschein ${i + 1}: die Art passt nicht zur Pruefung.`);
|
|
192
|
+
return info.check;
|
|
193
|
+
};
|
|
194
|
+
const saldo = (check, c, i) => Number.isInteger(check.balanceCents) && c <= check.balanceCents
|
|
195
|
+
? null
|
|
196
|
+
: nein('voucher_insufficient_balance', `Gutschein ${i + 1}: der Betrag uebersteigt den Saldo.`);
|
|
197
|
+
for (const [i, v] of vouchers.entries()) {
|
|
198
|
+
if (!isNewFormVoucher(v)) {
|
|
199
|
+
const g = altEintrag(v, i);
|
|
200
|
+
if ('fehler' in g)
|
|
201
|
+
return g;
|
|
202
|
+
if (nulltopf && g.action === 'sell' && g.type === 'value') {
|
|
203
|
+
zusatz.push(position(0, Math.round(v.valueCents), 'voucher_float'));
|
|
204
|
+
g.imNullTopf = true;
|
|
205
|
+
}
|
|
206
|
+
gutscheine.push(g);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!ARTEN.includes(String(v.type)))
|
|
210
|
+
return nein('voucher_invalid', `Gutschein ${i + 1}: unbekannte Art.`);
|
|
211
|
+
if (v.action === 'sell') {
|
|
212
|
+
if (v.type === 'multi_purpose') {
|
|
213
|
+
const f = einstellung('multi_purpose');
|
|
214
|
+
if (f)
|
|
215
|
+
return f;
|
|
216
|
+
const c = v.amountCents;
|
|
217
|
+
if (nulltopf) {
|
|
218
|
+
zusatz.push(position(0, c, 'voucher_float'));
|
|
219
|
+
gutscheine.push({ action: 'sell', type: 'value', value: c / 100, imNullTopf: true });
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
gutscheine.push({ action: 'sell', type: 'value', value: c / 100 });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (v.type === 'single_purpose') {
|
|
226
|
+
const f = einstellung('single_purpose');
|
|
227
|
+
if (f)
|
|
228
|
+
return f;
|
|
229
|
+
const satz = ku ? 0 : o.voucherSettings.vatRateBp;
|
|
230
|
+
if (!Number.isInteger(satz) || satz < 0)
|
|
231
|
+
return nein('voucher_settings_missing', 'Der Satz des Einzweckgutscheins fehlt (Gutschein-Einstellung der Kasse).');
|
|
232
|
+
zusatz.push(position(satz, v.amountCents, 'voucher'));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const a = (o.voucherArticles ?? []).find((x) => x.id === v.articleId);
|
|
236
|
+
if (a == null)
|
|
237
|
+
return nein('voucher_item_mismatch', `Gutschein ${i + 1}: der Artikel fehlt (voucherArticles).`);
|
|
238
|
+
if (!Number.isInteger(a.priceCents) || a.priceCents <= 0 || !Number.isInteger(a.vatRateBp) || a.vatRateBp < 0) {
|
|
239
|
+
return nein('voucher_invalid', `Gutschein ${i + 1}: Artikel ohne Preis oder Satz.`);
|
|
240
|
+
}
|
|
241
|
+
const c = v.quantity * a.priceCents;
|
|
242
|
+
if (c > 1_000_000)
|
|
243
|
+
return nein('voucher_invalid', `Gutschein ${i + 1}: Betrag zu hoch.`);
|
|
244
|
+
zusatz.push(position(ku ? 0 : a.vatRateBp, c, 'voucher'));
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const check = pruefung(v, i);
|
|
249
|
+
if ('fehler' in check)
|
|
250
|
+
return check;
|
|
251
|
+
if (v.action === 'payout') {
|
|
252
|
+
// `gutschein-core.darfAuszahlen`: aktiv, oder abgelaufen bis zum Ende der Rueckzahlbarkeit.
|
|
253
|
+
if (!((check.balanceCents ?? 0) > 0))
|
|
254
|
+
return nein('voucher_used_up', `Gutschein ${i + 1}: kein Saldo.`);
|
|
255
|
+
if (check.status === 'expired') {
|
|
256
|
+
const bis = check.validity?.refundableUntil;
|
|
257
|
+
if (bis && o.today > bis)
|
|
258
|
+
return nein('voucher_expired', `Gutschein ${i + 1}: nicht mehr rueckzahlbar.`);
|
|
259
|
+
}
|
|
260
|
+
else if (check.status !== 'active') {
|
|
261
|
+
return nein(STATUS_FEHLER[check.status] ?? 'voucher_invalid', `Gutschein ${i + 1}: Status ${check.status}.`);
|
|
262
|
+
}
|
|
263
|
+
const c = v.amountCents;
|
|
264
|
+
const f = saldo(check, c, i);
|
|
265
|
+
if (f)
|
|
266
|
+
return f;
|
|
267
|
+
if (v.type === 'multi_purpose') {
|
|
268
|
+
zusatz.push(position(0, -c, 'voucher_float'));
|
|
269
|
+
gutscheine.push({ action: 'payout', type: 'value', value: c / 100, imNullTopf: true });
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
if (!Number.isInteger(check.vatRateBp))
|
|
273
|
+
return nein('voucher_invalid', `Gutschein ${i + 1}: Satz fehlt in der Pruefung.`);
|
|
274
|
+
zusatz.push(position(check.vatRateBp, -c, 'voucher'));
|
|
275
|
+
}
|
|
276
|
+
auszahlungCents += c;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (check.status !== 'active')
|
|
280
|
+
return nein(STATUS_FEHLER[check.status] ?? 'voucher_invalid', `Gutschein ${i + 1}: Status ${check.status}.`);
|
|
281
|
+
if (v.type === 'multi_purpose' || v.type === 'single_purpose') {
|
|
282
|
+
const c = v.amountCents;
|
|
283
|
+
const f = saldo(check, c, i);
|
|
284
|
+
if (f)
|
|
285
|
+
return f;
|
|
286
|
+
if (v.type === 'multi_purpose') {
|
|
287
|
+
gutscheine.push({ action: 'redeem', type: 'value', value: c / 100 });
|
|
288
|
+
}
|
|
289
|
+
else if (check.exempt === true || ku) {
|
|
290
|
+
// Befreit verkauft (`kuEingefroren`) oder Kleinunternehmer-Konto: der gedeckte Teil
|
|
291
|
+
// verlaesst anteilig alle Toepfe des Korbs.
|
|
292
|
+
const teile = kuTeile(kunde, c);
|
|
293
|
+
if (!teile)
|
|
294
|
+
return nein('voucher_rate_mismatch', `Gutschein ${i + 1}: der Korb deckt den Betrag nicht.`);
|
|
295
|
+
for (const t of teile) {
|
|
296
|
+
zusatz.push(position(t.satzBp, -t.cents, 'voucher'));
|
|
297
|
+
minus(t.satzBp, t.cents);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
if (!Number.isInteger(check.vatRateBp))
|
|
302
|
+
return nein('voucher_invalid', `Gutschein ${i + 1}: Satz fehlt in der Pruefung.`);
|
|
303
|
+
zusatz.push(position(check.vatRateBp, -c, 'voucher'));
|
|
304
|
+
minus(check.vatRateBp, c);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else if (v.type === 'service') {
|
|
308
|
+
const artikelId = check.article?.id;
|
|
309
|
+
let stueck = 0;
|
|
310
|
+
const jeSatz = new Map();
|
|
311
|
+
for (const k of v.positions ?? []) {
|
|
312
|
+
const it = kunde[k];
|
|
313
|
+
if (it == null || it.kind !== undefined || it.tip != null || !artikelId || it.articleId !== artikelId
|
|
314
|
+
|| !Number.isInteger(it.amount) || it.amount < 1 || belegt.has(k)) {
|
|
315
|
+
return nein('voucher_item_mismatch', `Gutschein ${i + 1}: Position ${k + 1} passt nicht zum Leistungsgutschein.`);
|
|
316
|
+
}
|
|
317
|
+
belegt.add(k);
|
|
318
|
+
stueck += it.amount;
|
|
319
|
+
const s = satzBp(it.vat);
|
|
320
|
+
if (!jeSatz.has(s))
|
|
321
|
+
jeSatz.set(s, []);
|
|
322
|
+
jeSatz.get(s).push(it);
|
|
323
|
+
}
|
|
324
|
+
if (!Number.isInteger(check.remainingQuantity) || stueck > check.remainingQuantity) {
|
|
325
|
+
return nein('voucher_insufficient_balance', `Gutschein ${i + 1}: nicht genug offene Stueck.`);
|
|
326
|
+
}
|
|
327
|
+
for (const [s, liste] of jeSatz) {
|
|
328
|
+
const c = centSumme(liste);
|
|
329
|
+
zusatz.push(position(s, -c, 'voucher'));
|
|
330
|
+
minus(s, c);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// Aktion: Rabatt ueber die Altmechanik promo.
|
|
335
|
+
const basis = centSumme(kunde.filter(istDeckbar));
|
|
336
|
+
const roh = Number.isInteger(check.discountCents) && check.discountCents > 0
|
|
337
|
+
? check.discountCents
|
|
338
|
+
: Math.floor((basis * (Number(check.discountPercentBp) || 0)) / 10000);
|
|
339
|
+
const rabatt = Math.min(roh, basis);
|
|
340
|
+
if (!(rabatt > 0))
|
|
341
|
+
return nein('voucher_invalid', `Gutschein ${i + 1}: kein Rabatt auf diesen Korb.`);
|
|
342
|
+
gutscheine.push({ action: 'redeem', type: 'promo', value: rabatt / 100 });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Einloese-Minuszeilen eines Topfs uebersteigen nie die deckbaren Positionen dieses Topfs.
|
|
346
|
+
for (const [topf, c] of minusJeTopf) {
|
|
347
|
+
const basis = centSumme(kunde.filter((it) => istDeckbar(it) && topfFuerSatz(it.vat) === topf));
|
|
348
|
+
if (c > basis)
|
|
349
|
+
return nein('voucher_rate_mismatch', 'Ein Gutschein deckt mehr als die Positionen seines Steuersatzes.');
|
|
350
|
+
}
|
|
351
|
+
return { zusatz, gutscheine, auszahlungCents, mitPlan: true, minusJeTopf };
|
|
352
|
+
}
|
|
@@ -7,4 +7,4 @@ export { rasterizeLogo } from './bild.js';
|
|
|
7
7
|
export { type BrandMarkRaster, BRAND_MARK_RASTERS, BRAND_MARK_PATHS } from './marke-daten.js';
|
|
8
8
|
export { brandMarkImage } from './marke.js';
|
|
9
9
|
export { type CodeTableTestSheetInput, type CodeTableTestSheetRow, type CodeTableTestSheet, CODE_TABLE_TEST_SHEET_CHARS, codeTableTestSheet, codeTableTestSheetBytes, codeTableReferenceImage, } from './code-table-test-sheet.js';
|
|
10
|
-
export { type ReceiptDueTip, type ReceiptDueTipRecipient, type ReceiptDueOptions, type ReceiptDueBuckets, type ReceiptDueBreakdown, type ReceiptDueErrorReason, receiptDueCents, receiptDueBreakdown, ReceiptDueError, RECEIPT_DUE_ERROR_REASONS, isReceiptDueError, } from './due.js';
|
|
10
|
+
export { type ReceiptDueTip, type ReceiptDueTipRecipient, type ReceiptDueOptions, type ReceiptDueBuckets, type ReceiptDueBreakdown, type ReceiptDueErrorReason, receiptDueCents, receiptDueBreakdown, ReceiptDueError, RECEIPT_DUE_ERROR_REASONS, isReceiptDueError, type VoucherDueInfo, type VoucherDueArticle, type VoucherRedeemContext, voucherRedeemMaxCents, VOUCHER_ZERO_RATE_BUCKET_FROM, } from './due.js';
|
|
@@ -7,4 +7,4 @@ export { rasterizeLogo } from './bild.js';
|
|
|
7
7
|
export { BRAND_MARK_RASTERS, BRAND_MARK_PATHS } from './marke-daten.js';
|
|
8
8
|
export { brandMarkImage } from './marke.js';
|
|
9
9
|
export { CODE_TABLE_TEST_SHEET_CHARS, codeTableTestSheet, codeTableTestSheetBytes, codeTableReferenceImage, } from './code-table-test-sheet.js';
|
|
10
|
-
export { receiptDueCents, receiptDueBreakdown, ReceiptDueError, RECEIPT_DUE_ERROR_REASONS, isReceiptDueError, } from './due.js';
|
|
10
|
+
export { receiptDueCents, receiptDueBreakdown, ReceiptDueError, RECEIPT_DUE_ERROR_REASONS, isReceiptDueError, voucherRedeemMaxCents, VOUCHER_ZERO_RATE_BUCKET_FROM, } from './due.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VatRate, VoucherAction, VoucherType } from '../enums/index.js';
|
|
2
|
-
import { CANCELLATION_REASONS, isCancellationReason, receiptCompanyTaxInfo, receiptItemTotalCents, isDiscountItem, isTipItem, receiptSubSumCents, receiptSumCents, voucherIsValid, } from '../models/index.js';
|
|
2
|
+
import { CANCELLATION_REASONS, isCancellationReason, receiptCompanyTaxInfo, receiptItemTotalCents, isDiscountItem, isTipItem, receiptSubSumCents, receiptSumCents, receiptValueVoucherSign, voucherIsValid, } from '../models/index.js';
|
|
3
3
|
import { parseServerTimeStamp, toViennaWallClock } from '../vienna-time.js';
|
|
4
4
|
import { KasseneckValidationError } from '../client/errors.js';
|
|
5
5
|
export const CURRENT_LAYOUT_RULESET = 2;
|
|
@@ -561,6 +561,152 @@ function gutscheinText(voucher) {
|
|
|
561
561
|
}
|
|
562
562
|
return text;
|
|
563
563
|
}
|
|
564
|
+
/*
|
|
565
|
+
* Gutscheine der neuen Form (Spec 2026-10-14, „Bon und Beleg“). Der Dart-
|
|
566
|
+
* Zwilling uebernimmt Texte und Spaltenbreiten (Zwoelftel) genau so:
|
|
567
|
+
*
|
|
568
|
+
* Verkauf Einzweck/Leistung Positionszeile 7:5 „1 x <Name> <Code>“ | „50,00 A“
|
|
569
|
+
* Verkauf Mehrzweck dieselbe Zeile (Position im 0-%-Topf oder, vor dem
|
|
570
|
+
* Stichtag, aus dem Eintrag) + Text „ USt bei Einlösung“
|
|
571
|
+
* Einloesung Einzweck/ 8:4 „Gutschein ····-9DWT“ | „-30,50 A“, nach der letzten
|
|
572
|
+
* Leistung Zeile dieses Gutscheins Text „ Einzweck, versteuert mit
|
|
573
|
+
* Beleg K1-ID-4711“ (bzw. „Leistung, …“, „…, befreit
|
|
574
|
+
* verkauft (Kleinunternehmer)“, 47 Zeichen: passt auf
|
|
575
|
+
* 80 mm) und 8:4 „ Rest am Gutschein“ | „19,50 “ bzw.
|
|
576
|
+
* „2 Stück“
|
|
577
|
+
* Einloesung Mehrzweck nach „Zwischensumme“ 8:4 „Gutschein ····-9DWT“ |
|
|
578
|
+
* „-17,50 €“ und „ Rest am Gutschein“ | „32,50 €“
|
|
579
|
+
* Auszahlung 8:4 „Auszahlung Gutschein ····-9DWT“ | „-12,00 D“,
|
|
580
|
+
* Text „ Grund: <Grund>“
|
|
581
|
+
* Aktion Rabattzeile wie bisher (Promotionsgutschein)
|
|
582
|
+
*
|
|
583
|
+
* Vermerke sind Textzeilen mit zwei fuehrenden Leerzeichen; das Raster bricht
|
|
584
|
+
* sie mit demselben Einzug um (grid.ts, haengender Einzug), auf 58 mm etwa
|
|
585
|
+
* „ Einzweck, versteuert mit“ / „ Beleg K1-ID-4711“.
|
|
586
|
+
*
|
|
587
|
+
* Die Zusatzzeilen kommen nur aus dem Eintrag: ein Beleg, den der Server fuer
|
|
588
|
+
* ein aelteres Paket aufbereitet hat (Eintraege mit Position entfernt, Code an
|
|
589
|
+
* der Position, functions/beleg-layout.js `gutscheineFuerPaket`), zeigt seine
|
|
590
|
+
* Positionen wie jede andere Position. Ein Betrag zaehlt immer genau einmal:
|
|
591
|
+
* Positionen in der Steuertabelle, Eintraege mit `zeroRateBucket` nie.
|
|
592
|
+
*/
|
|
593
|
+
/** Steht der Betrag des Eintrags als Position am Beleg (Einzweck, Leistung, 0-%-Topf)? */
|
|
594
|
+
function gutscheinStehtAlsPosition(voucher) {
|
|
595
|
+
return voucher.type === VoucherType.single_purpose || voucher.type === VoucherType.service || voucher.zeroRateBucket === true;
|
|
596
|
+
}
|
|
597
|
+
/** Mehrzweckgutschein mit Code (Altart `value`, vom Server als `multi_purpose` gekennzeichnet). */
|
|
598
|
+
function istMehrzweckMitCode(voucher) {
|
|
599
|
+
return voucher.type === VoucherType.value && voucher.voucherType === VoucherType.multi_purpose;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Code fuer die Belegzeile: die Maske des Eintrags, ohne den Platzhalter
|
|
603
|
+
* `no-code`, den der Server Alt-Eintraegen ohne Code setzt (index.js
|
|
604
|
+
* checkVouchersIsValid). Leer, wenn es keinen gibt.
|
|
605
|
+
*/
|
|
606
|
+
function gutscheinCodeZusatz(voucher) {
|
|
607
|
+
return typeof voucher.code === 'string' && voucher.code !== '' && voucher.code !== 'no-code' ? ` ${voucher.code}` : '';
|
|
608
|
+
}
|
|
609
|
+
/** Die vier letzten Zeichen des Codes (`codeSuffix`, sonst aus der Maske), oder null. */
|
|
610
|
+
function gutscheinEndung(voucher) {
|
|
611
|
+
if (typeof voucher.codeSuffix === 'string' && voucher.codeSuffix !== '')
|
|
612
|
+
return voucher.codeSuffix;
|
|
613
|
+
return typeof voucher.code === 'string' && voucher.code.length >= 4 && voucher.code !== 'no-code' ? voucher.code.slice(-4) : null;
|
|
614
|
+
}
|
|
615
|
+
const MEHRZWECK_UST_TEXT = ' USt bei Einlösung';
|
|
616
|
+
/**
|
|
617
|
+
* Welche Gutscheinposition (`voucher`/`voucher_float`) gehoert zu welchem
|
|
618
|
+
* Eintrag? Jede Position hoechstens einem Eintrag, Eintraege in ihrer
|
|
619
|
+
* Reihenfolge (der Server haengt die Positionen in derselben Reihenfolge an).
|
|
620
|
+
* Verkauf: erste freie Position der Art mit demselben Betrag und ohne Code im
|
|
621
|
+
* Namen (wie `gutscheineFuerPaket` des Servers). Einloesung: freie Positionen
|
|
622
|
+
* mit genau dem Namen `Gutschein ····-<Endung>`, Auszahlung: `Auszahlung
|
|
623
|
+
* Gutschein ····-<Endung>` (gutschein-beleg.plane) -- der Reihe nach, bis
|
|
624
|
+
* ihr Betrag den des Eintrags erreicht (ein Kleinunternehmer-Einzweck oder
|
|
625
|
+
* eine Leistung ueber mehrere Saetze hat je Satz eine Position; zwei
|
|
626
|
+
* Gutscheine mit derselben Endung teilen sich so nie eine Position). Ohne
|
|
627
|
+
* Betrag am Eintrag genau eine Position.
|
|
628
|
+
*/
|
|
629
|
+
function gutscheinPositionen(receipt) {
|
|
630
|
+
const zuordnung = new Map();
|
|
631
|
+
const istGutscheinzeile = (it) => it.kind === 'voucher' || it.kind === 'voucher_float';
|
|
632
|
+
for (const voucher of receipt.vouchers) {
|
|
633
|
+
if (!gutscheinStehtAlsPosition(voucher))
|
|
634
|
+
continue;
|
|
635
|
+
if (voucher.action === VoucherAction.sell) {
|
|
636
|
+
const art = voucher.zeroRateBucket === true ? 'voucher_float' : 'voucher';
|
|
637
|
+
const cents = Number.isInteger(voucher.amountCents) ? Math.abs(voucher.amountCents) : Math.abs(voucher.valueCents ?? 0);
|
|
638
|
+
const i = receipt.items.findIndex((it, k) => !zuordnung.has(k) && it.kind === art
|
|
639
|
+
&& Math.abs(receiptItemTotalCents(it)) === cents && !it.name.includes('····'));
|
|
640
|
+
if (i >= 0)
|
|
641
|
+
zuordnung.set(i, { voucher, letzte: true });
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const endung = gutscheinEndung(voucher);
|
|
645
|
+
if (endung == null || (voucher.action !== VoucherAction.redeem && voucher.action !== VoucherAction.payout))
|
|
646
|
+
continue;
|
|
647
|
+
const name = `${voucher.action === VoucherAction.payout ? 'Auszahlung ' : ''}Gutschein ····-${endung}`;
|
|
648
|
+
const ziel = Number.isInteger(voucher.amountCents) ? Math.abs(voucher.amountCents) : Math.abs(voucher.valueCents ?? 0);
|
|
649
|
+
const genommen = [];
|
|
650
|
+
let summe = 0;
|
|
651
|
+
receipt.items.forEach((it, k) => {
|
|
652
|
+
if (zuordnung.has(k) || !istGutscheinzeile(it) || it.name !== name)
|
|
653
|
+
return;
|
|
654
|
+
if (genommen.length > 0 && (ziel <= 0 || summe >= ziel))
|
|
655
|
+
return;
|
|
656
|
+
genommen.push(k);
|
|
657
|
+
summe += Math.abs(receiptItemTotalCents(it));
|
|
658
|
+
});
|
|
659
|
+
genommen.forEach((k, n) => zuordnung.set(k, { voucher, letzte: n === genommen.length - 1 }));
|
|
660
|
+
}
|
|
661
|
+
return zuordnung;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Zeilen einer Gutscheinposition mit ihrem Eintrag. Verkauf als gewoehnliche
|
|
665
|
+
* Positionszeile mit dem Code des Eintrags; Einloesung und Auszahlung ohne
|
|
666
|
+
* Menge in der Breite der Rabattzeilen (8:4), damit „Gutschein ····-9DWT“ auch
|
|
667
|
+
* auf 58 mm in einer Zeile steht.
|
|
668
|
+
*/
|
|
669
|
+
function gutscheinPositionsZeilen(item, satz, zu) {
|
|
670
|
+
const v = zu.voucher;
|
|
671
|
+
if (v.action === VoucherAction.sell) {
|
|
672
|
+
const zeilen = [positionsZeile({ ...item, name: `${item.name}${gutscheinCodeZusatz(v)}` }, satz)];
|
|
673
|
+
if (v.type === VoucherType.value)
|
|
674
|
+
zeilen.push(textZeile(MEHRZWECK_UST_TEXT));
|
|
675
|
+
return zeilen;
|
|
676
|
+
}
|
|
677
|
+
const zeilen = [{
|
|
678
|
+
kind: 'columns',
|
|
679
|
+
columns: [
|
|
680
|
+
{ text: item.name, width: 8, align: 'left' },
|
|
681
|
+
{ text: `${formatCents(receiptItemTotalCents(item))} ${satz.category}`, width: 4, align: 'right' },
|
|
682
|
+
],
|
|
683
|
+
}];
|
|
684
|
+
if (!zu.letzte)
|
|
685
|
+
return zeilen;
|
|
686
|
+
if (v.action === VoucherAction.payout) {
|
|
687
|
+
const grund = typeof v.reason === 'string' ? v.reason.trim() : '';
|
|
688
|
+
if (grund !== '')
|
|
689
|
+
zeilen.push(textZeile(` Grund: ${grund}`));
|
|
690
|
+
return zeilen;
|
|
691
|
+
}
|
|
692
|
+
const art = v.type === VoucherType.service ? 'Leistung' : 'Einzweck';
|
|
693
|
+
if (typeof v.taxedWith === 'string' && v.taxedWith !== '') {
|
|
694
|
+
zeilen.push(textZeile(` ${art}, versteuert mit Beleg ${v.taxedWith}`));
|
|
695
|
+
}
|
|
696
|
+
else if (v.exempt === true) {
|
|
697
|
+
zeilen.push(textZeile(` ${art}, befreit verkauft (Kleinunternehmer)`));
|
|
698
|
+
}
|
|
699
|
+
// Rest: Betrag fluchtet mit den Positionsbetraegen (zwei Zeichen statt
|
|
700
|
+
// „ A“), Stueck rechtsbuendig. Die Gesamtzahl kennt der Beleg nicht.
|
|
701
|
+
if (v.type === VoucherType.service) {
|
|
702
|
+
if (Number.isInteger(v.remainingQuantityAfter))
|
|
703
|
+
zeilen.push(paarZeile(' Rest am Gutschein', `${v.remainingQuantityAfter} Stück`, 8, 4));
|
|
704
|
+
}
|
|
705
|
+
else if (Number.isInteger(v.balanceAfterCents)) {
|
|
706
|
+
zeilen.push(paarZeile(' Rest am Gutschein', `${formatCents(v.balanceAfterCents)} `, 8, 4));
|
|
707
|
+
}
|
|
708
|
+
return zeilen;
|
|
709
|
+
}
|
|
564
710
|
// ----------------------------------------------------------------- Bauteile
|
|
565
711
|
function textZeile(text, align = 'left', bold = false) {
|
|
566
712
|
return { kind: 'text', text, align, bold };
|
|
@@ -905,13 +1051,15 @@ export function buildReceiptLayout(receipt, company, options = {}) {
|
|
|
905
1051
|
.filter((it) => !isTipItem(it) && !isDiscountItem(it))
|
|
906
1052
|
.reduce((summe, it) => summe + receiptItemTotalCents(it), 0);
|
|
907
1053
|
let zwischensummeGezeigt = false;
|
|
1054
|
+
const gutscheinZuordnung = gutscheinPositionen(receipt);
|
|
908
1055
|
let i = 0;
|
|
909
1056
|
while (i < receipt.items.length) {
|
|
910
1057
|
const item = receipt.items[i];
|
|
911
1058
|
if (!isTipItem(item) && !isDiscountItem(item)) {
|
|
912
1059
|
const satz = steuersatz(item.vat);
|
|
913
1060
|
gruppeVon(satz).bruttoCents += receiptItemTotalCents(item);
|
|
914
|
-
|
|
1061
|
+
const gutschein = gutscheinZuordnung.get(i);
|
|
1062
|
+
lines.push(...(gutschein ? gutscheinPositionsZeilen(item, satz, gutschein) : [positionsZeile(item, satz)]));
|
|
915
1063
|
i += 1;
|
|
916
1064
|
continue;
|
|
917
1065
|
}
|
|
@@ -946,28 +1094,44 @@ export function buildReceiptLayout(receipt, company, options = {}) {
|
|
|
946
1094
|
}
|
|
947
1095
|
// --- Gutscheine: verkaufte Wertgutscheine sind Umsatz zu 0 %, eingeloeste
|
|
948
1096
|
// Promotionsgutscheine mindern die Steuergruppen anteilig.
|
|
1097
|
+
// Am Storno-Beleg zaehlen Wertgutscheine mit umgekehrtem Vorzeichen
|
|
1098
|
+
// (receiptValueVoucherSign): der verkaufte wird zurueckgezahlt.
|
|
1099
|
+
// Eintraege mit `zeroRateBucket` stehen schon als Position (0-%-Topf).
|
|
1100
|
+
const wgVz = receiptValueVoucherSign(receipt);
|
|
949
1101
|
let promoCents = 0;
|
|
950
1102
|
for (const voucher of receipt.vouchers) {
|
|
1103
|
+
// Betrag steht als Position (Einzweck, Leistung, 0-%-Topf): weder Zeile noch 0-%-Zeile aus dem Eintrag.
|
|
1104
|
+
if (gutscheinStehtAlsPosition(voucher))
|
|
1105
|
+
continue;
|
|
951
1106
|
const wert = voucher.valueCents ?? 0;
|
|
1107
|
+
const vz = voucher.type === VoucherType.value ? wgVz : 1;
|
|
952
1108
|
if (voucherIsValid(voucher) && voucher.action === VoucherAction.sell) {
|
|
953
|
-
gruppeVon(steuersatz(VatRate.vat0)).bruttoCents += wert;
|
|
1109
|
+
gruppeVon(steuersatz(VatRate.vat0)).bruttoCents += vz * wert;
|
|
954
1110
|
}
|
|
955
1111
|
if (voucher.action === VoucherAction.sell && voucher.type === VoucherType.value) {
|
|
1112
|
+
// Mehrzweck mit Code (vor dem Stichtag nur als Eintrag): wie die Position im 0-%-Topf.
|
|
1113
|
+
const mehrzweck = istMehrzweckMitCode(voucher);
|
|
1114
|
+
const code = gutscheinCodeZusatz(voucher);
|
|
956
1115
|
lines.push({
|
|
957
1116
|
kind: 'columns',
|
|
958
1117
|
columns: [
|
|
959
|
-
{ text: `1 x ${gutscheinText(voucher)}`, width: 7, align: 'left' },
|
|
960
|
-
{ text: `${formatCents(wert)} ${VatRate.vat0.category}`, width: 5, align: 'right' },
|
|
1118
|
+
{ text: `1 x ${mehrzweck ? `Mehrzweckgutschein${code}` : gutscheinText(voucher)}`, width: 7, align: 'left' },
|
|
1119
|
+
{ text: `${formatCents(vz * wert)} ${VatRate.vat0.category}`, width: 5, align: 'right' },
|
|
961
1120
|
],
|
|
962
1121
|
});
|
|
1122
|
+
if (mehrzweck)
|
|
1123
|
+
lines.push(textZeile(MEHRZWECK_UST_TEXT));
|
|
963
1124
|
}
|
|
964
1125
|
if (voucher.action === VoucherAction.redeem && voucher.type === VoucherType.promo) {
|
|
965
|
-
|
|
1126
|
+
// Am Storno-Beleg steht der Promotionsgutschein nur als Bezug: den Rabatt
|
|
1127
|
+
// nimmt dort die Position „Gutschein-Ausgleich“ zurueck (receiptSubSumCents).
|
|
1128
|
+
if (wgVz === 1)
|
|
1129
|
+
promoCents += wert;
|
|
966
1130
|
lines.push({
|
|
967
1131
|
kind: 'columns',
|
|
968
1132
|
columns: [
|
|
969
1133
|
{ text: gutscheinText(voucher), width: 7, align: 'left' },
|
|
970
|
-
{ text: `-${formatCents(wert)}
|
|
1134
|
+
{ text: wgVz === 1 ? `-${formatCents(wert)} €` : '', width: 5, align: 'right' },
|
|
971
1135
|
],
|
|
972
1136
|
});
|
|
973
1137
|
}
|
|
@@ -1015,10 +1179,22 @@ export function buildReceiptLayout(receipt, company, options = {}) {
|
|
|
1015
1179
|
if (summeCents !== zwischensummeCents) {
|
|
1016
1180
|
lines.push(paarZeile('Zwischensumme', `${formatCents(zwischensummeCents)} €`));
|
|
1017
1181
|
for (const voucher of receipt.vouchers) {
|
|
1018
|
-
if (voucher.
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1182
|
+
if (voucher.type !== VoucherType.value || voucher.zeroRateBucket === true)
|
|
1183
|
+
continue;
|
|
1184
|
+
if (voucher.action !== VoucherAction.redeem && voucher.action !== VoucherAction.payout)
|
|
1185
|
+
continue;
|
|
1186
|
+
// Breite 8:4 — "Wertgutschein GS-1" (Label + Code) passt so auf 58 mm
|
|
1187
|
+
// in die Zeile, statt den Code in eine Folgezeile fliessen zu lassen.
|
|
1188
|
+
// Am Storno-Beleg geht der Wert auf den Gutschein zurueck: ohne Minus.
|
|
1189
|
+
// Mehrzweck mit Code: „Gutschein ····-9DWT“ und darunter der Rest
|
|
1190
|
+
// (Zahlungszeile laut Spec); eine Auszahlung ohne `zeroRateBucket`
|
|
1191
|
+
// (schreibt der Server nicht) zaehlt wie in receiptSumCents.
|
|
1192
|
+
const wert = voucher.valueCents ?? 0;
|
|
1193
|
+
const endung = istMehrzweckMitCode(voucher) ? gutscheinEndung(voucher) : null;
|
|
1194
|
+
const text = endung != null ? `Gutschein ····-${endung}` : gutscheinText(voucher);
|
|
1195
|
+
lines.push(paarZeile(voucher.action === VoucherAction.payout ? `Auszahlung ${text}` : text, wgVz === 1 ? `-${formatCents(wert)} €` : `${formatCents(wert)} €`, 8, 4));
|
|
1196
|
+
if (endung != null && wgVz === 1 && Number.isInteger(voucher.balanceAfterCents)) {
|
|
1197
|
+
lines.push(paarZeile(' Rest am Gutschein', `${formatCents(voucher.balanceAfterCents)} €`, 8, 4));
|
|
1022
1198
|
}
|
|
1023
1199
|
}
|
|
1024
1200
|
lines.push({ kind: 'rule', char: '-' });
|