Kartica emulator (BE) + fiskalna verifikacija + ime/prezime korisnika

- internal/be: emulator kartice (TCP listener za L-PFR mock)
- /v/: javna stranica za verifikaciju fiskalnog računa (QR kod)
- /servis/{id}/fiskalni-racun: štampa fiskalnog računa
- Podešavanja fiskalizacije: BE status/reset, proširen UI
- Korisnik: polja ime i prezime (model, DB, handler, admin profil)
- Fisk server: ažuriran receipt.py i server.py
-, : dokumentacija
This commit is contained in:
2026-06-26 23:17:52 +02:00
parent 985a2e7410
commit 97fd4490ea
21 changed files with 1425 additions and 116 deletions
+36 -17
View File
@@ -25,21 +25,26 @@ def load_locale(lang="latin"):
locale[key.strip()] = value.strip()
return locale
def _sr(n, decimals=2):
"""Srpski format: tačka za hiljade, zarez za decimale — 2.967,17"""
s = f"{n:,.{decimals}f}"
return s.replace(",", "\x00").replace(".", ",").replace("\x00", ".")
def price(n):
"""Formatira cenu: ###,###.00"""
return f"{n:,.2f}"
"""Formatira cenu: ###.###,00"""
return _sr(n, 2)
def qty(n):
"""Formatira količinu: ###,###.000"""
return f"{n:,.3f}"
"""Formatira količinu: ###.###,000"""
return _sr(n, 3)
def amount(n):
"""Formatira iznos: ###,###.00"""
return f"{n:,.2f}"
"""Formatira iznos: ###.###,00"""
return _sr(n, 2)
def number(n):
"""Formatira broj: ###,###"""
return f"{n:,.0f}"
"""Formatira broj: ###.###"""
return _sr(n, 0)
def dt(iso_string):
"""Konvertuje ISO datetime u format: dd.MM.yyyy. HH:mm:ss"""
@@ -103,6 +108,20 @@ TRANSACTION_TYPES_LAT = {
"PSX": "PREDRAČUN - PRODAJA", "PRX": "PREDRAČUN - REFUNDACIJA",
"TSX": "OBUKA - PRODAJA", "TRX": "OBUKA - REFUNDACIJA",
}
# NTech šalje invoiceType+transactionType kao reči (Normal/Sale) umesto koda (NSX)
_INV_TX_TO_CODE = {
("Normal", "Sale"): "NSX", ("Normal", "Refund"): "NRX",
("Advance", "Sale"): "ASX", ("Advance", "Refund"): "ARX",
("Copy", "Sale"): "CSX", ("Copy", "Refund"): "CRX",
("Training", "Sale"): "TSX", ("Training", "Refund"): "TRX",
("Proforma", "Sale"): "PSX", ("Proforma", "Refund"): "PRX",
}
def _tx_code(inv):
"""Vraća 3-slovni kod transakcije (NSX, NRX...) iz full_data rečnika."""
it = inv.get("invoiceType", "Normal")
tt = inv.get("transactionType", "Sale")
return _INV_TX_TO_CODE.get((it, tt), tt)
# ── Glavna funkcija ─────────────────────────────────────────
@@ -118,6 +137,7 @@ def generate_receipt(invoice_data, lang="latin"):
tx_types = TRANSACTION_TYPES_LAT if lang == "latin" else TRANSACTION_TYPES_CYR
W = 48
inv = invoice_data
tx_label = tx_types.get(_tx_code(inv), _tx_code(inv))
lines = []
# ── PREAMBLE ──
@@ -160,8 +180,6 @@ def generate_receipt(invoice_data, lang="latin"):
lines.append(layout(m.get("ref-doc-dt", "Ref. vreme"), dt(inv["referentDocumentDT"]), W))
# ── TIP TRANSAKCIJE ──
tx_code = inv.get("transactionType", "NSX")
tx_label = tx_types.get(tx_code, tx_code)
lines.append(title(tx_label, "-", W))
# ── ARTIKLI ──
@@ -215,7 +233,8 @@ def generate_receipt(invoice_data, lang="latin"):
if not is_covered_by_advance:
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")
lines.append(layout(ptype, amount(float(p.get("amount", 0))), W))
if inv.get("invoiceType") == "Proforma":
lines.append(layout(m.get("refund", "Povraćaj"), amount(0), W))
@@ -249,7 +268,7 @@ def generate_receipt(invoice_data, lang="latin"):
# ── PFR VREDNOSTI ──
lines.append(layout(m.get("sdc-time", "PFR vreme"), dt(inv.get("sdcDateTime", "")), W))
lines.append(layout(m.get("sdc-invoice-number", "PFR broj računa"), str(inv.get("invoiceNumber", "")), W))
lines.append(layout(m.get("sdc-invoice-counter", "Brojač računa"), str(inv.get("invoiceNumber", "")), W))
lines.append(layout(m.get("sdc-invoice-counter", "Brojač računa"), str(inv.get("invoiceCounter", "")), W))
lines.append(separator("=", W))
# ── QR KOD ──
@@ -290,8 +309,7 @@ def generate_receipt_html(invoice_data, lang="latin"):
tx_types = TRANSACTION_TYPES_LAT if lang == "latin" else TRANSACTION_TYPES_CYR
inv = invoice_data
tx_code = inv.get("transactionType", "NSX")
tx_label = tx_types.get(tx_code, tx_code)
tx_label = tx_types.get(_tx_code(inv), _tx_code(inv))
is_fiscal = inv.get("isFiscal", True)
is_refund = inv.get("transactionType") == "Refund"
inv_type = inv.get("invoiceType", "Normal")
@@ -331,7 +349,8 @@ def generate_receipt_html(invoice_data, lang="latin"):
payments_rows += f'<tr><td class="l">{m.get("advance-tax", "PDV na avans")}</td><td class="r">{amount(advance_tax)}</td></tr>'
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'<tr><td class="l">{ptype}</td><td class="r">{amount(float(p.get("amount", 0)))}</td></tr>'
if inv_type == "Proforma":
payments_rows += f'<tr><td class="l">{m.get("refund", "Povraćaj")}</td><td class="r">{amount(0)}</td></tr>'
@@ -404,7 +423,7 @@ def generate_receipt_html(invoice_data, lang="latin"):
.hdr-sub {{ font-size: 11pt; font-weight: bold; }}
.title {{ font-weight: bold; font-size: 12pt; margin: 1.5mm 0; }}
.qr {{ text-align: center; margin: 2mm 0; }}
.qr img {{ width: 25mm; height: 25mm; }}
.qr img {{ width: 60mm; height: 60mm; }}
.preamble {{ font-style: italic; margin: 1mm 0; font-size: 7pt; }}
.row {{ display: flex; justify-content: space-between; }}
.col-left {{ width: 72%; }}
@@ -472,7 +491,7 @@ def generate_receipt_html(invoice_data, lang="latin"):
<table>
<tr><td class="l">{m.get('sdc-time', 'PFR vreme')}</td><td class="r">{dt(inv.get('sdcDateTime', ''))}</td></tr>
<tr><td class="l">{m.get('sdc-invoice-number', 'PFR broj računa')}</td><td class="r">{inv.get('invoiceNumber', '')}</td></tr>
<tr><td class="l">{m.get('sdc-invoice-counter', 'Brojač računa')}</td><td class="r">{inv.get('invoiceNumber', '')}</td></tr>
<tr><td class="l">{m.get('sdc-invoice-counter', 'Brojač računa')}</td><td class="r">{inv.get('invoiceCounter', '')}</td></tr>
</table>
<div class="sep-double"></div>