From 84bb8aa158b960d83294e725b3e0b9586a0d372c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Fri, 26 Jun 2026 23:47:52 +0200 Subject: [PATCH] =?UTF-8?q?Cena=20sa=20PDV-om=20u=20magacinu=20+=20uslovno?= =?UTF-8?q?=20prikazivanje=20po=20modulu=20-=20migrations/087:=20nova=20ko?= =?UTF-8?q?lona=20cena=5Fsa=5Fpdv=20+=20popunjavanje=20postoje=C4=87ih=20-?= =?UTF-8?q?=20model.Artikal:=20polje=20CenaSaPdv,=20ispravljene=20CenaBezP?= =?UTF-8?q?dv/PdvIznos=20(neto)=20-=20sqlite/artikal.go:=20SELECT/Scan/INS?= =?UTF-8?q?ERT/UPDATE=20za=20cena=5Fsa=5Fpdv=20(6=20mesta)=20-=20magacin?= =?UTF-8?q?=5Fforma.go:=20pdv=5Fstopa=20iz=20forme,=20kalkulacija=20cena?= =?UTF-8?q?=5Fsa=5Fpdv,=20default=20PDV=2020%=20-=20magacin.html:=20dinari?= =?UTF-8?q?=20format=20(25.999,00),=20kolona=20Sa=20PDV-om,=20uslovno=20po?= =?UTF-8?q?=20Moduli.pdv=20-=20magacin=5Fforma.html:=20PDV=20dropdown=20+?= =?UTF-8?q?=20Cena=20sa=20PDV=20polje,=20uslovno=20po=20Moduli.pdv,=20opti?= =?UTF-8?q?mizovan=20layout=20(Naziv/Jed.=20mere/Kategorija=20u=201=20redu?= =?UTF-8?q?)=20-=20main.go:=20demo=20mod=20bez=20ke=C5=A1iranja=20=C5=A1ab?= =?UTF-8?q?lona=20(hot-reload)=20-=20model=5Ftest.go:=20a=C5=BEurirani=20t?= =?UTF-8?q?estovi=20za=20novo=20pona=C5=A1anje=20CenaBezPdv/PdvIznos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/ntech/main.go | 2 +- internal/db/sqlite/artikal.go | 20 +-- internal/handler/magacin_forma.go | 26 +++- internal/model/artikal.go | 7 +- internal/model/model_test.go | 13 +- migrations/087_artikal_cena_sa_pdv.sql | 4 + web/templates/stranice/magacin.html | 13 +- web/templates/stranice/magacin_forma.html | 169 +++++++++++++++------- 8 files changed, 176 insertions(+), 78 deletions(-) create mode 100644 migrations/087_artikal_cena_sa_pdv.sql diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 922ba93..31efa12 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -159,7 +159,7 @@ func main() { // čuva odabrani FS (disk ili embed) za hot-reload u razvoju i za keš u produkciji 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) if err != nil { slog.Error("Greška pri kreiranju keša šablona", "error", err) diff --git a/internal/db/sqlite/artikal.go b/internal/db/sqlite/artikal.go index 48b76fd..0bcc94b 100644 --- a/internal/db/sqlite/artikal.go +++ b/internal/db/sqlite/artikal.go @@ -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) diff --git a/internal/handler/magacin_forma.go b/internal/handler/magacin_forma.go index 0fef929..8293fe0 100644 --- a/internal/handler/magacin_forma.go +++ b/internal/handler/magacin_forma.go @@ -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, "" } diff --git a/internal/model/artikal.go b/internal/model/artikal.go index f0591b1..3b86cc8 100644 --- a/internal/model/artikal.go +++ b/internal/model/artikal.go @@ -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 diff --git a/internal/model/model_test.go b/internal/model/model_test.go index a8693fe..d47bdb4 100644 --- a/internal/model/model_test.go +++ b/internal/model/model_test.go @@ -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) } diff --git a/migrations/087_artikal_cena_sa_pdv.sql b/migrations/087_artikal_cena_sa_pdv.sql new file mode 100644 index 0000000..d13d385 --- /dev/null +++ b/migrations/087_artikal_cena_sa_pdv.sql @@ -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); diff --git a/web/templates/stranice/magacin.html b/web/templates/stranice/magacin.html index 49a4320..de4423a 100644 --- a/web/templates/stranice/magacin.html +++ b/web/templates/stranice/magacin.html @@ -80,6 +80,7 @@ Kategorija Količina Prodajna + {{if index .Moduli "pdv"}}Sa PDV-om{{end}} Lokacija Akcije @@ -101,7 +102,8 @@ {{if eq .Tip "usluga"}}usluga{{else}}trošak{{end}} {{end}} - {{dinariCeli .ProdajnaCena}} din + {{dinari .ProdajnaCena}} din + {{if index $.Moduli "pdv"}}{{dinari .CenaSaPdv}} din{{end}} {{if .Lokacija}}{{.Lokacija}}{{else}}—{{end}} @@ -134,7 +136,7 @@ {{else}} - + {{if or $.Filter.Pretraga $.KategorijaIDStr $.Filter.SamoKriticni}}Za datu pretragu nema stavki u bazi.{{else}}Nema stavki. Dodaj prvu.{{end}} @@ -192,8 +194,13 @@ {{end}}
- Cena: {{dinariCeli .ProdajnaCena}} din + Cena: {{dinari .ProdajnaCena}} din
+ {{if index $.Moduli "pdv"}} +
+ Sa PDV: {{dinari .CenaSaPdv}} din +
+ {{end}} {{if .Lokacija}}
Lokacija: {{.Lokacija}} diff --git a/web/templates/stranice/magacin_forma.html b/web/templates/stranice/magacin_forma.html index 0ee92ce..7bdbdec 100644 --- a/web/templates/stranice/magacin_forma.html +++ b/web/templates/stranice/magacin_forma.html @@ -43,44 +43,42 @@
- -
- - -
- + - -
- - {{$jm := .Artikal.JedinicaMere}} - -
- - -
- - +
+
+ + +
+
+ + {{$jm := .Artikal.JedinicaMere}} + +
+
+ + +
@@ -103,15 +101,38 @@
-
+ + {{if index .Moduli "pdv"}} +
+ + +
+ {{end}} + + +
- +
- +
+ {{if index .Moduli "pdv"}} +
+ + +
+ {{end}}
@@ -184,26 +205,66 @@ }); } - // dvosmerno povezivanje: nabavna + marža → prodajna, i prodajna → marža - var nabavna = document.querySelector('[name="nabavna_cena"]'); - var prodajna = document.querySelector('[name="prodajna_cena"]'); - var marza = document.querySelector('[name="marza"]'); - function broj(el) { return parseFloat(el.value) || 0; } - // postavljanje .value programski ne okida 'input', pa nema beskonačne petlje + // dvosmerno povezivanje: nabavna + marža → prodajna, prodajna ↔ cena sa PDV + var nabavna = document.getElementById('nabavna-cena'); + var prodajna = document.getElementById('prodajna-cena'); + var saPdv = document.getElementById('cena-sa-pdv'); + var pdvSel = document.getElementById('pdv-stopa'); + 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() { - var n = broj(nabavna); - if (n <= 0) return; - prodajna.value = (n * (1 + broj(marza) / 100)).toFixed(2); + var p = br(prodajna); + if (p > 0) saPdv.value = (p * (1 + pdv() / 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() { - var n = broj(nabavna), p = broj(prodajna); - if (n <= 0) { marza.value = ''; return; } - marza.value = ((p / n - 1) * 100).toFixed(2); + var n = br(nabavna), p = br(prodajna); + if (n > 0) marza.value = ((p / n - 1) * 100).toFixed(2); } - if (nabavna && prodajna && marza) { - marza.addEventListener('input', izProdajne); - nabavna.addEventListener('input', izProdajne); - prodajna.addEventListener('input', izMarze); + // pri promeni PDV stope — preračunaj cena sa PDV iz prodajne + function izPdvStope() { + if (prodajna.value !== '') izProdajne(); + } + + 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); + }); } })();