diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 60095ec..d81ed78 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -270,6 +270,7 @@ func main() { r.With(modul("pdv")).Get("/pdv/kpr/nova", h.NoviPdvKpr) r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kpr/nova", h.SacuvajPdvKpr) r.With(modul("pdv"), doz("pdv.obrisi")).Post("/pdv/kpr/obrisi/{id}", h.ObrisiPdvKpr) + r.With(modul("pdv")).Get("/pdv/obracun", h.PdvObracunStranica) 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 35ab4da..9489633 100644 --- a/internal/handler/kes.go +++ b/internal/handler/kes.go @@ -28,6 +28,7 @@ var saSidebar = []string{ "pdv_stope", "pdv_kir", "pdv_kir_forma", "pdv_kpr", "pdv_kpr_forma", + "pdv_obracun", "podsetnici", "podsetnik_forma", "profil_tema", "prodaja", "prodaja_detalji", "prodaja_forma", diff --git a/internal/handler/pdv_obracun.go b/internal/handler/pdv_obracun.go new file mode 100644 index 0000000..269291b --- /dev/null +++ b/internal/handler/pdv_obracun.go @@ -0,0 +1,72 @@ +package handler + +import ( + "net/http" + "time" + + "ntech/internal/db/sqlite" + "ntech/internal/model" +) + +// PodaciPdvObracun su podaci za stranicu obračuna PDV za period +type PodaciPdvObracun struct { + model.PodaciStranice + Od string + Do string + KirSume model.PdvKirSume + KprSume model.PdvKprSume + Obracun model.PdvObracun +} + +// PdvObracunStranica računa obavezu PDV za izabrani period. +// Kada period nije zadat, podrazumevano se uzima tekući mesec. +func (h *Handler) PdvObracunStranica(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") + // podrazumevani period: tekući mesec (od prvog do poslednjeg dana) + if odStr == "" && doStr == "" { + sada := time.Now() + prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location()) + poslednji := prvi.AddDate(0, 1, -1) + odStr = prvi.Format("2006-01-02") + doStr = poslednji.Format("2006-01-02") + } + + od := parsiraDatumOpcionalno(odStr) + do := parsiraDatumOpcionalno(doStr) + + kirZapisi, err := h.PdvKirRepo.Lista(r.Context(), od, do) + if err != nil { + http.Error(w, "Greška pri učitavanju knjige izdatih računa", http.StatusInternalServerError) + return + } + kprZapisi, err := h.PdvKprRepo.Lista(r.Context(), od, do) + if err != nil { + http.Error(w, "Greška pri učitavanju knjige primljenih računa", http.StatusInternalServerError) + return + } + + kirSume := model.SumirajKir(kirZapisi) + kprSume := model.SumirajKpr(kprZapisi) + + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "pdv-obracun" + ps.NaslovStranice = "PDV obračun" + h.renderujTemplate(w, "pdv_obracun", PodaciPdvObracun{ + PodaciStranice: ps, + Od: odStr, + Do: doStr, + KirSume: kirSume, + KprSume: kprSume, + Obracun: model.ObracunajPdv(kirSume, kprSume), + }) +} diff --git a/internal/model/pdv_evidencija.go b/internal/model/pdv_evidencija.go index baa912e..989e777 100644 --- a/internal/model/pdv_evidencija.go +++ b/internal/model/pdv_evidencija.go @@ -219,3 +219,48 @@ func KprIzNabavke(nabavka Nabavka, dobavljacNaziv, dobavljacPib, dobavljacMesto } return k } + +// PdvObracun je rezultat obračuna PDV za period: izlazni (dugovani) PDV iz KIR, +// prethodni (odbitni) PDV iz KPR i konačna obaveza. Pozitivna obaveza znači iznos +// za uplatu, negativna iznos za povraćaj / prenos poreskog kredita u naredni period. +type PdvObracun struct { + // izlazni (dugovani) PDV — iz KIR, po stopama + IzlazniPdvOpsta float64 + IzlazniPdvPosebna float64 + IzlazniPdvUkupno float64 + // prethodni (odbitni) PDV — iz KPR, po stopama (bez PDV bez prava na odbitak) + OdbitniPdvOpsta float64 + OdbitniPdvPosebna float64 + OdbitniPdvUkupno float64 + // obaveza = izlazni − odbitni (>0 za uplatu, <0 za povraćaj/prenos) + Obaveza float64 +} + +// ZaUplatu vraća true kada postoji obaveza za uplatu (izlazni PDV veći od odbitnog). +func (o PdvObracun) ZaUplatu() bool { + return o.Obaveza > 0 +} + +// ObavezaApsolutna vraća iznos obaveze bez predznaka (za prikaz povraćaja kao pozitivan broj). +func (o PdvObracun) ObavezaApsolutna() float64 { + if o.Obaveza < 0 { + return -o.Obaveza + } + return o.Obaveza +} + +// ObracunajPdv računa obavezu PDV iz zbirova KIR i KPR za isti period. +// PdvBezOdbitka iz KPR se namerno NE računa u odbitni PDV — to je PDV za koji +// ne postoji pravo na odbitak prethodnog poreza. +func ObracunajPdv(kir PdvKirSume, kpr PdvKprSume) PdvObracun { + o := PdvObracun{ + IzlazniPdvOpsta: kir.PdvOpsta, + IzlazniPdvPosebna: kir.PdvPosebna, + OdbitniPdvOpsta: kpr.PdvOpsta, + OdbitniPdvPosebna: kpr.PdvPosebna, + } + o.IzlazniPdvUkupno = o.IzlazniPdvOpsta + o.IzlazniPdvPosebna + o.OdbitniPdvUkupno = o.OdbitniPdvOpsta + o.OdbitniPdvPosebna + o.Obaveza = o.IzlazniPdvUkupno - o.OdbitniPdvUkupno + return o +} diff --git a/internal/model/pdv_evidencija_test.go b/internal/model/pdv_evidencija_test.go index 10920eb..22a95d8 100644 --- a/internal/model/pdv_evidencija_test.go +++ b/internal/model/pdv_evidencija_test.go @@ -70,3 +70,28 @@ func TestKprIzNabavke(t *testing.T) { t.Errorf("ukupno=%v, očekivano 400 (240+110+50)", k.Ukupno) } } + +func TestObracunajPdv(t *testing.T) { + // izlazni: 200 (opšta) + 50 (posebna) = 250; odbitni: 80 + 20 = 100; obaveza = 150 + kir := PdvKirSume{PdvOpsta: 200, PdvPosebna: 50} + kpr := PdvKprSume{PdvOpsta: 80, PdvPosebna: 20, PdvBezOdbitka: 30} + + o := ObracunajPdv(kir, kpr) + + if !blizu(o.IzlazniPdvUkupno, 250) { + t.Errorf("izlazni=%v, očekivano 250", o.IzlazniPdvUkupno) + } + // PdvBezOdbitka (30) ne sme da uđe u odbitni PDV + if !blizu(o.OdbitniPdvUkupno, 100) { + t.Errorf("odbitni=%v, očekivano 100 (bez PdvBezOdbitka)", o.OdbitniPdvUkupno) + } + if !blizu(o.Obaveza, 150) || !o.ZaUplatu() { + t.Errorf("obaveza=%v ZaUplatu=%v, očekivano 150/true", o.Obaveza, o.ZaUplatu()) + } + + // kada je odbitni veći od izlaznog → negativna obaveza (povraćaj/prenos), ZaUplatu=false + o2 := ObracunajPdv(PdvKirSume{PdvOpsta: 40}, PdvKprSume{PdvOpsta: 100}) + if !blizu(o2.Obaveza, -60) || o2.ZaUplatu() { + t.Errorf("obaveza=%v ZaUplatu=%v, očekivano -60/false", o2.Obaveza, o2.ZaUplatu()) + } +} diff --git a/web/templates/komponente/sidebar.html b/web/templates/komponente/sidebar.html index 720ab75..9a846af 100644 --- a/web/templates/komponente/sidebar.html +++ b/web/templates/komponente/sidebar.html @@ -105,6 +105,11 @@ KPR KPR — knjiga primljenih računa + + + Obračun + PDV obračun za period + {{end}} diff --git a/web/templates/stranice/pdv_obracun.html b/web/templates/stranice/pdv_obracun.html new file mode 100644 index 0000000..9196a41 --- /dev/null +++ b/web/templates/stranice/pdv_obracun.html @@ -0,0 +1,104 @@ +{{template "base" .}} + +{{define "naslov"}}PDV obračun — NTech{{end}} + +{{define "sadrzaj"}} +
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OsnovicaPDV
Izlazni (dugovani) PDV — KIR
Opšta stopa (20%){{printf "%.2f" .KirSume.OsnovicaOpsta}}{{printf "%.2f" .Obracun.IzlazniPdvOpsta}}
Posebna stopa (10%){{printf "%.2f" .KirSume.OsnovicaPosebna}}{{printf "%.2f" .Obracun.IzlazniPdvPosebna}}
Oslobođen promet{{printf "%.2f" .KirSume.OslobodenUkupno}}
Ukupno izlazni PDV{{printf "%.2f" .Obracun.IzlazniPdvUkupno}}
Odbitni (prethodni) PDV — KPR
Opšta stopa (20%){{printf "%.2f" .KprSume.OsnovicaOpsta}}{{printf "%.2f" .Obracun.OdbitniPdvOpsta}}
Posebna stopa (10%){{printf "%.2f" .KprSume.OsnovicaPosebna}}{{printf "%.2f" .Obracun.OdbitniPdvPosebna}}
PDV bez prava na odbitak{{printf "%.2f" .KprSume.PdvBezOdbitka}}
Ukupno odbitni PDV{{printf "%.2f" .Obracun.OdbitniPdvUkupno}}
+
+
+ + +
+
+
+ {{if .Obracun.ZaUplatu}}PDV obaveza za uplatu{{else}}PDV za povraćaj / prenos u naredni period{{end}} +
+
izlazni − odbitni PDV
+
+
+ {{printf "%.2f" .Obracun.ObavezaApsolutna}} RSD +
+
+ +
+{{end}}