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:
@@ -380,6 +380,7 @@ func main() {
|
|||||||
r.Get("/servis/izmeni/{id}", h.IzmeniNalog)
|
r.Get("/servis/izmeni/{id}", h.IzmeniNalog)
|
||||||
r.With(doz("servis.izmeni")).Post("/servis/izmeni/{id}", h.SacuvajIzmenaNaloga)
|
r.With(doz("servis.izmeni")).Post("/servis/izmeni/{id}", h.SacuvajIzmenaNaloga)
|
||||||
r.With(doz("servis.obrisi")).Post("/servis/obrisi/{id}", h.ObrisiNalog)
|
r.With(doz("servis.obrisi")).Post("/servis/obrisi/{id}", h.ObrisiNalog)
|
||||||
|
r.With(doz("servis.storno")).Post("/servis/storno/{id}", h.StornoNaloga)
|
||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}", h.DetaljiNaloga)
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}", h.DetaljiNaloga)
|
||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/radni-nalog", h.StampaRadnogNaloga)
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/radni-nalog", h.StampaRadnogNaloga)
|
||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/otpremnica", h.StampaOtpremnice)
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/otpremnica", h.StampaOtpremnice)
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ type ServisRepository interface {
|
|||||||
SacuvajOdlukuKlijenta(ctx context.Context, id int64, odluka string, odgovor string) error
|
SacuvajOdlukuKlijenta(ctx context.Context, id int64, odluka string, odgovor string) error
|
||||||
ObrisiOdlukuKlijenta(ctx context.Context, id int64) error
|
ObrisiOdlukuKlijenta(ctx context.Context, id int64) error
|
||||||
Obrisi(ctx context.Context, id int64, korisnikID *int64) error
|
Obrisi(ctx context.Context, id int64, korisnikID *int64) error
|
||||||
|
// Storno stornira nalog (vraća ugrađene delove na stanje, markira stornirano=1)
|
||||||
|
// bez brisanja — koristi se kad nalog već ima finansijske zapise (KIR/KPO/fiskalni)
|
||||||
|
Storno(ctx context.Context, id int64, razlog string, korisnikID *int64) error
|
||||||
SledeciBroj(ctx context.Context) (string, error)
|
SledeciBroj(ctx context.Context) (string, error)
|
||||||
SacuvajNaplatu(ctx context.Context, id int64, nacinPlacanja string, naplaceno float64) error
|
SacuvajNaplatu(ctx context.Context, id int64, nacinPlacanja string, naplaceno float64) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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.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.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,
|
COALESCE(kp.naziv, '') AS klijent_naziv,
|
||||||
(EXISTS(SELECT 1 FROM servis_radovi sr WHERE sr.nalog_id = sn.id AND sr.predlozeno = 1)
|
(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)
|
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,
|
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
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)
|
FROM servisni_nalozi WHERE id = ?`, id)
|
||||||
|
|
||||||
var n model.ServisniNalog
|
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,
|
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
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)
|
FROM servisni_nalozi WHERE javni_token = ?`, token)
|
||||||
|
|
||||||
var n model.ServisniNalog
|
var n model.ServisniNalog
|
||||||
@@ -461,6 +461,86 @@ func (r *ServisRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) er
|
|||||||
return nil
|
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.
|
// 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 {
|
func (r *ServisRepo) SacuvajNaplatu(ctx context.Context, id int64, nacinPlacanja string, naplaceno float64) error {
|
||||||
_, err := r.db.ExecContext(ctx,
|
_, 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 nacinPlacanja sql.NullString
|
||||||
var naplaceno sql.NullFloat64
|
var naplaceno sql.NullFloat64
|
||||||
|
var stornirano sql.NullInt64
|
||||||
|
var razlogStorniranja sql.NullString
|
||||||
|
|
||||||
args := []any{
|
args := []any{
|
||||||
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
||||||
&n.OpisKvara, &n.TrazeneNadogradnje, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
&n.OpisKvara, &n.TrazeneNadogradnje, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
||||||
&avans, &napomena, &garancijaDo, &garancijaDana, &n.DatumPrijema, &datumZavrsetka, &predvidjenDatum,
|
&avans, &napomena, &garancijaDo, &garancijaDana, &n.DatumPrijema, &datumZavrsetka, &predvidjenDatum,
|
||||||
&ostecenja, &pinUredjaja, &pribor, &napomenaKlijentu, &nalazDijagnostike, &uradjeno, &n.CenaDijagnostike, &popravkaOdbijena, &javniToken, &komentarKlijenta, &odlukaKlijenta, &datumOdluke,
|
&ostecenja, &pinUredjaja, &pribor, &napomenaKlijentu, &nalazDijagnostike, &uradjeno, &n.CenaDijagnostike, &popravkaOdbijena, &javniToken, &komentarKlijenta, &odlukaKlijenta, &datumOdluke,
|
||||||
&nacinPlacanja, &naplaceno,
|
&nacinPlacanja, &naplaceno, &stornirano, &razlogStorniranja,
|
||||||
}
|
}
|
||||||
|
|
||||||
if klijentNaziv != nil {
|
if klijentNaziv != nil {
|
||||||
@@ -565,6 +647,8 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
|||||||
if naplaceno.Valid {
|
if naplaceno.Valid {
|
||||||
n.Naplaceno = naplaceno.Float64
|
n.Naplaceno = naplaceno.Float64
|
||||||
}
|
}
|
||||||
|
n.Stornirano = stornirano.Int64 != 0
|
||||||
|
n.RazlogStorniranja = razlogStorniranja.String
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+246
-27
@@ -85,6 +85,7 @@ type PodaciDetaljiNaloga struct {
|
|||||||
FiskalniRacun *model.FiskalniRacun // nil ako nije fiskalizovano
|
FiskalniRacun *model.FiskalniRacun // nil ako nije fiskalizovano
|
||||||
RokPodizanja *time.Time // DatumZavrsetka + 30 dana; nil dok nije završeno
|
RokPodizanja *time.Time // DatumZavrsetka + 30 dana; nil dok nije završeno
|
||||||
FiskalGreska bool // true: fiskalizacija aktivna, status Preuzeto, ali nema fiskalnog računa
|
FiskalGreska bool // true: fiskalizacija aktivna, status Preuzeto, ali nema fiskalnog računa
|
||||||
|
PotrebnaIdentKupca bool // true: refundacija zahteva ručni unos PIB/JMBG kupca (nema ga iz klijenta)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
|
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
|
||||||
@@ -505,7 +506,9 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObrisiNalog prima POST zahtev i briše servisni nalog po ID-u
|
// ObrisiNalog prima POST zahtev i briše servisni nalog po ID-u. Odbija brisanje
|
||||||
|
// ako nalog već ima finansijske zapise (fiskalni račun, KIR ili KPO) — brisanje bi
|
||||||
|
// ostavilo te zapise sa mrtvim izvor_id; za takve naloge koristi se Storno.
|
||||||
func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
|
||||||
k, ok := h.zahtevajDozvolu(w, r, "servis.obrisi")
|
k, ok := h.zahtevajDozvolu(w, r, "servis.obrisi")
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -517,6 +520,17 @@ func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if imaFinansijskeZapise, err := h.servisImaFinansijskeZapise(r.Context(), id); err != nil {
|
||||||
|
slog.Error("provera finansijskih zapisa nije uspela", "id", id, "error", err)
|
||||||
|
http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
} else if imaFinansijskeZapise {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska",
|
||||||
|
"Nalog ima fiskalni račun i/ili KIR/KPO upis — ne može se obrisati. Koristite Storno.")
|
||||||
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.ServisRepo.Obrisi(r.Context(), id, &k.ID); err != nil {
|
if err := h.ServisRepo.Obrisi(r.Context(), id, &k.ID); err != nil {
|
||||||
slog.Error("greška pri brisanju naloga", "id", id, "error", err)
|
slog.Error("greška pri brisanju naloga", "id", id, "error", err)
|
||||||
http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError)
|
http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError)
|
||||||
@@ -526,6 +540,114 @@ func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/servis?obrisan=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/servis?obrisan=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// servisImaFinansijskeZapise proverava da li servisni nalog već ima fiskalni
|
||||||
|
// račun ili KIR/KPO upis — takav nalog se ne sme fizički obrisati.
|
||||||
|
func (h *Handler) servisImaFinansijskeZapise(ctx context.Context, id int64) (bool, error) {
|
||||||
|
if fr, _ := h.FiskalRepo.DohvatiPoServisu(ctx, id); fr != nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if postoji, err := h.PdvKirRepo.PostojiZaIzvor(ctx, "servis", id); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if postoji {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if postoji, err := h.KpoRepo.PostojiZaIzvor(ctx, "servis", id); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if postoji {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StornoNaloga stornira servisni nalog: vraća ugrađene delove na stanje, dograđuje
|
||||||
|
// storno stavke u KIR/KPO (original se ne briše — v. model.KirStorno/KpoStorno) i
|
||||||
|
// šalje fiskalni refund (best-effort). Koristi se umesto brisanja kad nalog već
|
||||||
|
// ima finansijske zapise.
|
||||||
|
func (h *Handler) StornoNaloga(w http.ResponseWriter, r *http.Request) {
|
||||||
|
k, ok := h.zahtevajDozvolu(w, r, "servis.storno")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := parseID(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
razlog := strings.TrimSpace(r.FormValue("razlog"))
|
||||||
|
poreskiBrojKupca := strings.TrimSpace(r.FormValue("poreski_broj_kupca"))
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
// refundacija po propisu zahteva identifikaciju kupca (PIB/JMBG)
|
||||||
|
var originalniFiskal *model.FiskalniRacun
|
||||||
|
if h.modulUkljucen(ctx, config.ModulFiskalizacija) {
|
||||||
|
if fr, _ := h.FiskalRepo.DohvatiPoServisu(ctx, id); fr != nil && !fr.Storniran {
|
||||||
|
originalniFiskal = fr
|
||||||
|
imaIdentKupca := false
|
||||||
|
if nalog, e := h.ServisRepo.DohvatiID(ctx, id); e == nil && nalog.KlijentID != nil {
|
||||||
|
if kk, e2 := h.KlijentiRepo.DohvatiID(ctx, *nalog.KlijentID); e2 == nil {
|
||||||
|
imaIdentKupca = kk.PIB != "" || kk.JMBG != ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !imaIdentKupca && poreskiBrojKupca == "" {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", errPotrebnaIdentKupca.Error())
|
||||||
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.ServisRepo.Storno(ctx, id, razlog, &k.ID); err != nil {
|
||||||
|
slog.Error("greška pri storniranju servisnog naloga", "id", id, "error", err)
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri storniranju. Možda je nalog već storniran.")
|
||||||
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k := middleware.KorisnikIzKonteksta(ctx); k != nil {
|
||||||
|
kid := k.ID
|
||||||
|
_ = h.ServisniLogRepo.Kreiraj(ctx, id, "storno", &kid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KIR — original se ne briše, dograđuje se storno stavka
|
||||||
|
if zapisi, e := h.PdvKirRepo.DohvatiPoIzvoru(ctx, "servis", id); e != nil {
|
||||||
|
slog.Error("čitanje vezanog KIR zapisa nije uspelo", "servis_id", id, "error", e)
|
||||||
|
} else {
|
||||||
|
for _, z := range zapisi {
|
||||||
|
storno := model.KirStorno(z, razlog, time.Now())
|
||||||
|
if _, e := h.PdvKirRepo.Kreiraj(ctx, &storno); e != nil {
|
||||||
|
slog.Error("upis storno KIR zapisa nije uspeo", "servis_id", id, "error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KPO — original se ne briše, dograđuje se storno stavka
|
||||||
|
if h.modulUkljucen(ctx, "kpo") {
|
||||||
|
if zapisi, e := h.KpoRepo.DohvatiPoIzvoru(ctx, "servis", id); e != nil {
|
||||||
|
slog.Error("čitanje vezanog KPO zapisa nije uspelo", "servis_id", id, "error", e)
|
||||||
|
} else {
|
||||||
|
for _, z := range zapisi {
|
||||||
|
storno := model.KpoStorno(z, razlog, time.Now())
|
||||||
|
if _, e := h.KpoRepo.Kreiraj(ctx, &storno); e != nil {
|
||||||
|
slog.Error("upis storno KPO zapisa nije uspeo", "servis_id", id, "error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fiskalni refund — best-effort
|
||||||
|
if originalniFiskal != nil {
|
||||||
|
if fk := h.fiskalKlijent(); fk != nil {
|
||||||
|
h.refundujServis(ctx, id, fk, originalniFiskal.ID, originalniFiskal.PfrBroj, poreskiBrojKupca)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
// DetaljiNaloga prikazuje sve podatke jednog servisnog naloga sa ugrađenim delovima
|
// DetaljiNaloga prikazuje sve podatke jednog servisnog naloga sa ugrađenim delovima
|
||||||
func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := parseID(chi.URLParam(r, "id"))
|
id, err := parseID(chi.URLParam(r, "id"))
|
||||||
@@ -601,10 +723,12 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
bezGarancije := nalog.GarancijaDana == nil || *nalog.GarancijaDana <= 0
|
bezGarancije := nalog.GarancijaDana == nil || *nalog.GarancijaDana <= 0
|
||||||
|
|
||||||
klijentNaziv := ""
|
klijentNaziv := ""
|
||||||
|
imaIdentKupca := false
|
||||||
if nalog.KlijentID != nil {
|
if nalog.KlijentID != nil {
|
||||||
klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
klijentNaziv = klijent.PunoIme()
|
klijentNaziv = klijent.PunoIme()
|
||||||
|
imaIdentKupca = klijent.PIB != "" || klijent.JMBG != ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,6 +886,9 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
podaci.FiskalniRacun == nil {
|
podaci.FiskalniRacun == nil {
|
||||||
podaci.FiskalGreska = true
|
podaci.FiskalGreska = true
|
||||||
}
|
}
|
||||||
|
// storno zahteva PIB/JMBG kupca za refundaciju ako klijent nema upisan poreski broj
|
||||||
|
podaci.PotrebnaIdentKupca = h.modulUkljucen(r.Context(), config.ModulFiskalizacija) &&
|
||||||
|
!nalog.Stornirano && podaci.FiskalniRacun != nil && !podaci.FiskalniRacun.Storniran && !imaIdentKupca
|
||||||
|
|
||||||
h.renderujTemplate(w, "servis_detalji", podaci)
|
h.renderujTemplate(w, "servis_detalji", podaci)
|
||||||
}
|
}
|
||||||
@@ -2286,32 +2413,7 @@ func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// napravi stavke za fiskalni račun
|
items := stavkeFiskalnogServisa(radovi, delovi)
|
||||||
items := make([]fiskal.InvoiceItem, 0)
|
|
||||||
for _, r := range radovi {
|
|
||||||
if r.Predlozeno {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
items = append(items, fiskal.InvoiceItem{
|
|
||||||
Name: r.Naziv,
|
|
||||||
Labels: []string{fiskal.OznakaPDV(r.PdvStopa)},
|
|
||||||
TotalAmount: fiskal.BrutoCena(r.Ukupno(), r.PdvStopa),
|
|
||||||
UnitPrice: fiskal.BrutoCena(r.CenaKomada, r.PdvStopa),
|
|
||||||
Quantity: r.Kolicina,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for _, d := range delovi {
|
|
||||||
if d.Predlozeno {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
items = append(items, fiskal.InvoiceItem{
|
|
||||||
Name: d.ArtikalNaziv,
|
|
||||||
Labels: []string{fiskal.OznakaPDV(d.PdvStopa)},
|
|
||||||
TotalAmount: fiskal.BrutoCena(d.Ukupno(), d.PdvStopa),
|
|
||||||
UnitPrice: fiskal.BrutoCena(d.CenaKomada, d.PdvStopa),
|
|
||||||
Quantity: float64(d.Kolicina),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
slog.Warn("fiskalizujServis: nema stavki za fiskalni račun, zahtev odbačen", "id", servisID)
|
slog.Warn("fiskalizujServis: nema stavki za fiskalni račun, zahtev odbačen", "id", servisID)
|
||||||
return
|
return
|
||||||
@@ -2373,6 +2475,123 @@ func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stavkeFiskalnogServisa gradi stavke fiskalnog računa od radova i ugrađenih
|
||||||
|
// delova servisnog naloga — koristi se i za Sale (fiskalizujServis) i za
|
||||||
|
// Refund (refundujServis), predloženi (neprihvaćeni) redovi se ne naplaćuju.
|
||||||
|
func stavkeFiskalnogServisa(radovi []model.ServisniRad, delovi []model.ServisniDeoSaArtiklom) []fiskal.InvoiceItem {
|
||||||
|
items := make([]fiskal.InvoiceItem, 0)
|
||||||
|
for _, r := range radovi {
|
||||||
|
if r.Predlozeno {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, fiskal.InvoiceItem{
|
||||||
|
Name: r.Naziv,
|
||||||
|
Labels: []string{fiskal.OznakaPDV(r.PdvStopa)},
|
||||||
|
TotalAmount: fiskal.BrutoCena(r.Ukupno(), r.PdvStopa),
|
||||||
|
UnitPrice: fiskal.BrutoCena(r.CenaKomada, r.PdvStopa),
|
||||||
|
Quantity: r.Kolicina,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, d := range delovi {
|
||||||
|
if d.Predlozeno {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, fiskal.InvoiceItem{
|
||||||
|
Name: d.ArtikalNaziv,
|
||||||
|
Labels: []string{fiskal.OznakaPDV(d.PdvStopa)},
|
||||||
|
TotalAmount: fiskal.BrutoCena(d.Ukupno(), d.PdvStopa),
|
||||||
|
UnitPrice: fiskal.BrutoCena(d.CenaKomada, d.PdvStopa),
|
||||||
|
Quantity: float64(d.Kolicina),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// refundujServis šalje fiskalni Refund zahtev za storniran servisni nalog —
|
||||||
|
// best-effort, greška se samo loguje (storno u bazi ostaje validan i bez uspešnog
|
||||||
|
// refunda; PraznineKnjigovodstva prati takve slučajeve preko stornoBezRefunda).
|
||||||
|
func (h *Handler) refundujServis(ctx context.Context, servisID int64, klijent *fiskal.Klijent, originalniFiskalID int64, referentBroj, poreskiBrojKupca string) {
|
||||||
|
nalog, err := h.ServisRepo.DohvatiID(ctx, servisID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("refundujServis: nije pronađen nalog", "id", servisID, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
radovi, err := h.ServisniRadoviRepo.DohvatiZaNalog(ctx, servisID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("refundujServis: greška pri dohvatanju radova", "id", servisID, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delovi, err := h.deloviSaPotrazivanima(ctx, servisID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("refundujServis: greška pri dohvatanju delova", "id", servisID, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := stavkeFiskalnogServisa(radovi, delovi)
|
||||||
|
if len(items) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pib, jmbg := "", ""
|
||||||
|
if nalog.KlijentID != nil {
|
||||||
|
if k, e := h.KlijentiRepo.DohvatiID(ctx, *nalog.KlijentID); e == nil {
|
||||||
|
pib, jmbg = k.PIB, k.JMBG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pib == "" && jmbg == "" {
|
||||||
|
pib, jmbg = klasifikujPoreskiBroj(poreskiBrojKupca)
|
||||||
|
}
|
||||||
|
|
||||||
|
zahtev := fiskal.InvoiceRequest{
|
||||||
|
InvoiceRequest: fiskal.InvoiceRequestBody{
|
||||||
|
InvoiceType: "Normal",
|
||||||
|
TransactionType: "Refund",
|
||||||
|
ReferentDocumentNumber: referentBroj,
|
||||||
|
Payment: []fiskal.PaymentItem{
|
||||||
|
{Amount: nalog.Naplaceno, PaymentType: fiskal.TipPlacanja(nalog.NacinPlacanja)},
|
||||||
|
},
|
||||||
|
Items: items,
|
||||||
|
Cashier: h.imeKasira(ctx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if pib != "" {
|
||||||
|
zahtev.InvoiceRequest.BuyerID = "10:" + pib
|
||||||
|
} else if jmbg != "" {
|
||||||
|
zahtev.InvoiceRequest.BuyerID = "01:" + jmbg
|
||||||
|
}
|
||||||
|
|
||||||
|
odgovor, errFisk := klijent.IzdajRacun(ctx, zahtev)
|
||||||
|
if errFisk != nil {
|
||||||
|
slog.Error("fiskalni refund servisa nije uspeo", "servis_id", servisID, "error", errFisk)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
poreskeJSON, _ := json.Marshal(odgovor.TaxItems)
|
||||||
|
siroviJSON, _ := json.Marshal(odgovor)
|
||||||
|
fr := &model.FiskalniRacun{
|
||||||
|
ServisID: servisID,
|
||||||
|
TipRacuna: "Normal",
|
||||||
|
TipTransakcije: "Refund",
|
||||||
|
PfrBroj: odgovor.InvoiceNumber,
|
||||||
|
PfrVreme: odgovor.SdcDateTime,
|
||||||
|
Brojac: odgovor.InvoiceCounter,
|
||||||
|
EkstenzijaBrojaca: odgovor.InvoiceCounterExtension,
|
||||||
|
UrlVerifikacija: odgovor.VerificationURL,
|
||||||
|
QRKod: odgovor.VerificationQRCode,
|
||||||
|
PoreskeStavke: string(poreskeJSON),
|
||||||
|
UkupnoZaNaplatu: odgovor.TotalAmount,
|
||||||
|
UkupanPorez: odgovor.TotalTax,
|
||||||
|
SiroviOdgovor: string(siroviJSON),
|
||||||
|
Potpisao: odgovor.SignedBy,
|
||||||
|
Zatrazio: odgovor.RequestedBy,
|
||||||
|
Poruka: odgovor.Messages,
|
||||||
|
}
|
||||||
|
if _, e := h.FiskalRepo.Kreiraj(ctx, fr); e != nil {
|
||||||
|
slog.Error("čuvanje fiskalnog refunda servisa nije uspelo", "servis_id", servisID, "error", e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = h.FiskalRepo.OznačiKaoStorniran(ctx, originalniFiskalID)
|
||||||
|
}
|
||||||
|
|
||||||
// RetryFiskalizacija pokušava ponovo da izda fiskalni račun za nalog čija je
|
// RetryFiskalizacija pokušava ponovo da izda fiskalni račun za nalog čija je
|
||||||
// prethodna fiskalizacija pala. Bezbedno za ponovni poziv — proverava da račun
|
// prethodna fiskalizacija pala. Bezbedno za ponovni poziv — proverava da račun
|
||||||
// još uvek ne postoji pre slanja zahteva ka ESIR/PFR.
|
// još uvek ne postoji pre slanja zahteva ka ESIR/PFR.
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ func (h *Handler) kpoKandidatiServisa(ctx context.Context) (naplaceni []model.Se
|
|||||||
}
|
}
|
||||||
naplaceni = make([]model.ServisniNalogSaKlijentom, 0, len(servisNalozi))
|
naplaceni = make([]model.ServisniNalogSaKlijentom, 0, len(servisNalozi))
|
||||||
for _, sn := range servisNalozi {
|
for _, sn := range servisNalozi {
|
||||||
if sn.Status == model.StatusPreuzeto && sn.Naplaceno != 0 {
|
if sn.Status == model.StatusPreuzeto && sn.Naplaceno != 0 && !sn.Stornirano {
|
||||||
naplaceni = append(naplaceni, sn)
|
naplaceni = append(naplaceni, sn)
|
||||||
} else {
|
} else {
|
||||||
nenaplaceniBroj++
|
nenaplaceniBroj++
|
||||||
@@ -211,17 +211,23 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// stornoBezRefunda broji stornirane prodajne naloge čiji je originalni fiskalni
|
// stornoBezRefunda broji stornirane prodajne i servisne naloge čiji je originalni
|
||||||
// račun i dalje neoznačen kao storniran — znači da je storno u bazi prošao, ali
|
// fiskalni račun i dalje neoznačen kao storniran — znači da je storno u bazi
|
||||||
// fiskalni refund ka ESIR/L-PFR uređaju NIJE uspeo (best-effort poziv je pao).
|
// prošao, ali fiskalni refund ka ESIR/L-PFR uređaju NIJE uspeo (best-effort poziv
|
||||||
// Servis nema storno funkciju — obuhvaćena je samo prodaja.
|
// je pao).
|
||||||
func (h *Handler) stornoBezRefunda(ctx context.Context) (int, error) {
|
func (h *Handler) stornoBezRefunda(ctx context.Context) (int, error) {
|
||||||
var n int
|
var n int
|
||||||
err := h.DB.QueryRowContext(ctx, `
|
err := h.DB.QueryRowContext(ctx, `
|
||||||
SELECT COUNT(*) FROM prodajni_nalozi pn
|
SELECT
|
||||||
JOIN fiskalni_racuni fr ON fr.prodaja_id = pn.id
|
(SELECT COUNT(*) FROM prodajni_nalozi pn
|
||||||
AND fr.tip_transakcije = 'Sale' AND fr.storniran = 0
|
JOIN fiskalni_racuni fr ON fr.prodaja_id = pn.id
|
||||||
WHERE pn.stornirano = 1
|
AND fr.tip_transakcije = 'Sale' AND fr.storniran = 0
|
||||||
|
WHERE pn.stornirano = 1)
|
||||||
|
+
|
||||||
|
(SELECT COUNT(*) FROM servisni_nalozi sn
|
||||||
|
JOIN fiskalni_racuni fr ON fr.servis_id = sn.id
|
||||||
|
AND fr.tip_transakcije = 'Sale' AND fr.storniran = 0
|
||||||
|
WHERE sn.stornirano = 1)
|
||||||
`).Scan(&n)
|
`).Scan(&n)
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ var sveAkcije = []string{
|
|||||||
"servis.dodaj",
|
"servis.dodaj",
|
||||||
"servis.izmeni",
|
"servis.izmeni",
|
||||||
"servis.obrisi",
|
"servis.obrisi",
|
||||||
|
"servis.storno",
|
||||||
"prodaja.pregled",
|
"prodaja.pregled",
|
||||||
"prodaja.dodaj",
|
"prodaja.dodaj",
|
||||||
"prodaja.storno",
|
"prodaja.storno",
|
||||||
@@ -77,7 +78,7 @@ func ImaDozvolu(uloga, akcija string) bool {
|
|||||||
return true
|
return true
|
||||||
// servis
|
// servis
|
||||||
case "servis.pregled", "servis.dodaj",
|
case "servis.pregled", "servis.dodaj",
|
||||||
"servis.izmeni", "servis.obrisi":
|
"servis.izmeni", "servis.obrisi", "servis.storno":
|
||||||
return true
|
return true
|
||||||
// prodaja
|
// prodaja
|
||||||
case "prodaja.pregled", "prodaja.dodaj", "prodaja.storno":
|
case "prodaja.pregled", "prodaja.dodaj", "prodaja.storno":
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ type ServisniNalog struct {
|
|||||||
JavniToken string
|
JavniToken string
|
||||||
NacinPlacanja string // način plaćanja pri preuzimanju (Gotovina, Kartica, Virman...)
|
NacinPlacanja string // način plaćanja pri preuzimanju (Gotovina, Kartica, Virman...)
|
||||||
Naplaceno float64 // iznos koji je naplaćen pri preuzimanju
|
Naplaceno float64 // iznos koji je naplaćen pri preuzimanju
|
||||||
|
Stornirano bool
|
||||||
|
RazlogStorniranja string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServisniLog je jedan zapis u istoriji događaja servisnog naloga.
|
// ServisniLog je jedan zapis u istoriji događaja servisnog naloga.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- Servisni nalog dobija storno (kao Prodaja i Nabavka) — do sada je jedini način
|
||||||
|
-- da se poništi naplaćen/preuzet nalog bio fizičko brisanje, koje ostavlja mrtve
|
||||||
|
-- KIR/KPO/fiskalne zapise iza sebe.
|
||||||
|
ALTER TABLE servisni_nalozi ADD COLUMN stornirano INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE servisni_nalozi ADD COLUMN razlog_storniranja TEXT;
|
||||||
@@ -45,6 +45,11 @@
|
|||||||
<span style="font-size:20px;font-weight:600;color:var(--tekst-glavni);font-family:monospace;">
|
<span style="font-size:20px;font-weight:600;color:var(--tekst-glavni);font-family:monospace;">
|
||||||
{{.Nalog.BrojNaloga}}
|
{{.Nalog.BrojNaloga}}
|
||||||
</span>
|
</span>
|
||||||
|
{{if .Nalog.Stornirano}}
|
||||||
|
<span style="display:inline-block;padding:3px 10px;border-radius:20px;font-size:12px;font-weight:500;background:rgba(220,38,38,0.12);color:#dc2626;">
|
||||||
|
Stornirano
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
{{if .Nalog.PopravkaOdbijena}}
|
{{if .Nalog.PopravkaOdbijena}}
|
||||||
<div style="padding:8px 14px;background:color-mix(in srgb, var(--crvena, #ef4444) 14%, transparent);border:1px solid color-mix(in srgb, var(--crvena, #ef4444) 35%, transparent);border-radius:8px;">
|
<div style="padding:8px 14px;background:color-mix(in srgb, var(--crvena, #ef4444) 14%, transparent);border:1px solid color-mix(in srgb, var(--crvena, #ef4444) 35%, transparent);border-radius:8px;">
|
||||||
<span style="font-size:15px;font-weight:700;color:var(--crvena, #ef4444);">✕ Klijent je odbio popravku</span>
|
<span style="font-size:15px;font-weight:700;color:var(--crvena, #ef4444);">✕ Klijent je odbio popravku</span>
|
||||||
@@ -1017,6 +1022,31 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
<!-- storno -->
|
||||||
|
{{if and (index .Dozvole "servis.storno") (eq .Nalog.Status "Preuzeto") (not .Nalog.Stornirano)}}
|
||||||
|
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
|
||||||
|
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||||
|
<div style="flex:1;min-width:200px;">
|
||||||
|
<div style="font-size:14px;font-weight:500;color:#f97316;margin-bottom:4px;">Storniranje naloga</div>
|
||||||
|
<div class="pomocni-tekst">
|
||||||
|
Storno vraća ugrađene delove na stanje u magacinu. Nalog ostaje u evidenciji sa oznakom storniran.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="/servis/storno/{{.Nalog.ID}}" style="display:flex;flex-direction:column;gap:8px;align-items:flex-end;">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
|
<input type="text" name="razlog" placeholder="Razlog storniranja (opciono)" style="width:240px;">
|
||||||
|
{{if .PotrebnaIdentKupca}}
|
||||||
|
<input type="text" name="poreski_broj_kupca" placeholder="PIB ili JMBG kupca (obavezno za refundaciju)" required style="width:240px;">
|
||||||
|
{{end}}
|
||||||
|
<button type="submit" class="btn-upozorenje"
|
||||||
|
data-potvrda="Stornirati nalog {{.Nalog.BrojNaloga}}?">
|
||||||
|
Storniraj nalog
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- zona za brisanje -->
|
<!-- zona za brisanje -->
|
||||||
{{/* Brisanje je moguće samo dok je „Primljeno" (greška pri unosu); čim uđe u proceduru naplaćuje se dijagnostika */}}
|
{{/* Brisanje je moguće samo dok je „Primljeno" (greška pri unosu); čim uđe u proceduru naplaćuje se dijagnostika */}}
|
||||||
{{if eq .Nalog.Status "Primljeno"}}
|
{{if eq .Nalog.Status "Primljeno"}}
|
||||||
|
|||||||
Reference in New Issue
Block a user