Servis: avans se fiskalizuje kao Advance/Sale, konačan račun ga zatvara

Avans se do sada nikad nije fiskalizovao — bio je samo interno polje. Zbog
toga je konačni fiskalni račun (pri preuzimanju) uvek prikazivao povraćaj
protiv PUNOG iznosa stavki, bez znanja da je avans već delimično plaćen.

Sad:
- avans se fiskalizuje kao Advance/Sale pri kreiranju/izmeni naloga
  (fiskalizujAvansServisa — fiskalizuje samo deltu ako se avans dopunjava)
- konačan Normal/Sale račun pri preuzimanju šalje advancePaid/advanceTax/
  advanceLastInvoiceNumber/advanceLastInvoiceDateTime — PFR sad zna za avans
  i tačno računa preostali dug i kusur
- ako avans premaši punu cenu, konačan račun se zatvara do pune cene, a
  razlika se automatski vraća posebnim Advance/Refund zahtevom
- servis_detalji modal prikazuje 'Avans premašuje cenu — vratiti klijentu X'
  umesto tihog svođenja duga na 0

Uzgred ispravljena dva mesta koja su koristila DohvatiPoServisu (samo
najnoviji zapis, bilo kog tipa) umesto DohvatiPoServisuITip(Normal,Sale):
- PromeniStatus je avansni račun pogrešno tretirao kao 'već fiskalizovano'
  i preskakao konačan račun kad god je nalog imao avans
- RetryFiskalizacija i FiskalGreska detekcija na servis_detalji stranici
  su imale isti problem — nalog sa avansom je izgledao fiskalizovan i
  dugme 'Pokušaj ponovo' se nije ni prikazivalo

PorezIzBrutoAvansa pretpostavlja opštu PDV stopu (20%) jer stavke u trenutku
uplate avansa još nisu poznate — nema opšteg rešenja bez pretpostavke.
This commit is contained in:
2026-07-02 12:52:04 +02:00
parent ca1c8c8241
commit fcda706edd
6 changed files with 282 additions and 18 deletions
+5
View File
@@ -401,6 +401,11 @@ type FiskalRepository interface {
Kreiraj(ctx context.Context, fr *model.FiskalniRacun) (int64, error)
DohvatiPoProdaji(ctx context.Context, prodajaID int64) (*model.FiskalniRacun, error)
DohvatiPoServisu(ctx context.Context, servisID int64) (*model.FiskalniRacun, error)
// DohvatiPoServisuITip vraća poslednji nestorniran račun tačno određenog tipa
// (npr. avansni Sale) — za razliku od DohvatiPoServisu (samo najnoviji, bilo kog tipa)
DohvatiPoServisuITip(ctx context.Context, servisID int64, tipRacuna, tipTransakcije string) (*model.FiskalniRacun, error)
// SumaAvansaPoServisu vraća neto fiskalizovan avans (Sale - Refund) za nalog
SumaAvansaPoServisu(ctx context.Context, servisID int64) (float64, error)
OznačiKaoStorniran(ctx context.Context, id int64) error
ServisiBezFiskalnog(ctx context.Context) (map[int64]bool, error)
ProdajeBezFiskalnog(ctx context.Context) (map[int64]bool, error)
+55
View File
@@ -117,6 +117,61 @@ func (r *FiskalRepo) DohvatiPoServisu(ctx context.Context, servisID int64) (*mod
return fr, nil
}
// DohvatiPoServisuITip vraća poslednji nestorniran fiskalni račun za dati servisni
// nalog sa tačno određenim tipom (npr. tip_racuna="Advance", tip_transakcije="Sale"
// za avansni račun) — za razliku od DohvatiPoServisu koji vraća samo najnoviji
// zapis bilo kog tipa. Vraća nil bez greške ako takav račun ne postoji.
func (r *FiskalRepo) DohvatiPoServisuITip(ctx context.Context, servisID int64, tipRacuna, tipTransakcije string) (*model.FiskalniRacun, error) {
fr := &model.FiskalniRacun{}
var storniran int
var sID, pID sql.NullInt64
err := r.db.QueryRowContext(ctx, `
SELECT id, prodaja_id, servis_id, tip_racuna, tip_transakcije, pfr_broj, pfr_vreme,
brojac, ekstenzija_brojaca, url_verifikacija, qr_kod,
poreske_stavke, ukupno_za_naplatu, ukupan_porez,
sirovi_odgovor, potpisao, zatrazio, poruka, storniran, vreme_kreiranja
FROM fiskalni_racuni
WHERE servis_id = ? AND tip_racuna = ? AND tip_transakcije = ? AND storniran = 0
ORDER BY id DESC LIMIT 1
`, servisID, tipRacuna, tipTransakcije).Scan(
&fr.ID, &pID, &sID, &fr.TipRacuna, &fr.TipTransakcije, &fr.PfrBroj, &fr.PfrVreme,
&fr.Brojac, &fr.EkstenzijaBrojaca, &fr.UrlVerifikacija, &fr.QRKod,
&fr.PoreskeStavke, &fr.UkupnoZaNaplatu, &fr.UkupanPorez,
&fr.SiroviOdgovor, &fr.Potpisao, &fr.Zatrazio, &fr.Poruka, &storniran, &fr.VremeKreiranja,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("ntech: FiskalRepo.DohvatiPoServisuITip: %w", err)
}
if pID.Valid {
fr.ProdajaID = pID.Int64
}
if sID.Valid {
fr.ServisID = sID.Int64
}
fr.Storniran = storniran != 0
return fr, nil
}
// SumaAvansaPoServisu vraća neto fiskalizovan avans za dati servisni nalog —
// zbir Advance/Sale iznosa umanjen za Advance/Refund iznose (nestornirani).
// Koristi se da se pri izmeni avansa fiskalizuje samo razlika (delta), ne ceo
// novi iznos ponovo.
func (r *FiskalRepo) SumaAvansaPoServisu(ctx context.Context, servisID int64) (float64, error) {
var suma float64
err := r.db.QueryRowContext(ctx, `
SELECT COALESCE(SUM(CASE WHEN tip_transakcije = 'Refund' THEN -ukupno_za_naplatu ELSE ukupno_za_naplatu END), 0)
FROM fiskalni_racuni
WHERE servis_id = ? AND tip_racuna = 'Advance' AND storniran = 0
`, servisID).Scan(&suma)
if err != nil {
return 0, fmt.Errorf("ntech: FiskalRepo.SumaAvansaPoServisu: %w", err)
}
return suma, nil
}
// ServisiBezFiskalnog vraća skup ID-eva servisnih naloga koji su u statusu "Preuzeto"
// a nemaju fiskalni račun (za oznaku u listi naloga).
func (r *FiskalRepo) ServisiBezFiskalnog(ctx context.Context) (map[int64]bool, error) {
+8 -2
View File
@@ -29,9 +29,15 @@ func NoviKlijent(baseURL, apiKey string) *Klijent {
}
}
// InvoiceRequest je omotač za Teron invoiceRequest JSON objekat.
// InvoiceRequest je omotač za Teron invoiceRequest JSON objekat. Polja Advance*
// su na vrhu (van invoiceRequest) — Teron ih tako očekuje za konačni račun koji
// zatvara prethodno izdat avansni račun (v. NapraviKonacniZahtevSaAvansom).
type InvoiceRequest struct {
InvoiceRequest InvoiceRequestBody `json:"invoiceRequest"`
InvoiceRequest InvoiceRequestBody `json:"invoiceRequest"`
AdvancePaid *float64 `json:"advancePaid,omitempty"`
AdvanceTax *float64 `json:"advanceTax,omitempty"`
AdvanceLastInvoiceNumber string `json:"advanceLastInvoiceNumber,omitempty"`
AdvanceLastInvoiceDateTime string `json:"advanceLastInvoiceDateTime,omitempty"`
}
// InvoiceRequestBody su podaci koje Teron očekuje unutar invoiceRequest polja.
+45
View File
@@ -142,3 +142,48 @@ func NapraviRefundZahtev(
zahtev.InvoiceRequest.ReferentDocumentNumber = referentBroj
return zahtev
}
// NazivAvansneStavke je naziv stavke na avansnim fiskalnim računima (isti naziv
// mora biti dosledan, jer ga PFR ne povezuje po ID-u već po sadržaju stavke).
const NazivAvansneStavke = "Аванс"
// StopaAvansa je pretpostavljena PDV stopa za avans — u trenutku uplate avansa
// stavke (radovi/delovi) obično još nisu poznate, pa se ne može tačno odrediti
// stvarna stopa. Koristi se opšta stopa (20%) kao razumna pretpostavka za
// srpsko tržište; ako firma posluje isključivo po posebnoj/nultoj stopi, ovo
// treba prilagoditi ručno (nema opšteg rešenja bez poznavanja stavki unapred).
const StopaAvansa = 20.0
// NapraviAvansZahtev gradi Advance/Sale zahtev za primljeni avans — jedna stavka
// "Аванс" u punom (bruto) iznosu.
func NapraviAvansZahtev(iznos float64, nacinPlacanja, kasir string) InvoiceRequest {
return InvoiceRequest{
InvoiceRequest: InvoiceRequestBody{
InvoiceType: "Advance",
TransactionType: "Sale",
Payment: []PaymentItem{
{Amount: iznos, PaymentType: TipPlacanja(nacinPlacanja)},
},
Items: []InvoiceItem{
{Name: NazivAvansneStavke, Labels: []string{OznakaPDV(StopaAvansa)}, TotalAmount: iznos, UnitPrice: iznos, Quantity: 1},
},
Cashier: kasir,
},
}
}
// NapraviAvansRefundZahtev gradi Advance/Refund zahtev za povraćaj dela ili celog
// avansa (npr. kad avans premaši konačnu cenu popravke). referentBroj je PfrBroj
// originalnog avansnog računa.
func NapraviAvansRefundZahtev(iznos float64, nacinPlacanja, kasir, referentBroj string) InvoiceRequest {
zahtev := NapraviAvansZahtev(iznos, nacinPlacanja, kasir)
zahtev.InvoiceRequest.TransactionType = "Refund"
zahtev.InvoiceRequest.ReferentDocumentNumber = referentBroj
return zahtev
}
// PorezIzBrutoAvansa izvlači poreski deo iz bruto iznosa avansa po StopaAvansa
// (npr. 500 din avansa po 20% → porez ≈ 83.33 din).
func PorezIzBrutoAvansa(bruto float64) float64 {
return math.Round(bruto*StopaAvansa/(100+StopaAvansa)*100) / 100
}
+161 -16
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"image/png"
"log/slog"
"math"
"net/http"
"strconv"
"strings"
@@ -78,6 +79,8 @@ type PodaciDetaljiNaloga struct {
Avans float64
PreostaloSve float64
PreostaloSveSaPdv float64
VisakAvansa float64 // >0 kad avans premaši ukupnu cenu — iznos za povraćaj klijentu
VisakAvansaSaPdv float64
ZakljucanStatus bool // onemogući promenu statusa dok ima potraživanih delova
CenaDijagnostikePredlog string // podrazumevana cena dijagnostike iz podešavanja (za prefill input-a)
KorisceneUsluge map[int64]bool // ID-evi usluga već dodatih na nalog — izostavljaju se iz dropdown-a
@@ -305,6 +308,10 @@ func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
return
}
if nalog.Avans != nil && *nalog.Avans > 0 {
h.fiskalizujAvansServisa(r.Context(), id, *nalog.Avans, "Gotovina")
}
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
}
@@ -499,6 +506,10 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
return
}
if nalog.Avans != nil && *nalog.Avans > 0 {
h.fiskalizujAvansServisa(r.Context(), id, *nalog.Avans, "Gotovina")
}
if autosave {
w.WriteHeader(http.StatusNoContent)
return
@@ -822,11 +833,15 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
avans = *nalog.Avans
}
preostaloSve := ukupnoSve - avans
var visakAvansa float64
if preostaloSve < 0 {
visakAvansa = -preostaloSve
preostaloSve = 0
}
preostaloSveSaPdv := ukupnoSveSaPdv - avans
var visakAvansaSaPdv float64
if preostaloSveSaPdv < 0 {
visakAvansaSaPdv = -preostaloSveSaPdv
preostaloSveSaPdv = 0
}
@@ -867,6 +882,8 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
Avans: avans,
PreostaloSve: preostaloSve,
PreostaloSveSaPdv: preostaloSveSaPdv,
VisakAvansa: visakAvansa,
VisakAvansaSaPdv: visakAvansaSaPdv,
ZakljucanStatus: zakljucanStatus,
CenaDijagnostikePredlog: podesavanja["servis_cena_dijagnostike"],
KorisceneUsluge: korisceneUsluge,
@@ -880,11 +897,13 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
} else if err != nil {
slog.Error("greška pri učitavanju fiskalnog računa za servis", "servis_id", id, "error", err)
}
// fiskalna greška: modul aktivan, nalog preuzet, ali nema fiskalnog računa
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) &&
nalog.Status == model.StatusPreuzeto &&
podaci.FiskalniRacun == nil {
podaci.FiskalGreska = true
// fiskalna greška: modul aktivan, nalog preuzet, ali nema KONAČNOG (Normal/Sale)
// fiskalnog računa — avansni (Advance/Sale) se ne računa kao konačan, inače bi
// nalog sa avansom lažno izgledao fiskalizovan i dugme Fiskalizuj bi bilo skriveno
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) && nalog.Status == model.StatusPreuzeto {
if konacan, err := h.FiskalRepo.DohvatiPoServisuITip(r.Context(), id, "Normal", "Sale"); err == nil && konacan == nil {
podaci.FiskalGreska = true
}
}
// storno zahteva PIB/JMBG kupca za refundaciju ako klijent nema upisan poreski broj
podaci.PotrebnaIdentKupca = h.modulUkljucen(r.Context(), config.ModulFiskalizacija) &&
@@ -2343,12 +2362,15 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
middleware.SetFlash(w, r, h.DB, "greska", "Naplata nije sačuvana. Pokušajte ponovo.")
}
// fiskalizacija servisa — ako je modul uključen; preskoči ako fiskalni račun
// za ovaj nalog već postoji (ponovni ulazak u Preuzeto ne sme duplirati PFR zahtev)
// fiskalizacija servisa — ako je modul uključen; preskoči ako KONAČAN (Normal/Sale)
// fiskalni račun za ovaj nalog već postoji (ponovni ulazak u Preuzeto ne sme
// duplirati PFR zahtev) — avansni (Advance/Sale) račun se ne računa kao konačan.
// iznos==0 je dozvoljen (npr. avans premašuje cenu) jer i tada treba izdati
// konačan račun da zatvori avans i pokrene eventualni povraćaj viška.
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
if fr, _ := h.FiskalRepo.DohvatiPoServisu(r.Context(), id); fr == nil {
if fr, _ := h.FiskalRepo.DohvatiPoServisuITip(r.Context(), id, "Normal", "Sale"); fr == nil {
klijent := h.fiskalKlijent()
if klijent != nil && iznos > 0 {
if klijent != nil {
h.fiskalizujServis(r.Context(), id, klijent, nacin, iznos, primljeno)
}
}
@@ -2405,6 +2427,68 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
}
// fiskalizujAvansServisa izdaje avansni fiskalni račun (Advance/Sale) za primljeni
// avans. Best-effort: greška se loguje, ne zaustavlja tok pozivaoca. noviAvans je
// UKUPAN trenutni iznos avansa na nalogu (ne delta) — funkcija sama izračunava i
// fiskalizuje samo razliku u odnosu na već fiskalizovan avans, da izmena/dopuna
// avansa ne fiskalizuje ceo iznos ponovo. Bez efekta ako razlika nije pozitivna
// (smanjenje avansa se ne fiskalizuje automatski — v. razgovor o povraćaju).
func (h *Handler) fiskalizujAvansServisa(ctx context.Context, servisID int64, noviAvans float64, nacinPlacanja string) {
if noviAvans <= 0 {
return
}
if !h.modulUkljucen(ctx, config.ModulFiskalizacija) {
return
}
vecFiskalizovano, err := h.FiskalRepo.SumaAvansaPoServisu(ctx, servisID)
if err != nil {
slog.Error("fiskalizujAvansServisa: provera postojećeg avansa nije uspela", "servis_id", servisID, "error", err)
return
}
delta := math.Round((noviAvans-vecFiskalizovano)*100) / 100
if delta <= 0 {
return
}
klijent := h.fiskalKlijent()
if klijent == nil {
return
}
if nacinPlacanja == "" {
nacinPlacanja = "Gotovina"
}
zahtev := fiskal.NapraviAvansZahtev(delta, nacinPlacanja, h.imeKasira(ctx))
odgovor, err := klijent.IzdajRacun(ctx, zahtev)
if err != nil {
slog.Error("fiskalizacija avansa servisa nije uspela", "servis_id", servisID, "error", err)
return
}
poreskeJSON, _ := json.Marshal(odgovor.TaxItems)
siroviJSON, _ := json.Marshal(odgovor)
fr := &model.FiskalniRacun{
ServisID: servisID,
TipRacuna: "Advance",
TipTransakcije: "Sale",
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 _, err := h.FiskalRepo.Kreiraj(ctx, fr); err != nil {
slog.Error("greška pri čuvanju avansnog fiskalnog računa servisa", "servis_id", servisID, "error", err)
}
}
// fiskalizujServis šalje fiskalni zahtev za servisni nalog pri preuzimanju.
// Best-effort: greške se loguju, ne zaustavljaju promenu statusa. primljeno je
// iznos koji je klijent stvarno predao — ako je veći od iznos (dug posle avansa),
@@ -2463,6 +2547,35 @@ func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent
zahtev.InvoiceRequest.BuyerID = "10:" + pib
}
// ako je avans fiskalizovan (v. fiskalizujAvansServisa), konačni račun ga
// zatvara — PFR sam raspoređuje avans na ovaj račun preko Advance* polja.
// Ako avans premašuje punu cenu (visak), zatvara se samo do pune cene, a
// razlika se posle uspešnog Sale-a vraća posebnim Advance/Refund zahtevom.
var visakAvansa float64
var avansRacun *model.FiskalniRacun
if avansNeto, e := h.FiskalRepo.SumaAvansaPoServisu(ctx, servisID); e == nil && avansNeto > 0 {
if ar, e2 := h.FiskalRepo.DohvatiPoServisuITip(ctx, servisID, "Advance", "Sale"); e2 == nil && ar != nil {
avansRacun = ar
punaCena := 0.0
for _, it := range items {
punaCena += it.TotalAmount
}
punaCena = math.Round(punaCena*100) / 100
iskoriscenAvans := avansNeto
if iskoriscenAvans > punaCena {
visakAvansa = math.Round((iskoriscenAvans-punaCena)*100) / 100
iskoriscenAvans = punaCena
}
porez := fiskal.PorezIzBrutoAvansa(iskoriscenAvans)
zahtev.AdvancePaid = &iskoriscenAvans
zahtev.AdvanceTax = &porez
zahtev.AdvanceLastInvoiceNumber = avansRacun.PfrBroj
zahtev.AdvanceLastInvoiceDateTime = avansRacun.PfrVreme
}
} else if e != nil {
slog.Error("fiskalizujServis: provera avansa nije uspela", "servis_id", servisID, "error", e)
}
odgovor, errFisk := klijent.IzdajRacun(ctx, zahtev)
if errFisk != nil {
slog.Error("fiskalizacija servisa nije uspela", "servis_id", servisID, "error", errFisk)
@@ -2492,6 +2605,40 @@ func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent
}
if _, err := h.FiskalRepo.Kreiraj(ctx, fr); err != nil {
slog.Error("greška pri čuvanju fiskalnog računa za servis", "servis_id", servisID, "error", err)
return
}
// avans je premašio punu cenu — vrati razliku posebnim Advance/Refund zahtevom
if visakAvansa > 0 && avansRacun != nil {
refundZahtev := fiskal.NapraviAvansRefundZahtev(visakAvansa, nacinPlacanja, kasir, avansRacun.PfrBroj)
refundOdgovor, errRefund := klijent.IzdajRacun(ctx, refundZahtev)
if errRefund != nil {
slog.Error("povraćaj viška avansa nije uspeo", "servis_id", servisID, "error", errRefund)
return
}
poreskeRefJSON, _ := json.Marshal(refundOdgovor.TaxItems)
siroviRefJSON, _ := json.Marshal(refundOdgovor)
refFr := &model.FiskalniRacun{
ServisID: servisID,
TipRacuna: "Advance",
TipTransakcije: "Refund",
PfrBroj: refundOdgovor.InvoiceNumber,
PfrVreme: refundOdgovor.SdcDateTime,
Brojac: refundOdgovor.InvoiceCounter,
EkstenzijaBrojaca: refundOdgovor.InvoiceCounterExtension,
UrlVerifikacija: refundOdgovor.VerificationURL,
QRKod: refundOdgovor.VerificationQRCode,
PoreskeStavke: string(poreskeRefJSON),
UkupnoZaNaplatu: refundOdgovor.TotalAmount,
UkupanPorez: refundOdgovor.TotalTax,
SiroviOdgovor: string(siroviRefJSON),
Potpisao: refundOdgovor.SignedBy,
Zatrazio: refundOdgovor.RequestedBy,
Poruka: refundOdgovor.Messages,
}
if _, err := h.FiskalRepo.Kreiraj(ctx, refFr); err != nil {
slog.Error("greška pri čuvanju povraćaja viška avansa", "servis_id", servisID, "error", err)
}
}
}
@@ -2625,8 +2772,9 @@ func (h *Handler) RetryFiskalizacija(w http.ResponseWriter, r *http.Request) {
return
}
// provjeri da fiskal još nije izdat (zaštita od duplikata)
if fr, _ := h.FiskalRepo.DohvatiPoServisu(r.Context(), id); fr != nil {
// provjeri da KONAČAN (Normal/Sale) fiskal još nije izdat (zaštita od duplikata)
// avansni (Advance/Sale) račun se ne računa kao konačan, ne sme blokirati retry
if fr, _ := h.FiskalRepo.DohvatiPoServisuITip(r.Context(), id, "Normal", "Sale"); fr != nil {
middleware.SetFlash(w, r, h.DB, "uspeh", "Fiskalni račun već postoji.")
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
return
@@ -2637,11 +2785,8 @@ func (h *Handler) RetryFiskalizacija(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Nalog nije u statusu Preuzeto", http.StatusBadRequest)
return
}
if nalog.Naplaceno <= 0 {
middleware.SetFlash(w, r, h.DB, "greska", "Iznos naplate je 0 — fiskalizacija nije moguća.")
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
return
}
// Naplaceno==0 je dozvoljeno kad avans u potpunosti pokriva cenu — konačan
// račun se i tada mora izdati da zatvori avans (i eventualno vrati višak)
klijent := h.fiskalKlijent()
if klijent == nil {
@@ -130,6 +130,14 @@
<span style="font-size:16px;font-weight:600;color:var(--tekst-glavni);">Za naplatu</span>
<span style="font-size:18px;font-weight:600;color:var(--tekst-glavni);" id="za-naplatu-iznos">{{if index .Moduli "pdv"}}{{dinari .PreostaloSveSaPdv}}{{else}}{{dinari .PreostaloSve}}{{end}} din</span>
</div>
{{$visak := .VisakAvansa}}{{if index .Moduli "pdv"}}{{$visak = .VisakAvansaSaPdv}}{{end}}
{{if gt $visak 0.0}}
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:color-mix(in srgb,#f59e0b 12%,transparent);border:0.5px solid #f59e0b;border-radius:8px;">
<span style="color:#92400e;font-weight:500;">Avans premašuje cenu — vratiti klijentu</span>
<span style="font-size:16px;font-weight:600;color:#92400e;">{{dinari $visak}} din</span>
</div>
<div class="pomocni-tekst">Ovaj iznos se automatski vraća kroz avansni povraćaj na fiskalnom računu.</div>
{{end}}
<form method="POST" action="/servis/{{.Nalog.ID}}/status" data-refresh="#sadrzaj" style="margin-top:8px;">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<input type="hidden" name="status" value="Preuzeto">