Merge develop → main: zalihe usklađenost (manjak/višak, storno nabavke, prosečna cena), popis štampa
This commit is contained in:
+2
-1
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
<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}}
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
@@ -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}} | Datum: {{.DatumPopisa}} | 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>
|
||||||
|
| 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}}
|
||||||
|
|||||||
@@ -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>
|
<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}}
|
||||||
|
|||||||
Reference in New Issue
Block a user