diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 2f950c8..4e4cb63 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -225,6 +225,20 @@ func main() { return ntechmw.RequireDozvolaMut(h.DozvoleRepo.ImaDozvolu, akcija) } + // modul vraća middleware koji propušta zahtev samo ako je zakonski modul + // uključen za firmu (profil firme). Sloj IZNAD RBAC-a — zahtev mora proći + // i „modul uključen" (ovo) i „korisnik sme" (doz/zahtevajDozvolu). + proveriModul := func(ctx context.Context, m string) bool { + pod, err := sqlite.DohvatiSvaPodesavanja(ctx, h.DB) + if err != nil { + return false + } + return config.ModulUkljucen(pod, m) + } + modul := func(m string) func(http.Handler) http.Handler { + return ntechmw.RequireModul(proveriModul, m) + } + r.Get("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/dashboard", http.StatusFound) }) @@ -245,6 +259,13 @@ func main() { r.Get("/podesavanja/backup", h.BackupBaze) r.With(doz("backup.pokreni")).Post("/podesavanja/backup/vrati", h.VratiBackup) + + // PDV evidencija — KIR (knjiga izdatih računa). Dostupno samo kada je modul + // „pdv" uključen za firmu (RequireModul), uz RBAC dozvolu pdv.*. + r.With(modul("pdv")).Get("/pdv/kir", h.PdvKir) + r.With(modul("pdv")).Get("/pdv/kir/nova", h.NoviPdvKir) + r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kir/nova", h.SacuvajPdvKir) + r.With(modul("pdv"), doz("pdv.obrisi")).Post("/pdv/kir/obrisi/{id}", h.ObrisiPdvKir) r.Get("/magacin", h.Magacin) r.Get("/magacin/novi", h.NoviArtikal) r.With(doz("artikal.dodaj")).Post("/magacin/novi", h.SacuvajArtikal) diff --git a/internal/handler/kes.go b/internal/handler/kes.go index e0a4e91..31b12cb 100644 --- a/internal/handler/kes.go +++ b/internal/handler/kes.go @@ -26,6 +26,7 @@ var saSidebar = []string{ "nabavke", "nabavka_forma", "nabavka_detalji", "podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem", "pdv_stope", + "pdv_kir", "pdv_kir_forma", "podsetnici", "podsetnik_forma", "profil_tema", "prodaja", "prodaja_detalji", "prodaja_forma", diff --git a/internal/handler/pdv_kir.go b/internal/handler/pdv_kir.go new file mode 100644 index 0000000..a0db83b --- /dev/null +++ b/internal/handler/pdv_kir.go @@ -0,0 +1,175 @@ +package handler + +import ( + "net/http" + "strconv" + "strings" + "time" + + "ntech/internal/db/sqlite" + "ntech/internal/middleware" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// PodaciPdvKir su podaci za pregled knjige izdatih računa +type PodaciPdvKir struct { + model.PodaciStranice + Zapisi []model.PdvKir + Sume model.PdvKirSume + Od string // filter perioda (YYYY-MM-DD), prazno = bez granice + Do string +} + +// PodaciPdvKirForma su podaci za formu unosa zapisa KIR +type PodaciPdvKirForma struct { + model.PodaciStranice + Greska string + Danas string // podrazumevani datum u formi +} + +// parsiraDatumOpcionalno vraća datum iz YYYY-MM-DD; prazan string daje nulti datum (bez filtera) +func parsiraDatumOpcionalno(s string) time.Time { + t, err := time.Parse("2006-01-02", strings.TrimSpace(s)) + if err != nil { + return time.Time{} + } + return t +} + +// parsiraIznos čita decimalni broj iz forme (prihvata i zarez); prazno/neispravno daje 0 +func parsiraIznos(s string) float64 { + v, err := strconv.ParseFloat(strings.TrimSpace(strings.Replace(s, ",", ".", 1)), 64) + if err != nil || v < 0 { + return 0 + } + return v +} + +// PdvKir renderuje pregled knjige izdatih računa sa sumama po stopama +func (h *Handler) PdvKir(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "pdv.pregled"); !ok { + return + } + podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + + odStr := r.URL.Query().Get("od") + doStr := r.URL.Query().Get("do") + zapisi, err := h.PdvKirRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr)) + if err != nil { + http.Error(w, "Greška pri učitavanju knjige izdatih računa", http.StatusInternalServerError) + return + } + + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "pdv-kir" + ps.NaslovStranice = "KIR — knjiga izdatih računa" + h.renderujTemplate(w, "pdv_kir", PodaciPdvKir{ + PodaciStranice: ps, + Zapisi: zapisi, + Sume: model.SumirajKir(zapisi), + Od: odStr, + Do: doStr, + }) +} + +// NoviPdvKir prikazuje praznu formu za unos zapisa u KIR +func (h *Handler) NoviPdvKir(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "pdv.dodaj"); !ok { + return + } + podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "pdv-kir" + ps.NaslovStranice = "Novi izlazni račun (KIR)" + h.renderujTemplate(w, "pdv_kir_forma", PodaciPdvKirForma{ + PodaciStranice: ps, + Danas: time.Now().Format("2006-01-02"), + }) +} + +// SacuvajPdvKir prima POST i upisuje novi zapis u KIR +func (h *Handler) SacuvajPdvKir(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "pdv.dodaj"); !ok { + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + datumPrometa, e1 := time.Parse("2006-01-02", strings.TrimSpace(r.FormValue("datum_prometa"))) + datumKnjizenja, e2 := time.Parse("2006-01-02", strings.TrimSpace(r.FormValue("datum_knjizenja"))) + brojDokumenta := strings.TrimSpace(r.FormValue("broj_dokumenta")) + kupacNaziv := strings.TrimSpace(r.FormValue("kupac_naziv")) + + greska := "" + switch { + case e1 != nil: + greska = "Datum prometa je obavezan i mora biti ispravan." + case e2 != nil: + greska = "Datum knjiženja je obavezan i mora biti ispravan." + case brojDokumenta == "": + greska = "Broj dokumenta je obavezan." + case kupacNaziv == "": + greska = "Naziv kupca je obavezan." + } + if greska != "" { + middleware.SetFlash(w, r, h.DB, "greska", greska) + http.Redirect(w, r, "/pdv/kir/nova", http.StatusSeeOther) + return + } + + z := model.PdvKir{ + DatumPrometa: datumPrometa, + DatumKnjizenja: datumKnjizenja, + BrojDokumenta: brojDokumenta, + KupacNaziv: kupacNaziv, + KupacPib: strings.TrimSpace(r.FormValue("kupac_pib")), + KupacMesto: strings.TrimSpace(r.FormValue("kupac_mesto")), + OsnovicaOpsta: parsiraIznos(r.FormValue("osnovica_opsta")), + PdvOpsta: parsiraIznos(r.FormValue("pdv_opsta")), + OsnovicaPosebna: parsiraIznos(r.FormValue("osnovica_posebna")), + PdvPosebna: parsiraIznos(r.FormValue("pdv_posebna")), + OslobodenSaPravom: parsiraIznos(r.FormValue("osloboden_sa_pravom")), + OslobodenBezPrava: parsiraIznos(r.FormValue("osloboden_bez_prava")), + Napomena: strings.TrimSpace(r.FormValue("napomena")), + } + // ukupna naknada sa PDV — zbir svih osnovica, PDV-a i oslobođenog prometa (računa server) + z.Ukupno = z.OsnovicaOpsta + z.PdvOpsta + z.OsnovicaPosebna + z.PdvPosebna + + z.OslobodenSaPravom + z.OslobodenBezPrava + + if _, err := h.PdvKirRepo.Kreiraj(r.Context(), &z); err != nil { + http.Error(w, "Greška pri čuvanju zapisa", http.StatusInternalServerError) + return + } + middleware.SetFlash(w, r, h.DB, "uspeh", "Izlazni račun je dodat u KIR.") + http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther) +} + +// ObrisiPdvKir briše zapis iz KIR +func (h *Handler) ObrisiPdvKir(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "pdv.obrisi"); !ok { + return + } + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID zapisa", http.StatusBadRequest) + return + } + if err := h.PdvKirRepo.Obrisi(r.Context(), id); err != nil { + http.Error(w, "Greška pri brisanju zapisa", http.StatusInternalServerError) + return + } + middleware.SetFlash(w, r, h.DB, "uspeh", "Zapis je obrisan iz KIR.") + http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther) +} diff --git a/internal/middleware/dozvole.go b/internal/middleware/dozvole.go index 2b19a09..850d621 100644 --- a/internal/middleware/dozvole.go +++ b/internal/middleware/dozvole.go @@ -38,6 +38,9 @@ var sveAkcije = []string{ "backup.pokreni", "tema.lokalno", "dashboard.prihod", + "pdv.pregled", + "pdv.dodaj", + "pdv.obrisi", } // SveAkcije vraća listu svih poznatih akcija — koristi se pri inicijalizaciji baze i resetu @@ -95,6 +98,9 @@ func ImaDozvolu(uloga, akcija string) bool { // dashboard — prihod samo admin+ case "dashboard.prihod": return true + // PDV evidencija (KIR/KPR) — administrativno, radnik nema + case "pdv.pregled", "pdv.dodaj", "pdv.obrisi": + return true } return false diff --git a/internal/model/pdv_evidencija.go b/internal/model/pdv_evidencija.go index 39c41f0..e909dd7 100644 --- a/internal/model/pdv_evidencija.go +++ b/internal/model/pdv_evidencija.go @@ -23,6 +23,57 @@ type PdvKir struct { DatumUnosa time.Time } +// OslobodenUkupno vraća zbir oslobođenog prometa (sa i bez prava na odbitak). +func (k PdvKir) OslobodenUkupno() float64 { + return k.OslobodenSaPravom + k.OslobodenBezPrava +} + +// OznakaPoreskogBroja vraća „JMBG" ako uneti broj ima 13 cifara (fizičko lice), +// inače „PIB" (pravno lice / preduzetnik — PIB ima 9 cifara). +func (k PdvKir) OznakaPoreskogBroja() string { + cifre := 0 + for _, r := range k.KupacPib { + if r >= '0' && r <= '9' { + cifre++ + } + } + if cifre == 13 { + return "JMBG" + } + return "PIB" +} + +// PdvKirSume su zbirovi kolona KIR-a (za red „ukupno" u pregledu knjige). +type PdvKirSume struct { + OsnovicaOpsta float64 + PdvOpsta float64 + OsnovicaPosebna float64 + PdvPosebna float64 + OslobodenSaPravom float64 + OslobodenBezPrava float64 + Ukupno float64 +} + +// OslobodenUkupno vraća zbir oslobođenog prometa (sa i bez prava na odbitak). +func (s PdvKirSume) OslobodenUkupno() float64 { + return s.OslobodenSaPravom + s.OslobodenBezPrava +} + +// SumirajKir sabira sve kolone iz liste KIR zapisa. +func SumirajKir(zapisi []PdvKir) PdvKirSume { + var s PdvKirSume + for _, z := range zapisi { + s.OsnovicaOpsta += z.OsnovicaOpsta + s.PdvOpsta += z.PdvOpsta + s.OsnovicaPosebna += z.OsnovicaPosebna + s.PdvPosebna += z.PdvPosebna + s.OslobodenSaPravom += z.OslobodenSaPravom + s.OslobodenBezPrava += z.OslobodenBezPrava + s.Ukupno += z.Ukupno + } + return s +} + // PdvKpr je jedan zapis u knjizi primljenih računa (ulazni PDV). type PdvKpr struct { ID int64 diff --git a/web/templates/komponente/sidebar.html b/web/templates/komponente/sidebar.html index b2c296f..4b3df81 100644 --- a/web/templates/komponente/sidebar.html +++ b/web/templates/komponente/sidebar.html @@ -93,6 +93,15 @@ {{end}} + {{/* PDV evidencija — vidljivo samo kada je modul „pdv" uključen za firmu (profil firme) i korisnik ima dozvolu */}} + {{if and (index .Moduli "pdv") (index .Dozvole "pdv.pregled")}} + + + KIR + KIR — knjiga izdatih računa + + {{end}} + diff --git a/web/templates/stranice/pdv_kir.html b/web/templates/stranice/pdv_kir.html new file mode 100644 index 0000000..f66faab --- /dev/null +++ b/web/templates/stranice/pdv_kir.html @@ -0,0 +1,86 @@ +{{template "base" .}} + +{{define "naslov"}}KIR — knjiga izdatih računa — NTech{{end}} + +{{define "sadrzaj"}} +
+ + +
+
+
+
+ + +
+
+ + +
+ + {{if or .Od .Do}}Poništi filter{{end}} +
+ + Nov izlazni račun +
+
+ + +
+
+ + + + + + + + + + + + + + + + + {{range .Zapisi}} + + + + + + + + + + + + + {{else}} + + {{end}} + + {{if .Zapisi}} + + + + + + + + + + + + + {{end}} +
Datum prometaBroj dok.KupacOsn. 20%PDV 20%Osn. 10%PDV 10%OslobođenoUkupno
{{.DatumPrometa.Format "02.01.2006."}}{{.BrojDokumenta}}{{.KupacNaziv}}{{if .KupacPib}}
{{.OznakaPoreskogBroja}}: {{.KupacPib}}
{{end}}
{{printf "%.2f" .OsnovicaOpsta}}{{printf "%.2f" .PdvOpsta}}{{printf "%.2f" .OsnovicaPosebna}}{{printf "%.2f" .PdvPosebna}}{{printf "%.2f" .OslobodenUkupno}}{{printf "%.2f" .Ukupno}} +
+ +
+
Nema zapisa u izabranom periodu. Dodaj prvi.
UKUPNO ({{len .Zapisi}}){{printf "%.2f" .Sume.OsnovicaOpsta}}{{printf "%.2f" .Sume.PdvOpsta}}{{printf "%.2f" .Sume.OsnovicaPosebna}}{{printf "%.2f" .Sume.PdvPosebna}}{{printf "%.2f" .Sume.OslobodenUkupno}}{{printf "%.2f" .Sume.Ukupno}}
+
+
+ +
+{{end}} diff --git a/web/templates/stranice/pdv_kir_forma.html b/web/templates/stranice/pdv_kir_forma.html new file mode 100644 index 0000000..e6eb6b5 --- /dev/null +++ b/web/templates/stranice/pdv_kir_forma.html @@ -0,0 +1,97 @@ +{{template "base" .}} + +{{define "naslov"}}Novi izlazni račun (KIR) — NTech{{end}} + +{{define "sadrzaj"}} +
+ + + + Nazad na KIR + + +
+ +
+
Dokument
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
Kupac
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
Iznosi po stopama
+
Ostavite 0 gde nema prometa. „Ukupno" se računa automatski kao zbir osnovica, PDV-a i oslobođenog prometa.
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ Odustani + +
+
+
+ +
+{{end}}