From 504b8ca717c432346e3d3541b7cc5fc40405f861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Tue, 30 Jun 2026 18:36:10 +0200 Subject: [PATCH] =?UTF-8?q?podesavanja:=20CodeQL=20suppression=20za=20go/r?= =?UTF-8?q?equest-forgery=20(SSRF=20ve=C4=87=20za=C5=A1ti=C4=87en)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BUG.md | 234 ------- Koncept.md | 1009 ------------------------------- internal/handler/podesavanja.go | 2 +- 3 files changed, 1 insertion(+), 1244 deletions(-) delete mode 100644 BUG.md delete mode 100644 Koncept.md diff --git a/BUG.md b/BUG.md deleted file mode 100644 index a1b6b36..0000000 --- a/BUG.md +++ /dev/null @@ -1,234 +0,0 @@ -# Bug lista — Dashboard "Prihod ovog meseca" - -Istraživanje logike `PrihodTekuciMesec` i sve komponente koje utiču na prikazanu cifru. - ---- - -## BUG-01 — Stornirani prodajni nalozi ulaze u prihod ⚠️ KRITIČAN - -**Fajl:** `internal/db/sqlite/izvestaj.go:39–48` - -**Problem:** SQL upit koji računa prihod ne filtrira stornirane naloge: - -```sql -SELECT SUM(ukupno) FROM prodajni_nalozi -WHERE substr(datum, 1, 7) = strftime('%Y-%m', 'now', 'localtime') --- ❌ nema: AND stornirano = 0 -``` - -Kolona `stornirano INTEGER NOT NULL DEFAULT 0` postoji od migracije `035_pos_faza1.sql`. Kada se nalog stornira, `stornirano = 1` i magacin se vraća, ali iznos ostaje u prihodu. - -**Posledica:** Svaki stornirani nalog uvećava prikazani mesečni prihod za iznos koji nije stvarno naplaćen. Na kraju meseca razlika može biti značajna ako ima više storniranja. - -**Isti propust na još tri mesta u istom fajlu:** -- `MesecniPrihodProdaja` (l.146) — grafikon prihoda po mesecima -- `PoslednjeProdaje` (l.104) — lista poslednjih prodaja na dashboardu -- `TopKlijenti` (l.220) — rang lista kupaca po vrednosti - -**Ispravka:** Dodati `AND stornirano = 0` u sve četiri SQL SELECT-e. - ---- - -## BUG-02 — Mešanje PDV-a: prodaja bruto, servis neto ⚠️ KRITIČAN - -**Fajl:** `internal/db/sqlite/izvestaj.go:39–48` - -**Problem:** Zbir spaja dve veličine koje nisu uporedive: - -| Izvor | Kolona | PDV | -|---|---|---| -| `prodajni_nalozi.ukupno` | `kolicina × cena_po_komadu` | **SA PDV-om** (bruto) | -| `servisni_nalozi.cena_konacna` | `dijagnostika + radovi + delovi` | **BEZ PDV-a** (neto) | - -Prodajna cena se unosi kao maloprodajna (bruto) cena — to potvrđuje `handler/prodaja.go:188–191`: -```go -ukupno += float64(s.Kolicina) * s.CenaPoKomadu // bruto -nalog.Ukupno = ukupno -``` - -`cena_konacna` za servis se auto-računa koristeći `rad.Ukupno()` i `deo.Ukupno()` (model `servis.go:77,127`) koji vraćaju `kolicina × cena_komada` — neto cenu bez PDV-a. - -**Posledica:** Prikazana cifra nije ni ukupan prihod sa PDV-om ni ukupan neto prihod. Za firme sa PDV-om (stopa 20%) to može biti razlika od 20% na delu koji dolazi iz servisa. - -**Ispravka:** Odlučiti se za jedinstven standard (preporučeno: **sa PDV-om**) i koristiti `cena_sa_pdv` kolone / `UkupnoSaPdv()` metode konzistentno. - ---- - -## BUG-03 — Delovi servisa ne ulaze u prihod kada je cena ručno uneta ⚠️ VAŽAN - -**Fajl:** `internal/db/sqlite/izvestaj.go:44–48`, `internal/handler/servis.go:1498–1500` - -**Problem:** Šablon servisa tretira `cena_konacna` kao cenu rada (bez delova): - -```go -// servis.go:1498–1500 -} else if nalog.CenaKonacna != nil { - ukupnoSve = *nalog.CenaKonacna + ukupnoDelovi // delovi se DODAJU na cenu_konacnu -``` - -Kada korisnik ručno unese `cena_konacna` (polje "Cena rada"), u bazu ide samo vrednost rada — delovi su odvojeni. SQL za prihod uzima samo `cena_konacna`: - -```sql -SELECT SUM(cena_konacna) FROM servisni_nalozi WHERE status = 'Preuzeto' ... -``` - -**Posledica:** Vrednost svih ugrađenih delova na servisnim nalozima sa ručno unetom `cena_konacna` **ne ulazi u prikazani prihod**. Prihod je manji od stvarnog. - -**Napomena:** Kada je status prelaz na "Preuzeto" i `cena_konacna` je bila `NULL`, auto-izračun (`servis.go:2041–2048`) uključuje delove u `cena_konacna`. Dakle ponašanje zavisi od toga da li je korisnik uneo cenu ručno ili je sistem auto-izračunao — nedoslednost u definiciji polja. - ---- - -## BUG-04 — Duplo računanje delova u naplaćenom iznosu pri auto-izračunu ⚠️ VAŽAN - -**Fajl:** `internal/handler/servis.go:2034–2083` - -**Problem:** Kada servisni nalog nema unesenu `cena_konacna` i prelazi u status "Preuzeto", dešava se: - -1. Auto-izračun (l.2037–2049): `cena_konacna = dijagnostika + radovi + **delovi**` → snima u DB -2. Izračun naplaćenog iznosa (l.2064–2073): čita novu `cena_konacna` iz DB, pa dodaje `ukupnoDelovi` još jednom: - -```go -nalog.CenaKonacna = &ukupno // ukupno već sadrži delove (l.2049) -// ... -nalog, _ = h.ServisRepo.DohvatiID(...) // čita iz DB — cena_konacna uključuje delove -iznos = *nalog.CenaKonacna + ukupnoDelovi // ← delovi se broje DVAPUT -``` - -**Posledica:** Iznos koji se fiskalizuje i beleži kao naplaćen veći je od stvarno dugovanog (delovi duplo). Ovo je greška u fiskalnom računu. - -**Reprodukcija:** Napraviti servisni nalog, dodati deo (npr. 1.000 din), ne unositi cenu rada, promeniti status u "Preuzeto" bez popunjavanja forme. Naplaćeni iznos biće 2.000 umesto 1.000. - ---- - -## BUG-05 — CSS skriva cifru prihoda — UX propust - -**Fajl:** `web/templates/stranice/dashboard.html:16–23` - -**Problem:** Cifra prihoda ima `opacity: 0` po defaultu i prikazuje se tek posle **1 sekunde hover-a**: - -```css -.prihod-cifra { - opacity: 0; - transition: opacity 0.3s ease; -} -.dash-stat:hover .prihod-cifra { - opacity: 1; - transition-delay: 1s; /* ← 1 sekunda čekanja */ -} -``` - -Labela "Prihod ovog meseca" je vidljiva, ali cifra nije. Na mobilnim uređajima (bez hover) cifra je **trajno nevidljiva**. - -**Posledica:** Korisnik ne može pročitati prihod na mobilnom uređaju. Na desktopu mora da zna da treba da čeka sekund na kartici. - ---- - -## Sažetak - -| # | Opis | Ozbiljnost | Fajl | -|---|---|---|---| -| BUG-01 | Stornirani nalozi u prihodu | Kritičan | `izvestaj.go:41–43` | -| BUG-02 | Prodaja SA PDV / servis BEZ PDV | Kritičan | `izvestaj.go:39–48` | -| BUG-03 | Delovi servisa izostaju kod ručne cene | Važan | `izvestaj.go:44–48` | -| BUG-04 | Duplo računanje delova u naplaćenom iznosu | Važan | `servis.go:2034–2083` | -| BUG-05 | CSS skriva cifru na mobilnom | UX | `dashboard.html:16–23` | - -> BUG-01 do BUG-05 su ISPRAVLJENI u ovoj sesiji. - ---- - -# Novi nalazi — pregled koda 2026-06-27 - -Drugi krug pregleda, posle ispravki BUG-01..05. - -> **BUG-06 do BUG-10 su ISPRAVLJENI i pokriveni testovima** (2026-06-27). -> -> | # | Ispravka | Test | -> |---|---|---| -> | 06/07 | `PdvKir.DodajNeto` — neto osnovica + PDV po stvarnoj stopi; servis KIR ga koristi | `TestPdvKirDodajNeto` | -> | 08 | `TopKlijenti` servis: `naplaceno+avans WHERE status='Preuzeto'` | `TestTopKlijenti_SamoPreuzetiNalozi` | -> | 09 | `kljuceviMeseci` sidri na 1. u mesecu | `TestKljuceviMeseci` | -> | 10 | prihod servisa = `SUM(naplaceno + COALESCE(avans,0))`, filter `(naplaceno>0 OR avans>0)` | `TestPrihodTekuciMesec_ServisSaAvansom`, `...PotpunoAvansiran`, `...GarancijaNeUlazi` | - -## BUG-06 — Servis KIR: neto cena tretirana kao bruto ⚠️ KRITIČAN (poreski) - -**Fajl:** `internal/handler/servis.go:2114–2133` (auto-upis u KIR pri prelasku u „Preuzeto") - -**Problem:** Kod računa osnovicu i PDV deljenjem sa 1.2, tretirajući `r.Ukupno()` kao bruto: - -```go -osnovica := r.Ukupno() / 1.2 // 20% PDV -pdv := r.Ukupno() - osnovica -kir.Ukupno += r.Ukupno() -``` - -Ali `cena_komada` rada/dela je **NETO** (bez PDV-a) — potvrđeno u `servisni_radovi.go:40`: -```go -rad.CenaSaPdv = rad.CenaKomada * (1 + rad.PdvStopa/100) -``` -Dakle `r.Ukupno() = kolicina × cena_komada` je već osnovica (neto). Deljenjem neto vrednosti sa 1.2 dobija se **premala osnovica**, a PDV se računa na pogrešnu (umanjenu) bazu. `kir.Ukupno` je zapravo neto, iako kolona „Ukupno" treba da bude bruto sa PDV-om. - -**Primer (rad 1000 din neto, 20%):** -- Tačno: osnovica 1000, PDV 200, ukupno 1200 -- Kod daje: osnovica 833.33, PDV 166.67, ukupno 1000 - -**Poređenje:** `KirIzProdaje` (`model/pdv_evidencija.go:98–116`) to radi **ispravno** — ali tamo je `cena_po_komadu` bruto, pa je deljenje opravdano. Servis je samo prekopirao obrazac bez korekcije za neto cenu. - -**Ispravka:** Za servis koristiti `r.UkupnoSaPdv()` kao bruto, ili direktno: `osnovica = r.Ukupno()`, `pdv = r.Ukupno() * stopa/100`. - ---- - -## BUG-07 — Servis KIR: hardkodovana stopa 20%, ignoriše PdvStopa ⚠️ VAŽAN (poreski) - -**Fajl:** `internal/handler/servis.go:2118, 2128` - -**Problem:** Stopa je fiksirana na `/ 1.2` (20%) iako i rad i deo imaju polje `PdvStopa`. Sve stavke se sabiraju u `OsnovicaOpsta`/`PdvOpsta`, bez razvrstavanja na opštu (20%), posebnu (10%) i oslobođen promet. - -`KirIzProdaje` to radi ispravno preko `switch s.PdvStopa { case 20 … case 10 … default … }`. Servisni KIR ne. Usluga/deo sa 10% ili 0% PDV biće pogrešno evidentiran. - -**Ispravka:** Razvrstati po `rad.PdvStopa` / `deo.PdvStopa` kao u `KirIzProdaje`. - ---- - -## BUG-08 — TopKlijenti: servis bez statusa + neto cena, nedosledno ⚠️ SREDNJI - -**Fajl:** `internal/db/sqlite/izvestaj.go:228–231` - -**Problem:** Podupit za servis i dalje koristi: -```sql -SELECT klijent_id, SUM(cena_konacna) … FROM servisni_nalozi -WHERE cena_konacna IS NOT NULL GROUP BY klijent_id -``` - -- **Nema filtera statusa** → broji i naloge koji još nisu preuzeti („U popravci", „Čeka delove"), pa i one koji nikad neće biti naplaćeni. -- Koristi **neto** `cena_konacna` (bez delova, posle BUG-04 fixa), dok dashboard i mesečni grafikon sada koriste **bruto** `naplaceno`. - -Posledica: isti klijent ima različitu „ukupnu vrednost" na različitim ekranima. - -**Ispravka:** Uskladiti sa ostatkom: `SUM(naplaceno) WHERE status='Preuzeto' AND naplaceno > 0`. (Prodajni podupit je već usklađen sa `stornirano = 0`.) - ---- - -## BUG-09 — Izveštaji: 12-mesečni grafikon preskače mesece na kraju meseca ⚠️ LATENTAN - -**Fajl:** `internal/handler/izvestaji.go:117–119` - -**Problem:** Petlja gradi ključeve meseci preko `sada.AddDate(0, -i, 0)`. Go-ov `AddDate` normalizuje prelivanje dana: npr. 31. mart − 1 mesec = „31. februar" → 3. mart. Na 29/30/31. u mesecu neki mesec se **duplira**, a drugi (npr. februar) dobije ključ koji se nikad ne generiše → prikaže 0. - -Ne manifestuje se danas (27.), ali se javlja svakog 29–31. u mesecu. - -**Ispravka:** Računati ključ preko prvog u mesecu, npr. `time.Date(god, mesec, 1, …)` sa ručnim oduzimanjem meseci, ili `AddDate` nad `BeginningOfMonth`. - ---- - -## BUG-10 — Avans se ne uračunava u prihod (regresija od fixa BUG-02/03) ⚠️ VAŽAN - -**Fajl:** `internal/db/sqlite/izvestaj.go:46–49` (i `MesecniPrihodServis`) - -**Problem:** Prihod od servisa sada je `SUM(naplaceno)`. Ali `naplaceno` je iznos naplaćen **pri preuzimanju** = `(cena_konacna + delovi) − avans` (`servis.go:2069–2073`). Avans je naplaćen ranije i nigde se ne evidentira kao zaseban prihod. - -Dve posledice: -1. Za naloge sa avansom, deo prihoda pokriven avansom **nedostaje** iz „prihoda meseca". Stari kod (`cena_konacna`) je taj deo uključivao. -2. Filter `naplaceno > 0` (dodat da izbaci garancijske popravke) **potpuno izbacuje** naloge gde je avans pokrio ceo iznos (`naplaceno = 0`) — iako su plaćeni. - -**Napomena:** Ovo je svesni kompromis mog prethodnog fixa (`naplaceno` je gotovinski tačan za naloge bez avansa). Treba odlučiti definiciju prihoda. Ako je cilj ukupan prihod: `SUM(naplaceno + COALESCE(avans, 0))` za `status='Preuzeto'`, uz uslov `(naplaceno > 0 OR avans > 0)` umesto samo `naplaceno > 0`. diff --git a/Koncept.md b/Koncept.md deleted file mode 100644 index adfdfae..0000000 --- a/Koncept.md +++ /dev/null @@ -1,1009 +0,0 @@ -# Koncept fiskalizacije — NTech + L-PFR - -Dokument opisuje arhitekturu fiskalizacije prema Tehničkom vodiču za L-PFR/ESIR -i plan implementacije za NTech + Fisk mock server. - ---- - -## 1. Učesnici u sistemu - -``` -┌──────────┐ zahtev za ┌──────────┐ potpisivanje ┌──────────┐ -│ NTech │─── fiskalizaciju ───→│ L-PFR │─── (APDU komande)──→│ Kartica │ -│ (ESIR) │ │ (mock) │ │ (BE) │ -│ │←── fiskalni račun ──│ │←── potpisani podaci─│ │ -└──────────┘ └──────────┘ └──────────┘ - │ - │ paketi za iščitavanje - ↓ - ┌──────────┐ - │ SUF │ - │ (PU RS) │ - └──────────┘ -``` - -| Komponenta | Šta je | Gde se nalazi | -|---|---|---| -| **ESIR** (NTech) | Elektronski sistem za izdavanje računa — kasa, POS | Kod obveznika | -| **L-PFR** (mock) | Lokalni procesor fiskalnih računa — "crna kutija" | Kod obveznika | -| **BE** (kartica) | Bezbednosni element na pametnoj kartici | Kod obveznika (fizički) | -| **SUF** | Sistem Uprave za Fiskalizaciju — Poreska uprava | Cloud PU RS | - -## 2. Bezbednosni element — pametna kartica (BE) - -Kartica je **jedini izvor identiteta** obveznika u sistemu fiskalizacije. -Nju izdaje Poreska uprava, personalizovana je za konkretnog obveznika -i poslovni prostor. - -### 2.1 Šta se nalazi na kartici - -| Podatak | Opis | Primer | -|---|---|---| -| **JID** | Jedinstveni identifikator — 8 alfanumeričkih znakova | `TRNMOCK1` | -| **PIB** | Poreski identifikacioni broj obveznika | `123456789` | -| **Naziv firme** | Poslovno ime obveznika | `Dalibor DOO` | -| **Poslovni prostor** | Adresa, grad, opština | `Beograd, Savski Venac` | -| **Sertifikat** | Digitalni sertifikat (validan ~3 godine) | `2024-01-01 → 2027-01-01` | -| **PIN** | Lični identifikacioni broj za pristup kartici | `1234` | -| **Brojači** | Ukupan broj računa + po tipu transakcije | `total, pp, pr, ap, kp...` | -| **Limit iščitavanja** | Maksimalni neisčitani iznos pre blokade | definiše PU | -| **Trenutni neisčitani iznos** | Akumulirani iznos od poslednjeg iščitavanja | resetuje se dokazom | -| **Ključevi** | Kriptografski ključevi za digitalno potpisivanje | par privatni/javni | - -### 2.2 Životni ciklus kartice - -1. Obveznik se uvodi u eFiskalizaciju preko portala ePorezi -2. Obveznik zahteva izdavanje BE preko ESF (Elektronski servisi za fiskalizaciju) -3. PU personalizuje karticu (upisuje sertifikat, PIB, JID, limit) -4. Kartica se fizički dostavlja obvezniku -5. Obveznik ubacuje karticu u L-PFR i unosi PIN -6. Kartica potpisuje račune dok ne dostigne limit → potrebno iščitavanje -7. Promena adrese poslovnog prostora → nova kartica -8. Odjava poslovnog prostora → opoziv svih kartica za taj prostor - -### 2.3 Ograničenja kartice - -- Limit iščitavanja — kad neisčitani iznos dostigne limit, kartica **blokira** potpisivanje -- Dokaz iščitavanja (Proof of Audit) sa SUF-a resetuje limit -- Bez interneta: radi u oflajn režimu, akumulira neisčitane račune -- Kartica NE može da se čita direktno — samo L-PFR komunicira sa njom - -## 3. L-PFR — Lokalni procesor fiskalnih računa - -L-PFR je softver/hardver koji radi po principu "crne kutije". -Komunicira samo sa: -- ESIR-om (prima zahteve, vraća fiskalne račune) -- BE karticom (potpisivanje, brojači) -- SUF-om (iščitavanje — internet ili lokalno) - -### 3.1 Odgovornosti L-PFR-a - -| Funkcija | Opis | -|---|---| -| Prijem zahteva | Prima podatke o transakciji od ESIR-a (JSON) | -| PDV obračun | Računa poresku obavezu po stopama (Ж=20%, Ђ=10%, А=0%) | -| Potpisivanje | Prosleđuje podatke kartici, dobija digitalni potpis | -| Generisanje broja | Formira jedinstveni broj računa: `{JID_ESIR}-{JID_BE}-{brojač}` | -| QR kod | Generiše verifikacioni URL i QR kod (40×40mm do 50×50mm) | -| Čuvanje | Pamti sve račune u internoj memoriji (neizbrisivo) | -| Iščitavanje | Šalje pakete za iščitavanje u SUF (internet ili USB/SD) | -| Blokada | Kad BE dostigne limit, blokira izdavanje novih računa | - -### 3.2 Vrste L-PFR-a - -| Vrsta | Opis | -|---|---| -| **Hardverski L-PFR** | Fizički uređaj sa čitačem kartica, ekranom, štampačem | -| **Softverski L-PFR** | Softver instaliran na računaru obveznika | -| **Razvojni L-PFR** | Softverska simulacija za development i testiranje ESIR-a | - -Naš Fisk mock je **Razvojni L-PFR**. - -### 3.3 Tipovi računa i transakcija - -| Tip računa | Oznaka | Transakcija | Brojač | Fiskalni? | -|---|---|---|---|---| -| Normal | ПП | Sale (Prodaja) | pp | Da | -| Normal | ПР | Refund (Refundacija) | pr | Da | -| Advance | АП | Sale | ap | Da | -| Advance | АР | Refund | ar | Da | -| Copy | КП | Sale | kp | Ne | -| Copy | КР | Refund | kr | Ne | -| Training | ОП | Sale | op | Ne | -| Training | ОР | Refund | or | Ne | - -## 4. ESIR — Elektronski sistem za izdavanje računa (NTech) - -ESIR je ono što koristi kasir. Njegove odgovornosti: - -1. Prikuplja podatke o transakciji (artikli, količine, cene, plaćanje) -2. Formira zahtev za fiskalizaciju i šalje ga L-PFR-u -3. Prima fiskalni račun od L-PFR-a -4. Prikazuje/štampa račun kupcu (sa QR kodom) -5. NE potpisuje račun — to radi kartica preko L-PFR-a - -### 4.1 Šta ESIR šalje L-PFR-u (InvoiceRequest) - -```json -{ - "invoiceRequest": { - "invoiceType": "Normal", - "transactionType": "Sale", - "cashier": "Dalibor", - "buyerId": "10:123456789", - "items": [ - { - "name": "RAM 16GB DDR4", - "labels": ["Ж"], - "totalAmount": 7200.00, - "unitPrice": 7200.00, - "quantity": 1.000 - } - ], - "payment": [ - { - "amount": 7200.00, - "paymentType": "Cash" - } - ] - } -} -``` - -**Napomena:** Podaci o firmi (PIB, naziv, adresa) su NA KARTICI (BE), ne u ESIR-u. -L-PFR ih čita sa kartice i stavlja u fiskalni račun. ESIR ih NE šalje. - -### 4.2 Šta L-PFR vraća ESIR-u (InvoiceResponse) - -```json -{ - "requestedBy": "NTECH001", - "signedBy": "TRNMOCK1", - "sdcDateTime": "2026-06-26T13:00:00.000+02:00", - "invoiceNumber": "NTECH001-TRNMOCK1-42", - "invoiceCounter": "3/42ПП", - "invoiceCounterExtension": "ПП", - "verificationUrl": "https://sandbox.suf.purs.gov.rs/v/?vl=NTECH001-TRNMOCK1-42", - "verificationQRCode": "iVBORw0KG...", - "totalAmount": 7200.00, - "totalTax": 1200.00, - "taxItems": [ - {"label": "Ж", "categoryName": "PDV", "rate": 20.0, "amount": 1200.00} - ], - "messages": "Success", - "journal": "========= FISKALNI RAČUN =========\n..." -} -``` - -## 5. Tok fiskalizacije — korak po korak - -``` - ESIR (NTech) L-PFR (mock) Kartica (BE) - │ │ │ - │ 1. POST /api/invoices │ │ - │ {invoiceRequest: {...}} │ │ - │────────────────────────────────→│ │ - │ │ │ - │ │ 2. Čita firmu sa kartice │ - │ │ (PIB, naziv, adresa) │ - │ │─────────────────────────────→│ - │ │ │ - │ │ 3. Provera PIN-a (ako treba)│ - │ │─────────────────────────────→│ - │ │←─────────────────────────────│ - │ │ │ - │ │ 4. Potpisivanje podataka │ - │ │ (APDU komanda) │ - │ │─────────────────────────────→│ - │ │←── potpis + novi brojač ─────│ - │ │ │ - │ │ 5. L-PFR računa PDV, │ - │ │ generiše QR, broj računa │ - │ │ │ - │ 6. Fiskalni račun + QR │ │ - │←────────────────────────────────│ │ - │ │ │ - │ 7. NTech čuva fiskalni račun │ 8. Paket za iščitavanje │ - │ prikazuje/štampa kupcu │ čeka slanje u SUF │ - │ │ │ -``` - -## 6. Komunikacija L-PFR ↔ kartica (BE) - -### Kako to radi u stvarnosti - -U pravom sistemu L-PFR ima **fizički čitač kartica**. Kartica je **pasivna** — -ne inicira ništa, ne "zove" nikoga. L-PFR je gospodar: šalje APDU komande -(ISO 7816) kartici kroz čitač, kartica odgovara. - -``` -Pravi sistem: -L-PFR ──APDU komanda──→ čitač kartica → kartica (BE) -L-PFR ←────────── odgovor (bajtovi) ────── kartica (BE) -``` - -Kartica je u suštini **server koji čeka komande** — samo ne sluša na mreži -nego na fizičkom interfejsu čitača. - -### Naš mock preslikava isti smer - -Kartica emulator **sluša** na TCP portu. Fisk (L-PFR mock) se **spaja** na nju -i šalje JSON komande — isti smer kao u stvarnosti, samo TCP+JSON umesto -fizički+APDU. - -Kartica emulator je implementirana unutar NTech Go binarnog fajla (goroutine) -jer tako ima direktan pristup bazi podataka za podatke o firmi, bez deljenja -fajlova između kontejnera. - -``` -┌──────────────────────────┐ HTTP :3000 ┌──────────────┐ -│ NTech (Go) │←─────────────→│ Fisk │ -│ ├── ESIR logika │ │ (L-PFR mock)│ -│ └── kartica emulator │←──────────────│ :4566 │ -│ sluša na :4567 │ TCP :4567 │ │ -└──────────────────────────┘ JSON komande └──────────────┘ -``` - -Fisk se spaja na `ntech:4567`, šalje komandu, čeka odgovor, zatvara konekciju. -NTech kartica emulator nikad ne inicira — samo odgovara. - -### 6.1 API kartice — 4 komande - -Sva komunikacija: JSON linija (`\n` terminated), request-response. - -#### `status` — pročitaj brojače, limit, status - -``` -→ {"command":"status"} -← { - "status":"ok", - "total_counter": 42, - "counters": {"pp":3,"pr":1,"ap":0,"ar":0,"kp":0,"kr":0,"op":0,"or":0}, - "limit": 500000, - "unread_amount": 150000 - } -``` - -#### `certificate` — identitet kartice (firma, PIB, JID) - -``` -→ {"command":"certificate"} -← { - "status":"ok", - "jid": "TRNMOCK1", - "tin": "RS123456789", - "tin_plain": "123456789", - "name": "Dalibor DOO", - "address": "Test Adresa 1", - "city": "Beograd", - "district": "Savski Venac", - "business_unit_id": "BU-001", - "location_name": "Dalibor DOO", - "valid_from": "2024-01-01T00:00:00+01:00", - "valid_to": "2027-01-01T00:00:00+01:00", - "issuer": "Poreska uprava RS" - } -``` - -#### `verify_pin` — provera PIN-a pre pristupa - -``` -→ {"command":"verify_pin", "pin":"1234"} -← {"status":"ok"} - // pogrešan PIN: -← {"status":"error", "code":"2100", "message":"Pogrešan PIN"} -``` - -#### `sign` — potpiši račun, inkrementiraj brojač - -``` -→ { - "command": "sign", - "invoice_type": "Normal", - "transaction_type": "Sale", - "total_amount": 7200.00 - } -← { - "status": "ok", - "counter": 42, - "counter_extension": "ПП", - "type_counter": 3, - "signature": "base64...", - "blocked": false - } - // ako je limit dostignut: -← {"status":"blocked", "message":"Limit iščitavanja dostignut"} -``` - -### 6.2 Šta izlazi iz mocka u emulator kartice - -| Podatak | Gde je sad (mock) | Gde treba (kartica emulator) | -|---|---|---| -| `PIN_BE = "1234"` | server.py:40 | kartica emulator | -| `BE_ID = "TRNMOCK1"` | server.py:27 | kartica emulator — `jid` | -| `ucitaj_firmu()` (naziv, PIB, adresa...) | server.py:47-77 | kartica emulator — `certificate` | -| `get_counter()` / `peek_counter()` | server.py:81-92 | kartica emulator — `sign` / `status` | -| `counter_ext()` | server.py:94-107 | kartica emulator — `sign` | -| `resp_certificate()` | server.py:221-230 | kartica emulator — `certificate` | -| `resp_verify_pin()` | server.py:194-204 | kartica emulator — `verify_pin` | - -### 6.3 Šta ostaje u mocku - -| Funkcija | Ostaje u mocku? | -|---|---| -| Prijem HTTP zahteva od ESIR-a (`/api/invoices`, ...) | ✅ da | -| `TAX_RATES`, `izracunaj_pdv()` | ✅ da | -| Generisanje QR koda | ✅ da | -| Generisanje `invoiceNumber` (`{ESIR_ID}-{JID}-{counter}`) | ✅ da — JID dobija od kartice | -| Snimanje računa (JSON, txt, html, QR PNG) | ✅ da | -| `generate_receipt()`, `generate_receipt_html()` | ✅ da | -| CORS, logging, rutiranje | ✅ da | -| `ucitaj_firmu()` | ❌ zamenjuje se pozivom `certificate` ka kartici | -| Brojači | ❌ idu na karticu | -| PIN verifikacija | ❌ ide na karticu | - -### 6.4 Kako mock poziva karticu (Python) - -```python -import socket, json - -SOCKET_PATH = "/tmp/ntech-be.sock" - -def be_command(cmd: dict) -> dict: - s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - s.connect(SOCKET_PATH) - s.sendall((json.dumps(cmd) + "\n").encode()) - resp = b"" - while True: - chunk = s.recv(4096) - if not chunk: - break - resp += chunk - if b"\n" in resp: - break - s.close() - return json.loads(resp.decode()) -``` - -### 6.5 Novi tok fiskalizacije sa emulatorom - -``` - ESIR (NTech) L-PFR (mock) Kartica (Go) - │ │ │ - │ POST /api/invoices │ │ - │────────────────────→│ │ - │ │ │ - │ │ 1. be_command({ │ - │ │ "command":"certificate"│ - │ │ }) │ - │ │─────────────────────────→│ - │ │←── firma, PIB, JID ──────│ - │ │ │ - │ │ 2. be_command({ │ - │ │ "command":"sign", │ - │ │ "invoice_type":"Normal│ - │ │ "transaction_type":...│ - │ │ "total_amount":7200 │ - │ │ }) │ - │ │─────────────────────────→│ - │ │←── counter, ext, potpis ─│ - │ │ │ - │ │ 3. računa PDV, generiše │ - │ │ QR, broj računa │ - │ │ │ - │←── fiskalni račun ──│ │ - │ │ │ -``` - -**Napomena — `verify_pin`:** PIN se proverava **jednom pri pokretanju** kartice (ili kad `isPinRequired=True` u `/api/status`), ne per-invoice. Komanda `verify_pin` postoji u API-ju kartice radi inicijalizacije i ponovnog otključavanja, ali je ne pozivamo u toku normalnog izdavanja računa. - -### 6.6 Šta radimo u NTech-u (Go) — emulator kartice - -Novi paket: `internal/be/` ili `cmd/karticab`: - -``` -internal/be/ -├── server.go # Unix socket listener, JSON parser -├── kartica.go # stanje kartice: brojači, limit, PIN, firma -└── config.go # env varijable -``` - -Tipovi podataka za karticu (JSON wire format): - -```go -type StatusResponse struct { - Status string `json:"status"` - TotalCounter int `json:"total_counter"` - Counters map[string]int `json:"counters"` - Limit float64 `json:"limit"` - UnreadAmount float64 `json:"unread_amount"` -} - -type CertificateResponse struct { - Status string `json:"status"` - JID string `json:"jid"` - TIN string `json:"tin"` - TINPlain string `json:"tin_plain"` - Name string `json:"name"` - Address string `json:"address"` - City string `json:"city"` - District string `json:"district"` - BusinessUnitID string `json:"business_unit_id"` - LocationName string `json:"location_name"` - ValidFrom string `json:"valid_from"` - ValidTo string `json:"valid_to"` - Issuer string `json:"issuer"` -} - -type SignResponse struct { - Status string `json:"status"` - Counter int `json:"counter"` - CounterExtension string `json:"counter_extension"` - TypeCounter int `json:"type_counter"` - Signature string `json:"signature"` - Blocked bool `json:"blocked"` -} -``` - -Pokretanje kartice (zaseban proces): - -```bash -NTECH_BE_SOCKET=/tmp/ntech-be.sock \ -NTECH_BE_PIN=1234 \ -NTECH_BE_JID=TRNMOCK1 \ -NTECH_BE_FIRMA_NAZIV="Dalibor DOO" \ -NTECH_BE_PIB=123456789 \ -NTECH_BE_ADRESA="Test Adresa 1" \ -NTECH_BE_GRAD=Beograd \ -go run ./cmd/kartica/ -``` - -### 6.7 Docker - -Kartica emulator je goroutine unutar NTech-a — nema zasebnog kontejnera. -NTech expose-uje dva porta: `3000` za ESIR HTTP i `4567` za karticu. - -```yaml -services: - ntech: - build: . - ports: - - "3000:3000" - - "4567:4567" # kartica emulator (goroutine unutar NTech-a) - environment: - - BE_PORT=4567 - - BE_PIN=1234 - - BE_JID=TRNMOCK1 - - fisk: - build: ./Fisk - ports: - - "4566:4566" - environment: - - BE_HOST=ntech # hostname NTech kontejnera u Docker mreži - - BE_PORT=4567 - - ESIR_ID=NTECH001 -``` - -## 7. Poznati bugovi u trenutnom mocku (pre refaktora) - -### 7.1 QR kod u HTML računu je prazan — `receipt.py:370` - -```python -# Trenutno (pogrešno): -qr_src = f"data:image/png;base64,{inv.get('qrCode', '')}" - -# Ispravno — polje se zove verificationQRCode: -qr_src = f"data:image/png;base64,{inv.get('verificationQRCode', '')}" -``` - -### 7.2 `invoiceCounter` prikazuje `invoiceNumber` — `receipt.py:252-253` - -```python -# Trenutno (oba reda prikazuju invoiceNumber): -lines.append(layout(..., str(inv.get("invoiceNumber", "")), W)) -lines.append(layout(..., str(inv.get("invoiceNumber", "")), W)) - -# Ispravno — drugi red: -lines.append(layout(m.get("sdc-invoice-counter", "Brojač računa"), str(inv.get("invoiceCounter", "")), W)) -``` - -## 8. Napomena — podaci firme - -Kartica emulator je goroutine unutar NTech procesa, pa ima direktan pristup -NTech bazi podataka. Pri pokretanju čita podatke firme iz `podesavanja` tabele -(naziv, PIB, adresa...) — isto što `ucitaj_firmu()` radi u Python mocku danas, -samo direktno bez deljenja fajlova između kontejnera. - -Ovo je ispravno modelovanje: prava kartica ima zamrznute podatke upisane pri -personalizaciji. Ako se adresa ili naziv firme promeni u NTech podešavanjima, -treba restartovati NTech (i kartica emulator se pokreće ponovo sa novim podacima). - ---- - -## 9. Funkcije u Fisk/ Python mocku - -### 9.1 server.py — L-PFR server - -| Funkcija | Linija | Opis | -|---|---|---| -| `ucitaj_firmu()` | ~47 | Čita naziv, PIB, adresu iz NTech SQLite-a (env `NTECH_SQLITE`); fallback na test vrednosti | -| `get_counter(tip)` | ~81 | Inkrementira i vraća brojač iz fajla u `COUNTER_DIR`; tip = "pp", "pr", "ap"... | -| `peek_counter(tip)` | ~87 | Čita brojač bez inkrementiranja | -| `counter_ext(invoice_type, transaction_type)` | ~94 | Vraća ćirilični sufiks (ПП, ПР, АП...) i ključ za brojač | -| `izracunaj_pdv(items)` | ~110 | Grupiše stavke po poreskoj oznaci; formula: `pdv = bruto * stopa / (100 + stopa)` | -| `_build_invoice_response(req, request_id)` | ~130 | **Glavni builder** — računa PDV, poziva karticu, generiše invoiceNumber, QR, snima JSON/txt/html/PNG | -| `resp_certificate()` | ~221 | Vraća identitet (firma, PIB, JID) — u budućnosti delegira kartici | -| `resp_verify_pin(body)` | ~194 | Proverava PIN — u budućnosti delegira kartici | -| `resp_status()` | ~170 | Vraća L-PFR status: `isPinRequired`, `lastInvoiceNumber`, TIN | -| `resp_attention()` | ~165 | Health check — vraća `"status":"ok"` | -| `resp_settings_get/post()` | ~240 | Teron podešavanja (poreske stope, locale) | -| `resp_invoice_final()` | ~260 | Konačni račun koji zatvara avansne transakcije | -| `resp_invoice_last/by_request/by_number/search()` | ~280+ | Pregled ranije izdatih računa | - -**Konstante:** -- `TAX_RATES`: `{"Ж":20, "Ђ":10, "Е":10, "А":0, "Г":0, "З":0}` + generičke oznake -- `BE_ID = "TRNMOCK1"` — JID bezb. elementa -- `ESIR_ID = "NTECH001"` — ESIR identifikator -- `PIN_BE = "1234"` — PIN kartice -- `ROUTES` — 12 HTTP ruta, Teron API kompatibilne - -### 9.2 receipt.py — generator fiskalnih računa - -| Funkcija | Opis | -|---|---| -| `generate_receipt(invoice_data, lang)` | Tekstualni račun širine 48 znakova, prati `agent-invoice.vm` template | -| `generate_receipt_html(invoice_data, lang)` | HTML za A4 štampu, monospace font, `@page size A4` | -| `generate_report(report_data, lang)` | Dnevni/periodični izveštaj (standard-report.vm) | -| `load_locale(lang)` | Učitava `locale_latin.properties` ili `locale_cyrillic.properties` iz `data/` | -| `price()`, `qty()`, `amount()` | Formatiranje brojeva (2 decimale, hiljade sa tačkom) | -| `center(text, width)` | Centriranje teksta na širini | -| `layout(levo, desno, width)` | Red sa levim i desnim tekstom, popunjen razmacima | -| `wrap(text, width)` | Prelom dugog teksta u više redova | -| `separator(width)` | Crtana linija razdvajača | -| `title(text, width)` | Naslov uokviren u `== tekst ==` | - -**Mapiranje tipova:** -- `TRANSACTION_TYPES_CYR` i `TRANSACTION_TYPES_LAT` — prevod `"Normal/Sale"` → `"ПРОМЕТ ПРОДАЈЕ"` / `"PROMET PRODAJE"` - ---- - -## 10. Detaljan tok dobijanja fiskalnog računa - -### 10.1 Normalni fiskalni račun (Promet Prodaja — ПП) - -**Preduslov:** Kartica ubačena, PIN prihvaćen (jednom pri pokretanju). - -``` -ESIR (NTech) L-PFR (Fisk) BE (Kartica) SUF - │ │ │ │ - │ 1. Kasir završava │ │ │ - │ transakciju u NTech │ │ │ - │ │ │ │ - │ 2. POST /api/invoices │ │ │ - │ {invoiceRequest} │ │ │ - │─────────────────────────→│ │ │ - │ │ │ │ - │ │ 3. certificate │ │ - │ │─────────────────────────→│ │ - │ │←── PIB, naziv, adresa ───│ │ - │ │ │ │ - │ │ 4. sign {type, amount} │ │ - │ │─────────────────────────→│ │ - │ │ BE: proverava limit │ │ - │ │ BE: ažurira brojač │ │ - │ │ BE: generiše potpis │ │ - │ │←── {counter, ext, sig} ──│ │ - │ │ │ │ - │ │ 5. računa PDV po stopama │ │ - │ │ generiše invoiceNumber│ │ - │ │ generiše QR kod │ │ - │ │ snima račun interno │ │ - │ │ │ │ - │ 6. {invoiceResponse} │ │ │ - │←─────────────────────────│ │ │ - │ │ │ │ - │ 7. prikazuje/štampa │ │ │ - │ kupcu (tekst + QR) │ │ │ - │ │ │ │ - │ │ 8. asinhro — kad ima net:│ │ - │ │ šalje paket ──────────┼────────────────→│ - │ │←──────────────────────── Proof of Audit ───│ -``` - -### 10.2 Format ključnih polja u odgovoru - -| Polje | Format | Primer | Ko generiše | -|---|---|---|---| -| `invoiceNumber` | `{ESIR_JID}-{BE_JID}-{ukupan_br}` | `NTECH001-TRNMOCK1-42` | L-PFR | -| `invoiceCounter` | `{br_ove_vrste}/{ukupan_br}{ext}` | `3/42ПП` | L-PFR + BE | -| `sdcDateTime` | ISO 8601 sa zonom | `2026-06-26T13:00:00.000+02:00` | L-PFR | -| `verificationUrl` | URL ka SUF portalu | `https://suf.purs.gov.rs/v/?vl=...` | L-PFR | -| `verificationQRCode` | base64 PNG | `iVBORw0KG...` | L-PFR (qrcode lib) | -| `totalTax` | float, zaokružen | `1200.00` | L-PFR | - -**ПФР broj (invoiceNumber) za L-PFR:** oba JID su isti (ESIR = BE su isti uređaj). -Za V-PFR su različiti jer V-PFR ima sopstveni BE u cloudu. - -### 10.3 Avansni tok (AP → PP) - -``` -1. AP (Avanс Prodaja) — plaćen avanс -2. AP može se ponavljati (više avansnih uplata) — svaki referencira prethodni AP -3. PP (Promet Prodaja) — zatvaranje transakcije, referencira poslednji AP - - PP MORA sadržati referentni broj poslednjeg AP - - PP iznos = ukupno - plaćeni avans -``` - -**Lanac referenci:** AP1 ← AP2 ← PP (ili AP1 ← AR1 ← AP2 ← PP itd.) - -### 10.4 Refundacija (PR) - -``` -PR (Promet Refundacija) — MORA imati: - - referenceDocumentNumber: PFR broj originalnog PP računa - - buyerId: OBAVEZNO (identifikacija kupca za refundaciju) -``` - ---- - -## 11. Na šta treba voditi računa - -### 11.1 Podaci firme dolaze sa kartice, NE iz ESIR-a - -``` -❌ Pogrešno: ESIR šalje naziv/PIB/adresu u invoiceRequest -✅ Ispravno: L-PFR čita sa kartice (certificate komanda), ESIR to ne zna -``` - -U mocku: `ucitaj_firmu()` čita iz SQLite. U emulatoru: Go goroutine čita iz iste baze. - -### 11.2 PDV se računa iz bruto iznosa (ne iz neto) - -``` -pdv = bruto * stopa / (100 + stopa) -# za Ж (20%): pdv = 1000 * 20 / 120 = 166.67 (ne 200!) -``` - -Mock formula je ispravna. Greška nastaje ako se uzme `bruto * stopa / 100`. - -### 11.3 PIN se proverava jednom — ne per-invoice - -- Pin se unosi pri **ubacivanju kartice** (pokretanje L-PFR-a) -- Ako je `isPinRequired=True` u `/api/status` — znači kartica je izvučena i ponovo ubačena -- `verify_pin` komanda postoji ali se NE poziva za svaki račun - -### 11.4 Kartica se blokira kad dostigne limit iščitavanja - -- Limit je iznos (ne broj računa) -- Kad se dostigne: `sign` vraća `{"status":"blocked"}` -- Rešenje: iščitavanje (internet → SUF šalje Proof of Audit → resetuje brojač) -- Bez interneta: lokalno iščitavanje (USB/SD → portal PU) -- Za vreme iščitavanja: L-PFR može i dalje da izdaje račune (iščitavanje nije bloker) - -### 11.5 QR kod NE SME sadržati logo ni sliku - -Standard propisuje: QR kod za verifikaciju ne sme biti ispisан na slici ili logu, -niti sadržati sliku ili logo unutar sebe. Minimalna veličina: 40×40mm, maksimalna: 50×50mm. - -### 11.6 Fiskalni dokumenti koji NISU fiskalni računi - -Sledeći tipovi NE registruju promet i moraju nositi napomenu **"OVO NIJE FISKALNI RAČUN"** -(napisano duplo većim fontom): - -| Tip | Šta je | -|---|---| -| **Kopija (КП/КР)** | Kopija već izdatog računa | -| **Obuka (ОП/ОР)** | Za trening kasira, testiranje ESIR-a | -| **Predračun (РП/РР)** | Obaveštavanje kupca o budućem prometu | - -### 11.7 Referentni broj — kada je obavezan - -| Situacija | Ref. broj obavezan? | -|---|---| -| Promet Refundacija (PR) | **DA** — PFR broj originalnog PP | -| Kopija | **DA** — PFR broj originalnog računa | -| Promet Prodaja zatvaranje avansa | **DA** — PFR broj poslednjeg AP | -| Avanс Prodaja (2. i naredni AP) | **DA** — PFR broj prethodnog AP | -| Obični Promet Prodaja | NE | -| Predračun | NE | - -### 11.8 Ime kasira dolazi iz ESIR-a, ne sa kartice - -```json -"invoiceRequest": { - "cashier": "Dalibor" // NTech šalje ovo -} -``` - -Kartica ne zna ko je kasir — to je podatak transakcije. L-PFR kopira polje `cashier` -iz zahteva direktno u odgovor/račun. Nije obavezno po standardu, ali korisno za evidenciju. - -### 11.9 ESIR broj vs PFR broj - -- **ESIR broj** (`buyerCostCenterId`, `referenceNumber`) — opcioni interni broj iz ESIR-a, npr. broj porudžbine -- **PFR broj** (`invoiceNumber`) — jedinstven, nepromenljiv, kreira ga L-PFR; format: `{JID_ESIR}-{JID_BE}-{sekvenca}` -- Referentni broj pri refundaciji = **PFR broj** originalnog, ne ESIR broj - -### 11.10 Bugovi u receipt.py — ✅ ispravljeni - -```python -# BUG 1 — receipt.py:370 — QR kod je prazan u HTML prikazu — ISPRAVLJENO -# Polje se zove verificationQRCode, ne qrCode -qr_src = f"data:image/png;base64,{inv.get('verificationQRCode', '')}" - -# BUG 2 — receipt.py:252 — oba reda prikazuju invoiceNumber — ISPRAVLJENO -str(inv.get("invoiceCounter", "")) # treći red sada koristi invoiceCounter -``` - -### 11.11 Docker — cross-container komunikacija - -Unix socket NE radi između Docker kontejnera bez deljenog volumena. -Rešenje: kartica emulator sluša na TCP portu `4567` unutar NTech kontejnera. - -``` -✅ Fisk env: BE_HOST=ntech BE_PORT=4567 -✅ NTech expose-uje: "4567:4567" -❌ Nemoj koristiti /tmp socket između kontejnera -``` - ---- - -## 12. Izgled fiskalnog računa — razlike i ispravke - -> **Status pregleda:** Sve GR greške su ispravljene. Videti §12.2 za detalje. -> Dodatne ispravke (kasir, servis modal, Čeka delove) u §12.5. - -### 12.1 Referentni račun (OMV) - -Pravi fiskalni račun koji generiše Teron L-PFR (`Dokumenta/Račun.png`): - -``` -============ ФИСКАЛНИ РАЧУН ============ - 101987198 - OMV SRBIJA DOO BEOGRAD - 1083363-Огранак БС Краљево - ДУШАНА ПОПОВИЋА - Краљево -Касир: Radojka Vuković -ЕСИР број: 700/11.71.0.0 ------------ПРОМЕТ ПРОДАЈА----------- - Артикли -======================================== -Назив Цена Кол. Укупно -OMV EP BMB 95 / l (Ђ) - 179,07 16,570 2.967,17 ----------------------------------------- -Укупан износ: 2.967,17 -Платна картица: 2.967,17 -======================================== -Ознака Име Стопа Порез -Ђ О-ПДВ 20,00% 494,53 ----------------------------------------- -Укупан износос пореза: 494,53 -======================================== -ПФР време: 18.12.2025. 11:28:25 -ПФР број рачуна: U92D83M9-U92D83M9-195100 -Бројач рачуна: 192838/195100ПП -======================================== -[QR KOD — centriran, ~60mm x 60mm] -======== КРАЈ ФИСKАЛНОГ РАЧУНА ======== -``` - -### 12.2 Grešake i ispravke - -#### GR-1: Formatiranje brojeva — ✅ ISPRAVLJENO - -**Problem:** Python `f"{n:,.2f}"` daje američki format: `2,967.17` -Srpski/evropski standard: `2.967,17` (tačka za hiljade, zarez za decimale) - -**Gde:** `receipt.py` — funkcije `price()`, `qty()`, `amount()`, `number()` - -**Ispravka:** -```python -def _sr(n, decimals=2): - s = f"{n:,.{decimals}f}" - return s.replace(",", "\x00").replace(".", ",").replace("\x00", ".") -``` -Sve funkcije `price()`, `qty()`, `amount()`, `number()` sada koriste `_sr()`. - ---- - -#### GR-2: Pogrešno polje za Brojač računa — ✅ ISPRAVLJENO - -**Problem:** `receipt.py` linija 252 koristila `inv.get("invoiceNumber")` umesto `inv.get("invoiceCounter")`. -Oba reda (PFR broj + Brojač) prikazivala isti `invoiceNumber`. - -**Ispravka:** Treći red sada koristi `inv.get("invoiceCounter", "")`. - ---- - -#### GR-3: Labele ne odgovaraju stvarnom računu — ✅ ISPRAVLJENO - -**Problem:** Locale fajlovi su imali pogrešne ili engleske labele. - -| Šta se prikazuje | Teron (stvarno) | Pre ispravke | Posle ispravke | -|---|---|---|---| -| Ukupan iznos | `Ukupan iznos` | `Za uplatu` | ✅ `Ukupan iznos` | -| Platna kartica | `Platna kartica` | `Card` | ✅ `Platna kartica` | -| Gotovina | `Gotovina` | `Cash` | ✅ `Gotovina` | -| Čekovi | `Čekovi` | `Check` | ✅ `Čekovi` | -| Vaučer | `Vaučer` | `Voucher` | ✅ `Vaučer` | -| Instant plaćanje | `Instant plaćanje` | `MobileMoney` | ✅ `Instant plaćanje` | -| Prenos na račun | `Prenos na račun` | `WireTransfer` | ✅ `Prenos na račun` | -| Ostalo | `Ostalo` | `Drugo` | ✅ `Ostalo` | -| PFR vreme | `PFR vreme` | `Vreme` | ✅ `PFR vreme` | -| PFR broj računa | `PFR broj računa` | `Broj računa` | ✅ `PFR broj računa` | -| Brojač računa | `Brojač računa` | `Brojač` | ✅ `Brojač računa` | - -**Ispravka:** -- `Fisk/data/locale_latin.properties` i `locale_cyrillic.properties` — dodati `Cash=Gotovina`, `Card=Platna kartica` itd., ispraviti PFR labele -- `receipt.py` — lookup za payment type: `p.get("paymentType", p.get("type", ""))` pa `m.get(pt, pt or "Ostalo")` (bez "Drugo" fallback-a) - ---- - -#### GR-4: QR kod nije centriran na računu — ✅ ISPRAVLJENO - -**Problem:** `StampaFiskalnog` renderovao ceo journal kao jedan `white-space:pre` blok. `` ugrađen unutar `
` ne može se centrirati CSS-om jer `white-space:pre` tretira razmake doslovno.
-
-**Gde:** `internal/handler/servis.go` — `StampaFiskalnog`
-
-**Ispravka:** Koristi se `strings.Cut(journal, "{{{{QR-KOD}}}}")` koji deli journal na deo pre i posle QR-a. Renderuje se kao:
-```html
-
…tekst pre QR…
-
- -
-
…tekst posle QR…
-``` -`body` ima `max-width:max-content;margin:0 auto` da se tekst i QR centriraju kao celina. - ---- - -#### GR-5: QR veličina u HTML verziji receipt.py — ✅ ISPRAVLJENO - -**Problem:** `.qr img { width:25mm; height:25mm; }` — premalo za skeniranje. - -**Ispravka:** Promenjena veličina na `width:60mm; height:60mm;` u `generate_receipt_html`. - ---- - -#### GR-6: Jezik hardkodovan na latinicu — ✅ ISPRAVLJENO - -**Problem:** `server.py` uvek pozivao `generate_receipt(full_data, "latin")`. - -**Ispravka:** -- Fisk čita `fiskalni_pismo` iz env var `FISKALNI_PISMO` (prioritet) ili iz NTech SQLite (`podesavanja` tabela, ključ `fiskalni_pismo`) -- Dodana funkcija `_ucitaj_fiskalni_pismo()` pri pokretanju servera -- Oba poziva `generate_receipt(full_data, FISKALNI_PISMO)` i `generate_receipt_html(full_data, FISKALNI_PISMO)` -- **Restart Fisk servera obavezan** posle promene podešavanja - ---- - -#### GR-7: UI za izbor pisma nedostajao — ✅ ISPRAVLJENO - -**Ispravka:** -- `web/templates/stranice/podesavanja_fiskalizacija.html` — dodat `