Dashboard/izveštaji: ispravka obračuna prihoda + KIR servisa, testovi

- Prihod meseca: izbačeni stornirani nalozi, servis preko naplaceno+avans
- Servis KIR: neto cena + PDV po stvarnoj stopi (PdvKir.DodajNeto), bez hardkodovanih 20%
- TopKlijenti: samo preuzeti nalozi sa naplatom/avansom
- Izveštaji: mesečni ključevi sidreni na 1. u mesecu (bez preliva dana)
- BUG-04: konačna cena servisa bez dvostrukog računanja delova
- Dashboard: uklonjena kartica „Prihod ovog meseca"
- Default port 3000
- Dodati testovi: prihod/storno/delovi/nivelacija/KIR/mesečni ključevi
This commit is contained in:
2026-06-27 09:41:07 +02:00
parent 60e7debad5
commit 3475422cb9
17 changed files with 1385 additions and 76 deletions
+20
View File
@@ -56,6 +56,26 @@ func TestRasporediTroskove(t *testing.T) {
metod: "kolicina",
ocekuje: []float64{50},
},
{
// negativan trošak (odobreni popust) — cena ostaje nepromenjena
naziv: "negativan trosak",
stavke: stavke,
trosak: -100,
metod: "vrednost",
ocekuje: []float64{100, 200},
},
{
// trošak 10 na 3 stavke po kolicini (9 kom ukupno) → 10/9/3 ≈ 1.111/kom
naziv: "zaokruzivanje na 2 decimale",
stavke: []StavkaNabavke{
{Kolicina: 3, CenaPoKomadu: 100},
{Kolicina: 3, CenaPoKomadu: 200},
{Kolicina: 3, CenaPoKomadu: 50},
},
trosak: 10,
metod: "kolicina",
ocekuje: []float64{101.11, 201.11, 51.11},
},
}
for _, s := range slucajevi {
+25 -13
View File
@@ -80,6 +80,30 @@ func SumirajKir(zapisi []PdvKir) PdvKirSume {
return s
}
// dodajStavku razvrstava jednu stavku (već izračunata osnovica + PDV) po stopi u
// odgovarajuće kolone KIR-a i uvećava ukupan (bruto) iznos.
func (k *PdvKir) dodajStavku(osnovica, pdv, stopa float64) {
switch stopa {
case 20:
k.OsnovicaOpsta += osnovica
k.PdvOpsta += pdv
case 10:
k.OsnovicaPosebna += osnovica
k.PdvPosebna += pdv
default:
// 0% / oslobođeno — osnovica u oslobođen promet sa pravom na odbitak
k.OslobodenSaPravom += osnovica
}
k.Ukupno += osnovica + pdv
}
// DodajNeto dodaje stavku zadatu NETO osnovicom i PDV stopom — za izvore gde je
// cena bez PDV-a (servisni rad/deo: cena_komada je neto, PDV se dodaje naviše).
// PDV se izvodi kao osnovica × stopa/100, a ukupno postaje bruto (osnovica + PDV).
func (k *PdvKir) DodajNeto(osnovica, stopa float64) {
k.dodajStavku(osnovica, osnovica*stopa/100, stopa)
}
// 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).
@@ -101,19 +125,7 @@ func KirIzProdaje(nalog ProdajniNalog, stavke []StavkaProdaje, kupacNaziv, kupac
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
k.dodajStavku(osnovica, ukupnoLinija-osnovica, s.PdvStopa)
}
return k
}
+29
View File
@@ -41,6 +41,35 @@ func TestKirIzProdaje(t *testing.T) {
}
}
// TestPdvKirDodajNeto: osnovica zadata kao NETO (servisni rad/deo) se NE deli,
// PDV se dodaje naviše po stvarnoj stopi i razvrstava u opštu/posebnu/oslobođen.
func TestPdvKirDodajNeto(t *testing.T) {
var k PdvKir
// rad 1000 neto @ 20% → osnovica 1000 (NE 833.33!), PDV 200, ukupno bruto 1200
k.DodajNeto(1000, 20)
if !blizu(k.OsnovicaOpsta, 1000) || !blizu(k.PdvOpsta, 200) {
t.Errorf("20%%: osnovica=%v pdv=%v, očekivano 1000/200 (neto se ne deli)", k.OsnovicaOpsta, k.PdvOpsta)
}
// deo 1000 neto @ 10% → posebna osnovica 1000, PDV 100
k.DodajNeto(1000, 10)
if !blizu(k.OsnovicaPosebna, 1000) || !blizu(k.PdvPosebna, 100) {
t.Errorf("10%%: osnovica=%v pdv=%v, očekivano 1000/100", k.OsnovicaPosebna, k.PdvPosebna)
}
// usluga 500 neto @ 0% → oslobođen promet, bez PDV
k.DodajNeto(500, 0)
if !blizu(k.OslobodenSaPravom, 500) {
t.Errorf("0%%: oslobođeno=%v, očekivano 500", k.OslobodenSaPravom)
}
// ukupno (bruto) = 1200 + 1100 + 500 = 2800
if !blizu(k.Ukupno, 2800) {
t.Errorf("ukupno=%v, očekivano 2800 (1200+1100+500)", k.Ukupno)
}
}
func TestKprIzNabavke(t *testing.T) {
nabavka := Nabavka{ID: 3, Napomena: "test", Datum: time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)}
stavke := []NabavkaStavkaPdv{
+89
View File
@@ -0,0 +1,89 @@
package model
import "testing"
func TestServisniNalog_PreostaloZaNaplatu(t *testing.T) {
slucajevi := []struct {
naziv string
konacna *float64
avans *float64
ocekivano *float64
}{
{
naziv: "cena_konacna nil → nil (nije uneta)",
konacna: nil,
avans: nil,
ocekivano: nil,
},
{
naziv: "bez avansa → puna cena",
konacna: ptr(2000),
avans: nil,
ocekivano: ptr(2000),
},
{
naziv: "avans manji od cene → razlika",
konacna: ptr(2000),
avans: ptr(500),
ocekivano: ptr(1500),
},
{
naziv: "avans jednak ceni → 0",
konacna: ptr(2000),
avans: ptr(2000),
ocekivano: ptr(0),
},
{
naziv: "avans veći od cene → 0 (ne ide u minus)",
konacna: ptr(1000),
avans: ptr(1500),
ocekivano: ptr(0),
},
}
for _, s := range slucajevi {
t.Run(s.naziv, func(t *testing.T) {
n := ServisniNalog{CenaKonacna: s.konacna, Avans: s.avans}
dobijeno := n.PreostaloZaNaplatu()
if s.ocekivano == nil {
if dobijeno != nil {
t.Errorf("dobijeno %v, očekivano nil", *dobijeno)
}
return
}
if dobijeno == nil {
t.Fatalf("dobijeno nil, očekivano %.2f", *s.ocekivano)
}
if !jednako(*dobijeno, *s.ocekivano) {
t.Errorf("dobijeno %.2f, očekivano %.2f", *dobijeno, *s.ocekivano)
}
})
}
}
func TestServisniDeo_Ukupno(t *testing.T) {
d := ServisniDeo{Kolicina: 3, CenaKomada: 150}
if !jednako(d.Ukupno(), 450) {
t.Errorf("Ukupno = %.2f, očekivano 450", d.Ukupno())
}
}
func TestServisniDeoSaArtiklom_UkupnoSaPdv(t *testing.T) {
d := ServisniDeoSaArtiklom{
ServisniDeo: ServisniDeo{Kolicina: 2},
CenaSaPdv: 600,
}
if !jednako(d.UkupnoSaPdv(), 1200) {
t.Errorf("UkupnoSaPdv = %.2f, očekivano 1200", d.UkupnoSaPdv())
}
}
func TestServisniRad_UkupnoIUkupnoSaPdv(t *testing.T) {
r := ServisniRad{Kolicina: 2, CenaKomada: 1000, PdvStopa: 20, CenaSaPdv: 1200}
if !jednako(r.Ukupno(), 2000) {
t.Errorf("Ukupno = %.2f, očekivano 2000", r.Ukupno())
}
if !jednako(r.UkupnoSaPdv(), 2400) {
t.Errorf("UkupnoSaPdv = %.2f, očekivano 2400", r.UkupnoSaPdv())
}
}