feat(pdv): obračun PDV za period (KIR − KPR)
Interni obračun: izlazni (dugovani) PDV iz KIR i odbitni (prethodni) PDV iz KPR po stopama, konačna obaveza za uplatu ili povraćaj/prenos. PdvBezOdbitka se ne računa u odbitni PDV. Stranica /pdv/obracun (podrazumevano tekući mesec), link u sidebaru. Brojčana podloga za budući zvanični PPPDV/POPDV obrazac.
This commit is contained in:
@@ -270,6 +270,7 @@ func main() {
|
|||||||
r.With(modul("pdv")).Get("/pdv/kpr/nova", h.NoviPdvKpr)
|
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.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"), 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", h.Magacin)
|
||||||
r.Get("/magacin/novi", h.NoviArtikal)
|
r.Get("/magacin/novi", h.NoviArtikal)
|
||||||
r.With(doz("artikal.dodaj")).Post("/magacin/novi", h.SacuvajArtikal)
|
r.With(doz("artikal.dodaj")).Post("/magacin/novi", h.SacuvajArtikal)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ var saSidebar = []string{
|
|||||||
"pdv_stope",
|
"pdv_stope",
|
||||||
"pdv_kir", "pdv_kir_forma",
|
"pdv_kir", "pdv_kir_forma",
|
||||||
"pdv_kpr", "pdv_kpr_forma",
|
"pdv_kpr", "pdv_kpr_forma",
|
||||||
|
"pdv_obracun",
|
||||||
"podsetnici", "podsetnik_forma",
|
"podsetnici", "podsetnik_forma",
|
||||||
"profil_tema",
|
"profil_tema",
|
||||||
"prodaja", "prodaja_detalji", "prodaja_forma",
|
"prodaja", "prodaja_detalji", "prodaja_forma",
|
||||||
|
|||||||
@@ -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),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -219,3 +219,48 @@ func KprIzNabavke(nabavka Nabavka, dobavljacNaziv, dobavljacPib, dobavljacMesto
|
|||||||
}
|
}
|
||||||
return k
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,3 +70,28 @@ func TestKprIzNabavke(t *testing.T) {
|
|||||||
t.Errorf("ukupno=%v, očekivano 400 (240+110+50)", k.Ukupno)
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,6 +105,11 @@
|
|||||||
<span>KPR</span>
|
<span>KPR</span>
|
||||||
<span class="nav-tooltip">KPR — knjiga primljenih računa</span>
|
<span class="nav-tooltip">KPR — knjiga primljenih računa</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/pdv/obracun" class="nav-stavka {{if eq .Stranica "pdv-obracun"}}aktivan{{end}}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="10" y2="10"/><line x1="14" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="10" y2="14"/><line x1="14" y1="14" x2="16" y2="14"/><line x1="8" y1="18" x2="16" y2="18"/></svg>
|
||||||
|
<span>Obračun</span>
|
||||||
|
<span class="nav-tooltip">PDV obračun za period</span>
|
||||||
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="nav-separator"></div>
|
<div class="nav-separator"></div>
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "naslov"}}PDV obračun — NTech{{end}}
|
||||||
|
|
||||||
|
{{define "sadrzaj"}}
|
||||||
|
<div class="stranica-stack" style="width:100%;max-width:100%;">
|
||||||
|
|
||||||
|
<!-- izbor perioda -->
|
||||||
|
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||||
|
<form method="GET" action="/pdv/obracun" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Od datuma</label>
|
||||||
|
<input type="date" name="od" value="{{.Od}}" style="padding:8px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Do datuma</label>
|
||||||
|
<input type="date" name="do" value="{{.Do}}" style="padding:8px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" style="padding:8px 16px;background:var(--sb-aktivan);color:var(--tekst-jak);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;">Prikaži</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- obračun po stopama -->
|
||||||
|
<div class="kartica animiraj" style="padding:0;overflow:hidden;margin-bottom:16px;">
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:560px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<th style="padding:10px 12px;"></th>
|
||||||
|
<th style="padding:10px 12px;text-align:right;">Osnovica</th>
|
||||||
|
<th style="padding:10px 12px;text-align:right;">PDV</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- izlazni (dugovani) PDV — iz KIR -->
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 12px;font-weight:500;" colspan="3">Izlazni (dugovani) PDV — KIR</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 12px;padding-left:24px;">Opšta stopa (20%)</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .KirSume.OsnovicaOpsta}}</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.IzlazniPdvOpsta}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 12px;padding-left:24px;">Posebna stopa (10%)</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .KirSume.OsnovicaPosebna}}</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.IzlazniPdvPosebna}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 12px;padding-left:24px;color:var(--tekst-sporedni);">Oslobođen promet</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;color:var(--tekst-sporedni);">{{printf "%.2f" .KirSume.OslobodenUkupno}}</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;color:var(--tekst-sporedni);">—</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);font-weight:500;background:var(--pozadina);">
|
||||||
|
<td style="padding:10px 12px;">Ukupno izlazni PDV</td>
|
||||||
|
<td style="padding:10px 12px;"></td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.IzlazniPdvUkupno}}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- odbitni (prethodni) PDV — iz KPR -->
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 12px;font-weight:500;padding-top:18px;" colspan="3">Odbitni (prethodni) PDV — KPR</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 12px;padding-left:24px;">Opšta stopa (20%)</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .KprSume.OsnovicaOpsta}}</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.OdbitniPdvOpsta}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 12px;padding-left:24px;">Posebna stopa (10%)</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .KprSume.OsnovicaPosebna}}</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.OdbitniPdvPosebna}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 12px;padding-left:24px;color:var(--tekst-sporedni);">PDV bez prava na odbitak</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;color:var(--tekst-sporedni);">—</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;color:var(--tekst-sporedni);">{{printf "%.2f" .KprSume.PdvBezOdbitka}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="font-weight:500;background:var(--pozadina);">
|
||||||
|
<td style="padding:10px 12px;">Ukupno odbitni PDV</td>
|
||||||
|
<td style="padding:10px 12px;"></td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.OdbitniPdvUkupno}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- rezultat: obaveza za uplatu / povraćaj -->
|
||||||
|
<div class="kartica animiraj" style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;
|
||||||
|
border-left:4px solid {{if .Obracun.ZaUplatu}}var(--greska){{else}}var(--uspeh){{end}};">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||||
|
{{if .Obracun.ZaUplatu}}PDV obaveza za uplatu{{else}}PDV za povraćaj / prenos u naredni period{{end}}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">izlazni − odbitni PDV</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:24px;font-weight:600;color:{{if .Obracun.ZaUplatu}}var(--greska){{else}}var(--uspeh){{end}};white-space:nowrap;">
|
||||||
|
{{printf "%.2f" .Obracun.ObavezaApsolutna}} RSD
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
Reference in New Issue
Block a user