Servis: ispravka logike potraživanih delova (5 bugova)
- BUG 1: pokriveni deo se sada prebacuje u ugrađene delove (servisni_delovi), ne samo skida sa magacina — roba više ne nestaje iz evidencije - BUG 2: DodajDeloNalogu sada atomično (UgradiIliPotrazuj) čita stanje i ugrađuje u istoj transakciji — nema TOCTOU, lager ne ide u minus - BUG 4: direktna izmena količine artikla beleži magacinski trag (korekcija) - BUG 5: greška pri dodavanju dela se propagira korisniku, ne guta se - uklonjen mrtav kod (Dodaj, DodajIliUvecaj) sa pogrešnim komentarom o minusu - migracija 068: cena_komada u potraživanim delovima - test TestPotrazivaniTok pokriva ceo tok
This commit is contained in:
@@ -210,7 +210,10 @@ type ProdajaRepository interface {
|
||||
// ServisniDeloviRepository definiše operacije nad ugrađenim delovima u servisu
|
||||
type ServisniDeloviRepository interface {
|
||||
DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error)
|
||||
Dodaj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (int64, error)
|
||||
// UgradiIliPotrazuj atomično (jedna transakcija) čita stanje magacina, ugrađuje
|
||||
// ono što fizički ima (skida sa lagera, lager NE ide u minus), a višak beleži u
|
||||
// potraživane delove. Vraća koliko je ugrađeno i koliko nedostaje.
|
||||
UgradiIliPotrazuj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (ugradjeno, nedostaje int, err error)
|
||||
DohvatiArtikalID(ctx context.Context, deoID int64) (int64, error)
|
||||
Obrisi(ctx context.Context, id int64, korisnikID *int64) error
|
||||
}
|
||||
@@ -218,7 +221,6 @@ type ServisniDeloviRepository interface {
|
||||
// ServisniPotrazivaniDeloviRepository definiše operacije nad delovima koji nedostaju
|
||||
type ServisniPotrazivaniDeloviRepository interface {
|
||||
DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniPotrazivaniDeo, error)
|
||||
DodajIliUvecaj(ctx context.Context, nalogID, artikalID int64, kolicina int) (int64, error)
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
ObrisiZaArtikal(ctx context.Context, nalogID, artikalID int64) error
|
||||
// ProveriIPocistiZaArtikal proverava potraživane redove za dati artikal nakon
|
||||
|
||||
@@ -49,84 +49,104 @@ func (r *ServisniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64)
|
||||
return rezultat, nil
|
||||
}
|
||||
|
||||
// Dodaj dodaje jedan artikal u servisni nalog, smanjuje stanje u magacinu i beleži promenu
|
||||
func (r *ServisniDeloviRepo) Dodaj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (int64, error) {
|
||||
// UgradiIliPotrazuj atomično (jedna transakcija) ugrađuje ono što fizički ima na
|
||||
// stanju, a višak beleži u potraživane delove. Stanje se čita i koristi unutar iste
|
||||
// transakcije pa lager NIKAD ne ide u minus (nema TOCTOU između čitanja i upisa).
|
||||
// Vraća koliko je ugrađeno i koliko nedostaje.
|
||||
func (r *ServisniDeloviRepo) UgradiIliPotrazuj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (ugradjeno, nedostaje int, err error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: begin tx: %w", err)
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var naziv string
|
||||
var stanjePre int
|
||||
err = tx.QueryRowContext(ctx,
|
||||
"SELECT naziv, kolicina FROM artikli WHERE id = ?", artikalID,
|
||||
).Scan(&naziv, &stanjePre)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: dohvati artikal: %w", err)
|
||||
if err = tx.QueryRowContext(ctx,
|
||||
"SELECT kolicina FROM artikli WHERE id = ?", artikalID,
|
||||
).Scan(&stanjePre); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: dohvati stanje: %w", err)
|
||||
}
|
||||
|
||||
// dozvoljavamo backorder: deo se može ugraditi i kad nema dovoljno na stanju
|
||||
// (lager tada ide u minus kao signal da treba nabavka); handler nalog prebacuje
|
||||
// u „Čeka delove". Zato ovde NE odbijamo prekoračenje.
|
||||
stanjePosle := stanjePre - kolicina
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, artikalID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: update stanje: %w", err)
|
||||
// ugrađujemo najviše onoliko koliko fizički imamo; ostatak ide u potraživane
|
||||
ugradjeno = kolicina
|
||||
if ugradjeno > stanjePre {
|
||||
ugradjeno = stanjePre
|
||||
}
|
||||
if ugradjeno < 0 {
|
||||
ugradjeno = 0
|
||||
}
|
||||
nedostaje = kolicina - ugradjeno
|
||||
|
||||
// ako isti artikal već postoji na nalogu — saberi količinu i osveži cenu;
|
||||
// u suprotnom kreiraj novi red (bez dupliranja istog artikla)
|
||||
var deoID int64
|
||||
var postojeciID int64
|
||||
var postojeciKol int
|
||||
err = tx.QueryRowContext(ctx,
|
||||
"SELECT id, kolicina FROM servisni_delovi WHERE nalog_id = ? AND artikal_id = ?",
|
||||
nalogID, artikalID,
|
||||
).Scan(&postojeciID, &postojeciKol)
|
||||
|
||||
if err == nil {
|
||||
// već postoji — saberi
|
||||
novaKol := postojeciKol + kolicina
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE servisni_delovi SET kolicina = ?, cena_komada = ? WHERE id = ?",
|
||||
novaKol, cenaKomada, postojeciID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: merge update: %w", err)
|
||||
// ugradi ono što imamo: merge u servisni_delovi, skini sa magacina, zabeleži promenu
|
||||
if ugradjeno > 0 {
|
||||
var postojeciID int64
|
||||
var postojeciKol int
|
||||
errRed := tx.QueryRowContext(ctx,
|
||||
"SELECT id, kolicina FROM servisni_delovi WHERE nalog_id = ? AND artikal_id = ?",
|
||||
nalogID, artikalID,
|
||||
).Scan(&postojeciID, &postojeciKol)
|
||||
if errRed == nil {
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"UPDATE servisni_delovi SET kolicina = ?, cena_komada = ? WHERE id = ?",
|
||||
postojeciKol+ugradjeno, cenaKomada, postojeciID,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: merge: %w", err)
|
||||
}
|
||||
} else if errors.Is(errRed, sql.ErrNoRows) {
|
||||
if _, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
nalogID, artikalID, ugradjeno, cenaKomada,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: insert: %w", err)
|
||||
}
|
||||
} else {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: proveri: %w", errRed)
|
||||
}
|
||||
deoID = postojeciID
|
||||
} else if errors.Is(err, sql.ErrNoRows) {
|
||||
// novi artikal na nalogu — insert
|
||||
rezultat, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
nalogID, artikalID, kolicina, cenaKomada,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: insert: %w", err)
|
||||
|
||||
stanjePosle := stanjePre - ugradjeno
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, artikalID,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: update stanje: %w", err)
|
||||
}
|
||||
deoID, err = rezultat.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: last insert id: %w", err)
|
||||
if err = zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaIzlazServis,
|
||||
-ugradjeno, stanjePre, stanjePosle, nalogID, korisnikID, ""); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: magacin: %w", err)
|
||||
}
|
||||
} else {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: proveri postojanje: %w", err)
|
||||
}
|
||||
|
||||
err = zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaIzlazServis,
|
||||
-kolicina, stanjePre, stanjePosle, nalogID, korisnikID, "")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: magacin: %w", err)
|
||||
// višak → potraživani delovi (ne skidamo sa lagera dok ne stigne)
|
||||
if nedostaje > 0 {
|
||||
var postojeciID int64
|
||||
var postojeciKol int
|
||||
errRed := tx.QueryRowContext(ctx,
|
||||
"SELECT id, kolicina FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND artikal_id = ?",
|
||||
nalogID, artikalID,
|
||||
).Scan(&postojeciID, &postojeciKol)
|
||||
if errRed == nil {
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"UPDATE servisni_potrazivani_delovi SET kolicina = ?, cena_komada = ? WHERE id = ?",
|
||||
postojeciKol+nedostaje, cenaKomada, postojeciID,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: potraživani update: %w", err)
|
||||
}
|
||||
} else if errors.Is(errRed, sql.ErrNoRows) {
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"INSERT INTO servisni_potrazivani_delovi (nalog_id, artikal_id, kolicina, cena_komada) VALUES (?, ?, ?, ?)",
|
||||
nalogID, artikalID, nedostaje, cenaKomada,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: potraživani insert: %w", err)
|
||||
}
|
||||
} else {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: potraživani proveri: %w", errRed)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: commit: %w", err)
|
||||
if err = tx.Commit(); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: commit: %w", err)
|
||||
}
|
||||
|
||||
return deoID, nil
|
||||
return ugradjeno, nedostaje, nil
|
||||
}
|
||||
|
||||
// Obrisi uklanja servisni deo i vraća količinu na stanje u magacinu
|
||||
|
||||
@@ -22,7 +22,7 @@ func NoviServisniPotrazivaniDeloviRepo(db *sql.DB) *ServisniPotrazivaniDeloviRep
|
||||
// DohvatiZaNalog vraća sve potraživane delove za dati servisni nalog
|
||||
func (r *ServisniPotrazivaniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniPotrazivaniDeo, error) {
|
||||
redovi, err := r.db.QueryContext(ctx, `
|
||||
SELECT spd.id, spd.nalog_id, spd.artikal_id, spd.kolicina, spd.datum,
|
||||
SELECT spd.id, spd.nalog_id, spd.artikal_id, spd.kolicina, spd.cena_komada, spd.datum,
|
||||
a.naziv
|
||||
FROM servisni_potrazivani_delovi spd
|
||||
JOIN artikli a ON a.id = spd.artikal_id
|
||||
@@ -36,7 +36,7 @@ func (r *ServisniPotrazivaniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalo
|
||||
var rezultat []model.ServisniPotrazivaniDeo
|
||||
for redovi.Next() {
|
||||
var d model.ServisniPotrazivaniDeo
|
||||
err := redovi.Scan(&d.ID, &d.NalogID, &d.ArtikalID, &d.Kolicina, &d.Datum, &d.ArtikalNaziv)
|
||||
err := redovi.Scan(&d.ID, &d.NalogID, &d.ArtikalID, &d.Kolicina, &d.CenaKomada, &d.Datum, &d.ArtikalNaziv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DohvatiZaNalog: scan: %w", err)
|
||||
}
|
||||
@@ -45,46 +45,6 @@ func (r *ServisniPotrazivaniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalo
|
||||
return rezultat, nil
|
||||
}
|
||||
|
||||
// DodajIliUvecaj dodaje novi potraživani deo ili uvećava količinu ako već postoji.
|
||||
// Vraća ID reda (novog ili postojećeg).
|
||||
func (r *ServisniPotrazivaniDeloviRepo) DodajIliUvecaj(ctx context.Context, nalogID, artikalID int64, kolicina int) (int64, error) {
|
||||
var postojeciID int64
|
||||
var postojeciKol int
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
"SELECT id, kolicina FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND artikal_id = ?",
|
||||
nalogID, artikalID,
|
||||
).Scan(&postojeciID, &postojeciKol)
|
||||
|
||||
if err == nil {
|
||||
novaKol := postojeciKol + kolicina
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
"UPDATE servisni_potrazivani_delovi SET kolicina = ? WHERE id = ?",
|
||||
novaKol, postojeciID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DodajIliUvecaj: update: %w", err)
|
||||
}
|
||||
return postojeciID, nil
|
||||
}
|
||||
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
rez, err := r.db.ExecContext(ctx,
|
||||
"INSERT INTO servisni_potrazivani_delovi (nalog_id, artikal_id, kolicina) VALUES (?, ?, ?)",
|
||||
nalogID, artikalID, kolicina,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DodajIliUvecaj: insert: %w", err)
|
||||
}
|
||||
id, err := rez.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DodajIliUvecaj: last insert id: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DodajIliUvecaj: proveri: %w", err)
|
||||
}
|
||||
|
||||
// Obrisi uklanja potraživani deo po ID-u
|
||||
func (r *ServisniPotrazivaniDeloviRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM servisni_potrazivani_delovi WHERE id = ?", id)
|
||||
@@ -136,7 +96,7 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con
|
||||
}
|
||||
|
||||
redovi, err := tx.QueryContext(ctx,
|
||||
"SELECT id, nalog_id, kolicina FROM servisni_potrazivani_delovi WHERE artikal_id = ? ORDER BY datum",
|
||||
"SELECT id, nalog_id, kolicina, cena_komada FROM servisni_potrazivani_delovi WHERE artikal_id = ? ORDER BY datum",
|
||||
artikalID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -147,11 +107,12 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con
|
||||
id int64
|
||||
nalogID int64
|
||||
kolicina int
|
||||
cena float64
|
||||
}
|
||||
var lista []red
|
||||
for redovi.Next() {
|
||||
var p red
|
||||
if err := redovi.Scan(&p.id, &p.nalogID, &p.kolicina); err != nil {
|
||||
if err := redovi.Scan(&p.id, &p.nalogID, &p.kolicina, &p.cena); err != nil {
|
||||
redovi.Close()
|
||||
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: scan: %w", err)
|
||||
}
|
||||
@@ -171,24 +132,30 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con
|
||||
break
|
||||
}
|
||||
if dostupno >= p.kolicina {
|
||||
// ceo red je pokriven — obriši ga i skini punu količinu sa magacina
|
||||
if _, err := tx.ExecContext(ctx, "DELETE FROM servisni_potrazivani_delovi WHERE id = ?", p.id); err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: delete: %w", err)
|
||||
// ceo red je pokriven — prebaci ga u ugrađene delove, skini sa magacina, obriši red
|
||||
if err := r.ugradiUNalog(ctx, tx, p.nalogID, artikalID, p.kolicina, p.cena); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.skiniSaMagacina(ctx, tx, artikalID, p.kolicina, &stanjeMagacin, p.nalogID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, "DELETE FROM servisni_potrazivani_delovi WHERE id = ?", p.id); err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: delete: %w", err)
|
||||
}
|
||||
obrisaniNalozi[p.nalogID] = struct{}{}
|
||||
dostupno -= p.kolicina
|
||||
} else {
|
||||
// delimično pokrivanje — sve dostupno ide na nalog; skini dostupno sa magacina
|
||||
novaKol := p.kolicina - dostupno
|
||||
if _, err := tx.ExecContext(ctx, "UPDATE servisni_potrazivani_delovi SET kolicina = ? WHERE id = ?", novaKol, p.id); err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: update: %w", err)
|
||||
// delimično pokrivanje — sve dostupno ide na nalog; prebaci u ugrađene, skini sa magacina
|
||||
if err := r.ugradiUNalog(ctx, tx, p.nalogID, artikalID, dostupno, p.cena); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.skiniSaMagacina(ctx, tx, artikalID, dostupno, &stanjeMagacin, p.nalogID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
novaKol := p.kolicina - dostupno
|
||||
if _, err := tx.ExecContext(ctx, "UPDATE servisni_potrazivani_delovi SET kolicina = ? WHERE id = ?", novaKol, p.id); err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: update: %w", err)
|
||||
}
|
||||
dostupno = 0
|
||||
}
|
||||
}
|
||||
@@ -213,6 +180,49 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con
|
||||
return otkljucani, nil
|
||||
}
|
||||
|
||||
// ugradiUNalog prebacuje pokrivenu količinu u ugrađene delove (servisni_delovi):
|
||||
// uvećava postojeći red za isti artikal na nalogu ili kreira novi. Radi unutar
|
||||
// prosleđene transakcije. Ne dira magacin — to radi skiniSaMagacina zasebno.
|
||||
func (r *ServisniPotrazivaniDeloviRepo) ugradiUNalog(ctx context.Context, tx *sql.Tx, nalogID, artikalID int64, kolicina int, cenaKomada float64) error {
|
||||
var postojeciID int64
|
||||
var postojeciKol int
|
||||
err := tx.QueryRowContext(ctx,
|
||||
"SELECT id, kolicina FROM servisni_delovi WHERE nalog_id = ? AND artikal_id = ?",
|
||||
nalogID, artikalID,
|
||||
).Scan(&postojeciID, &postojeciKol)
|
||||
|
||||
if err == nil {
|
||||
// ne pregazi postojeću cenu nulom (npr. legacy potraživani red bez cene)
|
||||
if cenaKomada > 0 {
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE servisni_delovi SET kolicina = ?, cena_komada = ? WHERE id = ?",
|
||||
postojeciKol+kolicina, cenaKomada, postojeciID,
|
||||
)
|
||||
} else {
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE servisni_delovi SET kolicina = ? WHERE id = ?",
|
||||
postojeciKol+kolicina, postojeciID,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ugradiUNalog: update: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
nalogID, artikalID, kolicina, cenaKomada,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ugradiUNalog: insert: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ugradiUNalog: proveri: %w", err)
|
||||
}
|
||||
|
||||
// skiniSaMagacina umanjuje stanje artikla za datu količinu (jer odlazi na servisni
|
||||
// nalog) i upisuje promenu u magacinski trag tipa PromenaIzlazServis. Radi unutar
|
||||
// prosleđene transakcije; *stanje drži tekuće stanje radi tačnog stanjePre/stanjePosle.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPotrazivaniTok pokriva ceo tok: ugradnja sa manjkom → potraživani deo →
|
||||
// dolazak robe → pokrivanje (prebacivanje u ugrađene, skidanje magacina, otključavanje).
|
||||
func TestPotrazivaniTok(t *testing.T) {
|
||||
db := testDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// artikal sa stanjem 2, nalog u statusu Primljeno
|
||||
if _, err := db.ExecContext(ctx, "INSERT INTO artikli (id, naziv, kolicina) VALUES (1, 'Test CPU', 2)"); err != nil {
|
||||
t.Fatalf("insert artikal: %v", err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx,
|
||||
"INSERT INTO servisni_nalozi (id, broj_naloga, uredjaj, opis_kvara, status) VALUES (1, 'SN-1', 'PC', 'kvar', 'Primljeno')",
|
||||
); err != nil {
|
||||
t.Fatalf("insert nalog: %v", err)
|
||||
}
|
||||
|
||||
deloviRepo := NoviServisniDeloviRepo(db)
|
||||
potrRepo := NoviServisniPotrazivaniDeloviRepo(db)
|
||||
|
||||
// traži 5, na stanju 2 → ugradi 2, nedostaje 3
|
||||
ugradjeno, nedostaje, err := deloviRepo.UgradiIliPotrazuj(ctx, 1, 1, 5, 100, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("UgradiIliPotrazuj: %v", err)
|
||||
}
|
||||
if ugradjeno != 2 || nedostaje != 3 {
|
||||
t.Fatalf("očekivano ugradjeno=2 nedostaje=3, dobijeno ugradjeno=%d nedostaje=%d", ugradjeno, nedostaje)
|
||||
}
|
||||
|
||||
// stanje magacina mora biti 0 (skinuto 2), NE u minus
|
||||
if stanje := skalarInt(t, db, "SELECT kolicina FROM artikli WHERE id=1"); stanje != 0 {
|
||||
t.Fatalf("posle ugradnje stanje očekivano 0, dobijeno %d", stanje)
|
||||
}
|
||||
// ugrađeni deo: 2 kom; potraživani: 3 kom
|
||||
if k := skalarInt(t, db, "SELECT kolicina FROM servisni_delovi WHERE nalog_id=1 AND artikal_id=1"); k != 2 {
|
||||
t.Fatalf("ugrađeni deo očekivano 2, dobijeno %d", k)
|
||||
}
|
||||
if k := skalarInt(t, db, "SELECT kolicina FROM servisni_potrazivani_delovi WHERE nalog_id=1 AND artikal_id=1"); k != 3 {
|
||||
t.Fatalf("potraživani očekivano 3, dobijeno %d", k)
|
||||
}
|
||||
|
||||
// stigla roba: nabavka 5 kom → stanje 0+5=5
|
||||
if _, err := db.ExecContext(ctx, "UPDATE artikli SET kolicina = kolicina + 5 WHERE id=1"); err != nil {
|
||||
t.Fatalf("nabavka: %v", err)
|
||||
}
|
||||
|
||||
// pokrivanje
|
||||
otkljucani, err := potrRepo.ProveriIPocistiZaArtikal(ctx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("ProveriIPocistiZaArtikal: %v", err)
|
||||
}
|
||||
if len(otkljucani) != 1 || otkljucani[0] != 1 {
|
||||
t.Fatalf("očekivan otključan nalog [1], dobijeno %v", otkljucani)
|
||||
}
|
||||
|
||||
// ugrađeni deo sada 5 kom (2 + 3 pokriveno)
|
||||
if k := skalarInt(t, db, "SELECT kolicina FROM servisni_delovi WHERE nalog_id=1 AND artikal_id=1"); k != 5 {
|
||||
t.Fatalf("ugrađeni deo posle pokrivanja očekivano 5, dobijeno %d", k)
|
||||
}
|
||||
// potraživanih više nema
|
||||
if k := skalarInt(t, db, "SELECT COUNT(*) FROM servisni_potrazivani_delovi WHERE nalog_id=1"); k != 0 {
|
||||
t.Fatalf("potraživani treba da su prazni, ostalo %d", k)
|
||||
}
|
||||
// stanje magacina: 5 − 3 (skinuto pri pokrivanju) = 2
|
||||
if stanje := skalarInt(t, db, "SELECT kolicina FROM artikli WHERE id=1"); stanje != 2 {
|
||||
t.Fatalf("stanje posle pokrivanja očekivano 2, dobijeno %d", stanje)
|
||||
}
|
||||
}
|
||||
|
||||
// skalarInt vraća jednu celobrojnu vrednost iz upita (za proveru stanja u testu)
|
||||
func skalarInt(t *testing.T, db *sql.DB, upit string) int {
|
||||
t.Helper()
|
||||
var v int
|
||||
if err := db.QueryRow(upit).Scan(&v); err != nil {
|
||||
t.Fatalf("skalarInt %q: %v", upit, err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -251,12 +251,24 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// promenu količine sprovodimo kroz KorigujKolicinu da bi se zabeležio magacinski
|
||||
// trag (korekcija); Izmeni menja samo metapodatke, ne dira stanje
|
||||
novaKolicina := artikal.Kolicina
|
||||
artikal.Kolicina = staraKolicina
|
||||
|
||||
artikal.ID = id
|
||||
if err := h.Artikli.Izmeni(r.Context(), &artikal); err != nil {
|
||||
http.Error(w, "Greška pri čuvanju izmene", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// ako se količina promenila, koriguj stanje uz magacinski trag
|
||||
if novaKolicina != staraKolicina {
|
||||
if e := h.Artikli.KorigujKolicinu(r.Context(), id, novaKolicina, &k.ID, "izmena artikla"); e != nil {
|
||||
slog.Error("korekcija količine pri izmeni artikla nije uspela", "artikal_id", id, "error", e)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -277,7 +289,7 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// ako je stanje poraslo, proveri potraživane servisne delove
|
||||
if artikal.Kolicina > staraKolicina {
|
||||
if novaKolicina > staraKolicina {
|
||||
otkljucani, err := h.ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal(r.Context(), id)
|
||||
if err != nil {
|
||||
slog.Error("provera potraživanih delova nije uspela", "artikal_id", id, "error", err)
|
||||
|
||||
@@ -535,36 +535,23 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// proveri stanje na magacinu: ako tražimo više nego što ima, ugrađujemo samo
|
||||
// ono što je na stanju, a razliku beležimo u potraživane delove (ne skida se
|
||||
// sa lagera dok ne stigne); lager NIKAD ne ide u minus
|
||||
ugradjeno := kolicina
|
||||
nedostaje := 0
|
||||
// ime artikla za poruke korisniku
|
||||
imeArtikla := ""
|
||||
if art, e := h.Artikli.DohvatiID(r.Context(), artikalID); e == nil && art != nil {
|
||||
imeArtikla = art.Naziv
|
||||
if kolicina > art.Kolicina {
|
||||
ugradjeno = art.Kolicina
|
||||
nedostaje = kolicina - art.Kolicina
|
||||
}
|
||||
}
|
||||
|
||||
// ugrađujemo samo ono što fizički imamo (ako ima > 0)
|
||||
if ugradjeno > 0 {
|
||||
if _, err := h.ServisniDeloviRepo.Dodaj(r.Context(), nalogID, artikalID, ugradjeno, cena, &k.ID); err != nil {
|
||||
slog.Error("greška pri dodavanju dela", "error", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.")
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// atomično: ugradi ono što imamo (skida sa lagera, ne ide u minus), višak u potraživane
|
||||
ugradjeno, nedostaje, err := h.ServisniDeloviRepo.UgradiIliPotrazuj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID)
|
||||
if err != nil {
|
||||
slog.Error("greška pri dodavanju dela", "error", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.")
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// višak → potraživani delovi (ne skidamo sa lagera)
|
||||
// ako nešto nedostaje, prebaci nalog u „Čeka delove" i obavesti korisnika
|
||||
if nedostaje > 0 {
|
||||
if _, err := h.ServisniPotrazivaniDeloviRepo.DodajIliUvecaj(r.Context(), nalogID, artikalID, nedostaje); err != nil {
|
||||
slog.Error("greška pri beleženju potraživanog dela", "error", err)
|
||||
}
|
||||
// automatski prebaci nalog u „Čeka delove"
|
||||
if e := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusCekaDelove); e != nil {
|
||||
slog.Error("greška pri prebacivanju naloga u Čeka delove", "error", e)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
const (
|
||||
StatusPrimljeno = "Primljeno"
|
||||
StatusCekaDelove = "Čeka delove"
|
||||
StatusUPopravci = "U popravci"
|
||||
StatusZavrseno = "Završeno"
|
||||
StatusPreuzeto = "Preuzeto"
|
||||
StatusUPopravci = "U popravci"
|
||||
StatusZavrseno = "Završeno"
|
||||
StatusPreuzeto = "Preuzeto"
|
||||
)
|
||||
|
||||
// SviStatusi je uređena lista statusa za prikaz u dropdownu
|
||||
@@ -72,11 +72,12 @@ type ServisniDeoSaArtiklom struct {
|
||||
// ServisniPotrazivaniDeo beleži artikle koji nedostaju za servisni nalog —
|
||||
// količina koja se traži a nije na stanju; ne skida se sa lagera dok ne stigne
|
||||
type ServisniPotrazivaniDeo struct {
|
||||
ID int64
|
||||
NalogID int64
|
||||
ArtikalID int64
|
||||
Kolicina int
|
||||
Datum time.Time
|
||||
ID int64
|
||||
NalogID int64
|
||||
ArtikalID int64
|
||||
Kolicina int
|
||||
CenaKomada float64
|
||||
Datum time.Time
|
||||
// za prikaz:
|
||||
ArtikalNaziv string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- cena po komadu za potraživani deo, da bi pri pokrivanju (kad roba stigne)
|
||||
-- deo mogao da se prebaci u ugrađene delove sa ispravnom cenom
|
||||
ALTER TABLE servisni_potrazivani_delovi ADD COLUMN cena_komada REAL NOT NULL DEFAULT 0;
|
||||
Reference in New Issue
Block a user