- 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
163 lines
4.6 KiB
Go
163 lines
4.6 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"ntech/internal/model"
|
|
)
|
|
|
|
// MagacinskePromeneRepo je SQLite implementacija MagacinskePromeneRepository interfejsa
|
|
type MagacinskePromeneRepo struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NoviMagacinskePromeneRepo kreira novi MagacinskePromeneRepo
|
|
func NoviMagacinskePromeneRepo(db *sql.DB) *MagacinskePromeneRepo {
|
|
return &MagacinskePromeneRepo{db: db}
|
|
}
|
|
|
|
// Lista vraća listu magacinskih promena, opcionalno filtrirano po artiklu
|
|
func (r *MagacinskePromeneRepo) Lista(ctx context.Context, artikalID *int64, limit int) ([]model.MagacinskaPromenaSaDetaljem, error) {
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
|
|
upit := `
|
|
SELECT mp.id, mp.artikal_id, a.naziv, mp.tip_promene, mp.referentni_id,
|
|
mp.promena_kolicine, mp.stanje_pre, mp.stanje_posle,
|
|
mp.korisnik_id, mp.napomena, mp.datum
|
|
FROM magacinske_promene mp
|
|
JOIN artikli a ON a.id = mp.artikal_id
|
|
WHERE 1=1`
|
|
|
|
args := []any{}
|
|
|
|
if artikalID != nil {
|
|
upit += " AND mp.artikal_id = ?"
|
|
args = append(args, *artikalID)
|
|
}
|
|
|
|
upit += " ORDER BY mp.datum DESC LIMIT ?"
|
|
args = append(args, limit)
|
|
|
|
redovi, err := r.db.QueryContext(ctx, upit, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ntech: MagacinskePromeneRepo.Lista: %w", err)
|
|
}
|
|
defer redovi.Close()
|
|
|
|
var rezultat []model.MagacinskaPromenaSaDetaljem
|
|
for redovi.Next() {
|
|
var p model.MagacinskaPromenaSaDetaljem
|
|
var korisnikID sql.NullInt64
|
|
var napomena sql.NullString
|
|
|
|
err := redovi.Scan(
|
|
&p.ID, &p.ArtikalID, &p.ArtikalNaziv, &p.TipPromene, &p.ReferentniID,
|
|
&p.PromenaKolicine, &p.StanjePre, &p.StanjePosle,
|
|
&korisnikID, &napomena, &p.Datum,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ntech: MagacinskePromeneRepo.Lista: scan: %w", err)
|
|
}
|
|
|
|
if korisnikID.Valid {
|
|
v := korisnikID.Int64
|
|
p.KorisnikID = &v
|
|
}
|
|
p.Napomena = napomena.String
|
|
|
|
rezultat = append(rezultat, p)
|
|
}
|
|
|
|
return rezultat, nil
|
|
}
|
|
|
|
// zabeleziMagacinPromenu je interni helper koji upisuje jednu promenu stanja artikla.
|
|
// Poziva se iz Kreiraj/Storno unutar postojeće transakcije.
|
|
func zabeleziMagacinPromenu(
|
|
ctx context.Context,
|
|
tx *sql.Tx,
|
|
artikalID int64,
|
|
tipPromene string,
|
|
promenaKolicine, stanjePre, stanjePosle int,
|
|
referentniID int64,
|
|
korisnikID *int64,
|
|
napomena string,
|
|
) error {
|
|
_, err := tx.ExecContext(ctx, `
|
|
INSERT INTO magacinske_promene
|
|
(artikal_id, tip_promene, promena_kolicine, stanje_pre, stanje_posle,
|
|
referentni_id, korisnik_id, napomena)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
artikalID, tipPromene, promenaKolicine, stanjePre, stanjePosle,
|
|
referentniID, nullInt64(korisnikID), nullString(napomena),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("ntech: zabeleziMagacinPromenu: %w", err)
|
|
}
|
|
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
|
|
}
|