Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Utskriftsformatet

Kassan ritar aldrig själv. Allt som ska ut på papper - kvitto, X- och Z-rapport, journalutskrift, kortterminalens rapport, verifikation och tillgodokvitto - byggs först som en kvittolayout, typen ReceiptLayout, och skickas som JSON till skrivartjänsten receiptprinter. Det är skrivartjänsten som vet vilken skrivare som sitter i andra änden och gör om layouten till en rasterbild, till ESC/POS-text, till Worldline-terminalens textformat eller till en PDF.

Det här skiljer på två frågor som annars blandas ihop: vad som står på kvittot (kassans sak, och en del av det fiskala underlaget) och hur det ser ut på just den här skrivaren (skrivartjänstens sak). En egen skrivartjänst behöver alltså bara förstå formatet nedan; den behöver inte veta något om moms, kontrollenheter eller kvittonummer.

Typen finns i två exemplar som hålls lika för hand: rust-poc/src/receipt_layout/mod.rs på kassans sida och rust-poc/receiptprinter/src/receipt_layout_types.rs på mottagarens. Skrivartjänsten är en fristående crate som inte beror på kassan, så det är trådformatet - inte en delad Rust-typ - som är kontraktet. Ett test på kassans sida (serializes_to_designmd_documented_shape) låser formatet.

Kvittolayouten

ReceiptLayout
├── receipt_width    "flexible"
├── rows[]           text | image | qr
├── document_number  kvittonummer, 0 = inget dokument
└── footer[]         fria fotrader
fälttypbetydelse
receipt_widthsträngBreddläge. Bara "flexible" finns, och ingen renderare i receiptprinter läser fältet - bredden bestäms helt av skrivartjänstens konfiguration.
rowslista av raderKvittots innehåll uppifrån och ned. Varje rad har fältet type: "text", "image" eller "qr".
document_numberheltalKvittonumret layouten hör till (samma nummer som receipt_number i journalens PAYMENT). 0 betyder att layouten inte hör till något numrerat dokument - X/Z-rapporter, journalutskrifter och verifikationer sätter 0. Får saknas (blir 0). Receiptprinter läser det inte.
footerlista av strängarFria fotrader som skrivs ut under rows, var och en som en vänsterställd rad i stilen normal. Kommer från company_info.json. Utelämnas när den är tom; bara kvittot sätter den.

Belopp är redan formaterade strängar när de kommer hit ("35:00"), inte heltal i öre. Layouten är en presentation, inte data att räkna på.

Textrad ("type": "text")

En textrad har en av två former. En kolumn: columns har ett enda element och align säger var det hamnar. Flera kolumner: tabs_percent och alignments har ett element per kolumn. Typen tvingar inte fram den ena eller andra formen; det är en konvention som alla layoutbyggare i kassan följer. Det som avgör hur raden tolkas är om tabs_percent är tom eller inte.

fälttypbetydelse
stylesträngRadens stil, se tabellen nedan. Okända namn ritas som normal.
columnslista av strängarTexten, en sträng per kolumn.
align"left", "right", "center"Justering i enkolumnsformen. Utelämnas när den inte är satt; saknas den i enkolumnsform blir den left. Ignoreras när tabs_percent har värden.
tabs_percentlista av talVarje kolumns startposition i procent av den utskrivbara bredden. Utelämnas när den är tom.
alignmentslista av "left"/"right"/"center"Varje kolumns justering, parallell med columns. Saknade element blir left. Utelämnas när den är tom.
column_styleslista av sträng eller nullStil per kolumn, parallell med columns. null, ett saknat element eller en tom lista betyder “radens style”. Utelämnas när den är tom.
row_idheltalevent_id för den LINE_ADDED raden visar. Sätts bara av layouten för ett pågående kvitto (displayen, se Displayformatet); alla utskrivna layouter saknar det. Båda renderarna i receiptprinter ignorerar fältet.

Stilnamnen, som de tolkas av de olika renderarna:

stylerasterbild (print_layout.rs)ESC/POS-text (escpos_text.rs)Worldline (samport_printer.rs)PDF (voucher_pdf.rs)
normal24 pt, normal viktinga stilkoderinga stilkodernormal
header30 pt, fetfet (ESC E), normal storlekfetfet, större
bold_total24 pt, fetfet (ESC E)fetfet
dbl_heightdubbel höjdGS ! dubbel höjd, samma bredddubbel (höjd och bredd)dubbel höjd
dbl_widthdubbel breddGS ! dubbel bredddubbeldubbel bredd (Tz 200)
dbl_sizedubbel höjd och breddGS ! dubbel höjd och bredddubbeldubbel höjd och bredd

Worldline-terminalens skrivare har bara ett “dubbelt” läge, och det gör tecknen både högre och bredare, så alla tre dbl_* blir samma sak där.

Kassans layoutbyggare använder i dag normal, header, bold_total och dbl_size som radstil (dbl_size för rubriken KVITTO), och dbl_width som kolumnstil för beloppet på kvittots SUBTOTAL-rad.

Bildrad ("type": "image")

fälttypbetydelse
sourcesträngLogiskt namn på en bild som finns lagrad hos skrivartjänsten (se /asset/:id nedan), till exempel "logo". Aldrig bildens bytes.
align"left"/"right"/"center"Var bilden placeras.
width_percenttalBildens bredd i procent av den utskrivbara bredden. Höjden följer bildens proportioner.

En bild som inte finns lagrad, eller inte går att avkoda, ritas som en platshållarruta i rasterläget - aldrig ett avbrott, eftersom ett kvitto är ett lagstadgat dokument och inte får falla på en logga. I textläge (CHD6800) skrivs "[BILD: <source>]", utom när source är "logo" och en logga har laddats upp: då skrivs den ur skrivarens NV-minne med FS p. Worldline-terminalen hoppar över bildrader helt, och PDF:en gör det också.

QR-rad ("type": "qr")

fälttypbetydelse
datasträngDet som ska kodas. Används för tillgodokvittot.
align"left"/"right"/"center"Placering.
width_percenttalKodens sida i procent av den utskrivbara bredden (koden är kvadratisk).

Raden bär data, inte en bild: skrivaren vet sin egen punktbredd och kan rita modulerna skarpt, där en skalad bitmapp kan bli oläsbar. I rasterläget ritas koden in i bilden; går den inte att koda blir det en platshållarruta. I ESC/POS-textläget skickas Epsons QR-kommando med fast modulstorlek 6 (width_percent används inte) - koden säger själv att sekvensen inte är verifierad på en CHD6800. Worldline skriver QR med sitt eget kommando och centrerar bara när align är "center". PDF:en hoppar över QR-rader.

Kolumner och procenttabbar

tabs_percent anger var varje kolumn börjar, i procent av den utskrivbara bredden - inte hur bred den är. En rad med "tabs_percent": [0.0, 85.0] och "alignments": ["left", "right"] har första kolumnen från vänsterkanten och andra kolumnen från 85 %. Kvittots momsrader använder [5.0, 85.0] för att dra in moms och brutto en bit.

I rasterläget (layout_text_row_pixels) går kolumn i från sin tabb till nästa kolumns tabb, och den sista kolumnen till högerkanten. En vänsterställd kolumn börjar vid sin tabb, en högerställd slutar vid sitt spanns slut och en centrerad centreras i spannet. En kolumn som inte får plats klipps aldrig: nästa kolumn knuffas åt höger (minst ett mellanslag), och en högerställd text som är för lång växer åt vänster. Blir bilden bredare än pappret skalas hela bilden ned till skrivarens bredd före rastreringen. Alla kolumner på en rad delar samma baslinje, och radhöjden är den högsta av kolumnernas stilar.

I textläget (CHD6800 och Worldline, render_text_row) används procentsatserna inte alls - att listan inte är tom väljer bara flerkolumnsvägen. Skälet är skrivarnas fasta teckenrutnät och att CHD6800:ns skrivare inte förstår några positioneringskommandon. I stället fördelas en rads tecken så här:

  1. Kolumnerna behandlas från höger. Den sista kolumnen - normalt beloppet - får hela sin bredd först och kortas aldrig.
  2. Mellan två kolumner reserveras ett mellanslag.
  3. Det som blir över går till kolumnerna till vänster. Den första kolumnen - normalt en benämning - kortas av om det inte räcker.
  4. Allt överskott hamnar som utfyllnad i första kolumnen, så den sista kolumnen ligger mot högerkanten.

En enkolumnsrad kortas till radens bredd och placeras med inledande mellanslag enligt align. Bredden är 30 tecken på CHD6800 (DEFAULT_WIDTH_CHARS, uppmätt på riktig hårdvara) och 34 på Worldline-terminalen (flaggan --worldline-width-chars). En kolumn i dbl_width/dbl_size räknas som två tecken per tecken på CHD6800; på Worldline räknas alla dbl_* dubbelt, och en rad med någon dubbel kolumn får två teckens marginal eftersom terminalens dubbla tecken är något mer än dubbelt så breda. Text kodas som CP437 på CHD6800 och som latin-1 på Worldline (där ESC-tecken tas bort så att texten inte kan ge kommandon); tecken som saknas i CP437 blir ?.

En egen skrivartjänst som skriver text bör följa samma prioritering: beloppet får aldrig kortas, benämningen får det.

Hur kassan skickar

Kassan läser skrivartjänstens adress ur config/printer_service.json (printer_service.base_url, till exempel http://127.0.0.1:8080) och skickar med blockerande HTTP och 20 sekunders timeout (printer_http_sink.rs). Ordningen efter ett köp eller en rapport är:

  1. POST /print_layout med layouten.
  2. Om svaret är 2xx: POST /feed_cut med {"feed": 3, "cut": true} (RECEIPT_FEED_LINES).

Matning och kniv är avsiktligt ett eget anrop. /print_layout hör ihop med vad ett dokument innehåller; när en utskriftsomgång är slut är en annan fråga, och en anropare som vill skriva flera dokument i rad ska kunna klippa en gång på slutet.

Hur ett misslyckat anrop hanteras beror på dokumentet:

  • X- och Z-rapporter och journalutskrifter skrivs ut av kassan själv, genom dess PrintSink (receipt_sink.rs). web_till och interactive sätter dess layoutskrivare till receiptprinter (till::layout_printer_via_http), som gör de två anropen ovan. Utskriften räknas som lyckad när /print_layout svarat 2xx; en misslyckad /feed_cut efteråt loggas bara. Utfallet journalförs i X_REPORT_PRINT_ATTEMPT, Z_REPORT_PRINT_ATTEMPT och JOURNAL_PRINT_ATTEMPT (se Journalminnet). För Z-rapporten är utskriften en spärr: svarar skrivaren inte stängs inte perioden, utan kassan frågar om ett nytt försök och väntar på Ja (försök igen) eller Nej (avbryt; perioden förblir öppen).
  • Kvitton, kortterminalens rapport, verifikationen och övriga dokument är “skjut och glöm” i web_till: misslyckas utskriften loggas det, men ingenting i kassan rullas tillbaka och operatören får ingen fråga.

Efter en Z-rapport skickas också verifikationen (/print_layout), SIE-filen (/export/...) och verifikationen och Z-rapporten som PDF (/render_pdf med arkivhuvuden), i den ordningen.

Skrivartjänstens HTTP-gränssnitt

Receiptprinter lyssnar som standard på port 8080 (--http :8080). Det finns två servrar med olika uppsättning vägar: den vanliga (http_server.rs, för USB-, nätverks-, Windows- och Worldline-skrivare) och den seriella (serial_http_server.rs, för CHD6800:ns inbyggda skrivare, startas med --serial-http).

vägvanligseriellkroppsvar
POST /print_layoutjajaReceiptLayoutPrintResponse
POST /feed_cutjaja{"feed": n, "cut": bool}PrintResponse
POST /printja-PNG/JPEG, se nedanPrintResponse
POST /render_pdfja-ReceiptLayoutPDF-bytes
POST /open_drawerja-ingenPrintResponse
GET /statusja--status-JSON
GET /ja--HTML-sida som beskriver API:t
POST/GET/DELETE /asset/:idjajase nedanAssetResponse / PNG
POST/GET /export/:voucher/:name, GET /export/:voucher, GET /exportjajase nedanse nedan

Den vanliga servern har också /setup-vägarna för inställningssidan; de är inte en del av utskriftsformatet och beskrivs inte här.

Svaret PrintResponse

{"success": true, "message": "printed successfully"}
väglyckat messageHTTP-status
/print_layout, /print"printed successfully"200
/feed_cut"fed/cut successfully"200
/open_drawer"drawer opened"200

Ogiltig JSON ger 400 med {"success": false, "message": "invalid JSON: ..."}. Ett fel från skrivaren ger 500 med felet i message. Kassan tittar bara på statuskoden.

POST /print_layout

Kroppen är en ReceiptLayout. Vad som händer beror på skrivaren:

  • Rasterskrivare (USB, nätverk, Windows-kö): layouten ritas som en gråskalebild i skrivarens bredd (--width, standard 576 punkter), med det inbäddade typsnittet DejaVu Sans Mono, ditheras med Floyd-Steinberg och skickas som ESC/POS- eller StarPRNT-raster. Tjänsten matar och klipper aldrig själv efter en /print_layout; det gör anroparen med ett eget /feed_cut, som kassan alltid skickar efteråt. Flaggorna --cut och --feed gäller bara /print.
  • CHD6800 (seriell server): layouten blir ESC/POS-text enligt textläget ovan, inleds med ESC @ och val av teckentabell PC437 och avslutas med tre radmatningar. Ingen kniv.
  • Worldline-terminalen (--worldline): layouten blir Samports textformat och skickas till terminalens POST /Prints. Bildrader hoppas över; QR skrivs med terminalens eget kommando. Avslutas med tre tomma rader så att kvittot kan rivas av.

POST /feed_cut

{"feed": 3, "cut": true}. Båda fälten kan saknas och är då 0 och false; en tom kropp är en giltig no-op. Worldline-terminalen har ingen kniv och ignorerar cut (matar minst en rad).

POST /print

Den äldre vägen: kassan renderar själv och skickar en färdig bild. Antingen multipart med fältet file, eller JSON:

{"type": "png", "data": "<base64>", "cut": false, "feed": 2}

type måste finnas men används bara som etikett - bildformatet avläses ur själva bytesen. cut och feed är valfria och ersätter tjänstens standardvärden för just det jobbet. Worldline-terminalen vägrar med ett fel, eftersom den inte tar grafik.

POST /render_pdf

Samma ReceiptLayout som /print_layout, men svaret är en PDF (Content-Type: application/pdf) i stället för en utskrift. Ingen skrivare berörs, så anropet kan inte blockeras av en pågående utskrift. Remsan ritas centrerad och inramad på ett A4-ark, 80 mm bred, som text med inbäddat typsnitt. Bild- och QR-rader hoppas över; fotraderna kommer med.

Svarshuvuden:

huvudvärde
X-Pdf-Overflowedtrue om remsan inte fick plats på en sida och kapades nedtill, annars false. Uppdelning i spalter och sidor finns inte ännu.
X-Pdf-Archivedskipped när inga arkivhuvuden skickades, true när filen arkiverades, annars error: ....

Skickar anroparen både X-Export-Voucher och X-Export-Name sparas PDF:en också i exportarkivet, i katalogen för den verifikationen. Kassan använder namnen verifikat.pdf och zrapport.pdf. Ett misslyckat arkiv fäller aldrig svaret.

POST /open_drawer

Ingen kropp. Fungerar bara mot nätverksskrivare och Windows-kö (StarPRNT:s lådkommando); för USB och Worldline svarar tjänsten 500 med ett fel som säger det. Kassan anropar inte vägen i dag.

GET /status

{
  "healthy": true,
  "printer": "usb",
  "printer_connected": true,
  "usb_connected": true,
  "usb_vendor": "0519",
  "usb_product": "000b",
  "serial_connected": true,
  "serial_port": "/dev/ttyACM0",
  "ws_addr": ":8765",
  "width": 576,
  "asset_dir": "/opt/rgo/assets",
  "uptime_seconds": 512.3
}

Statuskoden är 200 när healthy är sant, annars 503, så att en övervakning kan nöja sig med koden. healthy avgörs bara av den valda skrivaren och är samma värde som printer_connected. printer säger vilken skrivare som valts och hur den kontrolleras, i samma ordning som tjänsten väljer skrivare:

printerkontroll
"worldline"TCP-anslutning till Worldline-terminalens adress lyckas (inom 0,5 s).
"windows"Windows-kön med det inställda namnet finns.
"network"TCP-anslutning till nätverksskrivaren lyckas (port 9100 om ingen anges, inom 0,5 s).
"usb"USB-skrivaren med inställt vendor/product-id finns.

usb_connected och serial_connected redovisas fortfarande för sig, men påverkar inte healthy; en installation utan seriell port eller med en annan skrivare än USB kan alltså vara frisk. Kontrollen säger att skrivaren går att nå, inte att den har papper.

/asset/:id - loggan

POST /asset/logo med {"type": "png", "data": "<base64>"} lagrar en bild under namnet logo. type får utelämnas, men anges den måste den vara png. Svaret:

{"success": true, "message": "stored logo", "width": 576, "height": 168}

GET /asset/:id returnerar PNG:en, DELETE /asset/:id tar bort den (404 om den inte fanns). På den seriella servern rastreras loggan också in i skrivarens NV-minne vid uppladdning, så att varje kvitto bara behöver skicka fyra byte för att skriva den. Det är namnet här som en bildrads source pekar på.

/export - arkivet för bokföringsfiler

Kroppen är själva filen, utan JSON eller base64 (en SIE-fil är CP437-bytes, inte UTF-8-text).

  • POST /export/:voucher/:name sparar filen. Svar: {"success": true, "message": "stored <voucher>/<name>", "path": "..."}.
  • GET /export/:voucher/:name hämtar den (application/pdf för .pdf, annars application/octet-stream).
  • GET /export/:voucher listar katalogen: {"voucher": "...", "sie": ..., "attachments": ..., "other": ...}.
  • GET /export listar alla verifikationer: {"vouchers": [...]}.

Kassan namnger katalogen <kassabeteckning>_voucher_<serie><nummer>, till exempel kassa-1_voucher_z000011, och filerna bokforing.si, verifikat.pdf och zrapport.pdf.

Exempel: ett kvitto

Så här ser kroppen till POST /print_layout ut för ett kontantköp av två likadana varor med streckkod, med logga och fottext. Raderna är de som build_receipt_layout_with_logo (receipt_layout/kvitto.rs) bygger, i samma ordning. Två slag av samma vara slås ihop till en rad med en detaljrad under; i journalen ligger de kvar som två LINE_ADDED.

{
  "receipt_width": "flexible",
  "rows": [
    {"type": "image", "source": "logo", "align": "center", "width_percent": 60.0},
    {"type": "text", "style": "dbl_size", "columns": ["KVITTO"], "align": "center"},
    {"type": "text", "style": "normal", "columns": ["Kvitto nr 42"], "align": "center"},
    {"type": "text", "style": "header", "columns": ["Exempelbutiken AB"], "align": "center"},
    {"type": "text", "style": "normal", "columns": ["Org.nr", "556000-0000"],
     "tabs_percent": [0.0, 50.0], "alignments": ["left", "left"]},
    {"type": "text", "style": "normal", "columns": ["Exempelgatan 1"], "align": "center"},
    {"type": "text", "style": "normal", "columns": ["123 45 Exempelstad"], "align": "center"},
    {"type": "text", "style": "normal", "columns": ["Kaffe", "70:00"],
     "tabs_percent": [0.0, 85.0], "alignments": ["left", "right"]},
    {"type": "text", "style": "normal", "columns": ["  2 x 35:00"], "align": "left"},
    {"type": "text", "style": "normal", "columns": ["  7300000000000"], "align": "left"},
    {"type": "text", "style": "bold_total", "columns": ["SUBTOTAL", "70:00"],
     "tabs_percent": [0.0, 85.0], "alignments": ["left", "right"],
     "column_styles": [null, "dbl_width"]},
    {"type": "text", "style": "normal", "columns": ["12% netto", "62:50"],
     "tabs_percent": [0.0, 85.0], "alignments": ["left", "right"]},
    {"type": "text", "style": "normal", "columns": ["moms", "7:50"],
     "tabs_percent": [5.0, 85.0], "alignments": ["left", "right"]},
    {"type": "text", "style": "normal", "columns": ["brutto", "70:00"],
     "tabs_percent": [5.0, 85.0], "alignments": ["left", "right"]},
    {"type": "text", "style": "bold_total", "columns": ["BETALT (KONTANT)", "70:00"],
     "tabs_percent": [0.0, 85.0], "alignments": ["left", "right"]},
    {"type": "text", "style": "normal", "columns": ["Mottaget", "100:00"],
     "tabs_percent": [0.0, 85.0], "alignments": ["left", "right"]},
    {"type": "text", "style": "normal", "columns": ["Växel", "30:00"],
     "tabs_percent": [0.0, 85.0], "alignments": ["left", "right"]},
    {"type": "text", "style": "normal", "columns": ["Kontrollenhet", "EXEMPEL00000000001"],
     "tabs_percent": [0.0, 85.0], "alignments": ["left", "right"]},
    {"type": "text", "style": "normal", "columns": ["Kontrollkod:"], "align": "left"},
    {"type": "text", "style": "normal", "columns": ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"], "align": "left"},
    {"type": "text", "style": "normal", "columns": [";BBBBBBBBBBBBBBBBBBBBBBBBBB"], "align": "left"}
  ],
  "document_number": 42,
  "footer": ["Tack för ditt köp!", "Öppet vardagar 10-18"]
}

Några saker att lägga märke till:

  • Tomma fält utelämnas: en enkolumnsrad har varken tabs_percent eller alignments, en flerkolumnsrad har inget align.
  • Kontrollkoden delas vid semikolonet; andra halvan börjar med ;.
  • Mottaget/Växel finns bara för kontant med angivet belopp.
  • Vid kortköp läggs kortterminalens egna kvittorader in som enkolumnsrader mellan betalraderna och kontrollkoden.

Svaret vid lyckad utskrift, och därefter kassans matning:

POST /print_layout  ->  200 {"success": true, "message": "printed successfully"}
POST /feed_cut      {"feed": 3, "cut": true}
                    ->  200 {"success": true, "message": "fed/cut successfully"}

Och ett fel från skrivaren:

{"success": false, "message": "open USB printer: ..."}

med status 500.

Andra dokument i samma format

Det finns inget separat format för rapporter eller verifikationer - alla är ReceiptLayout:

dokumentbyggs avanmärkning
kvittoreceipt_layout/kvitto.rsdocument_number = kvittonumret, footer från company_info.json
X/Z-rapportreceipt_layout/rapport.rs (report_data_to_receipt_layout)document_number 0, ingen fottext
journalutskriftreceipt_layout/journal.rsvarje journalrad pretty-printad som JSON, en enkolumnsrad per textrad
kortterminalens rapportreceipt_layout/kortterminal.rs
verifikationreceipt_layout/verifikation.rsskrivs ut och skickas till /render_pdf
tillgodokvittoreceipt_layout/vardehandling.rsinnehåller en QR-rad

Verifikationens PDF är alltså inte en egen A4-layout utan samma remsa som skrivs ut, ritad på ett A4-ark. Det håller papper och arkiv lika.

Kassan har också en egen, stilblind textrenderare (receipt_sink::render_receipt_layout_as_text, 35 tecken bred) som skriver kvitton och rapporter till kassans standardutdata - för rapporterna bara när ingen skrivartjänst är inställd. Den följer tabs_percent bokstavligt och struntar i stilar; den är inte en del av skrivartjänsten.