Merge feature/kw-kalkulacija-troskovi: Faza C — marža po kategoriji/artiklu + zavisni troškovi (§5.4)
This commit is contained in:
@@ -282,6 +282,7 @@ func main() {
|
|||||||
r.With(doz("artikal.izmeni")).Get("/nivelacije", h.Nivelacije)
|
r.With(doz("artikal.izmeni")).Get("/nivelacije", h.Nivelacije)
|
||||||
r.Get("/magacin/kategorije", h.Kategorije)
|
r.Get("/magacin/kategorije", h.Kategorije)
|
||||||
r.With(doz("kategorija.dodaj")).Post("/magacin/kategorije/dodaj", h.DodajKategoriju)
|
r.With(doz("kategorija.dodaj")).Post("/magacin/kategorije/dodaj", h.DodajKategoriju)
|
||||||
|
r.With(doz("kategorija.izmeni")).Post("/magacin/kategorije/izmeni/{id}", h.IzmeniKategoriju)
|
||||||
r.With(doz("kategorija.obrisi")).Get("/magacin/kategorije/obrisi/{id}", h.ObrisiKategoriju)
|
r.With(doz("kategorija.obrisi")).Get("/magacin/kategorije/obrisi/{id}", h.ObrisiKategoriju)
|
||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke", h.Nabavke)
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke", h.Nabavke)
|
||||||
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)
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ type ArtikalRepository interface {
|
|||||||
// KategorijaRepository definiše operacije nad kategorijama
|
// KategorijaRepository definiše operacije nad kategorijama
|
||||||
type KategorijaRepository interface {
|
type KategorijaRepository interface {
|
||||||
Lista(ctx context.Context) ([]model.Kategorija, error)
|
Lista(ctx context.Context) ([]model.Kategorija, error)
|
||||||
|
DohvatiID(ctx context.Context, id int64) (*model.Kategorija, error)
|
||||||
Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error)
|
Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error)
|
||||||
|
Izmeni(ctx context.Context, k *model.Kategorija) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// PdvStopaRepository definiše operacije nad šifarnikom PDV stopa
|
// PdvStopaRepository definiše operacije nad šifarnikom PDV stopa
|
||||||
@@ -79,7 +81,8 @@ type NabavkaRepository interface {
|
|||||||
Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error)
|
Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error)
|
||||||
DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error)
|
DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error)
|
||||||
DohvatiStavke(ctx context.Context, nabavkaID int64) ([]model.StavkaSaArtiklom, error)
|
DohvatiStavke(ctx context.Context, nabavkaID int64) ([]model.StavkaSaArtiklom, error)
|
||||||
Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke) (int64, error)
|
DohvatiTroskove(ctx context.Context, nabavkaID int64) ([]model.NabavkaTrosak, error)
|
||||||
|
Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke, troskovi []model.NabavkaTrosak) (int64, error)
|
||||||
Obrisi(ctx context.Context, id int64) error
|
Obrisi(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
SELECT
|
SELECT
|
||||||
a.id, a.kategorija_id, a.naziv, a.opis,
|
a.id, a.kategorija_id, a.naziv, a.opis,
|
||||||
a.kolicina, a.kolicina_min, a.lokacija,
|
a.kolicina, a.kolicina_min, a.lokacija,
|
||||||
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.napomena, a.datum_unosa,
|
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.marza, a.napomena, a.datum_unosa,
|
||||||
COALESCE(k.naziv, '') as kategorija_naziv
|
COALESCE(k.naziv, '') as kategorija_naziv, k.marza as kategorija_marza
|
||||||
FROM artikli a
|
FROM artikli a
|
||||||
LEFT JOIN kategorije k ON a.kategorija_id = k.id
|
LEFT JOIN kategorije k ON a.kategorija_id = k.id
|
||||||
WHERE 1=1`
|
WHERE 1=1`
|
||||||
@@ -59,12 +59,13 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var a model.ArtikalSaKategorijom
|
var a model.ArtikalSaKategorijom
|
||||||
var kategorijaID sql.NullInt64
|
var kategorijaID sql.NullInt64
|
||||||
|
var marza, katMarza sql.NullFloat64
|
||||||
|
|
||||||
err := redovi.Scan(
|
err := redovi.Scan(
|
||||||
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
||||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
||||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.Napomena, &a.DatumUnosa,
|
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa,
|
||||||
&a.KategorijaNaziv,
|
&a.KategorijaNaziv, &katMarza,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ArtikalRepo.Lista: scan: %w", err)
|
return nil, fmt.Errorf("ntech: ArtikalRepo.Lista: scan: %w", err)
|
||||||
@@ -73,6 +74,12 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
if kategorijaID.Valid {
|
if kategorijaID.Valid {
|
||||||
a.KategorijaID = &kategorijaID.Int64
|
a.KategorijaID = &kategorijaID.Int64
|
||||||
}
|
}
|
||||||
|
if marza.Valid {
|
||||||
|
a.Marza = &marza.Float64
|
||||||
|
}
|
||||||
|
if katMarza.Valid {
|
||||||
|
a.KategorijaMarza = &katMarza.Float64
|
||||||
|
}
|
||||||
|
|
||||||
a.KriticnaZaliha = a.Kolicina <= a.KolicinMin
|
a.KriticnaZaliha = a.Kolicina <= a.KolicinMin
|
||||||
|
|
||||||
@@ -86,14 +93,15 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) {
|
func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) {
|
||||||
var a model.Artikal
|
var a model.Artikal
|
||||||
var kategorijaID sql.NullInt64
|
var kategorijaID sql.NullInt64
|
||||||
|
var marza sql.NullFloat64
|
||||||
|
|
||||||
err := r.db.QueryRowContext(ctx, `
|
err := r.db.QueryRowContext(ctx, `
|
||||||
SELECT id, kategorija_id, naziv, opis, kolicina, kolicina_min,
|
SELECT id, kategorija_id, naziv, opis, kolicina, kolicina_min,
|
||||||
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, napomena, datum_unosa
|
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena, datum_unosa
|
||||||
FROM artikli WHERE id = ?`, id).Scan(
|
FROM artikli WHERE id = ?`, id).Scan(
|
||||||
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
||||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
||||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.Napomena, &a.DatumUnosa,
|
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err)
|
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err)
|
||||||
@@ -102,6 +110,9 @@ func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal,
|
|||||||
if kategorijaID.Valid {
|
if kategorijaID.Valid {
|
||||||
a.KategorijaID = &kategorijaID.Int64
|
a.KategorijaID = &kategorijaID.Int64
|
||||||
}
|
}
|
||||||
|
if marza.Valid {
|
||||||
|
a.Marza = &marza.Float64
|
||||||
|
}
|
||||||
|
|
||||||
return &a, nil
|
return &a, nil
|
||||||
}
|
}
|
||||||
@@ -111,10 +122,10 @@ func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, err
|
|||||||
rezultat, err := r.db.ExecContext(ctx, `
|
rezultat, err := r.db.ExecContext(ctx, `
|
||||||
INSERT INTO artikli
|
INSERT INTO artikli
|
||||||
(kategorija_id, naziv, opis, kolicina, kolicina_min, lokacija,
|
(kategorija_id, naziv, opis, kolicina, kolicina_min, lokacija,
|
||||||
nabavna_cena, prodajna_cena, pdv_stopa, napomena)
|
nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina, a.KolicinMin,
|
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina, a.KolicinMin,
|
||||||
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Napomena,
|
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: ArtikalRepo.Kreiraj: %w", err)
|
return 0, fmt.Errorf("ntech: ArtikalRepo.Kreiraj: %w", err)
|
||||||
@@ -134,11 +145,11 @@ func (r *ArtikalRepo) Izmeni(ctx context.Context, a *model.Artikal) error {
|
|||||||
UPDATE artikli SET
|
UPDATE artikli SET
|
||||||
kategorija_id = ?, naziv = ?, opis = ?, kolicina = ?,
|
kategorija_id = ?, naziv = ?, opis = ?, kolicina = ?,
|
||||||
kolicina_min = ?, lokacija = ?,
|
kolicina_min = ?, lokacija = ?,
|
||||||
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, napomena = ?
|
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, marza = ?, napomena = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina,
|
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina,
|
||||||
a.KolicinMin, a.Lokacija,
|
a.KolicinMin, a.Lokacija,
|
||||||
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Napomena, a.ID,
|
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena, a.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ntech: ArtikalRepo.Izmeni: %w", err)
|
return fmt.Errorf("ntech: ArtikalRepo.Izmeni: %w", err)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func NovaKategorijaRepo(db *sql.DB) *KategorijaRepo {
|
|||||||
|
|
||||||
// Lista vraća sve kategorije
|
// Lista vraća sve kategorije
|
||||||
func (r *KategorijaRepo) Lista(ctx context.Context) ([]model.Kategorija, error) {
|
func (r *KategorijaRepo) Lista(ctx context.Context) ([]model.Kategorija, error) {
|
||||||
redovi, err := r.db.QueryContext(ctx, "SELECT id, naziv, opis FROM kategorije ORDER BY naziv ASC")
|
redovi, err := r.db.QueryContext(ctx, "SELECT id, naziv, opis, marza FROM kategorije ORDER BY naziv ASC")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: %w", err)
|
return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: %w", err)
|
||||||
}
|
}
|
||||||
@@ -30,12 +30,16 @@ func (r *KategorijaRepo) Lista(ctx context.Context) ([]model.Kategorija, error)
|
|||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var k model.Kategorija
|
var k model.Kategorija
|
||||||
var opis sql.NullString
|
var opis sql.NullString
|
||||||
if err := redovi.Scan(&k.ID, &k.Naziv, &opis); err != nil {
|
var marza sql.NullFloat64
|
||||||
|
if err := redovi.Scan(&k.ID, &k.Naziv, &opis, &marza); err != nil {
|
||||||
return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: scan: %w", err)
|
return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: scan: %w", err)
|
||||||
}
|
}
|
||||||
if opis.Valid {
|
if opis.Valid {
|
||||||
k.Opis = opis.String
|
k.Opis = opis.String
|
||||||
}
|
}
|
||||||
|
if marza.Valid {
|
||||||
|
k.Marza = &marza.Float64
|
||||||
|
}
|
||||||
rezultat = append(rezultat, k)
|
rezultat = append(rezultat, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +49,8 @@ func (r *KategorijaRepo) Lista(ctx context.Context) ([]model.Kategorija, error)
|
|||||||
// Kreiraj dodaje novu kategoriju
|
// Kreiraj dodaje novu kategoriju
|
||||||
func (r *KategorijaRepo) Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error) {
|
func (r *KategorijaRepo) Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error) {
|
||||||
rezultat, err := r.db.ExecContext(ctx,
|
rezultat, err := r.db.ExecContext(ctx,
|
||||||
"INSERT INTO kategorije (naziv, opis) VALUES (?, ?)",
|
"INSERT INTO kategorije (naziv, opis, marza) VALUES (?, ?, ?)",
|
||||||
k.Naziv, k.Opis,
|
k.Naziv, k.Opis, k.Marza,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: KategorijaRepo.Kreiraj: %w", err)
|
return 0, fmt.Errorf("ntech: KategorijaRepo.Kreiraj: %w", err)
|
||||||
@@ -59,3 +63,35 @@ func (r *KategorijaRepo) Kreiraj(ctx context.Context, k *model.Kategorija) (int6
|
|||||||
|
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DohvatiID vraća jednu kategoriju po ID-u
|
||||||
|
func (r *KategorijaRepo) DohvatiID(ctx context.Context, id int64) (*model.Kategorija, error) {
|
||||||
|
var k model.Kategorija
|
||||||
|
var opis sql.NullString
|
||||||
|
var marza sql.NullFloat64
|
||||||
|
err := r.db.QueryRowContext(ctx,
|
||||||
|
"SELECT id, naziv, opis, marza FROM kategorije WHERE id = ?", id).
|
||||||
|
Scan(&k.ID, &k.Naziv, &opis, &marza)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: KategorijaRepo.DohvatiID: %w", err)
|
||||||
|
}
|
||||||
|
if opis.Valid {
|
||||||
|
k.Opis = opis.String
|
||||||
|
}
|
||||||
|
if marza.Valid {
|
||||||
|
k.Marza = &marza.Float64
|
||||||
|
}
|
||||||
|
return &k, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Izmeni ažurira naziv, opis i maržu postojeće kategorije
|
||||||
|
func (r *KategorijaRepo) Izmeni(ctx context.Context, k *model.Kategorija) error {
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
"UPDATE kategorije SET naziv = ?, opis = ?, marza = ? WHERE id = ?",
|
||||||
|
k.Naziv, k.Opis, k.Marza, k.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: KategorijaRepo.Izmeni: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ 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.datum,
|
n.id, n.dobavljac_id, n.napomena, n.ukupno, n.metod_raspodele, 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 +36,10 @@ 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 sql.NullString
|
var napomena, metod sql.NullString
|
||||||
|
|
||||||
err := redovi.Scan(
|
err := redovi.Scan(
|
||||||
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &n.Datum,
|
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod, &n.Datum,
|
||||||
&n.DobavljacNaziv,
|
&n.DobavljacNaziv,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -50,6 +50,7 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err
|
|||||||
n.DobavljacID = &dobavljacID.Int64
|
n.DobavljacID = &dobavljacID.Int64
|
||||||
}
|
}
|
||||||
n.Napomena = napomena.String
|
n.Napomena = napomena.String
|
||||||
|
n.MetodRaspodele = metod.String
|
||||||
|
|
||||||
rezultat = append(rezultat, n)
|
rezultat = append(rezultat, n)
|
||||||
}
|
}
|
||||||
@@ -61,12 +62,12 @@ 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 sql.NullString
|
var napomena, metod sql.NullString
|
||||||
|
|
||||||
err := r.db.QueryRowContext(ctx, `
|
err := r.db.QueryRowContext(ctx, `
|
||||||
SELECT id, dobavljac_id, napomena, ukupno, datum
|
SELECT id, dobavljac_id, napomena, ukupno, metod_raspodele, datum
|
||||||
FROM nabavke WHERE id = ?`, id).Scan(
|
FROM nabavke WHERE id = ?`, id).Scan(
|
||||||
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &n.Datum,
|
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod, &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)
|
||||||
@@ -76,6 +77,7 @@ func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka,
|
|||||||
n.DobavljacID = &dobavljacID.Int64
|
n.DobavljacID = &dobavljacID.Int64
|
||||||
}
|
}
|
||||||
n.Napomena = napomena.String
|
n.Napomena = napomena.String
|
||||||
|
n.MetodRaspodele = metod.String
|
||||||
|
|
||||||
return &n, nil
|
return &n, nil
|
||||||
}
|
}
|
||||||
@@ -113,8 +115,33 @@ func (r *NabavkaRepo) DohvatiStavke(ctx context.Context, nabavkaID int64) ([]mod
|
|||||||
return rezultat, nil
|
return rezultat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kreiraj upisuje novu nabavku sa svim stavkama u jednoj transakciji i ažurira stanje magacina
|
// DohvatiTroskove vraća sve zavisne troškove jedne nabavke
|
||||||
func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke) (int64, error) {
|
func (r *NabavkaRepo) DohvatiTroskove(ctx context.Context, nabavkaID int64) ([]model.NabavkaTrosak, error) {
|
||||||
|
redovi, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT id, nabavka_id, naziv, iznos
|
||||||
|
FROM nabavka_troskovi
|
||||||
|
WHERE nabavka_id = ?
|
||||||
|
ORDER BY id ASC`, nabavkaID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiTroskove: %w", err)
|
||||||
|
}
|
||||||
|
defer redovi.Close()
|
||||||
|
|
||||||
|
var rezultat []model.NabavkaTrosak
|
||||||
|
for redovi.Next() {
|
||||||
|
var t model.NabavkaTrosak
|
||||||
|
if err := redovi.Scan(&t.ID, &t.NabavkaID, &t.Naziv, &t.Iznos); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiTroskove: scan: %w", err)
|
||||||
|
}
|
||||||
|
rezultat = append(rezultat, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rezultat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kreiraj upisuje novu nabavku sa svim stavkama i zavisnim troškovima u jednoj
|
||||||
|
// transakciji i ažurira stanje magacina
|
||||||
|
func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke, troskovi []model.NabavkaTrosak) (int64, error) {
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: begin: %w", err)
|
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: begin: %w", err)
|
||||||
@@ -130,9 +157,9 @@ func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []mo
|
|||||||
|
|
||||||
// upisujemo zaglavlje nabavke
|
// upisujemo zaglavlje nabavke
|
||||||
rezultat, err := tx.ExecContext(ctx, `
|
rezultat, err := tx.ExecContext(ctx, `
|
||||||
INSERT INTO nabavke (dobavljac_id, napomena, ukupno)
|
INSERT INTO nabavke (dobavljac_id, napomena, ukupno, metod_raspodele)
|
||||||
VALUES (?, ?, ?)`,
|
VALUES (?, ?, ?, ?)`,
|
||||||
nullInt64(n.DobavljacID), nullString(n.Napomena), ukupno,
|
nullInt64(n.DobavljacID), nullString(n.Napomena), ukupno, nullString(n.MetodRaspodele),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: insert nabavka: %w", err)
|
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: insert nabavka: %w", err)
|
||||||
@@ -143,6 +170,18 @@ func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []mo
|
|||||||
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: last insert id: %w", err)
|
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: last insert id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upisujemo zavisne troškove (ako ih ima)
|
||||||
|
for _, t := range troskovi {
|
||||||
|
_, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO nabavka_troskovi (nabavka_id, naziv, iznos)
|
||||||
|
VALUES (?, ?, ?)`,
|
||||||
|
nabavkaID, t.Naziv, t.Iznos,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: insert trošak: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// upisujemo svaku stavku i ažuriramo stanje artikla u magacinu
|
// upisujemo svaku stavku i ažuriramo stanje artikla u magacinu
|
||||||
for _, s := range stavke {
|
for _, s := range stavke {
|
||||||
_, err := tx.ExecContext(ctx, `
|
_, err := tx.ExecContext(ctx, `
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"ntech/internal/db/sqlite"
|
"ntech/internal/db/sqlite"
|
||||||
"ntech/internal/model"
|
"ntech/internal/model"
|
||||||
@@ -64,6 +65,7 @@ func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) {
|
|||||||
k := &model.Kategorija{
|
k := &model.Kategorija{
|
||||||
Naziv: naziv,
|
Naziv: naziv,
|
||||||
Opis: r.FormValue("opis"),
|
Opis: r.FormValue("opis"),
|
||||||
|
Marza: parsirajMarzu(r.FormValue("marza")),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.KategorijeRepo.Kreiraj(r.Context(), k); err != nil {
|
if _, err := h.KategorijeRepo.Kreiraj(r.Context(), k); err != nil {
|
||||||
@@ -74,6 +76,56 @@ func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/magacin/kategorije?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/magacin/kategorije?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IzmeniKategoriju prima POST i ažurira naziv, opis i maržu postojeće kategorije
|
||||||
|
func (h *Handler) IzmeniKategoriju(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, ok := h.zahtevajDozvolu(w, r, "kategorija.izmeni"); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Neispravan ID kategorije", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
naziv := r.FormValue("naziv")
|
||||||
|
if naziv == "" {
|
||||||
|
http.Redirect(w, r, "/magacin/kategorije", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
k := &model.Kategorija{
|
||||||
|
ID: id,
|
||||||
|
Naziv: naziv,
|
||||||
|
Opis: r.FormValue("opis"),
|
||||||
|
Marza: parsirajMarzu(r.FormValue("marza")),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.KategorijeRepo.Izmeni(r.Context(), k); err != nil {
|
||||||
|
http.Error(w, "Greška pri čuvanju izmene kategorije", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/magacin/kategorije?sacuvano=1", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsirajMarzu pretvara tekst iz forme u *float64; prazno/neispravno → nil (NULL u bazi)
|
||||||
|
func parsirajMarzu(s string) *float64 {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil || v < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
// ObrisiKategoriju briše kategoriju po ID-u
|
// ObrisiKategoriju briše kategoriju po ID-u
|
||||||
func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, ok := h.zahtevajDozvolu(w, r, "kategorija.obrisi"); !ok {
|
if _, ok := h.zahtevajDozvolu(w, r, "kategorija.obrisi"); !ok {
|
||||||
|
|||||||
@@ -249,6 +249,15 @@ func parseFormuArtikla(r *http.Request) (model.Artikal, string) {
|
|||||||
artikal.ProdajnaCena = v
|
artikal.ProdajnaCena = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// marža (%) je opciona; prazno polje ostaje NULL (artikal nasleđuje maržu kategorije/globalnu)
|
||||||
|
if m := r.FormValue("marza"); m != "" {
|
||||||
|
v, err := strconv.ParseFloat(m, 64)
|
||||||
|
if err != nil || v < 0 {
|
||||||
|
return artikal, "Marža mora biti pozitivan broj."
|
||||||
|
}
|
||||||
|
artikal.Marza = &v
|
||||||
|
}
|
||||||
|
|
||||||
if katID := r.FormValue("kategorija_id"); katID != "" {
|
if katID := r.FormValue("kategorija_id"); katID != "" {
|
||||||
id, err := strconv.ParseInt(katID, 10, 64)
|
id, err := strconv.ParseInt(katID, 10, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
+70
-15
@@ -39,19 +39,26 @@ type PodaciDetaljiNabavke struct {
|
|||||||
model.PodaciStranice
|
model.PodaciStranice
|
||||||
Nabavka model.Nabavka
|
Nabavka model.Nabavka
|
||||||
Stavke []model.StavkaSaArtiklom
|
Stavke []model.StavkaSaArtiklom
|
||||||
|
Troskovi []model.NabavkaTrosak
|
||||||
|
UkupanTrosak float64
|
||||||
DobavljacNaziv string
|
DobavljacNaziv string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
func artikalUJSON(artikli []model.ArtikalSaKategorijom) template.JS {
|
func artikalUJSON(artikli []model.ArtikalSaKategorijom) template.JS {
|
||||||
type stavka struct {
|
type stavka struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Naziv string `json:"naziv"`
|
Naziv string `json:"naziv"`
|
||||||
PdvStopa float64 `json:"pdv_stopa"`
|
PdvStopa float64 `json:"pdv_stopa"`
|
||||||
|
Marza *float64 `json:"marza"` // marža artikla; null = nije postavljeno
|
||||||
|
KategorijaMarza *float64 `json:"kategorija_marza"` // marža kategorije; fallback ako artikal nema
|
||||||
}
|
}
|
||||||
lista := make([]stavka, 0, len(artikli))
|
lista := make([]stavka, 0, len(artikli))
|
||||||
for _, a := range artikli {
|
for _, a := range artikli {
|
||||||
lista = append(lista, stavka{ID: a.ID, Naziv: a.Naziv, PdvStopa: a.PdvStopa})
|
lista = append(lista, stavka{
|
||||||
|
ID: a.ID, Naziv: a.Naziv, PdvStopa: a.PdvStopa,
|
||||||
|
Marza: a.Marza, KategorijaMarza: a.KategorijaMarza,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(lista)
|
b, _ := json.Marshal(lista)
|
||||||
return template.JS(b)
|
return template.JS(b)
|
||||||
@@ -134,7 +141,7 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nabavka, stavke, greska := parseFormuNabavke(r)
|
nabavka, stavke, troskovi, greska := parseFormuNabavke(r)
|
||||||
if greska != "" {
|
if greska != "" {
|
||||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
artikli, _ := h.Artikli.Lista(r.Context(), db.ArtikalFilter{})
|
artikli, _ := h.Artikli.Lista(r.Context(), db.ArtikalFilter{})
|
||||||
@@ -155,7 +162,7 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := h.NabavkeRepo.Kreiraj(r.Context(), &nabavka, stavke)
|
id, err := h.NabavkeRepo.Kreiraj(r.Context(), &nabavka, stavke, troskovi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Greška pri čuvanju nabavke", http.StatusInternalServerError)
|
http.Error(w, "Greška pri čuvanju nabavke", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -194,6 +201,13 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
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
|
||||||
@@ -207,7 +221,7 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
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
|
||||||
}
|
}
|
||||||
if e := h.Artikli.AzurirajCene(r.Context(), s.ArtikalID, s.CenaPoKomadu, prodajna); e != nil {
|
if e := h.Artikli.AzurirajCene(r.Context(), s.ArtikalID, kalkNabavna[i], 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
|
||||||
}
|
}
|
||||||
@@ -247,6 +261,12 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
troskovi, err := h.NabavkeRepo.DohvatiTroskove(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Greška pri učitavanju troškova", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||||
@@ -265,10 +285,16 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
|
|||||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||||
ps.Stranica = "nabavke"
|
ps.Stranica = "nabavke"
|
||||||
ps.NaslovStranice = "Detalji nabavke"
|
ps.NaslovStranice = "Detalji nabavke"
|
||||||
|
var ukupanTrosak float64
|
||||||
|
for _, t := range troskovi {
|
||||||
|
ukupanTrosak += t.Iznos
|
||||||
|
}
|
||||||
podaci := PodaciDetaljiNabavke{
|
podaci := PodaciDetaljiNabavke{
|
||||||
PodaciStranice: ps,
|
PodaciStranice: ps,
|
||||||
Nabavka: *nabavka,
|
Nabavka: *nabavka,
|
||||||
Stavke: stavke,
|
Stavke: stavke,
|
||||||
|
Troskovi: troskovi,
|
||||||
|
UkupanTrosak: ukupanTrosak,
|
||||||
DobavljacNaziv: dobavljacNaziv,
|
DobavljacNaziv: dobavljacNaziv,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,8 +324,9 @@ func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/nabavke?obrisan=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/nabavke?obrisan=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseFormuNabavke čita zaglavlje i stavke iz HTTP forme i vraća model i eventualnu grešku
|
// parseFormuNabavke čita zaglavlje, stavke i zavisne troškove iz HTTP forme
|
||||||
func parseFormuNabavke(r *http.Request) (model.Nabavka, []model.StavkaNabavke, string) {
|
// i vraća model, stavke, troškove i eventualnu grešku
|
||||||
|
func parseFormuNabavke(r *http.Request) (model.Nabavka, []model.StavkaNabavke, []model.NabavkaTrosak, string) {
|
||||||
var nabavka model.Nabavka
|
var nabavka model.Nabavka
|
||||||
|
|
||||||
// opcioni dobavljač
|
// opcioni dobavljač
|
||||||
@@ -317,28 +344,28 @@ func parseFormuNabavke(r *http.Request) (model.Nabavka, []model.StavkaNabavke, s
|
|||||||
cene := r.Form["cena_po_komadu[]"]
|
cene := r.Form["cena_po_komadu[]"]
|
||||||
|
|
||||||
if len(artikalIDovi) == 0 {
|
if len(artikalIDovi) == 0 {
|
||||||
return nabavka, nil, "Nabavka mora imati najmanje jednu stavku."
|
return nabavka, nil, nil, "Nabavka mora imati najmanje jednu stavku."
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(artikalIDovi) != len(kolicine) || len(artikalIDovi) != len(cene) {
|
if len(artikalIDovi) != len(kolicine) || len(artikalIDovi) != len(cene) {
|
||||||
return nabavka, nil, "Greška u podacima forme — broj stavki nije ispravan."
|
return nabavka, nil, nil, "Greška u podacima forme — broj stavki nije ispravan."
|
||||||
}
|
}
|
||||||
|
|
||||||
var stavke []model.StavkaNabavke
|
var stavke []model.StavkaNabavke
|
||||||
for i := range artikalIDovi {
|
for i := range artikalIDovi {
|
||||||
artikalID, err := strconv.ParseInt(strings.TrimSpace(artikalIDovi[i]), 10, 64)
|
artikalID, err := strconv.ParseInt(strings.TrimSpace(artikalIDovi[i]), 10, 64)
|
||||||
if err != nil || artikalID <= 0 {
|
if err != nil || artikalID <= 0 {
|
||||||
return nabavka, nil, "Neispravan artikal u stavci."
|
return nabavka, nil, nil, "Neispravan artikal u stavci."
|
||||||
}
|
}
|
||||||
|
|
||||||
kolicina, err := strconv.Atoi(strings.TrimSpace(kolicine[i]))
|
kolicina, err := strconv.Atoi(strings.TrimSpace(kolicine[i]))
|
||||||
if err != nil || kolicina <= 0 {
|
if err != nil || kolicina <= 0 {
|
||||||
return nabavka, nil, "Količina mora biti pozitivan broj."
|
return nabavka, nil, nil, "Količina mora biti pozitivan broj."
|
||||||
}
|
}
|
||||||
|
|
||||||
cena, err := strconv.ParseFloat(strings.TrimSpace(cene[i]), 64)
|
cena, err := strconv.ParseFloat(strings.TrimSpace(cene[i]), 64)
|
||||||
if err != nil || cena < 0 {
|
if err != nil || cena < 0 {
|
||||||
return nabavka, nil, "Cena mora biti pozitivan broj."
|
return nabavka, nil, nil, "Cena mora biti pozitivan broj."
|
||||||
}
|
}
|
||||||
|
|
||||||
stavke = append(stavke, model.StavkaNabavke{
|
stavke = append(stavke, model.StavkaNabavke{
|
||||||
@@ -348,7 +375,35 @@ func parseFormuNabavke(r *http.Request) (model.Nabavka, []model.StavkaNabavke, s
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nabavka, stavke, ""
|
// zavisni troškovi (opcioni, paralelni nizovi); prazni redovi se preskaču
|
||||||
|
naziviT := r.Form["trosak_naziv[]"]
|
||||||
|
iznosiT := r.Form["trosak_iznos[]"]
|
||||||
|
var troskovi []model.NabavkaTrosak
|
||||||
|
for i := range naziviT {
|
||||||
|
naziv := strings.TrimSpace(naziviT[i])
|
||||||
|
if naziv == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var iznos float64
|
||||||
|
if i < len(iznosiT) {
|
||||||
|
iznos, _ = strconv.ParseFloat(strings.TrimSpace(iznosiT[i]), 64)
|
||||||
|
}
|
||||||
|
if iznos <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
troskovi = append(troskovi, model.NabavkaTrosak{Naziv: naziv, Iznos: iznos})
|
||||||
|
}
|
||||||
|
|
||||||
|
// metod raspodele je bitan samo ako ima troškova; default je po vrednosti
|
||||||
|
if len(troskovi) > 0 {
|
||||||
|
metod := r.FormValue("metod_raspodele")
|
||||||
|
if metod != "kolicina" {
|
||||||
|
metod = "vrednost"
|
||||||
|
}
|
||||||
|
nabavka.MetodRaspodele = metod
|
||||||
|
}
|
||||||
|
|
||||||
|
return nabavka, stavke, troskovi, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderujFormuNabavke renderuje HTML šablon forme za unos nove nabavke
|
// renderujFormuNabavke renderuje HTML šablon forme za unos nove nabavke
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ var sveAkcije = []string{
|
|||||||
"artikal.premesti",
|
"artikal.premesti",
|
||||||
"kategorija.pregled",
|
"kategorija.pregled",
|
||||||
"kategorija.dodaj",
|
"kategorija.dodaj",
|
||||||
|
"kategorija.izmeni",
|
||||||
"kategorija.obrisi",
|
"kategorija.obrisi",
|
||||||
"nabavka.pregled",
|
"nabavka.pregled",
|
||||||
"nabavka.dodaj",
|
"nabavka.dodaj",
|
||||||
@@ -62,7 +63,7 @@ func ImaDozvolu(uloga, akcija string) bool {
|
|||||||
"artikal.obrisi", "artikal.premesti":
|
"artikal.obrisi", "artikal.premesti":
|
||||||
return true
|
return true
|
||||||
// kategorija
|
// kategorija
|
||||||
case "kategorija.pregled", "kategorija.dodaj", "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":
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Artikal struct {
|
|||||||
NabavnaCena float64
|
NabavnaCena float64
|
||||||
ProdajnaCena float64
|
ProdajnaCena float64
|
||||||
PdvStopa float64
|
PdvStopa float64
|
||||||
|
Marza *float64 // podrazumevana marža (%) za kalkulaciju; NULL = nije postavljeno
|
||||||
Napomena string
|
Napomena string
|
||||||
DatumUnosa time.Time
|
DatumUnosa time.Time
|
||||||
}
|
}
|
||||||
@@ -33,11 +34,13 @@ type Kategorija struct {
|
|||||||
ID int64
|
ID int64
|
||||||
Naziv string
|
Naziv string
|
||||||
Opis string
|
Opis string
|
||||||
|
Marza *float64 // podrazumevana marža (%) za artikle ove kategorije; NULL = nije postavljeno
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArtikalSaKategorijom je artikal sa nazivom kategorije — za prikaz u tabeli
|
// ArtikalSaKategorijom je artikal sa nazivom kategorije — za prikaz u tabeli
|
||||||
type ArtikalSaKategorijom struct {
|
type ArtikalSaKategorijom struct {
|
||||||
Artikal
|
Artikal
|
||||||
KategorijaNaziv string
|
KategorijaNaziv string
|
||||||
|
KategorijaMarza *float64 // marža kategorije; za fallback predloga marže pri nabavci
|
||||||
KriticnaZaliha bool
|
KriticnaZaliha bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// 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
|
||||||
Datum time.Time
|
MetodRaspodele string // "vrednost" ili "kolicina"; prazno = nema zavisnih troškova
|
||||||
|
Datum time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NabavkaTrosak je jedna stavka zavisnih troškova nabavke (npr. prevoz, carina)
|
||||||
|
type NabavkaTrosak struct {
|
||||||
|
ID int64
|
||||||
|
NabavkaID int64
|
||||||
|
Naziv string
|
||||||
|
Iznos float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// StavkaNabavke predstavlja jednu liniju (artikal) unutar nabavke
|
// StavkaNabavke predstavlja jednu liniju (artikal) unutar nabavke
|
||||||
@@ -21,6 +33,44 @@ type StavkaNabavke struct {
|
|||||||
Ukupno float64
|
Ukupno float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RasporediTroskove raspodeljuje ukupan zavisni trošak na stavke nabavke i vraća
|
||||||
|
// kalkulativnu nabavnu cenu po komadu za svaku stavku (isti redosled kao ulazne stavke).
|
||||||
|
// metod "kolicina" deli trošak po broju komada; svaka druga vrednost (uklj. "vrednost")
|
||||||
|
// deli po nabavnoj vrednosti stavke (količina × cena).
|
||||||
|
// Ako nema troška ili je osnovica nula, vraća nepromenjenu cenu po komadu.
|
||||||
|
func RasporediTroskove(stavke []StavkaNabavke, ukupanTrosak float64, metod string) []float64 {
|
||||||
|
kalk := make([]float64, len(stavke))
|
||||||
|
|
||||||
|
// osnovica raspodele po izabranom metodu
|
||||||
|
var osnovica float64
|
||||||
|
for _, s := range stavke {
|
||||||
|
if metod == "kolicina" {
|
||||||
|
osnovica += float64(s.Kolicina)
|
||||||
|
} else {
|
||||||
|
osnovica += float64(s.Kolicina) * s.CenaPoKomadu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, s := range stavke {
|
||||||
|
kalk[i] = s.CenaPoKomadu
|
||||||
|
// bez troška, bez osnovice ili bez količine — nema šta da se raspodeli
|
||||||
|
if ukupanTrosak <= 0 || osnovica <= 0 || s.Kolicina == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var udeo float64
|
||||||
|
if metod == "kolicina" {
|
||||||
|
udeo = float64(s.Kolicina) / osnovica
|
||||||
|
} else {
|
||||||
|
udeo = (float64(s.Kolicina) * s.CenaPoKomadu) / osnovica
|
||||||
|
}
|
||||||
|
trosakPoKomadu := (ukupanTrosak * udeo) / float64(s.Kolicina)
|
||||||
|
// zaokruženo na 2 decimale — kalkulativna nabavna je cena koja se čuva
|
||||||
|
kalk[i] = math.Round((s.CenaPoKomadu+trosakPoKomadu)*100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return kalk
|
||||||
|
}
|
||||||
|
|
||||||
// NabavkaSaDetaljem je nabavka sa nazivom dobavljača — za prikaz u listi
|
// NabavkaSaDetaljem je nabavka sa nazivom dobavljača — za prikaz u listi
|
||||||
type NabavkaSaDetaljem struct {
|
type NabavkaSaDetaljem struct {
|
||||||
Nabavka
|
Nabavka
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// proverava raspodelu zavisnih troškova na stavke nabavke po oba metoda
|
||||||
|
func TestRasporediTroskove(t *testing.T) {
|
||||||
|
// dve stavke: A = 10×100 (vrednost 1000), B = 10×200 (vrednost 2000)
|
||||||
|
stavke := []StavkaNabavke{
|
||||||
|
{Kolicina: 10, CenaPoKomadu: 100},
|
||||||
|
{Kolicina: 10, CenaPoKomadu: 200},
|
||||||
|
}
|
||||||
|
|
||||||
|
slucajevi := []struct {
|
||||||
|
naziv string
|
||||||
|
stavke []StavkaNabavke
|
||||||
|
trosak float64
|
||||||
|
metod string
|
||||||
|
ocekuje []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
naziv: "bez troška — cena nepromenjena",
|
||||||
|
stavke: stavke,
|
||||||
|
trosak: 0,
|
||||||
|
metod: "vrednost",
|
||||||
|
ocekuje: []float64{100, 200},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// trošak 300 po vrednosti: A nosi 1/3 (100 → 10/kom), B nosi 2/3 (200 → 20/kom)
|
||||||
|
naziv: "po vrednosti",
|
||||||
|
stavke: stavke,
|
||||||
|
trosak: 300,
|
||||||
|
metod: "vrednost",
|
||||||
|
ocekuje: []float64{110, 220},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// trošak 300 po količini: svaka stavka 1/2 (150 → 15/kom)
|
||||||
|
naziv: "po količini",
|
||||||
|
stavke: stavke,
|
||||||
|
trosak: 300,
|
||||||
|
metod: "kolicina",
|
||||||
|
ocekuje: []float64{115, 215},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// osnovica po vrednosti je 0 (sve cene 0) — nema raspodele
|
||||||
|
naziv: "nulta osnovica vrednosti",
|
||||||
|
stavke: []StavkaNabavke{{Kolicina: 5, CenaPoKomadu: 0}},
|
||||||
|
trosak: 100,
|
||||||
|
metod: "vrednost",
|
||||||
|
ocekuje: []float64{0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// stavka bez količine se preskače (bez deljenja nulom)
|
||||||
|
naziv: "količina nula",
|
||||||
|
stavke: []StavkaNabavke{{Kolicina: 0, CenaPoKomadu: 50}},
|
||||||
|
trosak: 100,
|
||||||
|
metod: "kolicina",
|
||||||
|
ocekuje: []float64{50},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range slucajevi {
|
||||||
|
t.Run(s.naziv, func(t *testing.T) {
|
||||||
|
dobijeno := RasporediTroskove(s.stavke, s.trosak, s.metod)
|
||||||
|
if len(dobijeno) != len(s.ocekuje) {
|
||||||
|
t.Fatalf("dužina = %d, očekivano %d", len(dobijeno), len(s.ocekuje))
|
||||||
|
}
|
||||||
|
for i := range s.ocekuje {
|
||||||
|
if dobijeno[i] != s.ocekuje[i] {
|
||||||
|
t.Errorf("stavka %d: dobijeno %.2f, očekivano %.2f", i, dobijeno[i], s.ocekuje[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Dodaje podrazumevanu maržu (%) na artikle i kategorije za kalkulaciju
|
||||||
|
-- prodajne cene pri nabavci. Kolone su NULL kada marža nije postavljena —
|
||||||
|
-- tako se „nije postavljeno" razlikuje od svesno unete marže 0%.
|
||||||
|
-- Redosled izvođenja marže: artikal.marza → kategorija.marza → globalna kalkulacija_marza.
|
||||||
|
ALTER TABLE artikli ADD COLUMN marza REAL;
|
||||||
|
ALTER TABLE kategorije ADD COLUMN marza REAL;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Zavisni troškovi nabavke (carina, prevoz, špedicija…) i metod njihove raspodele.
|
||||||
|
-- Troškovi se raspodeljuju na stavke i ulaze u kalkulativnu nabavnu cenu, iz koje
|
||||||
|
-- se računa prodajna. Slobodne stavke (naziv + iznos), više redova po nabavci.
|
||||||
|
|
||||||
|
-- metod raspodele po nabavci: 'vrednost' (po nabavnoj vrednosti stavke) ili 'kolicina'.
|
||||||
|
-- NULL/prazno = nema zavisnih troškova (kalkulacija kao u Fazi B).
|
||||||
|
ALTER TABLE nabavke ADD COLUMN metod_raspodele TEXT;
|
||||||
|
|
||||||
|
-- pojedinačne stavke zavisnih troškova; brišu se zajedno sa nabavkom (CASCADE)
|
||||||
|
CREATE TABLE IF NOT EXISTS nabavka_troskovi (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nabavka_id INTEGER NOT NULL REFERENCES nabavke(id) ON DELETE CASCADE,
|
||||||
|
naziv TEXT NOT NULL,
|
||||||
|
iznos REAL NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nabavka_troskovi_nabavka ON nabavka_troskovi(nabavka_id);
|
||||||
+50
-3
@@ -189,6 +189,8 @@ document.addEventListener('alpine:init', () => {
|
|||||||
stavke: [{artikal_id: '', kolicina: 1, cena: 0, marza: 0, prodajna: 0}],
|
stavke: [{artikal_id: '', kolicina: 1, cena: 0, marza: 0, prodajna: 0}],
|
||||||
artikliOpcije: [],
|
artikliOpcije: [],
|
||||||
marzaDefault: 0,
|
marzaDefault: 0,
|
||||||
|
troskovi: [], // zavisni troškovi {naziv, iznos}
|
||||||
|
metodRaspodele: 'vrednost', // 'vrednost' ili 'kolicina'
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
modal: false,
|
modal: false,
|
||||||
modalUcitavanje: false,
|
modalUcitavanje: false,
|
||||||
@@ -222,21 +224,66 @@ document.addEventListener('alpine:init', () => {
|
|||||||
},
|
},
|
||||||
dodajStavku() {
|
dodajStavku() {
|
||||||
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, marza: this.marzaDefault, prodajna: 0})
|
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, marza: this.marzaDefault, prodajna: 0})
|
||||||
|
this.preracunajSve()
|
||||||
},
|
},
|
||||||
// PDV stopa izabranog artikla (iz JSON liste) — za obračun prodajne cene
|
// PDV stopa izabranog artikla (iz JSON liste) — za obračun prodajne cene
|
||||||
pdvStopa(artikalId) {
|
pdvStopa(artikalId) {
|
||||||
const a = this.artikliOpcije.find(x => String(x.id) === String(artikalId))
|
const a = this.artikliOpcije.find(x => String(x.id) === String(artikalId))
|
||||||
return a ? (parseFloat(a.pdv_stopa) || 0) : 0
|
return a ? (parseFloat(a.pdv_stopa) || 0) : 0
|
||||||
},
|
},
|
||||||
// prodajna (sa PDV) = nabavna × (1 + marža/100) × (1 + pdvStopa/100), zaokruženo na 2 decimale
|
// pri izboru artikla predloži maržu: artikal → kategorija → globalna, pa izračunaj prodajnu
|
||||||
izracunajProdajnu(s) {
|
izaberiArtikal(s) {
|
||||||
|
const a = this.artikliOpcije.find(x => String(x.id) === String(s.artikal_id))
|
||||||
|
if (a) {
|
||||||
|
if (a.marza != null) s.marza = a.marza
|
||||||
|
else if (a.kategorija_marza != null) s.marza = a.kategorija_marza
|
||||||
|
else s.marza = this.marzaDefault
|
||||||
|
}
|
||||||
|
this.izracunajProdajnu(s)
|
||||||
|
},
|
||||||
|
// ukupan zavisni trošak nabavke
|
||||||
|
ukupanTrosak() {
|
||||||
|
return this.troskovi.reduce((z, t) => z + (parseFloat(t.iznos) || 0), 0)
|
||||||
|
},
|
||||||
|
// osnovica raspodele po izabranom metodu (zbir po svim stavkama)
|
||||||
|
osnovicaRaspodele() {
|
||||||
|
return this.stavke.reduce((z, s) => {
|
||||||
|
const kol = parseFloat(s.kolicina) || 0
|
||||||
|
return z + (this.metodRaspodele === 'kolicina' ? kol : kol * (parseFloat(s.cena) || 0))
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
|
// kalkulativna nabavna cena po komadu (fakturna + raspodeljeni trošak) — isto kao server
|
||||||
|
kalkNabavna(s) {
|
||||||
const cena = parseFloat(s.cena) || 0
|
const cena = parseFloat(s.cena) || 0
|
||||||
|
const kol = parseFloat(s.kolicina) || 0
|
||||||
|
const trosak = this.ukupanTrosak()
|
||||||
|
const osn = this.osnovicaRaspodele()
|
||||||
|
if (trosak <= 0 || osn <= 0 || kol <= 0) return cena
|
||||||
|
const baza = this.metodRaspodele === 'kolicina' ? kol : kol * cena
|
||||||
|
const trosakPoKomadu = (trosak * (baza / osn)) / kol
|
||||||
|
return Math.round((cena + trosakPoKomadu) * 100) / 100
|
||||||
|
},
|
||||||
|
// prodajna (sa PDV) = kalkulativna nabavna × (1 + marža/100) × (1 + pdvStopa/100)
|
||||||
|
izracunajProdajnu(s) {
|
||||||
|
const nabavna = this.kalkNabavna(s)
|
||||||
const marza = parseFloat(s.marza) || 0
|
const marza = parseFloat(s.marza) || 0
|
||||||
const pdv = this.pdvStopa(s.artikal_id)
|
const pdv = this.pdvStopa(s.artikal_id)
|
||||||
s.prodajna = Math.round(cena * (1 + marza / 100) * (1 + pdv / 100) * 100) / 100
|
s.prodajna = Math.round(nabavna * (1 + marza / 100) * (1 + pdv / 100) * 100) / 100
|
||||||
|
},
|
||||||
|
// raspodela zavisi od svih stavki — promena troška/metoda/količine/cene preračunava sve
|
||||||
|
preracunajSve() {
|
||||||
|
this.stavke.forEach(s => this.izracunajProdajnu(s))
|
||||||
|
},
|
||||||
|
dodajTrosak() {
|
||||||
|
this.troskovi.push({naziv: '', iznos: 0})
|
||||||
|
},
|
||||||
|
ukloniTrosak(i) {
|
||||||
|
this.troskovi.splice(i, 1)
|
||||||
|
this.preracunajSve()
|
||||||
},
|
},
|
||||||
ukloniStavku(i) {
|
ukloniStavku(i) {
|
||||||
if (this.stavke.length > 1) this.stavke.splice(i, 1)
|
if (this.stavke.length > 1) this.stavke.splice(i, 1)
|
||||||
|
this.preracunajSve()
|
||||||
},
|
},
|
||||||
ukupnoStavke(s) {
|
ukupnoStavke(s) {
|
||||||
return (parseFloat(s.kolicina) * parseFloat(s.cena) || 0).toFixed(2)
|
return (parseFloat(s.kolicina) * parseFloat(s.cena) || 0).toFixed(2)
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
<input type="text" name="opis" placeholder="Kratak opis kategorije..."
|
<input type="text" name="opis" placeholder="Kratak opis kategorije..."
|
||||||
style="width:100%;">
|
style="width:100%;">
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Marža (%)</label>
|
||||||
|
<input type="number" name="marza" min="0" step="0.01"
|
||||||
|
placeholder="prazno = globalna marža" style="width:100%;">
|
||||||
|
</div>
|
||||||
<div style="display:flex;justify-content:flex-end;">
|
<div style="display:flex;justify-content:flex-end;">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
style="padding:8px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">
|
style="padding:8px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">
|
||||||
@@ -76,6 +81,38 @@
|
|||||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">{{.Opis}}</div>
|
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">{{.Opis}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
{{if .Marza}}
|
||||||
|
<span style="font-size:12px;color:var(--tekst-sporedni);white-space:nowrap;">marža {{.Marza}}%</span>
|
||||||
|
{{end}}
|
||||||
|
{{if index $.Dozvole "kategorija.izmeni"}}
|
||||||
|
<button type="button" class="btn-primarno-malo" onclick="this.nextElementSibling.showModal()">Izmeni</button>
|
||||||
|
{{/* nativni modal — isti obrazac kao u magacinu (top layer, centriran) */}}
|
||||||
|
<dialog id="kat-{{.ID}}" class="premesti-modal" onclick="if(event.target===this)this.close()">
|
||||||
|
<form method="dialog" class="premesti-zaglavlje">
|
||||||
|
<h3>Izmeni kategoriju</h3>
|
||||||
|
<button type="submit" class="premesti-zatvori" aria-label="Zatvori">×</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/magacin/kategorije/izmeni/{{.ID}}" style="display:flex;flex-direction:column;gap:12px;padding:16px;">
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Naziv <span style="color:#dc2626;">*</span></label>
|
||||||
|
<input type="text" name="naziv" value="{{.Naziv}}" required
|
||||||
|
style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Opis</label>
|
||||||
|
<input type="text" name="opis" value="{{.Opis}}"
|
||||||
|
style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Marža (%)</label>
|
||||||
|
<input type="number" name="marza" min="0" step="0.01" value="{{if .Marza}}{{.Marza}}{{end}}"
|
||||||
|
placeholder="prazno = globalna marža"
|
||||||
|
style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primarno" style="align-self:flex-end;">Sačuvaj</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
{{end}}
|
||||||
{{if index $.Dozvole "kategorija.obrisi"}}
|
{{if index $.Dozvole "kategorija.obrisi"}}
|
||||||
<a href="/magacin/kategorije/obrisi/{{.ID}}" class="btn-obrisi-malo"
|
<a href="/magacin/kategorije/obrisi/{{.ID}}" class="btn-obrisi-malo"
|
||||||
data-potvrda="Da li ste sigurni da želite da obrišete ovu kategoriju?">
|
data-potvrda="Da li ste sigurni da želite da obrišete ovu kategoriju?">
|
||||||
|
|||||||
@@ -71,6 +71,13 @@
|
|||||||
<input type="number" name="prodajna_cena" value="{{.Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;">
|
<input type="number" name="prodajna_cena" value="{{.Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- marža za kalkulaciju; prazno = nasleđuje maržu kategorije ili globalnu -->
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Marža (%)</label>
|
||||||
|
<input type="number" name="marza" value="{{if .Artikal.Marza}}{{.Artikal.Marza}}{{end}}" min="0" step="0.01" style="width:100%;"
|
||||||
|
placeholder="prazno = po kategoriji / globalna">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- lokacija -->
|
<!-- lokacija -->
|
||||||
<div>
|
<div>
|
||||||
<label class="polje-labela">Lokacija u magacinu</label>
|
<label class="polje-labela">Lokacija u magacinu</label>
|
||||||
|
|||||||
@@ -122,6 +122,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- zavisni troškovi -->
|
||||||
|
{{if .Troskovi}}
|
||||||
|
<div class="kartica detalji-kartica animiraj" style="padding:0;overflow:hidden;">
|
||||||
|
<div style="padding:14px 16px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
|
||||||
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Zavisni troškovi</span>
|
||||||
|
<span class="pomocni-tekst">
|
||||||
|
Raspodela: {{if eq .Nabavka.MetodRaspodele "kolicina"}}po količini{{else}}po vrednosti stavke{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="padding:8px 16px;">
|
||||||
|
{{range .Troskovi}}
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<span style="font-size:14px;color:var(--tekst-glavni);">{{.Naziv}}</span>
|
||||||
|
<span style="font-size:14px;color:var(--tekst-sporedni);">{{printf "%.2f" .Iznos}} din</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;">
|
||||||
|
<span style="font-size:13px;font-weight:500;color:var(--tekst-sporedni);">Ukupno troškovi:</span>
|
||||||
|
<span style="font-size:15px;font-weight:600;color:var(--tekst-glavni);">{{printf "%.2f" .UkupanTrosak}} din</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- zona za brisanje -->
|
<!-- zona za brisanje -->
|
||||||
<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;">
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
<td style="padding:8px 10px;">
|
<td style="padding:8px 10px;">
|
||||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
||||||
@change="izracunajProdajnu(stavka)" :disabled="isMobile" style="width:100%;">
|
@change="izaberiArtikal(stavka)" :disabled="isMobile" style="width:100%;">
|
||||||
<option value="">— odaberi artikal —</option>
|
<option value="">— odaberi artikal —</option>
|
||||||
<template x-for="a in artikliOpcije" :key="a.id">
|
<template x-for="a in artikliOpcije" :key="a.id">
|
||||||
<option :value="a.id" x-text="a.naziv"></option>
|
<option :value="a.id" x-text="a.naziv"></option>
|
||||||
@@ -107,11 +107,12 @@
|
|||||||
</td>
|
</td>
|
||||||
<td style="padding:8px 10px;">
|
<td style="padding:8px 10px;">
|
||||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina"
|
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina"
|
||||||
|
@input="preracunajSve()"
|
||||||
min="1" :disabled="isMobile" style="width:100%;text-align:center;">
|
min="1" :disabled="isMobile" style="width:100%;text-align:center;">
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:8px 10px;">
|
<td style="padding:8px 10px;">
|
||||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena"
|
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena"
|
||||||
@input="izracunajProdajnu(stavka)"
|
@input="preracunajSve()"
|
||||||
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
|
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:8px 10px;">
|
<td style="padding:8px 10px;">
|
||||||
@@ -166,7 +167,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
|
||||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
||||||
@change="izracunajProdajnu(stavka)" :disabled="!isMobile" style="width:100%;">
|
@change="izaberiArtikal(stavka)" :disabled="!isMobile" style="width:100%;">
|
||||||
<option value="">— odaberi artikal —</option>
|
<option value="">— odaberi artikal —</option>
|
||||||
<template x-for="a in artikliOpcije" :key="a.id">
|
<template x-for="a in artikliOpcije" :key="a.id">
|
||||||
<option :value="a.id" x-text="a.naziv"></option>
|
<option :value="a.id" x-text="a.naziv"></option>
|
||||||
@@ -176,11 +177,11 @@
|
|||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Količina</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Količina</label>
|
||||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" min="1" :disabled="!isMobile" style="width:100%;">
|
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" @input="preracunajSve()" min="1" :disabled="!isMobile" style="width:100%;">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label>
|
||||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" @input="izracunajProdajnu(stavka)" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
|
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" @input="preracunajSve()" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||||
@@ -205,6 +206,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- zavisni troškovi -->
|
||||||
|
<div class="kartica forma-kartica animiraj" style="margin-bottom:16px;">
|
||||||
|
<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:8px;">
|
||||||
|
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Zavisni troškovi</span>
|
||||||
|
<button type="button" @click="dodajTrosak()" class="btn-primarno" style="font-size:13px;padding:6px 14px;">
|
||||||
|
+ Dodaj trošak
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="troskovi.length > 0">
|
||||||
|
<div class="kolona" style="gap:10px;">
|
||||||
|
<!-- metod raspodele -->
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
||||||
|
<label class="polje-labela" style="margin:0;">Raspodela na stavke:</label>
|
||||||
|
<select name="metod_raspodele" x-model="metodRaspodele" @change="preracunajSve()" style="max-width:240px;">
|
||||||
|
<option value="vrednost">po vrednosti stavke</option>
|
||||||
|
<option value="kolicina">po količini</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- redovi troškova -->
|
||||||
|
<template x-for="(t, i) in troskovi" :key="i">
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
|
<input type="text" :name="'trosak_naziv[]'" x-model="t.naziv"
|
||||||
|
placeholder="npr. Prevoz, Carina, Špedicija" style="flex:1;">
|
||||||
|
<input type="number" :name="'trosak_iznos[]'" x-model="t.iznos"
|
||||||
|
@input="preracunajSve()" min="0" step="0.01"
|
||||||
|
placeholder="iznos (din)" style="width:140px;text-align:right;">
|
||||||
|
<button type="button" @click="ukloniTrosak(i)"
|
||||||
|
style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:18px;line-height:1;padding:2px 6px;border-radius:4px;"
|
||||||
|
title="Ukloni trošak">×</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div style="text-align:right;font-size:13px;color:var(--tekst-sporedni);">
|
||||||
|
Ukupno troškovi: <strong x-text="ukupanTrosak().toFixed(2) + ' din'"></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="troskovi.length === 0">
|
||||||
|
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||||
|
Nema zavisnih troškova. Dodaj trošak (prevoz, carina…) — raspodeliće se na stavke i ući u kalkulativnu nabavnu cenu.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- dugmad forme -->
|
<!-- dugmad forme -->
|
||||||
<div style="display:flex;justify-content:flex-end;gap:10px;">
|
<div style="display:flex;justify-content:flex-end;gap:10px;">
|
||||||
<a href="/nabavke" class="btn-sekundarno">Odustani</a>
|
<a href="/nabavke" class="btn-sekundarno">Odustani</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user