Cena sa PDV-om u magacinu + uslovno prikazivanje po modulu

- migrations/087: nova kolona cena_sa_pdv + popunjavanje postojećih
- model.Artikal: polje CenaSaPdv, ispravljene CenaBezPdv/PdvIznos (neto)
- sqlite/artikal.go: SELECT/Scan/INSERT/UPDATE za cena_sa_pdv (6 mesta)
- magacin_forma.go: pdv_stopa iz forme, kalkulacija cena_sa_pdv, default PDV 20%
- magacin.html: dinari format (25.999,00), kolona Sa PDV-om, uslovno po Moduli.pdv
- magacin_forma.html: PDV dropdown + Cena sa PDV polje, uslovno po Moduli.pdv,
  optimizovan layout (Naziv/Jed. mere/Kategorija u 1 redu)
- main.go: demo mod bez keširanja šablona (hot-reload)
- model_test.go: ažurirani testovi za novo ponašanje CenaBezPdv/PdvIznos
This commit is contained in:
2026-06-26 23:47:52 +02:00
parent 5f8fbf5b46
commit 34a427e046
8 changed files with 176 additions and 78 deletions
+10 -10
View File
@@ -30,7 +30,7 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
SELECT
a.id, a.kategorija_id, a.sifra, a.barkod, a.naziv, a.opis, a.tip, a.jedinica_mere,
a.kolicina, a.kolicina_min, a.lokacija,
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.marza, a.napomena, a.datum_unosa, a.arhiviran,
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.cena_sa_pdv, a.marza, a.napomena, a.datum_unosa, a.arhiviran,
COALESCE(k.naziv, '') as kategorija_naziv, k.marza as kategorija_marza
FROM artikli a
LEFT JOIN kategorije k ON a.kategorija_id = k.id
@@ -93,7 +93,7 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
err := redovi.Scan(
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere,
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.CenaSaPdv, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,
&a.KategorijaNaziv, &katMarza,
)
if err != nil {
@@ -136,11 +136,11 @@ func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal,
err := r.db.QueryRowContext(ctx, `
SELECT id, kategorija_id, sifra, barkod, naziv, opis, tip, jedinica_mere, kolicina, kolicina_min,
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena, datum_unosa, arhiviran
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, cena_sa_pdv, marza, napomena, datum_unosa, arhiviran
FROM artikli WHERE id = ?`, id).Scan(
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere,
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.CenaSaPdv, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,
)
if err != nil {
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err)
@@ -176,10 +176,10 @@ func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, err
rezultat, err := r.db.ExecContext(ctx, `
INSERT INTO artikli
(kategorija_id, sifra, barkod, naziv, opis, tip, jedinica_mere, kolicina, kolicina_min, lokacija,
nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
nabavna_cena, prodajna_cena, pdv_stopa, cena_sa_pdv, marza, napomena)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Tip, a.JedinicaMere, a.Kolicina, a.KolicinMin,
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena,
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.CenaSaPdv, a.Marza, a.Napomena,
)
if err != nil {
return 0, fmt.Errorf("ntech: ArtikalRepo.Kreiraj: %w", err)
@@ -207,11 +207,11 @@ func (r *ArtikalRepo) Izmeni(ctx context.Context, a *model.Artikal) error {
UPDATE artikli SET
kategorija_id = ?, sifra = ?, barkod = ?, naziv = ?, opis = ?, tip = ?, jedinica_mere = ?,
kolicina = ?, kolicina_min = ?, lokacija = ?,
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, marza = ?, napomena = ?
WHERE id = ?`,
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, cena_sa_pdv = ?, marza = ?, napomena = ?
WHERE id = ?`,
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Tip, a.JedinicaMere,
a.Kolicina, a.KolicinMin, a.Lokacija,
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena, a.ID,
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.CenaSaPdv, a.Marza, a.Napomena, a.ID,
)
if err != nil {
return fmt.Errorf("ntech: ArtikalRepo.Izmeni: %w", err)
+25 -1
View File
@@ -62,7 +62,7 @@ func (h *Handler) NoviArtikal(w http.ResponseWriter, r *http.Request) {
PodaciStranice: ps,
Kategorije: kategorije,
Dobavljaci: dobavljaci,
Artikal: model.Artikal{Sifra: predlogSifre, Tip: tip, JedinicaMere: "kom"},
Artikal: model.Artikal{Sifra: predlogSifre, Tip: tip, JedinicaMere: "kom", PdvStopa: 20.0},
Izmena: false,
})
}
@@ -389,6 +389,15 @@ func parseFormuArtikla(r *http.Request) (model.Artikal, string) {
artikal.NabavnaCena = v
}
// PDV stopa — podrazumevano 20%
pdvStopa := 20.0
if p := r.FormValue("pdv_stopa"); p != "" {
if v, err := strconv.ParseFloat(p, 64); err == nil && v >= 0 {
pdvStopa = v
}
}
artikal.PdvStopa = pdvStopa
if c := r.FormValue("prodajna_cena"); c != "" {
v, err := strconv.ParseFloat(c, 64)
if err != nil || v < 0 {
@@ -413,6 +422,21 @@ func parseFormuArtikla(r *http.Request) (model.Artikal, string) {
}
}
// kalkulacija cena_sa_pdv na osnovu unetog polja
if c := r.FormValue("cena_sa_pdv"); c != "" {
// korisnik je uneo bruto cenu → izračunaj neto
if v, err := strconv.ParseFloat(c, 64); err == nil && v >= 0 {
artikal.CenaSaPdv = v
// ako prodajna_cena nije uneta ručno, izračunaj je
if r.FormValue("prodajna_cena") == "" {
artikal.ProdajnaCena = v / (1 + artikal.PdvStopa/100)
}
}
} else {
// korisnik je uneo neto cenu → izračunaj bruto
artikal.CenaSaPdv = artikal.ProdajnaCena * (1 + artikal.PdvStopa/100)
}
return artikal, ""
}
+4 -3
View File
@@ -25,6 +25,7 @@ type Artikal struct {
NabavnaCena float64
ProdajnaCena float64
PdvStopa float64
CenaSaPdv float64 // prodajna cena sa PDV-om (bruto); = ProdajnaCena * (1 + PdvStopa/100)
Marza *float64 // podrazumevana marža (%) za kalkulaciju; NULL = nije postavljeno
Napomena string
DatumUnosa time.Time
@@ -37,14 +38,14 @@ func (a Artikal) PratiLager() bool {
return a.Tip == TipProizvod || a.Tip == ""
}
// CenaBezPdv izračunava prodajnu cenu bez PDV-a
// CenaBezPdv vraća prodajnu cenu bez PDV-a (neto)
func (a Artikal) CenaBezPdv() float64 {
return a.ProdajnaCena / (1 + a.PdvStopa/100)
return a.ProdajnaCena
}
// PdvIznos izračunava iznos PDV-a za jednu jedinicu
func (a Artikal) PdvIznos() float64 {
return a.ProdajnaCena - a.CenaBezPdv()
return a.CenaSaPdv - a.ProdajnaCena
}
// Kategorija predstavlja kategoriju artikala
+7 -6
View File
@@ -15,19 +15,20 @@ func ptr(v float64) *float64 { return &v }
func TestArtikalCenaBezPdvIPdvIznos(t *testing.T) {
testovi := []struct {
naziv string
prodajna float64
prodajna float64 // neto
cenaSaPdv float64 // bruto
stopa float64
ocekBezPdv float64
ocekPdvIznos float64
}{
{"stopa 20%", 120, 20, 100, 20},
{"stopa 0%", 100, 0, 100, 0},
{"stopa 10%", 110, 10, 100, 10},
{"nula cena", 0, 20, 0, 0},
{"stopa 20%", 100, 120, 20, 100, 20},
{"stopa 0%", 100, 100, 0, 100, 0},
{"stopa 10%", 100, 110, 10, 100, 10},
{"nula cena", 0, 0, 20, 0, 0},
}
for _, tt := range testovi {
t.Run(tt.naziv, func(t *testing.T) {
a := Artikal{ProdajnaCena: tt.prodajna, PdvStopa: tt.stopa}
a := Artikal{ProdajnaCena: tt.prodajna, CenaSaPdv: tt.cenaSaPdv, PdvStopa: tt.stopa}
if got := a.CenaBezPdv(); !jednako(got, tt.ocekBezPdv) {
t.Errorf("CenaBezPdv() = %v, očekivano %v", got, tt.ocekBezPdv)
}