diff --git a/Fisk/server.py b/Fisk/server.py index 9793eb1..0b5e3c0 100755 --- a/Fisk/server.py +++ b/Fisk/server.py @@ -227,7 +227,10 @@ def log(msg): # ── QR ────────────────────────────────────────────────────── def generate_qr(url): - img = qrcode.make(url) + qr = qrcode.QRCode(box_size=4, border=2) + qr.add_data(url) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") buf = io.BytesIO() img.save(buf, format="PNG") return base64.b64encode(buf.getvalue()).decode("utf-8") @@ -366,12 +369,22 @@ def _build_invoice_response(req, request_id): "total_amount": total_amount, }) + # Ako je BE restovan posle naše PIN verifikacije, pinUnesen je opet False — + # resetuj flag, ponovo verifikuj PIN, pa probaj sign još jednom. + if sign.get("status") != "ok" and "PIN" in (sign.get("message") or ""): + global _be_pin_verifikovan + _be_pin_verifikovan = False + _osiguraj_be_pin() + sign = be_command({ + "command": "sign", + "invoice_type": invoice_type, + "transaction_type": transaction_type, + "total_amount": total_amount, + }) + if sign.get("status") == "blocked": raise RuntimeError(f"Kartica blokirana: {sign.get('message')}") if sign.get("status") != "ok": - # npr. "error"/2101 (PIN nije verifikovan) — ne sme tiho da propadne u - # podrazumevane vrednosti counter=1 (v. BUG.md #34), jer bi to izdalo - # fiskalni broj koji se ponavlja iz računa u račun. raise RuntimeError(f"Kartica potpis nije uspeo: {sign.get('message', sign.get('status'))}") jid = cert.get("jid", ESIR_ID) diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 90ccc60..af30d73 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -324,6 +324,7 @@ func main() { r.With(modul("fiskalizacija"), ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "fiskal.pazar")).Get("/fiskal/pazar", h.FiskalniPazar) r.With(modul("fiskalizacija"), doz("fiskal.pazar")).Post("/fiskal/pazar/izvestaj", h.FiskalniIzvestaj) r.With(modul("fiskalizacija"), doz("fiskal.zakljucenje")).Post("/fiskal/pazar/zakljuci", h.ZakljuciFiskalniDan) + r.With(modul("fiskalizacija"), doz("prodaja.dodaj")).Post("/fiskal/batch-fiskalizuj", h.BatchFiskalizacija) r.With(pregledPodesavanja).Get("/admin/podesavanja/kalkulacija-pdv", h.PdvStope) r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/dodaj", h.DodajPdvStopu) r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/izmeni", h.IzmeniPdvStopu) diff --git a/internal/be/server.go b/internal/be/server.go index 8607676..6346407 100644 --- a/internal/be/server.go +++ b/internal/be/server.go @@ -13,7 +13,7 @@ import ( // maxKonekcija ograničava broj istovremenih TCP konekcija — bez ovoga bi klijent // koji samo otvori konekciju i ništa ne pošalje mogao da drži neograničen broj -// gorutina/file descriptor-a (v. BUG.md #36). +// gorutina/file descriptor-a. const maxKonekcija = 32 // citanjeTimeout je rok po redu (komandi) koji se osvežava pre svakog čitanja — diff --git a/internal/be/server_test.go b/internal/be/server_test.go index 8ec8eda..1784312 100644 --- a/internal/be/server_test.go +++ b/internal/be/server_test.go @@ -8,7 +8,7 @@ import ( "time" ) -// TestObradiKonekcijaZatvaraNakonTisine proverava ispravku BUG.md #36 — konekcija +// TestObradiKonekcijaZatvaraNakonTisine proverava — konekcija // koja ništa ne pošalje mora biti zatvorena posle citanjeTimeout-a, ne držana // zauvek. func TestObradiKonekcijaZatvaraNakonTisine(t *testing.T) { diff --git a/internal/config/setup.go b/internal/config/setup.go index 88a5ddf..e596aab 100644 --- a/internal/config/setup.go +++ b/internal/config/setup.go @@ -47,7 +47,7 @@ func nadjiLokalneAdrese() []string { // obradiSetupPotvrdi prima izabrani port sa setup stranice, upisuje ntech.env i // signalizira gotov kanalu da server može da se ugasi. Odbija nevažeći JSON i -// portove van dozvoljenog opsega (v. BUG.md #38 — ranije se greška dekodiranja +// portove van dozvoljenog opsega (ranije se greška dekodiranja // tiho gutala i upisivala port 0). func obradiSetupPotvrdi(envFajl string, gotov chan<- struct{}) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { diff --git a/internal/db/sqlite/izvestaj_test.go b/internal/db/sqlite/izvestaj_test.go index e9bdf29..f895a59 100644 --- a/internal/db/sqlite/izvestaj_test.go +++ b/internal/db/sqlite/izvestaj_test.go @@ -65,7 +65,7 @@ func TestIzvestajPraznaBaza(t *testing.T) { } } -// ─── BUG-01: stornirani prodajni nalozi ne smeju ući u prihod ─────────────── +// ─── stornirani prodajni nalozi ne smeju ući u prihod ──────────────────────── func TestPrihodTekuciMesec_IgnorisuSeStornirani(t *testing.T) { ctx := context.Background() @@ -160,7 +160,7 @@ func TestTopKlijenti_IgnorisuSeStornirani(t *testing.T) { } } -// ─── BUG-02/03: servis prihod koristi naplaceno (bruto, uključuje delove) ─── +// ─── servis prihod koristi naplaceno (bruto, uključuje delove) ──────────────── func TestPrihodTekuciMesec_ServisKoristiNaplaceno(t *testing.T) { ctx := context.Background() @@ -221,7 +221,7 @@ func TestMesecniPrihodServis_KoristiNaplaceno(t *testing.T) { } } -// ─── BUG-10: avans se uračunava u prihod (naplaceno + avans) ───────────────── +// ─── avans se uračunava u prihod (naplaceno + avans) ────────────────────────── func TestPrihodTekuciMesec_ServisSaAvansom(t *testing.T) { ctx := context.Background() @@ -280,7 +280,7 @@ func TestPrihodTekuciMesec_GarancijaNeUlazi(t *testing.T) { } } -// ─── BUG-08: TopKlijenti broji samo preuzete naloge sa naplatom/avansom ────── +// ─── TopKlijenti broji samo preuzete naloge sa naplatom/avansom ─────────────── func TestTopKlijenti_SamoPreuzetiNalozi(t *testing.T) { ctx := context.Background() @@ -343,7 +343,7 @@ func TestPrihodTekuciMesec_Kombinovani(t *testing.T) { } } -// ─── BUG-04: auto-izračun cena_konacna ne sme da uključuje delove ──────────── +// ─── auto-izračun cena_konacna ne sme da uključuje delove ───────────────────── // // Testiramo repo sloj direktno: simuliramo šta handler radi pri prelasku u // Preuzeto kad CenaKonacna == nil — upisujemo samo dijagnostiku+radove, pa @@ -381,7 +381,7 @@ func TestServisAutoIzracunCeneKonacne_BezDuplogDela(t *testing.T) { `INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno) VALUES (%d, %d, 1, 300.0, 0)`, nalogID, artID)) - // simuliramo ispravnu handler logiku (BUG-04 fix): + // simuliramo ispravnu handler logiku: // cena_konacna = dijagnostika + radovi (BEZ delova) cenaKonacna := 200.0 + 200.0 // dijagnostika + rad if err := servisRepo.AzurirajCenuKonacnu(ctx, nalogID, cenaKonacna); err != nil { diff --git a/internal/fiskal/fiskal_test.go b/internal/fiskal/fiskal_test.go index 59c58a7..5403067 100644 --- a/internal/fiskal/fiskal_test.go +++ b/internal/fiskal/fiskal_test.go @@ -89,7 +89,7 @@ func TestIzdajRacunKontekstIstekao(t *testing.T) { } func TestIzdajRacunPrezivljavaOtkazivanjeOriginalnogKonteksta(t *testing.T) { - // simulira #35 iz BUG.md: HTTP handler-ov r.Context() se otkaže (npr. klijent + // simulira: HTTP handler-ov r.Context() se otkaže (npr. klijent // zatvori konekciju), ali fiskalni poziv nastavlja preko odvojenog konteksta. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(30 * time.Millisecond) diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go index 3544ac1..5c4e097 100644 --- a/internal/handler/podesavanja.go +++ b/internal/handler/podesavanja.go @@ -287,6 +287,7 @@ type PodaciPodesavanja struct { PfrUrl string PfrTip string PfrKasir string + PfrApiKey string VerifyHost string BePin string BeLimit string @@ -689,6 +690,7 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) { "pfr_tip": r.FormValue("pfr_tip"), "pfr_kasir": r.FormValue("pfr_kasir"), "verify_host": r.FormValue("verify_host"), + "pfr_api_key": r.FormValue("pfr_api_key"), "be_pin": r.FormValue("be_pin"), "be_limit": r.FormValue("be_limit"), "be_enabled": r.FormValue("be_enabled"), @@ -1225,6 +1227,7 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac PfrUrl: vrednostIliDefault(podesavanja, "pfr_url", "http://127.0.0.1:4566"), PfrTip: vrednostIliDefault(podesavanja, "pfr_tip", "teron"), PfrKasir: podesavanja["pfr_kasir"], + PfrApiKey: podesavanja["pfr_api_key"], VerifyHost: podesavanja["verify_host"], BePin: vrednostIliDefault(podesavanja, "be_pin", "1234"), BeLimit: vrednostIliDefault(podesavanja, "be_limit", "500000"), diff --git a/internal/handler/prodaja.go b/internal/handler/prodaja.go index 7ac9c4b..892cc75 100644 --- a/internal/handler/prodaja.go +++ b/internal/handler/prodaja.go @@ -508,20 +508,18 @@ func (h *Handler) StampaFiskalnogProdaje(w http.ResponseWriter, r *http.Request) fmt.Fprint(w, `
`) fmt.Fprint(w, pre) - fmt.Fprint(w, ``) - if hasQR && fr.QRKod != "" { - fmt.Fprintf(w, `
`) fmt.Fprint(w, post) fmt.Fprint(w, ``) diff --git a/internal/handler/servis.go b/internal/handler/servis.go index c4cdcbc..200aa06 100644 --- a/internal/handler/servis.go +++ b/internal/handler/servis.go @@ -2980,10 +2980,11 @@ func (h *Handler) StampaFiskalnog(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `
`) fmt.Fprint(w, pre) - fmt.Fprint(w, ``) - if hasQR && fr.QRKod != "" { - fmt.Fprintf(w, `
`) fmt.Fprint(w, post) fmt.Fprint(w, ``) } diff --git a/internal/handler/uskladjivanje.go b/internal/handler/uskladjivanje.go index 893fc4c..2ce4121 100644 --- a/internal/handler/uskladjivanje.go +++ b/internal/handler/uskladjivanje.go @@ -2,9 +2,12 @@ package handler import ( "context" + "net/http" + "strconv" "time" appdb "ntech/internal/db" + "ntech/internal/middleware" "ntech/internal/model" ) @@ -174,12 +177,14 @@ func (h *Handler) kirKandidatiServisa(ctx context.Context) ([]model.ServisniNalo // (double-submit) pa ih automatski backfill preskače. func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnjigovodstva, error) { var p model.PrazninaKnjigovodstva + var kandidatiP []kandidatUskladjivanja if h.modulUkljucen(ctx, "pdv") { - _, postojiMapP, kandidatiP, err := h.kirKandidatiProdaje(ctx) + _, postojiMapP, kp, err := h.kirKandidatiProdaje(ctx) if err != nil { return p, err } + kandidatiP = kp _, postojiMapS, kandidatiS, err := h.kirKandidatiServisa(ctx) if err != nil { return p, err @@ -211,10 +216,11 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji } if h.modulUkljucen(ctx, "kpo") { - _, postojiMapP, kandidatiP, err := h.kpoKandidatiProdaje(ctx) + _, postojiMapP, kp, err := h.kpoKandidatiProdaje(ctx) if err != nil { return p, err } + kandidatiP = kp _, postojiMapS, kandidatiS, _, err := h.kpoKandidatiServisa(ctx) if err != nil { return p, err @@ -258,6 +264,17 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji } p.BezFiskalnogServis = len(p.BezFiskalnogServisID) } + + // sumnjivi duplikati za fiskal — isti kandidati kao za KPO/PDV, + // ako su već učitani; ako ne, učitavamo iz prodaje (samo brojimo, + // preskaču se u automatskoj batch fiskalizaciji) + if len(kandidatiP) > 0 { + dupli := sumnjiviDuplikati(kandidatiP) + for id := range dupli { + p.SumnjiviFiskalID = append(p.SumnjiviFiskalID, id) + } + } + if idsP, idsS, err := h.stornoBezRefunda(ctx); err == nil { p.BezRefundaProdajaID = idsP p.BezRefundaServisID = idsS @@ -330,3 +347,36 @@ func istKlijent(a, b *int64) bool { } return *a == *b } + +// BatchFiskalizacija pokreće fiskalizaciju svih prodaja i servisa koji nemaju +// fiskalni račun. Best-effort: ako neka fiskalizacija padne, nastavlja se. +func (h *Handler) BatchFiskalizacija(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + klijent := h.fiskalKlijent(ctx) + if klijent == nil { + middleware.SetFlash(w, r, h.DB, "greska", "Fiskalni servis nije dostupan. Proverite vezu sa ESIR/PFR.") + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) + return + } + + izdato := 0 + if m, err := h.FiskalRepo.ProdajeBezFiskalnog(ctx); err == nil { + for id := range m { + h.fiskalizujProdaju(ctx, id, klijent, 0) + izdato++ + } + } + if m, err := h.FiskalRepo.ServisiBezFiskalnog(ctx); err == nil { + for id := range m { + h.fiskalizujServis(ctx, id, klijent, "Gotovina", 0, 0) + izdato++ + } + } + + if izdato == 0 { + middleware.SetFlash(w, r, h.DB, "uspeh", "Svi nalozi već imaju fiskalni račun.") + } else { + middleware.SetFlash(w, r, h.DB, "uspeh", "Pokrenuta fiskalizacija za "+strconv.Itoa(izdato)+" naloga.") + } + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) +} diff --git a/internal/handler/utils.go b/internal/handler/utils.go index e341a4f..5b61662 100644 --- a/internal/handler/utils.go +++ b/internal/handler/utils.go @@ -19,7 +19,7 @@ func parseID(s string) (int64, error) { // znaka, a inače "" (1-2 znaka se ignorišu). Ranije je ovo bio klijentski // hx-trigger uslov (keyup[length==0||length>=3]) — uklonjen jer ga htmx // evaluira preko eval()/new Function(), što CSP bez 'unsafe-eval' blokira -// (v. BUG.md #41). Ovde se ista optimizacija radi na serveru, bez eval-a. +// Ovde se ista optimizacija radi na serveru, bez eval-a. func ignorisiKratkuPretragu(pretraga string) string { if l := len([]rune(strings.TrimSpace(pretraga))); l > 0 && l < 3 { return "" diff --git a/internal/model/stranica.go b/internal/model/stranica.go index 54d07d2..ce63615 100644 --- a/internal/model/stranica.go +++ b/internal/model/stranica.go @@ -95,6 +95,7 @@ type PrazninaKnjigovodstva struct { BezRefundaProdajaID []int64 // ID-jevi stornirane prodaje bez refunda — za direktan link sa dashboard-a BezRefundaServisID []int64 // ID-jevi storniranih servisa bez refunda — za direktan link sa dashboard-a SumnjiviDupli int + SumnjiviFiskalID []int64 // ID-jevi sumnjivih duplikata u fiskalizaciji — preskaču se u automatskoj batch fiskalizaciji } // BezKir vraća ukupan broj naloga (prodaja + servis) bez KIR upisa. diff --git a/web/templates/stranice/dashboard.html b/web/templates/stranice/dashboard.html index 9a27368..20938d7 100644 --- a/web/templates/stranice/dashboard.html +++ b/web/templates/stranice/dashboard.html @@ -51,7 +51,19 @@
mock-api-key-0000.