From 86cbace2134b258249b7b23ab4493eaad59295bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Fri, 19 Jun 2026 19:56:02 +0200 Subject: [PATCH] =?UTF-8?q?Izve=C5=A1taji:=20popis=20magacina=20(inventura?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nova stranica /izvestaji/popis — forma za unos stvarnog stanja - Razlika se prikazuje u realnom vremenu (JS) dok se kuca - Pri snimanju: samo izmenjene količine upisuju se kao korekcija u magacinske_promene sa napomenom (podrazumevano "Godišnji popis") - Nova metoda KorigujKolicinu u ArtikalRepository — transakciona, ažurira kolicina i upisuje promenu tipa korekcija - Link Popis (inventura) dodat na stranicu izveštaja --- cmd/ntech/main.go | 2 + internal/db/repository.go | 2 + internal/db/sqlite/artikal.go | 29 +++++ internal/handler/izvestaji.go | 94 ++++++++++++++++ internal/handler/kes.go | 2 +- web/templates/stranice/izvestaji.html | 1 + web/templates/stranice/popis.html | 153 ++++++++++++++++++++++++++ 7 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 web/templates/stranice/popis.html diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 7b8edac..5e07458 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -344,6 +344,8 @@ func main() { r.Get("/izvestaji", h.Izvestaji) r.Get("/izvestaji/prometni-list", h.PrometniListMagacina) r.Get("/izvestaji/stanje-zaliha", h.StanjeZalihaIzvestaj) + r.Get("/izvestaji/popis", h.Popis) + r.With(doz("artikal.izmeni")).Post("/izvestaji/popis", h.SacuvajPopis) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja", h.Prodaja) r.Get("/prodaja/nova", h.NovaProdaja) r.With(doz("prodaja.dodaj")).Post("/prodaja/nova", h.SacuvajProdaju) diff --git a/internal/db/repository.go b/internal/db/repository.go index bd18afa..94cee0d 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -19,6 +19,8 @@ type ArtikalRepository interface { Obrisi(ctx context.Context, id int64) error // SledecaSifra vraća predlog sledeće auto-šifre (npr. ART-00042) SledecaSifra(ctx context.Context) (string, error) + // KorigujKolicinu postavlja novu količinu artikla i upisuje korekciju u magacinske_promene + KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena string) error } // KategorijaRepository definiše operacije nad kategorijama diff --git a/internal/db/sqlite/artikal.go b/internal/db/sqlite/artikal.go index 6ee041c..ce3e5f0 100644 --- a/internal/db/sqlite/artikal.go +++ b/internal/db/sqlite/artikal.go @@ -229,3 +229,32 @@ func (r *ArtikalRepo) Obrisi(ctx context.Context, id int64) error { return nil } + +// KorigujKolicinu postavlja novu količinu i upisuje korekciju u magacinske_promene +func (r *ArtikalRepo) KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena string) error { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: begin: %w", err) + } + defer tx.Rollback() + + var staraCena float64 + var staraKolicina int + err = tx.QueryRowContext(ctx, "SELECT kolicina, nabavna_cena FROM artikli WHERE id = ?", artikalID). + Scan(&staraKolicina, &staraCena) + if err != nil { + return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: dohvati: %w", err) + } + + if _, err = tx.ExecContext(ctx, "UPDATE artikli SET kolicina = ? WHERE id = ?", novaKolicina, artikalID); err != nil { + return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: update: %w", err) + } + + promena := novaKolicina - staraKolicina + if err = zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaKorekcija, promena, + staraKolicina, novaKolicina, 0, korisnikID, napomena); err != nil { + return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: %w", err) + } + + return tx.Commit() +} diff --git a/internal/handler/izvestaji.go b/internal/handler/izvestaji.go index e87e2ae..95008ab 100644 --- a/internal/handler/izvestaji.go +++ b/internal/handler/izvestaji.go @@ -6,9 +6,12 @@ import ( "html/template" "log/slog" "net/http" + "strconv" "time" + appdbPkg "ntech/internal/db" "ntech/internal/db/sqlite" + "ntech/internal/middleware" "ntech/internal/model" ) @@ -295,3 +298,94 @@ func (h *Handler) StanjeZalihaIzvestaj(w http.ResponseWriter, r *http.Request) { BrojArtikala: len(zalihe), }) } + +// PodaciPopisa su podaci za stranicu popisa +type PodaciPopisa struct { + model.PodaciStranice + Artikli []model.ArtikalSaKategorijom + Sacuvano bool + Greska string +} + +// Popis prikazuje formu za unos stvarnog stanja (inventuru) +func (h *Handler) Popis(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "artikal.izmeni"); !ok { + return + } + + artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{}) + if err != nil { + http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError) + return + } + + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "izvestaji" + ps.NaslovStranice = "Popis" + + h.renderujTemplate(w, "popis", PodaciPopisa{ + PodaciStranice: ps, + Artikli: artikli, + Sacuvano: r.URL.Query().Get("sacuvano") == "1", + }) +} + +// SacuvajPopis prima POST formu sa prebrojenim količinama i upisuje korekcije +func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) { + k := middleware.KorisnikIzKonteksta(r.Context()) + if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "artikal.izmeni") { + http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{}) + if err != nil { + http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError) + return + } + + napomena := r.FormValue("napomena") + if napomena == "" { + napomena = "Godišnji popis" + } + + var greskaBroj int + for _, a := range artikli { + kljuc := fmt.Sprintf("kolicina_%d", a.ID) + vr := r.FormValue(kljuc) + if vr == "" { + continue + } + nova, err := strconv.Atoi(vr) + if err != nil || nova < 0 { + continue + } + if nova == a.Kolicina { + continue + } + if err := h.Artikli.KorigujKolicinu(r.Context(), a.ID, nova, &k.ID, napomena); err != nil { + slog.Error("popis: korekcija artikla", "id", a.ID, "error", err) + greskaBroj++ + } + } + + if greskaBroj > 0 { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "izvestaji" + ps.NaslovStranice = "Popis" + h.renderujTemplate(w, "popis", PodaciPopisa{ + PodaciStranice: ps, + Artikli: artikli, + Greska: fmt.Sprintf("Došlo je do greške pri upisivanju %d artikala.", greskaBroj), + }) + return + } + + http.Redirect(w, r, "/izvestaji/popis?sacuvano=1", http.StatusSeeOther) +} diff --git a/internal/handler/kes.go b/internal/handler/kes.go index 3b68b8d..8a05553 100644 --- a/internal/handler/kes.go +++ b/internal/handler/kes.go @@ -19,7 +19,7 @@ var saSidebar = []string{ "admin_korisnici", "admin_profil", "admin_login_istorija", "admin_dozvole", "dashboard", "dobavljaci", "dobavljac_forma", - "izvestaji", "prometni_list", "stanje_zaliha", + "izvestaji", "prometni_list", "stanje_zaliha", "popis", "kategorije", "klijenti", "klijent_forma", "magacin", "magacin_forma", "magacin_kartica", diff --git a/web/templates/stranice/izvestaji.html b/web/templates/stranice/izvestaji.html index 3ed14f1..9a919e7 100644 --- a/web/templates/stranice/izvestaji.html +++ b/web/templates/stranice/izvestaji.html @@ -26,6 +26,7 @@
Prometni list magacina Stanje zaliha + Popis (inventura)
diff --git a/web/templates/stranice/popis.html b/web/templates/stranice/popis.html new file mode 100644 index 0000000..ba153ab --- /dev/null +++ b/web/templates/stranice/popis.html @@ -0,0 +1,153 @@ +{{template "base" .}} + +{{define "naslov"}}Popis — NTech{{end}} + +{{define "dodatni-css"}} + +{{end}} + +{{define "sadrzaj"}} +
+ + {{if .Sacuvano}} +
Popis je uspešno sačuvan. Korekcije su upisane u magacinsku evidenciju.
+ {{end}} + {{if .Greska}} +
{{.Greska}}
+ {{end}} + + +
+
+
+
Godišnji popis magacina
+
+ Unesite stvarno prebrojano stanje za svaki artikal. Polja sa razlikom biće istaknuta narandžasto. + Samo artikli sa izmenjenom količinom biće korigovani — ostali ostaju nepromenjeni. +
+
+ ← Izveštaji +
+
+ + +
+ + +
+
+ Artikli +
+
+ + +
+ +
+
+ + {{if not .Artikli}} +
+ Nema artikala u magacinu. +
+ {{else}} +
+ + + + + + + + + + + + + {{range .Artikli}} + + + + + + + + + {{end}} + +
ArtikalŠifraKategorijaKnjižno stanjeStvarno stanjeRazlika
{{.Naziv}}{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}{{.Kolicina}} + + + +
+
+ + +
+ +
+ {{end}} +
+
+ +
+ + +{{end}}