Files
GoNtech/internal/model/servis.go
T
Dasko f74cb04024 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.
2026-07-02 11:30:55 +02:00

236 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package model
import (
"fmt"
"time"
)
// Statusi servisnog naloga
const (
StatusPrimljeno = "Primljeno"
StatusUDijagnostici = "U dijagnostici"
StatusCekaDelove = "Čeka delove"
StatusUPopravci = "U popravci"
StatusZavrseno = "Završeno"
StatusPreuzeto = "Preuzeto"
)
// SviStatusi je uređena lista statusa za prikaz u dropdownu
var SviStatusi = []string{
StatusPrimljeno,
StatusUDijagnostici,
StatusCekaDelove,
StatusUPopravci,
StatusZavrseno,
StatusPreuzeto,
}
// ServisniNalog predstavlja jedan servisni nalog
type ServisniNalog struct {
ID int64
KlijentID *int64
TehnicarID *int64
BrojNaloga string
Uredjaj string
SerijskiBroj string
OpisKvara string
TrazeneNadogradnje string
Status string
CenaOd *float64
CenaDo *float64
CenaKonacna *float64
Avans *float64
Napomena string
GarancijaDo *time.Time
GarancijaDana *int // trajanje garancije u danima (od završetka); nil/0 = bez garancije
DatumPrijema time.Time
PredvidjenDatum *time.Time
DatumZavrsetka *time.Time
Ostecenja string
PinUredjaja string
Pribor string
NapomenaKlijentu string
NalazDijagnostike string // šta je serviser utvrdio pregledom (dijagnoza + predlog popravke)
Uradjeno string // šta je serviser stvarno uradio tokom popravke
CenaDijagnostike float64 // taksa kad klijent ne prihvati popravku; 0 = ne naplaćuje se
PopravkaOdbijena bool // klijent odbio popravku posle dijagnostike → naplaćuje se samo dijagnostika
KomentarKlijenta string // poruka klijenta uz prihvatanje/odbijanje predloga
OdlukaKlijenta string // '' / 'prihvaceno' / 'odbijeno'
DatumOdluke *time.Time // kada je klijent doneo odluku
JavniToken string
NacinPlacanja string // način plaćanja pri preuzimanju (Gotovina, Kartica, Virman...)
Naplaceno float64 // iznos koji je naplaćen pri preuzimanju
Stornirano bool
RazlogStorniranja string
}
// ServisniLog je jedan zapis u istoriji događaja servisnog naloga.
type ServisniLog struct {
ID int64
NalogID int64
Dogadjaj string // npr. "status:U dijagnostici", "odluka:prihvaceno"
Napomena string
KorisnikID *int64
Datum time.Time
}
// ServisniDeo predstavlja jedan artikal ugrađen u servisni nalog
type ServisniDeo struct {
ID int64
NalogID int64
ArtikalID int64
Kolicina int
CenaKomada float64
Datum time.Time
Predlozeno bool // true = serviser predložio posle dijagnostike; čeka odobrenje klijenta
}
// Ukupno vraća ukupnu vrednost dela (kolicina × cena)
func (d ServisniDeo) Ukupno() float64 {
return float64(d.Kolicina) * d.CenaKomada
}
// UkupnoSaPdv vraća ukupnu vrednost dela sa PDV-om (kolicina × cena_sa_pdv)
func (d ServisniDeoSaArtiklom) UkupnoSaPdv() float64 {
return float64(d.Kolicina) * d.CenaSaPdv
}
// ServisniDeoSaArtiklom je servisni deo sa nazivom artikla — za prikaz
type ServisniDeoSaArtiklom struct {
ServisniDeo
ArtikalNaziv string
ArtikalSifra string
PdvStopa float64
CenaSaPdv float64
Potrazivano int // komada koji nedostaju (iz servisni_potrazivani_delovi), 0 ako nema
}
// 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
CenaKomada float64
Datum time.Time
Predlozeno bool
// za prikaz:
ArtikalNaziv string
}
// ServisniRad predstavlja jednu stavku rada (uslugu) na servisnom nalogu.
// Naziv i cena su snapshot iz cenovnika usluga; cena se može menjati po nalogu.
type ServisniRad struct {
ID int64
NalogID int64
UslugaID int64 // 0 ako je usluga u međuvremenu obrisana iz cenovnika
Naziv string
Sifra string
Kolicina float64
CenaKomada float64
PdvStopa float64
CenaSaPdv float64 // CenaKomada * (1 + PdvStopa/100)
Datum string
Predlozeno bool // true = serviser predložio posle dijagnostike; čeka odobrenje klijenta
}
// Ukupno vraća ukupnu vrednost rada (kolicina × cena)
func (r ServisniRad) Ukupno() float64 {
return r.Kolicina * r.CenaKomada
}
// UkupnoSaPdv vraća ukupnu vrednost rada sa PDV-om (kolicina × cena_sa_pdv)
func (r ServisniRad) UkupnoSaPdv() float64 {
return r.Kolicina * r.CenaSaPdv
}
// ServisniNalogSaKlijentom proširuje ServisniNalog sa nazivom klijenta za prikaz u listi
type ServisniNalogSaKlijentom struct {
ServisniNalog
KlijentNaziv string
ImaPredlog bool // ima predloženih stavki koje čekaju odobrenje klijenta
}
// KlijentIDVrednost vraća vrednost KlijentID pointera, ili 0 ako je nil
func (n ServisniNalog) KlijentIDVrednost() int64 {
if n.KlijentID == nil {
return 0
}
return *n.KlijentID
}
// TehnicarIDVrednost vraća vrednost TehnicarID pointera, ili 0 ako je nil
func (n ServisniNalog) TehnicarIDVrednost() int64 {
if n.TehnicarID == nil {
return 0
}
return *n.TehnicarID
}
// CenaOdStr vraća formatiranu procenu od, ili prazan string ako nije uneta
func (n ServisniNalog) CenaOdStr() string {
if n.CenaOd == nil {
return ""
}
return fmt.Sprintf("%.2f", *n.CenaOd)
}
// CenaDoStr vraća formatiranu procenu do, ili prazan string ako nije uneta
func (n ServisniNalog) CenaDoStr() string {
if n.CenaDo == nil {
return ""
}
return fmt.Sprintf("%.2f", *n.CenaDo)
}
// CenaKonacnaStr vraća formatiranu konačnu cenu, ili prazan string ako nije uneta
func (n ServisniNalog) CenaKonacnaStr() string {
if n.CenaKonacna == nil {
return ""
}
return fmt.Sprintf("%.2f", *n.CenaKonacna)
}
// AvansStr vraća formatirani avans, ili prazan string ako nije unet
func (n ServisniNalog) AvansStr() string {
if n.Avans == nil {
return ""
}
return fmt.Sprintf("%.2f", *n.Avans)
}
// CenaDijagnostikeStr vraća cenu dijagnostike za <input>, ili prazan string kad je 0
func (n ServisniNalog) CenaDijagnostikeStr() string {
if n.CenaDijagnostike == 0 {
return ""
}
return fmt.Sprintf("%.2f", n.CenaDijagnostike)
}
// PreostaloZaNaplatu vraća razliku konacna_cena avans, minimum 0.
// Vraća nil ako konačna cena nije uneta.
func (n ServisniNalog) PreostaloZaNaplatu() *float64 {
if n.CenaKonacna == nil {
return nil
}
avans := 0.0
if n.Avans != nil {
avans = *n.Avans
}
v := *n.CenaKonacna - avans
if v < 0 {
v = 0
}
return &v
}
// PreostaloZaNaplatuStr vraća formatirano preostalo za naplatu, ili prazan string
func (n ServisniNalog) PreostaloZaNaplatuStr() string {
v := n.PreostaloZaNaplatu()
if v == nil {
return ""
}
return fmt.Sprintf("%.2f", *v)
}