Servis: KIR backfill i detekcija praznina (kao Prodaja)

Servisni KIR upis nije imao nijedan mehanizam oporavka — ako auto-upis pri
preuzimanju padne, B2B servisni promet tiho ispadne iz PDV evidencije bez traga
za korisnika. Dodato po uzoru na Prodaju:
- kirKandidatiServisa (preuzeti nalozi sa identifikovanim kupcem)
- KirBackfillServisa ruta + dugme "Uvezi iz servisa" na KIR stranici
- PraznineKnjigovodstva sad broji i servisne KIR praznine (dashboard upozorenje)
This commit is contained in:
2026-07-02 11:31:30 +02:00
parent f74cb04024
commit 9899c280c8
4 changed files with 126 additions and 4 deletions
+69
View File
@@ -296,6 +296,75 @@ func (h *Handler) KirBackfillProdaje(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther)
}
// KirBackfillServisa kreira KIR zapise za sve preuzete servisne naloge sa
// identifikovanim kupcem koji ih nemaju. Namenjen za jednokratno popunjavanje.
func (h *Handler) KirBackfillServisa(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "pdv.dodaj"); !ok {
return
}
ctx := r.Context()
sada := time.Now()
nalozi, postojiMap, kandidati, err := h.kirKandidatiServisa(ctx)
if err != nil {
http.Error(w, "Greška pri učitavanju servisa", http.StatusInternalServerError)
return
}
duplikati := sumnjiviDuplikati(kandidati)
var kreirano, preskoceno, sumnjivo int
for _, sn := range nalozi {
if postojiMap[sn.ID] {
preskoceno++
continue
}
if duplikati[sn.ID] {
sumnjivo++
continue
}
radovi, err := h.ServisniRadoviRepo.DohvatiZaNalog(ctx, sn.ID)
if err != nil {
slog.Error("backfill KIR: greška pri čitanju radova", "servis_id", sn.ID, "error", err)
continue
}
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(ctx, sn.ID)
if err != nil {
slog.Error("backfill KIR: greška pri čitanju delova", "servis_id", sn.ID, "error", err)
continue
}
klijent, err := h.KlijentiRepo.DohvatiID(ctx, *sn.KlijentID)
if err != nil {
slog.Error("backfill KIR: greška pri čitanju klijenta", "servis_id", sn.ID, "error", err)
continue
}
pib := klijent.PIB
if klijent.Tip != "pravno" {
pib = klijent.JMBG
}
datumPrometa := sn.DatumPrijema
if sn.DatumZavrsetka != nil {
datumPrometa = *sn.DatumZavrsetka
}
kir := model.KirIzServisa(sn.ServisniNalog, radovi, delovi, klijent.PunoIme(), pib, klijent.Mesto, datumPrometa, sada)
if kir.Ukupno <= 0 {
preskoceno++
continue
}
if _, e := h.PdvKirRepo.Kreiraj(ctx, &kir); e != nil {
slog.Error("backfill KIR: greška pri kreiranju zapisa", "servis_id", sn.ID, "error", e)
continue
}
kreirano++
}
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)
}
// PodaciDnevniPazar su podaci za formu zbirnog KIR unosa (maloprodaja)
type PodaciDnevniPazar struct {
model.PodaciStranice
+49 -4
View File
@@ -139,6 +139,36 @@ func (h *Handler) kpoKandidatiServisa(ctx context.Context) (naplaceni []model.Se
return naplaceni, postojiMap, kandidati, nenaplaceniBroj, nil
}
// kirKandidatiServisa vraća sve preuzete servisne naloge sa identifikovanim kupcem
// (KlijentID != nil, isti kriterijum kao u Prodaji — B2B/identifikovan kupac) sa
// mapom postojećih KIR upisa i kandidatima za detekciju duplikata.
func (h *Handler) kirKandidatiServisa(ctx context.Context) ([]model.ServisniNalogSaKlijentom, map[int64]bool, []kandidatUskladjivanja, error) {
svi, err := h.ServisRepo.Lista(ctx, "", "")
if err != nil {
return nil, nil, nil, err
}
var kandidatNalozi []model.ServisniNalogSaKlijentom
for _, sn := range svi {
if sn.Status == model.StatusPreuzeto && !sn.Stornirano && !sn.PopravkaOdbijena && sn.KlijentID != nil {
kandidatNalozi = append(kandidatNalozi, sn)
}
}
postojiMap := make(map[int64]bool, len(kandidatNalozi))
var kandidati []kandidatUskladjivanja
for _, sn := range kandidatNalozi {
postoji, _ := h.PdvKirRepo.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 kandidatNalozi, postojiMap, kandidati, 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.
@@ -146,16 +176,31 @@ func (h *Handler) PraznineKnjigovodstva(ctx context.Context) (model.PrazninaKnji
var p model.PrazninaKnjigovodstva
if h.modulUkljucen(ctx, "pdv") {
_, postojiMap, kandidati, err := h.kirKandidatiProdaje(ctx)
_, postojiMapP, kandidatiP, err := h.kirKandidatiProdaje(ctx)
if err != nil {
return p, err
}
duplikati := sumnjiviDuplikati(kandidati)
for id, postoji := range postojiMap {
_, postojiMapS, kandidatiS, err := h.kirKandidatiServisa(ctx)
if err != nil {
return p, err
}
duplikatiP := sumnjiviDuplikati(kandidatiP)
duplikatiS := sumnjiviDuplikati(kandidatiS)
for id, postoji := range postojiMapP {
if postoji {
continue
}
if duplikati[id] {
if duplikatiP[id] {
p.SumnjiviDupli++
continue
}
p.BezKir++
}
for id, postoji := range postojiMapS {
if postoji {
continue
}
if duplikatiS[id] {
p.SumnjiviDupli++
continue
}