- 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
1010 lines
42 KiB
Markdown
1010 lines
42 KiB
Markdown
# 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. `<img>` ugrađen unutar `<pre>` 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
|
||
<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.go` — `PodaciPodesavanja` 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:
|
||
```python
|
||
_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:**
|
||
```python
|
||
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:
|
||
```go
|
||
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:
|
||
```go
|
||
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.
|