From 1539ec799f082410db2086c4a064e7cccac1f99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Sun, 14 Jun 2026 02:27:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(pdv):=20izvor=20veza=20u=20KIR/KPR=20?= =?UTF-8?q?=E2=80=94=20temelj=20za=20auto-punjenje=20(Faza=202b-0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kolone izvor ('rucno'/'prodaja'/'nabavka') + izvor_id na pdv_kir/pdv_kpr (migracija 044, postojeći zapisi → 'rucno'). Repo Kreiraj upisuje izvor, nova ObrisiPoIzvoru za čišćenje pri stornu/brisanju izvora. Test. --- internal/db/repository.go | 4 + internal/db/sqlite/pdv_evidencija.go | 101 ++++++++++++++++------ internal/db/sqlite/pdv_evidencija_test.go | 44 ++++++++++ internal/model/pdv_evidencija.go | 4 + migrations/044_kir_kpr_izvor.sql | 8 ++ 5 files changed, 133 insertions(+), 28 deletions(-) create mode 100644 migrations/044_kir_kpr_izvor.sql diff --git a/internal/db/repository.go b/internal/db/repository.go index 0cfc3e1..aa82b94 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -38,6 +38,8 @@ 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 } // PdvKprRepository definiše operacije nad knjigom primljenih računa (KPR) @@ -46,6 +48,8 @@ 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 } // ArtikalFilter definiše parametre za filtriranje liste artikala diff --git a/internal/db/sqlite/pdv_evidencija.go b/internal/db/sqlite/pdv_evidencija.go index 8e2b470..8f620c0 100644 --- a/internal/db/sqlite/pdv_evidencija.go +++ b/internal/db/sqlite/pdv_evidencija.go @@ -28,7 +28,7 @@ func (r *PdvKirRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKi 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, ''), datum_unosa + COALESCE(napomena, ''), izvor, izvor_id, datum_unosa FROM pdv_kir WHERE 1=1` args := []any{} if !od.IsZero() { @@ -49,14 +49,8 @@ func (r *PdvKirRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKi var rezultat []model.PdvKir for redovi.Next() { - var k model.PdvKir - if err := redovi.Scan( - &k.ID, &k.DatumPrometa, &k.DatumKnjizenja, &k.BrojDokumenta, - &k.KupacNaziv, &k.KupacPib, &k.KupacMesto, - &k.OsnovicaOpsta, &k.PdvOpsta, &k.OsnovicaPosebna, &k.PdvPosebna, - &k.OslobodenSaPravom, &k.OslobodenBezPrava, &k.Ukupno, - &k.Napomena, &k.DatumUnosa, - ); err != nil { + k, err := skenirajKir(redovi.Scan) + if err != nil { return nil, fmt.Errorf("ntech: PdvKirRepo.Lista: scan: %w", err) } rezultat = append(rezultat, k) @@ -69,26 +63,39 @@ func (r *PdvKirRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKi // DohvatiID vraća jedan zapis KIR-a po identifikatoru func (r *PdvKirRepo) DohvatiID(ctx context.Context, id int64) (*model.PdvKir, error) { - var k model.PdvKir - err := r.db.QueryRowContext(ctx, ` + red := r.db.QueryRowContext(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, ''), datum_unosa - FROM pdv_kir WHERE id = ?`, id).Scan( - &k.ID, &k.DatumPrometa, &k.DatumKnjizenja, &k.BrojDokumenta, - &k.KupacNaziv, &k.KupacPib, &k.KupacMesto, - &k.OsnovicaOpsta, &k.PdvOpsta, &k.OsnovicaPosebna, &k.PdvPosebna, - &k.OslobodenSaPravom, &k.OslobodenBezPrava, &k.Ukupno, - &k.Napomena, &k.DatumUnosa, - ) + COALESCE(napomena, ''), izvor, izvor_id, datum_unosa + FROM pdv_kir WHERE id = ?`, id) + k, err := skenirajKir(red.Scan) if err != nil { return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiID: %w", err) } return &k, nil } +// skenirajKir čita jedan red KIR-a; izvor_id je nullable pa ide preko sql.NullInt64 +func skenirajKir(scan func(...any) error) (model.PdvKir, error) { + var k model.PdvKir + var izvorID sql.NullInt64 + if err := scan( + &k.ID, &k.DatumPrometa, &k.DatumKnjizenja, &k.BrojDokumenta, + &k.KupacNaziv, &k.KupacPib, &k.KupacMesto, + &k.OsnovicaOpsta, &k.PdvOpsta, &k.OsnovicaPosebna, &k.PdvPosebna, + &k.OslobodenSaPravom, &k.OslobodenBezPrava, &k.Ukupno, + &k.Napomena, &k.Izvor, &izvorID, &k.DatumUnosa, + ); err != nil { + return model.PdvKir{}, err + } + if izvorID.Valid { + k.IzvorID = &izvorID.Int64 + } + return k, nil +} + // Kreiraj dodaje novi zapis u KIR i vraća njegov id func (r *PdvKirRepo) Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error) { rez, err := r.db.ExecContext(ctx, ` @@ -96,12 +103,13 @@ func (r *PdvKirRepo) Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error datum_prometa, datum_knjizenja, broj_dokumenta, kupac_naziv, kupac_pib, kupac_mesto, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, - osloboden_sa_pravom, osloboden_bez_prava, ukupno, napomena - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + osloboden_sa_pravom, osloboden_bez_prava, ukupno, napomena, izvor, izvor_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, k.DatumPrometa, k.DatumKnjizenja, k.BrojDokumenta, k.KupacNaziv, k.KupacPib, k.KupacMesto, k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna, - k.OslobodenSaPravom, k.OslobodenBezPrava, k.Ukupno, k.Napomena) + k.OslobodenSaPravom, k.OslobodenBezPrava, k.Ukupno, k.Napomena, + izvorIliRucno(k.Izvor), izvorIDArg(k.IzvorID)) if err != nil { return 0, fmt.Errorf("ntech: PdvKirRepo.Kreiraj: %w", err) } @@ -120,6 +128,30 @@ 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) + } + return nil +} + +// izvorIliRucno vraća izvor ili podrazumevano „rucno" (izvor kolona je NOT NULL) +func izvorIliRucno(izvor string) string { + if izvor == "" { + return "rucno" + } + return izvor +} + +// izvorIDArg pretvara *int64 u argument za upit (nil → NULL) +func izvorIDArg(id *int64) any { + if id == nil { + return nil + } + return *id +} + // --- KPR: knjiga primljenih računa --- // PdvKprRepo je SQLite implementacija PdvKprRepository interfejsa @@ -139,7 +171,7 @@ func (r *PdvKprRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKp dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''), osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, pdv_bez_odbitka, osloboden_nabavka, ukupno, - COALESCE(napomena, ''), datum_unosa + COALESCE(napomena, ''), izvor, izvor_id, datum_unosa FROM pdv_kpr WHERE 1=1` args := []any{} if !od.IsZero() { @@ -179,7 +211,7 @@ func (r *PdvKprRepo) DohvatiID(ctx context.Context, id int64) (*model.PdvKpr, er dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''), osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, pdv_bez_odbitka, osloboden_nabavka, ukupno, - COALESCE(napomena, ''), datum_unosa + COALESCE(napomena, ''), izvor, izvor_id, datum_unosa FROM pdv_kpr WHERE id = ?`, id) k, err := skenirajKpr(red.Scan) if err != nil { @@ -192,18 +224,22 @@ func (r *PdvKprRepo) DohvatiID(ctx context.Context, id int64) (*model.PdvKpr, er func skenirajKpr(scan func(...any) error) (model.PdvKpr, error) { var k model.PdvKpr var datumPlacanja sql.NullTime + var izvorID sql.NullInt64 if err := scan( &k.ID, &k.DatumPrometa, &k.DatumKnjizenja, &datumPlacanja, &k.BrojDokumenta, &k.DobavljacNaziv, &k.DobavljacPib, &k.DobavljacMesto, &k.OsnovicaOpsta, &k.PdvOpsta, &k.OsnovicaPosebna, &k.PdvPosebna, &k.PdvBezOdbitka, &k.OslobodenNabavka, &k.Ukupno, - &k.Napomena, &k.DatumUnosa, + &k.Napomena, &k.Izvor, &izvorID, &k.DatumUnosa, ); err != nil { return model.PdvKpr{}, err } if datumPlacanja.Valid { k.DatumPlacanja = &datumPlacanja.Time } + if izvorID.Valid { + k.IzvorID = &izvorID.Int64 + } return k, nil } @@ -218,12 +254,13 @@ func (r *PdvKprRepo) Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error datum_prometa, datum_knjizenja, datum_placanja, broj_dokumenta, dobavljac_naziv, dobavljac_pib, dobavljac_mesto, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, - pdv_bez_odbitka, osloboden_nabavka, ukupno, napomena - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + pdv_bez_odbitka, osloboden_nabavka, ukupno, napomena, izvor, izvor_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, k.DatumPrometa, k.DatumKnjizenja, datumPlacanja, k.BrojDokumenta, k.DobavljacNaziv, k.DobavljacPib, k.DobavljacMesto, k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna, - k.PdvBezOdbitka, k.OslobodenNabavka, k.Ukupno, k.Napomena) + k.PdvBezOdbitka, k.OslobodenNabavka, k.Ukupno, k.Napomena, + izvorIliRucno(k.Izvor), izvorIDArg(k.IzvorID)) if err != nil { return 0, fmt.Errorf("ntech: PdvKprRepo.Kreiraj: %w", err) } @@ -241,3 +278,11 @@ 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) + } + return nil +} diff --git a/internal/db/sqlite/pdv_evidencija_test.go b/internal/db/sqlite/pdv_evidencija_test.go index 2397a11..fff0c75 100644 --- a/internal/db/sqlite/pdv_evidencija_test.go +++ b/internal/db/sqlite/pdv_evidencija_test.go @@ -127,3 +127,47 @@ func TestPdvKprRepo(t *testing.T) { } }) } + +func TestPdvKirIzvor(t *testing.T) { + db := testDB(t) + repo := NoviPdvKirRepo(db) + ctx := context.Background() + dp := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC) + izvorID := int64(7) + + // ručni zapis (bez izvora) → default „rucno", izvor_id NULL + if _, err := repo.Kreiraj(ctx, &model.PdvKir{ + DatumPrometa: dp, DatumKnjizenja: dp, BrojDokumenta: "R-1", KupacNaziv: "Ručni", + }); err != nil { + t.Fatalf("Kreiraj ručni: %v", err) + } + // auto zapis iz prodaje + if _, err := repo.Kreiraj(ctx, &model.PdvKir{ + DatumPrometa: dp, DatumKnjizenja: dp, BrojDokumenta: "P-1", KupacNaziv: "Iz prodaje", + Izvor: "prodaja", IzvorID: &izvorID, + }); err != nil { + t.Fatalf("Kreiraj auto: %v", err) + } + + sve, _ := repo.Lista(ctx, time.Time{}, time.Time{}) + if len(sve) != 2 { + t.Fatalf("očekivano 2 zapisa, dobijeno %d", len(sve)) + } + for _, z := range sve { + if z.BrojDokumenta == "R-1" && (z.Izvor != "rucno" || z.IzvorID != nil) { + t.Errorf("ručni zapis: izvor=%q izvor_id=%v, očekivano rucno/nil", z.Izvor, z.IzvorID) + } + if z.BrojDokumenta == "P-1" && (z.Izvor != "prodaja" || z.IzvorID == nil || *z.IzvorID != 7) { + t.Errorf("auto zapis: izvor=%q izvor_id=%v, očekivano prodaja/7", z.Izvor, z.IzvorID) + } + } + + // ObrisiPoIzvoru briše samo vezani auto zapis, ručni ostaje + if err := repo.ObrisiPoIzvoru(ctx, "prodaja", 7); err != nil { + t.Fatalf("ObrisiPoIzvoru: %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)) + } +} diff --git a/internal/model/pdv_evidencija.go b/internal/model/pdv_evidencija.go index c9f8204..6cd161f 100644 --- a/internal/model/pdv_evidencija.go +++ b/internal/model/pdv_evidencija.go @@ -20,6 +20,8 @@ type PdvKir struct { OslobodenBezPrava float64 Ukupno float64 Napomena string + Izvor string // "rucno" | "prodaja" | "nabavka" + IzvorID *int64 // id izvornog naloga (nil za ručni unos) DatumUnosa time.Time } @@ -92,6 +94,8 @@ type PdvKpr struct { OslobodenNabavka float64 Ukupno float64 Napomena string + Izvor string // "rucno" | "prodaja" | "nabavka" + IzvorID *int64 // id izvorne nabavke (nil za ručni unos) DatumUnosa time.Time } diff --git a/migrations/044_kir_kpr_izvor.sql b/migrations/044_kir_kpr_izvor.sql new file mode 100644 index 0000000..d756047 --- /dev/null +++ b/migrations/044_kir_kpr_izvor.sql @@ -0,0 +1,8 @@ +-- Veza KIR/KPR zapisa sa izvorom (Faza 2b — automatsko punjenje). +-- izvor: 'rucno' (ručni unos), 'prodaja', 'nabavka'; izvor_id = id naloga/nabavke. +-- Postojeći zapisi dobijaju 'rucno' (DEFAULT). Omogućava brisanje vezanog zapisa +-- pri stornu/brisanju izvora i sprečava duplikate. +ALTER TABLE pdv_kir ADD COLUMN izvor TEXT NOT NULL DEFAULT 'rucno'; +ALTER TABLE pdv_kir ADD COLUMN izvor_id INTEGER; +ALTER TABLE pdv_kpr ADD COLUMN izvor TEXT NOT NULL DEFAULT 'rucno'; +ALTER TABLE pdv_kpr ADD COLUMN izvor_id INTEGER;