Merge develop → main: zalihe usklađenost (manjak/višak, storno nabavke, prosečna cena), popis štampa

This commit is contained in:
2026-06-30 17:09:53 +02:00
19 changed files with 885 additions and 133 deletions
+2 -1
View File
@@ -350,7 +350,7 @@ func main() {
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke/nova", h.NovaNabavka) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke/nova", h.NovaNabavka)
r.With(doz("nabavka.dodaj")).Post("/nabavke/nova", h.SacuvajNabavku) r.With(doz("nabavka.dodaj")).Post("/nabavke/nova", h.SacuvajNabavku)
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke/{id}", h.DetaljiNabavke) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke/{id}", h.DetaljiNabavke)
r.With(doz("nabavka.obrisi")).Post("/nabavke/obrisi/{id}", h.ObrisiNabavku) r.With(doz("nabavka.storno")).Post("/nabavke/storno/{id}", h.StornoNabavke)
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "dobavljac.pregled")).Get("/dobavljaci", h.Dobavljaci) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "dobavljac.pregled")).Get("/dobavljaci", h.Dobavljaci)
r.Get("/dobavljaci/novi", h.NoviDobavljac) r.Get("/dobavljaci/novi", h.NoviDobavljac)
r.With(doz("dobavljac.dodaj")).Post("/dobavljaci/novi", h.SacuvajDobavljaca) r.With(doz("dobavljac.dodaj")).Post("/dobavljaci/novi", h.SacuvajDobavljaca)
@@ -400,6 +400,7 @@ func main() {
r.Get("/izvestaji/stanje-zaliha", h.StanjeZalihaIzvestaj) r.Get("/izvestaji/stanje-zaliha", h.StanjeZalihaIzvestaj)
r.Get("/izvestaji/popis", h.Popis) r.Get("/izvestaji/popis", h.Popis)
r.With(doz("artikal.izmeni")).Post("/izvestaji/popis", h.SacuvajPopis) r.With(doz("artikal.izmeni")).Post("/izvestaji/popis", h.SacuvajPopis)
r.Get("/izvestaji/popis/stampa", h.PopisStampa)
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja", h.Prodaja) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja", h.Prodaja)
r.Get("/prodaja/nova", h.NovaProdaja) r.Get("/prodaja/nova", h.NovaProdaja)
r.With(doz("prodaja.dodaj")).Post("/prodaja/nova", h.SacuvajProdaju) r.With(doz("prodaja.dodaj")).Post("/prodaja/nova", h.SacuvajProdaju)
+3 -3
View File
@@ -29,8 +29,8 @@ type ArtikalRepository interface {
Vrati(ctx context.Context, id int64) error Vrati(ctx context.Context, id int64) error
// SledecaSifra vraća predlog sledeće auto-šifre (npr. KOMP-0042 ili ART-0042) // SledecaSifra vraća predlog sledeće auto-šifre (npr. KOMP-0042 ili ART-0042)
SledecaSifra(ctx context.Context, kategorijaID *int64) (string, error) SledecaSifra(ctx context.Context, kategorijaID *int64) (string, error)
// KorigujKolicinu postavlja novu količinu artikla i upisuje korekciju u magacinske_promene // KorigujKolicinu postavlja novu količinu artikla i upisuje promenu u magacinske_promene
KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena string) error KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena, tipPromene string) error
// DobavljaciArtikla vraća ID-jeve dobavljača vezanih za artikal // DobavljaciArtikla vraća ID-jeve dobavljača vezanih za artikal
DobavljaciArtikla(ctx context.Context, artikalID int64) ([]int64, error) DobavljaciArtikla(ctx context.Context, artikalID int64) ([]int64, error)
// PostaviDobavljaceArtikla zamenjuje skup dobavljača artikla datim ID-jevima // PostaviDobavljaceArtikla zamenjuje skup dobavljača artikla datim ID-jevima
@@ -152,7 +152,7 @@ type NabavkaRepository interface {
DohvatiStavke(ctx context.Context, nabavkaID int64) ([]model.StavkaSaArtiklom, error) DohvatiStavke(ctx context.Context, nabavkaID int64) ([]model.StavkaSaArtiklom, error)
DohvatiTroskove(ctx context.Context, nabavkaID int64) ([]model.NabavkaTrosak, error) DohvatiTroskove(ctx context.Context, nabavkaID int64) ([]model.NabavkaTrosak, error)
Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke, troskovi []model.NabavkaTrosak, korisnikID *int64) (int64, error) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke, troskovi []model.NabavkaTrosak, korisnikID *int64) (int64, error)
Obrisi(ctx context.Context, id int64, korisnikID *int64) error Storno(ctx context.Context, id int64, razlog string, korisnikID *int64) error
} }
// DobavljacRepository definiše operacije nad dobavljačima // DobavljacRepository definiše operacije nad dobavljačima
+4 -3
View File
@@ -397,8 +397,9 @@ func (r *ArtikalRepo) SveDobavljaceArtikala(ctx context.Context) (map[int64][]in
return mapa, nil return mapa, nil
} }
// KorigujKolicinu postavlja novu količinu i upisuje korekciju u magacinske_promene // KorigujKolicinu postavlja novu količinu i upisuje promenu u magacinske_promene;
func (r *ArtikalRepo) KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena string) error { // tipPromene određuje vrstu traga (korekcija, manjak_popis, visak_popis)
func (r *ArtikalRepo) KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena, tipPromene string) error {
tx, err := r.db.BeginTx(ctx, nil) tx, err := r.db.BeginTx(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: begin: %w", err) return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: begin: %w", err)
@@ -418,7 +419,7 @@ func (r *ArtikalRepo) KorigujKolicinu(ctx context.Context, artikalID int64, nova
} }
promena := novaKolicina - staraKolicina promena := novaKolicina - staraKolicina
if err = zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaKorekcija, promena, if err = zabeleziMagacinPromenu(ctx, tx, artikalID, tipPromene, promena,
staraKolicina, novaKolicina, 0, korisnikID, napomena); err != nil { staraKolicina, novaKolicina, 0, korisnikID, napomena); err != nil {
return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: %w", err) return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: %w", err)
} }
+53 -23
View File
@@ -22,7 +22,8 @@ func NoviNabavkaRepo(db *sql.DB) *NabavkaRepo {
func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error) { func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error) {
redovi, err := r.db.QueryContext(ctx, ` redovi, err := r.db.QueryContext(ctx, `
SELECT SELECT
n.id, n.dobavljac_id, n.napomena, n.ukupno, n.metod_raspodele, n.datum, n.id, n.dobavljac_id, n.napomena, n.ukupno, n.metod_raspodele,
n.stornirano, n.razlog_storniranja, n.datum,
COALESCE(d.naziv, '') AS dobavljac_naziv COALESCE(d.naziv, '') AS dobavljac_naziv
FROM nabavke n FROM nabavke n
LEFT JOIN dobavljaci d ON n.dobavljac_id = d.id LEFT JOIN dobavljaci d ON n.dobavljac_id = d.id
@@ -36,10 +37,11 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err
for redovi.Next() { for redovi.Next() {
var n model.NabavkaSaDetaljem var n model.NabavkaSaDetaljem
var dobavljacID sql.NullInt64 var dobavljacID sql.NullInt64
var napomena, metod sql.NullString var napomena, metod, razlogStorniranja sql.NullString
err := redovi.Scan( err := redovi.Scan(
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod, &n.Datum, &n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod,
&n.Stornirano, &razlogStorniranja, &n.Datum,
&n.DobavljacNaziv, &n.DobavljacNaziv,
) )
if err != nil { if err != nil {
@@ -51,6 +53,7 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err
} }
n.Napomena = napomena.String n.Napomena = napomena.String
n.MetodRaspodele = metod.String n.MetodRaspodele = metod.String
n.RazlogStorniranja = razlogStorniranja.String
rezultat = append(rezultat, n) rezultat = append(rezultat, n)
} }
@@ -62,12 +65,14 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err
func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error) { func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error) {
var n model.Nabavka var n model.Nabavka
var dobavljacID sql.NullInt64 var dobavljacID sql.NullInt64
var napomena, metod sql.NullString var napomena, metod, razlogStorniranja sql.NullString
err := r.db.QueryRowContext(ctx, ` err := r.db.QueryRowContext(ctx, `
SELECT id, dobavljac_id, napomena, ukupno, metod_raspodele, datum SELECT id, dobavljac_id, napomena, ukupno, metod_raspodele,
stornirano, razlog_storniranja, datum
FROM nabavke WHERE id = ?`, id).Scan( FROM nabavke WHERE id = ?`, id).Scan(
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod, &n.Datum, &n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod,
&n.Stornirano, &razlogStorniranja, &n.Datum,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiID: %w", err) return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiID: %w", err)
@@ -78,6 +83,7 @@ func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka,
} }
n.Napomena = napomena.String n.Napomena = napomena.String
n.MetodRaspodele = metod.String n.MetodRaspodele = metod.String
n.RazlogStorniranja = razlogStorniranja.String
return &n, nil return &n, nil
} }
@@ -194,17 +200,26 @@ func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []mo
} }
var stanjePre int var stanjePre int
var staraNabavnaCena float64
err = tx.QueryRowContext(ctx, err = tx.QueryRowContext(ctx,
"SELECT kolicina FROM artikli WHERE id = ?", s.ArtikalID, "SELECT kolicina, nabavna_cena FROM artikli WHERE id = ?", s.ArtikalID,
).Scan(&stanjePre) ).Scan(&stanjePre, &staraNabavnaCena)
if err != nil { if err != nil {
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: dohvati stanje: %w", err) return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: dohvati stanje: %w", err)
} }
// ponderisana prosečna nabavna cena (MRS 2)
stanjePosle := stanjePre + s.Kolicina stanjePosle := stanjePre + s.Kolicina
var novaProsecna float64
if stanjePosle > 0 {
novaProsecna = (float64(stanjePre)*staraNabavnaCena + float64(s.Kolicina)*s.CenaPoKomadu) / float64(stanjePosle)
} else {
novaProsecna = s.CenaPoKomadu
}
_, err = tx.ExecContext(ctx, _, err = tx.ExecContext(ctx,
"UPDATE artikli SET kolicina = ? WHERE id = ?", "UPDATE artikli SET kolicina = ?, nabavna_cena = ? WHERE id = ?",
stanjePosle, s.ArtikalID, stanjePosle, novaProsecna, s.ArtikalID,
) )
if err != nil { if err != nil {
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: update kolicina: %w", err) return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: update kolicina: %w", err)
@@ -223,19 +238,31 @@ func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []mo
return nabavkaID, nil return nabavkaID, nil
} }
// Obrisi briše nabavku po ID-u, vraća količine artikala na stanje i beleži korekciju u magacinski trag. // Storno označava nabavku kao storniranu, vraća količine artikala na stanje i beleži
func (r *NabavkaRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) error { // korekciju u magacinski trag. Nabavna cena se NE računa unazad — ostaje kakva jeste.
func (r *NabavkaRepo) Storno(ctx context.Context, id int64, razlog string, korisnikID *int64) error {
tx, err := r.db.BeginTx(ctx, nil) tx, err := r.db.BeginTx(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: begin: %w", err) return fmt.Errorf("ntech: NabavkaRepo.Storno: begin: %w", err)
} }
defer tx.Rollback() defer tx.Rollback()
// učitaj stavke pre brisanja (CASCADE ih briše zajedno sa nabavkom) // provera da li je već stornirano
var stornirano bool
if err := tx.QueryRowContext(ctx,
"SELECT stornirano FROM nabavke WHERE id = ?", id,
).Scan(&stornirano); err != nil {
return fmt.Errorf("ntech: NabavkaRepo.Storno: provera: %w", err)
}
if stornirano {
return fmt.Errorf("ntech: NabavkaRepo.Storno: nabavka je već stornirana")
}
// učitaj stavke
redovi, err := tx.QueryContext(ctx, redovi, err := tx.QueryContext(ctx,
"SELECT artikal_id, kolicina FROM stavke_nabavke WHERE nabavka_id = ?", id) "SELECT artikal_id, kolicina FROM stavke_nabavke WHERE nabavka_id = ?", id)
if err != nil { if err != nil {
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: dohvati stavke: %w", err) return fmt.Errorf("ntech: NabavkaRepo.Storno: dohvati stavke: %w", err)
} }
type stavka struct { type stavka struct {
artikalID int64 artikalID int64
@@ -246,7 +273,7 @@ func (r *NabavkaRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) e
var s stavka var s stavka
if err := redovi.Scan(&s.artikalID, &s.kolicina); err != nil { if err := redovi.Scan(&s.artikalID, &s.kolicina); err != nil {
redovi.Close() redovi.Close()
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: scan stavka: %w", err) return fmt.Errorf("ntech: NabavkaRepo.Storno: scan stavka: %w", err)
} }
stavke = append(stavke, s) stavke = append(stavke, s)
} }
@@ -258,7 +285,7 @@ func (r *NabavkaRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) e
if err := tx.QueryRowContext(ctx, if err := tx.QueryRowContext(ctx,
"SELECT kolicina FROM artikli WHERE id = ?", s.artikalID, "SELECT kolicina FROM artikli WHERE id = ?", s.artikalID,
).Scan(&stanjePre); err != nil { ).Scan(&stanjePre); err != nil {
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: dohvati stanje: %w", err) return fmt.Errorf("ntech: NabavkaRepo.Storno: dohvati stanje: %w", err)
} }
stanjePosle := stanjePre - s.kolicina stanjePosle := stanjePre - s.kolicina
if stanjePosle < 0 { if stanjePosle < 0 {
@@ -267,20 +294,23 @@ func (r *NabavkaRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) e
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, s.artikalID, "UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, s.artikalID,
); err != nil { ); err != nil {
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: update stanje: %w", err) return fmt.Errorf("ntech: NabavkaRepo.Storno: update stanje: %w", err)
} }
if err := zabeleziMagacinPromenu(ctx, tx, s.artikalID, model.PromenaKorekcija, if err := zabeleziMagacinPromenu(ctx, tx, s.artikalID, model.PromenaKorekcija,
-s.kolicina, stanjePre, stanjePosle, id, korisnikID, "brisanje nabavke"); err != nil { -s.kolicina, stanjePre, stanjePosle, id, korisnikID, "storno nabavke: "+razlog); err != nil {
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: magacin: %w", err) return fmt.Errorf("ntech: NabavkaRepo.Storno: magacin: %w", err)
} }
} }
if _, err := tx.ExecContext(ctx, "DELETE FROM nabavke WHERE id = ?", id); err != nil { if _, err := tx.ExecContext(ctx,
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: delete: %w", err) "UPDATE nabavke SET stornirano = 1, razlog_storniranja = ? WHERE id = ?",
nullString(razlog), id,
); err != nil {
return fmt.Errorf("ntech: NabavkaRepo.Storno: update nabavka: %w", err)
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: commit: %w", err) return fmt.Errorf("ntech: NabavkaRepo.Storno: commit: %w", err)
} }
return nil return nil
} }
+190 -7
View File
@@ -316,9 +316,12 @@ func (h *Handler) StanjeZalihaIzvestaj(w http.ResponseWriter, r *http.Request) {
// PodaciPopisa su podaci za stranicu popisa // PodaciPopisa su podaci za stranicu popisa
type PodaciPopisa struct { type PodaciPopisa struct {
model.PodaciStranice model.PodaciStranice
Artikli []model.ArtikalSaKategorijom Artikli []model.ArtikalSaKategorijom
Sacuvano bool Sacuvano bool
Greska string Greska string
TipPopisa string // godišnji | vanredni | preuzimanje
TipPopisaNaziv string // „Godišnji popis", „Vanredni popis", „Popis pri preuzimanju dužnosti"
DatumPopisa string // formatiran datum za prikaz
} }
// Popis prikazuje formu za unos stvarnog stanja (inventuru) // Popis prikazuje formu za unos stvarnog stanja (inventuru)
@@ -327,7 +330,7 @@ func (h *Handler) Popis(w http.ResponseWriter, r *http.Request) {
return return
} }
artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{}) artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{Tip: model.TipProizvod})
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
return return
@@ -337,14 +340,30 @@ func (h *Handler) Popis(w http.ResponseWriter, r *http.Request) {
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "izvestaji" ps.Stranica = "izvestaji"
ps.NaslovStranice = "Popis" ps.NaslovStranice = "Popis"
tp := "godišnji"
h.renderujTemplate(w, "popis", PodaciPopisa{ h.renderujTemplate(w, "popis", PodaciPopisa{
PodaciStranice: ps, PodaciStranice: ps,
Artikli: artikli, Artikli: artikli,
Sacuvano: r.URL.Query().Get("sacuvano") == "1", Sacuvano: r.URL.Query().Get("sacuvano") == "1",
TipPopisa: tp,
TipPopisaNaziv: tipPopisaNaziv(tp),
DatumPopisa: time.Now().Format("02.01.2006."),
}) })
} }
// tipPopisaNaziv vraća lokalizovani naziv za tip popisa
func tipPopisaNaziv(tp string) string {
switch tp {
case "vanredni":
return "Vanredni popis"
case "preuzimanje":
return "Popis pri preuzimanju dužnosti"
default:
return "Godišnji popis"
}
}
// SacuvajPopis prima POST formu sa prebrojenim količinama i upisuje korekcije // SacuvajPopis prima POST formu sa prebrojenim količinama i upisuje korekcije
func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) k := middleware.KorisnikIzKonteksta(r.Context())
@@ -357,15 +376,26 @@ func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
return return
} }
artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{}) artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{Tip: model.TipProizvod})
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
return return
} }
tipPopisa := r.FormValue("tip_popisa")
if tipPopisa == "" {
tipPopisa = "godišnji"
}
napomena := r.FormValue("napomena") napomena := r.FormValue("napomena")
if napomena == "" { if napomena == "" {
napomena = "Godišnji popis" switch tipPopisa {
case "vanredni":
napomena = "Vanredni popis"
case "preuzimanje":
napomena = "Popis pri preuzimanju dužnosti"
default:
napomena = "Godišnji popis"
}
} }
var greskaBroj int var greskaBroj int
@@ -383,7 +413,11 @@ func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
continue continue
} }
povecano := nova > a.Kolicina povecano := nova > a.Kolicina
if err := h.Artikli.KorigujKolicinu(r.Context(), a.ID, nova, &k.ID, napomena); err != nil { tipPromene := model.PromenaManjak
if povecano {
tipPromene = model.PromenaVisak
}
if err := h.Artikli.KorigujKolicinu(r.Context(), a.ID, nova, &k.ID, napomena, tipPromene); err != nil {
slog.Error("popis: korekcija artikla", "id", a.ID, "error", err) slog.Error("popis: korekcija artikla", "id", a.ID, "error", err)
greskaBroj++ greskaBroj++
continue continue
@@ -413,9 +447,158 @@ func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
PodaciStranice: ps, PodaciStranice: ps,
Artikli: artikli, Artikli: artikli,
Greska: fmt.Sprintf("Došlo je do greške pri upisivanju %d artikala.", greskaBroj), Greska: fmt.Sprintf("Došlo je do greške pri upisivanju %d artikala.", greskaBroj),
TipPopisa: tipPopisa,
TipPopisaNaziv: tipPopisaNaziv(tipPopisa),
DatumPopisa: time.Now().Format("02.01.2006."),
}) })
return return
} }
http.Redirect(w, r, "/izvestaji/popis?sacuvano=1", http.StatusSeeOther) http.Redirect(w, r, "/izvestaji/popis?sacuvano=1", http.StatusSeeOther)
} }
// PodaciPopisStampa su podaci za štampu popisa (standalone template)
type PodaciPopisStampa struct {
NazivFirme string
KorisnikIme string
TipPopisaNaziv string
DatumPopisa string
Redovi []PopisStampaRed
UkupnoKnjiznoKol int
UkupnoKnjiznoVredStr string
UkupnoStvarnoKol int
UkupnoRazlikaKolStr string
UkupnoRazlikaVredStr string
ManjakKol int
ManjakVredStr string
VisakKol int
VisakVredStr string
}
// PopisStampaRed je jedan red u štampi popisa
type PopisStampaRed struct {
Naziv string
Sifra string
KategorijaNaziv string
JedinicaMere string
NabavnaCenaStr string
Kolicina int
KnjiznaVrednostStr string
StvarnaKolicina int
Razlika int
RazlikaStr string
RazlikaVrednost float64
RazlikaVrednostStr string
}
// PopisStampa prikazuje standalone stranicu za štampu popisa
func (h *Handler) PopisStampa(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{Tip: model.TipProizvod})
if err != nil {
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
return
}
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
tip := "godišnji"
var redovi []PopisStampaRed
var ukKnjKol, ukStvKol, ukRazKol, manjakKol, visakKol int
var ukKnjVred, ukRazVred, manjakVred, visakVred float64
for _, a := range artikli {
knjKol := a.Kolicina
cena := a.NabavnaCena
knjVred := cena * float64(knjKol)
stvKol := knjKol
razlika := stvKol - knjKol
razlikaVred := cena * float64(razlika)
ukKnjKol += knjKol
ukKnjVred += knjVred
ukStvKol += stvKol
ukRazKol += razlika
ukRazVred += razlikaVred
if razlika < 0 {
manjakKol += -razlika
manjakVred += -razlikaVred
} else if razlika > 0 {
visakKol += razlika
visakVred += razlikaVred
}
jm := a.JedinicaMere
if jm == "" {
jm = "kom"
}
razlikaStr := "—"
if razlika > 0 {
razlikaStr = fmt.Sprintf("+%d", razlika)
} else if razlika < 0 {
razlikaStr = fmt.Sprintf("%d", razlika)
}
razlikaVredStr := "—"
if razlikaVred != 0 {
predznak := ""
if razlikaVred > 0 {
predznak = "+"
}
razlikaVredStr = predznak + formatirajDinare(razlikaVred, 2)
}
redovi = append(redovi, PopisStampaRed{
Naziv: a.Naziv,
Sifra: a.Sifra,
KategorijaNaziv: a.KategorijaNaziv,
JedinicaMere: jm,
NabavnaCenaStr: formatirajDinare(cena, 0),
Kolicina: knjKol,
KnjiznaVrednostStr: formatirajDinare(knjVred, 2),
StvarnaKolicina: stvKol,
Razlika: razlika,
RazlikaStr: razlikaStr,
RazlikaVrednost: razlikaVred,
RazlikaVrednostStr: razlikaVredStr,
})
}
ukRazKolStr := "—"
if ukRazKol > 0 {
ukRazKolStr = fmt.Sprintf("+%d", ukRazKol)
} else if ukRazKol < 0 {
ukRazKolStr = fmt.Sprintf("%d", ukRazKol)
}
ukRazVredStr := "—"
if ukRazVred != 0 {
predznak := ""
if ukRazVred > 0 {
predznak = "+"
}
ukRazVredStr = predznak + formatirajDinare(ukRazVred, 2)
}
h.renderujStandalone(w, "popis_stampa", PodaciPopisStampa{
NazivFirme: podesavanja["naziv_firme"],
KorisnikIme: middleware.KorisnikIzKonteksta(r.Context()).KorisnickoIme,
TipPopisaNaziv: tipPopisaNaziv(tip),
DatumPopisa: time.Now().Format("02.01.2006."),
Redovi: redovi,
UkupnoKnjiznoKol: ukKnjKol,
UkupnoKnjiznoVredStr: formatirajDinare(ukKnjVred, 2),
UkupnoStvarnoKol: ukStvKol,
UkupnoRazlikaKolStr: ukRazKolStr,
UkupnoRazlikaVredStr: ukRazVredStr,
ManjakKol: manjakKol,
ManjakVredStr: formatirajDinare(manjakVred, 2),
VisakKol: visakKol,
VisakVredStr: formatirajDinare(visakVred, 2),
})
}
+1 -1
View File
@@ -43,7 +43,7 @@ var saSidebar = []string{
// standalone su šabloni bez base layouta // standalone su šabloni bez base layouta
var standaloneIme = []string{ var standaloneIme = []string{
"prijava", "setup", "totp_provera", "prodaja_stampa", "servis_radni_nalog", "servis_otpremnica", "servis_revers", "servis_predracun", "servis_nalepnica", "servis_status_javni", "servis_garantni_list", "fiskal_verifikacija", "prijava", "setup", "totp_provera", "prodaja_stampa", "servis_radni_nalog", "servis_otpremnica", "servis_revers", "servis_predracun", "servis_nalepnica", "servis_status_javni", "servis_garantni_list", "fiskal_verifikacija", "popis_stampa",
} }
// sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima. // sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima.
+1 -1
View File
@@ -264,7 +264,7 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
// ako se količina promenila, koriguj stanje uz magacinski trag // ako se količina promenila, koriguj stanje uz magacinski trag
if novaKolicina != staraKolicina { if novaKolicina != staraKolicina {
if e := h.Artikli.KorigujKolicinu(r.Context(), id, novaKolicina, &k.ID, "izmena artikla"); e != nil { if e := h.Artikli.KorigujKolicinu(r.Context(), id, novaKolicina, &k.ID, "izmena artikla", model.PromenaKorekcija); e != nil {
slog.Error("korekcija količine pri izmeni artikla nije uspela", "artikal_id", id, "error", e) slog.Error("korekcija količine pri izmeni artikla nije uspela", "artikal_id", id, "error", e)
} }
} }
+22 -19
View File
@@ -43,6 +43,7 @@ type PodaciDetaljiNabavke struct {
Troskovi []model.NabavkaTrosak Troskovi []model.NabavkaTrosak
UkupanTrosak float64 UkupanTrosak float64
DobavljacNaziv string DobavljacNaziv string
Sacuvano bool
} }
// artikalUJSON pretvara listu artikala u template.JS vrednost bezbednu za umetanje u <script> tag // artikalUJSON pretvara listu artikala u template.JS vrednost bezbednu za umetanje u <script> tag
@@ -208,20 +209,14 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
} }
} }
// kalkulacija: ažuriraj nabavnu i prodajnu cenu artikla iz forme + nivelacioni trag. // kalkulacija: ažuriraj prodajnu cenu artikla iz forme + nivelacioni trag.
// nabavna cena je već ponderisana prosečna (postavlja je Kreiraj) — ne preglašava se.
// prodajna[] je paralelni niz uz stavke (isti redosled kao artikal_id[]). // prodajna[] je paralelni niz uz stavke (isti redosled kao artikal_id[]).
prodajne := r.Form["prodajna[]"] prodajne := r.Form["prodajna[]"]
var korisnikID *int64 var korisnikID *int64
if k != nil { if k != nil {
korisnikID = &k.ID korisnikID = &k.ID
} }
// kalkulativna nabavna cena po stavci (fakturna + raspodeljeni zavisni trošak) —
// računa se na serveru; bez troškova je jednaka čistoj ceni po komadu.
var ukupanTrosak float64
for _, t := range troskovi {
ukupanTrosak += t.Iznos
}
kalkNabavna := model.RasporediTroskove(stavke, ukupanTrosak, nabavka.MetodRaspodele)
for i, s := range stavke { for i, s := range stavke {
if i >= len(prodajne) { if i >= len(prodajne) {
break break
@@ -230,12 +225,13 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
if e != nil || prodajna <= 0 { if e != nil || prodajna <= 0 {
continue // prazno/nula ne sme da pregazi postojeću cenu continue // prazno/nula ne sme da pregazi postojeću cenu
} }
// stara prodajna cena — za nivelacioni zapis // stara prodajna i tekuća (ponderisana) nabavna — nabavnu zadržavamo
var staraProdajna float64 var staraProdajna, nabavna float64
if a, e := h.Artikli.DohvatiID(r.Context(), s.ArtikalID); e == nil { if a, e := h.Artikli.DohvatiID(r.Context(), s.ArtikalID); e == nil {
staraProdajna = a.ProdajnaCena staraProdajna = a.ProdajnaCena
nabavna = a.NabavnaCena
} }
if e := h.Artikli.AzurirajCene(r.Context(), s.ArtikalID, kalkNabavna[i], prodajna); e != nil { if e := h.Artikli.AzurirajCene(r.Context(), s.ArtikalID, nabavna, prodajna); e != nil {
slog.Error("kalkulacija: ažuriranje cena nije uspelo", "artikal_id", s.ArtikalID, "error", e) slog.Error("kalkulacija: ažuriranje cena nije uspelo", "artikal_id", s.ArtikalID, "error", e)
continue continue
} }
@@ -333,14 +329,15 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
Troskovi: troskovi, Troskovi: troskovi,
UkupanTrosak: ukupanTrosak, UkupanTrosak: ukupanTrosak,
DobavljacNaziv: dobavljacNaziv, DobavljacNaziv: dobavljacNaziv,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
} }
h.renderujTemplate(w, "nabavka_detalji", podaci) h.renderujTemplate(w, "nabavka_detalji", podaci)
} }
// ObrisiNabavku prima POST zahtev i briše nabavku po ID-u // StornoNabavke prima POST zahtev i stornira nabavku po ID-u (umesto fizičkog brisanja)
func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) { func (h *Handler) StornoNabavke(w http.ResponseWriter, r *http.Request) {
k, ok := h.zahtevajDozvolu(w, r, "nabavka.obrisi") k, ok := h.zahtevajDozvolu(w, r, "nabavka.storno")
if !ok { if !ok {
return return
} }
@@ -349,17 +346,23 @@ func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Neispravan ID nabavke", http.StatusBadRequest) http.Error(w, "Neispravan ID nabavke", http.StatusBadRequest)
return return
} }
if err := r.ParseForm(); err != nil {
if err := h.NabavkeRepo.Obrisi(r.Context(), id, &k.ID); err != nil { http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
http.Error(w, "Greška pri brisanju nabavke", http.StatusInternalServerError)
return return
} }
// ukloni vezani auto-KPR zapis (ako ga je ova nabavka kreirala) razlog := strings.TrimSpace(r.FormValue("razlog"))
if err := h.NabavkeRepo.Storno(r.Context(), id, razlog, &k.ID); err != nil {
slog.Error("greška pri storniranju nabavke", "error", err)
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
return
}
// stornirana nabavka ne ulazi u PDV — ukloni vezani auto-KPR zapis
if err := h.PdvKprRepo.ObrisiPoIzvoru(r.Context(), "nabavka", id); err != nil { if err := h.PdvKprRepo.ObrisiPoIzvoru(r.Context(), "nabavka", id); err != nil {
slog.Error("brisanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err) slog.Error("brisanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err)
} }
http.Redirect(w, r, "/nabavke?obrisan=1", http.StatusSeeOther) http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
} }
// parseFormuNabavke čita zaglavlje, stavke i zavisne troškove iz HTTP forme // parseFormuNabavke čita zaglavlje, stavke i zavisne troškove iz HTTP forme
+2 -1
View File
@@ -15,6 +15,7 @@ var sveAkcije = []string{
"nabavka.pregled", "nabavka.pregled",
"nabavka.dodaj", "nabavka.dodaj",
"nabavka.obrisi", "nabavka.obrisi",
"nabavka.storno",
"dobavljac.pregled", "dobavljac.pregled",
"dobavljac.dodaj", "dobavljac.dodaj",
"dobavljac.izmeni", "dobavljac.izmeni",
@@ -66,7 +67,7 @@ func ImaDozvolu(uloga, akcija string) bool {
case "kategorija.pregled", "kategorija.dodaj", "kategorija.izmeni", "kategorija.obrisi": case "kategorija.pregled", "kategorija.dodaj", "kategorija.izmeni", "kategorija.obrisi":
return true return true
// nabavka // nabavka
case "nabavka.pregled", "nabavka.dodaj", "nabavka.obrisi": case "nabavka.pregled", "nabavka.dodaj", "nabavka.obrisi", "nabavka.storno":
return true return true
// dobavljač // dobavljač
case "dobavljac.pregled", "dobavljac.dodaj", case "dobavljac.pregled", "dobavljac.dodaj",
+2
View File
@@ -9,6 +9,8 @@ const (
PromenaIzlazServis = "izlaz_servis" PromenaIzlazServis = "izlaz_servis"
PromenaPovracaj = "povracaj" PromenaPovracaj = "povracaj"
PromenaKorekcija = "korekcija" PromenaKorekcija = "korekcija"
PromenaManjak = "manjak_popis"
PromenaVisak = "visak_popis"
) )
// MagacinskaPromenaSaDetaljem je promena stanja artikla sa nazivom artikla // MagacinskaPromenaSaDetaljem je promena stanja artikla sa nazivom artikla
+8 -6
View File
@@ -7,12 +7,14 @@ import (
// Nabavka predstavlja zaglavlje jedne nabavke // Nabavka predstavlja zaglavlje jedne nabavke
type Nabavka struct { type Nabavka struct {
ID int64 ID int64
DobavljacID *int64 DobavljacID *int64
Napomena string Napomena string
Ukupno float64 Ukupno float64
MetodRaspodele string // "vrednost" ili "kolicina"; prazno = nema zavisnih troškova MetodRaspodele string // "vrednost" ili "kolicina"; prazno = nema zavisnih troškova
Datum time.Time Stornirano bool
RazlogStorniranja string
Datum time.Time
} }
// NabavkaTrosak je jedna stavka zavisnih troškova nabavke (npr. prevoz, carina) // NabavkaTrosak je jedna stavka zavisnih troškova nabavke (npr. prevoz, carina)
+25
View File
@@ -0,0 +1,25 @@
-- Proširuje CHECK constraint na magacinske_promene da uključi manjak_popis i visak_popis
-- SQLite ne podržava ALTER TABLE za CHECK, pa rekreiramo tabelu
PRAGMA foreign_keys=OFF;
CREATE TABLE magacinske_promene_nova (
id INTEGER PRIMARY KEY AUTOINCREMENT,
artikal_id INTEGER NOT NULL REFERENCES artikli(id) ON DELETE RESTRICT,
tip_promene TEXT NOT NULL CHECK (tip_promene IN (
'ulaz_nabavka', 'izlaz_prodaja', 'izlaz_servis',
'povracaj', 'korekcija', 'manjak_popis', 'visak_popis'
)),
referentni_id INTEGER NOT NULL,
promena_kolicine INTEGER NOT NULL,
stanje_pre INTEGER NOT NULL,
stanje_posle INTEGER NOT NULL,
korisnik_id INTEGER REFERENCES korisnici(id) ON DELETE SET NULL,
napomena TEXT,
datum DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO magacinske_promene_nova SELECT * FROM magacinske_promene;
DROP TABLE magacinske_promene;
ALTER TABLE magacinske_promene_nova RENAME TO magacinske_promene;
PRAGMA foreign_keys=ON;
+3
View File
@@ -0,0 +1,3 @@
-- Nabavke — storniranje umesto fizičkog brisanja (računovodstveni tretman ulaznih dokumenata)
ALTER TABLE nabavke ADD COLUMN stornirano INTEGER NOT NULL DEFAULT 0;
ALTER TABLE nabavke ADD COLUMN razlog_storniranja TEXT;
@@ -129,6 +129,10 @@
<span class="bedz" style="background:rgba(168,85,247,0.12);color:#a855f7;">Povraćaj</span> <span class="bedz" style="background:rgba(168,85,247,0.12);color:#a855f7;">Povraćaj</span>
{{else if eq .TipPromene "korekcija"}} {{else if eq .TipPromene "korekcija"}}
<span class="bedz" style="background:rgba(156,163,175,0.12);color:#9ca3af;">Korekcija</span> <span class="bedz" style="background:rgba(156,163,175,0.12);color:#9ca3af;">Korekcija</span>
{{else if eq .TipPromene "manjak_popis"}}
<span class="bedz" style="background:rgba(220,38,38,0.12);color:#dc2626;">Manjak (popis)</span>
{{else if eq .TipPromene "visak_popis"}}
<span class="bedz" style="background:rgba(34,197,94,0.12);color:#22c55e;">Višak (popis)</span>
{{else}} {{else}}
<span class="bedz">{{.TipPromene}}</span> <span class="bedz">{{.TipPromene}}</span>
{{end}} {{end}}
+29 -8
View File
@@ -14,6 +14,10 @@
{{define "sadrzaj"}} {{define "sadrzaj"}}
<div class="kolona" style="gap:16px;"> <div class="kolona" style="gap:16px;">
{{if .Sacuvano}}
<div class="poruka-uspeh poruka-animacija">Nabavka je uspešno sačuvana.</div>
{{end}}
<!-- nazad dugme --> <!-- nazad dugme -->
<a href="/nabavke" class="nazad-link" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML"> <a href="/nabavke" class="nazad-link" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
@@ -23,7 +27,14 @@
<!-- zaglavlje nabavke --> <!-- zaglavlje nabavke -->
<div class="kartica detalji-kartica animiraj"> <div class="kartica detalji-kartica animiraj">
<div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;"> <div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Detalji nabavke</span> <span style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Detalji nabavke</span>
{{if .Nabavka.Stornirano}}
<span style="display:inline-block;padding:3px 10px;border-radius:20px;font-size:12px;font-weight:500;background:rgba(220,38,38,0.12);color:#dc2626;">
Stornirano
</span>
{{end}}
</span>
<span class="pomocni-tekst">{{.Nabavka.Datum.Format "02.01.2006. u 15:04"}}</span> <span class="pomocni-tekst">{{.Nabavka.Datum.Format "02.01.2006. u 15:04"}}</span>
</div> </div>
@@ -46,6 +57,12 @@
{{dinari .Nabavka.Ukupno}} din {{dinari .Nabavka.Ukupno}} din
</div> </div>
</div> </div>
{{if .Nabavka.RazlogStorniranja}}
<div style="grid-column:1/-1;">
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Razlog storniranja</div>
<div style="font-size:13px;color:#dc2626;">{{.Nabavka.RazlogStorniranja}}</div>
</div>
{{end}}
</div> </div>
</div> </div>
@@ -146,23 +163,27 @@
</div> </div>
{{end}} {{end}}
<!-- zona za brisanje --> <!-- zona za storniranje -->
{{if and (index .Dozvole "nabavka.storno") (not .Nabavka.Stornirano)}}
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;"> <div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;"> <div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;"> <div style="flex:1;min-width:200px;">
<div style="font-size:14px;font-weight:500;color:#dc2626;margin-bottom:4px;">Brisanje nabavke</div> <div style="font-size:14px;font-weight:500;color:#f97316;margin-bottom:4px;">Storniranje nabavke</div>
<div class="pomocni-tekst"> <div class="pomocni-tekst">
Brisanje je trajno i ne vraća količine artikala u magacin. Storno vraća količine artikala na stanje u magacinu. Nabavka ostaje u evidenciji sa oznakom stornirana. Nabavna cena se ne menja.
</div> </div>
</div> </div>
<form method="POST" action="/nabavke/obrisi/{{.Nabavka.ID}}"> <form method="POST" action="/nabavke/storno/{{.Nabavka.ID}}" style="display:flex;flex-direction:column;gap:8px;align-items:flex-end;">
<button type="submit" class="btn-opasno" <input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
data-potvrda="Da li ste sigurni da želite da obrišete ovu nabavku? Brisanje ne vraća količine artikala u magacin."> <input type="text" name="razlog" placeholder="Razlog storniranja (opciono)" style="width:240px;">
Obriši nabavku <button type="submit" class="btn-upozorenje"
data-potvrda="Stornirati ovu nabavku? Količine artikala biće vraćene na stanje.">
Storniraj nabavku
</button> </button>
</form> </form>
</div> </div>
</div> </div>
{{end}}
</div> </div>
{{end}} {{end}}
+6 -18
View File
@@ -37,7 +37,10 @@
{{.Datum.Format "02.01.2006."}} {{.Datum.Format "02.01.2006."}}
</td> </td>
<td style="padding:12px 16px;font-size:14px;font-weight:500;color:var(--tekst-glavni);"> <td style="padding:12px 16px;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
{{if .DobavljacNaziv}}{{.DobavljacNaziv}}{{else}}—{{end}} <span style="display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap;">
{{if .DobavljacNaziv}}{{.DobavljacNaziv}}{{else}}—{{end}}
{{if .Stornirano}}<span style="padding:2px 8px;border-radius:20px;font-size:11px;font-weight:500;background:rgba(220,38,38,0.12);color:#dc2626;">Stornirano</span>{{end}}
</span>
</td> </td>
<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 .Napomena}}{{.Napomena}}{{else}}—{{end}} {{if .Napomena}}{{.Napomena}}{{else}}—{{end}}
@@ -50,14 +53,6 @@
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/nabavke/{{.ID}}" class="btn-primarno-malo"> <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/nabavke/{{.ID}}" class="btn-primarno-malo">
Detalji Detalji
</a> </a>
{{if index $.Dozvole "nabavka.obrisi"}}
<form method="POST" action="/nabavke/obrisi/{{.ID}}" style="display:inline;">
<button type="submit" class="btn-obrisi-malo"
data-potvrda="Da li ste sigurni? Brisanje nabavke ne vraća količine artikala u magacin.">
Obriši
</button>
</form>
{{end}}
</div> </div>
</td> </td>
</tr> </tr>
@@ -79,8 +74,9 @@
<div class="kartica nabavka-kartica animiraj"> <div class="kartica nabavka-kartica animiraj">
<div class="red-izmedju" style="align-items:flex-start;gap:12px;margin-bottom:10px;"> <div class="red-izmedju" style="align-items:flex-start;gap:12px;margin-bottom:10px;">
<div> <div>
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);"> <div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
{{if .DobavljacNaziv}}{{.DobavljacNaziv}}{{else}}Bez dobavljača{{end}} {{if .DobavljacNaziv}}{{.DobavljacNaziv}}{{else}}Bez dobavljača{{end}}
{{if .Stornirano}}<span style="padding:2px 8px;border-radius:20px;font-size:11px;font-weight:500;background:rgba(220,38,38,0.12);color:#dc2626;">Stornirano</span>{{end}}
</div> </div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;"> <div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">
{{.Datum.Format "02.01.2006."}} {{.Datum.Format "02.01.2006."}}
@@ -97,14 +93,6 @@
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/nabavke/{{.ID}}" class="btn-primarno-malo" style="flex:1;justify-content:center;"> <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/nabavke/{{.ID}}" class="btn-primarno-malo" style="flex:1;justify-content:center;">
Detalji Detalji
</a> </a>
{{if index $.Dozvole "nabavka.obrisi"}}
<form method="POST" action="/nabavke/obrisi/{{.ID}}" style="flex:1;">
<button type="submit" class="btn-obrisi-malo" style="width:100%;justify-content:center;"
data-potvrda="Da li ste sigurni? Brisanje nabavke ne vraća količine artikala u magacin.">
Obriši
</button>
</form>
{{end}}
</div> </div>
</div> </div>
{{else}} {{else}}
+361 -42
View File
@@ -5,7 +5,7 @@
{{define "dodatni-css"}} {{define "dodatni-css"}}
<style> <style>
.popis-input { .popis-input {
width: 80px; width: 90px;
text-align: right; text-align: right;
font-family: monospace; font-family: monospace;
font-size: 14px; font-size: 14px;
@@ -15,7 +15,7 @@
border-color: #f97316; border-color: #f97316;
background: rgba(249,115,22,0.06); background: rgba(249,115,22,0.06);
} }
.popis-razlika { .popis-razlika, .popis-vrednost-razlika {
font-family: monospace; font-family: monospace;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
@@ -23,12 +23,190 @@
text-align: right; text-align: right;
} }
.red-kriticno td { background: rgba(220,38,38,0.04); } .red-kriticno td { background: rgba(220,38,38,0.04); }
.popis-suma {
display: flex;
gap: 24px;
flex-wrap: wrap;
font-size: 13px;
}
.popis-suma-stavka { display: flex; flex-direction: column; gap: 2px; }
.popis-suma-labela { color: var(--tekst-slabi); font-size: 12px; }
.popis-suma-vrednost { font-weight: 600; font-family: monospace; font-size: 14px; }
.boja-manjak { color: #dc2626; }
.boja-visak { color: #22c55e; }
/* ── MOBILNI (≤768px): tabela → kartice ──────────────────── */
@media (max-width: 768px) {
/* zaglavlje forme: Tip i Napomena u svoj red, jednake pune širine */
.popis-polje { width: 100%; }
#tip-popisa, #napomena-polje { width: 100% !important; }
#tip-popisa { padding-right: 28px; }
/* kartica prikazuje SVE kolone kao desktop; samo zbirni red (tfoot)
se sakriva — ukupno je u sumarnim karticama ispod */
#popis-tabela tfoot { display: none !important; }
/* svaki red postaje kartica, svaka ćelija je labela:vrednost */
#popis-tabela, #popis-tabela tbody, #popis-tabela tr, #popis-tabela td { display: block; width: auto; }
#popis-tabela thead { display: none; }
#popis-tabela tr {
border: 0.5px solid var(--ivica);
border-radius: 10px;
padding: 10px 14px;
margin-bottom: 10px;
}
#popis-tabela td {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
text-align: right !important;
padding: 5px 0 !important;
border: none !important;
font-size: 14px;
}
#popis-tabela td::before {
content: attr(data-label);
font-weight: 500;
color: var(--tekst-sporedni);
text-align: left;
white-space: nowrap;
}
/* prva ćelija (naziv) je naslov kartice */
#popis-tabela td.kart-naslov {
justify-content: flex-start;
text-align: left !important;
font-size: 15px;
font-weight: 600;
border-bottom: 0.5px solid var(--ivica) !important;
padding-bottom: 9px !important;
margin-bottom: 5px;
}
#popis-tabela td.kart-naslov::before { content: none; }
/* „Stvarno kol." input — bez spiner strelica (Firefox seče broj) */
.popis-input {
width: 130px;
-moz-appearance: textfield;
appearance: textfield;
}
.popis-input::-webkit-outer-spin-button,
.popis-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
}
/* ── ŠTAMPA: A4 landscape, fiksni layout, NE responsive ───────
Print poništava mobilne kartice (!important) — i sa telefona
se štampa A4 tabela, ne prilagođava se ekranu. */
@media print {
@page { size: A4 landscape; margin: 10mm 8mm; }
/* ── resetuj pozadinu i boje (dark tema + glass efekti) ── */
html, body, .raspored, .glavni-sadrzaj, .sadrzaj, main, .kolona,
.kartica, .kartica div, .kartica p, .kartica span, .kartica label {
background: #fff !important;
color: #111 !important;
text-shadow: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
html { background: #fff !important; }
body { min-width: 780px; font-size: 12px; }
body::before, body::after { display: none !important; }
.raspored { position: static !important; z-index: auto !important; }
.glavni-sadrzaj { margin-left: 0 !important; }
.sadrzaj { padding: 0 !important; max-width: none !important; }
/* sakrij hrom */
.sidebar, .sidebar-overlay, .topbar, #hamburger,
.btn-primarno, .btn-sekundarno, .nazad-link,
.poruka-uspeh, .poruka-greska, .flash-toast,
#potvrda-modal, .dugme-stampa, .popis-suma { display: none !important; }
/* sakrij info karticu (uputstvo) */
.popis-info-kartica { display: none !important; }
/* sakrij okvire forme koji nisu tabela */
.popis-forma-zaglavlje,
.popis-forma-sumarno,
.popis-forma-dugme { display: none !important; }
/* ništa ne sme da seče/skroluje */
* { overflow: visible !important; }
/* zaglavlje štampe — naziv firme, datum, tip popisa */
.stampa-zaglavlje { display: block !important; }
/* vrati tabelu iz kartica (mobilni) nazad u pravu tabelu */
#popis-tabela {
display: table !important;
width: 100% !important;
border-collapse: collapse;
font-size: 10px;
}
#popis-tabela thead { display: table-header-group !important; }
#popis-tabela tbody { display: table-row-group !important; }
#popis-tabela tfoot { display: table-footer-group !important; }
#popis-tabela tr {
display: table-row !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
break-inside: avoid;
}
#popis-tabela th {
display: table-cell !important;
border: 0.5px solid #888 !important;
padding: 4px 6px !important;
font-size: 9px !important;
font-weight: 600 !important;
text-align: center !important;
background: #f0f0f0 !important;
color: #111 !important;
}
#popis-tabela th:first-child { text-align: left !important; }
#popis-tabela td {
display: table-cell !important;
border: 0.5px solid #ccc !important;
padding: 3px 6px !important;
text-align: right !important;
font-size: 10px !important;
color: #111 !important;
}
#popis-tabela td.kart-naslov {
text-align: left !important;
font-size: 10px !important;
font-weight: 500 !important;
}
#popis-tabela td::before, #popis-tabela th::before { content: none !important; }
/* inputi — izgledaju kao običan tekst */
.popis-input {
-moz-appearance: textfield !important;
appearance: textfield !important;
border: none !important;
background: transparent !important;
width: auto !important;
max-width: 60px !important;
padding: 0 !important;
font-size: 10px !important;
color: #111 !important;
text-align: right !important;
}
/* sakrij labele iz zaglavlja forme (Tip, Napomena) */
.popis-polje, form .polje-labela { display: none !important; }
}
</style> </style>
{{end}} {{end}}
{{define "sadrzaj"}} {{define "sadrzaj"}}
<div class="kolona" style="gap:16px;"> <div class="kolona" style="gap:16px;">
<!-- zaglavlje štampe (skriveno na ekranu, vidljivo pri štampi) -->
<div class="stampa-zaglavlje" style="display:none;padding:0;margin-bottom:14px;">
<div style="font-size:16px;font-weight:700;margin-bottom:2px;">{{.NazivFirme}}</div>
<div style="font-size:11px;color:#555;">Popis magacina — {{.TipPopisaNaziv}} &nbsp;|&nbsp; Datum: {{.DatumPopisa}} &nbsp;|&nbsp; Sačinio: {{.KorisnikIme}}</div>
</div>
{{if .Sacuvano}} {{if .Sacuvano}}
<div class="poruka-uspeh poruka-animacija">Popis je uspešno sačuvan. Korekcije su upisane u magacinsku evidenciju.</div> <div class="poruka-uspeh poruka-animacija">Popis je uspešno sačuvan. Korekcije su upisane u magacinsku evidenciju.</div>
{{end}} {{end}}
@@ -37,16 +215,23 @@
{{end}} {{end}}
<!-- uputstvo --> <!-- uputstvo -->
<div class="kartica animiraj" style="padding:14px 18px;"> <div class="kartica animiraj popis-info-kartica" style="padding:14px 18px;">
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;"> <div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
<div style="flex:1;min-width:260px;"> <div style="flex:1;min-width:260px;">
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);margin-bottom:4px;">Godišnji popis magacina</div> <div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);margin-bottom:4px;">Popis magacina</div>
<div style="font-size:13px;color:var(--tekst-slabi);line-height:1.6;"> <div style="font-size:13px;color:var(--tekst-slabi);line-height:1.6;">
Unesite stvarno prebrojano stanje za svaki artikal. Polja sa razlikom biće istaknuta narandžasto. 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. Samo artikli sa izmenjenom količinom biće korigovani — ostali ostaju nepromenjeni.
</div> </div>
<div style="font-size:12px;color:var(--tekst-slabi);margin-top:6px;">
Datum popisa: <strong style="color:var(--tekst-glavni);">{{.DatumPopisa}}</strong>
&nbsp;|&nbsp; Sačinio: <strong style="color:var(--tekst-glavni);">{{.KorisnikIme}}</strong>
</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/izvestaji" class="btn-sekundarno" style="white-space:nowrap;">← Izveštaji</a>
<button type="button" onclick="window.open('/izvestaji/popis/stampa','_blank')" class="btn-sekundarno" style="white-space:nowrap;">🖨 Štampaj</button>
</div> </div>
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/izvestaji" class="btn-sekundarno" style="white-space:nowrap;">← Izveštaji</a>
</div> </div>
</div> </div>
@@ -55,12 +240,20 @@
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}"> <input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
<div class="kartica animiraj" style="padding:0;overflow:hidden;"> <div class="kartica animiraj" style="padding:0;overflow:hidden;">
<div style="padding:12px 20px;border-bottom:0.5px solid var(--ivica);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;"> <div style="padding:12px 20px;border-bottom:0.5px solid var(--ivica);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;" class="popis-forma-zaglavlje">
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Artikli</span> <span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Artikli</span>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;"> <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<div> <div class="popis-polje">
<label class="polje-labela" style="display:inline;margin-right:6px;">Tip:</label>
<select name="tip_popisa" id="tip-popisa" style="width:auto;height:auto;font-size:13px;padding:5px 28px 5px 8px;" onchange="azurirajNapomenu()">
<option value="godišnji"{{if eq .TipPopisa "godišnji"}} selected{{end}}>Godišnji</option>
<option value="vanredni"{{if eq .TipPopisa "vanredni"}} selected{{end}}>Vanredni</option>
<option value="preuzimanje"{{if eq .TipPopisa "preuzimanje"}} selected{{end}}>Preuzimanje dužnosti</option>
</select>
</div>
<div class="popis-polje">
<label class="polje-labela" style="display:inline;margin-right:6px;">Napomena:</label> <label class="polje-labela" style="display:inline;margin-right:6px;">Napomena:</label>
<input type="text" name="napomena" value="Godišnji popis" style="width:200px;font-size:13px;padding:5px 8px;"> <input type="text" name="napomena" id="napomena-polje" value="Godišnji popis" style="width:220px;font-size:13px;padding:5px 8px;">
</div> </div>
<button type="submit" class="btn-primarno">Sačuvaj popis</button> <button type="submit" class="btn-primarno">Sačuvaj popis</button>
</div> </div>
@@ -72,25 +265,32 @@
</div> </div>
{{else}} {{else}}
<div style="overflow-x:auto;"> <div style="overflow-x:auto;">
<table class="tabela"> <table class="tabela" id="popis-tabela">
<thead> <thead>
<tr> <tr>
<th>Artikal</th> <th>Artikal</th>
<th>Šifra</th> <th class="kol-sek">Šifra</th>
<th>Kategorija</th> <th class="kol-sek">Kategorija</th>
<th style="text-align:right;">Knjižno stanje</th> <th class="kol-sek" style="text-align:right;">JM</th>
<th style="text-align:right;">Stvarno stanje</th> <th class="kol-sek" style="text-align:right;">Nab. cena</th>
<th style="text-align:right;">Razlika</th> <th style="text-align:right;">Knj. kol.</th>
<th class="kol-sek" style="text-align:right;">Knj. vrednost</th>
<th style="text-align:right;">Stvarno kol.</th>
<th style="text-align:right;">Razlika kol.</th>
<th class="kol-sek" style="text-align:right;">Razlika vrednost</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range .Artikli}} {{range .Artikli}}
<tr class="{{if le .Kolicina .KolicinMin}}red-kriticno{{end}}" id="red-{{.ID}}"> <tr class="{{if le .Kolicina .KolicinMin}}red-kriticno{{end}}" id="red-{{.ID}}">
<td style="font-weight:500;">{{.Naziv}}</td> <td class="kart-naslov" style="font-weight:500;">{{.Naziv}}</td>
<td style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td> <td class="kol-sek" data-label="Šifra" style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
<td style="font-size:12px;color:var(--tekst-slabi);">{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}</td> <td class="kol-sek" data-label="Kategorija" style="font-size:12px;color:var(--tekst-slabi);">{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}</td>
<td style="text-align:right;font-family:monospace;font-weight:600;" id="knjizno-{{.ID}}">{{.Kolicina}}</td> <td class="kol-sek" data-label="JM" style="text-align:right;font-size:12px;color:var(--tekst-slabi);">{{if .JedinicaMere}}{{.JedinicaMere}}{{else}}kom{{end}}</td>
<td style="text-align:right;"> <td class="kol-sek" data-label="Nab. cena" style="text-align:right;font-family:monospace;font-size:13px;">{{dinari .NabavnaCena}}</td>
<td data-label="Knj. kol." style="text-align:right;font-family:monospace;font-weight:600;" id="knjizno-{{.ID}}">{{.Kolicina}}</td>
<td class="kol-sek" data-label="Knj. vrednost" style="text-align:right;font-family:monospace;font-size:13px;" id="knjvred-{{.ID}}"></td>
<td data-label="Stvarno kol." style="text-align:right;">
<input type="number" <input type="number"
class="popis-input" class="popis-input"
name="kolicina_{{.ID}}" name="kolicina_{{.ID}}"
@@ -98,20 +298,63 @@
value="{{.Kolicina}}" value="{{.Kolicina}}"
min="0" min="0"
data-knjizno="{{.Kolicina}}" data-knjizno="{{.Kolicina}}"
data-cena="{{printf "%.4f" .NabavnaCena}}"
data-pdv="{{printf "%.2f" .PdvStopa}}"
data-id="{{.ID}}" data-id="{{.ID}}"
oninput="azurirajRazliku(this)"> oninput="azurirajRed(this)">
</td> </td>
<td style="text-align:right;"> <td data-label="Razlika kol." style="text-align:right;">
<span class="popis-razlika" id="razlika-{{.ID}}"></span> <span class="popis-razlika" id="razlika-{{.ID}}"></span>
</td> </td>
<td class="kol-sek" data-label="Razlika vrednost" style="text-align:right;">
<span class="popis-vrednost-razlika" id="vrazlika-{{.ID}}"></span>
</td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
<tfoot>
<tr style="font-weight:600;background:var(--pozadina);">
<td colspan="5" style="text-align:right;font-size:12px;color:var(--tekst-slabi);">UKUPNO:</td>
<td style="text-align:right;font-family:monospace;" id="suma-knjizno-kol"></td>
<td style="text-align:right;font-family:monospace;" id="suma-knjizno-vred"></td>
<td style="text-align:right;font-family:monospace;" id="suma-stvarno-kol"></td>
<td style="text-align:right;font-family:monospace;" id="suma-razlika-kol"></td>
<td style="text-align:right;font-family:monospace;" id="suma-razlika-vred"></td>
</tr>
</tfoot>
</table> </table>
</div> </div>
<!-- sumarni pregled manjkova/viškova -->
<div class="popis-forma-sumarno" style="padding:14px 20px;border-top:0.5px solid var(--ivica);">
<div class="popis-suma">
<div class="popis-suma-stavka">
<span class="popis-suma-labela">Manjkovi (kom)</span>
<span class="popis-suma-vrednost boja-manjak" id="sum-manjak-kol">0</span>
</div>
<div class="popis-suma-stavka">
<span class="popis-suma-labela">Manjkovi (vrednost)</span>
<span class="popis-suma-vrednost boja-manjak" id="sum-manjak-vred">0,00</span>
</div>
<div class="popis-suma-stavka">
<span class="popis-suma-labela">Viškovi (kom)</span>
<span class="popis-suma-vrednost boja-visak" id="sum-visak-kol">0</span>
</div>
<div class="popis-suma-stavka">
<span class="popis-suma-labela">Viškovi (vrednost)</span>
<span class="popis-suma-vrednost boja-visak" id="sum-visak-vred">0,00</span>
</div>
{{if index .Moduli "pdv"}}
<div class="popis-suma-stavka" style="border-left:0.5px solid var(--ivica);padding-left:24px;">
<span class="popis-suma-labela">Dugovani PDV na manjak</span>
<span class="popis-suma-vrednost boja-manjak" id="sum-pdv-manjak" title="Manjak delova se smatra sopstvenom potrošnjom — PDV se duguje na nabavnu vrednost (normativ rashoda ≈ 0).">0,00</span>
</div>
{{end}}
</div>
</div>
<!-- dugme na dnu --> <!-- dugme na dnu -->
<div style="padding:14px 20px;border-top:0.5px solid var(--ivica);display:flex;justify-content:flex-end;"> <div class="popis-forma-dugme" style="padding:14px 20px;border-top:0.5px solid var(--ivica);display:flex;justify-content:flex-end;">
<button type="submit" class="btn-primarno">Sačuvaj popis</button> <button type="submit" class="btn-primarno">Sačuvaj popis</button>
</div> </div>
{{end}} {{end}}
@@ -121,33 +364,109 @@
</div> </div>
<script> <script>
function azurirajRazliku(input) { var napomePoTipu = {
var id = input.dataset.id; "godišnji": "Godišnji popis",
"vanredni": "Vanredni popis",
"preuzimanje": "Popis pri preuzimanju dužnosti"
};
function azurirajNapomenu() {
var tip = document.getElementById('tip-popisa').value;
document.getElementById('napomena-polje').value = napomePoTipu[tip] || "Godišnji popis";
}
function fmt(n) {
return n.toLocaleString('sr-RS', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function azurirajRed(input) {
var id = input.dataset.id;
var knjizno = parseInt(input.dataset.knjizno, 10); var knjizno = parseInt(input.dataset.knjizno, 10);
var cena = parseFloat(input.dataset.cena) || 0;
var stvarno = parseInt(input.value, 10); var stvarno = parseInt(input.value, 10);
var span = document.getElementById('razlika-' + id); var spanKol = document.getElementById('razlika-' + id);
var spanVred = document.getElementById('vrazlika-' + id);
if (isNaN(stvarno)) { if (isNaN(stvarno)) {
span.textContent = '—'; spanKol.textContent = '—'; spanVred.textContent = '—';
span.style.color = ''; spanKol.style.color = ''; spanVred.style.color = '';
input.classList.remove('izmenjeno'); input.classList.remove('izmenjeno');
return;
}
var razlika = stvarno - knjizno;
if (razlika === 0) {
span.textContent = '—';
span.style.color = '';
input.classList.remove('izmenjeno');
} else if (razlika > 0) {
span.textContent = '+' + razlika;
span.style.color = '#22c55e';
input.classList.add('izmenjeno');
} else { } else {
span.textContent = razlika; var razlika = stvarno - knjizno;
span.style.color = '#dc2626'; if (razlika === 0) {
input.classList.add('izmenjeno'); spanKol.textContent = '—'; spanVred.textContent = '—';
spanKol.style.color = ''; spanVred.style.color = '';
input.classList.remove('izmenjeno');
} else {
var boja = razlika > 0 ? '#22c55e' : '#dc2626';
spanKol.textContent = (razlika > 0 ? '+' : '') + razlika;
spanVred.textContent = (razlika > 0 ? '+' : '') + fmt(razlika * cena);
spanKol.style.color = boja; spanVred.style.color = boja;
input.classList.add('izmenjeno');
}
} }
azurirajSumu();
} }
function azurirajSumu() {
var inputs = document.querySelectorAll('.popis-input');
var knjiznoKol = 0, knjiznoVred = 0, stvarnoKol = 0;
var manjKol = 0, manjVred = 0, visakKol = 0, visakVred = 0;
var pdvManjak = 0;
inputs.forEach(function(inp) {
var kn = parseInt(inp.dataset.knjizno, 10) || 0;
var cena = parseFloat(inp.dataset.cena) || 0;
var pdv = parseFloat(inp.dataset.pdv) || 0;
var st = parseInt(inp.value, 10);
if (isNaN(st)) st = kn;
knjiznoKol += kn;
knjiznoVred += kn * cena;
stvarnoKol += st;
var r = st - kn;
if (r < 0) {
var manjVrArt = Math.abs(r) * cena;
manjKol += Math.abs(r);
manjVred += manjVrArt;
// manjak delova = sopstvena potrošnja → PDV na nabavnu vrednost (normativ ≈ 0)
pdvManjak += manjVrArt * pdv / 100;
}
if (r > 0) { visakKol += r; visakVred += r * cena; }
});
var razlikaKol = stvarnoKol - knjiznoKol;
var razlikaVred = visakVred - manjVred;
var el = function(id) { return document.getElementById(id); };
if (el('suma-knjizno-kol')) el('suma-knjizno-kol').textContent = knjiznoKol;
if (el('suma-knjizno-vred')) el('suma-knjizno-vred').textContent = fmt(knjiznoVred);
if (el('suma-stvarno-kol')) el('suma-stvarno-kol').textContent = stvarnoKol;
if (el('suma-razlika-kol')) {
el('suma-razlika-kol').textContent = (razlikaKol >= 0 ? '+' : '') + razlikaKol;
el('suma-razlika-kol').style.color = razlikaKol === 0 ? '' : (razlikaKol > 0 ? '#22c55e' : '#dc2626');
}
if (el('suma-razlika-vred')) {
el('suma-razlika-vred').textContent = (razlikaVred >= 0 ? '+' : '') + fmt(razlikaVred);
el('suma-razlika-vred').style.color = razlikaVred === 0 ? '' : (razlikaVred > 0 ? '#22c55e' : '#dc2626');
}
if (el('sum-manjak-kol')) el('sum-manjak-kol').textContent = manjKol;
if (el('sum-manjak-vred')) el('sum-manjak-vred').textContent = fmt(manjVred);
if (el('sum-visak-kol')) el('sum-visak-kol').textContent = visakKol;
if (el('sum-visak-vred')) el('sum-visak-vred').textContent = fmt(visakVred);
if (el('sum-pdv-manjak')) el('sum-pdv-manjak').textContent = fmt(pdvManjak);
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.popis-input').forEach(function(inp) {
var id = inp.dataset.id;
var kn = parseInt(inp.dataset.knjizno, 10) || 0;
var cena = parseFloat(inp.dataset.cena) || 0;
var el = document.getElementById('knjvred-' + id);
if (el) el.textContent = fmt(kn * cena);
});
azurirajSumu();
});
</script> </script>
{{end}} {{end}}
+165
View File
@@ -0,0 +1,165 @@
<!DOCTYPE html>
<html lang="sr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Popis — {{.NazivFirme}}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
font-size: 12px;
color: #111;
background: #fff;
min-width: 780px;
}
.strana { max-width: 780px; margin: 0 auto; padding: 24px 32px; }
.zaglavlje { margin-bottom: 16px; }
.firma-naziv { font-size: 18px; font-weight: 700; }
.dokument-naslov { font-size: 14px; font-weight: 600; margin-top: 4px; }
.dokument-meta { font-size: 11px; color: #555; margin-top: 4px; line-height: 1.6; }
.tabela { width: 100%; border-collapse: collapse; margin-top: 12px; }
.tabela th {
font-size: 9px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.3px; color: #555; padding: 5px 6px;
border-bottom: 1.5px solid #888; border-top: 1px solid #ccc;
text-align: center; background: #f7f7f7;
}
.tabela th:first-child { text-align: left; }
.tabela td {
padding: 4px 6px; border-bottom: 0.5px solid #ddd;
font-size: 10px; text-align: right;
}
.tabela td:first-child { text-align: left; font-weight: 500; }
.tabela td.slabo { color: #777; font-size: 9px; font-family: monospace; }
.tabela td.manjak { color: #b91c1c; }
.tabela td.visak { color: #15803d; }
tfoot td {
font-weight: 700; font-size: 10px;
border-top: 1.5px solid #888; border-bottom: 1.5px solid #888;
background: #f7f7f7;
}
.sumarno { margin-top: 16px; padding-top: 10px; border-top: 1px solid #ccc; }
.sumarno-stavke { display: flex; gap: 32px; font-size: 11px; flex-wrap: wrap; }
.sumarno-stavka { display: flex; flex-direction: column; gap: 1px; }
.sumarno-labela { color: #777; font-size: 9px; }
.sumarno-manjak { color: #b91c1c; }
.sumarno-visak { color: #15803d; }
.potpis { margin-top: 28px; display: flex; gap: 60px; }
.potpis-stavka { flex: 1; }
.potpis-linija { border-top: 1px solid #888; margin-top: 28px; padding-top: 3px; font-size: 10px; color: #777; }
@media print {
body { font-size: 11px; }
.strana { padding: 20px 28px; max-width: 780px; }
@page { size: A4 landscape; margin: 8mm 10mm; }
.tabela tr { break-inside: avoid; }
.tabela thead { display: table-header-group; }
.dugme-stampa { display: none !important; }
}
.dugme-stampa {
position: fixed; bottom: 24px; right: 24px;
background: #111; color: #fff; border: none;
padding: 10px 20px; border-radius: 8px;
font-size: 13px; font-weight: 500; cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.dugme-stampa:hover { background: #333; }
</style>
</head>
<body>
<div class="strana">
<div class="zaglavlje">
<div class="firma-naziv">{{.NazivFirme}}</div>
<div class="dokument-naslov">Popis magacina</div>
<div class="dokument-meta">
{{.TipPopisaNaziv}} &nbsp;|&nbsp; Datum: {{.DatumPopisa}} &nbsp;|&nbsp; Sačinio: {{.KorisnikIme}}
</div>
</div>
<table class="tabela">
<thead>
<tr>
<th>Artikal</th>
<th>Šifra</th>
<th>Kategorija</th>
<th>JM</th>
<th>Nab. cena</th>
<th>Knj. kol.</th>
<th>Knj. vrednost</th>
<th>Stvarno kol.</th>
<th>Razlika kol.</th>
<th>Razlika vred.</th>
</tr>
</thead>
<tbody>
{{range .Redovi}}
<tr>
<td>{{.Naziv}}</td>
<td class="slabo">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
<td class="slabo">{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}</td>
<td>{{if .JedinicaMere}}{{.JedinicaMere}}{{else}}kom{{end}}</td>
<td>{{.NabavnaCenaStr}}</td>
<td>{{.Kolicina}}</td>
<td>{{.KnjiznaVrednostStr}}</td>
<td>{{.StvarnaKolicina}}</td>
<td class="{{if gt .Razlika 0}}visak{{else if lt .Razlika 0}}manjak{{end}}">{{.RazlikaStr}}</td>
<td class="{{if gt .RazlikaVrednost 0.0}}visak{{else if lt .RazlikaVrednost 0.0}}manjak{{end}}">{{.RazlikaVrednostStr}}</td>
</tr>
{{end}}
</tbody>
<tfoot>
<tr>
<td colspan="5" style="text-align:right;">UKUPNO:</td>
<td>{{.UkupnoKnjiznoKol}}</td>
<td>{{.UkupnoKnjiznoVredStr}}</td>
<td>{{.UkupnoStvarnoKol}}</td>
<td>{{.UkupnoRazlikaKolStr}}</td>
<td>{{.UkupnoRazlikaVredStr}}</td>
</tr>
</tfoot>
</table>
<div class="sumarno">
<div class="sumarno-stavke">
<div class="sumarno-stavka">
<span class="sumarno-labela">Manjkovi (kom)</span>
<span class="sumarno-manjak">{{.ManjakKol}}</span>
</div>
<div class="sumarno-stavka">
<span class="sumarno-labela">Manjkovi (vrednost)</span>
<span class="sumarno-manjak">{{.ManjakVredStr}}</span>
</div>
<div class="sumarno-stavka">
<span class="sumarno-labela">Viškovi (kom)</span>
<span class="sumarno-visak">{{.VisakKol}}</span>
</div>
<div class="sumarno-stavka">
<span class="sumarno-labela">Viškovi (vrednost)</span>
<span class="sumarno-visak">{{.VisakVredStr}}</span>
</div>
</div>
</div>
<div class="potpis">
<div class="potpis-stavka">
<div class="potpis-linija">Potpis lica koje je izvršilo popis</div>
</div>
<div class="potpis-stavka">
<div class="potpis-linija">Potpis odgovornog lica</div>
</div>
</div>
</div>
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
<script>window.onload = function() { setTimeout(function() { window.print(); }, 300); }</script>
</body>
</html>
@@ -66,6 +66,10 @@
<span class="bedz" style="background:rgba(168,85,247,0.12);color:#a855f7;">Povraćaj</span> <span class="bedz" style="background:rgba(168,85,247,0.12);color:#a855f7;">Povraćaj</span>
{{else if eq .TipPromene "korekcija"}} {{else if eq .TipPromene "korekcija"}}
<span class="bedz" style="background:rgba(156,163,175,0.12);color:#9ca3af;">Korekcija</span> <span class="bedz" style="background:rgba(156,163,175,0.12);color:#9ca3af;">Korekcija</span>
{{else if eq .TipPromene "manjak_popis"}}
<span class="bedz" style="background:rgba(220,38,38,0.12);color:#dc2626;">Manjak (popis)</span>
{{else if eq .TipPromene "visak_popis"}}
<span class="bedz" style="background:rgba(34,197,94,0.12);color:#22c55e;">Višak (popis)</span>
{{else}} {{else}}
<span class="bedz">{{.TipPromene}}</span> <span class="bedz">{{.TipPromene}}</span>
{{end}} {{end}}