Files
GoNtech/internal/db/sqlite/magacin.go
T
Dasko 127cebea3c 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
2026-07-03 23:21:29 +02:00

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
}