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 97fd4490ea
commit 84bb8aa158
8 changed files with 176 additions and 78 deletions
+1 -1
View File
@@ -159,7 +159,7 @@ func main() {
// čuva odabrani FS (disk ili embed) za hot-reload u razvoju i za keš u produkciji // čuva odabrani FS (disk ili embed) za hot-reload u razvoju i za keš u produkciji
h.TemplatesFS = templFS h.TemplatesFS = templFS
if ntechEnv := os.Getenv("NTECH_ENV"); ntechEnv == "production" || ntechEnv == "demo" { if ntechEnv := os.Getenv("NTECH_ENV"); ntechEnv == "production" {
kes, err := handler.KreirajKes(templFS) kes, err := handler.KreirajKes(templFS)
if err != nil { if err != nil {
slog.Error("Greška pri kreiranju keša šablona", "error", err) slog.Error("Greška pri kreiranju keša šablona", "error", err)
+10 -10
View File
@@ -30,7 +30,7 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
SELECT SELECT
a.id, a.kategorija_id, a.sifra, a.barkod, a.naziv, a.opis, a.tip, a.jedinica_mere, 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.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 COALESCE(k.naziv, '') as kategorija_naziv, k.marza as kategorija_marza
FROM artikli a FROM artikli a
LEFT JOIN kategorije k ON a.kategorija_id = k.id 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( err := redovi.Scan(
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere, &a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere,
&a.Kolicina, &a.KolicinMin, &a.Lokacija, &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, &a.KategorijaNaziv, &katMarza,
) )
if err != nil { if err != nil {
@@ -136,11 +136,11 @@ func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal,
err := r.db.QueryRowContext(ctx, ` err := r.db.QueryRowContext(ctx, `
SELECT id, kategorija_id, sifra, barkod, naziv, opis, tip, jedinica_mere, kolicina, kolicina_min, 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( FROM artikli WHERE id = ?`, id).Scan(
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere, &a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere,
&a.Kolicina, &a.KolicinMin, &a.Lokacija, &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 { if err != nil {
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err) 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, ` rezultat, err := r.db.ExecContext(ctx, `
INSERT INTO artikli INSERT INTO artikli
(kategorija_id, sifra, barkod, naziv, opis, tip, jedinica_mere, kolicina, kolicina_min, lokacija, (kategorija_id, sifra, barkod, naziv, opis, tip, jedinica_mere, kolicina, kolicina_min, lokacija,
nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena) nabavna_cena, prodajna_cena, pdv_stopa, cena_sa_pdv, marza, napomena)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Tip, a.JedinicaMere, a.Kolicina, a.KolicinMin, 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 { if err != nil {
return 0, fmt.Errorf("ntech: ArtikalRepo.Kreiraj: %w", err) 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 UPDATE artikli SET
kategorija_id = ?, sifra = ?, barkod = ?, naziv = ?, opis = ?, tip = ?, jedinica_mere = ?, kategorija_id = ?, sifra = ?, barkod = ?, naziv = ?, opis = ?, tip = ?, jedinica_mere = ?,
kolicina = ?, kolicina_min = ?, lokacija = ?, kolicina = ?, kolicina_min = ?, lokacija = ?,
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, marza = ?, napomena = ? nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, cena_sa_pdv = ?, marza = ?, napomena = ?
WHERE id = ?`, WHERE id = ?`,
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Tip, a.JedinicaMere, a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Tip, a.JedinicaMere,
a.Kolicina, a.KolicinMin, a.Lokacija, 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 { if err != nil {
return fmt.Errorf("ntech: ArtikalRepo.Izmeni: %w", err) 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, PodaciStranice: ps,
Kategorije: kategorije, Kategorije: kategorije,
Dobavljaci: dobavljaci, 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, Izmena: false,
}) })
} }
@@ -389,6 +389,15 @@ func parseFormuArtikla(r *http.Request) (model.Artikal, string) {
artikal.NabavnaCena = v 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 != "" { if c := r.FormValue("prodajna_cena"); c != "" {
v, err := strconv.ParseFloat(c, 64) v, err := strconv.ParseFloat(c, 64)
if err != nil || v < 0 { 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, "" return artikal, ""
} }
+4 -3
View File
@@ -25,6 +25,7 @@ type Artikal struct {
NabavnaCena float64 NabavnaCena float64
ProdajnaCena float64 ProdajnaCena float64
PdvStopa 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 Marza *float64 // podrazumevana marža (%) za kalkulaciju; NULL = nije postavljeno
Napomena string Napomena string
DatumUnosa time.Time DatumUnosa time.Time
@@ -37,14 +38,14 @@ func (a Artikal) PratiLager() bool {
return a.Tip == TipProizvod || a.Tip == "" 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 { func (a Artikal) CenaBezPdv() float64 {
return a.ProdajnaCena / (1 + a.PdvStopa/100) return a.ProdajnaCena
} }
// PdvIznos izračunava iznos PDV-a za jednu jedinicu // PdvIznos izračunava iznos PDV-a za jednu jedinicu
func (a Artikal) PdvIznos() float64 { func (a Artikal) PdvIznos() float64 {
return a.ProdajnaCena - a.CenaBezPdv() return a.CenaSaPdv - a.ProdajnaCena
} }
// Kategorija predstavlja kategoriju artikala // Kategorija predstavlja kategoriju artikala
+7 -6
View File
@@ -15,19 +15,20 @@ func ptr(v float64) *float64 { return &v }
func TestArtikalCenaBezPdvIPdvIznos(t *testing.T) { func TestArtikalCenaBezPdvIPdvIznos(t *testing.T) {
testovi := []struct { testovi := []struct {
naziv string naziv string
prodajna float64 prodajna float64 // neto
cenaSaPdv float64 // bruto
stopa float64 stopa float64
ocekBezPdv float64 ocekBezPdv float64
ocekPdvIznos float64 ocekPdvIznos float64
}{ }{
{"stopa 20%", 120, 20, 100, 20}, {"stopa 20%", 100, 120, 20, 100, 20},
{"stopa 0%", 100, 0, 100, 0}, {"stopa 0%", 100, 100, 0, 100, 0},
{"stopa 10%", 110, 10, 100, 10}, {"stopa 10%", 100, 110, 10, 100, 10},
{"nula cena", 0, 20, 0, 0}, {"nula cena", 0, 0, 20, 0, 0},
} }
for _, tt := range testovi { for _, tt := range testovi {
t.Run(tt.naziv, func(t *testing.T) { 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) { if got := a.CenaBezPdv(); !jednako(got, tt.ocekBezPdv) {
t.Errorf("CenaBezPdv() = %v, očekivano %v", got, tt.ocekBezPdv) t.Errorf("CenaBezPdv() = %v, očekivano %v", got, tt.ocekBezPdv)
} }
+4
View File
@@ -0,0 +1,4 @@
ALTER TABLE artikli ADD COLUMN cena_sa_pdv REAL NOT NULL DEFAULT 0;
UPDATE artikli
SET cena_sa_pdv = ROUND(prodajna_cena * (1 + pdv_stopa / 100), 2);
+10 -3
View File
@@ -80,6 +80,7 @@
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Kategorija</th> <th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Kategorija</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-jak);">Količina</th> <th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-jak);">Količina</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-jak);">Prodajna</th> <th style="padding:12px 16px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-jak);">Prodajna</th>
{{if index .Moduli "pdv"}}<th style="padding:12px 16px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-jak);">Sa PDV-om</th>{{end}}
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Lokacija</th> <th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Lokacija</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-jak);">Akcije</th> <th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-jak);">Akcije</th>
</tr> </tr>
@@ -101,7 +102,8 @@
<span style="font-size:12px;color:var(--tekst-sporedni);">{{if eq .Tip "usluga"}}usluga{{else}}trošak{{end}}</span> <span style="font-size:12px;color:var(--tekst-sporedni);">{{if eq .Tip "usluga"}}usluga{{else}}trošak{{end}}</span>
{{end}} {{end}}
</td> </td>
<td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinariCeli .ProdajnaCena}} din</td> <td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .ProdajnaCena}} din</td>
{{if index $.Moduli "pdv"}}<td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .CenaSaPdv}} din</td>{{end}}
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);"> <td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
{{if .Lokacija}}{{.Lokacija}}{{else}}—{{end}} {{if .Lokacija}}{{.Lokacija}}{{else}}—{{end}}
</td> </td>
@@ -134,7 +136,7 @@
</tr> </tr>
{{else}} {{else}}
<tr> <tr>
<td colspan="7" style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);"> <td colspan="{{if index .Moduli "pdv"}}8{{else}}7{{end}}" style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
{{if or $.Filter.Pretraga $.KategorijaIDStr $.Filter.SamoKriticni}}Za datu pretragu nema stavki u bazi.{{else}}Nema stavki. <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/magacin/novi?tip={{$.Tip}}" style="color:var(--sb-akcent);">Dodaj prvu.</a>{{end}} {{if or $.Filter.Pretraga $.KategorijaIDStr $.Filter.SamoKriticni}}Za datu pretragu nema stavki u bazi.{{else}}Nema stavki. <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/magacin/novi?tip={{$.Tip}}" style="color:var(--sb-akcent);">Dodaj prvu.</a>{{end}}
</td> </td>
</tr> </tr>
@@ -192,8 +194,13 @@
{{end}} {{end}}
</div> </div>
<div class="pomocni-tekst"> <div class="pomocni-tekst">
<span style="color:var(--tekst-glavni);font-weight:500;">Cena:</span> {{dinariCeli .ProdajnaCena}} din <span style="color:var(--tekst-glavni);font-weight:500;">Cena:</span> {{dinari .ProdajnaCena}} din
</div> </div>
{{if index $.Moduli "pdv"}}
<div class="pomocni-tekst">
<span style="color:var(--tekst-glavni);font-weight:500;">Sa PDV:</span> {{dinari .CenaSaPdv}} din
</div>
{{end}}
{{if .Lokacija}} {{if .Lokacija}}
<div class="pomocni-tekst"> <div class="pomocni-tekst">
<span style="color:var(--tekst-glavni);font-weight:500;">Lokacija:</span> {{.Lokacija}} <span style="color:var(--tekst-glavni);font-weight:500;">Lokacija:</span> {{.Lokacija}}
+115 -54
View File
@@ -43,44 +43,42 @@
</div> </div>
</div> </div>
<!-- naziv --> <!-- naziv / jedinica mere / kategorija — u 1 redu -->
<div>
<label class="polje-labela">
Naziv <span style="color:#dc2626;">*</span>
</label>
<input type="text" name="naziv" value="{{.Artikal.Naziv}}"
placeholder="npr. RAM DDR4 8GB Kingston"
style="width:100%;">
</div>
<!-- Magacin je isključivo za proizvode (prate lager); usluge i troškovi <!-- Magacin je isključivo za proizvode (prate lager); usluge i troškovi
imaju svoje module, zato ovde tip više nije izbor --> imaju svoje module, zato ovde tip više nije izbor -->
<input type="hidden" name="tip" value="proizvod"> <input type="hidden" name="tip" value="proizvod">
<!-- jedinica mere --> <div style="display:grid;grid-template-columns:1fr 110px 1fr;gap:12px;">
<div> <div>
<label class="polje-labela">Jedinica mere</label> <label class="polje-labela">
{{$jm := .Artikal.JedinicaMere}} Naziv <span style="color:#dc2626;">*</span>
<select name="jedinica_mere" style="width:100%;"> </label>
<option value="kom" {{if eq $jm "kom"}}selected{{end}}>kom</option> <input type="text" name="naziv" value="{{.Artikal.Naziv}}"
<option value="sat" {{if eq $jm "sat"}}selected{{end}}>sat</option> placeholder="npr. RAM DDR4 8GB Kingston"
<option value="set" {{if eq $jm "set"}}selected{{end}}>set</option> style="width:100%;">
<option value="m" {{if eq $jm "m"}}selected{{end}}>m</option> </div>
<option value="m2" {{if eq $jm "m2"}}selected{{end}}></option> <div>
<option value="l" {{if eq $jm "l"}}selected{{end}}>l</option> <label class="polje-labela">Jed. mere</label>
<option value="kg" {{if eq $jm "kg"}}selected{{end}}>kg</option> {{$jm := .Artikal.JedinicaMere}}
<option value="pak" {{if eq $jm "pak"}}selected{{end}}>pak</option> <select name="jedinica_mere" style="width:100%;">
</select> <option value="kom" {{if eq $jm "kom"}}selected{{end}}>kom</option>
</div> <option value="sat" {{if eq $jm "sat"}}selected{{end}}>sat</option>
<option value="set" {{if eq $jm "set"}}selected{{end}}>set</option>
<!-- kategorija --> <option value="m" {{if eq $jm "m"}}selected{{end}}>m</option>
<div> <option value="m2" {{if eq $jm "m2"}}selected{{end}}></option>
<label class="polje-labela">Kategorija</label> <option value="l" {{if eq $jm "l"}}selected{{end}}>l</option>
<select name="kategorija_id" style="width:100%;"> <option value="kg" {{if eq $jm "kg"}}selected{{end}}>kg</option>
<option value="">— bez kategorije —</option> <option value="pak" {{if eq $jm "pak"}}selected{{end}}>pak</option>
{{range .Kategorije}} </select>
<option value="{{.ID}}" {{if eq (printf "%d" .ID) $.KategorijaIDStr}}selected{{end}}>{{.Naziv}}</option> </div>
{{end}} <div>
</select> <label class="polje-labela">Kategorija</label>
<select name="kategorija_id" style="width:100%;">
<option value="">— bez kategorije —</option>
{{range .Kategorije}}
<option value="{{.ID}}" {{if eq (printf "%d" .ID) $.KategorijaIDStr}}selected{{end}}>{{.Naziv}}</option>
{{end}}
</select>
</div>
</div> </div>
<!-- opis --> <!-- opis -->
@@ -103,15 +101,38 @@
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <!-- PDV stopa -->
{{if index .Moduli "pdv"}}
<div>
<label class="polje-labela">PDV stopa</label>
<select name="pdv_stopa" id="pdv-stopa"
style="width:100%;padding:8px 12px;background:var(--pozadina);border:0.5px solid var(--ivica);border-radius:8px;color:var(--tekst-glavni);font-size:14px;">
<option value="20" {{if eq .Artikal.PdvStopa 20.0}}selected{{end}}>Opšta stopa — 20%</option>
<option value="10" {{if eq .Artikal.PdvStopa 10.0}}selected{{end}}>Posebna stopa — 10%</option>
<option value="0" {{if eq .Artikal.PdvStopa 0.0}}selected{{end}}>Oslobođeno — 0%</option>
</select>
</div>
{{end}}
<!-- cene: nabavna / prodajna / sa PDV -->
<div style="display:grid;grid-template-columns:1fr 1fr{{if index .Moduli "pdv"}} 1fr{{end}};gap:12px;">
<div> <div>
<label class="polje-labela">Nabavna cena (din)</label> <label class="polje-labela">Nabavna cena (din)</label>
<input type="number" name="nabavna_cena" value="{{.Artikal.NabavnaCena}}" min="0" step="0.01" style="width:100%;"> <input type="number" id="nabavna-cena" name="nabavna_cena"
value="{{printf "%.2f" .Artikal.NabavnaCena}}" min="0" step="0.01" style="width:100%;">
</div> </div>
<div> <div>
<label class="polje-labela">Prodajna cena (din)</label> <label class="polje-labela">Prodajna cena (din)</label>
<input type="number" name="prodajna_cena" value="{{.Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;"> <input type="number" id="prodajna-cena" name="prodajna_cena"
value="{{printf "%.2f" .Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;">
</div> </div>
{{if index .Moduli "pdv"}}
<div>
<label class="polje-labela">Cena sa PDV-om (din)</label>
<input type="number" id="cena-sa-pdv" name="cena_sa_pdv"
value="{{printf "%.2f" .Artikal.CenaSaPdv}}" min="0" step="0.01" style="width:100%;">
</div>
{{end}}
</div> </div>
<!-- marža za kalkulaciju; prazno = nasleđuje maržu kategorije ili globalnu --> <!-- marža za kalkulaciju; prazno = nasleđuje maržu kategorije ili globalnu -->
@@ -184,26 +205,66 @@
}); });
} }
// dvosmerno povezivanje: nabavna + marža → prodajna, i prodajna → marža // dvosmerno povezivanje: nabavna + marža → prodajna, prodajna ↔ cena sa PDV
var nabavna = document.querySelector('[name="nabavna_cena"]'); var nabavna = document.getElementById('nabavna-cena');
var prodajna = document.querySelector('[name="prodajna_cena"]'); var prodajna = document.getElementById('prodajna-cena');
var marza = document.querySelector('[name="marza"]'); var saPdv = document.getElementById('cena-sa-pdv');
function broj(el) { return parseFloat(el.value) || 0; } var pdvSel = document.getElementById('pdv-stopa');
// postavljanje .value programski ne okida 'input', pa nema beskonačne petlje var marza = document.querySelector('[name="marza"]');
function pdv() { return parseFloat(pdvSel.value) || 0; }
function br(el) { return parseFloat(el.value) || 0; }
// prodajna → cena sa PDV
function izProdajne() { function izProdajne() {
var n = broj(nabavna); var p = br(prodajna);
if (n <= 0) return; if (p > 0) saPdv.value = (p * (1 + pdv() / 100)).toFixed(2);
prodajna.value = (n * (1 + broj(marza) / 100)).toFixed(2); izMarze();
} }
// cena sa PDV → prodajna
function izSaPdv() {
var s = br(saPdv);
if (s > 0) prodajna.value = (s / (1 + pdv() / 100)).toFixed(2);
izMarze();
}
// nabavna + marža → prodajna (i onda prodajna → sa PDV)
function izMarzeKalc() {
var n = br(nabavna), m = br(marza);
if (n > 0 && marza.value !== '') {
prodajna.value = (n * (1 + m / 100)).toFixed(2);
izProdajne();
}
}
// prodajna + nabavna → marža
function izMarze() { function izMarze() {
var n = broj(nabavna), p = broj(prodajna); var n = br(nabavna), p = br(prodajna);
if (n <= 0) { marza.value = ''; return; } if (n > 0) marza.value = ((p / n - 1) * 100).toFixed(2);
marza.value = ((p / n - 1) * 100).toFixed(2);
} }
if (nabavna && prodajna && marza) { // pri promeni PDV stope — preračunaj cena sa PDV iz prodajne
marza.addEventListener('input', izProdajne); function izPdvStope() {
nabavna.addEventListener('input', izProdajne); if (prodajna.value !== '') izProdajne();
prodajna.addEventListener('input', izMarze); }
if (nabavna && prodajna && saPdv && marza && pdvSel) {
prodajna.addEventListener('input', izProdajne);
saPdv.addEventListener('input', izSaPdv);
nabavna.addEventListener('input', izMarzeKalc);
marza.addEventListener('input', izMarzeKalc);
pdvSel.addEventListener('change', izPdvStope);
} else if (nabavna && prodajna && marza) {
// bez PDV modula — samo osnovna kalkulacija nabavna+marža↔prodajna
marza.addEventListener('input', function() {
var n = br(nabavna);
if (n > 0) prodajna.value = (n * (1 + br(marza) / 100)).toFixed(2);
});
nabavna.addEventListener('input', function() {
var n = br(nabavna);
if (n > 0 && marza.value !== '') prodajna.value = (n * (1 + br(marza) / 100)).toFixed(2);
});
prodajna.addEventListener('input', function() {
var n = br(nabavna);
if (n > 0) marza.value = ((br(prodajna) / n - 1) * 100).toFixed(2);
});
} }
})(); })();
</script> </script>