Compare commits
10
Commits
21e79055e9
...
3d891d7023
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d891d7023
|
||
|
|
2b7006e910
|
||
|
|
225d766d14
|
||
|
|
43005048d8
|
||
|
|
67955cff3b
|
||
|
|
78cca0e695
|
||
|
|
19cad79abe
|
||
|
|
decc1e4735
|
||
|
|
119754916d
|
||
|
|
2774c40545
|
+2
-1
@@ -350,7 +350,7 @@ func main() {
|
||||
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(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.Get("/dobavljaci/novi", h.NoviDobavljac)
|
||||
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/popis", h.Popis)
|
||||
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.Get("/prodaja/nova", h.NovaProdaja)
|
||||
r.With(doz("prodaja.dodaj")).Post("/prodaja/nova", h.SacuvajProdaju)
|
||||
|
||||
@@ -29,8 +29,8 @@ type ArtikalRepository interface {
|
||||
Vrati(ctx context.Context, id int64) error
|
||||
// SledecaSifra vraća predlog sledeće auto-šifre (npr. KOMP-0042 ili ART-0042)
|
||||
SledecaSifra(ctx context.Context, kategorijaID *int64) (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
|
||||
// KorigujKolicinu postavlja novu količinu artikla i upisuje promenu u magacinske_promene
|
||||
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(ctx context.Context, artikalID int64) ([]int64, error)
|
||||
// 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)
|
||||
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)
|
||||
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
|
||||
|
||||
@@ -397,8 +397,9 @@ func (r *ArtikalRepo) SveDobavljaceArtikala(ctx context.Context) (map[int64][]in
|
||||
return mapa, 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 {
|
||||
// KorigujKolicinu postavlja novu količinu i upisuje promenu u magacinske_promene;
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
if err = zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaKorekcija, promena,
|
||||
if err = zabeleziMagacinPromenu(ctx, tx, artikalID, tipPromene, promena,
|
||||
staraKolicina, novaKolicina, 0, korisnikID, napomena); err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: %w", err)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ func NoviNabavkaRepo(db *sql.DB) *NabavkaRepo {
|
||||
func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error) {
|
||||
redovi, err := r.db.QueryContext(ctx, `
|
||||
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
|
||||
FROM nabavke n
|
||||
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() {
|
||||
var n model.NabavkaSaDetaljem
|
||||
var dobavljacID sql.NullInt64
|
||||
var napomena, metod sql.NullString
|
||||
var napomena, metod, razlogStorniranja sql.NullString
|
||||
|
||||
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,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -51,6 +53,7 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err
|
||||
}
|
||||
n.Napomena = napomena.String
|
||||
n.MetodRaspodele = metod.String
|
||||
n.RazlogStorniranja = razlogStorniranja.String
|
||||
|
||||
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) {
|
||||
var n model.Nabavka
|
||||
var dobavljacID sql.NullInt64
|
||||
var napomena, metod sql.NullString
|
||||
var napomena, metod, razlogStorniranja sql.NullString
|
||||
|
||||
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(
|
||||
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod, &n.Datum,
|
||||
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod,
|
||||
&n.Stornirano, &razlogStorniranja, &n.Datum,
|
||||
)
|
||||
if err != nil {
|
||||
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.MetodRaspodele = metod.String
|
||||
n.RazlogStorniranja = razlogStorniranja.String
|
||||
|
||||
return &n, nil
|
||||
}
|
||||
@@ -194,17 +200,26 @@ func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []mo
|
||||
}
|
||||
|
||||
var stanjePre int
|
||||
var staraNabavnaCena float64
|
||||
err = tx.QueryRowContext(ctx,
|
||||
"SELECT kolicina FROM artikli WHERE id = ?", s.ArtikalID,
|
||||
).Scan(&stanjePre)
|
||||
"SELECT kolicina, nabavna_cena FROM artikli WHERE id = ?", s.ArtikalID,
|
||||
).Scan(&stanjePre, &staraNabavnaCena)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: dohvati stanje: %w", err)
|
||||
}
|
||||
|
||||
// ponderisana prosečna nabavna cena (MRS 2)
|
||||
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,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?",
|
||||
stanjePosle, s.ArtikalID,
|
||||
"UPDATE artikli SET kolicina = ?, nabavna_cena = ? WHERE id = ?",
|
||||
stanjePosle, novaProsecna, s.ArtikalID,
|
||||
)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// Obrisi briše nabavku po ID-u, vraća količine artikala na stanje i beleži korekciju u magacinski trag.
|
||||
func (r *NabavkaRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) error {
|
||||
// Storno označava nabavku kao storniranu, vraća količine artikala na stanje i beleži
|
||||
// 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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: begin: %w", err)
|
||||
return fmt.Errorf("ntech: NabavkaRepo.Storno: begin: %w", err)
|
||||
}
|
||||
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,
|
||||
"SELECT artikal_id, kolicina FROM stavke_nabavke WHERE nabavka_id = ?", id)
|
||||
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 {
|
||||
artikalID int64
|
||||
@@ -246,7 +273,7 @@ func (r *NabavkaRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) e
|
||||
var s stavka
|
||||
if err := redovi.Scan(&s.artikalID, &s.kolicina); err != nil {
|
||||
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)
|
||||
}
|
||||
@@ -258,7 +285,7 @@ func (r *NabavkaRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) e
|
||||
if err := tx.QueryRowContext(ctx,
|
||||
"SELECT kolicina FROM artikli WHERE id = ?", s.artikalID,
|
||||
).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
|
||||
if stanjePosle < 0 {
|
||||
@@ -267,20 +294,23 @@ func (r *NabavkaRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) e
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, s.artikalID,
|
||||
); 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,
|
||||
-s.kolicina, stanjePre, stanjePosle, id, korisnikID, "brisanje nabavke"); err != nil {
|
||||
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: magacin: %w", err)
|
||||
-s.kolicina, stanjePre, stanjePosle, id, korisnikID, "storno nabavke: "+razlog); err != nil {
|
||||
return fmt.Errorf("ntech: NabavkaRepo.Storno: magacin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, "DELETE FROM nabavke WHERE id = ?", id); err != nil {
|
||||
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: delete: %w", err)
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
"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 {
|
||||
return fmt.Errorf("ntech: NabavkaRepo.Obrisi: commit: %w", err)
|
||||
return fmt.Errorf("ntech: NabavkaRepo.Storno: commit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -316,9 +316,12 @@ func (h *Handler) StanjeZalihaIzvestaj(w http.ResponseWriter, r *http.Request) {
|
||||
// PodaciPopisa su podaci za stranicu popisa
|
||||
type PodaciPopisa struct {
|
||||
model.PodaciStranice
|
||||
Artikli []model.ArtikalSaKategorijom
|
||||
Sacuvano bool
|
||||
Greska string
|
||||
Artikli []model.ArtikalSaKategorijom
|
||||
Sacuvano bool
|
||||
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)
|
||||
@@ -327,7 +330,7 @@ func (h *Handler) Popis(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -337,14 +340,30 @@ func (h *Handler) Popis(w http.ResponseWriter, r *http.Request) {
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "izvestaji"
|
||||
ps.NaslovStranice = "Popis"
|
||||
tp := "godišnji"
|
||||
|
||||
h.renderujTemplate(w, "popis", PodaciPopisa{
|
||||
PodaciStranice: ps,
|
||||
Artikli: artikli,
|
||||
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
|
||||
func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
@@ -357,15 +376,26 @@ func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tipPopisa := r.FormValue("tip_popisa")
|
||||
if tipPopisa == "" {
|
||||
tipPopisa = "godišnji"
|
||||
}
|
||||
napomena := r.FormValue("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
|
||||
@@ -383,7 +413,11 @@ func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
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)
|
||||
greskaBroj++
|
||||
continue
|
||||
@@ -413,9 +447,158 @@ func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Artikli: artikli,
|
||||
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
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ var saSidebar = []string{
|
||||
|
||||
// standalone su šabloni bez base layouta
|
||||
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.
|
||||
|
||||
@@ -264,7 +264,7 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ako se količina promenila, koriguj stanje uz magacinski trag
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+22
-19
@@ -43,6 +43,7 @@ type PodaciDetaljiNabavke struct {
|
||||
Troskovi []model.NabavkaTrosak
|
||||
UkupanTrosak float64
|
||||
DobavljacNaziv string
|
||||
Sacuvano bool
|
||||
}
|
||||
|
||||
// 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[]).
|
||||
prodajne := r.Form["prodajna[]"]
|
||||
var korisnikID *int64
|
||||
if k != nil {
|
||||
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 {
|
||||
if i >= len(prodajne) {
|
||||
break
|
||||
@@ -230,12 +225,13 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
if e != nil || prodajna <= 0 {
|
||||
continue // prazno/nula ne sme da pregazi postojeću cenu
|
||||
}
|
||||
// stara prodajna cena — za nivelacioni zapis
|
||||
var staraProdajna float64
|
||||
// stara prodajna i tekuća (ponderisana) nabavna — nabavnu zadržavamo
|
||||
var staraProdajna, nabavna float64
|
||||
if a, e := h.Artikli.DohvatiID(r.Context(), s.ArtikalID); e == nil {
|
||||
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)
|
||||
continue
|
||||
}
|
||||
@@ -333,14 +329,15 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
|
||||
Troskovi: troskovi,
|
||||
UkupanTrosak: ukupanTrosak,
|
||||
DobavljacNaziv: dobavljacNaziv,
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
}
|
||||
|
||||
h.renderujTemplate(w, "nabavka_detalji", podaci)
|
||||
}
|
||||
|
||||
// ObrisiNabavku prima POST zahtev i briše nabavku po ID-u
|
||||
func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
k, ok := h.zahtevajDozvolu(w, r, "nabavka.obrisi")
|
||||
// StornoNabavke prima POST zahtev i stornira nabavku po ID-u (umesto fizičkog brisanja)
|
||||
func (h *Handler) StornoNabavke(w http.ResponseWriter, r *http.Request) {
|
||||
k, ok := h.zahtevajDozvolu(w, r, "nabavka.storno")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -349,17 +346,23 @@ func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Neispravan ID nabavke", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.NabavkeRepo.Obrisi(r.Context(), id, &k.ID); err != nil {
|
||||
http.Error(w, "Greška pri brisanju nabavke", http.StatusInternalServerError)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
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 {
|
||||
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
|
||||
|
||||
@@ -15,6 +15,7 @@ var sveAkcije = []string{
|
||||
"nabavka.pregled",
|
||||
"nabavka.dodaj",
|
||||
"nabavka.obrisi",
|
||||
"nabavka.storno",
|
||||
"dobavljac.pregled",
|
||||
"dobavljac.dodaj",
|
||||
"dobavljac.izmeni",
|
||||
@@ -66,7 +67,7 @@ func ImaDozvolu(uloga, akcija string) bool {
|
||||
case "kategorija.pregled", "kategorija.dodaj", "kategorija.izmeni", "kategorija.obrisi":
|
||||
return true
|
||||
// nabavka
|
||||
case "nabavka.pregled", "nabavka.dodaj", "nabavka.obrisi":
|
||||
case "nabavka.pregled", "nabavka.dodaj", "nabavka.obrisi", "nabavka.storno":
|
||||
return true
|
||||
// dobavljač
|
||||
case "dobavljac.pregled", "dobavljac.dodaj",
|
||||
|
||||
@@ -9,6 +9,8 @@ const (
|
||||
PromenaIzlazServis = "izlaz_servis"
|
||||
PromenaPovracaj = "povracaj"
|
||||
PromenaKorekcija = "korekcija"
|
||||
PromenaManjak = "manjak_popis"
|
||||
PromenaVisak = "visak_popis"
|
||||
)
|
||||
|
||||
// MagacinskaPromenaSaDetaljem je promena stanja artikla sa nazivom artikla
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
|
||||
// Nabavka predstavlja zaglavlje jedne nabavke
|
||||
type Nabavka struct {
|
||||
ID int64
|
||||
DobavljacID *int64
|
||||
Napomena string
|
||||
Ukupno float64
|
||||
MetodRaspodele string // "vrednost" ili "kolicina"; prazno = nema zavisnih troškova
|
||||
Datum time.Time
|
||||
ID int64
|
||||
DobavljacID *int64
|
||||
Napomena string
|
||||
Ukupno float64
|
||||
MetodRaspodele string // "vrednost" ili "kolicina"; prazno = nema zavisnih troškova
|
||||
Stornirano bool
|
||||
RazlogStorniranja string
|
||||
Datum time.Time
|
||||
}
|
||||
|
||||
// NabavkaTrosak je jedna stavka zavisnih troškova nabavke (npr. prevoz, carina)
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
{{else if eq .TipPromene "korekcija"}}
|
||||
<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}}
|
||||
<span class="bedz">{{.TipPromene}}</span>
|
||||
{{end}}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
{{define "sadrzaj"}}
|
||||
<div class="kolona" style="gap:16px;">
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh poruka-animacija">Nabavka je uspešno sačuvana.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<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>
|
||||
@@ -23,7 +27,14 @@
|
||||
<!-- zaglavlje nabavke -->
|
||||
<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;">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -46,6 +57,12 @@
|
||||
{{dinari .Nabavka.Ukupno}} din
|
||||
</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>
|
||||
|
||||
@@ -146,23 +163,27 @@
|
||||
</div>
|
||||
{{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 style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||
<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">
|
||||
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>
|
||||
<form method="POST" action="/nabavke/obrisi/{{.Nabavka.ID}}">
|
||||
<button type="submit" class="btn-opasno"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete ovu nabavku? Brisanje ne vraća količine artikala u magacin.">
|
||||
Obriši nabavku
|
||||
<form method="POST" action="/nabavke/storno/{{.Nabavka.ID}}" style="display:flex;flex-direction:column;gap:8px;align-items:flex-end;">
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja (opciono)" style="width:240px;">
|
||||
<button type="submit" class="btn-upozorenje"
|
||||
data-potvrda="Stornirati ovu nabavku? Količine artikala biće vraćene na stanje.">
|
||||
Storniraj nabavku
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -37,7 +37,10 @@
|
||||
{{.Datum.Format "02.01.2006."}}
|
||||
</td>
|
||||
<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 style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
|
||||
{{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">
|
||||
Detalji
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -79,8 +74,9 @@
|
||||
<div class="kartica nabavka-kartica animiraj">
|
||||
<div class="red-izmedju" style="align-items:flex-start;gap:12px;margin-bottom:10px;">
|
||||
<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 .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 style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">
|
||||
{{.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;">
|
||||
Detalji
|
||||
</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>
|
||||
{{else}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
.popis-input {
|
||||
width: 80px;
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
@@ -15,7 +15,7 @@
|
||||
border-color: #f97316;
|
||||
background: rgba(249,115,22,0.06);
|
||||
}
|
||||
.popis-razlika {
|
||||
.popis-razlika, .popis-vrednost-razlika {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -23,12 +23,190 @@
|
||||
text-align: right;
|
||||
}
|
||||
.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>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<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}} | Datum: {{.DatumPopisa}} | Sačinio: {{.KorisnikIme}}</div>
|
||||
</div>
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh poruka-animacija">Popis je uspešno sačuvan. Korekcije su upisane u magacinsku evidenciju.</div>
|
||||
{{end}}
|
||||
@@ -37,16 +215,23 @@
|
||||
{{end}}
|
||||
|
||||
<!-- 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="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;">
|
||||
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.
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--tekst-slabi);margin-top:6px;">
|
||||
Datum popisa: <strong style="color:var(--tekst-glavni);">{{.DatumPopisa}}</strong>
|
||||
| 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>
|
||||
<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>
|
||||
|
||||
@@ -55,12 +240,20 @@
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<button type="submit" class="btn-primarno">Sačuvaj popis</button>
|
||||
</div>
|
||||
@@ -72,25 +265,32 @@
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="tabela">
|
||||
<table class="tabela" id="popis-tabela">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artikal</th>
|
||||
<th>Šifra</th>
|
||||
<th>Kategorija</th>
|
||||
<th style="text-align:right;">Knjižno stanje</th>
|
||||
<th style="text-align:right;">Stvarno stanje</th>
|
||||
<th style="text-align:right;">Razlika</th>
|
||||
<th class="kol-sek">Šifra</th>
|
||||
<th class="kol-sek">Kategorija</th>
|
||||
<th class="kol-sek" style="text-align:right;">JM</th>
|
||||
<th class="kol-sek" style="text-align:right;">Nab. cena</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Artikli}}
|
||||
<tr class="{{if le .Kolicina .KolicinMin}}red-kriticno{{end}}" id="red-{{.ID}}">
|
||||
<td 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 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 style="text-align:right;">
|
||||
<td class="kart-naslov" style="font-weight:500;">{{.Naziv}}</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 class="kol-sek" data-label="Kategorija" style="font-size:12px;color:var(--tekst-slabi);">{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}</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 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"
|
||||
class="popis-input"
|
||||
name="kolicina_{{.ID}}"
|
||||
@@ -98,20 +298,63 @@
|
||||
value="{{.Kolicina}}"
|
||||
min="0"
|
||||
data-knjizno="{{.Kolicina}}"
|
||||
data-cena="{{printf "%.4f" .NabavnaCena}}"
|
||||
data-pdv="{{printf "%.2f" .PdvStopa}}"
|
||||
data-id="{{.ID}}"
|
||||
oninput="azurirajRazliku(this)">
|
||||
oninput="azurirajRed(this)">
|
||||
</td>
|
||||
<td style="text-align:right;">
|
||||
<td data-label="Razlika kol." style="text-align:right;">
|
||||
<span class="popis-razlika" id="razlika-{{.ID}}">—</span>
|
||||
</td>
|
||||
<td class="kol-sek" data-label="Razlika vrednost" style="text-align:right;">
|
||||
<span class="popis-vrednost-razlika" id="vrazlika-{{.ID}}">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</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>
|
||||
</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 -->
|
||||
<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>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -121,33 +364,109 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function azurirajRazliku(input) {
|
||||
var id = input.dataset.id;
|
||||
var napomePoTipu = {
|
||||
"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 cena = parseFloat(input.dataset.cena) || 0;
|
||||
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)) {
|
||||
span.textContent = '—';
|
||||
span.style.color = '';
|
||||
spanKol.textContent = '—'; spanVred.textContent = '—';
|
||||
spanKol.style.color = ''; spanVred.style.color = '';
|
||||
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 {
|
||||
span.textContent = razlika;
|
||||
span.style.color = '#dc2626';
|
||||
input.classList.add('izmenjeno');
|
||||
var razlika = stvarno - knjizno;
|
||||
if (razlika === 0) {
|
||||
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>
|
||||
{{end}}
|
||||
|
||||
@@ -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}} | Datum: {{.DatumPopisa}} | 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>
|
||||
{{else if eq .TipPromene "korekcija"}}
|
||||
<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}}
|
||||
<span class="bedz">{{.TipPromene}}</span>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user