diff --git a/Fisk/server.py b/Fisk/server.py index 2f4f41e..5f75ac6 100755 --- a/Fisk/server.py +++ b/Fisk/server.py @@ -413,6 +413,14 @@ def _build_invoice_response(req, request_id): "messages": "Success", } + # povraćaj — ako je primljeno više nego što je duženo (npr. kupac dao krupniju + # novčanicu), razlika se ispisuje na računu; kod refundacije nema povraćaja + payments = inv_req.get("payment", [{"type": "Cash", "amount": total_amount}]) + total_paid = round(sum(float(p.get("amount", 0)) for p in payments), 2) + refund = round(total_paid - total_amount, 2) if transaction_type != "Refund" else 0 + if refund < 0: + refund = 0 + # Puni podaci za snimanje i generisanje računa full_data = { **odgovor, @@ -420,7 +428,7 @@ def _build_invoice_response(req, request_id): "invoiceType": invoice_type, "transactionType": transaction_type, "items": items, - "payments": inv_req.get("payment", [{"type": "Cash", "amount": total_amount}]), + "payments": payments, "cashier": inv_req.get("cashier", "Kasir"), "buyerId": inv_req.get("buyerId", ""), "referentDocumentNumber": inv_req.get("referentDocumentNumber", ""), @@ -430,7 +438,7 @@ def _build_invoice_response(req, request_id): "store": firma["locationName"], "address": firma["address"], "district": firma["district"], - "refund": 0, + "refund": refund, # za avansni konačni "advancePaid": req.get("advancePaid", 0), "advanceTax": req.get("advanceTax", 0), diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index d0d7825..5cb538e 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -420,6 +420,7 @@ func main() { r.With(doz("prodaja.storno")).Post("/prodaja/storno/{id}", h.StornoProdaje) r.With(doz("prodaja.dodaj")).Post("/prodaja/{id}/fiskalizuj", h.RetryFiskalizacijaProdaje) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}/stampa", h.StampaProdaje) + r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}/fiskalni-racun", h.StampaFiskalnogProdaje) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}", h.DetaljiProdaje) // podsetnici diff --git a/internal/fiskal/mapiranje.go b/internal/fiskal/mapiranje.go index f74fa0e..f639666 100644 --- a/internal/fiskal/mapiranje.go +++ b/internal/fiskal/mapiranje.go @@ -51,11 +51,14 @@ func TipPlacanja(nacin string) string { // NapraviZahtev konvertuje prodajni nalog i stavke u Teron InvoiceRequest. // VAŽNO: sve cene se konvertuju iz neto u bruto pre slanja (BrutoCena). -// pib/jmbg se koriste za buyerId identifikaciju kupca. +// pib/jmbg se koriste za buyerId identifikaciju kupca. primljeno je iznos koji je +// kupac stvarno predao (za obračun povraćaja na fiskalnom računu) — ako je 0 ili +// manji od ukupnog iznosa, šalje se tačan iznos duga (bez povraćaja). func NapraviZahtev( nalog model.ProdajniNalog, stavke []model.StavkaProdajeSaArtiklom, pib, jmbg, kasir string, + primljeno float64, ) InvoiceRequest { items := make([]InvoiceItem, 0, len(stavke)) ukupanIznos := 0.0 @@ -90,6 +93,13 @@ func NapraviZahtev( // Ukupno za plaćanje je uvek suma bruto stavki (neto iz naloga bi bilo pogrešno) ukupnoPlacanje := ukupanIznos + // ako je kupac dao više od duga, na računu se iskazuje stvarno primljen iznos + // (fiskalni uređaj sam izračunava povraćaj kao razliku) + iznosPlacanja := ukupnoPlacanje + if primljeno > ukupnoPlacanje { + iznosPlacanja = primljeno + } + nacinPlacanja := nalog.NacinPlacanja if nacinPlacanja == "" { nacinPlacanja = "Gotovina" @@ -101,7 +111,7 @@ func NapraviZahtev( TransactionType: "Sale", Payment: []PaymentItem{ { - Amount: ukupnoPlacanje, + Amount: iznosPlacanja, PaymentType: TipPlacanja(nacinPlacanja), }, }, @@ -127,7 +137,7 @@ func NapraviRefundZahtev( stavke []model.StavkaProdajeSaArtiklom, pib, jmbg, kasir, referentBroj string, ) InvoiceRequest { - zahtev := NapraviZahtev(nalog, stavke, pib, jmbg, kasir) + zahtev := NapraviZahtev(nalog, stavke, pib, jmbg, kasir, 0) zahtev.InvoiceRequest.TransactionType = "Refund" zahtev.InvoiceRequest.ReferentDocumentNumber = referentBroj return zahtev diff --git a/internal/handler/prodaja.go b/internal/handler/prodaja.go index 37dd18b..e72f557 100644 --- a/internal/handler/prodaja.go +++ b/internal/handler/prodaja.go @@ -325,9 +325,14 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) { } // Fiskalizacija — ako je modul uključen (best-effort: prodaja ostaje validna i bez fiskalizacije) + racunKreiran := false if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) { if klijent := h.fiskalKlijent(); klijent != nil { - h.fiskalizujProdaju(r.Context(), id, klijent) + primljeno, _ := strconv.ParseFloat(strings.TrimSpace(r.FormValue("primljeno")), 64) + h.fiskalizujProdaju(r.Context(), id, klijent, primljeno) + if fr, _ := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id); fr != nil { + racunKreiran = true + } } } @@ -347,7 +352,11 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) { } } - http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther) + redirectURL := "/prodaja/" + strconv.FormatInt(id, 10) + "?sacuvano=1" + if r.FormValue("prikazi_racun") == "1" && racunKreiran { + redirectURL += "&racun=1" + } + http.Redirect(w, r, redirectURL, http.StatusSeeOther) } // DetaljiProdaje prikazuje pregled jednog prodajnog naloga sa svim stavkama @@ -470,6 +479,60 @@ func (h *Handler) StampaProdaje(w http.ResponseWriter, r *http.Request) { h.renderujStandalone(w, "prodaja_stampa", podaci) } +// StampaFiskalnogProdaje renderuje journal izdatog fiskalnog računa za prodaju (za štampu/uvid) +func (h *Handler) StampaFiskalnogProdaje(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID", http.StatusBadRequest) + return + } + fr, err := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id) + if err != nil || fr == nil { + http.Error(w, "Fiskalni račun nije pronađen", http.StatusNotFound) + return + } + nalog, _ := h.ProdajaRepo.DohvatiID(r.Context(), id) + journal := "" + if fr.SiroviOdgovor != "" { + var raw map[string]any + if json.Unmarshal([]byte(fr.SiroviOdgovor), &raw) == nil { + if j, ok := raw["journal"].(string); ok { + journal = j + } + } + } + if nalog != nil { + journal += "\n\n--- NTech prodaja: " + nalog.BrojNaloga + " ---\n" + } + + // Podeli journal na deo pre i posle QR placeholder-a + const qrPlaceholder = "{{{{QR-KOD}}}}" + pre, post, hasQR := strings.Cut(journal, qrPlaceholder) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, `
`) + fmt.Fprint(w, pre) + fmt.Fprint(w, ``) + + if hasQR && fr.QRKod != "" { + fmt.Fprintf(w, `
`) + fmt.Fprint(w, post) + fmt.Fprint(w, ``) + + fmt.Fprint(w, ``) +} + // ObrisiProdaju prima POST zahtev i stornira nalog umesto fizičkog brisanja. // Jednom izdat račun ne sme da nestane — umesto brisanja koristi se storno. func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) { @@ -733,7 +796,9 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string, // fiskalizujProdaju šalje fiskalni zahtev za prodajni nalog. Best-effort: greške // se loguju, ne zaustavljaju tok pozivaoca (kreiranje prodaje ili ručni retry). -func (h *Handler) fiskalizujProdaju(ctx context.Context, prodajaID int64, klijent *fiskal.Klijent) { +// primljeno je iznos koji je kupac predao (za tačan povraćaj na računu) — 0 ako +// nije poznat (npr. ručni retry), tada se šalje tačan dug bez povraćaja. +func (h *Handler) fiskalizujProdaju(ctx context.Context, prodajaID int64, klijent *fiskal.Klijent, primljeno float64) { nalog, err := h.ProdajaRepo.DohvatiID(ctx, prodajaID) if err != nil { slog.Error("fiskalizujProdaju: nije pronađen nalog", "prodaja_id", prodajaID, "error", err) @@ -757,7 +822,7 @@ func (h *Handler) fiskalizujProdaju(ctx context.Context, prodajaID int64, klijen } } - zahtev := fiskal.NapraviZahtev(*nalog, stavke, pib, jmbg, kasir) + zahtev := fiskal.NapraviZahtev(*nalog, stavke, pib, jmbg, kasir, primljeno) odgovor, errFisk := klijent.IzdajRacun(ctx, zahtev) if errFisk != nil { @@ -822,7 +887,7 @@ func (h *Handler) RetryFiskalizacijaProdaje(w http.ResponseWriter, r *http.Reque return } - h.fiskalizujProdaju(r.Context(), id, klijent) + h.fiskalizujProdaju(r.Context(), id, klijent, 0) if fr, _ := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id); fr != nil { middleware.SetFlash(w, r, h.DB, "uspeh", "Fiskalni račun izdat.") diff --git a/web/static/js/ntech.js b/web/static/js/ntech.js index c92471f..3eade36 100644 --- a/web/static/js/ntech.js +++ b/web/static/js/ntech.js @@ -138,6 +138,10 @@ document.addEventListener('alpine:init', () => { Alpine.data('prodajaForma', () => ({ stavke: [{artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: 20, popust: 0, kategorija_naziv: '', _naziv: '', _pretraga: '', _prikaziListu: false, _limit: 30}], zbirnoHover: false, + nacinPlacanja: 'gotovina', + primljenoIznos: '', + prikaziRacun: true, + _fiskalniTab: null, artikliOpcije: [], pretragaArtikal: '', pdvObveznik: true, @@ -357,6 +361,24 @@ document.addEventListener('alpine:init', () => { this.stavke = this.stavke.filter(s => s.artikal_id) this.$nextTick(() => e.target.submit()) }, + otvoriModalNaplate() { + this.primljenoIznos = '' + this.$refs.dijalogNaplate.showModal() + }, + kusurIznos() { + return (parseFloat(this.primljenoIznos) || 0) - parseFloat(this.ukupnoSvega()) + }, + naplataValidna() { + return this.primljenoIznos !== '' && (parseFloat(this.primljenoIznos) || 0) >= parseFloat(this.ukupnoSvega()) + }, + // pripremiFiskalniTab otvara prazan tab SINHRONO unutar klika (očuvan user gesture), + // da bi kasnije, posle redirekta na stranicu detalja prodaje, mogao da se imenom + // popuni URL-om fiskalnog računa bez blokiranja popup-blokera + pripremiFiskalniTab() { + if (this.prikaziRacun) { + this._fiskalniTab = window.open('', 'ntech-fiskalni-tab') + } + }, ukupnoStavke(s) { const cena = parseFloat(s.cena) || 0 const popust = parseFloat(s.popust) || 0 diff --git a/web/templates/stranice/prodaja_detalji.html b/web/templates/stranice/prodaja_detalji.html index 66a5d24..80c2901 100644 --- a/web/templates/stranice/prodaja_detalji.html +++ b/web/templates/stranice/prodaja_detalji.html @@ -287,6 +287,15 @@ {{end}} + {{end}} {{if .FiskalGreska}} diff --git a/web/templates/stranice/prodaja_forma.html b/web/templates/stranice/prodaja_forma.html index 63bff77..055ac45 100644 --- a/web/templates/stranice/prodaja_forma.html +++ b/web/templates/stranice/prodaja_forma.html @@ -46,54 +46,44 @@ Nova prodaja