Prodaja: Obriši stornira potpuno kao Storno (KIR, fiskalni refund, KPO)

ObrisiProdaju je brisao samo auto-KIR zapis, ali ne i fiskalni refund i KPO
zapis, za razliku od StornoProdaje koji radi sve troje. Izdvojena zajednička
stornirajProdaju funkcija koju sada koriste oba handlera. Dozvola ujednačena
na prodaja.obrisi (u skladu sa rutom u main.go) — ranije se interno
proveravala prodaja.storno, što je bilo nekonzistentno sa ruterom.
This commit is contained in:
2026-07-01 12:34:46 +02:00
parent f94f6a560e
commit e551e5f09a
+35 -22
View File
@@ -399,22 +399,21 @@ func (h *Handler) StampaProdaje(w http.ResponseWriter, r *http.Request) {
// ObrisiProdaju prima POST zahtev i stornira nalog umesto fizičkog brisanja. // ObrisiProdaju prima POST zahtev i stornira nalog umesto fizičkog brisanja.
// Jednom izdat račun ne sme da nestane — umesto brisanja koristi se storno. // Jednom izdat račun ne sme da nestane — umesto brisanja koristi se storno.
func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "prodaja.storno"); !ok { k, ok := h.zahtevajDozvolu(w, r, "prodaja.obrisi")
if !ok {
return return
} }
k := middleware.KorisnikIzKonteksta(r.Context())
id, err := parseID(chi.URLParam(r, "id")) id, err := parseID(chi.URLParam(r, "id"))
if err != nil { if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return return
} }
if err := h.ProdajaRepo.Storno(r.Context(), id, "administrativno uklanjanje", &k.ID); err != nil { if err := h.stornirajProdaju(r.Context(), id, "administrativno uklanjanje", &k.ID); err != nil {
slog.Error("greška pri uklanjanju naloga", "error", err) slog.Error("greška pri uklanjanju naloga", "error", err)
} middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju. Možda je nalog već storniran.")
// ukloni vezani auto-KIR zapis (ako ga je ova prodaja kreirala) http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
if err := h.PdvKirRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); err != nil { return
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
} }
http.Redirect(w, r, "/prodaja?obrisan=1", http.StatusSeeOther) http.Redirect(w, r, "/prodaja?obrisan=1", http.StatusSeeOther)
@@ -506,35 +505,49 @@ func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
return return
} }
razlog := strings.TrimSpace(r.FormValue("razlog")) razlog := strings.TrimSpace(r.FormValue("razlog"))
if err := h.ProdajaRepo.Storno(r.Context(), id, razlog, &k.ID); err != nil { if err := h.stornirajProdaju(r.Context(), id, razlog, &k.ID); err != nil {
slog.Error("greška pri storniranju naloga", "error", err) slog.Error("greška pri storniranju naloga", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri storniranju. Možda je nalog već storniran.") middleware.SetFlash(w, r, h.DB, "greska", "Greška pri storniranju. Možda je nalog već storniran.")
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther) http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
return return
} }
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
}
// 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) error {
if err := h.ProdajaRepo.Storno(ctx, id, razlog, korisnikID); err != nil {
return err
}
// stornirana prodaja ne ulazi u PDV — ukloni vezani auto-KIR zapis // stornirana prodaja ne ulazi u PDV — ukloni vezani auto-KIR zapis
if err := h.PdvKirRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); err != nil { if err := h.PdvKirRepo.ObrisiPoIzvoru(ctx, "prodaja", id); err != nil {
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err) slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
} }
// fiskalni refund — best-effort (storno ostaje validan i bez uspešnog refunda) // fiskalni refund — best-effort
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) { if h.modulUkljucen(ctx, config.ModulFiskalizacija) {
if fr, _ := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id); fr != nil && !fr.Storniran { if fr, _ := h.FiskalRepo.DohvatiPoProdaji(ctx, id); fr != nil && !fr.Storniran {
if fk := h.fiskalKlijent(); fk != nil { if fk := h.fiskalKlijent(); fk != nil {
if nalogFisk, e := h.ProdajaRepo.DohvatiID(r.Context(), id); e == nil { if nalogFisk, e := h.ProdajaRepo.DohvatiID(ctx, id); e == nil {
if stavkeFisk, e2 := h.ProdajaRepo.DohvatiStavke(r.Context(), id); e2 == nil { if stavkeFisk, e2 := h.ProdajaRepo.DohvatiStavke(ctx, id); e2 == nil {
kasirFisk, _ := sqlite.DohvatiPodesavanje(r.Context(), h.DB, "pfr_kasir") kasirFisk, _ := sqlite.DohvatiPodesavanje(ctx, h.DB, "pfr_kasir")
if kasirFisk == "" { if kasirFisk == "" {
kasirFisk = "NTech" kasirFisk = "NTech"
} }
var pibFisk, jmbgFisk string var pibFisk, jmbgFisk string
if nalogFisk.KlijentID != nil { if nalogFisk.KlijentID != nil {
if kk, e3 := h.KlijentiRepo.DohvatiID(r.Context(), *nalogFisk.KlijentID); e3 == nil { if kk, e3 := h.KlijentiRepo.DohvatiID(ctx, *nalogFisk.KlijentID); e3 == nil {
pibFisk, jmbgFisk = kk.PIB, kk.JMBG pibFisk, jmbgFisk = kk.PIB, kk.JMBG
} }
} }
zahtevFisk := fiskal.NapraviRefundZahtev(*nalogFisk, stavkeFisk, pibFisk, jmbgFisk, kasirFisk, fr.PfrBroj) zahtevFisk := fiskal.NapraviRefundZahtev(*nalogFisk, stavkeFisk, pibFisk, jmbgFisk, kasirFisk, fr.PfrBroj)
if odgFisk, errFisk := fk.IzdajRacun(r.Context(), zahtevFisk); errFisk != nil { if odgFisk, errFisk := fk.IzdajRacun(ctx, zahtevFisk); errFisk != nil {
slog.Error("fiskalni refund nije uspeo", "prodaja_id", id, "error", errFisk) slog.Error("fiskalni refund nije uspeo", "prodaja_id", id, "error", errFisk)
} else { } else {
poreskeJSON, _ := json.Marshal(odgFisk.TaxItems) poreskeJSON, _ := json.Marshal(odgFisk.TaxItems)
@@ -557,10 +570,10 @@ func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
Zatrazio: odgFisk.RequestedBy, Zatrazio: odgFisk.RequestedBy,
Poruka: odgFisk.Messages, Poruka: odgFisk.Messages,
} }
if _, errFisk = h.FiskalRepo.Kreiraj(r.Context(), refundFr); errFisk != nil { if _, errFisk = h.FiskalRepo.Kreiraj(ctx, refundFr); errFisk != nil {
slog.Error("čuvanje fiskalnog refunda nije uspelo", "prodaja_id", id, "error", errFisk) slog.Error("čuvanje fiskalnog refunda nije uspelo", "prodaja_id", id, "error", errFisk)
} else { } else {
_ = h.FiskalRepo.OznačiKaoStorniran(r.Context(), fr.ID) _ = h.FiskalRepo.OznačiKaoStorniran(ctx, fr.ID)
} }
} }
} }
@@ -570,13 +583,13 @@ func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
} }
// brisanje KPO zapisa na storno // brisanje KPO zapisa na storno
if h.modulUkljucen(r.Context(), "kpo") { if h.modulUkljucen(ctx, "kpo") {
if e := h.KpoRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); e != nil { if e := h.KpoRepo.ObrisiPoIzvoru(ctx, "prodaja", id); e != nil {
slog.Error("brisanje vezanog KPO zapisa nije uspelo", "prodaja_id", id, "error", e) slog.Error("brisanje vezanog KPO zapisa nije uspelo", "prodaja_id", id, "error", e)
} }
} }
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther) return nil
} }
// fiskalizujProdaju šalje fiskalni zahtev za prodajni nalog. Best-effort: greške // fiskalizujProdaju šalje fiskalni zahtev za prodajni nalog. Best-effort: greške