Sprečeno dupliranje prodaje/servisa, alat za usklađivanje šifri artikla, ispravke osvežavanja stranica, zaštita od duplikata kategorije/artikla/usluge/troška

- Prodaja i servis: broj naloga se generiše unutar transakcije upisa umesto unapred u handleru, čime se otklanja duplo kreiranje naloga pri dvostrukom slanju forme
- Servis izmena: uklonjen sudar generičkog i sopstvenog auto-snimanja forme (dva paralelna POST-a)
- Magacin: novi alat 'Uskladi šifre sa kategorijama' za masovnu ispravku šifri artikala posle uvođenja šifarnika po kategoriji
- Ispravljeno da forme na 9 stranica (kategorije, admin korisnici/dozvole, profil, podešavanja) posle akcije osveže prikaz umesto da ostanu zastarele do F5
- Kategorija, artikal, usluga i trošak: sprečeno dupliranje naziva/šifre/koda (UNIQUE indeksi + razumljive poruke grešaka)
This commit is contained in:
2026-07-07 03:50:31 +02:00
parent 1ff572dd77
commit bc60288a0d
28 changed files with 682 additions and 110 deletions
+23 -2
View File
@@ -23,18 +23,29 @@ func NoviProdajaRepo(db *sql.DB) *ProdajaRepo {
// SledeciBroj generiše sledeći broj naloga u formatu PR-GGMM-NNNN
// (GG dvocifrena godina, MM mesec); brojač NNNN se resetuje svakog meseca
func (r *ProdajaRepo) SledeciBroj(ctx context.Context) (string, error) {
return sledeciBrojProdaje(ctx, r.db)
}
// sledeciBrojProdaje čita i generiše sledeći broj naloga preko prosleđenog
// izvršioca upita (r.db ili tx). Kreiraj je poziva NAD ISTOM transakcijom
// u kojoj upisuje nalog — pošto konekcija koristi _txlock=immediate, write
// lock se drži od BeginTx, pa je ovo čitanje-pa-upis atomsko i bez race-a
// između dva konkurentna/duplirana zahteva.
func sledeciBrojProdaje(ctx context.Context, q interface {
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}) (string, error) {
sada := time.Now()
// prefiks "PR-GGMM-" je dug 8 karaktera, pa brojač počinje od 9. karaktera
prefiks := fmt.Sprintf("PR-%02d%02d-", sada.Year()%100, int(sada.Month()))
uzorak := prefiks + "%"
var sledeci int
err := r.db.QueryRowContext(ctx, `
err := q.QueryRowContext(ctx, `
SELECT COALESCE(MAX(CAST(SUBSTR(broj_naloga, 9) AS INTEGER)), 0) + 1
FROM prodajni_nalozi
WHERE broj_naloga LIKE ?`, uzorak).Scan(&sledeci)
if err != nil {
return "", fmt.Errorf("ntech: ProdajaRepo.SledeciBroj: %w", err)
return "", fmt.Errorf("ntech: sledeciBrojProdaje: %w", err)
}
return fmt.Sprintf("%s%04d", prefiks, sledeci), nil
@@ -184,6 +195,16 @@ func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavk
}
defer tx.Rollback()
// broj naloga se generiše OVDE, unutar iste transakcije kao insert —
// ne sme se prosleđivati kao unapred generisana vrednost iz handlera,
// jer bi dupliran/ponovljen POST zahtev tada napravio dva zasebna,
// validna naloga sa istim stavkama umesto da drugi bude odbijen.
brojNaloga, err := sledeciBrojProdaje(ctx, tx)
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: broj naloga: %w", err)
}
n.BrojNaloga = brojNaloga
// insert zaglavlja naloga pre stavki da bismo imali nalogID za magacin
rezultat, err := tx.ExecContext(ctx, `
INSERT INTO prodajni_nalozi (klijent_id, broj_naloga, napomena, ukupno, nacin_placanja, datum)