diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 5c6fb55..d45149b 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -1,6 +1,7 @@ package handler import ( + "context" "database/sql" "html/template" "io/fs" @@ -130,6 +131,16 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) { h.PdvKprRepo = sqlite.NoviPdvKprRepo(novaDB) } +// modulUkljucen vraća da li je zakonski modul (npr. „pdv") uključen za firmu prema profilu. +// Koristi se pri automatskom punjenju KIR/KPR iz prodaje/nabavke. +func (h *Handler) modulUkljucen(ctx context.Context, modul string) bool { + podesavanja, err := sqlite.DohvatiSvaPodesavanja(ctx, h.DB) + if err != nil { + return false + } + return config.ModulUkljucen(podesavanja, modul) +} + // zahtevajDozvolu vraća prijavljenog korisnika ako njegova uloga sme da izvrši akciju. // U suprotnom šalje 403 sa srpskom porukom i vraća ok=false (handler tada return-uje). func (h *Handler) zahtevajDozvolu(w http.ResponseWriter, r *http.Request, akcija string) (*model.Korisnik, bool) { diff --git a/internal/handler/prodaja.go b/internal/handler/prodaja.go index b75fd78..0f3ebda 100644 --- a/internal/handler/prodaja.go +++ b/internal/handler/prodaja.go @@ -197,6 +197,22 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) { return } + // automatski zavedi u KIR ako je firma PDV obveznik i prodaja je na klijenta (B2B faktura). + // Maloprodaja građanima (bez klijenta) ide zbirno preko fiskalizacije (Faza 3) — preskače se. + if nalog.KlijentID != nil && h.modulUkljucen(r.Context(), "pdv") { + if klijent, e := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID); e == nil { + nalog.ID = id + pib := klijent.PIB + if klijent.Tip != "pravno" { + pib = klijent.JMBG + } + kir := model.KirIzProdaje(nalog, stavke, klijent.PunoIme(), pib, klijent.Mesto) + if _, e := h.PdvKirRepo.Kreiraj(r.Context(), &kir); e != nil { + slog.Error("auto-upis u KIR nije uspeo", "prodaja_id", id, "error", e) + } + } + } + http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther) } @@ -319,6 +335,10 @@ func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) { http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError) return } + // ukloni vezani auto-KIR zapis (ako ga je ova prodaja kreirala) + if err := h.PdvKirRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); err != nil { + slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err) + } http.Redirect(w, r, "/prodaja?obrisan=1", http.StatusSeeOther) } @@ -408,6 +428,10 @@ func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther) return } + // stornirana prodaja ne ulazi u PDV — ukloni vezani auto-KIR zapis + if err := h.PdvKirRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); err != nil { + slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err) + } middleware.SetFlash(w, r, h.DB, "uspeh", "Prodajni nalog je storniran.") http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther) } diff --git a/internal/model/pdv_evidencija.go b/internal/model/pdv_evidencija.go index 6cd161f..9f27e9e 100644 --- a/internal/model/pdv_evidencija.go +++ b/internal/model/pdv_evidencija.go @@ -76,6 +76,44 @@ func SumirajKir(zapisi []PdvKir) PdvKirSume { return s } +// KirIzProdaje gradi KIR zapis iz prodaje: stavke se grupišu po PDV stopi +// (20→opšta, 10→posebna, ostalo→oslobođeno). CenaPoKomadu je prodajna cena SA PDV, +// pa se osnovica izvodi deljenjem sa (1 + stopa/100). +func KirIzProdaje(nalog ProdajniNalog, stavke []StavkaProdaje, kupacNaziv, kupacPib, kupacMesto string) PdvKir { + id := nalog.ID + k := PdvKir{ + DatumPrometa: nalog.Datum, + DatumKnjizenja: nalog.Datum, + BrojDokumenta: nalog.BrojNaloga, + KupacNaziv: kupacNaziv, + KupacPib: kupacPib, + KupacMesto: kupacMesto, + Izvor: "prodaja", + IzvorID: &id, + } + for _, s := range stavke { + ukupnoLinija := float64(s.Kolicina) * s.CenaPoKomadu + osnovica := ukupnoLinija + if s.PdvStopa > 0 { + osnovica = ukupnoLinija / (1 + s.PdvStopa/100) + } + pdv := ukupnoLinija - osnovica + switch s.PdvStopa { + case 20: + k.OsnovicaOpsta += osnovica + k.PdvOpsta += pdv + case 10: + k.OsnovicaPosebna += osnovica + k.PdvPosebna += pdv + default: + // 0% / oslobođeno — osnovica bez PDV-a u oslobođen promet sa pravom na odbitak + k.OslobodenSaPravom += osnovica + } + k.Ukupno += ukupnoLinija + } + return k +} + // PdvKpr je jedan zapis u knjizi primljenih računa (ulazni PDV). type PdvKpr struct { ID int64 diff --git a/internal/model/pdv_evidencija_test.go b/internal/model/pdv_evidencija_test.go new file mode 100644 index 0000000..58b1663 --- /dev/null +++ b/internal/model/pdv_evidencija_test.go @@ -0,0 +1,42 @@ +package model + +import ( + "math" + "testing" + "time" +) + +func blizu(a, b float64) bool { return math.Abs(a-b) < 0.01 } + +func TestKirIzProdaje(t *testing.T) { + nalog := ProdajniNalog{ID: 5, BrojNaloga: "P-1", Datum: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)} + stavke := []StavkaProdaje{ + // 20%: 2 × 120 = 240 (osnovica 200, PDV 40) + {Kolicina: 2, CenaPoKomadu: 120, PdvStopa: 20}, + // 10%: 1 × 110 = 110 (osnovica 100, PDV 10) + {Kolicina: 1, CenaPoKomadu: 110, PdvStopa: 10}, + // 0%: 1 × 50 = 50 (oslobođeno, bez PDV) + {Kolicina: 1, CenaPoKomadu: 50, PdvStopa: 0}, + } + + k := KirIzProdaje(nalog, stavke, "Kupac doo", "123456789", "Niš") + + if k.Izvor != "prodaja" || k.IzvorID == nil || *k.IzvorID != 5 { + t.Errorf("izvor=%q izvor_id=%v, očekivano prodaja/5", k.Izvor, k.IzvorID) + } + if k.BrojDokumenta != "P-1" || k.KupacNaziv != "Kupac doo" || k.KupacPib != "123456789" { + t.Errorf("zaglavlje ne odgovara: %+v", k) + } + if !blizu(k.OsnovicaOpsta, 200) || !blizu(k.PdvOpsta, 40) { + t.Errorf("opšta: osnovica=%v pdv=%v, očekivano 200/40", k.OsnovicaOpsta, k.PdvOpsta) + } + if !blizu(k.OsnovicaPosebna, 100) || !blizu(k.PdvPosebna, 10) { + t.Errorf("posebna: osnovica=%v pdv=%v, očekivano 100/10", k.OsnovicaPosebna, k.PdvPosebna) + } + if !blizu(k.OslobodenSaPravom, 50) { + t.Errorf("oslobođeno=%v, očekivano 50", k.OslobodenSaPravom) + } + if !blizu(k.Ukupno, 400) { + t.Errorf("ukupno=%v, očekivano 400 (240+110+50)", k.Ukupno) + } +} diff --git a/web/templates/stranice/pdv_kir.html b/web/templates/stranice/pdv_kir.html index f66faab..cf297a9 100644 --- a/web/templates/stranice/pdv_kir.html +++ b/web/templates/stranice/pdv_kir.html @@ -55,9 +55,13 @@ {{printf "%.2f" .OslobodenUkupno}} {{printf "%.2f" .Ukupno}} + {{if eq .Izvor "rucno"}}
+ {{else}} + iz prodaje + {{end}} {{else}}