Knjigovodstvo i fiskalizacija: dashboard upozorenje za praznine, KPO dozvole, pojednostavljen storno, ispravke verifikacije i kasira

- Dashboard kartica prikazuje naloge bez KIR/KPO/fiskalnog upisa i sumnjive duplikate (double-submit), sa zaštitom od automatskog dupliranja prihoda pri bulk dopuni
- Registrovane nedostajuće KPO dozvole (kpo.pregled/dodaj/obrisi) — modul je bio potpuno nedostupan
- Uklonjen suvišan/nefunkcionalan 'Ukloni nalog' (duplirao Storno bez stvarne razlike); Storno u listi prodaje sad vodi direktno na formu
- Kasir na fiskalnom računu koristi ime prijavljenog korisnika umesto fiksnog podešavanja
- Fiskalni računi za nalog uvek prikazuju najnoviji zapis (original vs. refund posle storna)
- Verifikaciona stranica (/v/) jasno označena kao lokalna test simulacija, ne prava provera kod PURS-a; dodat prikaz kusura
- Mock brojač fiskalnih računa (kartica emulator) sad preživljava restart servera
This commit is contained in:
2026-07-01 23:01:26 +02:00
parent fcae2ae441
commit 3a0f98622b
20 changed files with 417 additions and 124 deletions
+2 -45
View File
@@ -27,7 +27,6 @@ type PodaciProdaje struct {
model.PodaciStranice
Nalozi []model.ProdajniNalogSaDetaljem
Sacuvano bool
Obrisan bool
Pretraga string
Od string
Do string
@@ -189,7 +188,6 @@ func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) {
PodaciStranice: ps,
Nalozi: nalozi,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
Obrisan: r.URL.Query().Get("obrisan") == "1",
Pretraga: pretraga,
Od: od,
Do: do,
@@ -533,39 +531,6 @@ pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;dis
fmt.Fprint(w, `<p style="margin-top:16px;"><button onclick="window.print()" style="padding:8px 16px;">Štampaj</button></p></body></html>`)
}
// ObrisiProdaju prima POST zahtev i stornira nalog umesto fizičkog brisanja.
// Jednom izdat račun ne sme da nestane — umesto brisanja koristi se storno.
func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) {
k, ok := h.zahtevajDozvolu(w, r, "prodaja.obrisi")
if !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
poreskiBrojKupca := strings.TrimSpace(r.FormValue("poreski_broj_kupca"))
if err := h.stornirajProdaju(r.Context(), id, "administrativno uklanjanje", &k.ID, poreskiBrojKupca); err != nil {
poruka := "Greška pri uklanjanju. Možda je nalog već storniran."
if errors.Is(err, errPotrebnaIdentKupca) {
poruka = errPotrebnaIdentKupca.Error()
} else {
slog.Error("greška pri uklanjanju naloga", "error", err)
}
middleware.SetFlash(w, r, h.DB, "greska", poruka)
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/prodaja?obrisan=1", http.StatusSeeOther)
}
// parseFormuProdaje čita zaglavlje i stavke iz HTTP forme i vraća model i eventualnu grešku
func parseFormuProdaje(r *http.Request) (model.ProdajniNalog, []model.StavkaProdaje, string) {
var nalog model.ProdajniNalog
@@ -701,8 +666,6 @@ func klasifikujPoreskiBroj(broj string) (pib, jmbg string) {
// stornirajProdaju sprovodi kompletan storno prodajnog naloga: menja status naloga i
// vraća artikle na stanje, uklanja vezani auto-KIR zapis, šalje fiskalni refund
// (best-effort — storno ostaje validan i bez uspešnog refunda) i briše KPO zapis.
// Zajednička je za StornoProdaje i ObrisiProdaju kako obe akcije imaju isti,
// potpuni efekat prema PDV/fiskalnoj i KPO evidenciji.
func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string, korisnikID *int64, poreskiBrojKupca string) error {
// Refundacija po propisu zahteva identifikaciju kupca (PIB/JMBG) — ako je nalog
// bez klijenta (ili klijent nema upisan PIB/JMBG), mora se ručno uneti pre storna.
@@ -735,10 +698,7 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string,
if fk := h.fiskalKlijent(); fk != nil {
if nalogFisk, e := h.ProdajaRepo.DohvatiID(ctx, id); e == nil {
if stavkeFisk, e2 := h.ProdajaRepo.DohvatiStavke(ctx, id); e2 == nil {
kasirFisk, _ := sqlite.DohvatiPodesavanje(ctx, h.DB, "pfr_kasir")
if kasirFisk == "" {
kasirFisk = "NTech"
}
kasirFisk := h.imeKasira(ctx)
var pibFisk, jmbgFisk string
if nalogFisk.KlijentID != nil {
if kk, e3 := h.KlijentiRepo.DohvatiID(ctx, *nalogFisk.KlijentID); e3 == nil {
@@ -810,10 +770,7 @@ func (h *Handler) fiskalizujProdaju(ctx context.Context, prodajaID int64, klijen
return
}
kasir, _ := sqlite.DohvatiPodesavanje(ctx, h.DB, "pfr_kasir")
if kasir == "" {
kasir = "NTech"
}
kasir := h.imeKasira(ctx)
var pib, jmbg string
if nalog.KlijentID != nil {