Knjigovodstvo: storno prodaje/nabavke više ne briše KIR/KPR/KPO

Storno je fizički brisao vezani KIR/KPR/KPO zapis — zakon o računovodstvu ne
dozvoljava brisanje poslovnih knjiga (KPO dodatno ima redni broj bez prekida).
Sad se original čuva, a storno se dograđuje kao poništavajuća stavka
(negirani iznosi, referenca na original) — model.KirStorno/KprStorno/KpoStorno.

Usput ispravljen i pravi uzrok prijavljenog bug-a: kirKandidatiProdaje je
filtrirala samo internu mapu, ne i listu naloga koju obilazi KirBackfillProdaje
— stornirani nalozi su zbog toga dobijali ponovni (pogrešan) KIR upis.
This commit is contained in:
2026-07-02 10:35:24 +02:00
parent 3a0f98622b
commit 45831467e0
10 changed files with 253 additions and 36 deletions
+11 -3
View File
@@ -377,9 +377,17 @@ func (h *Handler) StornoNabavke(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?greska=storno", http.StatusSeeOther)
return
}
// stornirana nabavka ne ulazi u PDV — ukloni vezani auto-KPR zapis
if err := h.PdvKprRepo.ObrisiPoIzvoru(r.Context(), "nabavka", id); err != nil {
slog.Error("brisanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err)
// stornirana nabavka se ne briše iz KPR (zakon o računovodstvu ne dozvoljava
// brisanje poslovnih knjiga) — dograđuje se storno stavka koja poništava original
if zapisi, err := h.PdvKprRepo.DohvatiPoIzvoru(r.Context(), "nabavka", id); err != nil {
slog.Error("čitanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err)
} else {
for _, z := range zapisi {
storno := model.KprStorno(z, razlog, time.Now())
if _, e := h.PdvKprRepo.Kreiraj(r.Context(), &storno); e != nil {
slog.Error("upis storno KPR zapisa nije uspeo", "nabavka_id", id, "error", e)
}
}
}
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
+22 -6
View File
@@ -687,9 +687,17 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string,
return err
}
// stornirana prodaja ne ulazi u PDV — ukloni vezani auto-KIR zapis
if err := h.PdvKirRepo.ObrisiPoIzvoru(ctx, "prodaja", id); err != nil {
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
// stornirana prodaja se ne briše iz KIR (zakon o računovodstvu ne dozvoljava
// brisanje poslovnih knjiga) — dograđuje se storno stavka koja poništava original
if zapisi, err := h.PdvKirRepo.DohvatiPoIzvoru(ctx, "prodaja", id); err != nil {
slog.Error("čitanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
} else {
for _, z := range zapisi {
storno := model.KirStorno(z, razlog, time.Now())
if _, e := h.PdvKirRepo.Kreiraj(ctx, &storno); e != nil {
slog.Error("upis storno KIR zapisa nije uspeo", "prodaja_id", id, "error", e)
}
}
}
// fiskalni refund — best-effort
@@ -744,10 +752,18 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string,
}
}
// brisanje KPO zapisa na storno
// stornirana prodaja se ne briše iz KPO (redni broj mora ostati bez prekida) —
// dograđuje se storno stavka koja poništava original
if h.modulUkljucen(ctx, "kpo") {
if e := h.KpoRepo.ObrisiPoIzvoru(ctx, "prodaja", id); e != nil {
slog.Error("brisanje vezanog KPO zapisa nije uspelo", "prodaja_id", id, "error", e)
if zapisi, e := h.KpoRepo.DohvatiPoIzvoru(ctx, "prodaja", id); e != nil {
slog.Error("čitanje vezanog KPO zapisa nije uspelo", "prodaja_id", id, "error", e)
} else {
for _, z := range zapisi {
storno := model.KpoStorno(z, razlog, time.Now())
if _, e := h.KpoRepo.Kreiraj(ctx, &storno); e != nil {
slog.Error("upis storno KPO zapisa nije uspeo", "prodaja_id", id, "error", e)
}
}
}
}
+9 -1
View File
@@ -59,10 +59,18 @@ func sumnjiviDuplikati(svi []kandidatUskladjivanja) map[int64]bool {
// KIR upis) sa mapom postojećih upisa i kandidatima za detekciju duplikata. Koristi se
// i za backfill (KirBackfillProdaje) i za brojanje praznina na dashboard-u.
func (h *Handler) kirKandidatiProdaje(ctx context.Context) ([]model.ProdajniNalogSaDetaljem, map[int64]bool, []kandidatUskladjivanja, error) {
nalozi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
svi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
if err != nil {
return nil, nil, nil, err
}
// stornirani nalozi se nikad ne razmatraju za KIR upis — ni za backfill ni za
// brojanje praznina — jer stornirana prodaja ne predstavlja oporeziv promet
var nalozi []model.ProdajniNalogSaDetaljem
for _, nd := range svi {
if !nd.Stornirano {
nalozi = append(nalozi, nd)
}
}
postojiMap := make(map[int64]bool, len(nalozi))
var kandidati []kandidatUskladjivanja
for _, nd := range nalozi {