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:
@@ -312,6 +312,7 @@ func main() {
|
|||||||
r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kir/nova", h.SacuvajPdvKir)
|
r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kir/nova", h.SacuvajPdvKir)
|
||||||
r.With(modul("pdv"), doz("pdv.obrisi")).Post("/pdv/kir/obrisi/{id}", h.ObrisiPdvKir)
|
r.With(modul("pdv"), doz("pdv.obrisi")).Post("/pdv/kir/obrisi/{id}", h.ObrisiPdvKir)
|
||||||
r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kir/backfill-prodaje", h.KirBackfillProdaje)
|
r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kir/backfill-prodaje", h.KirBackfillProdaje)
|
||||||
|
r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kir/backfill-servisa", h.KirBackfillServisa)
|
||||||
r.With(modul("pdv"), doz("pdv.dodaj")).Get("/pdv/kir/dnevni-pazar", h.DnevniPazarKir)
|
r.With(modul("pdv"), doz("pdv.dodaj")).Get("/pdv/kir/dnevni-pazar", h.DnevniPazarKir)
|
||||||
r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kir/dnevni-pazar", h.SacuvajDnevniPazarKir)
|
r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kir/dnevni-pazar", h.SacuvajDnevniPazarKir)
|
||||||
r.With(modul("pdv")).Get("/pdv/kpr", h.PdvKpr)
|
r.With(modul("pdv")).Get("/pdv/kpr", h.PdvKpr)
|
||||||
|
|||||||
@@ -296,6 +296,75 @@ func (h *Handler) KirBackfillProdaje(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther)
|
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)
|
// PodaciDnevniPazar su podaci za formu zbirnog KIR unosa (maloprodaja)
|
||||||
type PodaciDnevniPazar struct {
|
type PodaciDnevniPazar struct {
|
||||||
model.PodaciStranice
|
model.PodaciStranice
|
||||||
|
|||||||
@@ -139,6 +139,36 @@ func (h *Handler) kpoKandidatiServisa(ctx context.Context) (naplaceni []model.Se
|
|||||||
return naplaceni, postojiMap, kandidati, nenaplaceniBroj, nil
|
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
|
// PraznineKnjigovodstva broji naloge (prodaja + servis) koji čekaju KIR/KPO/fiskalni
|
||||||
// upis prema TRENUTNO aktivnim modulima firme, i naloge koji su sumnjivi duplikati
|
// upis prema TRENUTNO aktivnim modulima firme, i naloge koji su sumnjivi duplikati
|
||||||
// (double-submit) pa ih automatski backfill preskače.
|
// (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
|
var p model.PrazninaKnjigovodstva
|
||||||
|
|
||||||
if h.modulUkljucen(ctx, "pdv") {
|
if h.modulUkljucen(ctx, "pdv") {
|
||||||
_, postojiMap, kandidati, err := h.kirKandidatiProdaje(ctx)
|
_, postojiMapP, kandidatiP, err := h.kirKandidatiProdaje(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
duplikati := sumnjiviDuplikati(kandidati)
|
_, postojiMapS, kandidatiS, err := h.kirKandidatiServisa(ctx)
|
||||||
for id, postoji := range postojiMap {
|
if err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
duplikatiP := sumnjiviDuplikati(kandidatiP)
|
||||||
|
duplikatiS := sumnjiviDuplikati(kandidatiS)
|
||||||
|
for id, postoji := range postojiMapP {
|
||||||
if postoji {
|
if postoji {
|
||||||
continue
|
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++
|
p.SumnjiviDupli++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,13 @@
|
|||||||
Uvezi iz prodaje
|
Uvezi iz prodaje
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="POST" action="/pdv/kir/backfill-servisa" data-full-reload>
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
|
<button type="submit" class="btn-sekundarno" style="font-size:13px;padding:8px 16px;white-space:nowrap;"
|
||||||
|
data-potvrda="Uvesti sve preuzete servisne naloge koji još nisu u KIR-u?">
|
||||||
|
Uvezi iz servisa
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/pdv/kir/nova" class="btn-primarno" style="font-size:13px;padding:8px 16px;white-space:nowrap;">+ Nov izlazni račun</a>
|
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/pdv/kir/nova" class="btn-primarno" style="font-size:13px;padding:8px 16px;white-space:nowrap;">+ Nov izlazni račun</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user