Prodaja: skener barkoda, prilagodjeni dropdown artikala, ispravka cena sa/bez PDV-a po rezimu

This commit is contained in:
2026-07-01 19:16:34 +02:00
parent fedd4b5d60
commit 8730acc599
6 changed files with 208 additions and 60 deletions
+13 -7
View File
@@ -197,7 +197,8 @@ func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavk
}
// provera stanja, smanjenje i insert stavki
for _, s := range stavke {
for i := range stavke {
s := stavke[i]
var naziv, tip string
var stanjePre int
err := tx.QueryRowContext(ctx,
@@ -230,14 +231,18 @@ func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavk
}
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.
// CenaPoKomadu je cena za naplatu: kod punog PDV obveznika bruto (sa PDV-om,
// PDV se izdvaja naniže), kod firme na evidenciji nema PDV-a (PdvStopa=0, pa je bez razlike).
cenaBezPdv := s.CenaBezPdv
pdvIznos := s.PdvIznos
if cenaBezPdv == 0 {
cenaBezPdv = cenaPoslePopusta
pdvIznos = cenaBezPdv * s.PdvStopa / 100
cenaBezPdv = cenaPoslePopusta / (1 + s.PdvStopa/100)
pdvIznos = cenaPoslePopusta - cenaBezPdv
}
// vraćamo izračunate vrednosti u slajs pozivaoca (npr. za auto-upis u KIR posle Kreiraj)
stavke[i].CenaBezPdv = cenaBezPdv
stavke[i].PdvIznos = pdvIznos
stavke[i].Ukupno = ukupnoStavke
_, err = tx.ExecContext(ctx, `
INSERT INTO stavke_prodaje
(nalog_id, artikal_id, kolicina, cena_po_komadu, popust_procenat, ukupno, pdv_stopa, pdv_iznos, cena_bez_pdv)
@@ -443,9 +448,10 @@ func (r *ProdajaRepo) DnevniPrometMaloprodaje(ctx context.Context, datum string)
return p, fmt.Errorf("ntech: ProdajaRepo.DnevniPrometMaloprodaje: count: %w", err)
}
// stavke grupisane po PDV stopi
// stavke grupisane po PDV stopi — osnovica i PDV se čitaju iz već izračunatih
// cena_bez_pdv/pdv_iznos kolona (ukupno je bruto iznos za naplatu, ne osnovica)
redovi, err := r.db.QueryContext(ctx, `
SELECT s.pdv_stopa, SUM(s.ukupno), SUM(s.ukupno * s.pdv_stopa / 100.0)
SELECT s.pdv_stopa, SUM(s.cena_bez_pdv * s.kolicina), SUM(s.pdv_iznos * s.kolicina)
FROM stavke_prodaje s
JOIN prodajni_nalozi p ON p.id = s.nalog_id
WHERE p.klijent_id IS NULL AND p.stornirano = 0 AND DATE(p.datum) = ?
+9 -7
View File
@@ -81,7 +81,8 @@ func TestProdajaKreiraj_NedovoljnoStanja(t *testing.T) {
}
// TestProdajaKreiraj_PdvAutoKalkulacija: kada CenaBezPdv=0 a PdvStopa>0,
// cena_bez_pdv i pdv_iznos se automatski računaju iz neto cene (CenaPoKomadu je neto).
// cena_bez_pdv i pdv_iznos se automatski izdvajaju naniže iz bruto cene
// (CenaPoKomadu je kod punog PDV obveznika cena za naplatu, sa PDV-om).
func TestProdajaKreiraj_PdvAutoKalkulacija(t *testing.T) {
ctx := context.Background()
baza := testDB(t)
@@ -92,13 +93,14 @@ func TestProdajaKreiraj_PdvAutoKalkulacija(t *testing.T) {
Naziv: "Tastatura", Tip: model.TipProizvod, Kolicina: 10,
})
// neto cena 1000, PDV 20% → pdv_iznos = 200
_, err := prodRepo.Kreiraj(ctx, &model.ProdajniNalog{
BrojNaloga: "PR-PDV-001", Ukupno: 1000, NacinPlacanja: "gotovina", Datum: time.Now(),
}, []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 1000, PdvStopa: 20},
// bruto cena 1200, PDV 20% → cena_bez_pdv = 1000, pdv_iznos = 200
stavke := []model.StavkaProdaje{
{ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 1200, PdvStopa: 20},
// CenaBezPdv=0 namerno — treba da se auto-izračuna
}, nil)
}
_, err := prodRepo.Kreiraj(ctx, &model.ProdajniNalog{
BrojNaloga: "PR-PDV-001", Ukupno: 1200, NacinPlacanja: "gotovina", Datum: time.Now(),
}, stavke, nil)
if err != nil {
t.Fatalf("Kreiraj: %v", err)
}
+4 -7
View File
@@ -105,8 +105,9 @@ func (k *PdvKir) DodajNeto(osnovica, stopa float64) {
}
// 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 BEZ PDV
// (neto), PDV se dodaje naviše — isti obrazac kao kod servisnih radova/delova.
// (20→opšta, 10→posebna, ostalo→oslobođeno). Koristi već izračunatu CenaBezPdv
// (neto, popust je već uračunat pri upisu stavke — v. ProdajaRepo.Kreiraj), jer je
// CenaPoKomadu kod punog PDV obveznika bruto (cena za naplatu), ne neto.
// DatumPrometa je datum same prodaje (nalog.Datum); DatumKnjizenja je datum kada se
// ovaj KIR zapis stvarno upisuje u knjigu — za prodaju upisanu odmah to je isti trenutak,
// ali za naknadni backfill starih naloga upis kasni za periodom pa se prosleđuje posebno.
@@ -123,11 +124,7 @@ func KirIzProdaje(nalog ProdajniNalog, stavke []StavkaProdaje, kupacNaziv, kupac
IzvorID: &id,
}
for _, s := range stavke {
cenaPoslePopusta := s.CenaPoKomadu
if s.PopustProcenat > 0 {
cenaPoslePopusta = cenaPoslePopusta * (1 - s.PopustProcenat/100)
}
k.DodajNeto(float64(s.Kolicina)*cenaPoslePopusta, s.PdvStopa)
k.DodajNeto(float64(s.Kolicina)*s.CenaBezPdv, s.PdvStopa)
}
return k
}
+4 -4
View File
@@ -11,12 +11,12 @@ func blizu(a, b float64) bool { return math.Abs(a-b) < 0.01 }
func TestKirIzProdaje(t *testing.T) {
nalog := ProdajniNalog{ID: 5, BrojNaloga: "P-1", Datum: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)}
stavke := []StavkaProdaje{
// 20%: CenaPoKomadu je NETO → osnovica 2×120=240, PDV 240×20%=48
{Kolicina: 2, CenaPoKomadu: 120, PdvStopa: 20},
// 20%: CenaBezPdv je već izračunata neto vrednost (v. ProdajaRepo.Kreiraj) → osnovica 2×120=240, PDV 240×20%=48
{Kolicina: 2, CenaBezPdv: 120, PdvStopa: 20},
// 10%: osnovica 1×110=110, PDV 110×10%=11
{Kolicina: 1, CenaPoKomadu: 110, PdvStopa: 10},
{Kolicina: 1, CenaBezPdv: 110, PdvStopa: 10},
// 0%: 1 × 50 = 50 (oslobođeno, bez PDV)
{Kolicina: 1, CenaPoKomadu: 50, PdvStopa: 0},
{Kolicina: 1, CenaBezPdv: 50, PdvStopa: 0},
}
k := KirIzProdaje(nalog, stavke, "Kupac doo", "123456789", "Niš", nalog.Datum)