PFR API ključ, fiskalni račun formatiranje, popravke
- PFR API ključ (pfr_api_key) u podešavanjima i fiskalizaciji - Formatiranje fiskalnog računa: veličina fonta, QR kod, layout - PFR mock: QR box_size/border, automatski PIN retry posle BE restarta - Servis: animirane kanban kartice, formatiranje računa - Dashboard i testovi: sitne ispravke
This commit is contained in:
@@ -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 —
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -508,20 +508,18 @@ func (h *Handler) StampaFiskalnogProdaje(w http.ResponseWriter, r *http.Request)
|
||||
fmt.Fprint(w, `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Fiskalni račun</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;}
|
||||
body{font-family:monospace;font-size:12px;padding:20px;max-width:max-content;margin:0 auto;}
|
||||
pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;display:block;}
|
||||
@media print{body{font-size:11px;padding:10px;}}
|
||||
body{font-family:monospace;font-size:10px;padding:20px;max-width:max-content;margin:0 auto;}
|
||||
pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;}
|
||||
pre img{width:53.5mm;height:auto;}
|
||||
@media screen{pre img{width:289px;}}
|
||||
@media print{body{font-size:7px;padding:10px;}}
|
||||
</style></head><body>`)
|
||||
|
||||
fmt.Fprint(w, `<pre>`)
|
||||
fmt.Fprint(w, pre)
|
||||
fmt.Fprint(w, `</pre>`)
|
||||
|
||||
if hasQR && fr.QRKod != "" {
|
||||
fmt.Fprintf(w, `<div style="margin:10px 0;"><img src="data:image/png;base64,%s" style="display:block;margin:0 auto;width:72mm;height:72mm;"></div>`, fr.QRKod)
|
||||
fmt.Fprintf(w, `<img src="data:image/png;base64,%s">`, fr.QRKod)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `<pre>`)
|
||||
fmt.Fprint(w, post)
|
||||
fmt.Fprint(w, `</pre>`)
|
||||
|
||||
|
||||
@@ -2980,10 +2980,11 @@ func (h *Handler) StampaFiskalnog(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Fiskalni račun</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;}
|
||||
body{font-family:monospace;font-size:12px;padding:20px;max-width:max-content;margin:0 auto;}
|
||||
pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;display:block;}
|
||||
.ntech-razdvajac{margin:24px 0;border-top:2px dashed #999;padding-top:24px;}
|
||||
@media print{body{font-size:11px;padding:10px;}.ntech-razdvajac{page-break-before:always;}}
|
||||
body{font-family:monospace;font-size:10px;padding:20px;max-width:max-content;margin:0 auto;}
|
||||
pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;}
|
||||
pre img{width:53.5mm;height:auto;}
|
||||
@media screen{pre img{width:289px;}}
|
||||
@media print{body{font-size:7px;padding:10px;}.ntech-razdvajac{page-break-before:always;}}
|
||||
</style></head><body>`)
|
||||
|
||||
const qrPlaceholder = "{{{{QR-KOD}}}}"
|
||||
@@ -3007,13 +3008,9 @@ pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;dis
|
||||
|
||||
fmt.Fprint(w, `<pre>`)
|
||||
fmt.Fprint(w, pre)
|
||||
fmt.Fprint(w, `</pre>`)
|
||||
|
||||
if hasQR && fr.QRKod != "" {
|
||||
fmt.Fprintf(w, `<div style="margin:10px 0;"><img src="data:image/png;base64,%s" style="display:block;margin:0 auto;width:72mm;height:72mm;"></div>`, fr.QRKod)
|
||||
fmt.Fprintf(w, `<img src="data:image/png;base64,%s">`, fr.QRKod)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `<pre>`)
|
||||
fmt.Fprint(w, post)
|
||||
fmt.Fprint(w, `</pre>`)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user