# Koncept fiskalizacije — NTech + L-PFR Dokument opisuje arhitekturu fiskalizacije prema Tehničkom vodiču za L-PFR/ESIR i plan implementacije za NTech + Fisk mock server. --- ## 1. Učesnici u sistemu ``` ┌──────────┐ zahtev za ┌──────────┐ potpisivanje ┌──────────┐ │ NTech │─── fiskalizaciju ───→│ L-PFR │─── (APDU komande)──→│ Kartica │ │ (ESIR) │ │ (mock) │ │ (BE) │ │ │←── fiskalni račun ──│ │←── potpisani podaci─│ │ └──────────┘ └──────────┘ └──────────┘ │ │ paketi za iščitavanje ↓ ┌──────────┐ │ SUF │ │ (PU RS) │ └──────────┘ ``` | Komponenta | Šta je | Gde se nalazi | |---|---|---| | **ESIR** (NTech) | Elektronski sistem za izdavanje računa — kasa, POS | Kod obveznika | | **L-PFR** (mock) | Lokalni procesor fiskalnih računa — "crna kutija" | Kod obveznika | | **BE** (kartica) | Bezbednosni element na pametnoj kartici | Kod obveznika (fizički) | | **SUF** | Sistem Uprave za Fiskalizaciju — Poreska uprava | Cloud PU RS | ## 2. Bezbednosni element — pametna kartica (BE) Kartica je **jedini izvor identiteta** obveznika u sistemu fiskalizacije. Nju izdaje Poreska uprava, personalizovana je za konkretnog obveznika i poslovni prostor. ### 2.1 Šta se nalazi na kartici | Podatak | Opis | Primer | |---|---|---| | **JID** | Jedinstveni identifikator — 8 alfanumeričkih znakova | `TRNMOCK1` | | **PIB** | Poreski identifikacioni broj obveznika | `123456789` | | **Naziv firme** | Poslovno ime obveznika | `Dalibor DOO` | | **Poslovni prostor** | Adresa, grad, opština | `Beograd, Savski Venac` | | **Sertifikat** | Digitalni sertifikat (validan ~3 godine) | `2024-01-01 → 2027-01-01` | | **PIN** | Lični identifikacioni broj za pristup kartici | `1234` | | **Brojači** | Ukupan broj računa + po tipu transakcije | `total, pp, pr, ap, kp...` | | **Limit iščitavanja** | Maksimalni neisčitani iznos pre blokade | definiše PU | | **Trenutni neisčitani iznos** | Akumulirani iznos od poslednjeg iščitavanja | resetuje se dokazom | | **Ključevi** | Kriptografski ključevi za digitalno potpisivanje | par privatni/javni | ### 2.2 Životni ciklus kartice 1. Obveznik se uvodi u eFiskalizaciju preko portala ePorezi 2. Obveznik zahteva izdavanje BE preko ESF (Elektronski servisi za fiskalizaciju) 3. PU personalizuje karticu (upisuje sertifikat, PIB, JID, limit) 4. Kartica se fizički dostavlja obvezniku 5. Obveznik ubacuje karticu u L-PFR i unosi PIN 6. Kartica potpisuje račune dok ne dostigne limit → potrebno iščitavanje 7. Promena adrese poslovnog prostora → nova kartica 8. Odjava poslovnog prostora → opoziv svih kartica za taj prostor ### 2.3 Ograničenja kartice - Limit iščitavanja — kad neisčitani iznos dostigne limit, kartica **blokira** potpisivanje - Dokaz iščitavanja (Proof of Audit) sa SUF-a resetuje limit - Bez interneta: radi u oflajn režimu, akumulira neisčitane račune - Kartica NE može da se čita direktno — samo L-PFR komunicira sa njom ## 3. L-PFR — Lokalni procesor fiskalnih računa L-PFR je softver/hardver koji radi po principu "crne kutije". Komunicira samo sa: - ESIR-om (prima zahteve, vraća fiskalne račune) - BE karticom (potpisivanje, brojači) - SUF-om (iščitavanje — internet ili lokalno) ### 3.1 Odgovornosti L-PFR-a | Funkcija | Opis | |---|---| | Prijem zahteva | Prima podatke o transakciji od ESIR-a (JSON) | | PDV obračun | Računa poresku obavezu po stopama (Ж=20%, Ђ=10%, А=0%) | | Potpisivanje | Prosleđuje podatke kartici, dobija digitalni potpis | | Generisanje broja | Formira jedinstveni broj računa: `{JID_ESIR}-{JID_BE}-{brojač}` | | QR kod | Generiše verifikacioni URL i QR kod (40×40mm do 50×50mm) | | Čuvanje | Pamti sve račune u internoj memoriji (neizbrisivo) | | Iščitavanje | Šalje pakete za iščitavanje u SUF (internet ili USB/SD) | | Blokada | Kad BE dostigne limit, blokira izdavanje novih računa | ### 3.2 Vrste L-PFR-a | Vrsta | Opis | |---|---| | **Hardverski L-PFR** | Fizički uređaj sa čitačem kartica, ekranom, štampačem | | **Softverski L-PFR** | Softver instaliran na računaru obveznika | | **Razvojni L-PFR** | Softverska simulacija za development i testiranje ESIR-a | Naš Fisk mock je **Razvojni L-PFR**. ### 3.3 Tipovi računa i transakcija | Tip računa | Oznaka | Transakcija | Brojač | Fiskalni? | |---|---|---|---|---| | Normal | ПП | Sale (Prodaja) | pp | Da | | Normal | ПР | Refund (Refundacija) | pr | Da | | Advance | АП | Sale | ap | Da | | Advance | АР | Refund | ar | Da | | Copy | КП | Sale | kp | Ne | | Copy | КР | Refund | kr | Ne | | Training | ОП | Sale | op | Ne | | Training | ОР | Refund | or | Ne | ## 4. ESIR — Elektronski sistem za izdavanje računa (NTech) ESIR je ono što koristi kasir. Njegove odgovornosti: 1. Prikuplja podatke o transakciji (artikli, količine, cene, plaćanje) 2. Formira zahtev za fiskalizaciju i šalje ga L-PFR-u 3. Prima fiskalni račun od L-PFR-a 4. Prikazuje/štampa račun kupcu (sa QR kodom) 5. NE potpisuje račun — to radi kartica preko L-PFR-a ### 4.1 Šta ESIR šalje L-PFR-u (InvoiceRequest) ```json { "invoiceRequest": { "invoiceType": "Normal", "transactionType": "Sale", "cashier": "Dalibor", "buyerId": "10:123456789", "items": [ { "name": "RAM 16GB DDR4", "labels": ["Ж"], "totalAmount": 7200.00, "unitPrice": 7200.00, "quantity": 1.000 } ], "payment": [ { "amount": 7200.00, "paymentType": "Cash" } ] } } ``` **Napomena:** Podaci o firmi (PIB, naziv, adresa) su NA KARTICI (BE), ne u ESIR-u. L-PFR ih čita sa kartice i stavlja u fiskalni račun. ESIR ih NE šalje. ### 4.2 Šta L-PFR vraća ESIR-u (InvoiceResponse) ```json { "requestedBy": "NTECH001", "signedBy": "TRNMOCK1", "sdcDateTime": "2026-06-26T13:00:00.000+02:00", "invoiceNumber": "NTECH001-TRNMOCK1-42", "invoiceCounter": "3/42ПП", "invoiceCounterExtension": "ПП", "verificationUrl": "https://sandbox.suf.purs.gov.rs/v/?vl=NTECH001-TRNMOCK1-42", "verificationQRCode": "iVBORw0KG...", "totalAmount": 7200.00, "totalTax": 1200.00, "taxItems": [ {"label": "Ж", "categoryName": "PDV", "rate": 20.0, "amount": 1200.00} ], "messages": "Success", "journal": "========= FISKALNI RAČUN =========\n..." } ``` ## 5. Tok fiskalizacije — korak po korak ``` ESIR (NTech) L-PFR (mock) Kartica (BE) │ │ │ │ 1. POST /api/invoices │ │ │ {invoiceRequest: {...}} │ │ │────────────────────────────────→│ │ │ │ │ │ │ 2. Čita firmu sa kartice │ │ │ (PIB, naziv, adresa) │ │ │─────────────────────────────→│ │ │ │ │ │ 3. Provera PIN-a (ako treba)│ │ │─────────────────────────────→│ │ │←─────────────────────────────│ │ │ │ │ │ 4. Potpisivanje podataka │ │ │ (APDU komanda) │ │ │─────────────────────────────→│ │ │←── potpis + novi brojač ─────│ │ │ │ │ │ 5. L-PFR računa PDV, │ │ │ generiše QR, broj računa │ │ │ │ │ 6. Fiskalni račun + QR │ │ │←────────────────────────────────│ │ │ │ │ │ 7. NTech čuva fiskalni račun │ 8. Paket za iščitavanje │ │ prikazuje/štampa kupcu │ čeka slanje u SUF │ │ │ │ ``` ## 6. Komunikacija L-PFR ↔ kartica (BE) ### Kako to radi u stvarnosti U pravom sistemu L-PFR ima **fizički čitač kartica**. Kartica je **pasivna** — ne inicira ništa, ne "zove" nikoga. L-PFR je gospodar: šalje APDU komande (ISO 7816) kartici kroz čitač, kartica odgovara. ``` Pravi sistem: L-PFR ──APDU komanda──→ čitač kartica → kartica (BE) L-PFR ←────────── odgovor (bajtovi) ────── kartica (BE) ``` Kartica je u suštini **server koji čeka komande** — samo ne sluša na mreži nego na fizičkom interfejsu čitača. ### Naš mock preslikava isti smer Kartica emulator **sluša** na TCP portu. Fisk (L-PFR mock) se **spaja** na nju i šalje JSON komande — isti smer kao u stvarnosti, samo TCP+JSON umesto fizički+APDU. Kartica emulator je implementirana unutar NTech Go binarnog fajla (goroutine) jer tako ima direktan pristup bazi podataka za podatke o firmi, bez deljenja fajlova između kontejnera. ``` ┌──────────────────────────┐ HTTP :3000 ┌──────────────┐ │ NTech (Go) │←─────────────→│ Fisk │ │ ├── ESIR logika │ │ (L-PFR mock)│ │ └── kartica emulator │←──────────────│ :4566 │ │ sluša na :4567 │ TCP :4567 │ │ └──────────────────────────┘ JSON komande └──────────────┘ ``` Fisk se spaja na `ntech:4567`, šalje komandu, čeka odgovor, zatvara konekciju. NTech kartica emulator nikad ne inicira — samo odgovara. ### 6.1 API kartice — 4 komande Sva komunikacija: JSON linija (`\n` terminated), request-response. #### `status` — pročitaj brojače, limit, status ``` → {"command":"status"} ← { "status":"ok", "total_counter": 42, "counters": {"pp":3,"pr":1,"ap":0,"ar":0,"kp":0,"kr":0,"op":0,"or":0}, "limit": 500000, "unread_amount": 150000 } ``` #### `certificate` — identitet kartice (firma, PIB, JID) ``` → {"command":"certificate"} ← { "status":"ok", "jid": "TRNMOCK1", "tin": "RS123456789", "tin_plain": "123456789", "name": "Dalibor DOO", "address": "Test Adresa 1", "city": "Beograd", "district": "Savski Venac", "business_unit_id": "BU-001", "location_name": "Dalibor DOO", "valid_from": "2024-01-01T00:00:00+01:00", "valid_to": "2027-01-01T00:00:00+01:00", "issuer": "Poreska uprava RS" } ``` #### `verify_pin` — provera PIN-a pre pristupa ``` → {"command":"verify_pin", "pin":"1234"} ← {"status":"ok"} // pogrešan PIN: ← {"status":"error", "code":"2100", "message":"Pogrešan PIN"} ``` #### `sign` — potpiši račun, inkrementiraj brojač ``` → { "command": "sign", "invoice_type": "Normal", "transaction_type": "Sale", "total_amount": 7200.00 } ← { "status": "ok", "counter": 42, "counter_extension": "ПП", "type_counter": 3, "signature": "base64...", "blocked": false } // ako je limit dostignut: ← {"status":"blocked", "message":"Limit iščitavanja dostignut"} ``` ### 6.2 Šta izlazi iz mocka u emulator kartice | Podatak | Gde je sad (mock) | Gde treba (kartica emulator) | |---|---|---| | `PIN_BE = "1234"` | server.py:40 | kartica emulator | | `BE_ID = "TRNMOCK1"` | server.py:27 | kartica emulator — `jid` | | `ucitaj_firmu()` (naziv, PIB, adresa...) | server.py:47-77 | kartica emulator — `certificate` | | `get_counter()` / `peek_counter()` | server.py:81-92 | kartica emulator — `sign` / `status` | | `counter_ext()` | server.py:94-107 | kartica emulator — `sign` | | `resp_certificate()` | server.py:221-230 | kartica emulator — `certificate` | | `resp_verify_pin()` | server.py:194-204 | kartica emulator — `verify_pin` | ### 6.3 Šta ostaje u mocku | Funkcija | Ostaje u mocku? | |---|---| | Prijem HTTP zahteva od ESIR-a (`/api/invoices`, ...) | ✅ da | | `TAX_RATES`, `izracunaj_pdv()` | ✅ da | | Generisanje QR koda | ✅ da | | Generisanje `invoiceNumber` (`{ESIR_ID}-{JID}-{counter}`) | ✅ da — JID dobija od kartice | | Snimanje računa (JSON, txt, html, QR PNG) | ✅ da | | `generate_receipt()`, `generate_receipt_html()` | ✅ da | | CORS, logging, rutiranje | ✅ da | | `ucitaj_firmu()` | ❌ zamenjuje se pozivom `certificate` ka kartici | | Brojači | ❌ idu na karticu | | PIN verifikacija | ❌ ide na karticu | ### 6.4 Kako mock poziva karticu (Python) ```python import socket, json SOCKET_PATH = "/tmp/ntech-be.sock" def be_command(cmd: dict) -> dict: s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(SOCKET_PATH) s.sendall((json.dumps(cmd) + "\n").encode()) resp = b"" while True: chunk = s.recv(4096) if not chunk: break resp += chunk if b"\n" in resp: break s.close() return json.loads(resp.decode()) ``` ### 6.5 Novi tok fiskalizacije sa emulatorom ``` ESIR (NTech) L-PFR (mock) Kartica (Go) │ │ │ │ POST /api/invoices │ │ │────────────────────→│ │ │ │ │ │ │ 1. be_command({ │ │ │ "command":"certificate"│ │ │ }) │ │ │─────────────────────────→│ │ │←── firma, PIB, JID ──────│ │ │ │ │ │ 2. be_command({ │ │ │ "command":"sign", │ │ │ "invoice_type":"Normal│ │ │ "transaction_type":...│ │ │ "total_amount":7200 │ │ │ }) │ │ │─────────────────────────→│ │ │←── counter, ext, potpis ─│ │ │ │ │ │ 3. računa PDV, generiše │ │ │ QR, broj računa │ │ │ │ │←── fiskalni račun ──│ │ │ │ │ ``` **Napomena — `verify_pin`:** PIN se proverava **jednom pri pokretanju** kartice (ili kad `isPinRequired=True` u `/api/status`), ne per-invoice. Komanda `verify_pin` postoji u API-ju kartice radi inicijalizacije i ponovnog otključavanja, ali je ne pozivamo u toku normalnog izdavanja računa. ### 6.6 Šta radimo u NTech-u (Go) — emulator kartice Novi paket: `internal/be/` ili `cmd/karticab`: ``` internal/be/ ├── server.go # Unix socket listener, JSON parser ├── kartica.go # stanje kartice: brojači, limit, PIN, firma └── config.go # env varijable ``` Tipovi podataka za karticu (JSON wire format): ```go type StatusResponse struct { Status string `json:"status"` TotalCounter int `json:"total_counter"` Counters map[string]int `json:"counters"` Limit float64 `json:"limit"` UnreadAmount float64 `json:"unread_amount"` } type CertificateResponse struct { Status string `json:"status"` JID string `json:"jid"` TIN string `json:"tin"` TINPlain string `json:"tin_plain"` Name string `json:"name"` Address string `json:"address"` City string `json:"city"` District string `json:"district"` BusinessUnitID string `json:"business_unit_id"` LocationName string `json:"location_name"` ValidFrom string `json:"valid_from"` ValidTo string `json:"valid_to"` Issuer string `json:"issuer"` } type SignResponse struct { Status string `json:"status"` Counter int `json:"counter"` CounterExtension string `json:"counter_extension"` TypeCounter int `json:"type_counter"` Signature string `json:"signature"` Blocked bool `json:"blocked"` } ``` Pokretanje kartice (zaseban proces): ```bash NTECH_BE_SOCKET=/tmp/ntech-be.sock \ NTECH_BE_PIN=1234 \ NTECH_BE_JID=TRNMOCK1 \ NTECH_BE_FIRMA_NAZIV="Dalibor DOO" \ NTECH_BE_PIB=123456789 \ NTECH_BE_ADRESA="Test Adresa 1" \ NTECH_BE_GRAD=Beograd \ go run ./cmd/kartica/ ``` ### 6.7 Docker Kartica emulator je goroutine unutar NTech-a — nema zasebnog kontejnera. NTech expose-uje dva porta: `3000` za ESIR HTTP i `4567` za karticu. ```yaml services: ntech: build: . ports: - "3000:3000" - "4567:4567" # kartica emulator (goroutine unutar NTech-a) environment: - BE_PORT=4567 - BE_PIN=1234 - BE_JID=TRNMOCK1 fisk: build: ./Fisk ports: - "4566:4566" environment: - BE_HOST=ntech # hostname NTech kontejnera u Docker mreži - BE_PORT=4567 - ESIR_ID=NTECH001 ``` ## 7. Poznati bugovi u trenutnom mocku (pre refaktora) ### 7.1 QR kod u HTML računu je prazan — `receipt.py:370` ```python # Trenutno (pogrešno): qr_src = f"data:image/png;base64,{inv.get('qrCode', '')}" # Ispravno — polje se zove verificationQRCode: qr_src = f"data:image/png;base64,{inv.get('verificationQRCode', '')}" ``` ### 7.2 `invoiceCounter` prikazuje `invoiceNumber` — `receipt.py:252-253` ```python # Trenutno (oba reda prikazuju invoiceNumber): lines.append(layout(..., str(inv.get("invoiceNumber", "")), W)) lines.append(layout(..., str(inv.get("invoiceNumber", "")), W)) # Ispravno — drugi red: lines.append(layout(m.get("sdc-invoice-counter", "Brojač računa"), str(inv.get("invoiceCounter", "")), W)) ``` ## 8. Napomena — podaci firme Kartica emulator je goroutine unutar NTech procesa, pa ima direktan pristup NTech bazi podataka. Pri pokretanju čita podatke firme iz `podesavanja` tabele (naziv, PIB, adresa...) — isto što `ucitaj_firmu()` radi u Python mocku danas, samo direktno bez deljenja fajlova između kontejnera. Ovo je ispravno modelovanje: prava kartica ima zamrznute podatke upisane pri personalizaciji. Ako se adresa ili naziv firme promeni u NTech podešavanjima, treba restartovati NTech (i kartica emulator se pokreće ponovo sa novim podacima). --- ## 9. Funkcije u Fisk/ Python mocku ### 9.1 server.py — L-PFR server | Funkcija | Linija | Opis | |---|---|---| | `ucitaj_firmu()` | ~47 | Čita naziv, PIB, adresu iz NTech SQLite-a (env `NTECH_SQLITE`); fallback na test vrednosti | | `get_counter(tip)` | ~81 | Inkrementira i vraća brojač iz fajla u `COUNTER_DIR`; tip = "pp", "pr", "ap"... | | `peek_counter(tip)` | ~87 | Čita brojač bez inkrementiranja | | `counter_ext(invoice_type, transaction_type)` | ~94 | Vraća ćirilični sufiks (ПП, ПР, АП...) i ključ za brojač | | `izracunaj_pdv(items)` | ~110 | Grupiše stavke po poreskoj oznaci; formula: `pdv = bruto * stopa / (100 + stopa)` | | `_build_invoice_response(req, request_id)` | ~130 | **Glavni builder** — računa PDV, poziva karticu, generiše invoiceNumber, QR, snima JSON/txt/html/PNG | | `resp_certificate()` | ~221 | Vraća identitet (firma, PIB, JID) — u budućnosti delegira kartici | | `resp_verify_pin(body)` | ~194 | Proverava PIN — u budućnosti delegira kartici | | `resp_status()` | ~170 | Vraća L-PFR status: `isPinRequired`, `lastInvoiceNumber`, TIN | | `resp_attention()` | ~165 | Health check — vraća `"status":"ok"` | | `resp_settings_get/post()` | ~240 | Teron podešavanja (poreske stope, locale) | | `resp_invoice_final()` | ~260 | Konačni račun koji zatvara avansne transakcije | | `resp_invoice_last/by_request/by_number/search()` | ~280+ | Pregled ranije izdatih računa | **Konstante:** - `TAX_RATES`: `{"Ж":20, "Ђ":10, "Е":10, "А":0, "Г":0, "З":0}` + generičke oznake - `BE_ID = "TRNMOCK1"` — JID bezb. elementa - `ESIR_ID = "NTECH001"` — ESIR identifikator - `PIN_BE = "1234"` — PIN kartice - `ROUTES` — 12 HTTP ruta, Teron API kompatibilne ### 9.2 receipt.py — generator fiskalnih računa | Funkcija | Opis | |---|---| | `generate_receipt(invoice_data, lang)` | Tekstualni račun širine 48 znakova, prati `agent-invoice.vm` template | | `generate_receipt_html(invoice_data, lang)` | HTML za A4 štampu, monospace font, `@page size A4` | | `generate_report(report_data, lang)` | Dnevni/periodični izveštaj (standard-report.vm) | | `load_locale(lang)` | Učitava `locale_latin.properties` ili `locale_cyrillic.properties` iz `data/` | | `price()`, `qty()`, `amount()` | Formatiranje brojeva (2 decimale, hiljade sa tačkom) | | `center(text, width)` | Centriranje teksta na širini | | `layout(levo, desno, width)` | Red sa levim i desnim tekstom, popunjen razmacima | | `wrap(text, width)` | Prelom dugog teksta u više redova | | `separator(width)` | Crtana linija razdvajača | | `title(text, width)` | Naslov uokviren u `== tekst ==` | **Mapiranje tipova:** - `TRANSACTION_TYPES_CYR` i `TRANSACTION_TYPES_LAT` — prevod `"Normal/Sale"` → `"ПРОМЕТ ПРОДАЈЕ"` / `"PROMET PRODAJE"` --- ## 10. Detaljan tok dobijanja fiskalnog računa ### 10.1 Normalni fiskalni račun (Promet Prodaja — ПП) **Preduslov:** Kartica ubačena, PIN prihvaćen (jednom pri pokretanju). ``` ESIR (NTech) L-PFR (Fisk) BE (Kartica) SUF │ │ │ │ │ 1. Kasir završava │ │ │ │ transakciju u NTech │ │ │ │ │ │ │ │ 2. POST /api/invoices │ │ │ │ {invoiceRequest} │ │ │ │─────────────────────────→│ │ │ │ │ │ │ │ │ 3. certificate │ │ │ │─────────────────────────→│ │ │ │←── PIB, naziv, adresa ───│ │ │ │ │ │ │ │ 4. sign {type, amount} │ │ │ │─────────────────────────→│ │ │ │ BE: proverava limit │ │ │ │ BE: ažurira brojač │ │ │ │ BE: generiše potpis │ │ │ │←── {counter, ext, sig} ──│ │ │ │ │ │ │ │ 5. računa PDV po stopama │ │ │ │ generiše invoiceNumber│ │ │ │ generiše QR kod │ │ │ │ snima račun interno │ │ │ │ │ │ │ 6. {invoiceResponse} │ │ │ │←─────────────────────────│ │ │ │ │ │ │ │ 7. prikazuje/štampa │ │ │ │ kupcu (tekst + QR) │ │ │ │ │ │ │ │ │ 8. asinhro — kad ima net:│ │ │ │ šalje paket ──────────┼────────────────→│ │ │←──────────────────────── Proof of Audit ───│ ``` ### 10.2 Format ključnih polja u odgovoru | Polje | Format | Primer | Ko generiše | |---|---|---|---| | `invoiceNumber` | `{ESIR_JID}-{BE_JID}-{ukupan_br}` | `NTECH001-TRNMOCK1-42` | L-PFR | | `invoiceCounter` | `{br_ove_vrste}/{ukupan_br}{ext}` | `3/42ПП` | L-PFR + BE | | `sdcDateTime` | ISO 8601 sa zonom | `2026-06-26T13:00:00.000+02:00` | L-PFR | | `verificationUrl` | URL ka SUF portalu | `https://suf.purs.gov.rs/v/?vl=...` | L-PFR | | `verificationQRCode` | base64 PNG | `iVBORw0KG...` | L-PFR (qrcode lib) | | `totalTax` | float, zaokružen | `1200.00` | L-PFR | **ПФР broj (invoiceNumber) za L-PFR:** oba JID su isti (ESIR = BE su isti uređaj). Za V-PFR su različiti jer V-PFR ima sopstveni BE u cloudu. ### 10.3 Avansni tok (AP → PP) ``` 1. AP (Avanс Prodaja) — plaćen avanс 2. AP može se ponavljati (više avansnih uplata) — svaki referencira prethodni AP 3. PP (Promet Prodaja) — zatvaranje transakcije, referencira poslednji AP - PP MORA sadržati referentni broj poslednjeg AP - PP iznos = ukupno - plaćeni avans ``` **Lanac referenci:** AP1 ← AP2 ← PP (ili AP1 ← AR1 ← AP2 ← PP itd.) ### 10.4 Refundacija (PR) ``` PR (Promet Refundacija) — MORA imati: - referenceDocumentNumber: PFR broj originalnog PP računa - buyerId: OBAVEZNO (identifikacija kupca za refundaciju) ``` --- ## 11. Na šta treba voditi računa ### 11.1 Podaci firme dolaze sa kartice, NE iz ESIR-a ``` ❌ Pogrešno: ESIR šalje naziv/PIB/adresu u invoiceRequest ✅ Ispravno: L-PFR čita sa kartice (certificate komanda), ESIR to ne zna ``` U mocku: `ucitaj_firmu()` čita iz SQLite. U emulatoru: Go goroutine čita iz iste baze. ### 11.2 PDV se računa iz bruto iznosa (ne iz neto) ``` pdv = bruto * stopa / (100 + stopa) # za Ж (20%): pdv = 1000 * 20 / 120 = 166.67 (ne 200!) ``` Mock formula je ispravna. Greška nastaje ako se uzme `bruto * stopa / 100`. ### 11.3 PIN se proverava jednom — ne per-invoice - Pin se unosi pri **ubacivanju kartice** (pokretanje L-PFR-a) - Ako je `isPinRequired=True` u `/api/status` — znači kartica je izvučena i ponovo ubačena - `verify_pin` komanda postoji ali se NE poziva za svaki račun ### 11.4 Kartica se blokira kad dostigne limit iščitavanja - Limit je iznos (ne broj računa) - Kad se dostigne: `sign` vraća `{"status":"blocked"}` - Rešenje: iščitavanje (internet → SUF šalje Proof of Audit → resetuje brojač) - Bez interneta: lokalno iščitavanje (USB/SD → portal PU) - Za vreme iščitavanja: L-PFR može i dalje da izdaje račune (iščitavanje nije bloker) ### 11.5 QR kod NE SME sadržati logo ni sliku Standard propisuje: QR kod za verifikaciju ne sme biti ispisан na slici ili logu, niti sadržati sliku ili logo unutar sebe. Minimalna veličina: 40×40mm, maksimalna: 50×50mm. ### 11.6 Fiskalni dokumenti koji NISU fiskalni računi Sledeći tipovi NE registruju promet i moraju nositi napomenu **"OVO NIJE FISKALNI RAČUN"** (napisano duplo većim fontom): | Tip | Šta je | |---|---| | **Kopija (КП/КР)** | Kopija već izdatog računa | | **Obuka (ОП/ОР)** | Za trening kasira, testiranje ESIR-a | | **Predračun (РП/РР)** | Obaveštavanje kupca o budućem prometu | ### 11.7 Referentni broj — kada je obavezan | Situacija | Ref. broj obavezan? | |---|---| | Promet Refundacija (PR) | **DA** — PFR broj originalnog PP | | Kopija | **DA** — PFR broj originalnog računa | | Promet Prodaja zatvaranje avansa | **DA** — PFR broj poslednjeg AP | | Avanс Prodaja (2. i naredni AP) | **DA** — PFR broj prethodnog AP | | Obični Promet Prodaja | NE | | Predračun | NE | ### 11.8 Ime kasira dolazi iz ESIR-a, ne sa kartice ```json "invoiceRequest": { "cashier": "Dalibor" // NTech šalje ovo } ``` Kartica ne zna ko je kasir — to je podatak transakcije. L-PFR kopira polje `cashier` iz zahteva direktno u odgovor/račun. Nije obavezno po standardu, ali korisno za evidenciju. ### 11.9 ESIR broj vs PFR broj - **ESIR broj** (`buyerCostCenterId`, `referenceNumber`) — opcioni interni broj iz ESIR-a, npr. broj porudžbine - **PFR broj** (`invoiceNumber`) — jedinstven, nepromenljiv, kreira ga L-PFR; format: `{JID_ESIR}-{JID_BE}-{sekvenca}` - Referentni broj pri refundaciji = **PFR broj** originalnog, ne ESIR broj ### 11.10 Bugovi u receipt.py — ✅ ispravljeni ```python # BUG 1 — receipt.py:370 — QR kod je prazan u HTML prikazu — ISPRAVLJENO # Polje se zove verificationQRCode, ne qrCode qr_src = f"data:image/png;base64,{inv.get('verificationQRCode', '')}" # BUG 2 — receipt.py:252 — oba reda prikazuju invoiceNumber — ISPRAVLJENO str(inv.get("invoiceCounter", "")) # treći red sada koristi invoiceCounter ``` ### 11.11 Docker — cross-container komunikacija Unix socket NE radi između Docker kontejnera bez deljenog volumena. Rešenje: kartica emulator sluša na TCP portu `4567` unutar NTech kontejnera. ``` ✅ Fisk env: BE_HOST=ntech BE_PORT=4567 ✅ NTech expose-uje: "4567:4567" ❌ Nemoj koristiti /tmp socket između kontejnera ``` --- ## 12. Izgled fiskalnog računa — razlike i ispravke > **Status pregleda:** Sve GR greške su ispravljene. Videti §12.2 za detalje. > Dodatne ispravke (kasir, servis modal, Čeka delove) u §12.5. ### 12.1 Referentni račun (OMV) Pravi fiskalni račun koji generiše Teron L-PFR (`Dokumenta/Račun.png`): ``` ============ ФИСКАЛНИ РАЧУН ============ 101987198 OMV SRBIJA DOO BEOGRAD 1083363-Огранак БС Краљево ДУШАНА ПОПОВИЋА Краљево Касир: Radojka Vuković ЕСИР број: 700/11.71.0.0 -----------ПРОМЕТ ПРОДАЈА----------- Артикли ======================================== Назив Цена Кол. Укупно OMV EP BMB 95 / l (Ђ) 179,07 16,570 2.967,17 ---------------------------------------- Укупан износ: 2.967,17 Платна картица: 2.967,17 ======================================== Ознака Име Стопа Порез Ђ О-ПДВ 20,00% 494,53 ---------------------------------------- Укупан износос пореза: 494,53 ======================================== ПФР време: 18.12.2025. 11:28:25 ПФР број рачуна: U92D83M9-U92D83M9-195100 Бројач рачуна: 192838/195100ПП ======================================== [QR KOD — centriran, ~60mm x 60mm] ======== КРАЈ ФИСKАЛНОГ РАЧУНА ======== ``` ### 12.2 Grešake i ispravke #### GR-1: Formatiranje brojeva — ✅ ISPRAVLJENO **Problem:** Python `f"{n:,.2f}"` daje američki format: `2,967.17` Srpski/evropski standard: `2.967,17` (tačka za hiljade, zarez za decimale) **Gde:** `receipt.py` — funkcije `price()`, `qty()`, `amount()`, `number()` **Ispravka:** ```python def _sr(n, decimals=2): s = f"{n:,.{decimals}f}" return s.replace(",", "\x00").replace(".", ",").replace("\x00", ".") ``` Sve funkcije `price()`, `qty()`, `amount()`, `number()` sada koriste `_sr()`. --- #### GR-2: Pogrešno polje za Brojač računa — ✅ ISPRAVLJENO **Problem:** `receipt.py` linija 252 koristila `inv.get("invoiceNumber")` umesto `inv.get("invoiceCounter")`. Oba reda (PFR broj + Brojač) prikazivala isti `invoiceNumber`. **Ispravka:** Treći red sada koristi `inv.get("invoiceCounter", "")`. --- #### GR-3: Labele ne odgovaraju stvarnom računu — ✅ ISPRAVLJENO **Problem:** Locale fajlovi su imali pogrešne ili engleske labele. | Šta se prikazuje | Teron (stvarno) | Pre ispravke | Posle ispravke | |---|---|---|---| | Ukupan iznos | `Ukupan iznos` | `Za uplatu` | ✅ `Ukupan iznos` | | Platna kartica | `Platna kartica` | `Card` | ✅ `Platna kartica` | | Gotovina | `Gotovina` | `Cash` | ✅ `Gotovina` | | Čekovi | `Čekovi` | `Check` | ✅ `Čekovi` | | Vaučer | `Vaučer` | `Voucher` | ✅ `Vaučer` | | Instant plaćanje | `Instant plaćanje` | `MobileMoney` | ✅ `Instant plaćanje` | | Prenos na račun | `Prenos na račun` | `WireTransfer` | ✅ `Prenos na račun` | | Ostalo | `Ostalo` | `Drugo` | ✅ `Ostalo` | | PFR vreme | `PFR vreme` | `Vreme` | ✅ `PFR vreme` | | PFR broj računa | `PFR broj računa` | `Broj računa` | ✅ `PFR broj računa` | | Brojač računa | `Brojač računa` | `Brojač` | ✅ `Brojač računa` | **Ispravka:** - `Fisk/data/locale_latin.properties` i `locale_cyrillic.properties` — dodati `Cash=Gotovina`, `Card=Platna kartica` itd., ispraviti PFR labele - `receipt.py` — lookup za payment type: `p.get("paymentType", p.get("type", ""))` pa `m.get(pt, pt or "Ostalo")` (bez "Drugo" fallback-a) --- #### GR-4: QR kod nije centriran na računu — ✅ ISPRAVLJENO **Problem:** `StampaFiskalnog` renderovao ceo journal kao jedan `white-space:pre` blok. `` ugrađen unutar `
` ne može se centrirati CSS-om jer `white-space:pre` tretira razmake doslovno.

**Gde:** `internal/handler/servis.go` — `StampaFiskalnog`

**Ispravka:** Koristi se `strings.Cut(journal, "{{{{QR-KOD}}}}")` koji deli journal na deo pre i posle QR-a. Renderuje se kao:
```html
…tekst pre QR…
…tekst posle QR…
``` `body` ima `max-width:max-content;margin:0 auto` da se tekst i QR centriraju kao celina. --- #### GR-5: QR veličina u HTML verziji receipt.py — ✅ ISPRAVLJENO **Problem:** `.qr img { width:25mm; height:25mm; }` — premalo za skeniranje. **Ispravka:** Promenjena veličina na `width:60mm; height:60mm;` u `generate_receipt_html`. --- #### GR-6: Jezik hardkodovan na latinicu — ✅ ISPRAVLJENO **Problem:** `server.py` uvek pozivao `generate_receipt(full_data, "latin")`. **Ispravka:** - Fisk čita `fiskalni_pismo` iz env var `FISKALNI_PISMO` (prioritet) ili iz NTech SQLite (`podesavanja` tabela, ključ `fiskalni_pismo`) - Dodana funkcija `_ucitaj_fiskalni_pismo()` pri pokretanju servera - Oba poziva `generate_receipt(full_data, FISKALNI_PISMO)` i `generate_receipt_html(full_data, FISKALNI_PISMO)` - **Restart Fisk servera obavezan** posle promene podešavanja --- #### GR-7: UI za izbor pisma nedostajao — ✅ ISPRAVLJENO **Ispravka:** - `web/templates/stranice/podesavanja_fiskalizacija.html` — dodat `