Knjigovodstvo: storno prodaje/nabavke više ne briše KIR/KPR/KPO

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.
This commit is contained in:
2026-07-02 10:35:24 +02:00
parent 3a0f98622b
commit 45831467e0
10 changed files with 253 additions and 36 deletions
+9 -5
View File
@@ -66,8 +66,9 @@ type PdvKirRepository interface {
DohvatiID(ctx context.Context, id int64) (*model.PdvKir, error) DohvatiID(ctx context.Context, id int64) (*model.PdvKir, error)
Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error) Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error)
Obrisi(ctx context.Context, id int64) error Obrisi(ctx context.Context, id int64) error
// ObrisiPoIzvoru briše zapise vezane za dati izvor (npr. pri stornu prodaje) // DohvatiPoIzvoru vraća zapise vezane za dati izvor (npr. za storno prodaje
ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error // 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 vraća true ako postoji bar jedan zapis za dati izvor i izvorID
PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error) PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error)
// PostojiPoBrojuDokumenta vraća true ako postoji zapis sa datim brojem dokumenta // 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) DohvatiID(ctx context.Context, id int64) (*model.PdvKpr, error)
Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error) Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error)
Obrisi(ctx context.Context, id int64) error Obrisi(ctx context.Context, id int64) error
// ObrisiPoIzvoru briše zapise vezane za dati izvor (npr. pri brisanju nabavke) // DohvatiPoIzvoru vraća zapise vezane za dati izvor (npr. za storno nabavke
ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error // 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 vraća true ako postoji bar jedan zapis za dati izvor i izvorID
PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error) 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) Kreiraj(ctx context.Context, z *model.KpoZapis) (int64, error)
Obrisi(ctx context.Context, id int64) error Obrisi(ctx context.Context, id int64) error
PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, 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)
} }
+36 -5
View File
@@ -115,10 +115,41 @@ func (r *KpoRepo) PostojiZaIzvor(ctx context.Context, izvor string, izvorID int6
return n > 0, nil return n > 0, nil
} }
// ObrisiPoIzvoru briše KPO zapise vezane za dati izvor (npr. pri stornu prodaje). // DohvatiPoIzvoru vraća sve KPO zapise vezane za dati izvor (npr. za storno prodaje).
func (r *KpoRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error { func (r *KpoRepo) DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.KpoZapis, error) {
if _, err := r.db.ExecContext(ctx, "DELETE FROM kpo_unosi WHERE izvor = ? AND izvor_id = ?", izvor, izvorID); err != nil { redovi, err := r.db.QueryContext(ctx, `
return fmt.Errorf("ntech: KpoRepo.ObrisiPoIzvoru: %w", err) 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()
} }
+50 -10
View File
@@ -128,12 +128,32 @@ func (r *PdvKirRepo) Obrisi(ctx context.Context, id int64) error {
return nil return nil
} }
// ObrisiPoIzvoru briše KIR zapise vezane za dati izvor (npr. pri stornu/brisanju prodaje) // DohvatiPoIzvoru vraća sve KIR zapise vezane za dati izvor (npr. za storno prodaje)
func (r *PdvKirRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error { func (r *PdvKirRepo) DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKir, error) {
if _, err := r.db.ExecContext(ctx, "DELETE FROM pdv_kir WHERE izvor = ? AND izvor_id = ?", izvor, izvorID); err != nil { redovi, err := r.db.QueryContext(ctx, `
return fmt.Errorf("ntech: PdvKirRepo.ObrisiPoIzvoru: %w", err) 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 // 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 return nil
} }
// ObrisiPoIzvoru briše KPR zapise vezane za dati izvor (npr. pri brisanju nabavke) // DohvatiPoIzvoru vraća sve KPR zapise vezane za dati izvor (npr. za storno nabavke)
func (r *PdvKprRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error { func (r *PdvKprRepo) DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKpr, error) {
if _, err := r.db.ExecContext(ctx, "DELETE FROM pdv_kpr WHERE izvor = ? AND izvor_id = ?", izvor, izvorID); err != nil { redovi, err := r.db.QueryContext(ctx, `
return fmt.Errorf("ntech: PdvKprRepo.ObrisiPoIzvoru: %w", err) 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 // PostojiZaIzvor vraća true ako postoji bar jedan KPR zapis za dati izvor i izvorID
+6 -6
View File
@@ -162,12 +162,12 @@ func TestPdvKirIzvor(t *testing.T) {
} }
} }
// ObrisiPoIzvoru briše samo vezani auto zapis, ručni ostaje // DohvatiPoIzvoru vraća samo vezani auto zapis
if err := repo.ObrisiPoIzvoru(ctx, "prodaja", 7); err != nil { poIzvoru, err := repo.DohvatiPoIzvoru(ctx, "prodaja", 7)
t.Fatalf("ObrisiPoIzvoru: %v", err) if err != nil {
t.Fatalf("DohvatiPoIzvoru: %v", err)
} }
preostali, _ := repo.Lista(ctx, time.Time{}, time.Time{}) if len(poIzvoru) != 1 || poIzvoru[0].BrojDokumenta != "P-1" {
if len(preostali) != 1 || preostali[0].BrojDokumenta != "R-1" { t.Errorf("očekivan samo auto zapis P-1, dobijeno %d", len(poIzvoru))
t.Errorf("posle ObrisiPoIzvoru očekivan samo ručni zapis, dobijeno %d", len(preostali))
} }
} }
+11 -3
View File
@@ -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) http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?greska=storno", http.StatusSeeOther)
return return
} }
// stornirana nabavka ne ulazi u PDV — ukloni vezani auto-KPR zapis // stornirana nabavka se ne briše iz KPR (zakon o računovodstvu ne dozvoljava
if err := h.PdvKprRepo.ObrisiPoIzvoru(r.Context(), "nabavka", id); err != nil { // brisanje poslovnih knjiga) — dograđuje se storno stavka koja poništava original
slog.Error("brisanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err) 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) http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
+22 -6
View File
@@ -687,9 +687,17 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string,
return err return err
} }
// stornirana prodaja ne ulazi u PDV — ukloni vezani auto-KIR zapis // stornirana prodaja se ne briše iz KIR (zakon o računovodstvu ne dozvoljava
if err := h.PdvKirRepo.ObrisiPoIzvoru(ctx, "prodaja", id); err != nil { // brisanje poslovnih knjiga) — dograđuje se storno stavka koja poništava original
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err) 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 // 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 h.modulUkljucen(ctx, "kpo") {
if e := h.KpoRepo.ObrisiPoIzvoru(ctx, "prodaja", id); e != nil { if zapisi, e := h.KpoRepo.DohvatiPoIzvoru(ctx, "prodaja", id); e != nil {
slog.Error("brisanje vezanog KPO zapisa nije uspelo", "prodaja_id", id, "error", e) 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)
}
}
} }
} }
+9 -1
View File
@@ -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 // 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. // 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) { 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 { if err != nil {
return nil, nil, nil, err 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)) postojiMap := make(map[int64]bool, len(nalozi))
var kandidati []kandidatUskladjivanja var kandidati []kandidatUskladjivanja
for _, nd := range nalozi { for _, nd := range nalozi {
+21
View File
@@ -22,3 +22,24 @@ type KpoSume struct {
Prihod float64 Prihod float64
Broj int 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,
}
}
+57
View File
@@ -129,6 +129,36 @@ func KirIzProdaje(nalog ProdajniNalog, stavke []StavkaProdaje, kupacNaziv, kupac
return k 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). // PdvKpr je jedan zapis u knjizi primljenih računa (ulazni PDV).
type PdvKpr struct { type PdvKpr struct {
ID int64 ID int64
@@ -153,6 +183,33 @@ type PdvKpr struct {
DatumUnosa time.Time 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). // OznakaPoreskogBroja vraća „JMBG" za 13-cifreni broj, inače „PIB" (dobavljači su obično firme).
func (k PdvKpr) OznakaPoreskogBroja() string { func (k PdvKpr) OznakaPoreskogBroja() string {
cifre := 0 cifre := 0
+32
View File
@@ -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) { func TestKprIzNabavke(t *testing.T) {
nabavka := Nabavka{ID: 3, Napomena: "test", Datum: time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)} nabavka := Nabavka{ID: 3, Napomena: "test", Datum: time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)}
stavke := []NabavkaStavkaPdv{ stavke := []NabavkaStavkaPdv{