@kreiseck/kasseneck-api 0.6.7
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/LICENSE +201 -0
- package/NOTICE +2 -0
- package/README.md +206 -0
- package/dist/cjs/client/api.d.ts +75 -0
- package/dist/cjs/client/api.js +40 -0
- package/dist/cjs/client/auth.d.ts +52 -0
- package/dist/cjs/client/auth.js +72 -0
- package/dist/cjs/client/cashregisters.d.ts +21 -0
- package/dist/cjs/client/cashregisters.js +36 -0
- package/dist/cjs/client/errors.d.ts +159 -0
- package/dist/cjs/client/errors.js +226 -0
- package/dist/cjs/client/index.d.ts +8 -0
- package/dist/cjs/client/index.js +44 -0
- package/dist/cjs/client/receipts.d.ts +245 -0
- package/dist/cjs/client/receipts.js +452 -0
- package/dist/cjs/client/reports.d.ts +43 -0
- package/dist/cjs/client/reports.js +73 -0
- package/dist/cjs/client/status.d.ts +43 -0
- package/dist/cjs/client/status.js +47 -0
- package/dist/cjs/client/transport.d.ts +117 -0
- package/dist/cjs/client/transport.js +312 -0
- package/dist/cjs/enums/credit-card-provider.d.ts +22 -0
- package/dist/cjs/enums/credit-card-provider.js +23 -0
- package/dist/cjs/enums/define-enum.d.ts +15 -0
- package/dist/cjs/enums/define-enum.js +16 -0
- package/dist/cjs/enums/index.d.ts +7 -0
- package/dist/cjs/enums/index.js +17 -0
- package/dist/cjs/enums/keck-payment-method.d.ts +57 -0
- package/dist/cjs/enums/keck-payment-method.js +23 -0
- package/dist/cjs/enums/receipt-type.d.ts +41 -0
- package/dist/cjs/enums/receipt-type.js +18 -0
- package/dist/cjs/enums/stripe-link-mode.d.ts +17 -0
- package/dist/cjs/enums/stripe-link-mode.js +18 -0
- package/dist/cjs/enums/vat-rate.d.ts +40 -0
- package/dist/cjs/enums/vat-rate.js +19 -0
- package/dist/cjs/enums/voucher-action.d.ts +10 -0
- package/dist/cjs/enums/voucher-action.js +11 -0
- package/dist/cjs/enums/voucher-type.d.ts +9 -0
- package/dist/cjs/enums/voucher-type.js +10 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.js +97 -0
- package/dist/cjs/kasse/artikel.d.ts +83 -0
- package/dist/cjs/kasse/artikel.js +79 -0
- package/dist/cjs/kasse/client.d.ts +10 -0
- package/dist/cjs/kasse/client.js +38 -0
- package/dist/cjs/kasse/drucker.d.ts +54 -0
- package/dist/cjs/kasse/drucker.js +41 -0
- package/dist/cjs/kasse/index.d.ts +11 -0
- package/dist/cjs/kasse/index.js +38 -0
- package/dist/cjs/kasse/settings.d.ts +115 -0
- package/dist/cjs/kasse/settings.js +62 -0
- package/dist/cjs/models/cancellation.d.ts +51 -0
- package/dist/cjs/models/cancellation.js +45 -0
- package/dist/cjs/models/cashregister.d.ts +77 -0
- package/dist/cjs/models/cashregister.js +53 -0
- package/dist/cjs/models/enum-payload.d.ts +26 -0
- package/dist/cjs/models/enum-payload.js +50 -0
- package/dist/cjs/models/hobex-receipt.d.ts +63 -0
- package/dist/cjs/models/hobex-receipt.js +86 -0
- package/dist/cjs/models/index.d.ts +10 -0
- package/dist/cjs/models/index.js +46 -0
- package/dist/cjs/models/receipt-company.d.ts +71 -0
- package/dist/cjs/models/receipt-company.js +52 -0
- package/dist/cjs/models/receipt-item.d.ts +100 -0
- package/dist/cjs/models/receipt-item.js +108 -0
- package/dist/cjs/models/receipt-summary.d.ts +92 -0
- package/dist/cjs/models/receipt-summary.js +54 -0
- package/dist/cjs/models/receipt.d.ts +108 -0
- package/dist/cjs/models/receipt.js +119 -0
- package/dist/cjs/models/report-month.d.ts +32 -0
- package/dist/cjs/models/report-month.js +65 -0
- package/dist/cjs/models/stripe-url-session.d.ts +18 -0
- package/dist/cjs/models/stripe-url-session.js +20 -0
- package/dist/cjs/models/voucher.d.ts +41 -0
- package/dist/cjs/models/voucher.js +56 -0
- package/dist/cjs/money.d.ts +23 -0
- package/dist/cjs/money.js +35 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/payments/hobex.d.ts +87 -0
- package/dist/cjs/payments/hobex.js +167 -0
- package/dist/cjs/payments/index.d.ts +33 -0
- package/dist/cjs/payments/index.js +42 -0
- package/dist/cjs/payments/stripe.d.ts +61 -0
- package/dist/cjs/payments/stripe.js +157 -0
- package/dist/cjs/printing/escpos.d.ts +219 -0
- package/dist/cjs/printing/escpos.js +636 -0
- package/dist/cjs/printing/index.d.ts +3 -0
- package/dist/cjs/printing/index.js +33 -0
- package/dist/cjs/printing/printable.d.ts +27 -0
- package/dist/cjs/printing/printable.js +90 -0
- package/dist/cjs/printing/webusb.d.ts +89 -0
- package/dist/cjs/printing/webusb.js +110 -0
- package/dist/cjs/react/index.d.ts +46 -0
- package/dist/cjs/react/index.js +51 -0
- package/dist/cjs/receipt/discount.d.ts +12 -0
- package/dist/cjs/receipt/discount.js +54 -0
- package/dist/cjs/receipt/epos.d.ts +53 -0
- package/dist/cjs/receipt/epos.js +95 -0
- package/dist/cjs/receipt/grid.d.ts +47 -0
- package/dist/cjs/receipt/grid.js +122 -0
- package/dist/cjs/receipt/index.d.ts +4 -0
- package/dist/cjs/receipt/index.js +29 -0
- package/dist/cjs/receipt/layout-escpos.d.ts +35 -0
- package/dist/cjs/receipt/layout-escpos.js +67 -0
- package/dist/cjs/receipt/layout.d.ts +180 -0
- package/dist/cjs/receipt/layout.js +594 -0
- package/dist/cjs/register/index.d.ts +51 -0
- package/dist/cjs/register/index.js +61 -0
- package/dist/cjs/register/pairing.d.ts +248 -0
- package/dist/cjs/register/pairing.js +279 -0
- package/dist/cjs/register/session.d.ts +37 -0
- package/dist/cjs/register/session.js +52 -0
- package/dist/cjs/vienna-time.d.ts +23 -0
- package/dist/cjs/vienna-time.js +114 -0
- package/dist/esm/client/api.d.ts +75 -0
- package/dist/esm/client/api.js +37 -0
- package/dist/esm/client/auth.d.ts +52 -0
- package/dist/esm/client/auth.js +68 -0
- package/dist/esm/client/cashregisters.d.ts +21 -0
- package/dist/esm/client/cashregisters.js +33 -0
- package/dist/esm/client/errors.d.ts +159 -0
- package/dist/esm/client/errors.js +212 -0
- package/dist/esm/client/index.d.ts +8 -0
- package/dist/esm/client/index.js +8 -0
- package/dist/esm/client/receipts.d.ts +245 -0
- package/dist/esm/client/receipts.js +438 -0
- package/dist/esm/client/reports.d.ts +43 -0
- package/dist/esm/client/reports.js +69 -0
- package/dist/esm/client/status.d.ts +43 -0
- package/dist/esm/client/status.js +43 -0
- package/dist/esm/client/transport.d.ts +117 -0
- package/dist/esm/client/transport.js +307 -0
- package/dist/esm/enums/credit-card-provider.d.ts +22 -0
- package/dist/esm/enums/credit-card-provider.js +20 -0
- package/dist/esm/enums/define-enum.d.ts +15 -0
- package/dist/esm/enums/define-enum.js +13 -0
- package/dist/esm/enums/index.d.ts +7 -0
- package/dist/esm/enums/index.js +7 -0
- package/dist/esm/enums/keck-payment-method.d.ts +57 -0
- package/dist/esm/enums/keck-payment-method.js +20 -0
- package/dist/esm/enums/receipt-type.d.ts +41 -0
- package/dist/esm/enums/receipt-type.js +15 -0
- package/dist/esm/enums/stripe-link-mode.d.ts +17 -0
- package/dist/esm/enums/stripe-link-mode.js +15 -0
- package/dist/esm/enums/vat-rate.d.ts +40 -0
- package/dist/esm/enums/vat-rate.js +16 -0
- package/dist/esm/enums/voucher-action.d.ts +10 -0
- package/dist/esm/enums/voucher-action.js +8 -0
- package/dist/esm/enums/voucher-type.d.ts +9 -0
- package/dist/esm/enums/voucher-type.js +7 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/kasse/artikel.d.ts +83 -0
- package/dist/esm/kasse/artikel.js +70 -0
- package/dist/esm/kasse/client.d.ts +10 -0
- package/dist/esm/kasse/client.js +33 -0
- package/dist/esm/kasse/drucker.d.ts +54 -0
- package/dist/esm/kasse/drucker.js +36 -0
- package/dist/esm/kasse/index.d.ts +11 -0
- package/dist/esm/kasse/index.js +13 -0
- package/dist/esm/kasse/settings.d.ts +115 -0
- package/dist/esm/kasse/settings.js +58 -0
- package/dist/esm/models/cancellation.d.ts +51 -0
- package/dist/esm/models/cancellation.js +40 -0
- package/dist/esm/models/cashregister.d.ts +77 -0
- package/dist/esm/models/cashregister.js +50 -0
- package/dist/esm/models/enum-payload.d.ts +26 -0
- package/dist/esm/models/enum-payload.js +44 -0
- package/dist/esm/models/hobex-receipt.d.ts +63 -0
- package/dist/esm/models/hobex-receipt.js +80 -0
- package/dist/esm/models/index.d.ts +10 -0
- package/dist/esm/models/index.js +10 -0
- package/dist/esm/models/receipt-company.d.ts +71 -0
- package/dist/esm/models/receipt-company.js +48 -0
- package/dist/esm/models/receipt-item.d.ts +100 -0
- package/dist/esm/models/receipt-item.js +101 -0
- package/dist/esm/models/receipt-summary.d.ts +92 -0
- package/dist/esm/models/receipt-summary.js +49 -0
- package/dist/esm/models/receipt.d.ts +108 -0
- package/dist/esm/models/receipt.js +113 -0
- package/dist/esm/models/report-month.d.ts +32 -0
- package/dist/esm/models/report-month.js +58 -0
- package/dist/esm/models/stripe-url-session.d.ts +18 -0
- package/dist/esm/models/stripe-url-session.js +16 -0
- package/dist/esm/models/voucher.d.ts +41 -0
- package/dist/esm/models/voucher.js +51 -0
- package/dist/esm/money.d.ts +23 -0
- package/dist/esm/money.js +31 -0
- package/dist/esm/payments/hobex.d.ts +87 -0
- package/dist/esm/payments/hobex.js +162 -0
- package/dist/esm/payments/index.d.ts +33 -0
- package/dist/esm/payments/index.js +33 -0
- package/dist/esm/payments/stripe.d.ts +61 -0
- package/dist/esm/payments/stripe.js +153 -0
- package/dist/esm/printing/escpos.d.ts +219 -0
- package/dist/esm/printing/escpos.js +616 -0
- package/dist/esm/printing/index.d.ts +3 -0
- package/dist/esm/printing/index.js +3 -0
- package/dist/esm/printing/printable.d.ts +27 -0
- package/dist/esm/printing/printable.js +87 -0
- package/dist/esm/printing/webusb.d.ts +89 -0
- package/dist/esm/printing/webusb.js +101 -0
- package/dist/esm/react/index.d.ts +46 -0
- package/dist/esm/react/index.js +47 -0
- package/dist/esm/receipt/discount.d.ts +12 -0
- package/dist/esm/receipt/discount.js +51 -0
- package/dist/esm/receipt/epos.d.ts +53 -0
- package/dist/esm/receipt/epos.js +86 -0
- package/dist/esm/receipt/grid.d.ts +47 -0
- package/dist/esm/receipt/grid.js +116 -0
- package/dist/esm/receipt/index.d.ts +4 -0
- package/dist/esm/receipt/index.js +4 -0
- package/dist/esm/receipt/layout-escpos.d.ts +35 -0
- package/dist/esm/receipt/layout-escpos.js +64 -0
- package/dist/esm/receipt/layout.d.ts +180 -0
- package/dist/esm/receipt/layout.js +583 -0
- package/dist/esm/register/index.d.ts +51 -0
- package/dist/esm/register/index.js +51 -0
- package/dist/esm/register/pairing.d.ts +248 -0
- package/dist/esm/register/pairing.js +270 -0
- package/dist/esm/register/session.d.ts +37 -0
- package/dist/esm/register/session.js +48 -0
- package/dist/esm/vienna-time.d.ts +23 -0
- package/dist/esm/vienna-time.js +110 -0
- package/fixtures/belege/langer-artikelname.json +43 -0
- package/fixtures/belege/null-ausfall.json +41 -0
- package/fixtures/belege/null-jahr.json +41 -0
- package/fixtures/belege/null-monat.json +41 -0
- package/fixtures/belege/null-pruef.json +41 -0
- package/fixtures/belege/null-schluss.json +41 -0
- package/fixtures/belege/null-start.json +40 -0
- package/fixtures/belege/rabattzeilen.json +61 -0
- package/fixtures/belege/signaturausfall-verkauf.json +49 -0
- package/fixtures/belege/storno-teil.json +48 -0
- package/fixtures/belege/storno-voll.json +54 -0
- package/fixtures/belege/testkasse-verkauf.json +50 -0
- package/fixtures/belege/testsignatur-verkauf.json +50 -0
- package/fixtures/belege/training.json +49 -0
- package/fixtures/belege/verkauf-bar.json +49 -0
- package/fixtures/belege/verkauf-karte.json +49 -0
- package/fixtures/belege/verkauf-kleinunternehmer.json +42 -0
- package/fixtures/erwartet/langer-artikelname.grid32.txt +25 -0
- package/fixtures/erwartet/langer-artikelname.grid48.txt +24 -0
- package/fixtures/erwartet/langer-artikelname.lines.json +216 -0
- package/fixtures/erwartet/null-ausfall.grid32.txt +28 -0
- package/fixtures/erwartet/null-ausfall.grid48.txt +25 -0
- package/fixtures/erwartet/null-ausfall.lines.json +220 -0
- package/fixtures/erwartet/null-jahr.grid32.txt +29 -0
- package/fixtures/erwartet/null-jahr.grid48.txt +25 -0
- package/fixtures/erwartet/null-jahr.lines.json +220 -0
- package/fixtures/erwartet/null-monat.grid32.txt +28 -0
- package/fixtures/erwartet/null-monat.grid48.txt +25 -0
- package/fixtures/erwartet/null-monat.lines.json +220 -0
- package/fixtures/erwartet/null-pruef.grid32.txt +28 -0
- package/fixtures/erwartet/null-pruef.grid48.txt +25 -0
- package/fixtures/erwartet/null-pruef.lines.json +220 -0
- package/fixtures/erwartet/null-schluss.grid32.txt +28 -0
- package/fixtures/erwartet/null-schluss.grid48.txt +25 -0
- package/fixtures/erwartet/null-schluss.lines.json +220 -0
- package/fixtures/erwartet/null-start.grid32.txt +28 -0
- package/fixtures/erwartet/null-start.grid48.txt +25 -0
- package/fixtures/erwartet/null-start.lines.json +220 -0
- package/fixtures/erwartet/rabattzeilen.grid32.txt +27 -0
- package/fixtures/erwartet/rabattzeilen.grid48.txt +26 -0
- package/fixtures/erwartet/rabattzeilen.lines.json +286 -0
- package/fixtures/erwartet/signaturausfall-verkauf.grid32.txt +28 -0
- package/fixtures/erwartet/signaturausfall-verkauf.grid48.txt +26 -0
- package/fixtures/erwartet/signaturausfall-verkauf.lines.json +266 -0
- package/fixtures/erwartet/storno-teil.grid32.txt +27 -0
- package/fixtures/erwartet/storno-teil.grid48.txt +26 -0
- package/fixtures/erwartet/storno-teil.lines.json +237 -0
- package/fixtures/erwartet/storno-voll.grid32.txt +29 -0
- package/fixtures/erwartet/storno-voll.grid48.txt +28 -0
- package/fixtures/erwartet/storno-voll.lines.json +277 -0
- package/fixtures/erwartet/testkasse-verkauf.grid32.txt +28 -0
- package/fixtures/erwartet/testkasse-verkauf.grid48.txt +27 -0
- package/fixtures/erwartet/testkasse-verkauf.lines.json +270 -0
- package/fixtures/erwartet/testsignatur-verkauf.grid32.txt +30 -0
- package/fixtures/erwartet/testsignatur-verkauf.grid48.txt +27 -0
- package/fixtures/erwartet/testsignatur-verkauf.lines.json +270 -0
- package/fixtures/erwartet/training.grid32.txt +33 -0
- package/fixtures/erwartet/training.grid48.txt +30 -0
- package/fixtures/erwartet/training.lines.json +277 -0
- package/fixtures/erwartet/verkauf-bar.grid32.txt +25 -0
- package/fixtures/erwartet/verkauf-bar.grid48.txt +24 -0
- package/fixtures/erwartet/verkauf-bar.lines.json +256 -0
- package/fixtures/erwartet/verkauf-karte.grid32.txt +25 -0
- package/fixtures/erwartet/verkauf-karte.grid48.txt +24 -0
- package/fixtures/erwartet/verkauf-karte.lines.json +256 -0
- package/fixtures/erwartet/verkauf-kleinunternehmer.grid32.txt +26 -0
- package/fixtures/erwartet/verkauf-kleinunternehmer.grid48.txt +25 -0
- package/fixtures/erwartet/verkauf-kleinunternehmer.lines.json +226 -0
- package/fixtures/manifest.json +107 -0
- package/package.json +135 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macht Text fuer den Bondrucker druckbar — Zwilling von `_printable` und
|
|
3
|
+
* `_isEmojiOrZeroWidth` in `kasseneck_api/lib/models/print_paper.dart`
|
|
4
|
+
* (Zeilen 53-88).
|
|
5
|
+
*
|
|
6
|
+
* Warum getrennt vom Erzeuger: Die Ersetzung **aendert die Laenge** ("€" wird
|
|
7
|
+
* zu "EUR", ein Emoji verschwindet). Wer Spalten rechnet, muss den fertigen
|
|
8
|
+
* Text sehen, nicht den rohen — deshalb entscheidet der Aufrufer, wann er
|
|
9
|
+
* ersetzt, und der Erzeuger bekommt bereits druckbaren Text. Der Erzeuger
|
|
10
|
+
* selbst wirft weiterhin bei Zeichen ausserhalb Latin-1; das ist die Schicht,
|
|
11
|
+
* auf der ein Fehler noch etwas bedeutet.
|
|
12
|
+
*
|
|
13
|
+
* Regel: bekannte Sonderzeichen auf ein ASCII-Aequivalent, Latin-1 (inklusive
|
|
14
|
+
* aller deutschen Umlaute) unveraendert, Emoji und Nullbreiten-Zeichen
|
|
15
|
+
* ersatzlos weg, alles Uebrige auf `?`. Ein Artikelname aus einer
|
|
16
|
+
* ERP-Uebernahme mit Gedankenstrich soll den Beleg nicht verhindern.
|
|
17
|
+
*/
|
|
18
|
+
/** Ersetzungen aus print_paper.dart, Schluessel sind Unicode-Codepunkte. */
|
|
19
|
+
const ERSETZUNGEN = new Map([
|
|
20
|
+
[0x2013, '-'], // – Halbgeviertstrich
|
|
21
|
+
[0x2014, '-'], // — Geviertstrich
|
|
22
|
+
[0x2011, '-'], // ‑ geschuetzter Bindestrich
|
|
23
|
+
[0x2212, '-'], // − Minus
|
|
24
|
+
[0x201c, '"'], // " oeffnendes Anfuehrungszeichen
|
|
25
|
+
[0x201d, '"'], // " schliessendes Anfuehrungszeichen
|
|
26
|
+
[0x201e, '"'], // „ tiefes Anfuehrungszeichen
|
|
27
|
+
[0x201f, '"'], // ‟
|
|
28
|
+
[0x2018, "'"], // ' einfaches Anfuehrungszeichen
|
|
29
|
+
[0x2019, "'"], // ' typografisches Apostroph
|
|
30
|
+
[0x201a, "'"], // ‚ tiefes einfaches Anfuehrungszeichen
|
|
31
|
+
[0x2032, "'"], // ′ Minutenzeichen
|
|
32
|
+
[0x2026, '...'], // … Auslassungspunkte
|
|
33
|
+
[0x2022, '*'], // • Aufzaehlungspunkt
|
|
34
|
+
[0x2713, 'x'], // ✓ Haken
|
|
35
|
+
[0x2714, 'x'], // ✔ fetter Haken
|
|
36
|
+
[0x20ac, 'EUR'], // € Euro
|
|
37
|
+
[0x2122, 'TM'], // ™ Markenzeichen
|
|
38
|
+
[0x20ba, 'TL'], // ₺ tuerkische Lira
|
|
39
|
+
]);
|
|
40
|
+
/**
|
|
41
|
+
* Emoji-, Modifier- und Nullbreiten-/Steuerzeichen, die auf dem Beleg nichts
|
|
42
|
+
* verloren haben. Sie werden entfernt statt zu `?` gemacht: ein einzelnes
|
|
43
|
+
* Emoji besteht oft aus mehreren Codepunkten und stuende sonst als Reihe von
|
|
44
|
+
* Fragezeichen auf dem Bon.
|
|
45
|
+
*/
|
|
46
|
+
function istEmojiOderNullbreite(codepunkt) {
|
|
47
|
+
return (codepunkt === 0x200d || // Zero-Width Joiner
|
|
48
|
+
(codepunkt >= 0x200b && codepunkt <= 0x200f) ||
|
|
49
|
+
codepunkt === 0x2060 ||
|
|
50
|
+
codepunkt === 0xfeff ||
|
|
51
|
+
(codepunkt >= 0xfe00 && codepunkt <= 0xfe0f) || // Variantenselektoren
|
|
52
|
+
(codepunkt >= 0x1f3fb && codepunkt <= 0x1f3ff) || // Hautton-Modifier
|
|
53
|
+
(codepunkt >= 0x1f000 && codepunkt <= 0x1faff) || // Emoji-Bloecke
|
|
54
|
+
(codepunkt >= 0x2600 && codepunkt <= 0x27bf) || // Symbole und Dingbats
|
|
55
|
+
(codepunkt >= 0x2b00 && codepunkt <= 0x2bff) || // Symbole und Pfeile
|
|
56
|
+
(codepunkt >= 0x2300 && codepunkt <= 0x23ff) // technische Symbole
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ersetzt alles, was der Bondrucker nicht darstellen kann, durch eine
|
|
61
|
+
* druckbare Entsprechung. Reine Funktion, ohne Zustand und ohne Drucker.
|
|
62
|
+
*
|
|
63
|
+
* Vor `escPosText`/`escPosRow` anwenden, wenn der Text aus fremder Hand kommt
|
|
64
|
+
* (Artikelstamm, ERP-Uebernahme, Kundenname). `•` wird hier zu `*`, und der
|
|
65
|
+
* Erzeuger sagt dasselbe — auch wer diese Funktion weglaesst, bekommt also
|
|
66
|
+
* dasselbe Zeichen (siehe ZEICHEN_ERSATZ in escpos.ts).
|
|
67
|
+
*/
|
|
68
|
+
export function escPosPrintableText(text) {
|
|
69
|
+
let ergebnis = '';
|
|
70
|
+
for (const zeichen of text) {
|
|
71
|
+
const codepunkt = zeichen.codePointAt(0);
|
|
72
|
+
const ersatz = ERSETZUNGEN.get(codepunkt);
|
|
73
|
+
if (ersatz !== undefined) {
|
|
74
|
+
ergebnis += ersatz;
|
|
75
|
+
}
|
|
76
|
+
else if (codepunkt <= 0xff) {
|
|
77
|
+
ergebnis += zeichen; // Latin-1 inklusive Umlaute bleibt unveraendert
|
|
78
|
+
}
|
|
79
|
+
else if (istEmojiOderNullbreite(codepunkt)) {
|
|
80
|
+
// ersatzlos entfernen
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
ergebnis += '?';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return ergebnis;
|
|
87
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bondrucker am USB-Kabel ueber WebUSB (Chrome/Edge auf Windows, macOS,
|
|
3
|
+
* Android, ChromeOS -- kein Safari, kein iPad). Der Browser bekommt das Geraet
|
|
4
|
+
* nur, wenn kein Treiber es haelt (Windows: ggf. WinUSB statt Epson-Treiber).
|
|
5
|
+
* Die Bytes sind dieselben wie am Bluetooth-Drucker: ESC/POS aus
|
|
6
|
+
* `escPosLayoutBytes`, exakt die Rasterzeilen.
|
|
7
|
+
*
|
|
8
|
+
* Alles hier ist strukturell getippt (kein `USBDevice`-DOM-Typ noetig), damit
|
|
9
|
+
* es ohne Browser testbar bleibt und in jeder App gleich laeuft.
|
|
10
|
+
*/
|
|
11
|
+
export interface UsbEndpointLike {
|
|
12
|
+
direction: string;
|
|
13
|
+
type: string;
|
|
14
|
+
endpointNumber: number;
|
|
15
|
+
}
|
|
16
|
+
export interface UsbAlternateLike {
|
|
17
|
+
alternateSetting: number;
|
|
18
|
+
interfaceClass: number;
|
|
19
|
+
endpoints: UsbEndpointLike[];
|
|
20
|
+
}
|
|
21
|
+
export interface UsbInterfaceLike {
|
|
22
|
+
interfaceNumber: number;
|
|
23
|
+
alternates: UsbAlternateLike[];
|
|
24
|
+
}
|
|
25
|
+
export interface UsbConfigurationLike {
|
|
26
|
+
configurationValue: number;
|
|
27
|
+
interfaces: UsbInterfaceLike[];
|
|
28
|
+
}
|
|
29
|
+
export interface UsbTransferOutResultLike {
|
|
30
|
+
status: string;
|
|
31
|
+
bytesWritten: number;
|
|
32
|
+
}
|
|
33
|
+
export interface UsbWriterLike {
|
|
34
|
+
transferOut: (endpointNumber: number, data: Uint8Array) => Promise<UsbTransferOutResultLike>;
|
|
35
|
+
}
|
|
36
|
+
export interface UsbIdentityLike {
|
|
37
|
+
vendorId: number;
|
|
38
|
+
productId: number;
|
|
39
|
+
serialNumber?: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** Das, was ein WebUSB-`USBDevice` bietet -- soweit hier gebraucht. */
|
|
42
|
+
export interface UsbDeviceLike extends UsbWriterLike, UsbIdentityLike {
|
|
43
|
+
productName?: string | null;
|
|
44
|
+
manufacturerName?: string | null;
|
|
45
|
+
opened: boolean;
|
|
46
|
+
configuration: UsbConfigurationLike | null;
|
|
47
|
+
open: () => Promise<void>;
|
|
48
|
+
close: () => Promise<void>;
|
|
49
|
+
selectConfiguration: (value: number) => Promise<void>;
|
|
50
|
+
claimInterface: (interfaceNumber: number) => Promise<void>;
|
|
51
|
+
releaseInterface: (interfaceNumber: number) => Promise<void>;
|
|
52
|
+
selectAlternateInterface: (interfaceNumber: number, alternateSetting: number) => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export interface UsbPrinterEndpoint {
|
|
55
|
+
interfaceNumber: number;
|
|
56
|
+
alternateSetting: number;
|
|
57
|
+
endpointNumber: number;
|
|
58
|
+
}
|
|
59
|
+
export interface UsbPrinterConnection {
|
|
60
|
+
device: UsbDeviceLike;
|
|
61
|
+
/** "Hersteller Produkt", z. B. "EPSON TM-T20III". */
|
|
62
|
+
name: string;
|
|
63
|
+
interfaceNumber: number;
|
|
64
|
+
endpointNumber: number;
|
|
65
|
+
/** Kennung fuers Wiederfinden ueber `navigator.usb.getDevices()`. */
|
|
66
|
+
key: string;
|
|
67
|
+
}
|
|
68
|
+
/** Epson Seiko USB-Vendor-ID -- als Vorfilter im Geraetedialog. */
|
|
69
|
+
export declare const USB_VENDOR_EPSON = 1208;
|
|
70
|
+
/** USB-Geraeteklasse "Drucker". */
|
|
71
|
+
export declare const USB_CLASS_PRINTER = 7;
|
|
72
|
+
/** Kennung `vvvv:pppp:serial` -- stabil ueber Neustarts, wenn der Drucker eine Seriennummer meldet. */
|
|
73
|
+
export declare function usbDeviceKey(d: UsbIdentityLike): string;
|
|
74
|
+
/** Druckerschnittstelle (Klasse 7) bevorzugt, sonst herstellerspezifisch (0xFF), sonst irgendein Bulk-OUT. */
|
|
75
|
+
export declare function usbFindPrinterEndpoint(d: {
|
|
76
|
+
configuration: UsbConfigurationLike | null;
|
|
77
|
+
}): UsbPrinterEndpoint | null;
|
|
78
|
+
/** Bytes blockweise an den OUT-Endpunkt; liefert die geschriebene Gesamtzahl, wirft bei Fehlstatus. */
|
|
79
|
+
export declare function usbWriteAll(dev: UsbWriterLike, endpointNumber: number, bytes: Uint8Array, blockSize?: number): Promise<number>;
|
|
80
|
+
/**
|
|
81
|
+
* Geraet oeffnen, Konfiguration waehlen, Druckerschnittstelle belegen. Wirft
|
|
82
|
+
* verstaendlich, wenn ein Treiber das Geraet haelt oder kein Drucker-Endpunkt
|
|
83
|
+
* da ist -- und schliesst das Geraet dann wieder.
|
|
84
|
+
*/
|
|
85
|
+
export declare function usbConnectPrinter(device: UsbDeviceLike): Promise<UsbPrinterConnection>;
|
|
86
|
+
/** Bytes an eine bestehende Verbindung. */
|
|
87
|
+
export declare function usbPrint(c: UsbPrinterConnection, bytes: Uint8Array): Promise<number>;
|
|
88
|
+
/** Schnittstelle freigeben und schliessen; Fehler dabei sind egal (Geraet ist ohnehin weg). */
|
|
89
|
+
export declare function usbDisconnect(c: UsbPrinterConnection): Promise<void>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bondrucker am USB-Kabel ueber WebUSB (Chrome/Edge auf Windows, macOS,
|
|
3
|
+
* Android, ChromeOS -- kein Safari, kein iPad). Der Browser bekommt das Geraet
|
|
4
|
+
* nur, wenn kein Treiber es haelt (Windows: ggf. WinUSB statt Epson-Treiber).
|
|
5
|
+
* Die Bytes sind dieselben wie am Bluetooth-Drucker: ESC/POS aus
|
|
6
|
+
* `escPosLayoutBytes`, exakt die Rasterzeilen.
|
|
7
|
+
*
|
|
8
|
+
* Alles hier ist strukturell getippt (kein `USBDevice`-DOM-Typ noetig), damit
|
|
9
|
+
* es ohne Browser testbar bleibt und in jeder App gleich laeuft.
|
|
10
|
+
*/
|
|
11
|
+
/** Epson Seiko USB-Vendor-ID -- als Vorfilter im Geraetedialog. */
|
|
12
|
+
export const USB_VENDOR_EPSON = 0x04b8;
|
|
13
|
+
/** USB-Geraeteklasse "Drucker". */
|
|
14
|
+
export const USB_CLASS_PRINTER = 7;
|
|
15
|
+
const HEX4 = (n) => n.toString(16).padStart(4, '0');
|
|
16
|
+
/** Kennung `vvvv:pppp:serial` -- stabil ueber Neustarts, wenn der Drucker eine Seriennummer meldet. */
|
|
17
|
+
export function usbDeviceKey(d) {
|
|
18
|
+
return `${HEX4(d.vendorId)}:${HEX4(d.productId)}:${d.serialNumber ?? ''}`;
|
|
19
|
+
}
|
|
20
|
+
/** Druckerschnittstelle (Klasse 7) bevorzugt, sonst herstellerspezifisch (0xFF), sonst irgendein Bulk-OUT. */
|
|
21
|
+
export function usbFindPrinterEndpoint(d) {
|
|
22
|
+
const konf = d.configuration;
|
|
23
|
+
if (!konf)
|
|
24
|
+
return null;
|
|
25
|
+
const kandidaten = [];
|
|
26
|
+
for (const i of konf.interfaces) {
|
|
27
|
+
for (const alt of i.alternates) {
|
|
28
|
+
const ep = alt.endpoints.find((x) => x.direction === 'out' && x.type === 'bulk');
|
|
29
|
+
if (!ep)
|
|
30
|
+
continue;
|
|
31
|
+
const rang = alt.interfaceClass === USB_CLASS_PRINTER ? 0 : alt.interfaceClass === 0xff ? 1 : 2;
|
|
32
|
+
kandidaten.push({ rang, e: { interfaceNumber: i.interfaceNumber, alternateSetting: alt.alternateSetting, endpointNumber: ep.endpointNumber } });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
kandidaten.sort((a, b) => a.rang - b.rang);
|
|
36
|
+
return kandidaten[0]?.e ?? null;
|
|
37
|
+
}
|
|
38
|
+
/** Bytes blockweise an den OUT-Endpunkt; liefert die geschriebene Gesamtzahl, wirft bei Fehlstatus. */
|
|
39
|
+
export async function usbWriteAll(dev, endpointNumber, bytes, blockSize = 16384) {
|
|
40
|
+
const groesse = Math.max(1, Math.floor(blockSize));
|
|
41
|
+
let n = 0;
|
|
42
|
+
for (let von = 0; von < bytes.length; von += groesse) {
|
|
43
|
+
// eigener ArrayBuffer je Block (BufferSource verlangt kein SharedArrayBuffer)
|
|
44
|
+
const block = new Uint8Array(Math.min(groesse, bytes.length - von));
|
|
45
|
+
block.set(bytes.subarray(von, von + block.length));
|
|
46
|
+
const r = await dev.transferOut(endpointNumber, block);
|
|
47
|
+
if (r.status !== 'ok')
|
|
48
|
+
throw new Error(`USB-Übertragung: ${r.status} nach ${n} Byte`);
|
|
49
|
+
n += r.bytesWritten;
|
|
50
|
+
}
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Geraet oeffnen, Konfiguration waehlen, Druckerschnittstelle belegen. Wirft
|
|
55
|
+
* verstaendlich, wenn ein Treiber das Geraet haelt oder kein Drucker-Endpunkt
|
|
56
|
+
* da ist -- und schliesst das Geraet dann wieder.
|
|
57
|
+
*/
|
|
58
|
+
export async function usbConnectPrinter(device) {
|
|
59
|
+
if (!device.opened)
|
|
60
|
+
await device.open();
|
|
61
|
+
try {
|
|
62
|
+
if (!device.configuration)
|
|
63
|
+
await device.selectConfiguration(1);
|
|
64
|
+
const ep = usbFindPrinterEndpoint(device);
|
|
65
|
+
if (!ep)
|
|
66
|
+
throw new Error('Kein Drucker-Endpunkt am USB-Gerät gefunden — ist das ein Bondrucker?');
|
|
67
|
+
try {
|
|
68
|
+
await device.claimInterface(ep.interfaceNumber);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
const grund = e instanceof Error && e.message ? ` (${e.message})` : '';
|
|
72
|
+
throw new Error(`USB-Schnittstelle belegt${grund} — ein Treiber hält den Drucker. Windows: Epson-Treiber entfernen oder mit Zadig auf WinUSB umstellen.`);
|
|
73
|
+
}
|
|
74
|
+
if (ep.alternateSetting !== 0)
|
|
75
|
+
await device.selectAlternateInterface(ep.interfaceNumber, ep.alternateSetting);
|
|
76
|
+
const name = [device.manufacturerName, device.productName].filter((t) => !!t).join(' ') || 'USB-Drucker';
|
|
77
|
+
return { device, name, interfaceNumber: ep.interfaceNumber, endpointNumber: ep.endpointNumber, key: usbDeviceKey(device) };
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
try {
|
|
81
|
+
await device.close();
|
|
82
|
+
}
|
|
83
|
+
catch { /* schon zu */ }
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Bytes an eine bestehende Verbindung. */
|
|
88
|
+
export function usbPrint(c, bytes) {
|
|
89
|
+
return usbWriteAll(c.device, c.endpointNumber, bytes);
|
|
90
|
+
}
|
|
91
|
+
/** Schnittstelle freigeben und schliessen; Fehler dabei sind egal (Geraet ist ohnehin weg). */
|
|
92
|
+
export async function usbDisconnect(c) {
|
|
93
|
+
try {
|
|
94
|
+
await c.device.releaseInterface(c.interfaceNumber);
|
|
95
|
+
}
|
|
96
|
+
catch { /* egal */ }
|
|
97
|
+
try {
|
|
98
|
+
await c.device.close();
|
|
99
|
+
}
|
|
100
|
+
catch { /* egal */ }
|
|
101
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { ReceiptLayout } from '../receipt/layout.js';
|
|
3
|
+
/**
|
|
4
|
+
* React-Adapter: zeichnet ein Beleg-Layout (siehe ../receipt/layout.ts).
|
|
5
|
+
*
|
|
6
|
+
* Eigener Einstiegspunkt `@kreiseck/kasseneck-api/react`. **React ist eine
|
|
7
|
+
* Peer-Abhaengigkeit**: wer nur Belege erzeugt, Bytes druckt oder ein Layout
|
|
8
|
+
* baut, soll React weder installieren noch laden muessen. Der Kern des Pakets
|
|
9
|
+
* ruehrt diese Datei deshalb nirgends an — ein Test haelt das fest.
|
|
10
|
+
*
|
|
11
|
+
* Der Adapter bringt bewusst kein Aussehen mit: Struktur, Ausrichtung und
|
|
12
|
+
* Betonung stehen im Markup, alles Weitere (Schriftart, Breite, Farben) macht
|
|
13
|
+
* das Stylesheet des Verbrauchers ueber die `keck-receipt-*`-Klassen. Ein
|
|
14
|
+
* Bon ist einspaltig und schmal; er sieht in jeder Anwendung anders aus.
|
|
15
|
+
*
|
|
16
|
+
* Der QR-Code wird **nicht** gezeichnet — dafuer braucht es einen QR-Erzeuger,
|
|
17
|
+
* und welchen, entscheidet die Anwendung. Vorgabe ist ein leeres Element, das
|
|
18
|
+
* die Nutzlast als `data-qr` traegt; ueber [ReceiptLayoutViewProps.renderQr]
|
|
19
|
+
* setzt der Aufrufer seinen eigenen Zeichner ein.
|
|
20
|
+
*/
|
|
21
|
+
export interface ReceiptLayoutViewProps {
|
|
22
|
+
layout: ReceiptLayout;
|
|
23
|
+
/** Zusaetzliche Klasse am aeusseren Element. */
|
|
24
|
+
className?: string;
|
|
25
|
+
/** Zeichnet den RKSV-QR-Code; ohne Angabe erscheint ein Platzhalter mit `data-qr`. */
|
|
26
|
+
renderQr?: (data: string) => ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* QR zunaechst verdeckt (weichgezeichnet) zeigen; ein Tipp macht ihn lesbar.
|
|
29
|
+
* Fuer Bildschirme, auf denen der Beleg nur zur Kontrolle steht -- der
|
|
30
|
+
* Signatur-QR gehoert dem Kunden und wird erst auf Verlangen freigegeben.
|
|
31
|
+
*/
|
|
32
|
+
qrVerdeckt?: boolean;
|
|
33
|
+
/** Text auf dem verdeckten QR (Vorgabe „Antippen zum Anzeigen“). */
|
|
34
|
+
qrVerdecktText?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function ReceiptLayoutView({ layout, className, renderQr, qrVerdeckt, qrVerdecktText }: ReceiptLayoutViewProps): ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* Verdeckter QR: weichgezeichnet und nicht scannbar, bis der Betrachter ihn
|
|
39
|
+
* antippt. Die Nutzlast bleibt trotzdem als `data-qr` am Element -- fuer Tests
|
|
40
|
+
* und Werkzeuge, nicht fuers Auge.
|
|
41
|
+
*/
|
|
42
|
+
export declare function QrVerdeckt({ data, text, children }: {
|
|
43
|
+
data: string;
|
|
44
|
+
text?: string;
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
}): ReactNode;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
const AUSRICHTUNG = {
|
|
4
|
+
left: 'left',
|
|
5
|
+
center: 'center',
|
|
6
|
+
right: 'right',
|
|
7
|
+
};
|
|
8
|
+
export function ReceiptLayoutView({ layout, className, renderQr, qrVerdeckt = false, qrVerdecktText }) {
|
|
9
|
+
const klasse = className === undefined ? 'keck-receipt' : `keck-receipt ${className}`;
|
|
10
|
+
return (_jsx("div", { className: klasse, "data-paper-size": layout.paperSize, children: layout.lines.map((zeile, index) => (_jsx(Zeile, { zeile: zeile, renderQr: renderQr, qrVerdeckt: qrVerdeckt, qrVerdecktText: qrVerdecktText }, index))) }));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Verdeckter QR: weichgezeichnet und nicht scannbar, bis der Betrachter ihn
|
|
14
|
+
* antippt. Die Nutzlast bleibt trotzdem als `data-qr` am Element -- fuer Tests
|
|
15
|
+
* und Werkzeuge, nicht fuers Auge.
|
|
16
|
+
*/
|
|
17
|
+
export function QrVerdeckt({ data, text = 'Antippen zum Anzeigen', children }) {
|
|
18
|
+
const [offen, setOffen] = useState(false);
|
|
19
|
+
return (_jsxs("button", { type: "button", className: offen ? 'keck-receipt-qr-toggle keck-receipt-qr-toggle--offen' : 'keck-receipt-qr-toggle', "aria-pressed": offen, "aria-label": offen ? 'QR-Code verdecken' : 'QR-Code anzeigen', "data-qr": data, onClick: () => setOffen((o) => !o), style: { position: 'relative', display: 'inline-block', border: 'none', background: 'transparent', padding: 0, cursor: 'pointer' }, children: [_jsx("span", { style: { display: 'inline-block', filter: offen ? undefined : 'blur(6px)', transition: 'filter .15s ease' }, children: children }), !offen && (_jsx("span", { className: "keck-receipt-qr-toggle-text", style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', fontSize: '0.75em', fontWeight: 700 }, children: text }))] }));
|
|
20
|
+
}
|
|
21
|
+
function Zeile({ zeile, renderQr, qrVerdeckt, qrVerdecktText }) {
|
|
22
|
+
switch (zeile.kind) {
|
|
23
|
+
case 'text':
|
|
24
|
+
return (_jsx("div", { className: "keck-receipt-text", style: { textAlign: AUSRICHTUNG[zeile.align], fontWeight: zeile.bold ? 'bold' : 'normal' }, children: zeile.text.length > 0 ? zeile.text : ' ' }));
|
|
25
|
+
case 'columns':
|
|
26
|
+
return (_jsx("div", { className: "keck-receipt-row", style: { display: 'flex', width: '100%' }, children: zeile.columns.map((spalte, index) => (_jsx("div", { className: "keck-receipt-cell", style: {
|
|
27
|
+
// Die Breite ist ein Zwoelftel-Anteil (wie beim Bondrucker);
|
|
28
|
+
// so bleibt die Aufteilung auf allen Ausgabewegen dieselbe.
|
|
29
|
+
flex: `0 0 ${((spalte.width / 12) * 100).toFixed(4)}%`,
|
|
30
|
+
textAlign: AUSRICHTUNG[spalte.align],
|
|
31
|
+
overflowWrap: 'break-word',
|
|
32
|
+
}, children: spalte.text }, index))) }));
|
|
33
|
+
case 'rule':
|
|
34
|
+
// Echte Linie statt einer Reihe Bindestriche: auf dem Bildschirm haengt
|
|
35
|
+
// deren Anzahl sonst an der Schriftbreite.
|
|
36
|
+
return _jsx("hr", { className: "keck-receipt-rule", "data-char": zeile.char });
|
|
37
|
+
case 'space':
|
|
38
|
+
return _jsx("div", { className: "keck-receipt-space", style: { height: `${zeile.lines}em` } });
|
|
39
|
+
case 'banner':
|
|
40
|
+
return (_jsx("div", { className: `keck-receipt-banner keck-receipt-banner--${zeile.ton}`, role: zeile.ton === 'warnung' ? 'alert' : undefined, style: { textAlign: 'center', fontWeight: 'bold', letterSpacing: '0.06em', padding: '0.2em 0.4em', border: '2px solid currentColor', margin: '0.3em 0' }, children: zeile.text }));
|
|
41
|
+
case 'qr':
|
|
42
|
+
{
|
|
43
|
+
const bild = renderQr !== undefined ? renderQr(zeile.data) : _jsx("div", { "data-qr": zeile.data, "aria-label": "RKSV-QR-Code" });
|
|
44
|
+
return (_jsx("div", { className: "keck-receipt-qr", style: { textAlign: 'center' }, children: qrVerdeckt ? _jsx(QrVerdeckt, { data: zeile.data, text: qrVerdecktText, children: bild }) : bild }));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReceiptItem } from '../models/receipt-item.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rabatt als negative Position(en) — eine je Steuersatz, anteilig zum
|
|
4
|
+
* Bruttoumsatz dieses Satzes. So stimmt die MwSt-Tabelle des Belegs immer, das
|
|
5
|
+
* DEP zeigt den Rabatt als Position, und Backend/Signatur bleiben unberuehrt
|
|
6
|
+
* (createReceipt erlaubt negative Positionen).
|
|
7
|
+
*
|
|
8
|
+
* Rundung nach dem groessten Rest (Hare-Niemeyer): die Cent, die beim
|
|
9
|
+
* Abrunden uebrig bleiben, gehen der Reihe nach an die Gruppen mit dem
|
|
10
|
+
* groessten Bruchteil; keine Zeile ist je groesser als der Umsatz ihres Satzes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function verteileRabatt(positionen: ReceiptItem[], rabattCents: number, name?: string): ReceiptItem[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { receiptItemTotalCents } from '../models/receipt-item.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rabatt als negative Position(en) — eine je Steuersatz, anteilig zum
|
|
4
|
+
* Bruttoumsatz dieses Satzes. So stimmt die MwSt-Tabelle des Belegs immer, das
|
|
5
|
+
* DEP zeigt den Rabatt als Position, und Backend/Signatur bleiben unberuehrt
|
|
6
|
+
* (createReceipt erlaubt negative Positionen).
|
|
7
|
+
*
|
|
8
|
+
* Rundung nach dem groessten Rest (Hare-Niemeyer): die Cent, die beim
|
|
9
|
+
* Abrunden uebrig bleiben, gehen der Reihe nach an die Gruppen mit dem
|
|
10
|
+
* groessten Bruchteil; keine Zeile ist je groesser als der Umsatz ihres Satzes.
|
|
11
|
+
*/
|
|
12
|
+
export function verteileRabatt(positionen, rabattCents, name = 'Rabatt') {
|
|
13
|
+
if (!Number.isInteger(rabattCents) || rabattCents < 0) {
|
|
14
|
+
throw new Error('Rabatt muss eine ganze Zahl in Cent >= 0 sein');
|
|
15
|
+
}
|
|
16
|
+
if (rabattCents === 0)
|
|
17
|
+
return [];
|
|
18
|
+
// Umsatz je Steuersatz (nur positive Positionen -- Rabatte/Stornos zaehlen nicht mit)
|
|
19
|
+
const gruppen = new Map();
|
|
20
|
+
for (const p of positionen) {
|
|
21
|
+
const betrag = receiptItemTotalCents(p);
|
|
22
|
+
if (betrag <= 0)
|
|
23
|
+
continue;
|
|
24
|
+
const key = typeof p.vat === 'object' ? p.vat.value : String(p.vat);
|
|
25
|
+
const g = gruppen.get(key) ?? { vat: p.vat, umsatz: 0 };
|
|
26
|
+
g.umsatz += betrag;
|
|
27
|
+
gruppen.set(key, g);
|
|
28
|
+
}
|
|
29
|
+
const gesamt = [...gruppen.values()].reduce((s, g) => s + g.umsatz, 0);
|
|
30
|
+
if (rabattCents > gesamt)
|
|
31
|
+
throw new Error('Rabatt uebersteigt den Umsatz');
|
|
32
|
+
const liste = [...gruppen.values()];
|
|
33
|
+
const exakt = liste.map((g) => (rabattCents * g.umsatz) / gesamt);
|
|
34
|
+
const anteile = exakt.map((x) => Math.floor(x));
|
|
35
|
+
let rest = rabattCents - anteile.reduce((s, a) => s + a, 0);
|
|
36
|
+
// Reihenfolge fuer die Restcent: groesster Bruchteil zuerst, bei Gleichstand groesserer Umsatz
|
|
37
|
+
const reihenfolge = liste.map((_, i) => i).sort((a, b) => (exakt[b] - anteile[b]) - (exakt[a] - anteile[a]) || liste[b].umsatz - liste[a].umsatz);
|
|
38
|
+
for (let runde = 0; rest > 0 && runde < liste.length + 1; runde++) {
|
|
39
|
+
for (const i of reihenfolge) {
|
|
40
|
+
if (rest === 0)
|
|
41
|
+
break;
|
|
42
|
+
if (anteile[i] < liste[i].umsatz) {
|
|
43
|
+
anteile[i] += 1;
|
|
44
|
+
rest -= 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return liste
|
|
49
|
+
.map((g, i) => ({ name, quantity: 1, vat: g.vat, priceCents: -anteile[i] }))
|
|
50
|
+
.filter((z) => z.priceCents < 0);
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ReceiptLayout } from './layout.js';
|
|
2
|
+
import type { PosPaperSize } from '../printing/escpos.js';
|
|
3
|
+
/**
|
|
4
|
+
* ePOS-Print XML (Epson TM-Drucker: Server Direct Print, ePOS-Print ueber
|
|
5
|
+
* HTTP) -- **aus dem Zeichenraster**: jede Rasterzeile wird eine <text>-Zeile
|
|
6
|
+
* mit exakt N Zeichen und Zeilenumbruch, Aufdrucke doppelt hoch (Warnungen
|
|
7
|
+
* invers), QR als <symbol>, Leerraum als <feed>, am Ende Schnitt. Kein
|
|
8
|
+
* eigenes Setzen -- was Bildschirm, ESC/POS und PDF zeigen, druckt der Epson
|
|
9
|
+
* Zeile fuer Zeile genauso.
|
|
10
|
+
*
|
|
11
|
+
* Nur der Inhalt <epos-print …>…</epos-print>; die Huelle (PrintRequestInfo
|
|
12
|
+
* fuer Server Direct Print bzw. SOAP fuer ePOS-Print) baut der Aufrufer.
|
|
13
|
+
*/
|
|
14
|
+
export interface EposPrintXmlOptions {
|
|
15
|
+
/** Zeichen je Zeile; Vorgabe nach `layout.paperSize` (32/48). */
|
|
16
|
+
zeichen?: number;
|
|
17
|
+
/** QR-Modulgroesse (Epson `width` 3..16), Vorgabe 6. */
|
|
18
|
+
qrBreite?: number;
|
|
19
|
+
/** Papierschnitt am Ende, Vorgabe true. */
|
|
20
|
+
cut?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function eposXmlEscape(text: string): string;
|
|
23
|
+
export declare function eposPrintXml(layout: ReceiptLayout, options?: EposPrintXmlOptions): string;
|
|
24
|
+
/**
|
|
25
|
+
* Epson-Drucker mit ePOS-Print direkt aus dem Browser ansprechen (ohne
|
|
26
|
+
* Server-Umweg): POST an `https://<ip>/cgi-bin/epos/service.cgi`. Der Drucker
|
|
27
|
+
* antwortet mit CORS- und Private-Network-Headern (am TM-T20 nachgemessen);
|
|
28
|
+
* sein selbstsigniertes Zertifikat muss der Browser einmal akzeptiert haben.
|
|
29
|
+
* Modelle ohne Server Direct Print (TM-T20 & Co.) drucken so trotzdem aus
|
|
30
|
+
* Kasse, Panel und Labor.
|
|
31
|
+
*/
|
|
32
|
+
export interface EposDirectOptions {
|
|
33
|
+
/** IPv4/Hostname des Druckers im lokalen Netz. */
|
|
34
|
+
ip: string;
|
|
35
|
+
/** ePOS Device ID (Druckermenue), Vorgabe `local_printer`. */
|
|
36
|
+
devid?: string;
|
|
37
|
+
/** Papier des Druckers -- bestimmt das Raster; Vorgabe: das des Layouts. */
|
|
38
|
+
papier?: PosPaperSize;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface EposResponse {
|
|
42
|
+
success: boolean;
|
|
43
|
+
/** Epson-Fehlercode (`EPTR_COVER_OPEN`, `EPTR_REC_EMPTY`, ...), leer bei Erfolg; `keine_antwort` bei unlesbarer Antwort. */
|
|
44
|
+
code: string;
|
|
45
|
+
status: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function eposServiceUrl(ip: string, devid?: string, timeoutMs?: number): string;
|
|
48
|
+
export declare function eposSoapEnvelope(innerXml: string): string;
|
|
49
|
+
export declare function eposParseResponse(text: string): EposResponse;
|
|
50
|
+
/** Beleg direkt drucken; wirft bei Netz-/Zertifikatsproblemen, sonst die Drucker-Antwort. */
|
|
51
|
+
export declare function eposDirectPrint(layout: ReceiptLayout, o: EposDirectOptions, fetchFn?: typeof fetch): Promise<EposResponse>;
|
|
52
|
+
/** Verbindungstest: leeres Dokument, druckt nichts, liefert den Druckerstatus. */
|
|
53
|
+
export declare function eposDirectStatus(o: EposDirectOptions, fetchFn?: typeof fetch): Promise<EposResponse>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { renderReceiptGrid, ZEICHEN_JE_PAPIER } from './grid.js';
|
|
2
|
+
export function eposXmlEscape(text) {
|
|
3
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
4
|
+
}
|
|
5
|
+
const NS = 'http://www.epson-pos.com/schemas/2011/03/epos-print';
|
|
6
|
+
export function eposPrintXml(layout, options = {}) {
|
|
7
|
+
const grid = renderReceiptGrid(layout, { zeichen: options.zeichen ?? ZEICHEN_JE_PAPIER[layout.paperSize] });
|
|
8
|
+
const qrBreite = Math.min(16, Math.max(3, Math.floor(options.qrBreite ?? 6)));
|
|
9
|
+
const out = [];
|
|
10
|
+
out.push(`<epos-print xmlns="${NS}">`);
|
|
11
|
+
out.push('<text lang="de"/>');
|
|
12
|
+
out.push('<text font="font_a"/>');
|
|
13
|
+
out.push('<text align="left"/>');
|
|
14
|
+
out.push('<text width="1" height="1" reverse="false" em="false"/>');
|
|
15
|
+
for (const z of grid.lines) {
|
|
16
|
+
switch (z.kind) {
|
|
17
|
+
case 'space':
|
|
18
|
+
out.push('<feed line="1"/>');
|
|
19
|
+
break;
|
|
20
|
+
case 'qr':
|
|
21
|
+
out.push('<text align="center"/>');
|
|
22
|
+
out.push(`<symbol type="qrcode_model_2" level="level_m" width="${qrBreite}" height="0" size="0">${eposXmlEscape(z.qr ?? '')}</symbol>`);
|
|
23
|
+
out.push('<text align="left"/>');
|
|
24
|
+
break;
|
|
25
|
+
case 'banner':
|
|
26
|
+
out.push(`<text width="1" height="2" reverse="${z.ton === 'warnung' ? 'true' : 'false'}" em="true">${eposXmlEscape(z.text)} </text>`);
|
|
27
|
+
out.push('<text width="1" height="1" reverse="false" em="false"/>');
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
if (z.bold)
|
|
31
|
+
out.push(`<text em="true">${eposXmlEscape(z.text)} </text>`);
|
|
32
|
+
else
|
|
33
|
+
out.push(`<text>${eposXmlEscape(z.text)} </text>`);
|
|
34
|
+
if (z.bold)
|
|
35
|
+
out.push('<text em="false"/>');
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (options.cut !== false) {
|
|
40
|
+
out.push('<feed line="2"/>');
|
|
41
|
+
out.push('<cut type="feed"/>');
|
|
42
|
+
}
|
|
43
|
+
out.push('</epos-print>');
|
|
44
|
+
return out.join('\n');
|
|
45
|
+
}
|
|
46
|
+
const EPOS_DEVID_VORGABE = 'local_printer';
|
|
47
|
+
export function eposServiceUrl(ip, devid = EPOS_DEVID_VORGABE, timeoutMs = 10000) {
|
|
48
|
+
const adresse = ip.trim();
|
|
49
|
+
if (!adresse || !/^[A-Za-z0-9.\-:]+$/.test(adresse))
|
|
50
|
+
throw new Error('Drucker-IP fehlt oder ist ungueltig.');
|
|
51
|
+
const geraet = devid.trim() || EPOS_DEVID_VORGABE;
|
|
52
|
+
return `https://${adresse}/cgi-bin/epos/service.cgi?devid=${encodeURIComponent(geraet)}&timeout=${Math.max(1000, Math.floor(timeoutMs))}`;
|
|
53
|
+
}
|
|
54
|
+
export function eposSoapEnvelope(innerXml) {
|
|
55
|
+
return '<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body>' + innerXml + '</s:Body></s:Envelope>';
|
|
56
|
+
}
|
|
57
|
+
export function eposParseResponse(text) {
|
|
58
|
+
const m = /<response\b([^>]*)>/.exec(text);
|
|
59
|
+
if (!m)
|
|
60
|
+
return { success: false, code: 'keine_antwort', status: '' };
|
|
61
|
+
const attr = (n) => new RegExp(`\\b${n}="([^"]*)"`).exec(m[1] ?? '')?.[1] ?? '';
|
|
62
|
+
return { success: attr('success') === 'true', code: attr('code'), status: attr('status') };
|
|
63
|
+
}
|
|
64
|
+
async function eposDirectSend(innerXml, o, fetchFn) {
|
|
65
|
+
const url = eposServiceUrl(o.ip, o.devid ?? EPOS_DEVID_VORGABE, o.timeoutMs ?? 10000);
|
|
66
|
+
let antwort;
|
|
67
|
+
try {
|
|
68
|
+
antwort = await fetchFn(url, { method: 'POST', headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '""' }, body: eposSoapEnvelope(innerXml) });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(`Drucker nicht erreichbar. Einmal https://${o.ip.trim()} im Browser öffnen und das Zertifikat akzeptieren, ` +
|
|
72
|
+
'Zugriff aufs lokale Netz erlauben, ePOS-Print am Drucker auf Enable.');
|
|
73
|
+
}
|
|
74
|
+
if (!antwort.ok)
|
|
75
|
+
throw new Error(`Drucker antwortet mit HTTP ${antwort.status}.`);
|
|
76
|
+
return eposParseResponse(await antwort.text());
|
|
77
|
+
}
|
|
78
|
+
/** Beleg direkt drucken; wirft bei Netz-/Zertifikatsproblemen, sonst die Drucker-Antwort. */
|
|
79
|
+
export function eposDirectPrint(layout, o, fetchFn = fetch) {
|
|
80
|
+
const papier = o.papier ?? layout.paperSize;
|
|
81
|
+
return eposDirectSend(eposPrintXml({ ...layout, paperSize: papier }, { zeichen: ZEICHEN_JE_PAPIER[papier] }), o, fetchFn);
|
|
82
|
+
}
|
|
83
|
+
/** Verbindungstest: leeres Dokument, druckt nichts, liefert den Druckerstatus. */
|
|
84
|
+
export function eposDirectStatus(o, fetchFn = fetch) {
|
|
85
|
+
return eposDirectSend(`<epos-print xmlns="${NS}"/>`, o, fetchFn);
|
|
86
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReceiptLayout } from './layout.js';
|
|
2
|
+
import type { PosPaperSize } from '../printing/escpos.js';
|
|
3
|
+
/**
|
|
4
|
+
* Zeichenraster: der Beleg als Zeilen mit **exakt N Zeichen** -- die einzige
|
|
5
|
+
* Wahrheit fuer Bildschirm, Bondruck und PDF. Kein Ausgabeweg rechnet
|
|
6
|
+
* Spalten selbst; wer das Raster setzt, setzt den Beleg.
|
|
7
|
+
*
|
|
8
|
+
* Regeln (fest, damit ueberall dasselbe herauskommt):
|
|
9
|
+
* - Papier 58 mm = 32 Zeichen, 80 mm = 48 (Font A); andere Breiten per Option.
|
|
10
|
+
* - Spalten in Zwoelfteln -> ganze Zeichen: `floor(N*w/12)`, der Rest an die
|
|
11
|
+
* letzte Spalte, jede mindestens 1. Zwischen zwei Spalten steht immer
|
|
12
|
+
* mindestens ein Leerzeichen (letztes Zeichen jeder nicht-letzten Spalte).
|
|
13
|
+
* Die letzte Spalte endet buendig am rechten Rand.
|
|
14
|
+
* - Text/Aufdruck/Spalteninhalt bricht **wortweise** um (`wortzeilenText`,
|
|
15
|
+
* dieselbe Regel wie der ESC/POS-Kern); ueberlange Woerter nach einem
|
|
16
|
+
* Bindestrich, sonst hart; geschuetztes Leerzeichen bricht nie.
|
|
17
|
+
* - Fliessregel: laeuft in einer Spaltenzeile nur eine Spalte ueber die erste
|
|
18
|
+
* Zeile hinaus, bekommt ihr Rest die volle Breite (lange Artikelnamen auf
|
|
19
|
+
* 58 mm); laufen mehrere weiter, bleibt das Raster.
|
|
20
|
+
* - Trennlinie ueber die volle Breite, Leerraum als Leerzeilen, QR als eigene
|
|
21
|
+
* Zeile mit Nutzlast (der Zeichner setzt das Bild).
|
|
22
|
+
*/
|
|
23
|
+
export declare const ZEICHEN_JE_PAPIER: Readonly<Record<PosPaperSize, number>>;
|
|
24
|
+
export type GridLineKind = 'text' | 'columns' | 'rule' | 'space' | 'qr' | 'banner';
|
|
25
|
+
export interface GridLine {
|
|
26
|
+
/** Genau `zeichen` Zeichen (bei `qr`: zentrierter Platzhalter). */
|
|
27
|
+
text: string;
|
|
28
|
+
kind: GridLineKind;
|
|
29
|
+
bold: boolean;
|
|
30
|
+
/** Bei `banner`: `warnung` = invers/auffaellig, `belegart` = Rahmen. */
|
|
31
|
+
ton?: 'belegart' | 'warnung';
|
|
32
|
+
/** Bei `qr`: die Nutzlast. */
|
|
33
|
+
qr?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ReceiptGrid {
|
|
36
|
+
lines: GridLine[];
|
|
37
|
+
zeichen: number;
|
|
38
|
+
}
|
|
39
|
+
export interface RenderReceiptGridOptions {
|
|
40
|
+
/** Zeichen je Zeile; Vorgabe nach `layout.paperSize` (32/48). */
|
|
41
|
+
zeichen?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Zwoelftel -> Zeichen je Spalte (ganze Zeichen, Rest an die letzte, mindestens 1). */
|
|
44
|
+
export declare function gridSpaltenBreiten(zwoelftel: readonly number[], zeichen: number): number[];
|
|
45
|
+
export declare function renderReceiptGrid(layout: ReceiptLayout, options?: RenderReceiptGridOptions): ReceiptGrid;
|
|
46
|
+
/** Klartext (eine Zeile je Rasterzeile) -- fuer Golden-Dateien und Logs. QR als Platzhalter. */
|
|
47
|
+
export declare function gridAlsText(grid: ReceiptGrid): string;
|