Files
GoNtech/Koncept.md
T
Dasko e6215a43fe 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
- Koncept.md, Magacin.md: dokumentacija
2026-06-26 23:17:52 +02:00

42 KiB
Raw Blame History

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)

{
  "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)

{
  "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)

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):

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):

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.

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

# 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 invoiceNumberreceipt.py:252-253

# 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

"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

# 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:

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. <img> ugrađen unutar <pre> ne može se centrirati CSS-om jer white-space:pre tretira razmake doslovno.

Gde: internal/handler/servis.goStampaFiskalnog

Ispravka: Koristi se strings.Cut(journal, "{{{{QR-KOD}}}}") koji deli journal na deo pre i posle QR-a. Renderuje se kao:

<pre>…tekst pre QR…</pre>
<div style="text-align:center">
  <img style="display:block;margin:0 auto;width:72mm;height:72mm;" ...>
</div>
<pre>…tekst posle QR…</pre>

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 <select id="fiskalni_pismo"> (Latinica / Ćirilica)
  • internal/handler/podesavanja.goPodaciPodesavanja dobilo FiskalPismo string; čita se i snima

GR-8: Tip transakcije "Sale" nije preveden — ISPRAVLJENO

Problem: generate_receipt i generate_receipt_html u receipt.py koristile dict TRANSACTION_TYPES_CYR/LAT sa ključevima "NSX", "NRX" itd., ali NTech šalje invoiceType:"Normal" + transactionType:"Sale". Ključevi se nisu poklapali → na računu pisalo Sale.

Ispravka: Dodat _INV_TX_TO_CODE dict koji mapira (invoiceType, transactionType) → interni kod:

_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",
}

Dodata _tx_code(inv) pomoćna funkcija; oba generatora sada koriste tx_types.get(_tx_code(inv), ...).


GR-9: Tip plaćanja prikazuje "Drugo" — ISPRAVLJENO

Problem: receipt.py tražio p.get("type", "") ali NTech šalje "paymentType". Svako plaćanje prikazivalo se kao Drugo.

Ispravka: p.get("paymentType", p.get("type", "")) — pokriva oba ključa radi kompatibilnosti.


GR-10: verify_host URL shema — ISPRAVLJENO

Problem: server.py slepljao http:// ispred VERIFY_HOST čak i kad je vrednost već imala https://. QR kod na računu vodio na http://https://....

Ispravka:

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='')}"

12.3 Redosled ispravki — sve završeno

Greška Fajl Status
GR-1: srpski format brojeva Fisk/receipt.py
GR-2: Brojač računa koristio pogrešno polje Fisk/receipt.py
GR-3: locale labele (payment + PFR) Fisk/data/locale_*.properties + receipt.py
GR-4: QR nije centriran internal/handler/servis.go
GR-5: QR veličina 25mm→60mm Fisk/receipt.py
GR-6: jezik hardkodovan na latin Fisk/server.py
GR-7: UI za izbor pisma podesavanja_fiskalizacija.html + podesavanja.go
GR-8: "Sale" nije preveden Fisk/receipt.py
GR-9: plaćanje uvek "Drugo" Fisk/receipt.py
GR-10: verify_host URL shema Fisk/server.py

12.4 Na šta voditi računa

  • Fisk čita podešavanja samo pri pokretanju — svaka promena verify_host ili fiskalni_pismo zahteva restart Fisk servera.
  • journal se čuva u bazi — već fiskalizovani računi imaju stari tekst; nova podešavanja važe tek za nove račune.
  • strings.Cut u StampaFiskalnog — journal se deli na pre/posle {{{{QR-KOD}}}}. Ako Fisk ne ubaci placeholder, QR se neće prikazati (ali tekst hoće).
  • Širina receipt teksta W=48 — srpski format 2.967,17 ima isti broj cifara kao američki 2,967.17, nema problema sa širinom.
  • Oba locale fajla moraju biti sinhronizovana — svaki ključ u latinici mora postojati i u ćirilici.

12.5 Ostale ispravke — servisni modul

S-1: Čeka delove — nalog ostajao zaključan — ISPRAVLJENO

Problem: DohvatiZaNalog vraćao sve redove uključujući predlozeno=1. Ako su svi pravi zahtevi ispunjeni ali postoje predlozeno=1 redovi (predlozi koji nisu odobreni), nalog je ostajao u "Čeka delove" beskonačno.

Gde: internal/handler/servis.go

Ispravka: Obe tačke odluke (PromeniStatus i self-heal na učitavanju detalja) sada filtriraju samo predlozeno=false redove:

var blokirajuci int
for _, p := range sviPotrazivani {
    if !p.Predlozeno { blokirajuci++ }
}

S-2: Preuzimanje modal — dugme aktivno pre unosa — ISPRAVLJENO

Problem: "Naplati i preuzmi" dugme bilo aktivno odmah, pre nego korisnik unese koliko je klijent platio. Moguće je bila greška pri naplati.

Gde: web/templates/stranice/servis_detalji.html — Preuzimanje modal

Ispravka:

  • Dugme #btn-naplati inicijalno disabled sa reduciranom opacity
  • Polje primljeno ima oninput="ntechPrimljenoUpdate(this)"
  • JS aktivira dugme tek kad primljeno >= Za naplatu (gotovina) ili automatski popunjava iznos za bezgotovinska plaćanja

S-3: Kasir na fiskalnom računu — ISPRAVLJENO

Problem: Na fiskalnom računu u redu "Kasir" pisalo "NTech" jer nije bilo polja za ime kasira.

Rešenje (u fazama):

  1. Dodat pfr_kasir u Podešavanja → Fiskalizacija (globalno polje za ime kasira)
  2. Zatim zamenjen pristupom na nivou korisnika: svaki korisnik ima ime i prezime u profilu

Finalna implementacija:

  • Migracija 086_korisnik_ime_prezime.sql — kolone ime TEXT DEFAULT '' i prezime TEXT DEFAULT '' u tabeli korisnici
  • Model Korisnik dobio Ime i Prezime polja
  • Admin profil stranica — nova kartica "Ime i prezime" sa formom (POST /admin/profil/ime-prezime)
  • fiskalizujServis čita ulogovanog korisnika iz konteksta:
if kor := middleware.KorisnikIzKonteksta(ctx); kor != nil {
    if kor.Ime != "" || kor.Prezime != "" {
        kasir = strings.TrimSpace(kor.Ime + " " + kor.Prezime)
    } else {
        kasir = kor.KorisnickoIme
    }
}

Fallback: "NTech" ako korisnik nije u kontekstu.