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:
@@ -8,9 +8,9 @@ pos-invoice-number=ЕСИР број
|
||||
pos-time=ЕСИР време
|
||||
ref-doc-number=Реф. број
|
||||
ref-doc-dt=Реф. време
|
||||
sdc-invoice-counter=Бројач
|
||||
sdc-invoice-number=Број рачуна
|
||||
sdc-time=Време
|
||||
sdc-invoice-counter=Бројач рачуна
|
||||
sdc-invoice-number=ПФР број рачуна
|
||||
sdc-time=ПФР време
|
||||
uid=УИД
|
||||
items=Артикли
|
||||
item-name=Назив
|
||||
@@ -25,17 +25,17 @@ quantity=Количина
|
||||
price=Цена
|
||||
amount=Износ
|
||||
unitPrice=Јед. цена
|
||||
tax-label=ПДВ
|
||||
tax-name=Порез
|
||||
tax-label=Ознака
|
||||
tax-name=Име
|
||||
tax-rate=Стопа
|
||||
tax-amount=Порез
|
||||
tax=Порез
|
||||
total-tax=Укупан порез
|
||||
total-tax=Укупан износ пореза
|
||||
advance-tax=Авансни порез
|
||||
to-pay=За уплату
|
||||
to-pay=Укупан износ
|
||||
paid-in-advance=Плаћено авансно
|
||||
remaining=Преостало
|
||||
refund=Рефундација
|
||||
refund=Повраћај
|
||||
total-refund=Укупна рефундација
|
||||
total-payment=Укупно уплаћено
|
||||
payment=Уплаћено
|
||||
@@ -46,3 +46,11 @@ report-number=Број извештаја
|
||||
per-transaction-type=ПРОМЕТ ПО ВРСТИ
|
||||
customer-signature=Потпис купца
|
||||
end-of-fiscal-invoice=КРАЈ ФИСКАЛНОГ РАЧУНА
|
||||
# Типови плаћања
|
||||
Cash=Готовина
|
||||
Card=Платна картица
|
||||
Check=Чекови
|
||||
Voucher=Ваучер
|
||||
MobileMoney=Инстант плаћање
|
||||
WireTransfer=Пренос на рачун
|
||||
Other=Остало
|
||||
|
||||
@@ -8,9 +8,9 @@ pos-invoice-number=ESIR broj
|
||||
pos-time=ESIR vreme
|
||||
ref-doc-number=Ref. broj
|
||||
ref-doc-dt=Ref. vreme
|
||||
sdc-invoice-counter=Brojač
|
||||
sdc-invoice-number=Broj računa
|
||||
sdc-time=Vreme
|
||||
sdc-invoice-counter=Brojač računa
|
||||
sdc-invoice-number=PFR broj računa
|
||||
sdc-time=PFR vreme
|
||||
uid=UID
|
||||
items=Artikli
|
||||
item-name=Naziv
|
||||
@@ -25,17 +25,17 @@ quantity=Količina
|
||||
price=Cena
|
||||
amount=Iznos
|
||||
unitPrice=Jed. cena
|
||||
tax-label=PDV
|
||||
tax-name=Porez
|
||||
tax-label=Oznaka
|
||||
tax-name=Ime
|
||||
tax-rate=Stopa
|
||||
tax-amount=Porez
|
||||
tax=Porez
|
||||
total-tax=Ukupan porez
|
||||
total-tax=Ukupan iznos poreza
|
||||
advance-tax=Avansni porez
|
||||
to-pay=Za uplatu
|
||||
to-pay=Ukupan iznos
|
||||
paid-in-advance=Plaćeno avansno
|
||||
remaining=Preostalo
|
||||
refund=Refundacija
|
||||
refund=Povraćaj
|
||||
total-refund=Ukupna refundacija
|
||||
total-payment=Ukupno uplaćeno
|
||||
payment=Uplaćeno
|
||||
@@ -46,3 +46,11 @@ report-number=Broj izveštaja
|
||||
per-transaction-type=PROMET PO VRSTI
|
||||
customer-signature=Potpis kupca
|
||||
end-of-fiscal-invoice=KRAJ FISKALNOG RAČUNA
|
||||
# Tipovi plaćanja
|
||||
Cash=Gotovina
|
||||
Card=Platna kartica
|
||||
Check=Čekovi
|
||||
Voucher=Vaučer
|
||||
MobileMoney=Instant plaćanje
|
||||
WireTransfer=Prenos na račun
|
||||
Other=Ostalo
|
||||
|
||||
+36
-17
@@ -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>
|
||||
|
||||
|
||||
+182
-38
@@ -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)
|
||||
|
||||
+1
-1
@@ -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..."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user