From 45f5db14d8045e4788ab9ec756ba9400c2b12a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Mon, 22 Jun 2026 15:42:22 +0200 Subject: [PATCH] =?UTF-8?q?Servis:=20pokrivanje=20potra=C5=BEivanih=20delo?= =?UTF-8?q?va=20skida=20magacin=20i=20otklju=C4=8Dava=20nalog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/sqlite/servisni_potrazivani_delovi.go | 69 ++++++++++++++++--- internal/handler/izvestaji.go | 16 +++++ 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/internal/db/sqlite/servisni_potrazivani_delovi.go b/internal/db/sqlite/servisni_potrazivani_delovi.go index be5a830..d7acb5b 100644 --- a/internal/db/sqlite/servisni_potrazivani_delovi.go +++ b/internal/db/sqlite/servisni_potrazivani_delovi.go @@ -106,13 +106,28 @@ func (r *ServisniPotrazivaniDeloviRepo) ObrisiZaArtikal(ctx context.Context, nal return nil } -// ProveriIPocistiZaArtikal poziva se nakon što stanje artikla poraste (nabavka) -// i čisti potraživane redove koji se sada mogu pokriti dostupnim stanjem (FIFO). -// Delimično pokrivanje smanjuje traženu količinu umesto brisanja. +// ProveriIPocistiZaArtikal poziva se nakon što stanje artikla poraste (nabavka +// ili direktan unos na kartici/popisu) i čisti potraživane redove koji se sada +// mogu pokriti dostupnim stanjem (FIFO). +// +// Kada se potraženi deo (delimično ili u celosti) pokrije, ta količina se ODMAH +// skida sa magacina jer fizički odlazi na servisni nalog; svaka takva promena se +// beleži u magacinski revizijski trag (tip PromenaIzlazServis). Delimično +// pokrivanje smanjuje traženu količinu umesto brisanja reda. +// +// Sve se izvršava u jednoj transakciji da bi skidanje magacina i čišćenje +// potraživanih redova bili atomični. +// // Vraća ID-eve naloga čiji su svi potraživani redovi obrisani (nalog može da se otključa). func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Context, artikalID int64) ([]int64, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: begin: %w", err) + } + defer tx.Rollback() + var stanje int - err := r.db.QueryRowContext(ctx, "SELECT kolicina FROM artikli WHERE id = ?", artikalID).Scan(&stanje) + err = tx.QueryRowContext(ctx, "SELECT kolicina FROM artikli WHERE id = ?", artikalID).Scan(&stanje) if err != nil { return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: stanje: %w", err) } @@ -120,14 +135,13 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con return nil, nil } - redovi, err := r.db.QueryContext(ctx, + redovi, err := tx.QueryContext(ctx, "SELECT id, nalog_id, kolicina FROM servisni_potrazivani_delovi WHERE artikal_id = ? ORDER BY datum", artikalID, ) if err != nil { return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: query: %w", err) } - defer redovi.Close() type red struct { id int64 @@ -138,6 +152,7 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con for redovi.Next() { var p red if err := redovi.Scan(&p.id, &p.nalogID, &p.kolicina); err != nil { + redovi.Close() return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: scan: %w", err) } lista = append(lista, p) @@ -147,22 +162,33 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con // prati naloge iz kojih smo obrisali red obrisaniNalozi := map[int64]struct{}{} + // tekuće stanje magacina koje se umanjuje kako pokrivamo potraživane delove; + // svaka promena se beleži sa stanjePre/stanjePosle radi tačnog revizijskog traga + stanjeMagacin := stanje dostupno := stanje for _, p := range lista { if dostupno <= 0 { break } if dostupno >= p.kolicina { - if _, err := r.db.ExecContext(ctx, "DELETE FROM servisni_potrazivani_delovi WHERE id = ?", p.id); err != nil { + // 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) } + if err := r.skiniSaMagacina(ctx, tx, artikalID, p.kolicina, &stanjeMagacin, p.nalogID); err != nil { + return nil, 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 := r.db.ExecContext(ctx, "UPDATE servisni_potrazivani_delovi SET kolicina = ? WHERE id = ?", novaKol, p.id); err != nil { + 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) } + if err := r.skiniSaMagacina(ctx, tx, artikalID, dostupno, &stanjeMagacin, p.nalogID); err != nil { + return nil, err + } dostupno = 0 } } @@ -171,12 +197,35 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con var otkljucani []int64 for nalogID := range obrisaniNalozi { var preostalo int - _ = r.db.QueryRowContext(ctx, + if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM servisni_potrazivani_delovi WHERE nalog_id = ?", nalogID, - ).Scan(&preostalo) + ).Scan(&preostalo); err != nil { + return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: prebroji: %w", err) + } if preostalo == 0 { otkljucani = append(otkljucani, nalogID) } } + + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: commit: %w", err) + } return otkljucani, nil } + +// 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. +func (r *ServisniPotrazivaniDeloviRepo) skiniSaMagacina(ctx context.Context, tx *sql.Tx, artikalID int64, kolicina int, stanje *int, nalogID int64) error { + stanjePre := *stanje + stanjePosle := stanjePre - kolicina + if _, err := tx.ExecContext(ctx, "UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, artikalID); err != nil { + return fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.skiniSaMagacina: update stanje: %w", err) + } + if err := zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaIzlazServis, + -kolicina, stanjePre, stanjePosle, nalogID, nil, "pokriven potraživani deo"); err != nil { + return fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.skiniSaMagacina: magacin: %w", err) + } + *stanje = stanjePosle + return nil +} diff --git a/internal/handler/izvestaji.go b/internal/handler/izvestaji.go index 5815c83..f519073 100644 --- a/internal/handler/izvestaji.go +++ b/internal/handler/izvestaji.go @@ -368,9 +368,25 @@ func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) { if nova == a.Kolicina { continue } + povecano := nova > a.Kolicina if err := h.Artikli.KorigujKolicinu(r.Context(), a.ID, nova, &k.ID, napomena); err != nil { slog.Error("popis: korekcija artikla", "id", a.ID, "error", err) greskaBroj++ + continue + } + // ako je stanje poraslo, počisti potraživane delove i otključaj naloge — + // pokriveni delovi se odmah skidaju sa magacina (vidi ProveriIPocistiZaArtikal) + if povecano { + otkljucani, err := h.ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal(r.Context(), a.ID) + if err != nil { + slog.Error("popis: provera potraživanih delova nije uspela", "artikal_id", a.ID, "error", err) + continue + } + for _, nalogID := range otkljucani { + if err := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusPrimljeno); err != nil { + slog.Error("popis: automatski reset statusa naloga nije uspeo", "nalog_id", nalogID, "error", err) + } + } } }