Bugfix: PDV obračun u prodaji, magacinski trag nabavke, fiskalizacija servisa, admin/podesavanja greške

This commit is contained in:
2026-06-28 16:02:26 +02:00
parent 99ae077082
commit f1acee98e8
9 changed files with 136 additions and 42 deletions
+4 -1
View File
@@ -2,6 +2,7 @@ package handler
import (
"html/template"
"log/slog"
"net/http"
"ntech/internal/auth"
@@ -549,7 +550,9 @@ func (h *Handler) AdminTotpDeaktivacija(w http.ResponseWriter, r *http.Request)
return
}
// isključenjem 2FA brišemo i rezervne kodove
_ = h.RezervniKodoviRepo.Obrisi(r.Context(), k.ID)
if err := h.RezervniKodoviRepo.Obrisi(r.Context(), k.ID); err != nil {
slog.Warn("brisanje rezervnih kodova nije uspelo", "korisnik_id", k.ID, "error", err)
}
http.Redirect(w, r, "/admin/profil?sacuvano=1", http.StatusSeeOther)
}
+4 -3
View File
@@ -176,7 +176,7 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
return
}
id, err := h.NabavkeRepo.Kreiraj(r.Context(), &nabavka, stavke, troskovi)
id, err := h.NabavkeRepo.Kreiraj(r.Context(), &nabavka, stavke, troskovi, &k.ID)
if err != nil {
http.Error(w, "Greška pri čuvanju nabavke", http.StatusInternalServerError)
return
@@ -340,7 +340,8 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
// ObrisiNabavku prima POST zahtev i briše nabavku po ID-u
func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "nabavka.obrisi"); !ok {
k, ok := h.zahtevajDozvolu(w, r, "nabavka.obrisi")
if !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
@@ -349,7 +350,7 @@ func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.NabavkeRepo.Obrisi(r.Context(), id); err != nil {
if err := h.NabavkeRepo.Obrisi(r.Context(), id, &k.ID); err != nil {
http.Error(w, "Greška pri brisanju nabavke", http.StatusInternalServerError)
return
}
+14 -4
View File
@@ -944,20 +944,26 @@ func (h *Handler) TestFiskalizacije(w http.ResponseWriter, r *http.Request) {
}
// SSRF zaštita: dozvoljeni su samo localhost i privatne mreže (192.168.x.x,
// 10.x.x.x, 172.16-31.x.x, 127.x.x.x). Port izvlačimo kao integer da presečemo
// CodeQL taint lanac — broj ne može da sadrži URL manipulaciju.
// 10.x.x.x, 172.16-31.x.x, 127.x.x.x). URL rekonstruišemo kroz url.URL{}
// iz validiranih komponenti da CodeQL prepozna sanitizaciju.
parsedURL, err := url.Parse(pfrURL)
if err != nil || !jePrivatnaAdresa(parsedURL.Hostname()) {
http.Error(w, "Nevažeći PFR URL — dozvoljeni su samo lokalni/privatni hostovi (127.x, 192.168.x, 10.x, 172.16-31.x)", http.StatusBadRequest)
return
}
host := parsedURL.Hostname()
port := 4566
if p := parsedURL.Port(); p != "" {
if n, e := strconv.Atoi(p); e == nil && n > 0 && n <= 65535 {
port = n
}
}
statusURL := fmt.Sprintf("http://%s:%d/api/status", parsedURL.Hostname(), port)
safeURL := url.URL{
Scheme: "http",
Host: net.JoinHostPort(host, strconv.Itoa(port)),
Path: "/api/status",
}
statusURL := safeURL.String()
klijent := &http.Client{Timeout: 5 * time.Second}
resp, err := klijent.Get(statusURL)
@@ -969,7 +975,11 @@ func (h *Handler) TestFiskalizacije(w http.ResponseWriter, r *http.Request) {
defer resp.Body.Close()
var status map[string]any
_ = json.NewDecoder(resp.Body).Decode(&status)
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<div class="fisk-status greska">&#10007; Neispravan odgovor servera — %s</div>`, html.EscapeString(err.Error()))
return
}
sdcDateTime, _ := status["sdcDateTime"].(string)
lastInvoice, _ := status["lastInvoiceNumber"].(string)
+24 -11
View File
@@ -2076,7 +2076,10 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
}
}
}
_ = h.ServisRepo.SacuvajNaplatu(r.Context(), id, nacin, iznos)
if err := h.ServisRepo.SacuvajNaplatu(r.Context(), id, nacin, iznos); err != nil {
slog.Error("greška pri čuvanju naplate servisa", "id", id, "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Naplata nije sačuvana. Pokušajte ponovo.")
}
// fiskalizacija servisa — ako je modul uključen
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
@@ -2145,8 +2148,16 @@ func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent
return
}
radovi, _ := h.ServisniRadoviRepo.DohvatiZaNalog(ctx, servisID)
delovi, _ := h.deloviSaPotrazivanima(ctx, servisID)
radovi, err := h.ServisniRadoviRepo.DohvatiZaNalog(ctx, servisID)
if err != nil {
slog.Error("fiskalizujServis: greška pri dohvatanju radova", "id", servisID, "error", err)
return
}
delovi, err := h.deloviSaPotrazivanima(ctx, servisID)
if err != nil {
slog.Error("fiskalizujServis: greška pri dohvatanju delova", "id", servisID, "error", err)
return
}
// napravi stavke za fiskalni račun
items := make([]fiskal.InvoiceItem, 0)
@@ -2154,12 +2165,11 @@ func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent
if r.Predlozeno {
continue
}
stopa := 20.0 // podrazumevano — servisne usluge su 20% PDV
items = append(items, fiskal.InvoiceItem{
Name: r.Naziv,
Labels: []string{fiskal.OznakaPDV(stopa)},
TotalAmount: fiskal.BrutoCena(r.Ukupno(), stopa),
UnitPrice: fiskal.BrutoCena(r.CenaKomada, stopa),
Labels: []string{fiskal.OznakaPDV(r.PdvStopa)},
TotalAmount: fiskal.BrutoCena(r.Ukupno(), r.PdvStopa),
UnitPrice: fiskal.BrutoCena(r.CenaKomada, r.PdvStopa),
Quantity: r.Kolicina,
})
}
@@ -2167,15 +2177,18 @@ func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent
if d.Predlozeno {
continue
}
stopa := 20.0
items = append(items, fiskal.InvoiceItem{
Name: d.ArtikalNaziv,
Labels: []string{fiskal.OznakaPDV(stopa)},
TotalAmount: fiskal.BrutoCena(d.Ukupno(), stopa),
UnitPrice: fiskal.BrutoCena(d.CenaKomada, stopa),
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 {
slog.Warn("fiskalizujServis: nema stavki za fiskalni račun, zahtev odbačen", "id", servisID)
return
}
pib := ""
if nalog.KlijentID != nil {