Prodaja: popust se pravilno primenjuje u PDV obračunu (KIR, cena_bez_pdv, fiskalizacija)

KirIzProdaje je za B2B fakture upisivao pun (nediskontovan) iznos u KIR, ignorišući
popust_procenat — PDV evidencija je bila precenjena za stavke sa popustom. Isti
propust je postojao u ProdajaRepo.Kreiraj (cena_bez_pdv/pdv_iznos po stavci računati
bez popusta, dok je ukupno polje popust uračunavalo). fiskal.NapraviZahtev je to
slučajno kompenzovao ponovnim primenjivanjem popusta nad već (pogrešno)
nediskontovanom vrednošću iz baze — sad bi to duplo diskontovalo, pa je uklonjeno.

Usput ispravljena i dva zastarela testa koja su pogrešno pretpostavljala da je
CenaPoKomadu bruto cena (aktuelna semantika, potvrđena i u ntech.js, jeste neto).
This commit is contained in:
2026-07-01 11:49:16 +02:00
parent bd5a63511b
commit 50bb82212d
5 changed files with 31 additions and 25 deletions
+9 -8
View File
@@ -210,20 +210,21 @@ func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavk
} }
} }
// CenaPoKomadu je neto (bez PDV); PDV se dodaje naviše
cenaBezPdv := s.CenaBezPdv
pdvIznos := s.PdvIznos
if cenaBezPdv == 0 {
cenaBezPdv = s.CenaPoKomadu
pdvIznos = cenaBezPdv * s.PdvStopa / 100
}
// popust umanjuje cenu po komadu pre računanja ukupnog i PDV-a // popust umanjuje cenu po komadu pre računanja ukupnog i PDV-a
cenaPoslePopusta := s.CenaPoKomadu cenaPoslePopusta := s.CenaPoKomadu
if s.PopustProcenat > 0 { if s.PopustProcenat > 0 {
cenaPoslePopusta = cenaPoslePopusta * (1 - s.PopustProcenat/100) cenaPoslePopusta = cenaPoslePopusta * (1 - s.PopustProcenat/100)
} }
ukupnoStavke := float64(s.Kolicina) * cenaPoslePopusta ukupnoStavke := float64(s.Kolicina) * cenaPoslePopusta
// CenaPoKomadu je neto (bez PDV); PDV se dodaje naviše.
// cena_bez_pdv/pdv_iznos moraju biti diskontovani da ostanu konzistentni sa ukupno.
cenaBezPdv := s.CenaBezPdv
pdvIznos := s.PdvIznos
if cenaBezPdv == 0 {
cenaBezPdv = cenaPoslePopusta
pdvIznos = cenaBezPdv * s.PdvStopa / 100
}
_, err = tx.ExecContext(ctx, ` _, err = tx.ExecContext(ctx, `
INSERT INTO stavke_prodaje INSERT INTO stavke_prodaje
(nalog_id, artikal_id, kolicina, cena_po_komadu, popust_procenat, ukupno, pdv_stopa, pdv_iznos, cena_bez_pdv) (nalog_id, artikal_id, kolicina, cena_po_komadu, popust_procenat, ukupno, pdv_stopa, pdv_iznos, cena_bez_pdv)
+4 -4
View File
@@ -81,7 +81,7 @@ func TestProdajaKreiraj_NedovoljnoStanja(t *testing.T) {
} }
// TestProdajaKreiraj_PdvAutoKalkulacija: kada CenaBezPdv=0 a PdvStopa>0, // TestProdajaKreiraj_PdvAutoKalkulacija: kada CenaBezPdv=0 a PdvStopa>0,
// cena_bez_pdv i pdv_iznos se automatski računaju iz bruto cene. // cena_bez_pdv i pdv_iznos se automatski računaju iz neto cene (CenaPoKomadu je neto).
func TestProdajaKreiraj_PdvAutoKalkulacija(t *testing.T) { func TestProdajaKreiraj_PdvAutoKalkulacija(t *testing.T) {
ctx := context.Background() ctx := context.Background()
baza := testDB(t) baza := testDB(t)
@@ -92,11 +92,11 @@ func TestProdajaKreiraj_PdvAutoKalkulacija(t *testing.T) {
Naziv: "Tastatura", Tip: model.TipProizvod, Kolicina: 10, Naziv: "Tastatura", Tip: model.TipProizvod, Kolicina: 10,
}) })
// bruto cena 1200, PDV 20% → neto = 1200/1.2 = 1000, pdv_iznos = 200 // neto cena 1000, PDV 20% → pdv_iznos = 200
_, err := prodRepo.Kreiraj(ctx, &model.ProdajniNalog{ _, err := prodRepo.Kreiraj(ctx, &model.ProdajniNalog{
BrojNaloga: "PR-PDV-001", Ukupno: 1200, NacinPlacanja: "gotovina", Datum: time.Now(), BrojNaloga: "PR-PDV-001", Ukupno: 1000, NacinPlacanja: "gotovina", Datum: time.Now(),
}, []model.StavkaProdaje{ }, []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 1200, PdvStopa: 20}, {ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 1000, PdvStopa: 20},
// CenaBezPdv=0 namerno — treba da se auto-izračuna // CenaBezPdv=0 namerno — treba da se auto-izračuna
}, nil) }, nil)
if err != nil { if err != nil {
+5 -4
View File
@@ -62,13 +62,14 @@ func NapraviZahtev(
if s.PdvStopa > 0 { if s.PdvStopa > 0 {
stopa = s.PdvStopa stopa = s.PdvStopa
} }
// CenaPoKomadu je neto cena — konvertujemo u bruto; popust umanjuje neto cenu pre PDV-a // CenaBezPdv iz baze je već diskontovana (popust primenjen pri kreiranju naloga);
// popust se ponovo primenjuje samo u fallback grani kad CenaBezPdv nije popunjena.
netoCena := s.CenaBezPdv netoCena := s.CenaBezPdv
if netoCena == 0 { if netoCena == 0 {
netoCena = s.CenaPoKomadu netoCena = s.CenaPoKomadu
} if s.PopustProcenat > 0 {
if s.PopustProcenat > 0 { netoCena = math.Round(netoCena*(1-s.PopustProcenat/100)*100) / 100
netoCena = math.Round(netoCena*(1-s.PopustProcenat/100)*100) / 100 }
} }
brutoCena := BrutoCena(netoCena, stopa) brutoCena := BrutoCena(netoCena, stopa)
brutoTotal := math.Round(brutoCena*float64(s.Kolicina)*100) / 100 brutoTotal := math.Round(brutoCena*float64(s.Kolicina)*100) / 100
+5 -1
View File
@@ -120,7 +120,11 @@ func KirIzProdaje(nalog ProdajniNalog, stavke []StavkaProdaje, kupacNaziv, kupac
IzvorID: &id, IzvorID: &id,
} }
for _, s := range stavke { for _, s := range stavke {
k.DodajNeto(float64(s.Kolicina)*s.CenaPoKomadu, s.PdvStopa) cenaPoslePopusta := s.CenaPoKomadu
if s.PopustProcenat > 0 {
cenaPoslePopusta = cenaPoslePopusta * (1 - s.PopustProcenat/100)
}
k.DodajNeto(float64(s.Kolicina)*cenaPoslePopusta, s.PdvStopa)
} }
return k return k
} }
+8 -8
View File
@@ -11,9 +11,9 @@ func blizu(a, b float64) bool { return math.Abs(a-b) < 0.01 }
func TestKirIzProdaje(t *testing.T) { func TestKirIzProdaje(t *testing.T) {
nalog := ProdajniNalog{ID: 5, BrojNaloga: "P-1", Datum: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)} nalog := ProdajniNalog{ID: 5, BrojNaloga: "P-1", Datum: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)}
stavke := []StavkaProdaje{ stavke := []StavkaProdaje{
// 20%: 2 × 120 = 240 (osnovica 200, PDV 40) // 20%: CenaPoKomadu je NETO → osnovica 2×120=240, PDV 240×20%=48
{Kolicina: 2, CenaPoKomadu: 120, PdvStopa: 20}, {Kolicina: 2, CenaPoKomadu: 120, PdvStopa: 20},
// 10%: 1 × 110 = 110 (osnovica 100, PDV 10) // 10%: osnovica 1×110=110, PDV 110×10%=11
{Kolicina: 1, CenaPoKomadu: 110, PdvStopa: 10}, {Kolicina: 1, CenaPoKomadu: 110, PdvStopa: 10},
// 0%: 1 × 50 = 50 (oslobođeno, bez PDV) // 0%: 1 × 50 = 50 (oslobođeno, bez PDV)
{Kolicina: 1, CenaPoKomadu: 50, PdvStopa: 0}, {Kolicina: 1, CenaPoKomadu: 50, PdvStopa: 0},
@@ -27,17 +27,17 @@ func TestKirIzProdaje(t *testing.T) {
if k.BrojDokumenta != "P-1" || k.KupacNaziv != "Kupac doo" || k.KupacPib != "123456789" { if k.BrojDokumenta != "P-1" || k.KupacNaziv != "Kupac doo" || k.KupacPib != "123456789" {
t.Errorf("zaglavlje ne odgovara: %+v", k) t.Errorf("zaglavlje ne odgovara: %+v", k)
} }
if !blizu(k.OsnovicaOpsta, 200) || !blizu(k.PdvOpsta, 40) { if !blizu(k.OsnovicaOpsta, 240) || !blizu(k.PdvOpsta, 48) {
t.Errorf("opšta: osnovica=%v pdv=%v, očekivano 200/40", k.OsnovicaOpsta, k.PdvOpsta) t.Errorf("opšta: osnovica=%v pdv=%v, očekivano 240/48", k.OsnovicaOpsta, k.PdvOpsta)
} }
if !blizu(k.OsnovicaPosebna, 100) || !blizu(k.PdvPosebna, 10) { if !blizu(k.OsnovicaPosebna, 110) || !blizu(k.PdvPosebna, 11) {
t.Errorf("posebna: osnovica=%v pdv=%v, očekivano 100/10", k.OsnovicaPosebna, k.PdvPosebna) t.Errorf("posebna: osnovica=%v pdv=%v, očekivano 110/11", k.OsnovicaPosebna, k.PdvPosebna)
} }
if !blizu(k.OslobodenSaPravom, 50) { if !blizu(k.OslobodenSaPravom, 50) {
t.Errorf("oslobođeno=%v, očekivano 50", k.OslobodenSaPravom) t.Errorf("oslobođeno=%v, očekivano 50", k.OslobodenSaPravom)
} }
if !blizu(k.Ukupno, 400) { if !blizu(k.Ukupno, 459) {
t.Errorf("ukupno=%v, očekivano 400 (240+110+50)", k.Ukupno) t.Errorf("ukupno=%v, očekivano 459 (288+121+50 bruto)", k.Ukupno)
} }
} }