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:
+17
-4
@@ -227,7 +227,10 @@ def log(msg):
|
|||||||
# ── QR ──────────────────────────────────────────────────────
|
# ── QR ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
def generate_qr(url):
|
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()
|
buf = io.BytesIO()
|
||||||
img.save(buf, format="PNG")
|
img.save(buf, format="PNG")
|
||||||
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||||
@@ -366,12 +369,22 @@ def _build_invoice_response(req, request_id):
|
|||||||
"total_amount": total_amount,
|
"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":
|
if sign.get("status") == "blocked":
|
||||||
raise RuntimeError(f"Kartica blokirana: {sign.get('message')}")
|
raise RuntimeError(f"Kartica blokirana: {sign.get('message')}")
|
||||||
if sign.get("status") != "ok":
|
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'))}")
|
raise RuntimeError(f"Kartica potpis nije uspeo: {sign.get('message', sign.get('status'))}")
|
||||||
|
|
||||||
jid = cert.get("jid", ESIR_ID)
|
jid = cert.get("jid", ESIR_ID)
|
||||||
|
|||||||
@@ -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"), 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.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("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(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/dodaj", h.DodajPdvStopu)
|
||||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/izmeni", h.IzmeniPdvStopu)
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/izmeni", h.IzmeniPdvStopu)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
// maxKonekcija ograničava broj istovremenih TCP konekcija — bez ovoga bi klijent
|
// 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
|
// 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
|
const maxKonekcija = 32
|
||||||
|
|
||||||
// citanjeTimeout je rok po redu (komandi) koji se osvežava pre svakog čitanja —
|
// citanjeTimeout je rok po redu (komandi) koji se osvežava pre svakog čitanja —
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"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
|
// koja ništa ne pošalje mora biti zatvorena posle citanjeTimeout-a, ne držana
|
||||||
// zauvek.
|
// zauvek.
|
||||||
func TestObradiKonekcijaZatvaraNakonTisine(t *testing.T) {
|
func TestObradiKonekcijaZatvaraNakonTisine(t *testing.T) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func nadjiLokalneAdrese() []string {
|
|||||||
|
|
||||||
// obradiSetupPotvrdi prima izabrani port sa setup stranice, upisuje ntech.env i
|
// 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
|
// 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).
|
// tiho gutala i upisivala port 0).
|
||||||
func obradiSetupPotvrdi(envFajl string, gotov chan<- struct{}) http.HandlerFunc {
|
func obradiSetupPotvrdi(envFajl string, gotov chan<- struct{}) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
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) {
|
func TestPrihodTekuciMesec_IgnorisuSeStornirani(t *testing.T) {
|
||||||
ctx := context.Background()
|
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) {
|
func TestPrihodTekuciMesec_ServisKoristiNaplaceno(t *testing.T) {
|
||||||
ctx := context.Background()
|
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) {
|
func TestPrihodTekuciMesec_ServisSaAvansom(t *testing.T) {
|
||||||
ctx := context.Background()
|
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) {
|
func TestTopKlijenti_SamoPreuzetiNalozi(t *testing.T) {
|
||||||
ctx := context.Background()
|
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
|
// Testiramo repo sloj direktno: simuliramo šta handler radi pri prelasku u
|
||||||
// Preuzeto kad CenaKonacna == nil — upisujemo samo dijagnostiku+radove, pa
|
// 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)
|
`INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno)
|
||||||
VALUES (%d, %d, 1, 300.0, 0)`, nalogID, artID))
|
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)
|
// cena_konacna = dijagnostika + radovi (BEZ delova)
|
||||||
cenaKonacna := 200.0 + 200.0 // dijagnostika + rad
|
cenaKonacna := 200.0 + 200.0 // dijagnostika + rad
|
||||||
if err := servisRepo.AzurirajCenuKonacnu(ctx, nalogID, cenaKonacna); err != nil {
|
if err := servisRepo.AzurirajCenuKonacnu(ctx, nalogID, cenaKonacna); err != nil {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ func TestIzdajRacunKontekstIstekao(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIzdajRacunPrezivljavaOtkazivanjeOriginalnogKonteksta(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.
|
// zatvori konekciju), ali fiskalni poziv nastavlja preko odvojenog konteksta.
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
time.Sleep(30 * time.Millisecond)
|
time.Sleep(30 * time.Millisecond)
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ type PodaciPodesavanja struct {
|
|||||||
PfrUrl string
|
PfrUrl string
|
||||||
PfrTip string
|
PfrTip string
|
||||||
PfrKasir string
|
PfrKasir string
|
||||||
|
PfrApiKey string
|
||||||
VerifyHost string
|
VerifyHost string
|
||||||
BePin string
|
BePin string
|
||||||
BeLimit string
|
BeLimit string
|
||||||
@@ -689,6 +690,7 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
|||||||
"pfr_tip": r.FormValue("pfr_tip"),
|
"pfr_tip": r.FormValue("pfr_tip"),
|
||||||
"pfr_kasir": r.FormValue("pfr_kasir"),
|
"pfr_kasir": r.FormValue("pfr_kasir"),
|
||||||
"verify_host": r.FormValue("verify_host"),
|
"verify_host": r.FormValue("verify_host"),
|
||||||
|
"pfr_api_key": r.FormValue("pfr_api_key"),
|
||||||
"be_pin": r.FormValue("be_pin"),
|
"be_pin": r.FormValue("be_pin"),
|
||||||
"be_limit": r.FormValue("be_limit"),
|
"be_limit": r.FormValue("be_limit"),
|
||||||
"be_enabled": r.FormValue("be_enabled"),
|
"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"),
|
PfrUrl: vrednostIliDefault(podesavanja, "pfr_url", "http://127.0.0.1:4566"),
|
||||||
PfrTip: vrednostIliDefault(podesavanja, "pfr_tip", "teron"),
|
PfrTip: vrednostIliDefault(podesavanja, "pfr_tip", "teron"),
|
||||||
PfrKasir: podesavanja["pfr_kasir"],
|
PfrKasir: podesavanja["pfr_kasir"],
|
||||||
|
PfrApiKey: podesavanja["pfr_api_key"],
|
||||||
VerifyHost: podesavanja["verify_host"],
|
VerifyHost: podesavanja["verify_host"],
|
||||||
BePin: vrednostIliDefault(podesavanja, "be_pin", "1234"),
|
BePin: vrednostIliDefault(podesavanja, "be_pin", "1234"),
|
||||||
BeLimit: vrednostIliDefault(podesavanja, "be_limit", "500000"),
|
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>
|
fmt.Fprint(w, `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Fiskalni račun</title>
|
||||||
<style>
|
<style>
|
||||||
*{box-sizing:border-box;}
|
*{box-sizing:border-box;}
|
||||||
body{font-family:monospace;font-size:12px;padding:20px;max-width:max-content;margin:0 auto;}
|
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;display:block;}
|
pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;}
|
||||||
@media print{body{font-size:11px;padding:10px;}}
|
pre img{width:53.5mm;height:auto;}
|
||||||
|
@media screen{pre img{width:289px;}}
|
||||||
|
@media print{body{font-size:7px;padding:10px;}}
|
||||||
</style></head><body>`)
|
</style></head><body>`)
|
||||||
|
|
||||||
fmt.Fprint(w, `<pre>`)
|
fmt.Fprint(w, `<pre>`)
|
||||||
fmt.Fprint(w, pre)
|
fmt.Fprint(w, pre)
|
||||||
fmt.Fprint(w, `</pre>`)
|
|
||||||
|
|
||||||
if hasQR && fr.QRKod != "" {
|
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, post)
|
||||||
fmt.Fprint(w, `</pre>`)
|
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>
|
fmt.Fprint(w, `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Fiskalni račun</title>
|
||||||
<style>
|
<style>
|
||||||
*{box-sizing:border-box;}
|
*{box-sizing:border-box;}
|
||||||
body{font-family:monospace;font-size:12px;padding:20px;max-width:max-content;margin:0 auto;}
|
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;display:block;}
|
pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;}
|
||||||
.ntech-razdvajac{margin:24px 0;border-top:2px dashed #999;padding-top:24px;}
|
pre img{width:53.5mm;height:auto;}
|
||||||
@media print{body{font-size:11px;padding:10px;}.ntech-razdvajac{page-break-before:always;}}
|
@media screen{pre img{width:289px;}}
|
||||||
|
@media print{body{font-size:7px;padding:10px;}.ntech-razdvajac{page-break-before:always;}}
|
||||||
</style></head><body>`)
|
</style></head><body>`)
|
||||||
|
|
||||||
const qrPlaceholder = "{{{{QR-KOD}}}}"
|
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)
|
fmt.Fprint(w, pre)
|
||||||
fmt.Fprint(w, `</pre>`)
|
|
||||||
|
|
||||||
if hasQR && fr.QRKod != "" {
|
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, post)
|
||||||
fmt.Fprint(w, `</pre>`)
|
fmt.Fprint(w, `</pre>`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
appdb "ntech/internal/db"
|
appdb "ntech/internal/db"
|
||||||
|
"ntech/internal/middleware"
|
||||||
"ntech/internal/model"
|
"ntech/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -174,12 +177,14 @@ func (h *Handler) kirKandidatiServisa(ctx context.Context) ([]model.ServisniNalo
|
|||||||
// (double-submit) pa ih automatski backfill preskače.
|
// (double-submit) pa ih automatski backfill preskače.
|
||||||
func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnjigovodstva, error) {
|
func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnjigovodstva, error) {
|
||||||
var p model.PrazninaKnjigovodstva
|
var p model.PrazninaKnjigovodstva
|
||||||
|
var kandidatiP []kandidatUskladjivanja
|
||||||
|
|
||||||
if h.modulUkljucen(ctx, "pdv") {
|
if h.modulUkljucen(ctx, "pdv") {
|
||||||
_, postojiMapP, kandidatiP, err := h.kirKandidatiProdaje(ctx)
|
_, postojiMapP, kp, err := h.kirKandidatiProdaje(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
kandidatiP = kp
|
||||||
_, postojiMapS, kandidatiS, err := h.kirKandidatiServisa(ctx)
|
_, postojiMapS, kandidatiS, err := h.kirKandidatiServisa(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, err
|
return p, err
|
||||||
@@ -211,10 +216,11 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji
|
|||||||
}
|
}
|
||||||
|
|
||||||
if h.modulUkljucen(ctx, "kpo") {
|
if h.modulUkljucen(ctx, "kpo") {
|
||||||
_, postojiMapP, kandidatiP, err := h.kpoKandidatiProdaje(ctx)
|
_, postojiMapP, kp, err := h.kpoKandidatiProdaje(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
kandidatiP = kp
|
||||||
_, postojiMapS, kandidatiS, _, err := h.kpoKandidatiServisa(ctx)
|
_, postojiMapS, kandidatiS, _, err := h.kpoKandidatiServisa(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, err
|
return p, err
|
||||||
@@ -258,6 +264,17 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji
|
|||||||
}
|
}
|
||||||
p.BezFiskalnogServis = len(p.BezFiskalnogServisID)
|
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 {
|
if idsP, idsS, err := h.stornoBezRefunda(ctx); err == nil {
|
||||||
p.BezRefundaProdajaID = idsP
|
p.BezRefundaProdajaID = idsP
|
||||||
p.BezRefundaServisID = idsS
|
p.BezRefundaServisID = idsS
|
||||||
@@ -330,3 +347,36 @@ func istKlijent(a, b *int64) bool {
|
|||||||
}
|
}
|
||||||
return *a == *b
|
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
|
// 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
|
// hx-trigger uslov (keyup[length==0||length>=3]) — uklonjen jer ga htmx
|
||||||
// evaluira preko eval()/new Function(), što CSP bez 'unsafe-eval' blokira
|
// 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 {
|
func ignorisiKratkuPretragu(pretraga string) string {
|
||||||
if l := len([]rune(strings.TrimSpace(pretraga))); l > 0 && l < 3 {
|
if l := len([]rune(strings.TrimSpace(pretraga))); l > 0 && l < 3 {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ type PrazninaKnjigovodstva struct {
|
|||||||
BezRefundaProdajaID []int64 // ID-jevi stornirane prodaje bez refunda — za direktan link sa dashboard-a
|
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
|
BezRefundaServisID []int64 // ID-jevi storniranih servisa bez refunda — za direktan link sa dashboard-a
|
||||||
SumnjiviDupli int
|
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.
|
// BezKir vraća ukupan broj naloga (prodaja + servis) bez KIR upisa.
|
||||||
|
|||||||
@@ -51,7 +51,19 @@
|
|||||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||||
{{ if gt .PrazninaKnjige.BezKir 0 }}<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/pdv/kir" class="btn-sekundarno" style="font-size:13px;">KIR — pokreni usklađivanje</a>{{ end }}
|
{{ if gt .PrazninaKnjige.BezKir 0 }}<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/pdv/kir" class="btn-sekundarno" style="font-size:13px;">KIR — pokreni usklađivanje</a>{{ end }}
|
||||||
{{ if gt .PrazninaKnjige.BezKpo 0 }}<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/kpo" class="btn-sekundarno" style="font-size:13px;">KPO — pokreni usklađivanje</a>{{ end }}
|
{{ if gt .PrazninaKnjige.BezKpo 0 }}<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/kpo" class="btn-sekundarno" style="font-size:13px;">KPO — pokreni usklađivanje</a>{{ end }}
|
||||||
</div>
|
{{ if or (gt .PrazninaKnjige.BezFiskalnogProdaja 0) (gt .PrazninaKnjige.BezFiskalnogServis 0) }}
|
||||||
|
<form method="POST" action="/fiskal/batch-fiskalizuj" style="margin:0;">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ $.CsrfToken }}">
|
||||||
|
<button type="submit" class="btn-primarno" style="font-size:13px;">Fiskalizuj sve</button>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
{{ if and .PrazninaKnjige.SumnjiviFiskalID (gt (len .PrazninaKnjige.SumnjiviFiskalID) 0) }}
|
||||||
|
<div style="margin-top:4px;font-size:12px;opacity:0.7;">
|
||||||
|
⚠ Sumnjivi duplikati (preskaču se):
|
||||||
|
{{ range $i, $id := .PrazninaKnjige.SumnjiviFiskalID }}{{ if $i }}, {{ end }}<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja/{{ $id }}" style="color:#b45309;text-decoration:line-through;">Prodaja #{{ $id }}</a>{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -54,6 +54,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="pfr_api_key" class="polje-labela">API ključ</label>
|
||||||
|
<input type="text" id="pfr_api_key" name="pfr_api_key" value="{{.PfrApiKey}}"
|
||||||
|
placeholder="mock-api-key-0000"
|
||||||
|
style="width:100%;padding:8px 12px;background:var(--pozadina);border:0.5px solid var(--ivica);border-radius:8px;color:var(--tekst-glavni);font-size:14px;box-sizing:border-box;font-family:monospace;">
|
||||||
|
<div class="pomocni-tekst" style="margin-top:4px;">Bearer token koji NTech šalje PFR serveru. Mock podrazumevano: <code>mock-api-key-0000</code>.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
style="padding:9px 18px;background:var(--sb-akcent);border:none;border-radius:8px;color:#fff;font-size:13px;font-weight:500;cursor:pointer;">
|
style="padding:9px 18px;background:var(--sb-akcent);border:none;border-radius:8px;color:#fff;font-size:13px;font-weight:500;cursor:pointer;">
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<div class="kanban-kartice">
|
<div class="kanban-kartice">
|
||||||
{{range $.Nalozi}}
|
{{range $.Nalozi}}
|
||||||
{{if eq .Status $status}}
|
{{if eq .Status $status}}
|
||||||
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/servis/{{.ID}}" class="kartica kanban-karta{{if .ImaPredlog}} kanban-ceka-odobrenje{{end}}">
|
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/servis/{{.ID}}" class="kartica kanban-karta animiraj{{if .ImaPredlog}} kanban-ceka-odobrenje{{end}}">
|
||||||
<div style="display:flex;align-items:center;gap:6px;">
|
<div style="display:flex;align-items:center;gap:6px;">
|
||||||
{{if .ImaPredlog}}<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#f59e0b;flex-shrink:0;" title="Čeka odobrenje klijenta"></span>{{end}}
|
{{if .ImaPredlog}}<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#f59e0b;flex-shrink:0;" title="Čeka odobrenje klijenta"></span>{{end}}
|
||||||
<span style="font-size:14px;font-weight:500;color:var(--sb-akcent);font-family:monospace;">{{.BrojNaloga}}</span>
|
<span style="font-size:14px;font-weight:500;color:var(--sb-akcent);font-family:monospace;">{{.BrojNaloga}}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user