Dinari: formatiranje iznosa sa separatorom hiljada (helper dinari/dinariCeli); nabavka po dobavljaču (auto-veza, filter artikala, izbor dobavljača u formi artikla); UI doterivanja stavki nabavke
This commit is contained in:
@@ -31,6 +31,14 @@ type ArtikalRepository interface {
|
||||
SledecaSifra(ctx context.Context, kategorijaID *int64) (string, error)
|
||||
// KorigujKolicinu postavlja novu količinu artikla i upisuje korekciju u magacinske_promene
|
||||
KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena string) error
|
||||
// DobavljaciArtikla vraća ID-jeve dobavljača vezanih za artikal
|
||||
DobavljaciArtikla(ctx context.Context, artikalID int64) ([]int64, error)
|
||||
// PostaviDobavljaceArtikla zamenjuje skup dobavljača artikla datim ID-jevima
|
||||
PostaviDobavljaceArtikla(ctx context.Context, artikalID int64, dobavljaciID []int64) error
|
||||
// PoveziDobavljaca dodaje vezu artikal–dobavljač ako ne postoji (auto pri nabavci)
|
||||
PoveziDobavljaca(ctx context.Context, artikalID, dobavljacID int64) error
|
||||
// SveDobavljaceArtikala vraća mapu artikal_id → lista dobavljac_id (za filter u nabavci)
|
||||
SveDobavljaceArtikala(ctx context.Context) (map[int64][]int64, error)
|
||||
}
|
||||
|
||||
// KategorijaRepository definiše operacije nad kategorijama
|
||||
|
||||
@@ -309,6 +309,79 @@ func (r *ArtikalRepo) Vrati(ctx context.Context, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DobavljaciArtikla vraća ID-jeve dobavljača vezanih za artikal
|
||||
func (r *ArtikalRepo) DobavljaciArtikla(ctx context.Context, artikalID int64) ([]int64, error) {
|
||||
redovi, err := r.db.QueryContext(ctx,
|
||||
"SELECT dobavljac_id FROM artikal_dobavljac WHERE artikal_id = ? ORDER BY dobavljac_id", artikalID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ArtikalRepo.DobavljaciArtikla: %w", err)
|
||||
}
|
||||
defer redovi.Close()
|
||||
|
||||
var ids []int64
|
||||
for redovi.Next() {
|
||||
var id int64
|
||||
if err := redovi.Scan(&id); err != nil {
|
||||
return nil, fmt.Errorf("ntech: ArtikalRepo.DobavljaciArtikla: scan: %w", err)
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// PostaviDobavljaceArtikla zamenjuje skup dobavljača artikla datim ID-jevima (u transakciji)
|
||||
func (r *ArtikalRepo) PostaviDobavljaceArtikla(ctx context.Context, artikalID int64, dobavljaciID []int64) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.PostaviDobavljaceArtikla: begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.ExecContext(ctx, "DELETE FROM artikal_dobavljac WHERE artikal_id = ?", artikalID); err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.PostaviDobavljaceArtikla: delete: %w", err)
|
||||
}
|
||||
for _, did := range dobavljaciID {
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
"INSERT OR IGNORE INTO artikal_dobavljac (artikal_id, dobavljac_id) VALUES (?, ?)", artikalID, did); err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.PostaviDobavljaceArtikla: insert: %w", err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.PostaviDobavljaceArtikla: commit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PoveziDobavljaca dodaje vezu artikal–dobavljač ako ne postoji (auto pri nabavci)
|
||||
func (r *ArtikalRepo) PoveziDobavljaca(ctx context.Context, artikalID, dobavljacID int64) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
"INSERT OR IGNORE INTO artikal_dobavljac (artikal_id, dobavljac_id) VALUES (?, ?)", artikalID, dobavljacID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.PoveziDobavljaca: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SveDobavljaceArtikala vraća mapu artikal_id → lista dobavljac_id
|
||||
func (r *ArtikalRepo) SveDobavljaceArtikala(ctx context.Context) (map[int64][]int64, error) {
|
||||
redovi, err := r.db.QueryContext(ctx,
|
||||
"SELECT artikal_id, dobavljac_id FROM artikal_dobavljac ORDER BY artikal_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ArtikalRepo.SveDobavljaceArtikala: %w", err)
|
||||
}
|
||||
defer redovi.Close()
|
||||
|
||||
mapa := make(map[int64][]int64)
|
||||
for redovi.Next() {
|
||||
var aid, did int64
|
||||
if err := redovi.Scan(&aid, &did); err != nil {
|
||||
return nil, fmt.Errorf("ntech: ArtikalRepo.SveDobavljaceArtikala: scan: %w", err)
|
||||
}
|
||||
mapa[aid] = append(mapa[aid], did)
|
||||
}
|
||||
return mapa, nil
|
||||
}
|
||||
|
||||
// KorigujKolicinu postavlja novu količinu i upisuje korekciju u magacinske_promene
|
||||
func (r *ArtikalRepo) KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena string) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
|
||||
@@ -53,6 +53,15 @@ var sablonskeFunkcije = template.FuncMap{
|
||||
}
|
||||
return fmt.Sprintf("%d", int64(math.Round(*v)))
|
||||
},
|
||||
// dinari formatira iznos sa separatorom hiljada (tačka) i 2 decimale (zarez):
|
||||
// 1234567.5 → "1.234.567,50"
|
||||
"dinari": func(v float64) string {
|
||||
return formatirajDinare(v, 2)
|
||||
},
|
||||
// dinariCeli formatira iznos sa separatorom hiljada, bez decimala: 1234567 → "1.234.567"
|
||||
"dinariCeli": func(v float64) string {
|
||||
return formatirajDinare(v, 0)
|
||||
},
|
||||
// statusPre vraća true ako je `a` pre `b` u redosledu statusa
|
||||
"statusPre": func(a, b string, statusi []string) bool {
|
||||
ia, ib := -1, -1
|
||||
@@ -172,3 +181,41 @@ func (h *Handler) renderujStandalone(w http.ResponseWriter, ime string, podaci a
|
||||
http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// formatirajDinare formatira broj sa tačkom kao separatorom hiljada i zarezom
|
||||
// za decimale (srpski format). decimale = broj decimalnih mesta (0 ili 2).
|
||||
func formatirajDinare(v float64, decimale int) string {
|
||||
negativan := v < 0
|
||||
if negativan {
|
||||
v = -v
|
||||
}
|
||||
|
||||
var ceoStr, decStr string
|
||||
if decimale == 2 {
|
||||
// radi u stotinkama da zaokruživanje pravilno prenese (npr. 1234567.999 → 1.234.568,00)
|
||||
stotinke := int64(math.Round(v * 100))
|
||||
ceoStr = fmt.Sprintf("%d", stotinke/100)
|
||||
decStr = fmt.Sprintf("%02d", stotinke%100)
|
||||
} else {
|
||||
ceoStr = fmt.Sprintf("%d", int64(math.Round(v)))
|
||||
}
|
||||
|
||||
// ubaci tačke na svake 3 cifre s desna
|
||||
var sb []byte
|
||||
n := len(ceoStr)
|
||||
for i, c := range ceoStr {
|
||||
if i > 0 && (n-i)%3 == 0 {
|
||||
sb = append(sb, '.')
|
||||
}
|
||||
sb = append(sb, byte(c))
|
||||
}
|
||||
rezultat := string(sb)
|
||||
|
||||
if decimale == 2 {
|
||||
rezultat += "," + decStr
|
||||
}
|
||||
if negativan {
|
||||
rezultat = "-" + rezultat
|
||||
}
|
||||
return rezultat
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@ import (
|
||||
// PodaciFormeArtikla su podaci za formu novog/izmenjenog artikla
|
||||
type PodaciFormeArtikla struct {
|
||||
model.PodaciStranice
|
||||
Artikal model.Artikal
|
||||
Kategorije []model.Kategorija
|
||||
KategorijaIDStr string
|
||||
Greska string
|
||||
Izmena bool
|
||||
Artikal model.Artikal
|
||||
Kategorije []model.Kategorija
|
||||
KategorijaIDStr string
|
||||
Dobavljaci []model.Dobavljac // svi dobavljači za izbor
|
||||
IzabraniDobavljaci map[int64]bool // dobavljači vezani za artikal (za checked stanje)
|
||||
Greska string
|
||||
Izmena bool
|
||||
}
|
||||
|
||||
// NoviArtikal prikazuje formu za unos novog artikla
|
||||
@@ -45,12 +47,15 @@ func (h *Handler) NoviArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
predlogSifre = "ART-0001"
|
||||
}
|
||||
|
||||
dobavljaci, _ := h.DobavljaciRepo.Lista(r.Context(), "")
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "magacin"
|
||||
ps.NaslovStranice = "Novi artikal"
|
||||
h.renderujFormuArtikla(w, PodaciFormeArtikla{
|
||||
PodaciStranice: ps,
|
||||
Kategorije: kategorije,
|
||||
Dobavljaci: dobavljaci,
|
||||
Artikal: model.Artikal{Sifra: predlogSifre, Tip: model.TipProizvod, JedinicaMere: "kom"},
|
||||
Izmena: false,
|
||||
})
|
||||
@@ -72,6 +77,7 @@ func (h *Handler) SacuvajArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
if greska != "" {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
kategorije, _ := h.KategorijeRepo.Lista(r.Context())
|
||||
dobavljaci, _ := h.DobavljaciRepo.Lista(r.Context(), "")
|
||||
katIDStr := ""
|
||||
if artikal.KategorijaID != nil {
|
||||
katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10)
|
||||
@@ -80,12 +86,14 @@ func (h *Handler) SacuvajArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
ps.Stranica = "magacin"
|
||||
ps.NaslovStranice = "Novi artikal"
|
||||
h.renderujFormuArtikla(w, PodaciFormeArtikla{
|
||||
PodaciStranice: ps,
|
||||
Artikal: artikal,
|
||||
Kategorije: kategorije,
|
||||
KategorijaIDStr: katIDStr,
|
||||
Greska: greska,
|
||||
Izmena: false,
|
||||
PodaciStranice: ps,
|
||||
Artikal: artikal,
|
||||
Kategorije: kategorije,
|
||||
KategorijaIDStr: katIDStr,
|
||||
Dobavljaci: dobavljaci,
|
||||
IzabraniDobavljaci: mapaDobavljaca(citajDobavljaceForme(r)),
|
||||
Greska: greska,
|
||||
Izmena: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -108,6 +116,11 @@ func (h *Handler) SacuvajArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
artikal.ID = id
|
||||
|
||||
// veži izabrane dobavljače (forma); modal nema to polje pa ostaje prazno
|
||||
if e := h.Artikli.PostaviDobavljaceArtikla(r.Context(), id, citajDobavljaceForme(r)); e != nil {
|
||||
slog.Error("čuvanje dobavljača artikla nije uspelo", "artikal_id", id, "error", e)
|
||||
}
|
||||
|
||||
// fetch zahtev (iz modala) dobija JSON sa ID-em i nazivom novog artikla
|
||||
if r.Header.Get("X-Requested-With") == "fetch" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -150,15 +163,25 @@ func (h *Handler) IzmeniArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10)
|
||||
}
|
||||
|
||||
dobavljaci, _ := h.DobavljaciRepo.Lista(r.Context(), "")
|
||||
izabrani := map[int64]bool{}
|
||||
if ids, e := h.Artikli.DobavljaciArtikla(r.Context(), id); e == nil {
|
||||
for _, did := range ids {
|
||||
izabrani[did] = true
|
||||
}
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "magacin"
|
||||
ps.NaslovStranice = "Izmeni artikal"
|
||||
h.renderujFormuArtikla(w, PodaciFormeArtikla{
|
||||
PodaciStranice: ps,
|
||||
Artikal: *artikal,
|
||||
Kategorije: kategorije,
|
||||
KategorijaIDStr: katIDStr,
|
||||
Izmena: true,
|
||||
PodaciStranice: ps,
|
||||
Artikal: *artikal,
|
||||
Kategorije: kategorije,
|
||||
KategorijaIDStr: katIDStr,
|
||||
Dobavljaci: dobavljaci,
|
||||
IzabraniDobavljaci: izabrani,
|
||||
Izmena: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -185,6 +208,7 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
|
||||
if greska != "" {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
kategorije, _ := h.KategorijeRepo.Lista(r.Context())
|
||||
dobavljaci, _ := h.DobavljaciRepo.Lista(r.Context(), "")
|
||||
artikal.ID = id
|
||||
katIDStr := ""
|
||||
if artikal.KategorijaID != nil {
|
||||
@@ -194,12 +218,14 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
|
||||
ps.Stranica = "magacin"
|
||||
ps.NaslovStranice = "Izmeni artikal"
|
||||
h.renderujFormuArtikla(w, PodaciFormeArtikla{
|
||||
PodaciStranice: ps,
|
||||
Artikal: artikal,
|
||||
Kategorije: kategorije,
|
||||
KategorijaIDStr: katIDStr,
|
||||
Greska: greska,
|
||||
Izmena: true,
|
||||
PodaciStranice: ps,
|
||||
Artikal: artikal,
|
||||
Kategorije: kategorije,
|
||||
KategorijaIDStr: katIDStr,
|
||||
Dobavljaci: dobavljaci,
|
||||
IzabraniDobavljaci: mapaDobavljaca(citajDobavljaceForme(r)),
|
||||
Greska: greska,
|
||||
Izmena: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -223,6 +249,11 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// ažuriraj dobavljače artikla prema formi
|
||||
if e := h.Artikli.PostaviDobavljaceArtikla(r.Context(), id, citajDobavljaceForme(r)); e != nil {
|
||||
slog.Error("čuvanje dobavljača artikla nije uspelo", "artikal_id", id, "error", e)
|
||||
}
|
||||
|
||||
// ako se prodajna cena promenila, automatski upiši nivelacioni zapis (izvor "izmena")
|
||||
if razlika := artikal.ProdajnaCena - staraCena; razlika > 0.005 || razlika < -0.005 {
|
||||
korisnikID := &k.ID
|
||||
@@ -240,6 +271,26 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/magacin?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// mapaDobavljaca pretvara listu ID-jeva u mapu za checked stanje u formi
|
||||
func mapaDobavljaca(ids []int64) map[int64]bool {
|
||||
m := make(map[int64]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
m[id] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// citajDobavljaceForme čita izabrane dobavljače (checkbox dobavljaci[]) iz forme
|
||||
func citajDobavljaceForme(r *http.Request) []int64 {
|
||||
var ids []int64
|
||||
for _, v := range r.Form["dobavljaci"] {
|
||||
if id, e := strconv.ParseInt(strings.TrimSpace(v), 10, 64); e == nil {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// parseFormuArtikla čita polja iz forme i vraća artikal i eventualnu grešku
|
||||
func parseFormuArtikla(r *http.Request) (model.Artikal, string) {
|
||||
naziv := r.FormValue("naziv")
|
||||
|
||||
@@ -46,7 +46,7 @@ type PodaciDetaljiNabavke struct {
|
||||
}
|
||||
|
||||
// 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, vezeDobavljaca map[int64][]int64) template.JS {
|
||||
type stavka struct {
|
||||
ID int64 `json:"id"`
|
||||
Naziv string `json:"naziv"`
|
||||
@@ -54,13 +54,19 @@ func artikalUJSON(artikli []model.ArtikalSaKategorijom) template.JS {
|
||||
NabavnaCena float64 `json:"nabavna_cena"` // poslednja nabavna cena — predlog za Cena/kom
|
||||
Marza *float64 `json:"marza"` // marža artikla; null = nije postavljeno
|
||||
KategorijaMarza *float64 `json:"kategorija_marza"` // marža kategorije; fallback ako artikal nema
|
||||
Dobavljaci []int64 `json:"dobavljaci"` // ID-jevi dobavljača koji isporučuju artikal
|
||||
}
|
||||
lista := make([]stavka, 0, len(artikli))
|
||||
for _, a := range artikli {
|
||||
dob := vezeDobavljaca[a.ID]
|
||||
if dob == nil {
|
||||
dob = []int64{}
|
||||
}
|
||||
lista = append(lista, stavka{
|
||||
ID: a.ID, Naziv: a.Naziv, PdvStopa: a.PdvStopa,
|
||||
NabavnaCena: a.NabavnaCena,
|
||||
Marza: a.Marza, KategorijaMarza: a.KategorijaMarza,
|
||||
Dobavljaci: dob,
|
||||
})
|
||||
}
|
||||
b, _ := json.Marshal(lista)
|
||||
@@ -120,13 +126,15 @@ func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
veze, _ := h.Artikli.SveDobavljaceArtikala(r.Context())
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "nabavke"
|
||||
ps.NaslovStranice = "Nova nabavka"
|
||||
h.renderujFormuNabavke(w, PodaciFormeNabavke{
|
||||
PodaciStranice: ps,
|
||||
Artikli: artikli,
|
||||
ArtikliJSON: artikalUJSON(artikli),
|
||||
ArtikliJSON: artikalUJSON(artikli, veze),
|
||||
Dobavljaci: dobavljaci,
|
||||
Kategorije: kategorije,
|
||||
Marza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
||||
@@ -151,13 +159,14 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
artikli, _ := h.Artikli.Lista(r.Context(), db.ArtikalFilter{})
|
||||
dobavljaci, _ := h.DobavljaciRepo.Lista(r.Context(), "")
|
||||
kategorije, _ := h.KategorijeRepo.Lista(r.Context())
|
||||
veze, _ := h.Artikli.SveDobavljaceArtikala(r.Context())
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "nabavke"
|
||||
ps.NaslovStranice = "Nova nabavka"
|
||||
h.renderujFormuNabavke(w, PodaciFormeNabavke{
|
||||
PodaciStranice: ps,
|
||||
Artikli: artikli,
|
||||
ArtikliJSON: artikalUJSON(artikli),
|
||||
ArtikliJSON: artikalUJSON(artikli, veze),
|
||||
Dobavljaci: dobavljaci,
|
||||
Kategorije: kategorije,
|
||||
Marza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
||||
@@ -243,6 +252,15 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// auto-veza artikal–dobavljač: svaki nabavljeni artikal se veže za dobavljača nabavke
|
||||
if nabavka.DobavljacID != nil {
|
||||
for _, s := range stavke {
|
||||
if e := h.Artikli.PoveziDobavljaca(r.Context(), s.ArtikalID, *nabavka.DobavljacID); e != nil {
|
||||
slog.Error("auto-veza dobavljača nije upisana", "artikal_id", s.ArtikalID, "error", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user