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:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user