Fiskalizacija: refund pri stornu, dnevni pazar KIR, popust na stavci
This commit is contained in:
@@ -29,7 +29,7 @@ var saSidebar = []string{
|
||||
"nabavke", "nabavka_forma", "nabavka_detalji",
|
||||
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem", "podesavanja_servis", "podesavanja_fiskalizacija",
|
||||
"pdv_stope",
|
||||
"pdv_kir", "pdv_kir_forma",
|
||||
"pdv_kir", "pdv_kir_forma", "pdv_kir_dnevni_pazar",
|
||||
"pdv_kpr", "pdv_kpr_forma",
|
||||
"pdv_obracun",
|
||||
"nivelacije",
|
||||
@@ -125,6 +125,8 @@ var sablonskeFunkcije = template.FuncMap{
|
||||
}
|
||||
return m, nil
|
||||
},
|
||||
// zbirF64 vraća zbir dva float64 — za aritmetiku u šablonima
|
||||
"zbirF64": func(a, b float64) float64 { return a + b },
|
||||
}
|
||||
|
||||
// KreirajKes parsuje sve šablone iz fsys i vraća ih keširane u mapi
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"math"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
@@ -245,3 +247,87 @@ func (h *Handler) KirBackfillProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", fmt.Sprintf("Backfill završen: %d novih KIR zapisa, %d preskočeno.", kreirano, preskoceno))
|
||||
http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// PodaciDnevniPazar su podaci za formu zbirnog KIR unosa (maloprodaja)
|
||||
type PodaciDnevniPazar struct {
|
||||
model.PodaciStranice
|
||||
Datum string
|
||||
Promet model.DnevniPrometKir
|
||||
Ukupno float64
|
||||
}
|
||||
|
||||
// DnevniPazarKir prikazuje formu za potvrdu zbirnog KIR unosa za izabrani datum.
|
||||
// Iznosi se računaju iz maloprodajnih naloga (bez klijenta) tog dana.
|
||||
func (h *Handler) DnevniPazarKir(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.dodaj"); !ok {
|
||||
return
|
||||
}
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
datum := strings.TrimSpace(r.URL.Query().Get("datum"))
|
||||
if datum == "" {
|
||||
datum = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
promet, err := h.ProdajaRepo.DnevniPrometMaloprodaje(r.Context(), datum)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri računanju dnevnog prometa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ukupno := math.Round((promet.OsnovicaOpsta+promet.PdvOpsta+promet.OsnovicaPosebna+promet.PdvPosebna)*100) / 100
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "pdv-kir"
|
||||
ps.NaslovStranice = "Dnevni pazar — KIR unos"
|
||||
h.renderujTemplate(w, "pdv_kir_dnevni_pazar", PodaciDnevniPazar{
|
||||
PodaciStranice: ps,
|
||||
Datum: datum,
|
||||
Promet: promet,
|
||||
Ukupno: ukupno,
|
||||
})
|
||||
}
|
||||
|
||||
// SacuvajDnevniPazarKir prima POST i upisuje zbirni KIR zapis za dnevni pazar.
|
||||
func (h *Handler) SacuvajDnevniPazarKir(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.dodaj"); !ok {
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
datum, e1 := time.Parse("2006-01-02", strings.TrimSpace(r.FormValue("datum")))
|
||||
if e1 != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Datum je obavezan.")
|
||||
http.Redirect(w, r, "/pdv/kir/dnevni-pazar", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
z := model.PdvKir{
|
||||
DatumPrometa: datum,
|
||||
DatumKnjizenja: datum,
|
||||
BrojDokumenta: fmt.Sprintf("FISK-%s", datum.Format("20060102")),
|
||||
KupacNaziv: "Promet fizičkim licima",
|
||||
OsnovicaOpsta: parsiraIznos(r.FormValue("osnovica_opsta")),
|
||||
PdvOpsta: parsiraIznos(r.FormValue("pdv_opsta")),
|
||||
OsnovicaPosebna: parsiraIznos(r.FormValue("osnovica_posebna")),
|
||||
PdvPosebna: parsiraIznos(r.FormValue("pdv_posebna")),
|
||||
Napomena: "Zbirni promet fizičkim licima — fiskalna kasa",
|
||||
Izvor: "rucno",
|
||||
}
|
||||
z.Ukupno = math.Round((z.OsnovicaOpsta+z.PdvOpsta+z.OsnovicaPosebna+z.PdvPosebna)*100) / 100
|
||||
|
||||
if _, err := h.PdvKirRepo.Kreiraj(r.Context(), &z); err != nil {
|
||||
slog.Error("greška pri čuvanju dnevnog pazara u KIR", "datum", datum, "error", err)
|
||||
http.Error(w, "Greška pri čuvanju zapisa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", fmt.Sprintf("Dnevni pazar za %s upisan u KIR.", datum.Format("02.01.2006.")))
|
||||
http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -532,6 +532,59 @@ func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.PdvKirRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); err != nil {
|
||||
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
|
||||
}
|
||||
|
||||
// fiskalni refund — best-effort (storno ostaje validan i bez uspešnog refunda)
|
||||
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
if fr, _ := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id); fr != nil && !fr.Storniran {
|
||||
if fk := h.fiskalKlijent(); fk != nil {
|
||||
if nalogFisk, e := h.ProdajaRepo.DohvatiID(r.Context(), id); e == nil {
|
||||
if stavkeFisk, e2 := h.ProdajaRepo.DohvatiStavke(r.Context(), id); e2 == nil {
|
||||
kasirFisk, _ := sqlite.DohvatiPodesavanje(r.Context(), h.DB, "pfr_kasir")
|
||||
if kasirFisk == "" {
|
||||
kasirFisk = "NTech"
|
||||
}
|
||||
var pibFisk, jmbgFisk string
|
||||
if nalogFisk.KlijentID != nil {
|
||||
if kk, e3 := h.KlijentiRepo.DohvatiID(r.Context(), *nalogFisk.KlijentID); e3 == nil {
|
||||
pibFisk, jmbgFisk = kk.PIB, kk.JMBG
|
||||
}
|
||||
}
|
||||
zahtevFisk := fiskal.NapraviRefundZahtev(*nalogFisk, stavkeFisk, pibFisk, jmbgFisk, kasirFisk, fr.PfrBroj)
|
||||
if odgFisk, errFisk := fk.IzdajRacun(r.Context(), zahtevFisk); errFisk != nil {
|
||||
slog.Error("fiskalni refund nije uspeo", "prodaja_id", id, "error", errFisk)
|
||||
} else {
|
||||
poreskeJSON, _ := json.Marshal(odgFisk.TaxItems)
|
||||
siroviJSON, _ := json.Marshal(odgFisk)
|
||||
refundFr := &model.FiskalniRacun{
|
||||
ProdajaID: id,
|
||||
TipRacuna: "Normal",
|
||||
TipTransakcije: "Refund",
|
||||
PfrBroj: odgFisk.InvoiceNumber,
|
||||
PfrVreme: odgFisk.SdcDateTime,
|
||||
Brojac: odgFisk.InvoiceCounter,
|
||||
EkstenzijaBrojaca: odgFisk.InvoiceCounterExtension,
|
||||
UrlVerifikacija: odgFisk.VerificationURL,
|
||||
QRKod: odgFisk.VerificationQRCode,
|
||||
PoreskeStavke: string(poreskeJSON),
|
||||
UkupnoZaNaplatu: odgFisk.TotalAmount,
|
||||
UkupanPorez: odgFisk.TotalTax,
|
||||
SiroviOdgovor: string(siroviJSON),
|
||||
Potpisao: odgFisk.SignedBy,
|
||||
Zatrazio: odgFisk.RequestedBy,
|
||||
Poruka: odgFisk.Messages,
|
||||
}
|
||||
if _, errFisk = h.FiskalRepo.Kreiraj(r.Context(), refundFr); errFisk != nil {
|
||||
slog.Error("čuvanje fiskalnog refunda nije uspelo", "prodaja_id", id, "error", errFisk)
|
||||
} else {
|
||||
_ = h.FiskalRepo.OznačiKaoStorniran(r.Context(), fr.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user