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:
@@ -36,7 +36,6 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var brojArtikala, aktivniServisi, kriticnaZaliha, aktivniPodsetnici int
|
||||
var prihodOvogMeseca float64
|
||||
|
||||
if n, err := h.IzvestajRepo.BrojArtikala(ctx); err != nil {
|
||||
slog.Error("dashboard: broj artikala", "error", err)
|
||||
@@ -50,15 +49,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
aktivniServisi = n
|
||||
}
|
||||
|
||||
// prihod se dohvata samo ako korisnik ima dozvolu dashboard.prihod
|
||||
korisnikDash := middleware.KorisnikIzKonteksta(ctx)
|
||||
if h.DozvoleRepo.ImaDozvolu(ctx, korisnikDash.Uloga, "dashboard.prihod") {
|
||||
if v, err := h.IzvestajRepo.PrihodTekuciMesec(ctx); err != nil {
|
||||
slog.Error("dashboard: prihod ovog meseca", "error", err)
|
||||
} else {
|
||||
prihodOvogMeseca = v
|
||||
}
|
||||
}
|
||||
|
||||
if n, err := h.IzvestajRepo.BrojKriticnihZaliha(ctx); err != nil {
|
||||
slog.Error("dashboard: kriticna zaliha", "error", err)
|
||||
@@ -133,7 +124,6 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
BrojArtikala: brojArtikala,
|
||||
AktivniServisi: aktivniServisi,
|
||||
PrihodOvogMeseca: prihodOvogMeseca,
|
||||
KriticnaZaliha: kriticnaZaliha,
|
||||
AktivniPodsetnici: aktivniPodsetnici,
|
||||
PoslednjiServisi: poslednjiServisi,
|
||||
|
||||
@@ -28,6 +28,19 @@ func formatujMesec(yyyymm string) string {
|
||||
return fmt.Sprintf("%s %d", srpskaImenaMeseci[mes], god)
|
||||
}
|
||||
|
||||
// kljuceviMeseci vraća ključeve ("2006-01") za poslednjih n meseci zaključno sa
|
||||
// mesecom datuma `sada`, hronološki (najstariji prvi). Sidri na prvi u mesecu —
|
||||
// inače AddDate prelije dan (npr. 31. mart − 1 mesec = „31. feb" → 3. mart) i neki
|
||||
// mesec bi se preskočio ili duplirao na 29–31. u mesecu.
|
||||
func kljuceviMeseci(sada time.Time, n int) []string {
|
||||
prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
|
||||
kljucevi := make([]string, 0, n)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
kljucevi = append(kljucevi, prvi.AddDate(0, -i, 0).Format("2006-01"))
|
||||
}
|
||||
return kljucevi
|
||||
}
|
||||
|
||||
// PodaciIzvestaja su podaci za stranicu izveštaja
|
||||
type PodaciIzvestaja struct {
|
||||
model.PodaciStranice
|
||||
@@ -108,15 +121,12 @@ func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// gradimo niz za poslednjih 12 meseci (hronološki)
|
||||
sada := time.Now()
|
||||
var mesecniPrihodi []MesecniPrihod
|
||||
var grafikonLabele []string
|
||||
var grafikonProdaja []float64
|
||||
var grafikonServis []float64
|
||||
|
||||
for i := 11; i >= 0; i-- {
|
||||
t := sada.AddDate(0, -i, 0)
|
||||
kljuc := t.Format("2006-01")
|
||||
for _, kljuc := range kljuceviMeseci(time.Now(), 12) {
|
||||
prod := prodajaPoMesecu[kljuc]
|
||||
serv := servisPoMesecu[kljuc]
|
||||
mesecniPrihodi = append(mesecniPrihodi, MesecniPrihod{
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestKljuceviMeseci proverava da se na kraju meseca (29–31.) nijedan mesec ne
|
||||
// preskoči niti duplira — klasičan AddBug sa prelivom dana.
|
||||
func TestKljuceviMeseci(t *testing.T) {
|
||||
slucajevi := []struct {
|
||||
naziv string
|
||||
sada time.Time
|
||||
n int
|
||||
ocekuje []string
|
||||
}{
|
||||
{
|
||||
// 31. mart: bez sidrenja na 1. dan, "31. feb" bi se prelio u mart i
|
||||
// februar bi nestao. Mora dati uredan niz jan..mart.
|
||||
naziv: "31. mart — bez preskakanja februara",
|
||||
sada: time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC),
|
||||
n: 3,
|
||||
ocekuje: []string{"2026-01", "2026-02", "2026-03"},
|
||||
},
|
||||
{
|
||||
// 31. maj − unazad: mart/april imaju 31/30 dana
|
||||
naziv: "31. maj — uredan niz unazad",
|
||||
sada: time.Date(2026, 5, 31, 0, 0, 0, 0, time.UTC),
|
||||
n: 4,
|
||||
ocekuje: []string{"2026-02", "2026-03", "2026-04", "2026-05"},
|
||||
},
|
||||
{
|
||||
// prelaz preko godine
|
||||
naziv: "30. januar — prelaz preko godine",
|
||||
sada: time.Date(2026, 1, 30, 0, 0, 0, 0, time.UTC),
|
||||
n: 3,
|
||||
ocekuje: []string{"2025-11", "2025-12", "2026-01"},
|
||||
},
|
||||
{
|
||||
// puna godina (12 meseci), bez duplikata
|
||||
naziv: "12 meseci, bez duplikata",
|
||||
sada: time.Date(2026, 6, 27, 0, 0, 0, 0, time.UTC),
|
||||
n: 12,
|
||||
ocekuje: []string{
|
||||
"2025-07", "2025-08", "2025-09", "2025-10", "2025-11", "2025-12",
|
||||
"2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range slucajevi {
|
||||
t.Run(s.naziv, func(t *testing.T) {
|
||||
dobijeno := kljuceviMeseci(s.sada, s.n)
|
||||
if len(dobijeno) != len(s.ocekuje) {
|
||||
t.Fatalf("dužina = %d, očekivano %d (%v)", len(dobijeno), len(s.ocekuje), dobijeno)
|
||||
}
|
||||
vidjeni := map[string]bool{}
|
||||
for i := range s.ocekuje {
|
||||
if dobijeno[i] != s.ocekuje[i] {
|
||||
t.Errorf("[%d] = %q, očekivano %q", i, dobijeno[i], s.ocekuje[i])
|
||||
}
|
||||
if vidjeni[dobijeno[i]] {
|
||||
t.Errorf("mesec %q se duplira", dobijeno[i])
|
||||
}
|
||||
vidjeni[dobijeno[i]] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2035,16 +2035,12 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if noviStatus == model.StatusPreuzeto {
|
||||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), id)
|
||||
if nalog != nil && nalog.CenaKonacna == nil {
|
||||
// auto-izračunaj: dijagnostika + radovi + delovi
|
||||
// auto-izračunaj: dijagnostika + radovi (delovi se računaju posebno)
|
||||
radovi, _ := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), id)
|
||||
delovi, _ := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id)
|
||||
ukupno := nalog.CenaDijagnostike
|
||||
for _, rad := range radovi {
|
||||
ukupno += rad.Ukupno()
|
||||
}
|
||||
for _, d := range delovi {
|
||||
ukupno += d.Ukupno()
|
||||
}
|
||||
h.ServisRepo.AzurirajCenuKonacnu(r.Context(), id, ukupno)
|
||||
nalog.CenaKonacna = &ukupno
|
||||
}
|
||||
@@ -2115,25 +2111,18 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
|
||||
Izvor: "servis",
|
||||
IzvorID: &id,
|
||||
}
|
||||
// cena_komada rada/dela je NETO; PDV se dodaje naviše po stvarnoj stopi
|
||||
for _, r := range radovi {
|
||||
if r.Predlozeno {
|
||||
continue
|
||||
}
|
||||
osnovica := r.Ukupno() / 1.2 // 20% PDV
|
||||
pdv := r.Ukupno() - osnovica
|
||||
kir.OsnovicaOpsta += osnovica
|
||||
kir.PdvOpsta += pdv
|
||||
kir.Ukupno += r.Ukupno()
|
||||
kir.DodajNeto(r.Ukupno(), r.PdvStopa)
|
||||
}
|
||||
for _, d := range delovi {
|
||||
if d.Predlozeno {
|
||||
continue
|
||||
}
|
||||
osnovica := d.Ukupno() / 1.2
|
||||
pdv := d.Ukupno() - osnovica
|
||||
kir.OsnovicaOpsta += osnovica
|
||||
kir.PdvOpsta += pdv
|
||||
kir.Ukupno += d.Ukupno()
|
||||
kir.DodajNeto(d.Ukupno(), d.PdvStopa)
|
||||
}
|
||||
if kir.Ukupno > 0 {
|
||||
if _, e := h.PdvKirRepo.Kreiraj(r.Context(), &kir); e != nil {
|
||||
|
||||
Reference in New Issue
Block a user