'
if not covered:
for p in inv.get("payments", []):
- ptype = m.get(p.get("type", ""), p.get("type", "Drugo"))
+ pt = p.get("paymentType", p.get("type", ""))
+ ptype = m.get(pt, pt or "Drugo")
payments_rows += f'
diff --git a/Fisk/server.py b/Fisk/server.py
index 7af6b40..2f4f41e 100755
--- a/Fisk/server.py
+++ b/Fisk/server.py
@@ -17,6 +17,9 @@ import time
from datetime import datetime, timezone, timedelta
from pathlib import Path
+import socket
+import urllib.parse
+
import qrcode
from receipt import generate_receipt, generate_receipt_html, generate_report, load_locale
@@ -24,7 +27,10 @@ from receipt import generate_receipt, generate_receipt_html, generate_report, lo
PORT = 4566 # Teron standard port
HOST = "0.0.0.0"
ESIR_ID = "NTECH001" # naš 8-char ESIR identifikator
-BE_ID = "TRNMOCK1" # simulirani BE/LPFR identifikator
+
+# Kartica emulator (NTech goroutine)
+BE_HOST = os.environ.get("BE_HOST", "127.0.0.1")
+BE_PORT = int(os.environ.get("BE_PORT", "4567"))
DATA_DIR = Path(__file__).parent / "data"
INVOICES_DIR = DATA_DIR / "invoices"
@@ -36,12 +42,94 @@ COUNTER_DIR = DATA_DIR / "counters"
for d in [DATA_DIR, INVOICES_DIR, QR_DIR, RECEIPTS_DIR, COUNTER_DIR]:
d.mkdir(parents=True, exist_ok=True)
-# Test PIN (pravi Teron traži PIN za BE karticu)
-PIN_BE = "1234"
-
-# NTech SQLite baza (read-only) — čita podatke o firmi
+# NTech SQLite baza (read-only) — fallback kad kartica emulator nije dostupan
NTECH_DB = os.environ.get("NTECH_SQLITE") or str(Path(__file__).parent.parent / "ntech.db")
+def _ucitaj_verify_host():
+ """Čita verify_host iz env, pa iz NTech SQLite baze."""
+ if v := os.environ.get("VERIFY_HOST", ""):
+ return v
+ try:
+ con = sqlite3.connect(f"file:{NTECH_DB}?mode=ro", uri=True)
+ try:
+ cur = con.execute("SELECT vrednost FROM podesavanja WHERE kljuc='verify_host'")
+ row = cur.fetchone()
+ return row[0] if row and row[0] else ""
+ finally:
+ con.close()
+ except Exception:
+ return ""
+
+# Host za verifikacioni link na QR kodu (npr. "ntech.moja-firma.rs:3000").
+# Ako je prazno, koristi se sandbox.suf.purs.gov.rs.
+VERIFY_HOST = _ucitaj_verify_host()
+
+def _ucitaj_fiskalni_pismo():
+ """Čita fiskalni_pismo iz env var FISKALNI_PISMO ili iz NTech SQLite baze.
+ Vrednosti: 'latin' (podrazumevano) ili 'cyrillic'."""
+ if v := os.environ.get("FISKALNI_PISMO", ""):
+ return v if v in ("latin", "cyrillic") else "latin"
+ try:
+ con = sqlite3.connect(f"file:{NTECH_DB}?mode=ro", uri=True)
+ try:
+ cur = con.execute("SELECT vrednost FROM podesavanja WHERE kljuc='fiskalni_pismo'")
+ row = cur.fetchone()
+ v = row[0] if row and row[0] else "latin"
+ return v if v in ("latin", "cyrillic") else "latin"
+ finally:
+ con.close()
+ except Exception:
+ return "latin"
+
+# Pismo fiskalnog računa: 'latin' ili 'cyrillic'
+FISKALNI_PISMO = _ucitaj_fiskalni_pismo()
+
+
+def be_command(cmd: dict) -> dict:
+ """Šalje JSON komandu kartica emulatoru (NTech TCP :4567) i vraća odgovor."""
+ try:
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.settimeout(3)
+ s.connect((BE_HOST, BE_PORT))
+ s.sendall((json.dumps(cmd) + "\n").encode("utf-8"))
+ buf = b""
+ while True:
+ chunk = s.recv(4096)
+ if not chunk:
+ break
+ buf += chunk
+ if b"\n" in buf:
+ break
+ return json.loads(buf.decode("utf-8").strip())
+ except Exception as e:
+ log(f" ⚠️ be_command({cmd.get('command')}) greška: {e}")
+ return {"status": "error", "message": str(e)}
+
+
+def build_vl(full_data):
+ """Gradi base64-kodirani payload za vl parametar verifikacionog URL-a."""
+ payload = {
+ "n": full_data.get("invoiceNumber", ""),
+ "ic": full_data.get("invoiceCounter", ""),
+ "t": full_data.get("sdcDateTime", ""),
+ "a": full_data.get("totalAmount", 0),
+ "c": full_data.get("tin", ""),
+ "co": full_data.get("company", ""),
+ "lo": full_data.get("store", ""),
+ "ad": full_data.get("address", ""),
+ "g": full_data.get("city", ""),
+ "di": full_data.get("district", ""),
+ "it": full_data.get("invoiceType", "Normal"),
+ "tr": full_data.get("transactionType", "Sale"),
+ "tx": full_data.get("taxItems", []),
+ "pm": full_data.get("payments", []),
+ "ca": full_data.get("cashier", ""),
+ "bi": full_data.get("buyerId", ""),
+ "items": full_data.get("items", []),
+ }
+ j = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
+ return base64.b64encode(j.encode("utf-8")).decode("ascii")
+
# ── Firma ───────────────────────────────────────────────────
def ucitaj_firmu():
@@ -178,17 +266,20 @@ def resp_attention():
return {"sdcDateTime": sada(), "status": "OK"}
def resp_status():
- f = ucitaj_firmu()
- last = peek_counter("total")
- last_num = f"{ESIR_ID}-{BE_ID}-{last}" if last >= 1 else ""
+ st = be_command({"command": "status"})
+ cert = be_command({"command": "certificate"})
+ total = st.get("total_counter", 0)
+ jid = cert.get("jid", "UNKNOWN")
+ tin = cert.get("tin", "RS000000000")
+ last_num = f"{ESIR_ID}-{jid}-{total}" if total >= 1 else ""
return {
- "isPinRequired": False,
+ "isPinRequired": st.get("pin_required", False),
"auditRequired": False,
"sdcDateTime": sada(),
"lastInvoiceNumber": last_num,
"protocolVersion": "1.0.0",
"serialNumber": ESIR_ID,
- "tin": f["tin"],
+ "tin": tin,
}
def resp_verify_pin(request_body=None):
@@ -197,11 +288,12 @@ def resp_verify_pin(request_body=None):
uneti = str(request_body.get("pin", "")).strip()
elif isinstance(request_body, str):
uneti = request_body.strip().strip('"')
- if uneti == PIN_BE:
+ resp = be_command({"command": "verify_pin", "pin": uneti})
+ if resp.get("status") == "ok":
log(" 🔓 PIN ispravan")
return {"status": "OK", "message": "PIN verifikovan"}
log(" 🔒 Pogrešan PIN")
- return {"status": "ERROR", "code": "2100", "message": "Pogrešan PIN"}
+ return {"status": "ERROR", "code": resp.get("code", "2100"), "message": resp.get("message", "Pogrešan PIN")}
def resp_settings_get():
return {
@@ -219,14 +311,14 @@ def resp_settings_post():
return {"status": "OK", "message": "Podešavanja sačuvana"}
def resp_certificate():
- f = ucitaj_firmu()
+ c = be_command({"command": "certificate"})
return {
- "serialNumber": BE_ID,
- "tin": f["tin"],
- "name": f["name"],
- "validFrom": "2024-01-01T00:00:00+01:00",
- "validTo": "2027-01-01T00:00:00+01:00",
- "issuer": "Poreska uprava RS",
+ "serialNumber": c.get("jid", ESIR_ID),
+ "tin": c.get("tin", "RS000000000"),
+ "name": c.get("name", ""),
+ "validFrom": c.get("valid_from", "2024-01-01T00:00:00+01:00"),
+ "validTo": c.get("valid_to", "2027-01-01T00:00:00+01:00"),
+ "issuer": c.get("issuer", "Poreska uprava RS"),
}
def _build_invoice_response(req, request_id):
@@ -238,27 +330,77 @@ def _build_invoice_response(req, request_id):
transaction_type = inv_req.get("transactionType", "Sale")
items = inv_req.get("items", [])
- # Brojači
- total_cnt = get_counter("total")
- ext, tip_key = counter_ext(invoice_type, transaction_type)
- type_cnt = get_counter(tip_key)
-
- invoice_number = f"{ESIR_ID}-{BE_ID}-{total_cnt}"
- invoice_counter = f"{type_cnt}/{total_cnt}{ext}"
- verification_url = f"https://sandbox.suf.purs.gov.rs/v/?vl={invoice_number}"
- qr_b64 = generate_qr(verification_url)
-
# PDV i ukupan iznos
tax_items = izracunaj_pdv(items)
total_amount = round(sum(float(i.get("totalAmount", 0)) for i in items), 2)
total_tax = round(sum(t["amount"] for t in tax_items), 2)
- firma = ucitaj_firmu()
+ # Kartica emulator: podatke firme i potpis/brojače
+ cert = be_command({"command": "certificate"})
+ sign = be_command({
+ "command": "sign",
+ "invoice_type": invoice_type,
+ "transaction_type": transaction_type,
+ "total_amount": total_amount,
+ })
+
+ if sign.get("status") == "blocked":
+ raise RuntimeError(f"Kartica blokirana: {sign.get('message')}")
+
+ jid = cert.get("jid", ESIR_ID)
+ total_cnt = sign.get("counter", 1)
+ type_cnt = sign.get("type_counter", 1)
+ ext = sign.get("counter_extension", "ПП")
+
+ invoice_number = f"{ESIR_ID}-{jid}-{total_cnt}"
+ invoice_counter = f"{type_cnt}/{total_cnt}{ext}"
+
+ # firma podaci sa kartice
+ firma = {
+ "tinPlain": cert.get("tin_plain", "000000000"),
+ "tin": cert.get("tin", "RS000000000"),
+ "name": cert.get("name", "Test Company DOO"),
+ "locationName": cert.get("location_name", cert.get("name", "Test Company DOO")),
+ "address": cert.get("address", "Test Adresa 1"),
+ "city": cert.get("city", "Beograd"),
+ "district": cert.get("district", "Savski Venac"),
+ }
+
+ # Verifikacioni URL i QR kod
+ if VERIFY_HOST:
+ vl_payload = {
+ "n": invoice_number,
+ "ic": invoice_counter,
+ "t": sada(),
+ "a": total_amount,
+ "c": firma["tinPlain"],
+ "co": firma["name"],
+ "lo": firma["locationName"],
+ "ad": firma["address"],
+ "g": firma["city"],
+ "di": firma["district"],
+ "it": invoice_type,
+ "tr": transaction_type,
+ "tx": tax_items,
+ "pm": inv_req.get("payment", [{"type": "Cash", "amount": total_amount}]),
+ "ca": inv_req.get("cashier", "Kasir"),
+ "bi": inv_req.get("buyerId", ""),
+ "items": items,
+ }
+ vl = base64.b64encode(
+ json.dumps(vl_payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
+ ).decode("ascii")
+ scheme = "https" if VERIFY_HOST.startswith("https://") else "http"
+ host = VERIFY_HOST.removeprefix("https://").removeprefix("http://")
+ verification_url = f"{scheme}://{host}/v/?vl={urllib.parse.quote(vl, safe='')}"
+ else:
+ verification_url = f"https://sandbox.suf.purs.gov.rs/v/?vl={invoice_number}"
+ qr_b64 = generate_qr(verification_url)
# Odgovor koji ide ka NTech-u (ESIR-u)
odgovor = {
"requestedBy": ESIR_ID,
- "signedBy": BE_ID,
+ "signedBy": jid,
"sdcDateTime": sada(),
"invoiceCounter": invoice_counter,
"invoiceCounterExtension": ext,
@@ -304,12 +446,12 @@ def _build_invoice_response(req, request_id):
with open(qr_path, "wb") as fh:
fh.write(base64.b64decode(qr_b64))
- # Generiši tekst i HTML račun
- receipt_text = generate_receipt(full_data, "latin")
+ # Generiši tekst i HTML račun (pismo određuje podešavanje fiskalni_pismo)
+ receipt_text = generate_receipt(full_data, FISKALNI_PISMO)
txt_path = RECEIPTS_DIR / f"{total_cnt:06d}_{request_id}.txt"
txt_path.write_text(receipt_text, encoding="utf-8")
- html_txt = generate_receipt_html(full_data, "latin")
+ html_txt = generate_receipt_html(full_data, FISKALNI_PISMO)
html_path = RECEIPTS_DIR / f"{total_cnt:06d}_{request_id}.html"
html_path.write_text(html_txt, encoding="utf-8")
@@ -505,7 +647,10 @@ class FiscalHandler(http.server.BaseHTTPRequestHandler):
# ── Main ─────────────────────────────────────────────────────
def main():
- firma = ucitaj_firmu()
+ cert = be_command({"command": "certificate"})
+ jid = cert.get("jid", "?")
+ tin = cert.get("tin_plain", "?")
+ name = cert.get("name", "?")
log("╔══════════════════════════════════════════════════╗")
log("║ 🧾 Teron L-PFR Mock Server ║")
log(f"║ http://{HOST}:{PORT}/ ║")
@@ -514,9 +659,8 @@ def main():
log(f" 🧾 Računi: {INVOICES_DIR}")
log(f" 📱 QR PNG: {QR_DIR}")
log(f" 📝 Log: {LOG_FILE}")
- log(f" 🏢 Firma: {firma['name']} | PIB: {firma['tinPlain']}")
- log(f" 🆔 ESIR ID: {ESIR_ID} | BE ID: {BE_ID}")
- log(f" 🔑 Test PIN: {PIN_BE}")
+ log(f" 🏢 Firma: {name} | PIB: {tin}")
+ log(f" 🆔 ESIR ID: {ESIR_ID} | BE JID: {jid} (kartica: {BE_HOST}:{BE_PORT})")
log(" ▶ Server pokrenut. Ctrl+C za gašenje.")
server = http.server.HTTPServer((HOST, PORT), FiscalHandler)
diff --git a/Fisk/stop.sh b/Fisk/stop.sh
index 748b85b..1fc1886 100755
--- a/Fisk/stop.sh
+++ b/Fisk/stop.sh
@@ -3,7 +3,7 @@
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PID_FILE="$SCRIPT_DIR/data/server.pid"
-PORT=8989
+PORT=4566
echo "⏹ Zaustavljam fiskalni mock server..."
diff --git a/Koncept.md b/Koncept.md
new file mode 100644
index 0000000..adfdfae
--- /dev/null
+++ b/Koncept.md
@@ -0,0 +1,1009 @@
+# 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 `