Strichcode mit dem Kassenartikel auf den Bon drucken
Der Kassierer musste den Artikel bisher von Hand in der Kasse suchen. Jetzt steht er als Strichcode unter dem Betrag und wird gescannt. CODE128 in Zeichensatz B, nicht das sonst uebliche CODE39: dort sind nur Grossbuchstaben erlaubt, ein Artikel namens "techniker" waere damit nicht darstellbar. Die Modulbreite waehlt escpos.js so gross, wie die Papierbreite zulaesst - breitere Balken liest ein Handscanner zuverlaessiger, gerade auf Thermopapier, das einen Tag in einer Hosentasche hinter sich hat. Daraus ergibt sich die Laengengrenze von 17 Zeichen bei 80 mm Papier. Artikelname und An/Aus liegen in den Einstellungen, nicht im Code: der Name haengt am Artikelstamm der Kasse und kann sich aendern, ohne dass jemand an die App muss. Zwei Faelle, die sonst still danebengingen, melden sich jetzt: - Umlaute und Sonderzeichen weist das Speichern ab, statt sie zu verschlucken und einen Code zu drucken, der einen anderen Artikel bedeutet. - Passt der Name nicht auf die Papierbreite, druckt receipt.js ihn nur in Klarschrift. Ein abgeschnittener Code wird von keinem Scanner gelesen. Geprueft auf dem Epson TM-T88IV: Zwischenbeleg und Endbeleg, ausgeschalteter Strichcode, Laengengrenze, geschweifte Klammer im Namen (wird verdoppelt, sonst liest der Drucker sie als Umschaltbefehl). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1c7d89a7d
commit
6ba0cdd07f
7 files changed
+229
-9
No files matched your search
@@ -288,6 +288,24 @@ Drei Wege führen zu einem Bon:
|
|||||||
| **✓ Beenden** | Endbeleg | ist beendet und in der Historie |
|
| **✓ Beenden** | Endbeleg | ist beendet und in der Historie |
|
||||||
| **🖨** in der Historie | Nachdruck desselben Belegs | — |
|
| **🖨** in der Historie | Nachdruck desselben Belegs | — |
|
||||||
|
|
||||||
|
#### Strichcode
|
||||||
|
|
||||||
|
Unter dem Betrag steht ein Strichcode mit dem Artikelnamen, den die Kasse führt
|
||||||
|
(ab Werk `techniker`). Name und An/Aus stehen in den Einstellungen, also in der
|
||||||
|
Datenbank — nicht im Code und nicht in der `.env`.
|
||||||
|
|
||||||
|
Symbologie ist **CODE128, Zeichensatz B**. CODE39, sonst der Standard für solche
|
||||||
|
Fälle, kennt nur Großbuchstaben und könnte `techniker` gar nicht abbilden.
|
||||||
|
Zeichensatz B deckt den druckbaren ASCII-Bereich ab; alles darüber — Umlaute
|
||||||
|
zumal — weist `PUT /api/settings` ab, statt es still zu verschlucken.
|
||||||
|
|
||||||
|
Die Modulbreite wählt `escpos.js` selbst: so breit, wie die Papierbreite es
|
||||||
|
zulässt (Ruhezonen eingerechnet), denn breitere Balken liest ein Handscanner
|
||||||
|
zuverlässiger. Daraus folgt die Längengrenze — bei 42 Zeichen je Zeile sind es
|
||||||
|
17 Zeichen, bei 32 noch 12. Passt der Name nicht, druckt `receipt.js` ihn nur
|
||||||
|
in Klarschrift: ein abgeschnittener Code würde ohnehin von keinem Scanner
|
||||||
|
gelesen.
|
||||||
|
|
||||||
**Reihenfolge ist Absicht:** Beim Beenden wird erst gespeichert, dann gedruckt.
|
**Reihenfolge ist Absicht:** Beim Beenden wird erst gespeichert, dann gedruckt.
|
||||||
Ein leerer Papierhalter ist damit ein Papierproblem und kein Datenproblem — der
|
Ein leerer Papierhalter ist damit ein Papierproblem und kein Datenproblem — der
|
||||||
Vorgang steht in der Historie und lässt sich von dort nachdrucken. Die
|
Vorgang steht in der Historie und lässt sich von dort nachdrucken. Die
|
||||||
@@ -453,7 +471,7 @@ Alle Endpunkte außer `/health`, `/api/login`, `/api/auth/status` und
|
|||||||
| `POST` | `/api/timers/:id/receipt` | Zwischenbon drucken, Timer bleibt unangetastet |
|
| `POST` | `/api/timers/:id/receipt` | Zwischenbon drucken, Timer bleibt unangetastet |
|
||||||
| `DELETE` | `/api/timers/:id` | verwerfen, **nichts** speichern |
|
| `DELETE` | `/api/timers/:id` | verwerfen, **nichts** speichern |
|
||||||
| `GET` | `/api/settings` | Tarif, Bon-Texte und Druckerzustand lesen |
|
| `GET` | `/api/settings` | Tarif, Bon-Texte und Druckerzustand lesen |
|
||||||
| `PUT` | `/api/settings` | `{rateAmount, rateInterval}` sowie optional `{receiptHeader, receiptFooter, printAutoOnStop}` |
|
| `PUT` | `/api/settings` | `{rateAmount, rateInterval}` sowie optional `{receiptHeader, receiptFooter, printAutoOnStop, receiptBarcodeEnabled, receiptBarcodeArticle}` |
|
||||||
| `GET` | `/api/printer/status` | Erreichbarkeit und Zustand des Druckers |
|
| `GET` | `/api/printer/status` | Erreichbarkeit und Zustand des Druckers |
|
||||||
| `POST` | `/api/printer/test` | Probedruck |
|
| `POST` | `/api/printer/test` | Probedruck |
|
||||||
| `POST` | `/api/printer/provision` | `{host, port, username, password}` → richtet den Pi ein und übernimmt ihn. Antwort enthält das Schrittprotokoll, nie die Zugangsdaten |
|
| `POST` | `/api/printer/provision` | `{host, port, username, password}` → richtet den Pi ein und übernimmt ihn. Antwort enthält das Schrittprotokoll, nie die Zugangsdaten |
|
||||||
|
|||||||
@@ -58,6 +58,23 @@ Zwei Wege, je nachdem, wie der Kunde geht:
|
|||||||
Zettel zeigt den Stand von jetzt und ist als *Zwischenstand* gekennzeichnet;
|
Zettel zeigt den Stand von jetzt und ist als *Zwischenstand* gekennzeichnet;
|
||||||
der Timer läuft unverändert weiter.
|
der Timer läuft unverändert weiter.
|
||||||
|
|
||||||
|
### Strichcode für die Kasse
|
||||||
|
|
||||||
|
Unter dem Betrag steht ein Strichcode. Die Kasse scannt ihn und hat den Artikel
|
||||||
|
sofort auf dem Bildschirm — niemand muss ihn in einer Liste suchen. Darunter
|
||||||
|
druckt der Drucker denselben Text noch einmal in Klarschrift, für den Fall, dass
|
||||||
|
der Scanner nicht anspringt oder der Zettel geknickt ist.
|
||||||
|
|
||||||
|
Welcher Artikel das ist, steht in den **Einstellungen** unter *Artikel in der
|
||||||
|
Kasse*; ab Werk `techniker`. Wird die Leistung in der Kasse umbenannt, wird der
|
||||||
|
Name hier geändert — ohne dass jemand an die App muss. Der ganze Block lässt
|
||||||
|
sich mit *Strichcode für die Kasse drucken* auch abschalten.
|
||||||
|
|
||||||
|
Zwei Grenzen setzt das Papier: Der Name darf **keine Umlaute und keine
|
||||||
|
Sonderzeichen** enthalten (ein Strichcode kann sie nicht abbilden) und nicht
|
||||||
|
länger sein, als quer auf die Rolle passt — bei 80 mm sind das 17 Zeichen. Beides
|
||||||
|
sagt die App beim Speichern, statt später einen unlesbaren Code zu drucken.
|
||||||
|
|
||||||
### Wenn nichts aus dem Drucker kommt
|
### Wenn nichts aus dem Drucker kommt
|
||||||
|
|
||||||
Der Vorgang ist trotzdem gespeichert — die App sagt in dem Fall ausdrücklich,
|
Der Vorgang ist trotzdem gespeichert — die App sagt in dem Fall ausdrücklich,
|
||||||
@@ -90,6 +107,9 @@ Darunter steht, was auf dem Bon gedruckt wird:
|
|||||||
bezahlen."
|
bezahlen."
|
||||||
* **Bon beim Beenden automatisch drucken** — ausgeschaltet kommt ein Bon nur
|
* **Bon beim Beenden automatisch drucken** — ausgeschaltet kommt ein Bon nur
|
||||||
noch über die 🖨-Knöpfe.
|
noch über die 🖨-Knöpfe.
|
||||||
|
* **Strichcode für die Kasse drucken** — siehe unten. Ausgeschaltet bleibt der
|
||||||
|
Bon genau wie vorher.
|
||||||
|
* **Artikel in der Kasse** — der Name, der im Strichcode steckt.
|
||||||
* **Probedruck** — druckt einen Beispielbon. Beantwortet die Frage, ob der
|
* **Probedruck** — druckt einen Beispielbon. Beantwortet die Frage, ob der
|
||||||
Drucker überhaupt erreichbar ist; darüber steht sein aktueller Zustand.
|
Drucker überhaupt erreichbar ist; darüber steht sein aktueller Zustand.
|
||||||
|
|
||||||
|
|||||||
+65
-1
@@ -20,6 +20,24 @@ const GS = 0x1d;
|
|||||||
// von Node plus diese eine Sonderbehandlung.
|
// von Node plus diese eine Sonderbehandlung.
|
||||||
const CODEPAGE_WPC1252 = 16;
|
const CODEPAGE_WPC1252 = 16;
|
||||||
|
|
||||||
|
// Breite eines Zeichens in Schriftart A, in Druckpunkten. Daraus ergibt sich,
|
||||||
|
// wie viel Platz ein Strichcode quer ueber das Papier hat.
|
||||||
|
const ZEICHENBREITE = 12;
|
||||||
|
|
||||||
|
// Ruhezone links und rechts vom Strichcode, in Modulen. Ohne diesen Abstand
|
||||||
|
// findet ein Scanner den Anfang des Codes nicht zuverlaessig.
|
||||||
|
const RUHEZONE = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wie viele Zeichen ein CODE128-Strichcode auf dieser Papierbreite noch
|
||||||
|
* verkraeftet. Bei der schmalsten noch lesbaren Modulbreite (2 Punkte)
|
||||||
|
* gerechnet, also der guenstigste Fall.
|
||||||
|
*/
|
||||||
|
function barcodeMaxLength(width = 42) {
|
||||||
|
const module = Math.floor((width * ZEICHENBREITE) / 2) - 35 - 2 * RUHEZONE;
|
||||||
|
return Math.max(0, Math.floor(module / 11));
|
||||||
|
}
|
||||||
|
|
||||||
/** Zeichen, die WPC1252 nicht kennt, auf etwas Druckbares abbilden. */
|
/** Zeichen, die WPC1252 nicht kennt, auf etwas Druckbares abbilden. */
|
||||||
const ERSATZ = {
|
const ERSATZ = {
|
||||||
'€': '\x80',
|
'€': '\x80',
|
||||||
@@ -131,6 +149,52 @@ class Bon {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strichcode in CODE128, Zeichensatz B.
|
||||||
|
*
|
||||||
|
* CODE128 statt des sonst ueblichen CODE39, weil CODE39 nur
|
||||||
|
* Grossbuchstaben kennt - ein Artikel namens "techniker" liesse sich damit
|
||||||
|
* nicht abbilden. Zeichensatz B deckt den druckbaren ASCII-Bereich ab und
|
||||||
|
* wird von jedem Handscanner gelesen.
|
||||||
|
*
|
||||||
|
* Die Modulbreite - die Breite des schmalsten Balkens - wird so gross
|
||||||
|
* gewaehlt, wie das Papier es zulaesst. Breitere Balken liest ein Scanner
|
||||||
|
* zuverlaessiger, gerade auf Thermopapier, das schon einen Tag in einer
|
||||||
|
* Hosentasche hinter sich hat.
|
||||||
|
*
|
||||||
|
* @param {string} text Nutzdaten, druckbares ASCII
|
||||||
|
* @param {object} [o]
|
||||||
|
* @param {number} [o.hoehe] Hoehe in Druckpunkten (8 Punkte = 1 mm)
|
||||||
|
* @param {boolean} [o.klarschrift] Nutzdaten zusaetzlich lesbar darunter
|
||||||
|
*/
|
||||||
|
barcode(text, { hoehe = 64, klarschrift = true } = {}) {
|
||||||
|
// Was der Zeichensatz nicht kann, wuerde der Drucker stillschweigend
|
||||||
|
// verschlucken und einen Code ausgeben, der etwas anderes bedeutet.
|
||||||
|
const daten = String(text == null ? '' : text).replace(/[^\x20-\x7e]/g, '');
|
||||||
|
if (!daten) return this;
|
||||||
|
|
||||||
|
// Ein CODE128-Zeichen belegt 11 Module, dazu kommen Start, Pruefzeichen
|
||||||
|
// und Stopp (zusammen 35) sowie die Ruhezone auf beiden Seiten.
|
||||||
|
const module = 11 * daten.length + 35 + 2 * RUHEZONE;
|
||||||
|
const modul = Math.max(2, Math.min(4, Math.floor((this.width * ZEICHENBREITE) / module)));
|
||||||
|
|
||||||
|
this.raw(GS, 0x68, Math.max(1, Math.min(255, hoehe))); // GS h Hoehe
|
||||||
|
this.raw(GS, 0x77, modul); // GS w Modulbreite
|
||||||
|
this.raw(GS, 0x66, 0); // GS f Schrift der Klarschrift
|
||||||
|
this.raw(GS, 0x48, klarschrift ? 2 : 0); // GS H Klarschrift darunter
|
||||||
|
|
||||||
|
// "{B" waehlt den Zeichensatz. Eine geschweifte Klammer in den Daten
|
||||||
|
// muss deshalb verdoppelt werden, sonst liest der Drucker sie als
|
||||||
|
// Umschaltbefehl und der Code enthaelt am Ende etwas anderes.
|
||||||
|
const nutz = Buffer.from('{B' + daten.split('{').join('{{'), 'latin1');
|
||||||
|
|
||||||
|
// Form 2 von GS k: die Laenge steht als Byte davor, statt die Daten mit
|
||||||
|
// einem Nullbyte abzuschliessen. Nur so darf jedes Zeichen vorkommen.
|
||||||
|
this.raw(GS, 0x6b, 73, nutz.length);
|
||||||
|
this.parts.push(nutz);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
feed(n = 1) {
|
feed(n = 1) {
|
||||||
return this.raw(ESC, 0x64, Math.max(0, Math.min(255, n)));
|
return this.raw(ESC, 0x64, Math.max(0, Math.min(255, n)));
|
||||||
}
|
}
|
||||||
@@ -149,4 +213,4 @@ class Bon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Bon, encodeText };
|
module.exports = { Bon, encodeText, barcodeMaxLength };
|
||||||
+54
-2
@@ -10,7 +10,7 @@
|
|||||||
* dem Zielhost gebaut wurde.
|
* dem Zielhost gebaut wurde.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { Bon } = require('./escpos');
|
const { Bon, barcodeMaxLength } = require('./escpos');
|
||||||
|
|
||||||
const p2 = n => String(n).padStart(2, '0');
|
const p2 = n => String(n).padStart(2, '0');
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ function unitCount(ms, rateInterval) {
|
|||||||
* @param {boolean} data.stillRunning
|
* @param {boolean} data.stillRunning
|
||||||
* @param {string} data.header Kopfzeilen, durch \n getrennt
|
* @param {string} data.header Kopfzeilen, durch \n getrennt
|
||||||
* @param {string} data.footer Fusszeilen, durch \n getrennt
|
* @param {string} data.footer Fusszeilen, durch \n getrennt
|
||||||
|
* @param {string} data.barcode Artikelname fuer den Strichcode; leer = keiner
|
||||||
* @param {number} data.width Zeichen je Zeile
|
* @param {number} data.width Zeichen je Zeile
|
||||||
* @returns {Buffer}
|
* @returns {Buffer}
|
||||||
*/
|
*/
|
||||||
@@ -117,6 +118,29 @@ function buildReceipt(data) {
|
|||||||
bon.line();
|
bon.line();
|
||||||
bon.hr();
|
bon.hr();
|
||||||
|
|
||||||
|
// --- Strichcode fuer die Kasse ------------------------------------------
|
||||||
|
// Der Kassierer soll den Artikel nicht in einer Liste suchen muessen: der
|
||||||
|
// Code enthaelt genau den Namen, unter dem die Kasse die Leistung fuehrt.
|
||||||
|
// Die Klarschrift darunter druckt der Drucker selbst mit - sie rettet den
|
||||||
|
// Vorgang, wenn der Scanner streikt oder das Papier geknickt ist.
|
||||||
|
if (data.barcode) {
|
||||||
|
bon.line();
|
||||||
|
bon.align('center');
|
||||||
|
bon.line('Artikel für die Kasse');
|
||||||
|
if (data.barcode.length <= barcodeMaxLength(width)) {
|
||||||
|
bon.barcode(data.barcode);
|
||||||
|
} else {
|
||||||
|
// Breiter als das Papier. Ein abgeschnittener Code wird von keinem
|
||||||
|
// Scanner gelesen, also lieber gar keiner: der Name allein reicht
|
||||||
|
// zum Eintippen. Passieren kann das nur, wenn die Papierbreite
|
||||||
|
// nachtraeglich kleiner gestellt wurde.
|
||||||
|
bon.bold(true).line(data.barcode).bold(false);
|
||||||
|
}
|
||||||
|
bon.align('left');
|
||||||
|
bon.line();
|
||||||
|
bon.hr();
|
||||||
|
}
|
||||||
|
|
||||||
// --- Fuss ---------------------------------------------------------------
|
// --- Fuss ---------------------------------------------------------------
|
||||||
bon.align('center');
|
bon.align('center');
|
||||||
if (interim) {
|
if (interim) {
|
||||||
@@ -140,12 +164,40 @@ function buildReceipt(data) {
|
|||||||
return bon.build();
|
return bon.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strichcodes aus der Bytefolge herausloesen und durch ihren Inhalt ersetzen.
|
||||||
|
*
|
||||||
|
* Eigene Schleife statt eines Ausdrucks, weil bei "GS k" die Laenge der Daten
|
||||||
|
* in einem Byte MITTEN in der Sequenz steht - wie weit zu springen ist, weiss
|
||||||
|
* man also erst beim Lesen. Genau das kann ein regulaerer Ausdruck nicht.
|
||||||
|
*/
|
||||||
|
function stripBarcode(s) {
|
||||||
|
let aus = '';
|
||||||
|
let i = 0;
|
||||||
|
while (i < s.length) {
|
||||||
|
if (s.charCodeAt(i) === 0x1d && s[i + 1] === 'k' &&
|
||||||
|
s.charCodeAt(i + 2) >= 65 && s.charCodeAt(i + 2) <= 73) {
|
||||||
|
const laenge = s.charCodeAt(i + 3);
|
||||||
|
const daten = s.substr(i + 4, laenge)
|
||||||
|
.replace(/^\{[ABC]/, '') // Wahl des Zeichensatzes
|
||||||
|
.split('{{').join('{'); // verdoppelte Klammer zuruecknehmen
|
||||||
|
aus += `[Strichcode: ${daten}]\n`;
|
||||||
|
i += 4 + laenge;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
aus += s[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return aus;
|
||||||
|
}
|
||||||
|
|
||||||
/** Bon als reiner Text - fuer Vorschau, Tests und das Journal. */
|
/** Bon als reiner Text - fuer Vorschau, Tests und das Journal. */
|
||||||
function toPlainText(buffer) {
|
function toPlainText(buffer) {
|
||||||
return buffer.toString('latin1')
|
return stripBarcode(buffer.toString('latin1'))
|
||||||
.replace(/\x1b@/g, '') // Zuruecksetzen
|
.replace(/\x1b@/g, '') // Zuruecksetzen
|
||||||
.replace(/\x1b[tadEG!-][\s\S]/g, '') // ESC + Kennbuchstabe + 1 Byte
|
.replace(/\x1b[tadEG!-][\s\S]/g, '') // ESC + Kennbuchstabe + 1 Byte
|
||||||
.replace(/\x1d![\s\S]/g, '') // GS ! Zeichengroesse
|
.replace(/\x1d![\s\S]/g, '') // GS ! Zeichengroesse
|
||||||
|
.replace(/\x1d[hwHf][\s\S]/g, '') // GS h/w/H/f Masse des Strichcodes
|
||||||
.replace(/\x1dV[\s\S]{2}/g, '') // GS V Schnitt
|
.replace(/\x1dV[\s\S]{2}/g, '') // GS V Schnitt
|
||||||
.replace(/\x80/g, 'EUR')
|
.replace(/\x80/g, 'EUR')
|
||||||
.replace(/\n{3,}/g, '\n\n')
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
|||||||
+38
-3
@@ -18,6 +18,7 @@ require('dotenv').config();
|
|||||||
const oidc = require('./oidc');
|
const oidc = require('./oidc');
|
||||||
const printer = require('./printer');
|
const printer = require('./printer');
|
||||||
const { buildReceipt } = require('./receipt');
|
const { buildReceipt } = require('./receipt');
|
||||||
|
const { barcodeMaxLength } = require('./escpos');
|
||||||
const { provision } = require('./provision');
|
const { provision } = require('./provision');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -152,6 +153,12 @@ async function initDatabase() {
|
|||||||
['Bitte an der Kasse bezahlen.\nVielen Dank fuer Ihren Besuch!']);
|
['Bitte an der Kasse bezahlen.\nVielen Dank fuer Ihren Besuch!']);
|
||||||
await dbRun(`INSERT OR IGNORE INTO settings (key,value) VALUES ('print_auto_on_stop','1')`);
|
await dbRun(`INSERT OR IGNORE INTO settings (key,value) VALUES ('print_auto_on_stop','1')`);
|
||||||
|
|
||||||
|
// Strichcode fuer die Kasse: der Kassierer soll den Artikel scannen
|
||||||
|
// koennen, statt ihn zu suchen. Der Name steht hier und nicht im Code,
|
||||||
|
// weil er sich mit dem Artikelstamm der Kasse aendern kann.
|
||||||
|
await dbRun(`INSERT OR IGNORE INTO settings (key,value) VALUES ('receipt_barcode_enabled','1')`);
|
||||||
|
await dbRun(`INSERT OR IGNORE INTO settings (key,value) VALUES ('receipt_barcode_article','techniker')`);
|
||||||
|
|
||||||
// Druckerzugang: die .env liefert nur die ERSTEN Werte. Danach lebt die
|
// Druckerzugang: die .env liefert nur die ERSTEN Werte. Danach lebt die
|
||||||
// Konfiguration in der Datenbank, damit die Oberflaeche einen neuen Pi
|
// Konfiguration in der Datenbank, damit die Oberflaeche einen neuen Pi
|
||||||
// einrichten kann, ohne dass jemand auf den Server muss.
|
// einrichten kann, ohne dass jemand auf den Server muss.
|
||||||
@@ -213,15 +220,19 @@ async function refreshPrinterConfig() {
|
|||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bon-Einstellungen: Kopf-/Fusstext und ob beim Beenden automatisch gedruckt wird. */
|
/** Bon-Einstellungen: Kopf-/Fusstext, Strichcode und der Druck beim Beenden. */
|
||||||
async function getPrintSettings() {
|
async function getPrintSettings() {
|
||||||
const rows = await dbAll(
|
const rows = await dbAll(
|
||||||
`SELECT key, value FROM settings WHERE key IN ('receipt_header','receipt_footer','print_auto_on_stop')`);
|
`SELECT key, value FROM settings WHERE key IN
|
||||||
|
('receipt_header','receipt_footer','print_auto_on_stop',
|
||||||
|
'receipt_barcode_enabled','receipt_barcode_article')`);
|
||||||
const map = Object.fromEntries(rows.map(r => [r.key, r.value]));
|
const map = Object.fromEntries(rows.map(r => [r.key, r.value]));
|
||||||
return {
|
return {
|
||||||
receiptHeader: map.receipt_header || '',
|
receiptHeader: map.receipt_header || '',
|
||||||
receiptFooter: map.receipt_footer || '',
|
receiptFooter: map.receipt_footer || '',
|
||||||
printAutoOnStop: map.print_auto_on_stop !== '0'
|
printAutoOnStop: map.print_auto_on_stop !== '0',
|
||||||
|
receiptBarcodeEnabled: map.receipt_barcode_enabled !== '0',
|
||||||
|
receiptBarcodeArticle: map.receipt_barcode_article || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +288,7 @@ async function printReceipt(vorgang) {
|
|||||||
...vorgang,
|
...vorgang,
|
||||||
header: texte.receiptHeader,
|
header: texte.receiptHeader,
|
||||||
footer: texte.receiptFooter,
|
footer: texte.receiptFooter,
|
||||||
|
barcode: texte.receiptBarcodeEnabled ? texte.receiptBarcodeArticle : '',
|
||||||
width: printer.width()
|
width: printer.width()
|
||||||
});
|
});
|
||||||
const res = await printer.print(daten);
|
const res = await printer.print(daten);
|
||||||
@@ -673,6 +685,29 @@ app.put('/api/settings', requireAuth, wrap(async (req, res) => {
|
|||||||
if (typeof b.printAutoOnStop === 'boolean') {
|
if (typeof b.printAutoOnStop === 'boolean') {
|
||||||
await setSetting('print_auto_on_stop', b.printAutoOnStop ? '1' : '0');
|
await setSetting('print_auto_on_stop', b.printAutoOnStop ? '1' : '0');
|
||||||
}
|
}
|
||||||
|
if (typeof b.receiptBarcodeEnabled === 'boolean') {
|
||||||
|
await setSetting('receipt_barcode_enabled', b.receiptBarcodeEnabled ? '1' : '0');
|
||||||
|
}
|
||||||
|
if (typeof b.receiptBarcodeArticle === 'string') {
|
||||||
|
const artikel = b.receiptBarcodeArticle.trim();
|
||||||
|
// Ein Strichcode kann nur den druckbaren ASCII-Bereich darstellen. Ein
|
||||||
|
// Umlaut wuerde stillschweigend verschwinden - dann scannt die Kasse
|
||||||
|
// spaeter einen Artikel, den es so nicht gibt. Lieber hier sagen, warum.
|
||||||
|
if (/[^\x20-\x7e]/.test(artikel)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Der Artikelname darf keine Umlaute oder Sonderzeichen enthalten - mehr kann ein Strichcode nicht abbilden.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Zu lang heisst: der Code waere breiter als das Papier und damit
|
||||||
|
// abgeschnitten. Ein abgeschnittener Code wird gar nicht erst gelesen.
|
||||||
|
const max = barcodeMaxLength(printer.width());
|
||||||
|
if (artikel.length > max) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Der Artikelname passt nicht auf die Papierbreite: höchstens ${max} Zeichen.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await setSetting('receipt_barcode_article', artikel);
|
||||||
|
}
|
||||||
|
|
||||||
// Druckerzugang von Hand pflegen - fuer einen Pi, der nicht ueber die
|
// Druckerzugang von Hand pflegen - fuer einen Pi, der nicht ueber die
|
||||||
// Einrichtung in dieser Oberflaeche aufgesetzt wurde.
|
// Einrichtung in dieser Oberflaeche aufgesetzt wurde.
|
||||||
|
|||||||
+16
-1
@@ -569,6 +569,9 @@ async function loadSettings() {
|
|||||||
document.getElementById('receiptHeader').value = settings.receiptHeader || '';
|
document.getElementById('receiptHeader').value = settings.receiptHeader || '';
|
||||||
document.getElementById('receiptFooter').value = settings.receiptFooter || '';
|
document.getElementById('receiptFooter').value = settings.receiptFooter || '';
|
||||||
document.getElementById('printAutoOnStop').checked = state.printAutoOnStop;
|
document.getElementById('printAutoOnStop').checked = state.printAutoOnStop;
|
||||||
|
document.getElementById('receiptBarcodeEnabled').checked = settings.receiptBarcodeEnabled !== false;
|
||||||
|
document.getElementById('receiptBarcodeArticle').value = settings.receiptBarcodeArticle || '';
|
||||||
|
toggleBarcodeField();
|
||||||
|
|
||||||
updateSettingsPreview();
|
updateSettingsPreview();
|
||||||
renderPrinterState(settings.printer);
|
renderPrinterState(settings.printer);
|
||||||
@@ -584,11 +587,16 @@ async function saveSettings() {
|
|||||||
const receiptHeader = document.getElementById('receiptHeader').value;
|
const receiptHeader = document.getElementById('receiptHeader').value;
|
||||||
const receiptFooter = document.getElementById('receiptFooter').value;
|
const receiptFooter = document.getElementById('receiptFooter').value;
|
||||||
const printAutoOnStop = document.getElementById('printAutoOnStop').checked;
|
const printAutoOnStop = document.getElementById('printAutoOnStop').checked;
|
||||||
|
const receiptBarcodeEnabled = document.getElementById('receiptBarcodeEnabled').checked;
|
||||||
|
const receiptBarcodeArticle = document.getElementById('receiptBarcodeArticle').value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rate = await api('/api/settings', {
|
const rate = await api('/api/settings', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ rateAmount, rateInterval, receiptHeader, receiptFooter, printAutoOnStop })
|
body: JSON.stringify({
|
||||||
|
rateAmount, rateInterval, receiptHeader, receiptFooter, printAutoOnStop,
|
||||||
|
receiptBarcodeEnabled, receiptBarcodeArticle
|
||||||
|
})
|
||||||
});
|
});
|
||||||
state.rate = rate;
|
state.rate = rate;
|
||||||
state.printAutoOnStop = rate.printAutoOnStop !== false;
|
state.printAutoOnStop = rate.printAutoOnStop !== false;
|
||||||
@@ -600,6 +608,13 @@ async function saveSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Der Artikelname interessiert nur, solange der Code auch gedruckt wird. */
|
||||||
|
function toggleBarcodeField() {
|
||||||
|
const an = document.getElementById('receiptBarcodeEnabled').checked;
|
||||||
|
document.getElementById('barcodeArticleGroup').style.opacity = an ? '1' : '0.45';
|
||||||
|
document.getElementById('receiptBarcodeArticle').disabled = !an;
|
||||||
|
}
|
||||||
|
|
||||||
async function resetSettings() {
|
async function resetSettings() {
|
||||||
if (!confirm('Tarif auf Standard zurücksetzen (€10 pro 10 Minuten)?')) return;
|
if (!confirm('Tarif auf Standard zurücksetzen (€10 pro 10 Minuten)?')) return;
|
||||||
document.getElementById('rateAmount').value = 10;
|
document.getElementById('rateAmount').value = 10;
|
||||||
|
|||||||
+17
-1
@@ -877,6 +877,22 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-row">
|
||||||
|
<input type="checkbox" id="receiptBarcodeEnabled" checked
|
||||||
|
onchange="toggleBarcodeField()">
|
||||||
|
<label for="receiptBarcodeEnabled">Strichcode für die Kasse drucken
|
||||||
|
<small>Die Kasse scannt ihn, statt den Artikel zu suchen.</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="barcodeArticleGroup">
|
||||||
|
<label for="receiptBarcodeArticle">Artikel in der Kasse</label>
|
||||||
|
<input type="text" id="receiptBarcodeArticle" maxlength="32"
|
||||||
|
placeholder="techniker" autocomplete="off" spellcheck="false">
|
||||||
|
<small>Genau der Name, unter dem die Kasse die Leistung führt.
|
||||||
|
Keine Umlaute – der Strichcode kann sie nicht abbilden.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-actions" style="margin-top: 1.25rem;">
|
<div class="settings-actions" style="margin-top: 1.25rem;">
|
||||||
<button type="button" class="btn btn-secondary" id="printerTestBtn"
|
<button type="button" class="btn btn-secondary" id="printerTestBtn"
|
||||||
onclick="testPrint()">🖨️ Probedruck</button>
|
onclick="testPrint()">🖨️ Probedruck</button>
|
||||||
@@ -950,6 +966,6 @@
|
|||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script src="/app.js?v=20260904-emoji"></script>
|
<script src="/app.js?v=20260912-barcode"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user