From 45831467e0c2af78434a48b08d4da17955fcc5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Thu, 2 Jul 2026 10:35:24 +0200 Subject: [PATCH] =?UTF-8?q?Knjigovodstvo:=20storno=20prodaje/nabavke=20vi?= =?UTF-8?q?=C5=A1e=20ne=20bri=C5=A1e=20KIR/KPR/KPO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Storno je fizički brisao vezani KIR/KPR/KPO zapis — zakon o računovodstvu ne dozvoljava brisanje poslovnih knjiga (KPO dodatno ima redni broj bez prekida). Sad se original čuva, a storno se dograđuje kao poništavajuća stavka (negirani iznosi, referenca na original) — model.KirStorno/KprStorno/KpoStorno. Usput ispravljen i pravi uzrok prijavljenog bug-a: kirKandidatiProdaje je filtrirala samo internu mapu, ne i listu naloga koju obilazi KirBackfillProdaje — stornirani nalozi su zbog toga dobijali ponovni (pogrešan) KIR upis. --- internal/db/repository.go | 14 ++++-- internal/db/sqlite/kpo.go | 41 ++++++++++++++-- internal/db/sqlite/pdv_evidencija.go | 60 +++++++++++++++++++---- internal/db/sqlite/pdv_evidencija_test.go | 12 ++--- internal/handler/nabavka.go | 14 ++++-- internal/handler/prodaja.go | 28 ++++++++--- internal/handler/uskladjivanje.go | 10 +++- internal/model/kpo.go | 21 ++++++++ internal/model/pdv_evidencija.go | 57 +++++++++++++++++++++ internal/model/pdv_evidencija_test.go | 32 ++++++++++++ 10 files changed, 253 insertions(+), 36 deletions(-) diff --git a/internal/db/repository.go b/internal/db/repository.go index c19ce99..1e63310 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -66,8 +66,9 @@ type PdvKirRepository interface { DohvatiID(ctx context.Context, id int64) (*model.PdvKir, error) Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error) Obrisi(ctx context.Context, id int64) error - // ObrisiPoIzvoru briše zapise vezane za dati izvor (npr. pri stornu prodaje) - ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error + // DohvatiPoIzvoru vraća zapise vezane za dati izvor (npr. za storno prodaje — + // original se ne briše, storno stavka se dograđuje na osnovu ovih zapisa) + DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKir, error) // PostojiZaIzvor vraća true ako postoji bar jedan zapis za dati izvor i izvorID PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error) // PostojiPoBrojuDokumenta vraća true ako postoji zapis sa datim brojem dokumenta @@ -83,8 +84,9 @@ type PdvKprRepository interface { DohvatiID(ctx context.Context, id int64) (*model.PdvKpr, error) Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error) Obrisi(ctx context.Context, id int64) error - // ObrisiPoIzvoru briše zapise vezane za dati izvor (npr. pri brisanju nabavke) - ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error + // DohvatiPoIzvoru vraća zapise vezane za dati izvor (npr. za storno nabavke — + // original se ne briše, storno stavka se dograđuje na osnovu ovih zapisa) + DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKpr, error) // PostojiZaIzvor vraća true ako postoji bar jedan zapis za dati izvor i izvorID PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error) } @@ -413,5 +415,7 @@ type KpoRepository interface { Kreiraj(ctx context.Context, z *model.KpoZapis) (int64, error) Obrisi(ctx context.Context, id int64) error PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error) - ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error + // DohvatiPoIzvoru vraća zapise vezane za dati izvor (npr. za storno prodaje — + // original se ne briše, storno stavka se dograđuje na osnovu ovih zapisa) + DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.KpoZapis, error) } diff --git a/internal/db/sqlite/kpo.go b/internal/db/sqlite/kpo.go index c530e95..bb73d49 100644 --- a/internal/db/sqlite/kpo.go +++ b/internal/db/sqlite/kpo.go @@ -115,10 +115,41 @@ func (r *KpoRepo) PostojiZaIzvor(ctx context.Context, izvor string, izvorID int6 return n > 0, nil } -// ObrisiPoIzvoru briše KPO zapise vezane za dati izvor (npr. pri stornu prodaje). -func (r *KpoRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error { - if _, err := r.db.ExecContext(ctx, "DELETE FROM kpo_unosi WHERE izvor = ? AND izvor_id = ?", izvor, izvorID); err != nil { - return fmt.Errorf("ntech: KpoRepo.ObrisiPoIzvoru: %w", err) +// DohvatiPoIzvoru vraća sve KPO zapise vezane za dati izvor (npr. za storno prodaje). +func (r *KpoRepo) DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.KpoZapis, error) { + redovi, err := r.db.QueryContext(ctx, ` + SELECT id, datum_prometa, redni_broj, broj_dokumenta, opis, prihod, + nacin_placanja, napomena, izvor, izvor_id, datum_unosa + FROM kpo_unosi WHERE izvor = ? AND izvor_id = ?`, izvor, izvorID) + if err != nil { + return nil, fmt.Errorf("ntech: KpoRepo.DohvatiPoIzvoru: %w", err) } - return nil + defer redovi.Close() + + var rezultat []model.KpoZapis + for redovi.Next() { + var z model.KpoZapis + var opis, nacinPlacanja, napomena sql.NullString + var redniBroj sql.NullInt64 + var izvorIDCol sql.NullInt64 + + if err := redovi.Scan( + &z.ID, &z.DatumPrometa, &redniBroj, &z.BrojDokumenta, &opis, &z.Prihod, + &nacinPlacanja, &napomena, &z.Izvor, &izvorIDCol, &z.DatumUnosa, + ); err != nil { + return nil, fmt.Errorf("ntech: KpoRepo.DohvatiPoIzvoru: scan: %w", err) + } + if redniBroj.Valid { + n := int(redniBroj.Int64) + z.RedniBroj = &n + } + z.Opis = opis.String + z.NacinPlacanja = nacinPlacanja.String + z.Napomena = napomena.String + if izvorIDCol.Valid { + z.IzvorID = &izvorIDCol.Int64 + } + rezultat = append(rezultat, z) + } + return rezultat, redovi.Err() } diff --git a/internal/db/sqlite/pdv_evidencija.go b/internal/db/sqlite/pdv_evidencija.go index 666c668..412bff3 100644 --- a/internal/db/sqlite/pdv_evidencija.go +++ b/internal/db/sqlite/pdv_evidencija.go @@ -128,12 +128,32 @@ func (r *PdvKirRepo) Obrisi(ctx context.Context, id int64) error { return nil } -// ObrisiPoIzvoru briše KIR zapise vezane za dati izvor (npr. pri stornu/brisanju prodaje) -func (r *PdvKirRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error { - if _, err := r.db.ExecContext(ctx, "DELETE FROM pdv_kir WHERE izvor = ? AND izvor_id = ?", izvor, izvorID); err != nil { - return fmt.Errorf("ntech: PdvKirRepo.ObrisiPoIzvoru: %w", err) +// DohvatiPoIzvoru vraća sve KIR zapise vezane za dati izvor (npr. za storno prodaje) +func (r *PdvKirRepo) DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKir, error) { + redovi, err := r.db.QueryContext(ctx, ` + SELECT id, datum_prometa, datum_knjizenja, broj_dokumenta, + kupac_naziv, COALESCE(kupac_pib, ''), COALESCE(kupac_mesto, ''), + osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, + osloboden_sa_pravom, osloboden_bez_prava, ukupno, + COALESCE(napomena, ''), izvor, izvor_id, datum_unosa + FROM pdv_kir WHERE izvor = ? AND izvor_id = ?`, izvor, izvorID) + if err != nil { + return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiPoIzvoru: %w", err) } - return nil + defer redovi.Close() + + var rezultat []model.PdvKir + for redovi.Next() { + k, err := skenirajKir(redovi.Scan) + if err != nil { + return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiPoIzvoru: scan: %w", err) + } + rezultat = append(rezultat, k) + } + if err := redovi.Err(); err != nil { + return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiPoIzvoru: %w", err) + } + return rezultat, nil } // PostojiZaIzvor vraća true ako postoji bar jedan KIR zapis za dati izvor i izvorID @@ -318,12 +338,32 @@ func (r *PdvKprRepo) Obrisi(ctx context.Context, id int64) error { return nil } -// ObrisiPoIzvoru briše KPR zapise vezane za dati izvor (npr. pri brisanju nabavke) -func (r *PdvKprRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error { - if _, err := r.db.ExecContext(ctx, "DELETE FROM pdv_kpr WHERE izvor = ? AND izvor_id = ?", izvor, izvorID); err != nil { - return fmt.Errorf("ntech: PdvKprRepo.ObrisiPoIzvoru: %w", err) +// DohvatiPoIzvoru vraća sve KPR zapise vezane za dati izvor (npr. za storno nabavke) +func (r *PdvKprRepo) DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKpr, error) { + redovi, err := r.db.QueryContext(ctx, ` + SELECT id, datum_prometa, datum_knjizenja, datum_placanja, broj_dokumenta, + dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''), + osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, + pdv_bez_odbitka, osloboden_nabavka, ukupno, + COALESCE(napomena, ''), izvor, izvor_id, uvoz, datum_unosa + FROM pdv_kpr WHERE izvor = ? AND izvor_id = ?`, izvor, izvorID) + if err != nil { + return nil, fmt.Errorf("ntech: PdvKprRepo.DohvatiPoIzvoru: %w", err) } - return nil + defer redovi.Close() + + var rezultat []model.PdvKpr + for redovi.Next() { + k, err := skenirajKpr(redovi.Scan) + if err != nil { + return nil, fmt.Errorf("ntech: PdvKprRepo.DohvatiPoIzvoru: scan: %w", err) + } + rezultat = append(rezultat, k) + } + if err := redovi.Err(); err != nil { + return nil, fmt.Errorf("ntech: PdvKprRepo.DohvatiPoIzvoru: %w", err) + } + return rezultat, nil } // PostojiZaIzvor vraća true ako postoji bar jedan KPR zapis za dati izvor i izvorID diff --git a/internal/db/sqlite/pdv_evidencija_test.go b/internal/db/sqlite/pdv_evidencija_test.go index fff0c75..0d845e8 100644 --- a/internal/db/sqlite/pdv_evidencija_test.go +++ b/internal/db/sqlite/pdv_evidencija_test.go @@ -162,12 +162,12 @@ func TestPdvKirIzvor(t *testing.T) { } } - // ObrisiPoIzvoru briše samo vezani auto zapis, ručni ostaje - if err := repo.ObrisiPoIzvoru(ctx, "prodaja", 7); err != nil { - t.Fatalf("ObrisiPoIzvoru: %v", err) + // DohvatiPoIzvoru vraća samo vezani auto zapis + poIzvoru, err := repo.DohvatiPoIzvoru(ctx, "prodaja", 7) + if err != nil { + t.Fatalf("DohvatiPoIzvoru: %v", err) } - preostali, _ := repo.Lista(ctx, time.Time{}, time.Time{}) - if len(preostali) != 1 || preostali[0].BrojDokumenta != "R-1" { - t.Errorf("posle ObrisiPoIzvoru očekivan samo ručni zapis, dobijeno %d", len(preostali)) + if len(poIzvoru) != 1 || poIzvoru[0].BrojDokumenta != "P-1" { + t.Errorf("očekivan samo auto zapis P-1, dobijeno %d", len(poIzvoru)) } } diff --git a/internal/handler/nabavka.go b/internal/handler/nabavka.go index da253f1..02451bd 100644 --- a/internal/handler/nabavka.go +++ b/internal/handler/nabavka.go @@ -377,9 +377,17 @@ func (h *Handler) StornoNabavke(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?greska=storno", http.StatusSeeOther) return } - // stornirana nabavka ne ulazi u PDV — ukloni vezani auto-KPR zapis - if err := h.PdvKprRepo.ObrisiPoIzvoru(r.Context(), "nabavka", id); err != nil { - slog.Error("brisanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err) + // stornirana nabavka se ne briše iz KPR (zakon o računovodstvu ne dozvoljava + // brisanje poslovnih knjiga) — dograđuje se storno stavka koja poništava original + if zapisi, err := h.PdvKprRepo.DohvatiPoIzvoru(r.Context(), "nabavka", id); err != nil { + slog.Error("čitanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err) + } else { + for _, z := range zapisi { + storno := model.KprStorno(z, razlog, time.Now()) + if _, e := h.PdvKprRepo.Kreiraj(r.Context(), &storno); e != nil { + slog.Error("upis storno KPR zapisa nije uspeo", "nabavka_id", id, "error", e) + } + } } http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther) diff --git a/internal/handler/prodaja.go b/internal/handler/prodaja.go index a4fcad0..d22e4b0 100644 --- a/internal/handler/prodaja.go +++ b/internal/handler/prodaja.go @@ -687,9 +687,17 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string, return err } - // stornirana prodaja ne ulazi u PDV — ukloni vezani auto-KIR zapis - if err := h.PdvKirRepo.ObrisiPoIzvoru(ctx, "prodaja", id); err != nil { - slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err) + // stornirana prodaja se ne briše iz KIR (zakon o računovodstvu ne dozvoljava + // brisanje poslovnih knjiga) — dograđuje se storno stavka koja poništava original + if zapisi, err := h.PdvKirRepo.DohvatiPoIzvoru(ctx, "prodaja", id); err != nil { + slog.Error("čitanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err) + } else { + for _, z := range zapisi { + storno := model.KirStorno(z, razlog, time.Now()) + if _, e := h.PdvKirRepo.Kreiraj(ctx, &storno); e != nil { + slog.Error("upis storno KIR zapisa nije uspeo", "prodaja_id", id, "error", e) + } + } } // fiskalni refund — best-effort @@ -744,10 +752,18 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string, } } - // brisanje KPO zapisa na storno + // stornirana prodaja se ne briše iz KPO (redni broj mora ostati bez prekida) — + // dograđuje se storno stavka koja poništava original if h.modulUkljucen(ctx, "kpo") { - if e := h.KpoRepo.ObrisiPoIzvoru(ctx, "prodaja", id); e != nil { - slog.Error("brisanje vezanog KPO zapisa nije uspelo", "prodaja_id", id, "error", e) + if zapisi, e := h.KpoRepo.DohvatiPoIzvoru(ctx, "prodaja", id); e != nil { + slog.Error("čitanje vezanog KPO zapisa nije uspelo", "prodaja_id", id, "error", e) + } else { + for _, z := range zapisi { + storno := model.KpoStorno(z, razlog, time.Now()) + if _, e := h.KpoRepo.Kreiraj(ctx, &storno); e != nil { + slog.Error("upis storno KPO zapisa nije uspeo", "prodaja_id", id, "error", e) + } + } } } diff --git a/internal/handler/uskladjivanje.go b/internal/handler/uskladjivanje.go index 77bbb23..d222934 100644 --- a/internal/handler/uskladjivanje.go +++ b/internal/handler/uskladjivanje.go @@ -59,10 +59,18 @@ func sumnjiviDuplikati(svi []kandidatUskladjivanja) map[int64]bool { // 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{}) + svi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{}) if err != nil { return nil, nil, nil, err } + // stornirani nalozi se nikad ne razmatraju za KIR upis — ni za backfill ni za + // brojanje praznina — jer stornirana prodaja ne predstavlja oporeziv promet + var nalozi []model.ProdajniNalogSaDetaljem + for _, nd := range svi { + if !nd.Stornirano { + nalozi = append(nalozi, nd) + } + } postojiMap := make(map[int64]bool, len(nalozi)) var kandidati []kandidatUskladjivanja for _, nd := range nalozi { diff --git a/internal/model/kpo.go b/internal/model/kpo.go index 45be523..eeab59c 100644 --- a/internal/model/kpo.go +++ b/internal/model/kpo.go @@ -22,3 +22,24 @@ type KpoSume struct { Prihod float64 Broj int } + +// KpoStorno gradi storno zapis za dati KPO red: negiran prihod, upisan pod istim +// izvorom/izvor_id kao original. Knjiga o ostvarenom prometu paušalaca vodi redni +// broj bez prekida (Pravilnik) — original se nikad ne briše, storno se dodaje kao +// poništavajuća stavka koja upućuje na original. +func KpoStorno(original KpoZapis, razlog string, datumStorna time.Time) KpoZapis { + napomena := "Storno naloga " + original.BrojDokumenta + if razlog != "" { + napomena += ": " + razlog + } + return KpoZapis{ + DatumPrometa: datumStorna, + BrojDokumenta: "STORNO-" + original.BrojDokumenta, + Opis: original.Opis, + Prihod: -original.Prihod, + NacinPlacanja: original.NacinPlacanja, + Napomena: napomena, + Izvor: original.Izvor, + IzvorID: original.IzvorID, + } +} diff --git a/internal/model/pdv_evidencija.go b/internal/model/pdv_evidencija.go index 2e34246..8178001 100644 --- a/internal/model/pdv_evidencija.go +++ b/internal/model/pdv_evidencija.go @@ -129,6 +129,36 @@ func KirIzProdaje(nalog ProdajniNalog, stavke []StavkaProdaje, kupacNaziv, kupac return k } +// KirStorno gradi storno (knjižno odobrenje) zapis za dati KIR red: iznosi su +// negirani original, upisan pod istim izvorom/izvor_id tako da PostojiZaIzvor +// ostane tačan i backfill ne pokuša ponovni upis. Zakon o računovodstvu ne +// dozvoljava brisanje knjigovodstvenih evidencija — storno se knjiži kao +// poništavajuća stavka koja upućuje na original, a ne kao brisanje reda. +func KirStorno(original PdvKir, razlog string, datumStorna time.Time) PdvKir { + napomena := "Storno računa " + original.BrojDokumenta + if razlog != "" { + napomena += ": " + razlog + } + return PdvKir{ + DatumPrometa: datumStorna, + DatumKnjizenja: datumStorna, + BrojDokumenta: "STORNO-" + original.BrojDokumenta, + KupacNaziv: original.KupacNaziv, + KupacPib: original.KupacPib, + KupacMesto: original.KupacMesto, + OsnovicaOpsta: -original.OsnovicaOpsta, + PdvOpsta: -original.PdvOpsta, + OsnovicaPosebna: -original.OsnovicaPosebna, + PdvPosebna: -original.PdvPosebna, + OslobodenSaPravom: -original.OslobodenSaPravom, + OslobodenBezPrava: -original.OslobodenBezPrava, + Ukupno: -original.Ukupno, + Napomena: napomena, + Izvor: original.Izvor, + IzvorID: original.IzvorID, + } +} + // PdvKpr je jedan zapis u knjizi primljenih računa (ulazni PDV). type PdvKpr struct { ID int64 @@ -153,6 +183,33 @@ type PdvKpr struct { DatumUnosa time.Time } +// KprStorno gradi storno zapis za dati KPR red — vidi KirStorno. +func KprStorno(original PdvKpr, razlog string, datumStorna time.Time) PdvKpr { + napomena := "Storno dokumenta " + original.BrojDokumenta + if razlog != "" { + napomena += ": " + razlog + } + return PdvKpr{ + DatumPrometa: datumStorna, + DatumKnjizenja: datumStorna, + BrojDokumenta: "STORNO-" + original.BrojDokumenta, + DobavljacNaziv: original.DobavljacNaziv, + DobavljacPib: original.DobavljacPib, + DobavljacMesto: original.DobavljacMesto, + OsnovicaOpsta: -original.OsnovicaOpsta, + PdvOpsta: -original.PdvOpsta, + OsnovicaPosebna: -original.OsnovicaPosebna, + PdvPosebna: -original.PdvPosebna, + PdvBezOdbitka: -original.PdvBezOdbitka, + OslobodenNabavka: -original.OslobodenNabavka, + Ukupno: -original.Ukupno, + Napomena: napomena, + Izvor: original.Izvor, + IzvorID: original.IzvorID, + Uvoz: original.Uvoz, + } +} + // OznakaPoreskogBroja vraća „JMBG" za 13-cifreni broj, inače „PIB" (dobavljači su obično firme). func (k PdvKpr) OznakaPoreskogBroja() string { cifre := 0 diff --git a/internal/model/pdv_evidencija_test.go b/internal/model/pdv_evidencija_test.go index c82f0d8..c206195 100644 --- a/internal/model/pdv_evidencija_test.go +++ b/internal/model/pdv_evidencija_test.go @@ -70,6 +70,38 @@ func TestPdvKirDodajNeto(t *testing.T) { } } +// TestKirStorno: storno stavka negira sve kolone originala, čuva izvor/izvor_id +// (da PostojiZaIzvor ostane tačan i backfill ne pokuša ponovni upis) i original +// se ne dira — storno se samo dograđuje kao novi red. +func TestKirStorno(t *testing.T) { + izvorID := int64(18) + original := PdvKir{ + BrojDokumenta: "PR-2607-0013", KupacNaziv: "Kupac doo", KupacPib: "123456789", + OsnovicaOpsta: 1000, PdvOpsta: 200, OsnovicaPosebna: 100, PdvPosebna: 10, + OslobodenSaPravom: 50, OslobodenBezPrava: 20, Ukupno: 1380, + Izvor: "prodaja", IzvorID: &izvorID, + } + datumStorna := time.Date(2026, 7, 2, 0, 0, 0, 0, time.UTC) + + s := KirStorno(original, "kupac odustao", datumStorna) + + if s.BrojDokumenta != "STORNO-PR-2607-0013" { + t.Errorf("broj_dokumenta=%q, očekivano STORNO-PR-2607-0013", s.BrojDokumenta) + } + if s.Izvor != "prodaja" || s.IzvorID == nil || *s.IzvorID != 18 { + t.Errorf("izvor=%q izvor_id=%v, očekivano prodaja/18 (isto kao original)", s.Izvor, s.IzvorID) + } + if !blizu(s.OsnovicaOpsta, -1000) || !blizu(s.PdvOpsta, -200) { + t.Errorf("opšta: osnovica=%v pdv=%v, očekivano -1000/-200", s.OsnovicaOpsta, s.PdvOpsta) + } + if !blizu(s.Ukupno, -1380) { + t.Errorf("ukupno=%v, očekivano -1380", s.Ukupno) + } + if original.Ukupno != 1380 { + t.Errorf("original izmenjen: ukupno=%v, očekivano da ostane 1380", original.Ukupno) + } +} + func TestKprIzNabavke(t *testing.T) { nabavka := Nabavka{ID: 3, Napomena: "test", Datum: time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)} stavke := []NabavkaStavkaPdv{