Journalminnet
Journalen är kassans fiskala minne. Varje sak som händer i kassan och som har betydelse i efterhand - en inslagen vara, en makulerad rad, en betalning, kontrollenhetens svar, en rapport - skrivs som en rad i journalen i samma ögonblick som den händer. Det är journalen, inte kassans minne eller databaser, som är sanningen om vad som registrerats: X- och Z-rapporternas siffror återskapas ur den vid omstart, och exporten till Skatteverket byggs ur den.
Därför gäller tre principer:
- Bara tillägg. En rad skrivs aldrig om och tas aldrig bort. Ett
misstag rättas med en ny händelse som pekar på den gamla
(
LINE_CORRECTED), inte genom att den gamla ändras. En ändrad fil går inte att skilja från ett manipulerat kassaregister. - Händelsen som den skedde, inte en tolkning. Två slag av samma vara
är två
LINE_ADDED, även om kvittot visar dem som en rad med antal 2. Kvittots sammanslagning är ren presentation. - Först journalen, sedan verkligheten. För de händelser som styr kassans tillstånd skrivs journalraden först, och kassan ändrar sitt tillstånd först när skrivningen lyckats.
Filformat
Journalen ligger i datakatalogens journal/. Det är
NDJSON: en JSON-objekt per rad,
avslutad med radbrytning, i UTF-8.
Det finns en fil per Z-period:
journal/journal_z000001.ndjson
journal/journal_z000002.ndjson
...
Namnet är journal_z följt av periodnumret med sex siffror
(period_file_name). Själva Z_REPORT-händelsen är den sista som
skrivs i en fil. Därefter fortsätter journalen i filen med Z-rapportens
eget löpnummer (z_report_sequence_number) plus ett. Normalt är det
samma sak som nästa fil i ordningen: fil N innehåller perioden som
avslutas av Z-rapport nummer N, och nästa händelse hamnar i fil
N+1.
Skillnaden syns efter ett avbrutet Z-försök (se
Z_REPORT_PRINT_ATTEMPT). Det tar ett Z-nummer utan att stänga
perioden, så nästa Z-rapport stänger fil N med nummer N+1, och
journalen fortsätter i fil N+2. Filnumreringen får alltså samma lucka
som Z-numren, och fil N+1 skapas aldrig. Alla läsare (omstarten,
journalutskriften och SKVFS-exporten) hoppar över en fil som saknas. När
kassan startar öppnar den filen för next_z_report_number i
counters.db och fortsätter skriva i den.
Not för äldre journaler: Kassor före 2026-10-20 räknade alltid upp filnumret med ett efter en
Z_REPORT. I sådana journaler följer filnumret inte Z-numret efter ett avbrutet Z-försök, och en period som löpte över en omstart kan vara delad på två filer. En läsare ska därför gå på händelserna, inte förutsätta att fil N slutar med Z-rapport N.
I samma katalog lägger kassan även SIE-filer och PDF-kopior av
verifikationer (voucher_z000011.si, voucher_z000011.pdf) och
SKVFS-exporter (skvfs_export_<tid>.xml). Läsarna tittar bara på
filer som matchar journal_z*.ndjson.
Gemensamma fält
Varje rad är ett platt JSON-objekt. Tre fält finns alltid, först:
| fält | typ | betydelse |
|---|---|---|
event_id | heltal | Löpnummer inom perioden. Börjar på 1 i varje ny fil. Sätts av journalen, aldrig av anroparen, och först när händelsen faktiskt accepterats. |
event_type | sträng | Händelsetypen, till exempel "LINE_ADDED". |
timestamp | sträng | Lokal tid utan tidszon, sekundprecision: "2026-10-06T16:59:26". Sätts vid själva skrivningen, inte när försöket påbörjades. |
Resten av fälten är händelsens egna och ligger på samma nivå. Deras
ordning är inte fast - de byggs ur en HashMap - så en läsare får
aldrig förlita sig på fältordning. Belopp är heltal i öre (fält som
slutar på _minor), momssatser är bråk (0.25, inte 25).
Det finns inget gemensamt kvittonummerfält. Händelser som hör till ett
visst kvitto bär receipt_number, document_number eller hänger ihop
genom ordningen i filen - se varje händelsetyp.
Ordning och perioder
En läsare ska läsa filerna i periodordning och raderna i filordning.
Ordningen i filen är den ordning händelserna skrevs; event_id stiger
normalt med den. Ett kvitto är inte en egen händelse utan ett
förlopp:
LINE_ADDED en rad slogs in (event_id 7)
LINE_ADDED (event_id 8)
LINE_CORRECTED rad 7 makulerades
PAYMENT betalning, receipt_number 35
CONTROL_UNIT_VERIFICATION kontrollenhetens svar för 35
RECEIPT_LAYOUT kvittot som det ser ut, document_number 35
RECEIPT_DISTRIBUTION papper eller elektroniskt
Alla LINE_ADDED sedan förra PAYMENT hör till kvittot som den
PAYMENT stänger. RECEIPT_DISTRIBUTION skrivs när kvittot lämnar
“slussen” - när operatören trycker KVITTO (papper) eller går vidare till
nästa kund (elektroniskt) - och kan därför komma en stund senare. Den
saknar kvittonummer och gäller det senast betalda kvittot.
En dagsavslutning ser ut så här:
CARD_TERMINAL_REPORT kortterminalens egen Z (om terminal finns)
Z_REPORT_PRINT_ATTEMPT ett eller flera utskriftsförsök
Z_REPORT sista raden i filen
--- ny fil ---
BOOKKEEPING_VOUCHER första raden i nästa period (om bokföring är på)
Verifikationen för Z-rapport N hamnar alltså som event_id 1 i fil
N+1, eftersom den byggs efter att Z_REPORT redan rullat över
journalen.
Kön (se nästa avsnitt) gör att ordningen i filen och event_id alltid
följs åt: en köad händelse skrivs före allt som kommer efter den.
Hur inget går förlorat
Skrivningen är enkel: för varje händelse öppnas periodfilen i
tilläggsläge, raden skrivs med en avslutande radbrytning, och filen
tvingas ut till disken (sync_data) innan den stängs. Först när det
lyckats räknas skrivningen som gjord. En journalrad som ett strömavbrott
kan ta tillbaka är inte en fiskal registrering.
Utöver det skyddar ordningen och beslutsträdet i journal.rs:
- Försök skriva. Finns köade händelser (se punkt 3) skrivs de
först, äldst först. Därefter skrivs den nya händelsen. Lyckas det
får händelsen sitt
event_idoch sin tid, och räknas som inträffad. Går kön inte att skriva skrivs inte heller den nya händelsen; den går då samma väg som ett misslyckat försök. - Misslyckas det frågar kassan operatören “Försök igen?”. Ja gör om försöket, så länge det behövs.
- Nej leder till frågan om händelsen ska köas. Ja lägger den i
en kö i minnet (högst 10,
MAX_QUEUE_SIZE), ger den ettevent_idoch låter kassan fortsätta som om den skrivits. Nej ångrar hela handlingen: raden slås inte in, betalningen sker inte, kvittonumret lämnas tillbaka. Är kön full stoppar kassan (HardStop).
Kön finns bara i minnet. Den töms vid nästa skrivning: varje gång
journalen ska skriva något - ett nytt record() eller ett “försök igen”
(retry()) - skrivs först alla köade händelser, i event_id-ordning.
Filen innehåller därför händelserna i event_id-ordning utan luckor,
och ingenting nytt hamnar i filen före en äldre köad händelse. Det som
fortfarande kan gå förlorat är en kö som aldrig hann skrivas: stängs
kassan medan händelser ligger i kön försvinner de, och deras event_id
blir en lucka i slutet av filen.
Not för äldre journaler: Före 2026-10-20 skrevs kön aldrig ut. En köad händelse försvann när kassan stängdes, och händelser som skrevs efter den fick högre
event_id. Journaler från den tiden kan alltså ha luckor ievent_idmitt i en fil.
Detta gäller de styrande händelserna, där kassans tillstånd ändras
först efter en lyckad skrivning: LINE_ADDED, LINE_CORRECTED,
PAYMENT, PRODUCT_CREATED, X_REPORT, Z_REPORT,
JOURNAL_PRINT_ATTEMPT och CHANGE_FUND_REGISTERED. Övriga händelser
är observerande: de skrivs med ett enkelt
försök, och skrivs inte alls om journalen redan väntar på ett beslut om
en annan händelse.
Kontrollenheten är den andra spärren. PAYMENT skrivs innan
kontrollenheten tillfrågas, men kvittot byggs, journalförs
(RECEIPT_LAYOUT) och skrivs ut först när kontrollenheten svarat ja.
Svarar den inte får operatören försöka igen; varje försök blir en egen
CONTROL_UNIT_VERIFICATION. Ger operatören upp lämnas kvittonumret
tillbaka och inget kvitto skapas - men PAYMENT ligger kvar i
journalen, eftersom journalen bara växer. En läsare som vill veta om en
betalning blev ett kvitto måste titta efter en
CONTROL_UNIT_VERIFICATION med "success": true för samma
receipt_number.
När kassan startar läser den igenom den aktuella periodfilen
(rebuild_period_state_from_journal) och räknar som kassan gör under
drift. Raderna (LINE_ADDED, med LINE_CORRECTED tillämpade) samlas
tills ett kvitto godkänts: en PAYMENT följd av en
CONTROL_UNIT_VERIFICATION för samma receipt_number med "success": true. Först då läggs kvittot till periodens momsackumulatorer, antal
kvitton, sålda varor och tjänster och försäljningen per betalsätt
(method). Ett kortköp som varken kunde verifieras eller återföras
(felet börjar med "card charge could not be voided", se
CONTROL_UNIT_VERIFICATION) räknas också, eftersom pengarna är dragna.
Ett avvisat kvitto räknas inte; dess rader står kvar som ett öppet
kvitto och räknas med den betalning som till slut godkänns. Innehåller
filen ingen CONTROL_UNIT_VERIFICATION alls (journaler från innan
händelsen fanns) räknas varje PAYMENT som ett kvitto.
Växelkassan återställs ur den senaste CHANGE_FUND_REGISTERED i filen.
En rad som inte går att tolka - till exempel en halvskriven sista rad
efter ett strömavbrott - hoppas över, och numreringen fortsätter efter
det högsta event_id som finns i filen.
Läsarna
| läsare | läser | hur |
|---|---|---|
X/Z-rapport (till.rs, report.rs) | indirekt | Rapporterna räknar ur kassans ackumulatorer i minnet, som byggs upp ur journalen vid start (se ovan). Grand total kommer ur counters.db. |
Journalutskrift (receipt_layout/journal.rs) | alla händelser i perioderna från-till | Varje rad pretty-printas som JSON med två blankstegs indrag, direkt ur råtexten så att fältordningen bevaras, en utskriftsrad per textrad. Inget tolkas, ingenting märks om. Trasiga rader hoppas över. |
SKVFS-export (skvfs_xml_export.rs) | period 1 till aktuell | Bygger kvitton ur LINE_ADDED (+ äldre DISCOUNT_APPLIED) fram till PAYMENT, med LINE_CORRECTED tillämpade på samma sätt som vid omstarten (en borttagen rad försvinner, new_amount_minor ersätter radens belopp). Kompletterar med CONTROL_UNIT_VERIFICATION och RECEIPT_DISTRIBUTION, tar med X_REPORT/Z_REPORT och hämtar kontrollenhetens tillverkningsnummer ur den senaste CONTROL_UNIT_IDENTITY. Allt annat exporteras inte, enligt SKVFS 2021:16 3 kap. 3 §. Startas med koden 2027 + SUBTOTAL utanför ett kvitto. |
Kontoplanens förhandsvisning (web_setup/konton.rs) | senaste Z_REPORT | Bygger om rapportdata ur sales_by_group_and_rate för att visa hur kontona skulle bli. |
| Bokföring | - | Verifikationen byggs ur Z-rapportens data i minnet och journalförs som BOOKKEEPING_VOUCHER; den läses inte tillbaka ur journalen. R-GO Fortnox läser exportarkivet, inte journalen. |
Händelsetyperna
JournalEventType i journal.rs har 18 typer. 16 av dem skrivs av
kassan; RECEIPT_OPENED och RECEIPT_CLOSED finns i typen men skrivs
aldrig. Dessutom förekommer tre typer i äldre journaler från
Python-versionen, se sist.
| händelse | skrivs när | styrande |
|---|---|---|
LINE_ADDED | en rad slås in | ja |
LINE_CORRECTED | en rad makuleras | ja |
PAYMENT | ett kvitto betalas | ja |
CONTROL_UNIT_VERIFICATION | kontrollenheten svarar på en betalning | nej |
RECEIPT_LAYOUT | ett kvitto godkänts av kontrollenheten | nej |
RECEIPT_DISTRIBUTION | kvittot lämnar slussen | nej |
X_REPORT | X-rapport tas | ja |
X_REPORT_PRINT_ATTEMPT | efter X-rapporten | nej |
Z_REPORT | Z-rapport tas, rullar över perioden | ja |
Z_REPORT_PRINT_ATTEMPT | varje utskriftsförsök av Z-rapporten | nej |
JOURNAL_PRINT_ATTEMPT | journalutskrift | ja |
CONTROL_UNIT_IDENTITY | kassan startar | nej |
BOOKKEEPING_VOUCHER | efter Z-rapporten, om bokföring är på | nej |
CARD_TERMINAL_REPORT | kortterminalens X/Z/avstämning | nej |
PRODUCT_CREATED | en okänd streckkod läggs upp som vara | ja |
CHANGE_FUND_REGISTERED | växelkassan registreras | ja |
RECEIPT_OPENED | aldrig | - |
RECEIPT_CLOSED | aldrig | - |
Exemplen nedan är riktiga rader ur testjournaler, med namn,
organisationsnummer, serienummer, kontrollkoder och terminal-id utbytta
mot platshållare. Exemplen för RECEIPT_LAYOUT och *_PRINT_ATTEMPT
är dessutom omskrivna till layoutens nuvarande form.
LINE_ADDED
En rad slogs in - antingen ett belopp på en varugruppstangent eller en vara från varuregistret via streckkod. En tryckning, en händelse.
| fält | typ | betydelse |
|---|---|---|
product_group_number | heltal | Varugruppen. |
product_group_name | sträng | Varugruppens namn vid en gruppförsäljning; vid streckkodsförsäljning artikelns namn (fältnamnet är historiskt). |
amount_minor | heltal | Radens totalbelopp i öre (antal × styckpris). |
quantity | heltal | Antal. Finns bara när det inte är 1; saknas det är antalet 1. |
unit_price_minor | heltal | Styckpris i öre. Finns bara tillsammans med quantity; saknas det är styckpriset lika med amount_minor. |
vat_rate | tal | Momssatsen vid försäljningen, som bråk. Sparas så att en senare ändring av varugruppen inte ändrar historiken. |
is_service | bool | Om gruppen räknas som tjänst (för X/Z:s antal varor och tjänster). |
sku | sträng | Artikelns identitet. Bara vid streckkodsförsäljning. |
barcode | sträng | Artikelns första registrerade streckkod. Bara vid streckkodsförsäljning, och bara om artikeln har en. |
Radens event_id är radens identitet: det är vad LINE_CORRECTED
pekar på och vad displayen kallar row_id.
{"event_id":7,"event_type":"LINE_ADDED","timestamp":"2026-10-06T16:59:26","product_group_number":2,"product_group_name":"Grupp 2","vat_rate":0.25,"is_service":false,"amount_minor":12400}
{"event_id":83,"event_type":"LINE_ADDED","timestamp":"2026-10-13T10:25:25","unit_price_minor":4500,"product_group_number":6,"product_group_name":"Grupp 6","vat_rate":0.25,"is_service":false,"amount_minor":18000,"quantity":4}
{"event_id":135,"event_type":"LINE_ADDED","timestamp":"2026-09-29T17:31:39","amount_minor":20000,"product_group_number":1,"product_group_name":"Grupp 1","is_service":false,"sku":"1234567890999","vat_rate":0.25,"barcode":"1234567890999"}
LINE_CORRECTED
En tidigare inslagen rad på det pågående kvittot ändras eller tas bort.
Den ursprungliga LINE_ADDED står kvar; det här är ett tillägg om den.
| fält | typ | betydelse |
|---|---|---|
corrects_event_id | heltal | event_id för den LINE_ADDED som rättas, i samma period. |
removed | bool | true: raden är makulerad. |
new_amount_minor | heltal | Nytt belopp i öre för raden. |
Händelsen har antingen removed eller new_amount_minor. I praktiken
skrivs bara makuleringar: BACKA tar bort den markerade raden, eller den
n:e raden bakifrån. Funktionen som skriver new_amount_minor
(Till::correct_line) finns, och både omstartsåterskapandet och
SKVFS-exporten läser fältet, men den anropas inte från något
tangentflöde i dag. Makulering är bara möjlig innan betalningen
påbörjats.
{"event_id":143,"event_type":"LINE_CORRECTED","timestamp":"2026-10-19T17:15:48","removed":true,"corrects_event_id":140}
PAYMENT
Kvittot betalas och får sitt kvittonummer. Stänger kvittot: alla
LINE_ADDED sedan förra PAYMENT hör hit. Ett kvitto betalas alltid
med ett betalsätt och en PAYMENT - delbetalning finns inte, bland
annat för att SKVFS-exporten och periodåterskapandet antar en PAYMENT
per kvitto.
| fält | typ | betydelse |
|---|---|---|
method | sträng | Betalsättets fasta kod: CASH, CARD, SWISH, INVOICE eller CREDIT (tillgodo). Översätts aldrig; visningsnamnet (KONTANT osv.) står bara på kvittot. |
amount_minor | heltal | Betalt belopp i öre (kvittots återstående belopp). |
tendered_minor | heltal eller null | Mottaget belopp vid kontant när operatören slagit in det; annars null. Alla andra betalsätt har null. |
receipt_number | heltal | Kvittonumret (löpnummer ur counters.db, taget innan händelsen skrivs). |
Vid kortbetalning har kortterminalen redan dragit pengarna när
PAYMENT skrivs; kortets uppgifter journalförs inte som egna fält
utan finns som textrader i RECEIPT_LAYOUT. INVOICE fakturerar inte -
kassan lämnar ett underlag och skriver en följesedel.
Se avsnittet om kontrollenheten ovan: en PAYMENT som aldrig fick ett
godkännande ligger kvar, och dess kvittonummer kan återanvändas av nästa
betalning.
{"event_id":8,"event_type":"PAYMENT","timestamp":"2026-10-06T16:59:28","tendered_minor":null,"amount_minor":12400,"method":"CASH","receipt_number":35}
{"event_id":66,"event_type":"PAYMENT","timestamp":"2026-10-13T10:23:07","receipt_number":5,"amount_minor":56900,"method":"SWISH","tendered_minor":null}
CONTROL_UNIT_VERIFICATION
Kontrollenhetens (CleanCash) svar på en betalning. En per försök - blir det omförsök finns flera för samma kvitto, och det är den sista som gäller.
| fält | typ | betydelse |
|---|---|---|
receipt_number | heltal | Kvittot det gäller. |
success | bool | Om kontrollenheten godkände kvittot. |
control_unit_id | sträng | Det id kontrollenheten själv skickar i svaret, om något; ofta tom sträng. Bara vid success: true. |
control_code | sträng | Kontrollkoden som skrivs på kvittot (59 tecken från en riktig CleanCash: 32 + ; + 26). Bara vid success: true. |
error | sträng | Felet. Bara vid success: false. |
Ett specialfall: om operatören ger upp efter ett kortköp och kortköpet
inte går att återkalla i terminalen, skrivs en extra
CONTROL_UNIT_VERIFICATION med success: false och felet "card charge could not be voided after control unit rejection: ...". Försäljningen
räknas då som gjord men inget kvitto skapas. Omstarten känner igen
felet och räknar försäljningen på samma sätt.
{"event_id":9,"event_type":"CONTROL_UNIT_VERIFICATION","timestamp":"2026-10-06T16:59:28","success":true,"receipt_number":35,"control_code":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA;BBBBBBBBBBBBBBBBBBBBBBBBBB","control_unit_id":""}
{"event_id":21,"event_type":"CONTROL_UNIT_VERIFICATION","timestamp":"2026-09-03T18:59:41","success":false,"error":"RH NAK: 003","receipt_number":15}
RECEIPT_LAYOUT
Kvittot som det ser ut - samma layout som skickas till skrivaren, inklusive loggans bildrad. Skrivs direkt efter ett godkännande från kontrollenheten, innan kvittot hamnar i slussen. Gör att journalen innehåller vad kunden faktiskt fick, inte bara siffrorna bakom.
| fält | typ | betydelse |
|---|---|---|
receipt_width | sträng | "flexible". |
rows | lista | Kvittots rader, se nedan. |
document_number | heltal | Kvittonumret, samma som PAYMENT:s receipt_number. |
footer | lista av strängar | Kvittots fotrader. Saknas när kvittot inte har några. |
Fälten är layoutens egen JSON-form, exakt samma som kroppen till
POST /print_layout i utskriftsformatet, bara
utplattad bland händelsens övriga fält. Allt som står där gäller också
här: tomma fält utelämnas (en enkolumnsrad har varken tabs_percent
eller alignments, en flerkolumnsrad saknar align), och
column_styles och footer följer med. En kolumnstil som det dubbelt
breda beloppet på SUBTOTAL syns alltså i journalen. row_id sätts
aldrig på ett utskrivet kvitto och förekommer därför inte.
{"event_id":94,"event_type":"RECEIPT_LAYOUT","timestamp":"2026-10-14T16:09:53","receipt_width":"flexible","document_number":9,"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 9"],"align":"center"},{"type":"text","style":"normal","columns":["Grupp 6","12:00"],"tabs_percent":[0.0,85.0],"alignments":["left","right"]},{"type":"text","style":"bold_total","columns":["SUBTOTAL","12:00"],"tabs_percent":[0.0,85.0],"alignments":["left","right"],"column_styles":[null,"dbl_width"]}],"footer":["Tack för ditt köp!"]}
(Förkortad: en riktig rad har alla kvittots rader, med företagsuppgifter, moms, betalning och kontrollkod.)
Not för äldre journaler: Före 2026-10-20 byggdes journalens form för hand. Där har varje textrad alltid
align,tabs_percentochalignments(alignärnulli flerkolumnsform och tomma listor skrivs som[]), medancolumn_styles,row_idochfootersaknas. En läsare som ska klara båda formerna kan behandlanull,[]och ett saknat fält som samma sak.
RECEIPT_DISTRIBUTION
Hur det betalda kvittot lämnade slussen. Lagen kräver att ett kvitto skapas, inte att det skrivs ut.
| fält | typ | betydelse |
|---|---|---|
status | sträng | "PAPER" - operatören tryckte KVITTO (eller kassan är inställd på att alltid skriva ut). "ELECTRONIC" - operatören gick vidare utan att begära papper. |
destination | sträng | Bara vid ELECTRONIC: vart kvittot gick. I dag alltid "grön papperskorg", en mottagare som tar emot men inte sparar. |
Händelsen saknar kvittonummer och gäller det senast betalda kvittot.
PAPER betyder att utskrift valdes; om skrivaren sedan lyckades
journalförs inte. Ett kvitto som ligger i slussen när kassan stängs får
ingen RECEIPT_DISTRIBUTION.
{"event_id":11,"event_type":"RECEIPT_DISTRIBUTION","timestamp":"2026-10-06T16:59:31","status":"PAPER"}
{"event_id":6,"event_type":"RECEIPT_DISTRIBUTION","timestamp":"2026-10-05T12:32:50","destination":"grön papperskorg","status":"ELECTRONIC"}
X_REPORT och Z_REPORT
Rapportens siffror, med samma fält för X och Z. Typen avgör vilken det
är. X_REPORT ändrar ingenting; Z_REPORT avslutar perioden, och att
den skrivs är det som rullar över journalen till nästa fil. Fälten
motsvarar SKVFS 2014:9 7 kap. 2 § (bokstäverna a-v), plus
sales_by_group_and_rate för bokföringen.
| fält | typ | betydelse |
|---|---|---|
company_name | sträng | Företagets namn (a). |
organisation_number | sträng | Organisationsnummer med bindestreck (a). |
produced_at | sträng | När rapporten togs fram, samma form som timestamp (b). |
register_designation | sträng | Kassabeteckning (d). |
total_sales_minor | heltal | Periodens försäljning (e). |
total_sales_by_main_group_minor | objekt | Per huvudgrupp (f). Huvudgrupper finns inte - alltid {}. |
vat_by_rate | lista | Moms per sats (g): {"vat_rate": tal, "vat_minor": heltal, "net_minor": heltal}. |
sales_by_group_and_rate | lista | Per varugrupp och momssats: {"group_number", "vat_rate", "debit_gross_minor", "debit_vat_minor", "credit_gross_minor", "credit_vat_minor"}. Inget SKVFS-fält; behövs för att kunna återskapa en verifikation ur journalen. Kreditsidan är alltid 0 eftersom returer inte finns. |
change_fund_minor | heltal | Växelkassa (h); 0 om ingen registrerats. |
items_sold_count | heltal | Antal sålda varor (i) - summa av antal, inte antal rader. |
services_sold_count | heltal | Antal sålda tjänster (j). |
receipt_count | heltal | Antal kvitton (k). |
drawer_openings_count | heltal | Lådöppningar (l). Alltid 0. |
receipt_copies_count, receipt_copies_total_minor | heltal | Kvittokopior (m). Alltid 0. |
training_mode_receipts_count, training_mode_total_minor | heltal | Övningsläge (n). Alltid 0. |
sales_by_payment_method_minor | objekt | Per betalsätt (o), nyckel = method-kod: {"CASH": 66600}. |
return_count, return_total_minor | heltal | Returer (p). Alltid 0. |
discount_total_minor | heltal | Rabatter (q). Alltid 0. |
other_reduction_count, other_reduction_total_minor | heltal | Övriga minskningar (r). Alltid 0. |
unfinished_sale_count, unfinished_sale_total_minor | heltal | Oavslutade försäljningar (s). Alltid 0. |
grand_total_sales_minor | heltal | Grand total försäljning (t), ur counters.db plus periodens. |
grand_total_returns_minor | heltal | Grand total retur (u). |
grand_total_net_minor | heltal | Grand total netto (v). |
z_report_sequence_number | heltal eller null | Z-rapportens löpnummer; null på X. |
control_unit_serial_number | sträng | Kontrollenhetens tillverkningsnummer; tom på X. |
control_unit_model | sträng | Kontrollenhetens modell; tom på X. |
control_unit_version | sträng | Kontrollenhetens version; tom på X. |
Fält som anges “alltid 0” finns för att formatet ska vara komplett; kassan har ännu ingen funktion som räknar dem.
{"event_id":18,"event_type":"Z_REPORT","timestamp":"2026-10-06T16:49:30","total_sales_by_main_group_minor":{},"register_designation":"Kassa 1","control_unit_version":"1.1","other_reduction_count":0,"grand_total_sales_minor":1155310,"produced_at":"2026-10-06T16:49:30","training_mode_total_minor":0,"receipt_count":2,"z_report_sequence_number":11,"services_sold_count":0,"drawer_openings_count":0,"control_unit_serial_number":"EXEMPEL00000000001","other_reduction_total_minor":0,"sales_by_payment_method_minor":{"CASH":66600},"discount_total_minor":0,"receipt_copies_total_minor":0,"unfinished_sale_count":0,"training_mode_receipts_count":0,"change_fund_minor":0,"return_count":0,"organisation_number":"556000-0000","receipt_copies_count":0,"total_sales_minor":66600,"unfinished_sale_total_minor":0,"vat_by_rate":[{"vat_rate":0.25,"vat_minor":13320,"net_minor":53280}],"grand_total_returns_minor":0,"items_sold_count":3,"sales_by_group_and_rate":[{"group_number":2,"vat_rate":0.25,"debit_gross_minor":11100,"debit_vat_minor":2220,"credit_gross_minor":0,"credit_vat_minor":0},{"group_number":4,"vat_rate":0.25,"debit_gross_minor":55500,"debit_vat_minor":11100,"credit_gross_minor":0,"credit_vat_minor":0}],"control_unit_model":"Exempelmodell","grand_total_net_minor":1155310,"return_total_minor":0,"company_name":"Exempelbutiken AB"}
En X_REPORT har samma fält med "z_report_sequence_number":null och
tomma kontrollenhetsfält.
X_REPORT_PRINT_ATTEMPT
Skrivs efter X_REPORT, med rapporten som den ritades. X-rapporten har
ingen omförsöksdialog: ett försök, utfallet journalförs.
| fält | typ | betydelse |
|---|---|---|
receipt_width, rows, document_number | Rapportens layout, i samma form som RECEIPT_LAYOUT. document_number är 0. | |
success | bool | Om utskriftsförsöket lyckades. |
Rapporter och journalutskrifter skrivs ut genom kassans PrintSink
(receipt_sink.rs). När en skrivartjänst är inställd
(config/printer_service.json) sätter både web_till och
interactive dess layoutskrivare till receiptprinter
(till::layout_printer_via_http): layouten skickas med
POST /print_layout och avslutas med POST /feed_cut. success speglar
alltså den riktiga utskriften: true när receiptprinter svarat 2xx på
/print_layout. En misslyckad matning efteråt loggas men ändrar inte
utfallet. Utan inställd skrivartjänst skriver PrintSink texten till
kassans standardutdata, och då lyckas försöket nästan alltid. Samma sak
gäller Z_REPORT_PRINT_ATTEMPT och JOURNAL_PRINT_ATTEMPT.
Not för äldre journaler: Före 2026-10-20 gällde
successalltid den interna textutskriften till standardutdata, och den riktiga utskriften till receiptprinter gjordes efteråt utan att journalföras. I sådana journaler sägersuccess: truealltså inget om papperet.
{"event_id":6,"event_type":"X_REPORT_PRINT_ATTEMPT","timestamp":"2026-10-06T16:52:12","receipt_width":"flexible","document_number":0,"success":true,"rows":[{"type":"text","style":"header","columns":["X-DAGRAPPORT"],"align":"center"}]}
(Förkortad.)
Z_REPORT_PRINT_ATTEMPT
Ett per utskriftsförsök av Z-rapporten. Z-rapporten måste skrivas ut
innan Z_REPORT journalförs. Svarar skrivaren inte frågar kassan
“Z-rapport: försök skriva ut igen?” och väntar på Ja eller Nej; varje
försök journalförs. Ger operatören upp (Nej) skrivs ingen Z_REPORT och
perioden förblir öppen - men Z-numret är redan taget och lämnas inte
tillbaka, så löpnumret får en lucka, och därmed också
journalfilernas numrering (se Filformat). Det är avsiktligt: luckan är
spåret av ett övergivet försök.
| fält | typ | betydelse |
|---|---|---|
receipt_width, rows, document_number | Rapportens layout, i samma form som RECEIPT_LAYOUT. document_number är 0. | |
z_report_number | heltal | Z-numret försöket gäller. |
success | bool | Om försöket lyckades (se X_REPORT_PRINT_ATTEMPT om vad som räknas). |
Om just den här loggningen inte kan skrivas överges den tyst, så att
journalen är fri för den Z_REPORT som måste skrivas.
{"event_id":17,"event_type":"Z_REPORT_PRINT_ATTEMPT","timestamp":"2026-10-06T16:49:30","z_report_number":11,"document_number":0,"receipt_width":"flexible","success":true,"rows":[{"type":"text","style":"header","columns":["Z-DAGRAPPORT"],"align":"center"},{"type":"text","style":"normal","columns":["Löpnummer","11"],"tabs_percent":[0.0,50.0],"alignments":["left","left"]}]}
(Förkortad.)
JOURNAL_PRINT_ATTEMPT
Att journalen skrevs ut. Bara faktumet loggas, inte innehållet - det finns ju redan i journalen. Utskriften görs utan siffror för den aktuella perioden, eller med två Z-nummer för ett intervall.
| fält | typ | betydelse |
|---|---|---|
from_z | heltal | Första perioden. |
to_z | heltal | Sista perioden (högst den aktuella). |
success | bool | Om utskriften lyckades (se X_REPORT_PRINT_ATTEMPT om vad som räknas). |
Händelsen är styrande: kan den inte skrivas får operatören samma omförsöksdialog som för en betalning, eftersom en journalutskrift i sig är ett spår av att journalen lästes.
{"event_id":3,"event_type":"JOURNAL_PRINT_ATTEMPT","timestamp":"2026-09-03T15:19:03","from_z":1,"to_z":3,"success":true}
CONTROL_UNIT_IDENTITY
Kontrollenhetens identitet, frågad vid varje start av kassan (CCSP IQ/IR), lyckad eller inte. SKVFS-exporten tar tillverkningsnumret ur den senaste lyckade, och vägrar exportera om ingen finns.
| fält | typ | betydelse |
|---|---|---|
success | bool | Om enheten svarade. |
manufacturing_id | sträng | Tillverkningsnummer. Bara vid success: true. |
protocol_version | sträng | CCSP-protokollversion. Bara vid success: true. |
firmware_version | sträng | Firmware. Bara vid success: true. |
error | sträng | Felet. Bara vid success: false. |
{"event_id":17,"event_type":"CONTROL_UNIT_IDENTITY","timestamp":"2026-10-06T17:01:02","success":true,"manufacturing_id":"EXEMPEL00000000001","firmware_version":"R1.07 L1","protocol_version":"2"}
{"event_id":96,"event_type":"CONTROL_UNIT_IDENTITY","timestamp":"2026-10-19T16:55:30","success":false,"error":"connect ws://127.0.0.1:8765/ws: Connection refused (os error 111)"}
BOOKKEEPING_VOUCHER
Verifikationen för bokföringen, byggd ur en Z-rapport. Själva verifikationen journalförs, inte bara filnamnet - filer kan tas bort, journalen finns kvar. Skrivs bara när kontoplanen är påslagen, och hamnar först i nästa periods fil.
| fält | typ | betydelse |
|---|---|---|
success | bool | true om verifikationen byggdes och SIE-filen skrevs. |
series | sträng | Verifikationsserie från kontoplanen, t.ex. "Z". |
number | heltal | Verifikationsnummer = Z-rapportens löpnummer. |
date | sträng | YYYYMMDD, ur rapportens produced_at. |
text | sträng | "Dagsrapport Z<nr>, <kassabeteckning>", med (DIFFERENS) om den inte balanserade. |
imbalance_minor | heltal | Debet minus kredit innan differensraden lades till; 0 om den balanserade av sig själv. |
sie_file | sträng | SIE-filens namn i journalkatalogen, t.ex. "voucher_z000011.si". |
lines | lista | Konteringsraderna: {"account": sträng, "debit_minor": heltal, "credit_minor": heltal, "text": sträng}. |
error | sträng | Finns när något gick fel. |
Två former: kunde verifikationen inte byggas alls (t.ex. ett fel i
kontoplanen) innehåller händelsen bara "success": false och error.
Byggdes den men SIE-filen kunde inte skrivas finns alla fält, med
"success": false och error. Z-rapporten fälls aldrig av ett fel här.
{"event_id":1,"event_type":"BOOKKEEPING_VOUCHER","timestamp":"2026-10-06T16:49:30","date":"20261006","series":"Z","imbalance_minor":0,"lines":[{"account":"1910","debit_minor":66600,"credit_minor":0,"text":"Försäljning KONTANT"},{"account":"2611","debit_minor":0,"credit_minor":13320,"text":"Utgående moms 25 %"},{"account":"3001","debit_minor":0,"credit_minor":53280,"text":"Försäljning 25 %"}],"text":"Dagsrapport Z11, Kassa 1","number":11,"sie_file":"voucher_z000011.si","success":true}
CARD_TERMINAL_REPORT
En rapport som körts i kortterminalen: dess X, dess Z eller en avstämning mot banken. Terminalens siffror sparas separat från kassans så att de kan jämföras. Kassans X kör terminalens X först, kassans Z kör terminalens Z först (innan något fiskalt börjar), och avstämningen har en egen tangent. En kassa utan kortterminal journalför ingenting här.
| fält | typ | betydelse |
|---|---|---|
kind | sträng | "X", "Z" eller "RECONCILIATION". |
success | bool | Om terminalen levererade rapporten. |
site | sträng | Terminalens butiks-id. Vid success: true. |
session | sträng | Terminalens period/session. Vid success: true. |
terminal_timestamp | sträng | Terminalens egen tid, som terminalen skickar den (YYYYMMDDHHMMSS i exemplen). Vid success: true. |
issuers | lista | Per kortutgivare: {"issuer_id": sträng, "transaction_count": heltal, "total_minor": heltal, "cash_minor": heltal, "deposit_minor": heltal}. cash_minor och deposit_minor ingår redan i total_minor. |
print_lines | lista av strängar | Terminalens egen utskrift, rad för rad, oförändrad. |
info | sträng | Fritext från värden. Bara när terminalen skickade någon. |
error | sträng | Felet. Bara vid success: false. |
{"event_id":273,"event_type":"CARD_TERMINAL_REPORT","timestamp":"2026-10-02T17:30:42","kind":"RECONCILIATION","site":"000000","terminal_timestamp":"20261002173041","issuers":[{"issuer_id":"05","transaction_count":0,"total_minor":0,"cash_minor":0,"deposit_minor":0},{"issuer_id":"41","transaction_count":9,"total_minor":118700,"cash_minor":0,"deposit_minor":0}],"success":true,"print_lines":["TERM: 00000000-000000","2026-10-02 17:30","VALUTA: SEK","AVST. PERIOD: 439","","DAGSAVSLUT","","STANDARD 9","BELOPP= 1187,00"],"session":"439"}
{"event_id":4,"event_type":"CARD_TERMINAL_REPORT","timestamp":"2026-10-06T16:52:12","error":"kortterminalen är inte ansluten","success":false,"kind":"X"}
(Den första raden är förkortad: en utgivare och terminalens sista utskriftsrader är borttagna.)
PRODUCT_CREATED
En okänd streckkod lästes in och operatören lade upp den som vara (pris
och varugrupp). Varuregistret i SQLite får ändras; journalen är beviset
på att en vara skapades och hur den såg ut då. Skrivs omedelbart före
den LINE_ADDED som säljer varan.
| fält | typ | betydelse |
|---|---|---|
sku | sträng | Artikelns identitet - den lästa koden. |
barcode | sträng | Den lästa streckkoden. |
product_group_number | heltal | Varugruppen. |
name | sträng | Varans namn, ärvt från varugruppen. |
sale_price_minor | heltal | Försäljningspris i öre. |
purchase_price_minor | heltal | Inköpspris i öre, räknat ur gruppens marginal. |
margin_percent | tal | Marginal i procent. |
vat_rate | tal | Momssats som bråk. |
discountable | bool | Om varan får rabatteras. |
Bara uppläggning från kassan skrivs. Varor som skapas, ändras eller tas
bort i inställningsprogrammet (web_setup, produkter.rs) skriver
ingen journalhändelse alls - trots att kommentaren vid
ProductCreated i journal.rs säger att varje ändring i
artikelregistret ska journalföras.
{"event_id":134,"event_type":"PRODUCT_CREATED","timestamp":"2026-09-29T17:31:39","purchase_price_minor":15385,"barcode":"1234567890999","sku":"1234567890999","margin_percent":30.0,"sale_price_minor":20000,"vat_rate":0.25,"discountable":true,"name":"Grupp 1","product_group_number":1}
CHANGE_FUND_REGISTERED
Växelkassan för perioden. Får registreras om fritt fram till periodens första köp, sedan är den låst tills nästa Z-rapport.
| fält | typ | betydelse |
|---|---|---|
change_fund_minor | heltal | Växelkassan i öre. |
replaced_minor | heltal | Det tidigare registrerade beloppet, när en registrering ersätter en annan i samma period. |
Händelsen är styrande: kassan sätter växelkassan i minnet först när
händelsen skrivits. Misslyckas skrivningen får operatören samma
omförsöksdialog som för en betalning, och lyckas ett senare “försök
igen” (eller köas händelsen) registreras beloppet då. Vid omstart
återställs växelkassan ur den senaste CHANGE_FUND_REGISTERED i
periodens fil.
{"event_id":2,"event_type":"CHANGE_FUND_REGISTERED","timestamp":"2026-10-06T16:51:51","change_fund_minor":111100}
{"event_id":3,"event_type":"CHANGE_FUND_REGISTERED","timestamp":"2026-10-06T16:52:08","replaced_minor":111100,"change_fund_minor":123400}
RECEIPT_OPENED och RECEIPT_CLOSED
Finns i JournalEventType men skrivs aldrig. Ett kvittos början och
slut framgår av LINE_ADDED och PAYMENT.
Äldre typer: DISCOUNT_APPLIED, X_REPORT_LAYOUT, Z_REPORT_LAYOUT
Journaler från den tidigare Python-versionen (ecrlogik/journal.py) kan
innehålla tre typer som Rust-kassan inte skriver. De syns bland annat i
testjournaler/.
DISCOUNT_APPLIED- en procentrabatt på en rad:corrects_event_id(radensLINE_ADDED),percent,discount_minor. SKVFS-exporten läser den fortfarande och lägger rabatten på raden. Rabatt finns inte i Rust-kassan.X_REPORT_LAYOUT,Z_REPORT_LAYOUT- rapportens layout (receipt_width,rows), ersatta av layouten i*_PRINT_ATTEMPT.
Dessa journaler skrevs också med blanksteg efter : och ,; läsare ska
inte förutsätta kompakt JSON.
Värt att veta för en läsare
- Värdehandlingar journalförs inte som egna händelser. Utfärdade och
inlösta tillgodokvitton finns i
values.db. I journalen syns en inlösen bara somPAYMENTmed"method": "CREDIT". - Kunddisplayen och displaytillståndet journalförs inte.
- Omstart läser den öppna periodens fil. Momsackumulatorerna,
antalen, försäljningen per betalsätt och växelkassan återskapas ur
den nyaste journalfilen om den inte är stängd med en
Z_REPORT, annars ur filen förnext_z_report_number(journal::open_period_number). Efter ett avbrutet Z-försök harnext_z_report_numberredan gått förbi den öppna periodens fil; då fortsätter kassan i den öppna filen. - Ett öppet kvitto kommer tillbaka vid omstart.
LINE_ADDEDefter det senaste godkända kvittot, medLINE_CORRECTEDtillämpade, läggs tillbaka på kvittot med sinaevent_id. Raderna står alltså kvar i kassan och kommer med på nästaPAYMENT- samma kvitto som omstarten och SKVFS-exporten bygger ur journalen. Före 2026-10-21 försvann raderna ur kassan men räknades ändå in i nästa betalda kvitto vid omstart. - Elektroniska kvitton i SKVFS-exporten. Exporten markerar ett
kvitto som elektroniskt (
ElektronisktKassakvitto) närRECEIPT_DISTRIBUTIONharstatus"ELECTRONIC", eller ett värde som börjar påelektroni äldre journaler.