Srednji prioritet iz pregleda koda: konzistentnost, refaktor, sitne popravke
- konstantno-vremensko poređenje CSRF tokena i kartica-PIN-a
- CSRF grupa proširena na /prijava, /prijava/totp, /setup
- router-level dozvola na svim /admin/podesavanja/* i modul("fiskalizacija")
na fiskal.pazar rutama
- KlijentRepo.Pronadji razlikuje sql.ErrNoRows od stvarnih DB grešaka
- izdvojen zajednički vratiStavkeNaStanje umesto 4 duplirana bloka
(ProdajaRepo/ServisRepo Storno/Obrisi)
- datumi u KIR/KPR sada se upisuju/porede kao "2006-01-02", usklađeno sa KPO
- ArtikalRepo.DohvatiVise (batch dohvat) — ukida N+1 upite u SacuvajNabavku
- KategorijaRepo.Obrisi umesto direktnog SQL-a u handleru, sa
ErrKategorijaUUpotrebi porukom
- KolicinMin -> KolicinaMin (tipfeler), Trosak/Usluga.DatumUnosa string -> time.Time
- dedup OznakaPoreskogBroja (KIR/KPR)
- inicijali() template funkcija (rune, ne bajtovi) za avatar inicijale
This commit is contained in:
+27
-19
@@ -238,14 +238,21 @@ func main() {
|
|||||||
http.FileServer(http.FS(staticFS)).ServeHTTP(w, req)
|
http.FileServer(http.FS(staticFS)).ServeHTTP(w, req)
|
||||||
})))
|
})))
|
||||||
|
|
||||||
// javne rute (bez autentifikacije)
|
// javne rute (bez autentifikacije), ali i dalje sa CSRF zaštitom — sprečava login-CSRF
|
||||||
r.Get("/prijava", h.PrikazPrijave)
|
r.Group(func(r chi.Router) {
|
||||||
r.Post("/prijava", h.Prijava)
|
r.Use(ntechmw.CsrfMiddleware)
|
||||||
r.Get("/prijava/totp", h.PrikazTotp)
|
r.Get("/prijava", h.PrikazPrijave)
|
||||||
r.Post("/prijava/totp", h.VerifikujTotp)
|
r.Post("/prijava", h.Prijava)
|
||||||
r.Get("/setup", h.PrikazSetupa)
|
r.Get("/prijava/totp", h.PrikazTotp)
|
||||||
r.Post("/setup", h.SacuvajSetup)
|
r.Post("/prijava/totp", h.VerifikujTotp)
|
||||||
|
r.Get("/setup", h.PrikazSetupa)
|
||||||
|
r.Post("/setup", h.SacuvajSetup)
|
||||||
|
})
|
||||||
r.Get("/odjava", h.Odjava)
|
r.Get("/odjava", h.Odjava)
|
||||||
|
// /status/{token}/* NAMERNO je van CSRF grupe — autentikacija je jednokratni
|
||||||
|
// tajni token u URL-u (capability model), ne kolačić/sesija, pa CSRF ovde ne
|
||||||
|
// primenjuje isti pretpostavljeni napadački model (napadač bez tokena ne može
|
||||||
|
// ni da pogodi rutu).
|
||||||
r.Get("/status/{token}", h.ServisJavniStatus)
|
r.Get("/status/{token}", h.ServisJavniStatus)
|
||||||
r.Post("/status/{token}/prihvati", h.ServisJavniPrihvati)
|
r.Post("/status/{token}/prihvati", h.ServisJavniPrihvati)
|
||||||
r.Post("/status/{token}/odbij", h.ServisJavniOdbij)
|
r.Post("/status/{token}/odbij", h.ServisJavniOdbij)
|
||||||
@@ -283,18 +290,19 @@ func main() {
|
|||||||
})
|
})
|
||||||
r.Get("/dashboard", h.Dashboard)
|
r.Get("/dashboard", h.Dashboard)
|
||||||
r.Get("/podesavanja", h.Podesavanja)
|
r.Get("/podesavanja", h.Podesavanja)
|
||||||
r.Get("/admin/podesavanja/opste", h.PodesavanjaOpste)
|
pregledPodesavanja := ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "podesavanja.pregled")
|
||||||
r.Get("/admin/podesavanja/izgled", h.PodesavanjaIzgled)
|
r.With(pregledPodesavanja).Get("/admin/podesavanja/opste", h.PodesavanjaOpste)
|
||||||
r.Get("/admin/podesavanja/sistem", h.PodesavanjaSistem)
|
r.With(pregledPodesavanja).Get("/admin/podesavanja/izgled", h.PodesavanjaIzgled)
|
||||||
r.Get("/admin/podesavanja/servis", h.PodesavanjaServis)
|
r.With(pregledPodesavanja).Get("/admin/podesavanja/sistem", h.PodesavanjaSistem)
|
||||||
r.Get("/admin/podesavanja/fiskalizacija", h.PodesavanjaFiskalizacija)
|
r.With(pregledPodesavanja).Get("/admin/podesavanja/servis", h.PodesavanjaServis)
|
||||||
r.Get("/podesavanja/fiskalizacija/test", h.TestFiskalizacije)
|
r.With(pregledPodesavanja).Get("/admin/podesavanja/fiskalizacija", h.PodesavanjaFiskalizacija)
|
||||||
r.Get("/podesavanja/fiskalizacija/be-status", h.BeStatus)
|
r.With(pregledPodesavanja).Get("/podesavanja/fiskalizacija/test", h.TestFiskalizacije)
|
||||||
r.Post("/podesavanja/fiskalizacija/be-reset-audit", h.BeResetAudit)
|
r.With(pregledPodesavanja).Get("/podesavanja/fiskalizacija/be-status", h.BeStatus)
|
||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "fiskal.pazar")).Get("/fiskal/pazar", h.FiskalniPazar)
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/fiskalizacija/be-reset-audit", h.BeResetAudit)
|
||||||
r.With(doz("fiskal.pazar")).Post("/fiskal/pazar/izvestaj", h.FiskalniIzvestaj)
|
r.With(modul("fiskalizacija"), ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "fiskal.pazar")).Get("/fiskal/pazar", h.FiskalniPazar)
|
||||||
r.With(doz("fiskal.zakljucenje")).Post("/fiskal/pazar/zakljuci", h.ZakljuciFiskalniDan)
|
r.With(modul("fiskalizacija"), doz("fiskal.pazar")).Post("/fiskal/pazar/izvestaj", h.FiskalniIzvestaj)
|
||||||
r.Get("/admin/podesavanja/kalkulacija-pdv", h.PdvStope)
|
r.With(modul("fiskalizacija"), doz("fiskal.zakljucenje")).Post("/fiskal/pazar/zakljuci", h.ZakljuciFiskalniDan)
|
||||||
|
r.With(pregledPodesavanja).Get("/admin/podesavanja/kalkulacija-pdv", h.PdvStope)
|
||||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/dodaj", h.DodajPdvStopu)
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/dodaj", h.DodajPdvStopu)
|
||||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/izmeni", h.IzmeniPdvStopu)
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/izmeni", h.IzmeniPdvStopu)
|
||||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/aktivnost", h.PromeniAktivnostPdvStope)
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/aktivnost", h.PromeniAktivnostPdvStope)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package be
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -254,7 +255,7 @@ func (k *Kartica) cmdCertificate() map[string]any {
|
|||||||
func (k *Kartica) cmdVerifyPin(pin string) map[string]any {
|
func (k *Kartica) cmdVerifyPin(pin string) map[string]any {
|
||||||
k.mu.Lock()
|
k.mu.Lock()
|
||||||
defer k.mu.Unlock()
|
defer k.mu.Unlock()
|
||||||
if pin != k.PIN {
|
if subtle.ConstantTimeCompare([]byte(pin), []byte(k.PIN)) != 1 {
|
||||||
return map[string]any{"status": "error", "code": "2100", "message": "Pogrešan PIN"}
|
return map[string]any{"status": "error", "code": "2100", "message": "Pogrešan PIN"}
|
||||||
}
|
}
|
||||||
k.pinUnesen = true
|
k.pinUnesen = true
|
||||||
|
|||||||
@@ -12,11 +12,17 @@ import (
|
|||||||
// (prodaja, nabavka, magacinske promene ili servisni nalozi). Tada se artikal arhivira.
|
// (prodaja, nabavka, magacinske promene ili servisni nalozi). Tada se artikal arhivira.
|
||||||
var ErrArtikalUUpotrebi = errors.New("ntech: artikal je u upotrebi")
|
var ErrArtikalUUpotrebi = errors.New("ntech: artikal je u upotrebi")
|
||||||
|
|
||||||
|
// ErrKategorijaUUpotrebi se vraća kad se kategorija ne može obrisati jer je
|
||||||
|
// referencirana od strane artikla.
|
||||||
|
var ErrKategorijaUUpotrebi = errors.New("ntech: kategorija je u upotrebi")
|
||||||
|
|
||||||
// ArtikalRepository definiše operacije nad artiklima
|
// ArtikalRepository definiše operacije nad artiklima
|
||||||
type ArtikalRepository interface {
|
type ArtikalRepository interface {
|
||||||
Lista(ctx context.Context, filter ArtikalFilter) ([]model.ArtikalSaKategorijom, error)
|
Lista(ctx context.Context, filter ArtikalFilter) ([]model.ArtikalSaKategorijom, error)
|
||||||
PrebrojiPoFilteru(ctx context.Context, filter ArtikalFilter) (int, error)
|
PrebrojiPoFilteru(ctx context.Context, filter ArtikalFilter) (int, error)
|
||||||
DohvatiID(ctx context.Context, id int64) (*model.Artikal, error)
|
DohvatiID(ctx context.Context, id int64) (*model.Artikal, error)
|
||||||
|
// DohvatiVise vraća artikle za dati skup ID-jeva kao mapu id → artikal (jedan upit)
|
||||||
|
DohvatiVise(ctx context.Context, ids []int64) (map[int64]*model.Artikal, error)
|
||||||
Kreiraj(ctx context.Context, a *model.Artikal) (int64, error)
|
Kreiraj(ctx context.Context, a *model.Artikal) (int64, error)
|
||||||
Izmeni(ctx context.Context, a *model.Artikal) error
|
Izmeni(ctx context.Context, a *model.Artikal) error
|
||||||
// AzurirajCene menja samo nabavnu i prodajnu cenu (kalkulacija pri nabavci)
|
// AzurirajCene menja samo nabavnu i prodajnu cenu (kalkulacija pri nabavci)
|
||||||
@@ -49,6 +55,8 @@ type KategorijaRepository interface {
|
|||||||
DohvatiID(ctx context.Context, id int64) (*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
|
Izmeni(ctx context.Context, k *model.Kategorija) error
|
||||||
|
// Obrisi briše kategoriju; vraća ErrKategorijaUUpotrebi ako je referencirana od artikla
|
||||||
|
Obrisi(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// PdvStopaRepository definiše operacije nad šifarnikom PDV stopa
|
// PdvStopaRepository definiše operacije nad šifarnikom PDV stopa
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
|
|
||||||
err := redovi.Scan(
|
err := redovi.Scan(
|
||||||
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere,
|
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere,
|
||||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
&a.Kolicina, &a.KolicinaMin, &a.Lokacija,
|
||||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.CenaSaPdv, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,
|
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.CenaSaPdv, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,
|
||||||
&a.KategorijaNaziv, &katMarza,
|
&a.KategorijaNaziv, &katMarza,
|
||||||
)
|
)
|
||||||
@@ -118,7 +118,7 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
}
|
}
|
||||||
|
|
||||||
// kritična zaliha važi samo za proizvode (usluge/troškovi nemaju lager)
|
// kritična zaliha važi samo za proizvode (usluge/troškovi nemaju lager)
|
||||||
a.KriticnaZaliha = a.PratiLager() && a.Kolicina <= a.KolicinMin
|
a.KriticnaZaliha = a.PratiLager() && a.Kolicina <= a.KolicinaMin
|
||||||
|
|
||||||
rezultat = append(rezultat, a)
|
rezultat = append(rezultat, a)
|
||||||
}
|
}
|
||||||
@@ -126,24 +126,23 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
return rezultat, nil
|
return rezultat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DohvatiID vraća jedan artikal po ID-u
|
const artikalKolone = `id, kategorija_id, sifra, barkod, naziv, opis, tip, jedinica_mere, kolicina, kolicina_min,
|
||||||
func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) {
|
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, cena_sa_pdv, marza, napomena, datum_unosa, arhiviran`
|
||||||
|
|
||||||
|
// skenirajArtikal čita jedan red artikla iz Scan-kompatibilnog izvora (red ili redovi)
|
||||||
|
func skenirajArtikal(scan func(...any) error) (*model.Artikal, error) {
|
||||||
var a model.Artikal
|
var a model.Artikal
|
||||||
var kategorijaID sql.NullInt64
|
var kategorijaID sql.NullInt64
|
||||||
var sifra, barkod sql.NullString
|
var sifra, barkod sql.NullString
|
||||||
var marza sql.NullFloat64
|
var marza sql.NullFloat64
|
||||||
var arhiviran int
|
var arhiviran int
|
||||||
|
|
||||||
err := r.db.QueryRowContext(ctx, `
|
if err := scan(
|
||||||
SELECT id, kategorija_id, sifra, barkod, naziv, opis, tip, jedinica_mere, kolicina, kolicina_min,
|
|
||||||
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, cena_sa_pdv, marza, napomena, datum_unosa, arhiviran
|
|
||||||
FROM artikli WHERE id = ?`, id).Scan(
|
|
||||||
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere,
|
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis, &a.Tip, &a.JedinicaMere,
|
||||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
&a.Kolicina, &a.KolicinaMin, &a.Lokacija,
|
||||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.CenaSaPdv, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,
|
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.CenaSaPdv, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,
|
||||||
)
|
); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err)
|
|
||||||
}
|
}
|
||||||
a.Arhiviran = arhiviran == 1
|
a.Arhiviran = arhiviran == 1
|
||||||
|
|
||||||
@@ -163,6 +162,52 @@ func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal,
|
|||||||
return &a, nil
|
return &a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DohvatiID vraća jedan artikal po ID-u
|
||||||
|
func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) {
|
||||||
|
row := r.db.QueryRowContext(ctx, "SELECT "+artikalKolone+" FROM artikli WHERE id = ?", id)
|
||||||
|
a, err := skenirajArtikal(row.Scan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err)
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DohvatiVise vraća artikle za dati skup ID-jeva kao mapu id → artikal — jedan upit
|
||||||
|
// umesto poziva DohvatiID u petlji (koristi se npr. pri obradi stavki nabavke).
|
||||||
|
func (r *ArtikalRepo) DohvatiVise(ctx context.Context, ids []int64) (map[int64]*model.Artikal, error) {
|
||||||
|
rezultat := make(map[int64]*model.Artikal, len(ids))
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return rezultat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
drzaci := make([]string, len(ids))
|
||||||
|
args := make([]any, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
drzaci[i] = "?"
|
||||||
|
args[i] = id
|
||||||
|
}
|
||||||
|
|
||||||
|
redovi, err := r.db.QueryContext(ctx,
|
||||||
|
"SELECT "+artikalKolone+" FROM artikli WHERE id IN ("+strings.Join(drzaci, ",")+")", args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiVise: %w", err)
|
||||||
|
}
|
||||||
|
defer redovi.Close()
|
||||||
|
|
||||||
|
for redovi.Next() {
|
||||||
|
a, err := skenirajArtikal(redovi.Scan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiVise: scan: %w", err)
|
||||||
|
}
|
||||||
|
rezultat[a.ID] = a
|
||||||
|
}
|
||||||
|
if err := redovi.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiVise: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rezultat, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Kreiraj dodaje novi artikal u bazu
|
// Kreiraj dodaje novi artikal u bazu
|
||||||
func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, error) {
|
func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, error) {
|
||||||
var sifra, barkod any
|
var sifra, barkod any
|
||||||
@@ -178,7 +223,7 @@ func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, err
|
|||||||
(kategorija_id, sifra, barkod, naziv, opis, tip, jedinica_mere, kolicina, kolicina_min, lokacija,
|
(kategorija_id, sifra, barkod, naziv, opis, tip, jedinica_mere, kolicina, kolicina_min, lokacija,
|
||||||
nabavna_cena, prodajna_cena, pdv_stopa, cena_sa_pdv, marza, napomena)
|
nabavna_cena, prodajna_cena, pdv_stopa, cena_sa_pdv, marza, napomena)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Tip, a.JedinicaMere, a.Kolicina, a.KolicinMin,
|
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Tip, a.JedinicaMere, a.Kolicina, a.KolicinaMin,
|
||||||
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.CenaSaPdv, a.Marza, a.Napomena,
|
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.CenaSaPdv, a.Marza, a.Napomena,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -210,7 +255,7 @@ func (r *ArtikalRepo) Izmeni(ctx context.Context, a *model.Artikal) error {
|
|||||||
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, cena_sa_pdv = ?, marza = ?, napomena = ?
|
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, cena_sa_pdv = ?, marza = ?, napomena = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Tip, a.JedinicaMere,
|
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Tip, a.JedinicaMere,
|
||||||
a.Kolicina, a.KolicinMin, a.Lokacija,
|
a.Kolicina, a.KolicinaMin, a.Lokacija,
|
||||||
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.CenaSaPdv, a.Marza, a.Napomena, a.ID,
|
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.CenaSaPdv, a.Marza, a.Napomena, a.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ func (r *sqliteIzvestajRepo) StanjeZaliha(ctx context.Context) ([]model.StanjeZa
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var s model.StanjeZalihaRed
|
var s model.StanjeZalihaRed
|
||||||
if err := rows.Scan(&s.Naziv, &s.Sifra, &s.Kategorija,
|
if err := rows.Scan(&s.Naziv, &s.Sifra, &s.Kategorija,
|
||||||
&s.Kolicina, &s.KolicinMin, &s.NabavnaCena, &s.ProdajnaCena,
|
&s.Kolicina, &s.KolicinaMin, &s.NabavnaCena, &s.ProdajnaCena,
|
||||||
&s.CenaSaPdv, &s.VrednostZalihe, &s.VrednostSaPdv); err != nil {
|
&s.CenaSaPdv, &s.VrednostZalihe, &s.VrednostSaPdv); err != nil {
|
||||||
return nil, fmt.Errorf("ntech: IzvestajRepo.StanjeZaliha: scan: %w", err)
|
return nil, fmt.Errorf("ntech: IzvestajRepo.StanjeZaliha: scan: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ func TestIzvestajArtikliBrojaci(t *testing.T) {
|
|||||||
t.Fatalf("Kreiraj: %v", err)
|
t.Fatalf("Kreiraj: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dodaj(&model.Artikal{Naziv: "A", Kolicina: 10, KolicinMin: 5})
|
dodaj(&model.Artikal{Naziv: "A", Kolicina: 10, KolicinaMin: 5})
|
||||||
dodaj(&model.Artikal{Naziv: "B", Kolicina: 2, KolicinMin: 5})
|
dodaj(&model.Artikal{Naziv: "B", Kolicina: 2, KolicinaMin: 5})
|
||||||
dodaj(&model.Artikal{Naziv: "C", Kolicina: 0, KolicinMin: 5})
|
dodaj(&model.Artikal{Naziv: "C", Kolicina: 0, KolicinaMin: 5})
|
||||||
|
|
||||||
if n, err := izv.BrojArtikala(ctx); err != nil || n != 3 {
|
if n, err := izv.BrojArtikala(ctx); err != nil || n != 3 {
|
||||||
t.Fatalf("BrojArtikala = %d, err=%v; očekivano 3", n, err)
|
t.Fatalf("BrojArtikala = %d, err=%v; očekivano 3", n, err)
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ package sqlite
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"ntech/internal/db"
|
||||||
"ntech/internal/model"
|
"ntech/internal/model"
|
||||||
|
|
||||||
|
mosqlite "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KategorijaRepo je SQLite implementacija KategorijaRepository interfejsa
|
// KategorijaRepo je SQLite implementacija KategorijaRepository interfejsa
|
||||||
@@ -109,3 +113,17 @@ func (r *KategorijaRepo) Izmeni(ctx context.Context, k *model.Kategorija) error
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Obrisi briše kategoriju. Ako je referencirana od artikla (FK ograničenje),
|
||||||
|
// vraća db.ErrKategorijaUUpotrebi da pozivalac može da prikaže razumljivu poruku.
|
||||||
|
func (r *KategorijaRepo) Obrisi(ctx context.Context, id int64) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, "DELETE FROM kategorije WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
var sqliteErr *mosqlite.Error
|
||||||
|
if errors.As(err, &sqliteErr) && sqliteErr.Code() == 787 {
|
||||||
|
return db.ErrKategorijaUUpotrebi
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ntech: KategorijaRepo.Obrisi: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ func (r *KlijentRepo) Pronadji(ctx context.Context, tip, ime, prezime, nazivFirm
|
|||||||
FROM klijenti WHERE tip = 'pravno' AND pib = ? LIMIT 1`, jmbg)
|
FROM klijenti WHERE tip = 'pravno' AND pib = ? LIMIT 1`, jmbg)
|
||||||
if err := skeniraj(row); err == nil {
|
if err := skeniraj(row); err == nil {
|
||||||
goto popuni
|
goto popuni
|
||||||
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, fmt.Errorf("ntech: KlijentRepo.Pronadji: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2. Naziv firme + mesto
|
// 2. Naziv firme + mesto
|
||||||
@@ -131,6 +133,8 @@ func (r *KlijentRepo) Pronadji(ctx context.Context, tip, ime, prezime, nazivFirm
|
|||||||
FROM klijenti WHERE tip = 'pravno' AND naziv_firme = ? AND mesto = ? LIMIT 1`, nazivFirme, mesto)
|
FROM klijenti WHERE tip = 'pravno' AND naziv_firme = ? AND mesto = ? LIMIT 1`, nazivFirme, mesto)
|
||||||
if err := skeniraj(row); err == nil {
|
if err := skeniraj(row); err == nil {
|
||||||
goto popuni
|
goto popuni
|
||||||
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, fmt.Errorf("ntech: KlijentRepo.Pronadji: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 3. Samo naziv firme
|
// 3. Samo naziv firme
|
||||||
@@ -152,6 +156,8 @@ func (r *KlijentRepo) Pronadji(ctx context.Context, tip, ime, prezime, nazivFirm
|
|||||||
FROM klijenti WHERE tip != 'pravno' AND jmbg = ? LIMIT 1`, jmbg)
|
FROM klijenti WHERE tip != 'pravno' AND jmbg = ? LIMIT 1`, jmbg)
|
||||||
if err := skeniraj(row); err == nil {
|
if err := skeniraj(row); err == nil {
|
||||||
goto popuni
|
goto popuni
|
||||||
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, fmt.Errorf("ntech: KlijentRepo.Pronadji: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2. Ime + prezime + mesto
|
// 2. Ime + prezime + mesto
|
||||||
@@ -161,6 +167,8 @@ func (r *KlijentRepo) Pronadji(ctx context.Context, tip, ime, prezime, nazivFirm
|
|||||||
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? AND mesto = ? LIMIT 1`, ime, prezime, mesto)
|
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? AND mesto = ? LIMIT 1`, ime, prezime, mesto)
|
||||||
if err := skeniraj(row); err == nil {
|
if err := skeniraj(row); err == nil {
|
||||||
goto popuni
|
goto popuni
|
||||||
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, fmt.Errorf("ntech: KlijentRepo.Pronadji: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 3. Ime + prezime + telefon
|
// 3. Ime + prezime + telefon
|
||||||
@@ -170,6 +178,8 @@ func (r *KlijentRepo) Pronadji(ctx context.Context, tip, ime, prezime, nazivFirm
|
|||||||
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? AND telefon = ? LIMIT 1`, ime, prezime, telefon)
|
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? AND telefon = ? LIMIT 1`, ime, prezime, telefon)
|
||||||
if err := skeniraj(row); err == nil {
|
if err := skeniraj(row); err == nil {
|
||||||
goto popuni
|
goto popuni
|
||||||
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, fmt.Errorf("ntech: KlijentRepo.Pronadji: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 4. Ime + prezime + email
|
// 4. Ime + prezime + email
|
||||||
@@ -179,6 +189,8 @@ func (r *KlijentRepo) Pronadji(ctx context.Context, tip, ime, prezime, nazivFirm
|
|||||||
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? AND email = ? LIMIT 1`, ime, prezime, email)
|
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? AND email = ? LIMIT 1`, ime, prezime, email)
|
||||||
if err := skeniraj(row); err == nil {
|
if err := skeniraj(row); err == nil {
|
||||||
goto popuni
|
goto popuni
|
||||||
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, fmt.Errorf("ntech: KlijentRepo.Pronadji: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 5. Samo ime + prezime (poslednji fallback)
|
// 5. Samo ime + prezime (poslednji fallback)
|
||||||
|
|||||||
@@ -100,3 +100,63 @@ func zabeleziMagacinPromenu(
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vratiStavkeNaStanje čita (artikal_id, kolicina) iz izvorne tabele stavki
|
||||||
|
// (stavke_prodaje ili servisni_delovi — uvek fiksan literal iz poziva, nikad
|
||||||
|
// korisnički unos) za dati nalog, vraća proizvode na stanje i beleži magacinsku
|
||||||
|
// promenu za svaki. Usluge i troškovi (koji ne prate stanje) se preskaču.
|
||||||
|
// Zajednički je za Storno/Obrisi u ProdajaRepo i ServisRepo — sve unutar
|
||||||
|
// postojeće transakcije pozivaoca.
|
||||||
|
func vratiStavkeNaStanje(ctx context.Context, tx *sql.Tx, tabelaStavki string, nalogID int64, korisnikID *int64, napomena string) error {
|
||||||
|
redovi, err := tx.QueryContext(ctx,
|
||||||
|
"SELECT artikal_id, kolicina FROM "+tabelaStavki+" WHERE nalog_id = ?", nalogID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dohvati stavke: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stavkaPovrat struct {
|
||||||
|
artikalID int64
|
||||||
|
kolicina int
|
||||||
|
}
|
||||||
|
var stavke []stavkaPovrat
|
||||||
|
for redovi.Next() {
|
||||||
|
var p stavkaPovrat
|
||||||
|
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
|
||||||
|
redovi.Close()
|
||||||
|
return fmt.Errorf("scan stavke: %w", err)
|
||||||
|
}
|
||||||
|
stavke = append(stavke, p)
|
||||||
|
}
|
||||||
|
redovi.Close()
|
||||||
|
if err := redovi.Err(); err != nil {
|
||||||
|
return fmt.Errorf("iteracija stavki: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range stavke {
|
||||||
|
var stanjePre int
|
||||||
|
var tip string
|
||||||
|
if err := tx.QueryRowContext(ctx,
|
||||||
|
"SELECT kolicina, tip FROM artikli WHERE id = ?", p.artikalID,
|
||||||
|
).Scan(&stanjePre, &tip); err != nil {
|
||||||
|
return fmt.Errorf("dohvati stanje: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// usluge i troškovi nemaju stanje na lageru — preskačemo povraćaj
|
||||||
|
if !(tip == model.TipProizvod || tip == "") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stanjePosle := stanjePre + p.kolicina
|
||||||
|
if _, err := tx.ExecContext(ctx,
|
||||||
|
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, p.artikalID,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("vrati stanje: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := zabeleziMagacinPromenu(ctx, tx, p.artikalID, model.PromenaPovracaj,
|
||||||
|
p.kolicina, stanjePre, stanjePosle, nalogID, korisnikID, napomena); err != nil {
|
||||||
|
return fmt.Errorf("magacin: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ func (r *PdvKirRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKi
|
|||||||
args := []any{}
|
args := []any{}
|
||||||
if !od.IsZero() {
|
if !od.IsZero() {
|
||||||
upit += " AND datum_prometa >= ?"
|
upit += " AND datum_prometa >= ?"
|
||||||
args = append(args, od)
|
args = append(args, od.Format("2006-01-02"))
|
||||||
}
|
}
|
||||||
if !do.IsZero() {
|
if !do.IsZero() {
|
||||||
upit += " AND datum_prometa <= ?"
|
upit += " AND datum_prometa <= ?"
|
||||||
args = append(args, do)
|
args = append(args, do.Format("2006-01-02"))
|
||||||
}
|
}
|
||||||
upit += " ORDER BY datum_prometa ASC, id ASC"
|
upit += " ORDER BY datum_prometa ASC, id ASC"
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ func (r *PdvKirRepo) Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error
|
|||||||
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
|
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
|
||||||
osloboden_sa_pravom, osloboden_bez_prava, ukupno, napomena, izvor, izvor_id
|
osloboden_sa_pravom, osloboden_bez_prava, ukupno, napomena, izvor, izvor_id
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
k.DatumPrometa, k.DatumKnjizenja, k.BrojDokumenta,
|
k.DatumPrometa.Format("2006-01-02"), k.DatumKnjizenja.Format("2006-01-02"), k.BrojDokumenta,
|
||||||
k.KupacNaziv, k.KupacPib, k.KupacMesto,
|
k.KupacNaziv, k.KupacPib, k.KupacMesto,
|
||||||
k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna,
|
k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna,
|
||||||
k.OslobodenSaPravom, k.OslobodenBezPrava, k.Ukupno, k.Napomena,
|
k.OslobodenSaPravom, k.OslobodenBezPrava, k.Ukupno, k.Napomena,
|
||||||
@@ -229,11 +229,11 @@ func (r *PdvKprRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKp
|
|||||||
args := []any{}
|
args := []any{}
|
||||||
if !od.IsZero() {
|
if !od.IsZero() {
|
||||||
upit += " AND datum_prometa >= ?"
|
upit += " AND datum_prometa >= ?"
|
||||||
args = append(args, od)
|
args = append(args, od.Format("2006-01-02"))
|
||||||
}
|
}
|
||||||
if !do.IsZero() {
|
if !do.IsZero() {
|
||||||
upit += " AND datum_prometa <= ?"
|
upit += " AND datum_prometa <= ?"
|
||||||
args = append(args, do)
|
args = append(args, do.Format("2006-01-02"))
|
||||||
}
|
}
|
||||||
upit += " ORDER BY datum_prometa ASC, id ASC"
|
upit += " ORDER BY datum_prometa ASC, id ASC"
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ func skenirajKpr(scan func(...any) error) (model.PdvKpr, error) {
|
|||||||
func (r *PdvKprRepo) Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error) {
|
func (r *PdvKprRepo) Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error) {
|
||||||
var datumPlacanja any
|
var datumPlacanja any
|
||||||
if k.DatumPlacanja != nil {
|
if k.DatumPlacanja != nil {
|
||||||
datumPlacanja = *k.DatumPlacanja
|
datumPlacanja = k.DatumPlacanja.Format("2006-01-02")
|
||||||
}
|
}
|
||||||
uvoz := 0
|
uvoz := 0
|
||||||
if k.Uvoz {
|
if k.Uvoz {
|
||||||
@@ -315,7 +315,7 @@ func (r *PdvKprRepo) Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error
|
|||||||
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
|
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
|
||||||
pdv_bez_odbitka, osloboden_nabavka, ukupno, napomena, izvor, izvor_id, uvoz
|
pdv_bez_odbitka, osloboden_nabavka, ukupno, napomena, izvor, izvor_id, uvoz
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
k.DatumPrometa, k.DatumKnjizenja, datumPlacanja, k.BrojDokumenta,
|
k.DatumPrometa.Format("2006-01-02"), k.DatumKnjizenja.Format("2006-01-02"), datumPlacanja, k.BrojDokumenta,
|
||||||
k.DobavljacNaziv, k.DobavljacPib, k.DobavljacMesto,
|
k.DobavljacNaziv, k.DobavljacPib, k.DobavljacMesto,
|
||||||
k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna,
|
k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna,
|
||||||
k.PdvBezOdbitka, k.OslobodenNabavka, k.Ukupno, k.Napomena,
|
k.PdvBezOdbitka, k.OslobodenNabavka, k.Ukupno, k.Napomena,
|
||||||
|
|||||||
@@ -294,53 +294,8 @@ func (r *ProdajaRepo) Storno(ctx context.Context, id int64, razlog string, koris
|
|||||||
}
|
}
|
||||||
|
|
||||||
// vraćanje stanja u magacin
|
// vraćanje stanja u magacin
|
||||||
redovi, err := tx.QueryContext(ctx,
|
if err := vratiStavkeNaStanje(ctx, tx, "stavke_prodaje", id, korisnikID, razlog); err != nil {
|
||||||
"SELECT artikal_id, kolicina FROM stavke_prodaje WHERE nalog_id = ?", id)
|
return fmt.Errorf("ntech: ProdajaRepo.Storno: %w", err)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: dohvati stavke: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type stavkaPovrat struct {
|
|
||||||
artikalID int64
|
|
||||||
kolicina int
|
|
||||||
}
|
|
||||||
var stavke []stavkaPovrat
|
|
||||||
for redovi.Next() {
|
|
||||||
var p stavkaPovrat
|
|
||||||
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
|
|
||||||
redovi.Close()
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: scan stavke: %w", err)
|
|
||||||
}
|
|
||||||
stavke = append(stavke, p)
|
|
||||||
}
|
|
||||||
redovi.Close()
|
|
||||||
|
|
||||||
for _, p := range stavke {
|
|
||||||
var stanjePre int
|
|
||||||
var tip string
|
|
||||||
err := tx.QueryRowContext(ctx, "SELECT kolicina, tip FROM artikli WHERE id = ?", p.artikalID).Scan(&stanjePre, &tip)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: dohvati stanje: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// usluge i troškovi nemaju stanje na lageru — preskačemo povraćaj
|
|
||||||
if !(tip == model.TipProizvod || tip == "") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
stanjePosle := stanjePre + p.kolicina
|
|
||||||
_, err = tx.ExecContext(ctx,
|
|
||||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, p.artikalID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: vrati stanje: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = zabeleziMagacinPromenu(ctx, tx, p.artikalID, model.PromenaPovracaj,
|
|
||||||
p.kolicina, stanjePre, stanjePosle, id, korisnikID, razlog)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: magacin: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx,
|
_, err = tx.ExecContext(ctx,
|
||||||
@@ -374,54 +329,8 @@ func (r *ProdajaRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !stornirano {
|
if !stornirano {
|
||||||
redovi, err := tx.QueryContext(ctx,
|
if err := vratiStavkeNaStanje(ctx, tx, "stavke_prodaje", id, korisnikID, "brisanje prodajnog naloga"); err != nil {
|
||||||
"SELECT artikal_id, kolicina FROM stavke_prodaje WHERE nalog_id = ?", id)
|
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: %w", err)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: dohvati stavke: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type povrat struct {
|
|
||||||
artikalID int64
|
|
||||||
kolicina int
|
|
||||||
}
|
|
||||||
var stavke []povrat
|
|
||||||
for redovi.Next() {
|
|
||||||
var p povrat
|
|
||||||
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
|
|
||||||
redovi.Close()
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: scan stavke: %w", err)
|
|
||||||
}
|
|
||||||
stavke = append(stavke, p)
|
|
||||||
}
|
|
||||||
redovi.Close()
|
|
||||||
|
|
||||||
for _, p := range stavke {
|
|
||||||
// usluge i troškovi nemaju stanje — vraćamo samo proizvodima
|
|
||||||
var stanjePre int
|
|
||||||
var tip string
|
|
||||||
err := tx.QueryRowContext(ctx,
|
|
||||||
"SELECT kolicina, tip FROM artikli WHERE id = ?", p.artikalID,
|
|
||||||
).Scan(&stanjePre, &tip)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: dohvati stanje: %w", err)
|
|
||||||
}
|
|
||||||
if !(tip == model.TipProizvod || tip == "") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
stanjePosle := stanjePre + p.kolicina
|
|
||||||
_, err = tx.ExecContext(ctx,
|
|
||||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, p.artikalID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: vrati stanje: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = zabeleziMagacinPromenu(ctx, tx, p.artikalID, model.PromenaPovracaj,
|
|
||||||
p.kolicina, stanjePre, stanjePosle, id, korisnikID, "brisanje prodajnog naloga")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: magacin: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -394,53 +394,8 @@ func (r *ServisRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) er
|
|||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
// pokupi ugrađene delove pre brisanja (CASCADE bi ih obrisao bez povraćaja)
|
// pokupi ugrađene delove pre brisanja (CASCADE bi ih obrisao bez povraćaja)
|
||||||
redovi, err := tx.QueryContext(ctx,
|
if err := vratiStavkeNaStanje(ctx, tx, "servisni_delovi", id, korisnikID, "brisanje servisnog naloga"); err != nil {
|
||||||
"SELECT artikal_id, kolicina FROM servisni_delovi WHERE nalog_id = ?", id)
|
return fmt.Errorf("ntech: ServisRepo.Obrisi: %w", err)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Obrisi: dohvati delove: %w", err)
|
|
||||||
}
|
|
||||||
type povrat struct {
|
|
||||||
artikalID int64
|
|
||||||
kolicina int
|
|
||||||
}
|
|
||||||
var delovi []povrat
|
|
||||||
for redovi.Next() {
|
|
||||||
var p povrat
|
|
||||||
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
|
|
||||||
redovi.Close()
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Obrisi: scan dela: %w", err)
|
|
||||||
}
|
|
||||||
delovi = append(delovi, p)
|
|
||||||
}
|
|
||||||
redovi.Close()
|
|
||||||
|
|
||||||
for _, p := range delovi {
|
|
||||||
// usluge i troškovi nemaju stanje na lageru — vraćamo samo proizvodima
|
|
||||||
var stanjePre int
|
|
||||||
var tip string
|
|
||||||
err := tx.QueryRowContext(ctx,
|
|
||||||
"SELECT kolicina, tip FROM artikli WHERE id = ?", p.artikalID,
|
|
||||||
).Scan(&stanjePre, &tip)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Obrisi: dohvati stanje: %w", err)
|
|
||||||
}
|
|
||||||
if !(tip == model.TipProizvod || tip == "") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
stanjePosle := stanjePre + p.kolicina
|
|
||||||
_, err = tx.ExecContext(ctx,
|
|
||||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, p.artikalID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Obrisi: vrati stanje: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = zabeleziMagacinPromenu(ctx, tx, p.artikalID, model.PromenaPovracaj,
|
|
||||||
p.kolicina, stanjePre, stanjePosle, id, korisnikID, "brisanje servisnog naloga")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Obrisi: magacin: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// potraživani delovi nemaju ON DELETE CASCADE — ručno ih čistimo da brisanje ne padne na FK
|
// potraživani delovi nemaju ON DELETE CASCADE — ručno ih čistimo da brisanje ne padne na FK
|
||||||
@@ -480,52 +435,8 @@ func (r *ServisRepo) Storno(ctx context.Context, id int64, razlog string, korisn
|
|||||||
return fmt.Errorf("ntech: ServisRepo.Storno: nalog je već storniran")
|
return fmt.Errorf("ntech: ServisRepo.Storno: nalog je već storniran")
|
||||||
}
|
}
|
||||||
|
|
||||||
redovi, err := tx.QueryContext(ctx,
|
if err := vratiStavkeNaStanje(ctx, tx, "servisni_delovi", id, korisnikID, "storno servisnog naloga"); err != nil {
|
||||||
"SELECT artikal_id, kolicina FROM servisni_delovi WHERE nalog_id = ?", id)
|
return fmt.Errorf("ntech: ServisRepo.Storno: %w", err)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Storno: dohvati delove: %w", err)
|
|
||||||
}
|
|
||||||
type povrat struct {
|
|
||||||
artikalID int64
|
|
||||||
kolicina int
|
|
||||||
}
|
|
||||||
var delovi []povrat
|
|
||||||
for redovi.Next() {
|
|
||||||
var p povrat
|
|
||||||
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
|
|
||||||
redovi.Close()
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Storno: scan dela: %w", err)
|
|
||||||
}
|
|
||||||
delovi = append(delovi, p)
|
|
||||||
}
|
|
||||||
redovi.Close()
|
|
||||||
|
|
||||||
for _, p := range delovi {
|
|
||||||
var stanjePre int
|
|
||||||
var tip string
|
|
||||||
err := tx.QueryRowContext(ctx,
|
|
||||||
"SELECT kolicina, tip FROM artikli WHERE id = ?", p.artikalID,
|
|
||||||
).Scan(&stanjePre, &tip)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Storno: dohvati stanje: %w", err)
|
|
||||||
}
|
|
||||||
if !(tip == model.TipProizvod || tip == "") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
stanjePosle := stanjePre + p.kolicina
|
|
||||||
_, err = tx.ExecContext(ctx,
|
|
||||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, p.artikalID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Storno: vrati stanje: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = zabeleziMagacinPromenu(ctx, tx, p.artikalID, model.PromenaPovracaj,
|
|
||||||
p.kolicina, stanjePre, stanjePosle, id, korisnikID, "storno servisnog naloga")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntech: ServisRepo.Storno: magacin: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tx.ExecContext(ctx,
|
if _, err := tx.ExecContext(ctx,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"ntech/internal/db"
|
||||||
"ntech/internal/db/sqlite"
|
"ntech/internal/db/sqlite"
|
||||||
|
"ntech/internal/middleware"
|
||||||
"ntech/internal/model"
|
"ntech/internal/model"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@@ -159,7 +162,12 @@ func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.DB.ExecContext(r.Context(), "DELETE FROM kategorije WHERE id = ?", id); err != nil {
|
if err := h.KategorijeRepo.Obrisi(r.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, db.ErrKategorijaUUpotrebi) {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", "Kategorija je u upotrebi kod artikala i ne može se obrisati.")
|
||||||
|
http.Redirect(w, r, "/magacin/kategorije", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
http.Error(w, "Greška pri brisanju kategorije", http.StatusInternalServerError)
|
http.Error(w, "Greška pri brisanju kategorije", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ var sablonskeFunkcije = template.FuncMap{
|
|||||||
},
|
},
|
||||||
// zbirF64 vraća zbir dva float64 — za aritmetiku u šablonima
|
// zbirF64 vraća zbir dva float64 — za aritmetiku u šablonima
|
||||||
"zbirF64": func(a, b float64) float64 { return a + b },
|
"zbirF64": func(a, b float64) float64 { return a + b },
|
||||||
|
// inicijali vraća prva najviše n RUNA stringa (ne bajta) — sigurno za ćirilicu
|
||||||
|
// i č/ć/š/ž/đ, za razliku od ugrađenog {{slice}} koji seče po bajtovima.
|
||||||
|
"inicijali": func(s string, n int) string {
|
||||||
|
r := []rune(s)
|
||||||
|
if len(r) > n {
|
||||||
|
r = r[:n]
|
||||||
|
}
|
||||||
|
return string(r)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// KreirajKes parsuje sve šablone iz fsys i vraća ih keširane u mapi
|
// KreirajKes parsuje sve šablone iz fsys i vraća ih keširane u mapi
|
||||||
|
|||||||
@@ -374,13 +374,13 @@ func parseFormuArtikla(r *http.Request, podrazumevanaStopa float64) (model.Artik
|
|||||||
if err != nil || v < 0 {
|
if err != nil || v < 0 {
|
||||||
return artikal, "Minimalna količina mora biti pozitivan broj."
|
return artikal, "Minimalna količina mora biti pozitivan broj."
|
||||||
}
|
}
|
||||||
artikal.KolicinMin = v
|
artikal.KolicinaMin = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// usluge i troškovi nemaju stanje na lageru
|
// usluge i troškovi nemaju stanje na lageru
|
||||||
if !artikal.PratiLager() {
|
if !artikal.PratiLager() {
|
||||||
artikal.Kolicina = 0
|
artikal.Kolicina = 0
|
||||||
artikal.KolicinMin = 0
|
artikal.KolicinaMin = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if c := r.FormValue("nabavna_cena"); c != "" {
|
if c := r.FormValue("nabavna_cena"); c != "" {
|
||||||
|
|||||||
@@ -196,12 +196,27 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jedan batch-dohvat svih artikala iz stavki — umesto DohvatiID po stavki u svakoj petlji ispod
|
||||||
|
artikalIDji := make([]int64, 0, len(stavke))
|
||||||
|
vidjeni := make(map[int64]bool, len(stavke))
|
||||||
|
for _, s := range stavke {
|
||||||
|
if !vidjeni[s.ArtikalID] {
|
||||||
|
vidjeni[s.ArtikalID] = true
|
||||||
|
artikalIDji = append(artikalIDji, s.ArtikalID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
artikliMapa, err := h.Artikli.DohvatiVise(r.Context(), artikalIDji)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("dohvat artikala za nabavku nije uspeo", "error", err)
|
||||||
|
artikliMapa = map[int64]*model.Artikal{}
|
||||||
|
}
|
||||||
|
|
||||||
// automatski zavedi u KPR ako je firma PDV obveznik; PDV se izvodi iz stope artikla
|
// automatski zavedi u KPR ako je firma PDV obveznik; PDV se izvodi iz stope artikla
|
||||||
if h.modulUkljucen(r.Context(), "pdv") {
|
if h.modulUkljucen(r.Context(), "pdv") {
|
||||||
var stavkePdv []model.NabavkaStavkaPdv
|
var stavkePdv []model.NabavkaStavkaPdv
|
||||||
for _, s := range stavke {
|
for _, s := range stavke {
|
||||||
var stopa float64
|
var stopa float64
|
||||||
if a, e := h.Artikli.DohvatiID(r.Context(), s.ArtikalID); e == nil {
|
if a, ok := artikliMapa[s.ArtikalID]; ok {
|
||||||
stopa = a.PdvStopa
|
stopa = a.PdvStopa
|
||||||
}
|
}
|
||||||
stavkePdv = append(stavkePdv, model.NabavkaStavkaPdv{
|
stavkePdv = append(stavkePdv, model.NabavkaStavkaPdv{
|
||||||
@@ -241,7 +256,7 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
// stara prodajna i tekuća (ponderisana) nabavna — nabavnu zadržavamo
|
// stara prodajna i tekuća (ponderisana) nabavna — nabavnu zadržavamo
|
||||||
var staraProdajna, nabavna float64
|
var staraProdajna, nabavna float64
|
||||||
if a, e := h.Artikli.DohvatiID(r.Context(), s.ArtikalID); e == nil {
|
if a, ok := artikliMapa[s.ArtikalID]; ok {
|
||||||
staraProdajna = a.ProdajnaCena
|
staraProdajna = a.ProdajnaCena
|
||||||
nabavna = a.NabavnaCena
|
nabavna = a.NabavnaCena
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -69,7 +70,7 @@ func CsrfMiddleware(next http.Handler) http.Handler {
|
|||||||
if submitted == "" {
|
if submitted == "" {
|
||||||
submitted = r.Header.Get("X-CSRF-Token")
|
submitted = r.Header.Get("X-CSRF-Token")
|
||||||
}
|
}
|
||||||
if token == "" || submitted != token {
|
if token == "" || subtle.ConstantTimeCompare([]byte(submitted), []byte(token)) != 1 {
|
||||||
http.Error(w,
|
http.Error(w,
|
||||||
"Neispravan sigurnosni token. Osvežite stranicu i pokušajte ponovo.",
|
"Neispravan sigurnosni token. Osvežite stranicu i pokušajte ponovo.",
|
||||||
http.StatusForbidden,
|
http.StatusForbidden,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Artikal struct {
|
|||||||
Tip string // proizvod | usluga | trosak
|
Tip string // proizvod | usluga | trosak
|
||||||
JedinicaMere string // kom, sat, set, m, l, kg ...
|
JedinicaMere string // kom, sat, set, m, l, kg ...
|
||||||
Kolicina int
|
Kolicina int
|
||||||
KolicinMin int
|
KolicinaMin int
|
||||||
Lokacija string
|
Lokacija string
|
||||||
NabavnaCena float64
|
NabavnaCena float64
|
||||||
ProdajnaCena float64
|
ProdajnaCena float64
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ type StanjeZalihaRed struct {
|
|||||||
Sifra string
|
Sifra string
|
||||||
Kategorija string
|
Kategorija string
|
||||||
Kolicina int
|
Kolicina int
|
||||||
KolicinMin int
|
KolicinaMin int
|
||||||
NabavnaCena float64
|
NabavnaCena float64
|
||||||
ProdajnaCena float64
|
ProdajnaCena float64
|
||||||
CenaSaPdv float64
|
CenaSaPdv float64
|
||||||
|
|||||||
@@ -37,8 +37,14 @@ func (k PdvKir) OslobodenUkupno() float64 {
|
|||||||
// OznakaPoreskogBroja vraća „JMBG" ako uneti broj ima 13 cifara (fizičko lice),
|
// OznakaPoreskogBroja vraća „JMBG" ako uneti broj ima 13 cifara (fizičko lice),
|
||||||
// inače „PIB" (pravno lice / preduzetnik — PIB ima 9 cifara).
|
// inače „PIB" (pravno lice / preduzetnik — PIB ima 9 cifara).
|
||||||
func (k PdvKir) OznakaPoreskogBroja() string {
|
func (k PdvKir) OznakaPoreskogBroja() string {
|
||||||
|
return oznakaPoreskogBroja(k.KupacPib)
|
||||||
|
}
|
||||||
|
|
||||||
|
// oznakaPoreskogBroja broji cifre u poreskom broju i vraća „JMBG" (13 cifara)
|
||||||
|
// ili „PIB" (ostalo) — zajednička logika za KIR (kupac) i KPR (dobavljač).
|
||||||
|
func oznakaPoreskogBroja(poreskiBroj string) string {
|
||||||
cifre := 0
|
cifre := 0
|
||||||
for _, r := range k.KupacPib {
|
for _, r := range poreskiBroj {
|
||||||
if r >= '0' && r <= '9' {
|
if r >= '0' && r <= '9' {
|
||||||
cifre++
|
cifre++
|
||||||
}
|
}
|
||||||
@@ -244,16 +250,7 @@ func KprStorno(original PdvKpr, razlog string, datumStorna time.Time) PdvKpr {
|
|||||||
|
|
||||||
// OznakaPoreskogBroja vraća „JMBG" za 13-cifreni broj, inače „PIB" (dobavljači su obično firme).
|
// OznakaPoreskogBroja vraća „JMBG" za 13-cifreni broj, inače „PIB" (dobavljači su obično firme).
|
||||||
func (k PdvKpr) OznakaPoreskogBroja() string {
|
func (k PdvKpr) OznakaPoreskogBroja() string {
|
||||||
cifre := 0
|
return oznakaPoreskogBroja(k.DobavljacPib)
|
||||||
for _, r := range k.DobavljacPib {
|
|
||||||
if r >= '0' && r <= '9' {
|
|
||||||
cifre++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cifre == 13 {
|
|
||||||
return "JMBG"
|
|
||||||
}
|
|
||||||
return "PIB"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PdvKprSume su zbirovi kolona KPR-a (za red „ukupno" u pregledu knjige).
|
// PdvKprSume su zbirovi kolona KPR-a (za red „ukupno" u pregledu knjige).
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// Trosak je stavka šifrarnika vrsta troškova (npr. Prevoz, Carina, Ambalaža).
|
// Trosak je stavka šifrarnika vrsta troškova (npr. Prevoz, Carina, Ambalaža).
|
||||||
// To je katalog dodatnih troškova koji se vezuju za radni nalog i ulaze u cenu
|
// To je katalog dodatnih troškova koji se vezuju za radni nalog i ulaze u cenu
|
||||||
// koštanja. NIJE evidencija rashoda firme (struja, kirija) — to je poseban modul.
|
// koštanja. NIJE evidencija rashoda firme (struja, kirija) — to je poseban modul.
|
||||||
@@ -10,5 +12,5 @@ type Trosak struct {
|
|||||||
Cena float64
|
Cena float64
|
||||||
Opis string
|
Opis string
|
||||||
Arhiviran bool
|
Arhiviran bool
|
||||||
DatumUnosa string
|
DatumUnosa time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// Usluga je stavka cenovnika usluga (npr. čišćenje laptopa, zamena ventilatora).
|
// Usluga je stavka cenovnika usluga (npr. čišćenje laptopa, zamena ventilatora).
|
||||||
// Za razliku od artikla, usluga ne prati lager i nema nabavnu cenu ni dobavljače —
|
// Za razliku od artikla, usluga ne prati lager i nema nabavnu cenu ni dobavljače —
|
||||||
// ima samo cenu usluge i PDV stopu. Kategorija je tekstualna oznaka.
|
// ima samo cenu usluge i PDV stopu. Kategorija je tekstualna oznaka.
|
||||||
@@ -13,7 +15,7 @@ type Usluga struct {
|
|||||||
PdvStopa float64
|
PdvStopa float64
|
||||||
Opis string
|
Opis string
|
||||||
Arhiviran bool
|
Arhiviran bool
|
||||||
DatumUnosa string
|
DatumUnosa time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Usluga) CenaSaPdv() float64 {
|
func (u Usluga) CenaSaPdv() float64 {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
{{if .AvatarPutanja}}
|
{{if .AvatarPutanja}}
|
||||||
<img src="{{.AvatarPutanja}}" alt="Avatar" style="width:100%;height:100%;object-fit:cover;display:block;">
|
<img src="{{.AvatarPutanja}}" alt="Avatar" style="width:100%;height:100%;object-fit:cover;display:block;">
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if .Korisnik}}{{slice .Korisnik 0 2}}{{else}}NT{{end}}
|
{{if .Korisnik}}{{inicijali .Korisnik 2}}{{else}}NT{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div id="avatar-meni" style="display:none;position:fixed;background:var(--kartica);backdrop-filter:none;-webkit-backdrop-filter:none;border:0.5px solid var(--ivica);border-radius:10px;box-shadow:var(--senka);min-width:160px;z-index:9999;overflow:hidden;transform-origin:top right;">
|
<div id="avatar-meni" style="display:none;position:fixed;background:var(--kartica);backdrop-filter:none;-webkit-backdrop-filter:none;border:0.5px solid var(--ivica);border-radius:10px;box-shadow:var(--senka);min-width:160px;z-index:9999;overflow:hidden;transform-origin:top right;">
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="polje-labela">Minimalna količina</label>
|
<label class="polje-labela">Minimalna količina</label>
|
||||||
<input type="number" name="kolicina_min" value="{{.Artikal.KolicinMin}}" min="0" style="width:100%;">
|
<input type="number" name="kolicina_min" value="{{.Artikal.KolicinaMin}}" min="0" style="width:100%;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:right;">
|
<div style="text-align:right;">
|
||||||
<div style="font-size:24px;font-weight:700;color:{{if le .Artikal.Kolicina .Artikal.KolicinMin}}#dc2626{{else}}var(--tekst-glavni){{end}};">
|
<div style="font-size:24px;font-weight:700;color:{{if le .Artikal.Kolicina .Artikal.KolicinaMin}}#dc2626{{else}}var(--tekst-glavni){{end}};">
|
||||||
{{.Artikal.Kolicina}} kom
|
{{.Artikal.Kolicina}} kom
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:12px;color:var(--tekst-slabi);">trenutno stanje</div>
|
<div style="font-size:12px;color:var(--tekst-slabi);">trenutno stanje</div>
|
||||||
{{if gt .Artikal.KolicinMin 0}}
|
{{if gt .Artikal.KolicinaMin 0}}
|
||||||
<div style="font-size:11px;color:var(--tekst-slabi);margin-top:2px;">min. {{.Artikal.KolicinMin}} kom</div>
|
<div style="font-size:11px;color:var(--tekst-slabi);margin-top:2px;">min. {{.Artikal.KolicinaMin}} kom</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -282,7 +282,7 @@
|
|||||||
</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 .KolicinaMin}}red-kriticno{{end}}" id="red-{{.ID}}">
|
||||||
<td class="kart-naslov" style="font-weight:500;">{{.Naziv}}</td>
|
<td class="kart-naslov" style="font-weight:500;">{{.Naziv}}</td>
|
||||||
<td class="kol-sek" data-label="Šifra" style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
|
<td class="kol-sek" data-label="Šifra" style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
|
||||||
<td class="kol-sek" data-label="Kategorija" style="font-size:12px;color:var(--tekst-slabi);">{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}</td>
|
<td class="kol-sek" data-label="Kategorija" style="font-size:12px;color:var(--tekst-slabi);">{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}</td>
|
||||||
|
|||||||
@@ -54,12 +54,12 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Zalihe}}
|
{{range .Zalihe}}
|
||||||
<tr {{if le .Kolicina .KolicinMin}}style="background:rgba(220,38,38,0.05);"{{end}}>
|
<tr {{if le .Kolicina .KolicinaMin}}style="background:rgba(220,38,38,0.05);"{{end}}>
|
||||||
<td style="font-weight:500;{{if le .Kolicina .KolicinMin}}color:#dc2626;{{end}}">{{.Naziv}}</td>
|
<td style="font-weight:500;{{if le .Kolicina .KolicinaMin}}color:#dc2626;{{end}}">{{.Naziv}}</td>
|
||||||
<td style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
|
<td style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
|
||||||
<td style="font-size:12px;color:var(--tekst-slabi);">{{if .Kategorija}}{{.Kategorija}}{{else}}—{{end}}</td>
|
<td style="font-size:12px;color:var(--tekst-slabi);">{{if .Kategorija}}{{.Kategorija}}{{else}}—{{end}}</td>
|
||||||
<td style="text-align:right;font-weight:600;{{if le .Kolicina .KolicinMin}}color:#dc2626;{{end}}">{{.Kolicina}}</td>
|
<td style="text-align:right;font-weight:600;{{if le .Kolicina .KolicinaMin}}color:#dc2626;{{end}}">{{.Kolicina}}</td>
|
||||||
<td style="text-align:right;font-size:12px;color:var(--tekst-slabi);">{{.KolicinMin}}</td>
|
<td style="text-align:right;font-size:12px;color:var(--tekst-slabi);">{{.KolicinaMin}}</td>
|
||||||
<td style="text-align:right;font-family:monospace;font-size:12px;">{{dinari .NabavnaCena}}</td>
|
<td style="text-align:right;font-family:monospace;font-size:12px;">{{dinari .NabavnaCena}}</td>
|
||||||
<td style="text-align:right;font-family:monospace;font-size:12px;">{{dinari .ProdajnaCena}}</td>
|
<td style="text-align:right;font-family:monospace;font-size:12px;">{{dinari .ProdajnaCena}}</td>
|
||||||
{{if index $.Moduli "pdv"}}<td style="text-align:right;font-family:monospace;font-size:12px;">{{dinari .CenaSaPdv}}</td>{{end}}
|
{{if index $.Moduli "pdv"}}<td style="text-align:right;font-family:monospace;font-size:12px;">{{dinari .CenaSaPdv}}</td>{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user