Fiskalni pazar: presek stanja, dnevni izveštaj i razdvajanje praznina po prodaji/servisu

- Nova stranica fiskal_pazar: presek prometa od poslednjeg zaključenja, zaključenje dana, generisanje PDF izveštaja
- fiskal.go: dodati pozivi Presek/ZakljuciDan/GenerisiIzvestaj ka /api/financial/summary i /api/financial/report/summary
- PrazninaKnjigovodstva razdvojena na Prodaja/Servis varijante sa ID listama za direktne linkove sa dashboard-a
This commit is contained in:
2026-07-02 19:14:54 +02:00
parent 56e52036b6
commit 7c215e70e8
16 changed files with 965 additions and 140 deletions
+71 -27
View File
@@ -194,7 +194,7 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji
p.SumnjiviDupli++
continue
}
p.BezKir++
p.BezKirProdajaID = append(p.BezKirProdajaID, id)
}
for id, postoji := range postojiMapS {
if postoji {
@@ -204,8 +204,10 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji
p.SumnjiviDupli++
continue
}
p.BezKir++
p.BezKirServisID = append(p.BezKirServisID, id)
}
p.BezKirProdaja = len(p.BezKirProdajaID)
p.BezKirServis = len(p.BezKirServisID)
}
if h.modulUkljucen(ctx, "kpo") {
@@ -227,7 +229,7 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji
p.SumnjiviDupli++
continue
}
p.BezKpo++
p.BezKpoProdajaID = append(p.BezKpoProdajaID, id)
}
for id, postoji := range postojiMapS {
if postoji {
@@ -237,44 +239,86 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji
p.SumnjiviDupli++
continue
}
p.BezKpo++
p.BezKpoServisID = append(p.BezKpoServisID, id)
}
p.BezKpoProdaja = len(p.BezKpoProdajaID)
p.BezKpoServis = len(p.BezKpoServisID)
}
if h.modulUkljucen(ctx, "fiskalizacija") {
if m, err := h.FiskalRepo.ProdajeBezFiskalnog(ctx); err == nil {
p.BezFiskalnogProdaja += len(m)
for id := range m {
p.BezFiskalnogProdajaID = append(p.BezFiskalnogProdajaID, id)
}
p.BezFiskalnogProdaja = len(p.BezFiskalnogProdajaID)
}
if m, err := h.FiskalRepo.ServisiBezFiskalnog(ctx); err == nil {
p.BezFiskalnogServis += len(m)
for id := range m {
p.BezFiskalnogServisID = append(p.BezFiskalnogServisID, id)
}
p.BezFiskalnogServis = len(p.BezFiskalnogServisID)
}
if n, err := h.stornoBezRefunda(ctx); err == nil {
p.BezRefunda = n
if idsP, idsS, err := h.stornoBezRefunda(ctx); err == nil {
p.BezRefundaProdajaID = idsP
p.BezRefundaServisID = idsS
p.BezRefundaProdaja = len(idsP)
p.BezRefundaServis = len(idsS)
}
}
return p, nil
}
// stornoBezRefunda broji stornirane prodajne i servisne 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).
func (h *Handler) stornoBezRefunda(ctx context.Context) (int, error) {
var n int
err := h.DB.QueryRowContext(ctx, `
SELECT
(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)
+
(SELECT COUNT(*) FROM servisni_nalozi sn
JOIN fiskalni_racuni fr ON fr.servis_id = sn.id
AND fr.tip_transakcije = 'Sale' AND fr.storniran = 0
WHERE sn.stornirano = 1)
`).Scan(&n)
return n, err
// stornoBezRefunda vraća ID-jeve storniranih prodajnih i servisnih naloga č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).
func (h *Handler) stornoBezRefunda(ctx context.Context) (prodajaIDs, servisIDs []int64, err error) {
redoviP, err := h.DB.QueryContext(ctx, `
SELECT DISTINCT pn.id FROM prodajni_nalozi pn
JOIN fiskalni_racuni fr ON fr.prodaja_id = pn.id
AND fr.tip_racuna = 'Normal' AND fr.tip_transakcije = 'Sale' AND fr.storniran = 0
WHERE pn.stornirano = 1
`)
if err != nil {
return nil, nil, err
}
defer redoviP.Close()
for redoviP.Next() {
var id int64
if err := redoviP.Scan(&id); err != nil {
return nil, nil, err
}
prodajaIDs = append(prodajaIDs, id)
}
if err := redoviP.Err(); err != nil {
return nil, nil, err
}
// tip_racuna='Normal' je bitno: servis sa avansom ima i Advance/Sale zapis koji
// se NIKAD ne stornira pri storno servisa (samo konačan Normal/Sale) — bez ovog
// filtera bi JOIN dao dva reda za isti servis (duplikat u listi na dashboard-u)
redoviS, err := h.DB.QueryContext(ctx, `
SELECT DISTINCT sn.id FROM servisni_nalozi sn
JOIN fiskalni_racuni fr ON fr.servis_id = sn.id
AND fr.tip_racuna = 'Normal' AND fr.tip_transakcije = 'Sale' AND fr.storniran = 0
WHERE sn.stornirano = 1
`)
if err != nil {
return nil, nil, err
}
defer redoviS.Close()
for redoviS.Next() {
var id int64
if err := redoviS.Scan(&id); err != nil {
return nil, nil, err
}
servisIDs = append(servisIDs, id)
}
if err := redoviS.Err(); err != nil {
return nil, nil, err
}
return prodajaIDs, servisIDs, nil
}
func istKlijent(a, b *int64) bool {