Knjigovodstvo i fiskalizacija: dashboard upozorenje za praznine, KPO dozvole, pojednostavljen storno, ispravke verifikacije i kasira
- Dashboard kartica prikazuje naloge bez KIR/KPO/fiskalnog upisa i sumnjive duplikate (double-submit), sa zaštitom od automatskog dupliranja prihoda pri bulk dopuni - Registrovane nedostajuće KPO dozvole (kpo.pregled/dodaj/obrisi) — modul je bio potpuno nedostupan - Uklonjen suvišan/nefunkcionalan 'Ukloni nalog' (duplirao Storno bez stvarne razlike); Storno u listi prodaje sad vodi direktno na formu - Kasir na fiskalnom računu koristi ime prijavljenog korisnika umesto fiksnog podešavanja - Fiskalni računi za nalog uvek prikazuju najnoviji zapis (original vs. refund posle storna) - Verifikaciona stranica (/v/) jasno označena kao lokalna test simulacija, ne prava provera kod PURS-a; dodat prikaz kusura - Mock brojač fiskalnih računa (kartica emulator) sad preživljava restart servera
This commit is contained in:
@@ -101,6 +101,13 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var prazninaKnjigovodstva model.PrazninaKnjigovodstva
|
||||
if p, err := h.PraznineKnjigovodstva(ctx); err != nil {
|
||||
slog.Error("dashboard: praznine knjigovodstva", "error", err)
|
||||
} else {
|
||||
prazninaKnjigovodstva = p
|
||||
}
|
||||
|
||||
// poslednjih 5 prodajnih naloga
|
||||
var poslednjeProdaje []model.StavkaProdajePregled
|
||||
if redovi, err := h.IzvestajRepo.PoslednjeProdaje(ctx, 5); err != nil {
|
||||
@@ -130,6 +137,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
KriticneZalihe: kriticneZalihe,
|
||||
PoslednjeProdaje: poslednjeProdaje,
|
||||
FlashGreska: flashGreska,
|
||||
PrazninaKnjige: prazninaKnjigovodstva,
|
||||
}
|
||||
|
||||
h.renderujTemplate(w, "dashboard", podaci)
|
||||
|
||||
@@ -52,6 +52,7 @@ type fiskalVerifikacijaPodaci struct {
|
||||
Podaci *FiskalVlPodaci
|
||||
Greska string
|
||||
JeFiskalni bool
|
||||
Kusur float64 // zbir plaćanja umanjen za ukupan iznos, prikazuje se samo ako je > 0
|
||||
}
|
||||
|
||||
func (h *Handler) FiskalVerifikacija(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -88,5 +89,13 @@ func (h *Handler) FiskalVerifikacija(w http.ResponseWriter, r *http.Request) {
|
||||
vlPodaci.InvoiceType != "Training" &&
|
||||
vlPodaci.InvoiceType != "Proforma"
|
||||
|
||||
var placeno float64
|
||||
for _, p := range vlPodaci.Payments {
|
||||
placeno += p.Amount
|
||||
}
|
||||
if kusur := placeno - vlPodaci.TotalAmount; kusur > 0.005 {
|
||||
podaci.Kusur = kusur
|
||||
}
|
||||
|
||||
h.renderujStandalone(w, "fiskal_verifikacija", podaci)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"ntech/internal/config"
|
||||
@@ -257,3 +258,25 @@ func (h *Handler) fiskalKlijent() *fiskal.Klijent {
|
||||
}
|
||||
return fiskal.NoviKlijent(url, key)
|
||||
}
|
||||
|
||||
// imeKasira vraća ime i prezime prijavljenog korisnika za polje "kasir" na fiskalnom
|
||||
// računu (korisničko ime kao rezerva ako ime/prezime nisu uneti). Pada nazad na
|
||||
// podešavanje pfr_kasir (npr. za pozadinske pozive bez korisnika u kontekstu), pa na
|
||||
// "NTech" ako ni to nije uneto.
|
||||
func (h *Handler) imeKasira(ctx context.Context) string {
|
||||
kasir := ""
|
||||
if k := middleware.KorisnikIzKonteksta(ctx); k != nil {
|
||||
if k.Ime != "" || k.Prezime != "" {
|
||||
kasir = strings.TrimSpace(k.Ime + " " + k.Prezime)
|
||||
} else {
|
||||
kasir = k.KorisnickoIme
|
||||
}
|
||||
}
|
||||
if kasir == "" {
|
||||
kasir, _ = sqlite.DohvatiPodesavanje(ctx, h.DB, "pfr_kasir")
|
||||
}
|
||||
if kasir == "" {
|
||||
kasir = "NTech"
|
||||
}
|
||||
return kasir
|
||||
}
|
||||
|
||||
+20
-12
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appdb "ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
@@ -161,24 +160,28 @@ func (h *Handler) KpoBackfill(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
var kreirano, preskoceno int
|
||||
var kreirano, preskoceno, sumnjivo int
|
||||
|
||||
// prodajni nalozi
|
||||
nalozi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
|
||||
nalozi, prodajaPostoji, prodajaKandidati, err := h.kpoKandidatiProdaje(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju prodaje", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
prodajaDuplikati := sumnjiviDuplikati(prodajaKandidati)
|
||||
for _, nd := range nalozi {
|
||||
if nd.Stornirano {
|
||||
preskoceno++
|
||||
continue
|
||||
}
|
||||
postoji, _ := h.KpoRepo.PostojiZaIzvor(ctx, "prodaja", nd.ID)
|
||||
if postoji {
|
||||
if prodajaPostoji[nd.ID] {
|
||||
preskoceno++
|
||||
continue
|
||||
}
|
||||
if prodajaDuplikati[nd.ID] {
|
||||
sumnjivo++
|
||||
continue
|
||||
}
|
||||
z := model.KpoZapis{
|
||||
DatumPrometa: nd.Datum,
|
||||
BrojDokumenta: nd.BrojNaloga,
|
||||
@@ -196,18 +199,19 @@ func (h *Handler) KpoBackfill(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// naplaćeni servisni nalozi
|
||||
servisNalozi, err := h.ServisRepo.Lista(ctx, "", "")
|
||||
naplaceni, servisPostoji, servisKandidati, nenaplaceniBroj, err := h.kpoKandidatiServisa(ctx)
|
||||
if err != nil {
|
||||
slog.Error("kpo backfill: greška pri učitavanju servisa", "error", err)
|
||||
} else {
|
||||
for _, sn := range servisNalozi {
|
||||
if sn.Status != model.StatusPreuzeto || sn.Naplaceno == 0 {
|
||||
preskoceno += nenaplaceniBroj
|
||||
servisDuplikati := sumnjiviDuplikati(servisKandidati)
|
||||
for _, sn := range naplaceni {
|
||||
if servisPostoji[sn.ID] {
|
||||
preskoceno++
|
||||
continue
|
||||
}
|
||||
postoji, _ := h.KpoRepo.PostojiZaIzvor(ctx, "servis", sn.ID)
|
||||
if postoji {
|
||||
preskoceno++
|
||||
if servisDuplikati[sn.ID] {
|
||||
sumnjivo++
|
||||
continue
|
||||
}
|
||||
datum := sn.DatumPrijema
|
||||
@@ -231,6 +235,10 @@ func (h *Handler) KpoBackfill(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", fmt.Sprintf("Backfill završen: %d novih KPO unosa, %d preskočeno.", kreirano, preskoceno))
|
||||
poruka := fmt.Sprintf("Backfill završen: %d novih KPO unosa, %d preskočeno.", kreirano, preskoceno)
|
||||
if sumnjivo > 0 {
|
||||
poruka += fmt.Sprintf(" %d sumnjivih duplikata NIJE upisano — proveri ručno.", sumnjivo)
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", poruka)
|
||||
http.Redirect(w, r, "/kpo", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appdb "ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
@@ -223,23 +222,27 @@ func (h *Handler) KirBackfillProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
sada := time.Now()
|
||||
|
||||
nalozi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
|
||||
nalozi, postojiMap, kandidati, err := h.kirKandidatiProdaje(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju prodaje", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
duplikati := sumnjiviDuplikati(kandidati)
|
||||
|
||||
var kreirano, preskoceno int
|
||||
var kreirano, preskoceno, sumnjivo int
|
||||
for _, nd := range nalozi {
|
||||
if nd.KlijentID == nil {
|
||||
preskoceno++
|
||||
continue
|
||||
}
|
||||
postoji, _ := h.PdvKirRepo.PostojiZaIzvor(ctx, "prodaja", nd.ID)
|
||||
if postoji {
|
||||
if postojiMap[nd.ID] {
|
||||
preskoceno++
|
||||
continue
|
||||
}
|
||||
if duplikati[nd.ID] {
|
||||
sumnjivo++
|
||||
continue
|
||||
}
|
||||
stavke, err := h.ProdajaRepo.DohvatiStavke(ctx, nd.ID)
|
||||
if err != nil {
|
||||
slog.Error("backfill KIR: greška pri čitanju stavki", "prodaja_id", nd.ID, "error", err)
|
||||
@@ -267,7 +270,11 @@ func (h *Handler) KirBackfillProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
kreirano++
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", fmt.Sprintf("Backfill završen: %d novih KIR zapisa, %d preskočeno.", kreirano, preskoceno))
|
||||
poruka := fmt.Sprintf("Backfill završen: %d novih KIR zapisa, %d preskočeno.", kreirano, preskoceno)
|
||||
if sumnjivo > 0 {
|
||||
poruka += fmt.Sprintf(" %d sumnjivih duplikata NIJE upisano — proveri ručno.", sumnjivo)
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", poruka)
|
||||
http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ type PodaciProdaje struct {
|
||||
model.PodaciStranice
|
||||
Nalozi []model.ProdajniNalogSaDetaljem
|
||||
Sacuvano bool
|
||||
Obrisan bool
|
||||
Pretraga string
|
||||
Od string
|
||||
Do string
|
||||
@@ -189,7 +188,6 @@ func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Nalozi: nalozi,
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
Obrisan: r.URL.Query().Get("obrisan") == "1",
|
||||
Pretraga: pretraga,
|
||||
Od: od,
|
||||
Do: do,
|
||||
@@ -533,39 +531,6 @@ pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;dis
|
||||
fmt.Fprint(w, `<p style="margin-top:16px;"><button onclick="window.print()" style="padding:8px 16px;">Štampaj</button></p></body></html>`)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
k, ok := h.zahtevajDozvolu(w, r, "prodaja.obrisi")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
poreskiBrojKupca := strings.TrimSpace(r.FormValue("poreski_broj_kupca"))
|
||||
if err := h.stornirajProdaju(r.Context(), id, "administrativno uklanjanje", &k.ID, poreskiBrojKupca); err != nil {
|
||||
poruka := "Greška pri uklanjanju. Možda je nalog već storniran."
|
||||
if errors.Is(err, errPotrebnaIdentKupca) {
|
||||
poruka = errPotrebnaIdentKupca.Error()
|
||||
} else {
|
||||
slog.Error("greška pri uklanjanju naloga", "error", err)
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "greska", poruka)
|
||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/prodaja?obrisan=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// parseFormuProdaje čita zaglavlje i stavke iz HTTP forme i vraća model i eventualnu grešku
|
||||
func parseFormuProdaje(r *http.Request) (model.ProdajniNalog, []model.StavkaProdaje, string) {
|
||||
var nalog model.ProdajniNalog
|
||||
@@ -701,8 +666,6 @@ func klasifikujPoreskiBroj(broj string) (pib, jmbg string) {
|
||||
// stornirajProdaju sprovodi kompletan storno prodajnog naloga: menja status naloga i
|
||||
// vraća artikle na stanje, uklanja vezani auto-KIR zapis, šalje fiskalni refund
|
||||
// (best-effort — storno ostaje validan i bez uspešnog refunda) i briše KPO zapis.
|
||||
// Zajednička je za StornoProdaje i ObrisiProdaju kako obe akcije imaju isti,
|
||||
// potpuni efekat prema PDV/fiskalnoj i KPO evidenciji.
|
||||
func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string, korisnikID *int64, poreskiBrojKupca string) error {
|
||||
// Refundacija po propisu zahteva identifikaciju kupca (PIB/JMBG) — ako je nalog
|
||||
// bez klijenta (ili klijent nema upisan PIB/JMBG), mora se ručno uneti pre storna.
|
||||
@@ -735,10 +698,7 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string,
|
||||
if fk := h.fiskalKlijent(); fk != nil {
|
||||
if nalogFisk, e := h.ProdajaRepo.DohvatiID(ctx, id); e == nil {
|
||||
if stavkeFisk, e2 := h.ProdajaRepo.DohvatiStavke(ctx, id); e2 == nil {
|
||||
kasirFisk, _ := sqlite.DohvatiPodesavanje(ctx, h.DB, "pfr_kasir")
|
||||
if kasirFisk == "" {
|
||||
kasirFisk = "NTech"
|
||||
}
|
||||
kasirFisk := h.imeKasira(ctx)
|
||||
var pibFisk, jmbgFisk string
|
||||
if nalogFisk.KlijentID != nil {
|
||||
if kk, e3 := h.KlijentiRepo.DohvatiID(ctx, *nalogFisk.KlijentID); e3 == nil {
|
||||
@@ -810,10 +770,7 @@ func (h *Handler) fiskalizujProdaju(ctx context.Context, prodajaID int64, klijen
|
||||
return
|
||||
}
|
||||
|
||||
kasir, _ := sqlite.DohvatiPodesavanje(ctx, h.DB, "pfr_kasir")
|
||||
if kasir == "" {
|
||||
kasir = "NTech"
|
||||
}
|
||||
kasir := h.imeKasira(ctx)
|
||||
|
||||
var pib, jmbg string
|
||||
if nalog.KlijentID != nil {
|
||||
|
||||
@@ -2340,20 +2340,7 @@ func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent
|
||||
}
|
||||
}
|
||||
|
||||
kasir := ""
|
||||
if kor := middleware.KorisnikIzKonteksta(ctx); kor != nil {
|
||||
if kor.Ime != "" || kor.Prezime != "" {
|
||||
kasir = strings.TrimSpace(kor.Ime + " " + kor.Prezime)
|
||||
} else {
|
||||
kasir = kor.KorisnickoIme
|
||||
}
|
||||
}
|
||||
if kasir == "" {
|
||||
kasir, _ = sqlite.DohvatiPodesavanje(ctx, h.DB, "pfr_kasir")
|
||||
}
|
||||
if kasir == "" {
|
||||
kasir = "NTech"
|
||||
}
|
||||
kasir := h.imeKasira(ctx)
|
||||
|
||||
zahtev := fiskal.InvoiceRequest{
|
||||
InvoiceRequest: fiskal.InvoiceRequestBody{
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
appdb "ntech/internal/db"
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// kandidatUskladjivanja opisuje jedan nalog koji se razmatra za automatski backfill
|
||||
// KIR/KPO upisa — koristi se samo za detekciju sumnjivih duplikata (double-submit),
|
||||
// ne za sam upis.
|
||||
type kandidatUskladjivanja struct {
|
||||
ID int64
|
||||
KlijentID *int64
|
||||
Iznos float64
|
||||
Datum time.Time
|
||||
ImaUpis bool
|
||||
}
|
||||
|
||||
// sumnjiviDuplikati vraća ID-eve naloga BEZ upisa koje automatski backfill treba da
|
||||
// PRESKOČI — jer postoji drugi (bliski) nalog istog klijenta i iznosa u roku od minuta
|
||||
// koji već ima upis, ili je stariji i takođe čeka na upis. Ovo je znak dvostrukog
|
||||
// slanja forme (double-submit bag na naplati), ne dva odvojena prometa — automatska
|
||||
// dopuna bi inače duplirala prihod. Prosleđuje se CEO skup naloga (i sa i bez upisa)
|
||||
// radi poređenja.
|
||||
func sumnjiviDuplikati(svi []kandidatUskladjivanja) map[int64]bool {
|
||||
const prozor = 60 * time.Second
|
||||
sumnjivi := make(map[int64]bool)
|
||||
for _, a := range svi {
|
||||
if a.ImaUpis {
|
||||
continue
|
||||
}
|
||||
for _, b := range svi {
|
||||
if a.ID == b.ID {
|
||||
continue
|
||||
}
|
||||
if !istKlijent(a.KlijentID, b.KlijentID) || a.Iznos != b.Iznos {
|
||||
continue
|
||||
}
|
||||
razlika := a.Datum.Sub(b.Datum)
|
||||
if razlika < 0 {
|
||||
razlika = -razlika
|
||||
}
|
||||
if razlika > prozor {
|
||||
continue
|
||||
}
|
||||
if b.ImaUpis || b.ID < a.ID {
|
||||
sumnjivi[a.ID] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return sumnjivi
|
||||
}
|
||||
|
||||
// kirKandidatiProdaje vraća sve B2B prodajne naloge (nezavisno od toga da li već imaju
|
||||
// KIR upis) sa mapom postojećih upisa i kandidatima za detekciju duplikata. Koristi se
|
||||
// i za backfill (KirBackfillProdaje) i za brojanje praznina na dashboard-u.
|
||||
func (h *Handler) kirKandidatiProdaje(ctx context.Context) ([]model.ProdajniNalogSaDetaljem, map[int64]bool, []kandidatUskladjivanja, error) {
|
||||
nalozi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
postojiMap := make(map[int64]bool, len(nalozi))
|
||||
var kandidati []kandidatUskladjivanja
|
||||
for _, nd := range nalozi {
|
||||
if nd.KlijentID == nil {
|
||||
continue
|
||||
}
|
||||
postoji, _ := h.PdvKirRepo.PostojiZaIzvor(ctx, "prodaja", nd.ID)
|
||||
postojiMap[nd.ID] = postoji
|
||||
kandidati = append(kandidati, kandidatUskladjivanja{
|
||||
ID: nd.ID, KlijentID: nd.KlijentID, Iznos: nd.Ukupno, Datum: nd.Datum, ImaUpis: postoji,
|
||||
})
|
||||
}
|
||||
return nalozi, postojiMap, kandidati, nil
|
||||
}
|
||||
|
||||
// kpoKandidatiProdaje vraća sve nestornirane prodajne naloge sa mapom postojećih KPO
|
||||
// upisa i kandidatima za detekciju duplikata.
|
||||
func (h *Handler) kpoKandidatiProdaje(ctx context.Context) ([]model.ProdajniNalogSaDetaljem, map[int64]bool, []kandidatUskladjivanja, error) {
|
||||
nalozi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
postojiMap := make(map[int64]bool, len(nalozi))
|
||||
var kandidati []kandidatUskladjivanja
|
||||
for _, nd := range nalozi {
|
||||
if nd.Stornirano {
|
||||
continue
|
||||
}
|
||||
postoji, _ := h.KpoRepo.PostojiZaIzvor(ctx, "prodaja", nd.ID)
|
||||
postojiMap[nd.ID] = postoji
|
||||
kandidati = append(kandidati, kandidatUskladjivanja{
|
||||
ID: nd.ID, KlijentID: nd.KlijentID, Iznos: nd.Ukupno, Datum: nd.Datum, ImaUpis: postoji,
|
||||
})
|
||||
}
|
||||
return nalozi, postojiMap, kandidati, nil
|
||||
}
|
||||
|
||||
// kpoKandidatiServisa vraća sve naplaćene i preuzete servisne naloge sa mapom
|
||||
// postojećih KPO upisa, kandidatima za detekciju duplikata, i brojem naloga koji su
|
||||
// izostavljeni jer nisu (još) naplaćeni/preuzeti.
|
||||
func (h *Handler) kpoKandidatiServisa(ctx context.Context) (naplaceni []model.ServisniNalogSaKlijentom, postojiMap map[int64]bool, kandidati []kandidatUskladjivanja, nenaplaceniBroj int, err error) {
|
||||
servisNalozi, err := h.ServisRepo.Lista(ctx, "", "")
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, err
|
||||
}
|
||||
naplaceni = make([]model.ServisniNalogSaKlijentom, 0, len(servisNalozi))
|
||||
for _, sn := range servisNalozi {
|
||||
if sn.Status == model.StatusPreuzeto && sn.Naplaceno != 0 {
|
||||
naplaceni = append(naplaceni, sn)
|
||||
} else {
|
||||
nenaplaceniBroj++
|
||||
}
|
||||
}
|
||||
postojiMap = make(map[int64]bool, len(naplaceni))
|
||||
for _, sn := range naplaceni {
|
||||
postoji, _ := h.KpoRepo.PostojiZaIzvor(ctx, "servis", sn.ID)
|
||||
postojiMap[sn.ID] = postoji
|
||||
datum := sn.DatumPrijema
|
||||
if sn.DatumZavrsetka != nil {
|
||||
datum = *sn.DatumZavrsetka
|
||||
}
|
||||
kandidati = append(kandidati, kandidatUskladjivanja{
|
||||
ID: sn.ID, KlijentID: sn.KlijentID, Iznos: sn.Naplaceno, Datum: datum, ImaUpis: postoji,
|
||||
})
|
||||
}
|
||||
return naplaceni, postojiMap, kandidati, nenaplaceniBroj, nil
|
||||
}
|
||||
|
||||
// PraznineKnjigovodstva broji naloge (prodaja + servis) koji čekaju KIR/KPO/fiskalni
|
||||
// upis prema TRENUTNO aktivnim modulima firme, i naloge koji su sumnjivi duplikati
|
||||
// (double-submit) pa ih automatski backfill preskače.
|
||||
func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnjigovodstva, error) {
|
||||
var p model.PrazninaKnjigovodstva
|
||||
|
||||
if h.modulUkljucen(ctx, "pdv") {
|
||||
_, postojiMap, kandidati, err := h.kirKandidatiProdaje(ctx)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
duplikati := sumnjiviDuplikati(kandidati)
|
||||
for id, postoji := range postojiMap {
|
||||
if postoji {
|
||||
continue
|
||||
}
|
||||
if duplikati[id] {
|
||||
p.SumnjiviDupli++
|
||||
continue
|
||||
}
|
||||
p.BezKir++
|
||||
}
|
||||
}
|
||||
|
||||
if h.modulUkljucen(ctx, "kpo") {
|
||||
_, postojiMapP, kandidatiP, err := h.kpoKandidatiProdaje(ctx)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
_, postojiMapS, kandidatiS, _, err := h.kpoKandidatiServisa(ctx)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
duplikatiP := sumnjiviDuplikati(kandidatiP)
|
||||
duplikatiS := sumnjiviDuplikati(kandidatiS)
|
||||
for id, postoji := range postojiMapP {
|
||||
if postoji {
|
||||
continue
|
||||
}
|
||||
if duplikatiP[id] {
|
||||
p.SumnjiviDupli++
|
||||
continue
|
||||
}
|
||||
p.BezKpo++
|
||||
}
|
||||
for id, postoji := range postojiMapS {
|
||||
if postoji {
|
||||
continue
|
||||
}
|
||||
if duplikatiS[id] {
|
||||
p.SumnjiviDupli++
|
||||
continue
|
||||
}
|
||||
p.BezKpo++
|
||||
}
|
||||
}
|
||||
|
||||
if h.modulUkljucen(ctx, "fiskalizacija") {
|
||||
if m, err := h.FiskalRepo.ProdajeBezFiskalnog(ctx); err == nil {
|
||||
p.BezFiskalnog += len(m)
|
||||
}
|
||||
if m, err := h.FiskalRepo.ServisiBezFiskalnog(ctx); err == nil {
|
||||
p.BezFiskalnog += len(m)
|
||||
}
|
||||
if n, err := h.stornoBezRefunda(ctx); err == nil {
|
||||
p.BezRefunda = n
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// stornoBezRefunda broji stornirane prodajne naloge čiji je originalni fiskalni
|
||||
// račun i dalje neoznačen kao storniran — znači da je storno u bazi prošao, ali
|
||||
// fiskalni refund ka ESIR/L-PFR uređaju NIJE uspeo (best-effort poziv je pao).
|
||||
// Servis nema storno funkciju — obuhvaćena je samo prodaja.
|
||||
func (h *Handler) stornoBezRefunda(ctx context.Context) (int, error) {
|
||||
var n int
|
||||
err := h.DB.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM prodajni_nalozi pn
|
||||
JOIN fiskalni_racuni fr ON fr.prodaja_id = pn.id
|
||||
AND fr.tip_transakcije = 'Sale' AND fr.storniran = 0
|
||||
WHERE pn.stornirano = 1
|
||||
`).Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func istKlijent(a, b *int64) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
Reference in New Issue
Block a user