Servis: storno naloga (kao Prodaja/Nabavka) — KIR/KPO storno stavke, fiskalni refund
Do sada je jedini način da se poništi naplaćen/preuzet servisni nalog bio fizičko brisanje (ObrisiNalog), koje ostavlja KIR/KPO/fiskalne zapise sa mrtvim izvor_id — prihod ostaje prijavljen za nalog koji više ne postoji. Migracija 100: servisni_nalozi dobija stornirano/razlog_storniranja (kao prodajni_nalozi/nabavke). ServisRepo.Storno vraća ugrađene delove na stanje ali NE briše nalog. Novi handler StornoNaloga (dozvola servis.storno): - KIR/KPO original ostaje, dograđuje se storno stavka (model.KirStorno/KpoStorno) - fiskalni refund (best-effort) preko refundujServis — Refund zahtev sa ReferentDocumentNumber ka originalnom PFR broju, isti obrazac kao stornirajProdaju - ObrisiNalog sad odbija brisanje ako nalog već ima fiskalni/KIR/KPO zapis i upućuje na Storno stornoBezRefunda (dashboard upozorenje) prošireno da prati i servisne stornose bez uspešnog fiskalnog refunda, ne samo prodaju.
This commit is contained in:
@@ -58,7 +58,7 @@ func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]mode
|
||||
sn.id, sn.klijent_id, sn.tehnicar_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj,
|
||||
sn.opis_kvara, sn.trazene_nadogradnje, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna,
|
||||
sn.avans, sn.napomena, sn.garancija_do, sn.garancija_dana, sn.datum_prijema, sn.datum_zavrsetka, sn.predvidjen_datum,
|
||||
sn.ostecenja, sn.pin_uredjaja, sn.pribor, sn.napomena_klijentu, sn.nalaz_dijagnostike, sn.uradjeno, sn.cena_dijagnostike, sn.popravka_odbijena, sn.javni_token, sn.komentar_klijenta, sn.odluka_klijenta, sn.datum_odluke, sn.nacin_placanja, sn.naplaceno,
|
||||
sn.ostecenja, sn.pin_uredjaja, sn.pribor, sn.napomena_klijentu, sn.nalaz_dijagnostike, sn.uradjeno, sn.cena_dijagnostike, sn.popravka_odbijena, sn.javni_token, sn.komentar_klijenta, sn.odluka_klijenta, sn.datum_odluke, sn.nacin_placanja, sn.naplaceno, sn.stornirano, sn.razlog_storniranja,
|
||||
COALESCE(kp.naziv, '') AS klijent_naziv,
|
||||
(EXISTS(SELECT 1 FROM servis_radovi sr WHERE sr.nalog_id = sn.id AND sr.predlozeno = 1)
|
||||
OR EXISTS(SELECT 1 FROM servisni_delovi sd WHERE sd.nalog_id = sn.id AND sd.predlozeno = 1)
|
||||
@@ -108,7 +108,7 @@ func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNa
|
||||
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta, odluka_klijenta, datum_odluke, nacin_placanja, naplaceno
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta, odluka_klijenta, datum_odluke, nacin_placanja, naplaceno, stornirano, razlog_storniranja
|
||||
FROM servisni_nalozi WHERE id = ?`, id)
|
||||
|
||||
var n model.ServisniNalog
|
||||
@@ -160,7 +160,7 @@ func (r *ServisRepo) DohvatiJavniToken(ctx context.Context, token string) (*mode
|
||||
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta, odluka_klijenta, datum_odluke, nacin_placanja, naplaceno
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta, odluka_klijenta, datum_odluke, nacin_placanja, naplaceno, stornirano, razlog_storniranja
|
||||
FROM servisni_nalozi WHERE javni_token = ?`, token)
|
||||
|
||||
var n model.ServisniNalog
|
||||
@@ -461,6 +461,86 @@ func (r *ServisRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// Storno stornira servisni nalog: vraća ugrađene delove na stanje (isto kao
|
||||
// Obrisi) ali NALOG SE NE BRIŠE — samo se markira stornirano=1, po istom principu
|
||||
// kao Prodaja. Finansijski zapisi (KIR/KPO/fiskalni) se ne diraju ovde — to radi
|
||||
// handler posle uspešnog storna, dograđivanjem storno stavki na osnovu izvor_id.
|
||||
func (r *ServisRepo) Storno(ctx context.Context, id int64, razlog string, korisnikID *int64) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var vecStornirano bool
|
||||
if err := tx.QueryRowContext(ctx, "SELECT stornirano FROM servisni_nalozi WHERE id = ?", id).Scan(&vecStornirano); err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: provera: %w", err)
|
||||
}
|
||||
if vecStornirano {
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: nalog je već storniran")
|
||||
}
|
||||
|
||||
redovi, err := tx.QueryContext(ctx,
|
||||
"SELECT artikal_id, kolicina FROM servisni_delovi WHERE nalog_id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: dohvati delove: %w", err)
|
||||
}
|
||||
type povrat struct {
|
||||
artikalID int64
|
||||
kolicina int
|
||||
}
|
||||
var delovi []povrat
|
||||
for redovi.Next() {
|
||||
var p povrat
|
||||
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
|
||||
redovi.Close()
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: scan dela: %w", err)
|
||||
}
|
||||
delovi = append(delovi, p)
|
||||
}
|
||||
redovi.Close()
|
||||
|
||||
for _, p := range delovi {
|
||||
var stanjePre int
|
||||
var tip string
|
||||
err := tx.QueryRowContext(ctx,
|
||||
"SELECT kolicina, tip FROM artikli WHERE id = ?", p.artikalID,
|
||||
).Scan(&stanjePre, &tip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: dohvati stanje: %w", err)
|
||||
}
|
||||
if !(tip == model.TipProizvod || tip == "") {
|
||||
continue
|
||||
}
|
||||
|
||||
stanjePosle := stanjePre + p.kolicina
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, p.artikalID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: vrati stanje: %w", err)
|
||||
}
|
||||
|
||||
err = zabeleziMagacinPromenu(ctx, tx, p.artikalID, model.PromenaPovracaj,
|
||||
p.kolicina, stanjePre, stanjePosle, id, korisnikID, "storno servisnog naloga")
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: magacin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
"UPDATE servisni_nalozi SET stornirano = 1, razlog_storniranja = ? WHERE id = ?",
|
||||
nullString(razlog), id,
|
||||
); err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: update: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Storno: commit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SacuvajNaplatu beleži način plaćanja i naplaćeni iznos pri preuzimanju uređaja.
|
||||
func (r *ServisRepo) SacuvajNaplatu(ctx context.Context, id int64, nacinPlacanja string, naplaceno float64) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
@@ -485,13 +565,15 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
||||
|
||||
var nacinPlacanja sql.NullString
|
||||
var naplaceno sql.NullFloat64
|
||||
var stornirano sql.NullInt64
|
||||
var razlogStorniranja sql.NullString
|
||||
|
||||
args := []any{
|
||||
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
||||
&n.OpisKvara, &n.TrazeneNadogradnje, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
||||
&avans, &napomena, &garancijaDo, &garancijaDana, &n.DatumPrijema, &datumZavrsetka, &predvidjenDatum,
|
||||
&ostecenja, &pinUredjaja, &pribor, &napomenaKlijentu, &nalazDijagnostike, &uradjeno, &n.CenaDijagnostike, &popravkaOdbijena, &javniToken, &komentarKlijenta, &odlukaKlijenta, &datumOdluke,
|
||||
&nacinPlacanja, &naplaceno,
|
||||
&nacinPlacanja, &naplaceno, &stornirano, &razlogStorniranja,
|
||||
}
|
||||
|
||||
if klijentNaziv != nil {
|
||||
@@ -565,6 +647,8 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
||||
if naplaceno.Valid {
|
||||
n.Naplaceno = naplaceno.Float64
|
||||
}
|
||||
n.Stornirano = stornirano.Int64 != 0
|
||||
n.RazlogStorniranja = razlogStorniranja.String
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user