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:
@@ -0,0 +1,274 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// seedArtikalSaStanjem kreira artikal sa zadatom količinom i vraća njegov ID.
|
||||
func seedArtikalSaStanjem(t *testing.T, ctx context.Context, repo *ArtikalRepo, naziv string, kolicina int) int64 {
|
||||
t.Helper()
|
||||
id, err := repo.Kreiraj(ctx, &model.Artikal{
|
||||
Naziv: naziv,
|
||||
Kolicina: kolicina,
|
||||
Tip: model.TipProizvod,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("seedArtikalSaStanjem %q: %v", naziv, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// seedUsluga kreira artikal tipa "usluga" (bez praćenja lagera).
|
||||
func seedUsluga(t *testing.T, ctx context.Context, repo *ArtikalRepo, naziv string) int64 {
|
||||
t.Helper()
|
||||
id, err := repo.Kreiraj(ctx, &model.Artikal{
|
||||
Naziv: naziv,
|
||||
Tip: model.TipUsluga,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("seedUsluga %q: %v", naziv, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// prodajNalog kreira prodajni nalog sa zadatim stavkama i vraća ID naloga.
|
||||
func prodajNalog(t *testing.T, ctx context.Context, repo *ProdajaRepo, broj string, stavke []model.StavkaProdaje) int64 {
|
||||
t.Helper()
|
||||
ukupno := 0.0
|
||||
for _, s := range stavke {
|
||||
ukupno += float64(s.Kolicina) * s.CenaPoKomadu
|
||||
}
|
||||
id, err := repo.Kreiraj(ctx, &model.ProdajniNalog{
|
||||
BrojNaloga: broj,
|
||||
Ukupno: ukupno,
|
||||
NacinPlacanja: "gotovina",
|
||||
Datum: time.Now(),
|
||||
}, stavke, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("prodajNalog %q: %v", broj, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// dohvatiKolicinu čita trenutnu količinu artikla iz baze.
|
||||
func dohvatiKolicinu(t *testing.T, ctx context.Context, repo *ArtikalRepo, id int64) int {
|
||||
t.Helper()
|
||||
a, err := repo.DohvatiID(ctx, id)
|
||||
if err != nil || a == nil {
|
||||
t.Fatalf("dohvatiKolicinu id=%d: %v", id, err)
|
||||
}
|
||||
return a.Kolicina
|
||||
}
|
||||
|
||||
// ─── testovi ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// Osnovni slučaj: storno vraća tačnu količinu u magacin.
|
||||
func TestStornoVracaKolicinuUMagacin(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
baza := testDB(t)
|
||||
artRepo := NoviArtikalRepo(baza)
|
||||
prodRepo := NoviProdajaRepo(baza)
|
||||
|
||||
artID := seedArtikalSaStanjem(t, ctx, artRepo, "Punjač", 10)
|
||||
|
||||
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-001", []model.StavkaProdaje{
|
||||
{ArtikalID: artID, Kolicina: 3, CenaPoKomadu: 100},
|
||||
})
|
||||
|
||||
// poslije prodaje: 10 - 3 = 7
|
||||
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 7 {
|
||||
t.Fatalf("posle prodaje: kolicina = %d, očekivano 7", got)
|
||||
}
|
||||
|
||||
if err := prodRepo.Storno(ctx, nalogID, "test storno", nil); err != nil {
|
||||
t.Fatalf("Storno: %v", err)
|
||||
}
|
||||
|
||||
// posle storna: 7 + 3 = 10 (prvobitno stanje)
|
||||
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 10 {
|
||||
t.Errorf("posle storna: kolicina = %d, očekivano 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Nalog sa više stavki — sve se vraćaju.
|
||||
func TestStornoViseSlavki(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
baza := testDB(t)
|
||||
artRepo := NoviArtikalRepo(baza)
|
||||
prodRepo := NoviProdajaRepo(baza)
|
||||
|
||||
aID := seedArtikalSaStanjem(t, ctx, artRepo, "Miš", 20)
|
||||
bID := seedArtikalSaStanjem(t, ctx, artRepo, "Tastatura", 15)
|
||||
|
||||
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-002", []model.StavkaProdaje{
|
||||
{ArtikalID: aID, Kolicina: 5, CenaPoKomadu: 50},
|
||||
{ArtikalID: bID, Kolicina: 3, CenaPoKomadu: 80},
|
||||
})
|
||||
|
||||
if err := prodRepo.Storno(ctx, nalogID, "povrat", nil); err != nil {
|
||||
t.Fatalf("Storno: %v", err)
|
||||
}
|
||||
|
||||
if got := dohvatiKolicinu(t, ctx, artRepo, aID); got != 20 {
|
||||
t.Errorf("Miš: kolicina = %d, očekivano 20", got)
|
||||
}
|
||||
if got := dohvatiKolicinu(t, ctx, artRepo, bID); got != 15 {
|
||||
t.Errorf("Tastatura: kolicina = %d, očekivano 15", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Usluga (tip != "proizvod") — storno je dozvoljen ali se količina ne vraća.
|
||||
func TestStornoUslugaNeVracaKolicinu(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
baza := testDB(t)
|
||||
artRepo := NoviArtikalRepo(baza)
|
||||
prodRepo := NoviProdajaRepo(baza)
|
||||
|
||||
// proizvod koji ima lager
|
||||
artID := seedArtikalSaStanjem(t, ctx, artRepo, "Kabl", 10)
|
||||
// usluga koja nema lager
|
||||
uslugaID := seedUsluga(t, ctx, artRepo, "Instalacija")
|
||||
|
||||
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-003", []model.StavkaProdaje{
|
||||
{ArtikalID: artID, Kolicina: 2, CenaPoKomadu: 200},
|
||||
{ArtikalID: uslugaID, Kolicina: 1, CenaPoKomadu: 500},
|
||||
})
|
||||
|
||||
if err := prodRepo.Storno(ctx, nalogID, "test", nil); err != nil {
|
||||
t.Fatalf("Storno: %v", err)
|
||||
}
|
||||
|
||||
// proizvod se vraća: 10 - 2 + 2 = 10
|
||||
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 10 {
|
||||
t.Errorf("Kabl: kolicina = %d, očekivano 10", got)
|
||||
}
|
||||
// usluga nema količinu — proveri da nije pogrešno promenjena
|
||||
usluga, _ := artRepo.DohvatiID(ctx, uslugaID)
|
||||
if usluga.Kolicina != 0 {
|
||||
t.Errorf("Instalacija (usluga): kolicina = %d, očekivano 0 (usluge nemaju lager)", usluga.Kolicina)
|
||||
}
|
||||
}
|
||||
|
||||
// Dupli storno — drugi pokušaj mora da vrati grešku.
|
||||
func TestStornoVecStorniran(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
baza := testDB(t)
|
||||
artRepo := NoviArtikalRepo(baza)
|
||||
prodRepo := NoviProdajaRepo(baza)
|
||||
|
||||
artID := seedArtikalSaStanjem(t, ctx, artRepo, "USB hub", 5)
|
||||
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-004", []model.StavkaProdaje{
|
||||
{ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 300},
|
||||
})
|
||||
|
||||
if err := prodRepo.Storno(ctx, nalogID, "prvi", nil); err != nil {
|
||||
t.Fatalf("prvi Storno: %v", err)
|
||||
}
|
||||
|
||||
// drugi storno mora da vrati grešku
|
||||
if err := prodRepo.Storno(ctx, nalogID, "drugi", nil); err == nil {
|
||||
t.Error("drugi Storno nije vratio grešku — trebalo je da odbije dupli storno")
|
||||
}
|
||||
|
||||
// količina ne sme biti duplo vraćena: 5 - 1 + 1 = 5, ne 6
|
||||
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 5 {
|
||||
t.Errorf("posle duplog storna: kolicina = %d, očekivano 5 (nema duplog vraćanja)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Storno pa Obrisi — Obrisi ne sme ponovo da vrati robu (storno je već vratio).
|
||||
func TestObrisiPosleStornaNeVracaDuplo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
baza := testDB(t)
|
||||
artRepo := NoviArtikalRepo(baza)
|
||||
prodRepo := NoviProdajaRepo(baza)
|
||||
|
||||
artID := seedArtikalSaStanjem(t, ctx, artRepo, "Adapter", 8)
|
||||
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-005", []model.StavkaProdaje{
|
||||
{ArtikalID: artID, Kolicina: 2, CenaPoKomadu: 150},
|
||||
})
|
||||
|
||||
// storno vraća 2 kom: 8 - 2 + 2 = 8
|
||||
if err := prodRepo.Storno(ctx, nalogID, "storno pre brisanja", nil); err != nil {
|
||||
t.Fatalf("Storno: %v", err)
|
||||
}
|
||||
|
||||
// brisanje storniranog naloga — ne sme ponovo da vrati robu
|
||||
if err := prodRepo.Obrisi(ctx, nalogID, nil); err != nil {
|
||||
t.Fatalf("Obrisi: %v", err)
|
||||
}
|
||||
|
||||
// mora ostati 8, ne 10 (8 + 2 duplo)
|
||||
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 8 {
|
||||
t.Errorf("posle storno+obrisi: kolicina = %d, očekivano 8 (ne 10)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Magacinska promena se beleži pri stornu (tip = PromenaPovracaj).
|
||||
func TestStornoBeležiMagacinskuPromenu(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
baza := testDB(t)
|
||||
artRepo := NoviArtikalRepo(baza)
|
||||
prodRepo := NoviProdajaRepo(baza)
|
||||
|
||||
artID := seedArtikalSaStanjem(t, ctx, artRepo, "Monitor", 3)
|
||||
nalogID := prodajNalog(t, ctx, prodRepo, "PR-S-006", []model.StavkaProdaje{
|
||||
{ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 500},
|
||||
})
|
||||
|
||||
if err := prodRepo.Storno(ctx, nalogID, "povrat kupca", nil); err != nil {
|
||||
t.Fatalf("Storno: %v", err)
|
||||
}
|
||||
|
||||
// proveravamo magacinsku promenu direktno u bazi
|
||||
var tipPromene string
|
||||
var promenaKolicine int
|
||||
err := baza.QueryRowContext(ctx,
|
||||
`SELECT tip_promene, promena_kolicine FROM magacinske_promene
|
||||
WHERE artikal_id = ? AND referentni_id = ? AND tip_promene = ?`,
|
||||
artID, nalogID, model.PromenaPovracaj,
|
||||
).Scan(&tipPromene, &promenaKolicine)
|
||||
if err != nil {
|
||||
t.Fatalf("magacinska promena nije pronađena: %v", err)
|
||||
}
|
||||
if tipPromene != model.PromenaPovracaj {
|
||||
t.Errorf("tip_promene = %q, očekivano %q", tipPromene, model.PromenaPovracaj)
|
||||
}
|
||||
if promenaKolicine != 1 {
|
||||
t.Errorf("promena_kolicine = %d, očekivano 1", promenaKolicine)
|
||||
}
|
||||
}
|
||||
|
||||
// Delimični storno: dva naloga za isti artikal — storno prvog ne dira drugi.
|
||||
func TestStornoDvaOdvojenaNaloga(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
baza := testDB(t)
|
||||
artRepo := NoviArtikalRepo(baza)
|
||||
prodRepo := NoviProdajaRepo(baza)
|
||||
|
||||
artID := seedArtikalSaStanjem(t, ctx, artRepo, "SSD", 10)
|
||||
|
||||
nalog1 := prodajNalog(t, ctx, prodRepo, "PR-S-007", []model.StavkaProdaje{
|
||||
{ArtikalID: artID, Kolicina: 3, CenaPoKomadu: 200},
|
||||
})
|
||||
prodajNalog(t, ctx, prodRepo, "PR-S-008", []model.StavkaProdaje{
|
||||
{ArtikalID: artID, Kolicina: 2, CenaPoKomadu: 200},
|
||||
})
|
||||
// posle oba: 10 - 3 - 2 = 5
|
||||
|
||||
// storniramo samo prvi
|
||||
if err := prodRepo.Storno(ctx, nalog1, "greška", nil); err != nil {
|
||||
t.Fatalf("Storno nalog1: %v", err)
|
||||
}
|
||||
|
||||
// 5 + 3 (vraćen nalog1) = 8; nalog2 (2 kom) ostaje prodat
|
||||
if got := dohvatiKolicinu(t, ctx, artRepo, artID); got != 8 {
|
||||
t.Errorf("posle storna prvog: kolicina = %d, očekivano 8", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user