feat(pdv): izvor veza u KIR/KPR — temelj za auto-punjenje (Faza 2b-0)

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.
This commit is contained in:
2026-06-14 02:27:23 +02:00
parent 966d1f6c98
commit 1539ec799f
5 changed files with 133 additions and 28 deletions
+4
View File
@@ -38,6 +38,8 @@ 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)
ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error
} }
// PdvKprRepository definiše operacije nad knjigom primljenih računa (KPR) // 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) 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)
ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error
} }
// ArtikalFilter definiše parametre za filtriranje liste artikala // ArtikalFilter definiše parametre za filtriranje liste artikala
+73 -28
View File
@@ -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, ''), kupac_naziv, COALESCE(kupac_pib, ''), COALESCE(kupac_mesto, ''),
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
osloboden_sa_pravom, osloboden_bez_prava, ukupno, osloboden_sa_pravom, osloboden_bez_prava, ukupno,
COALESCE(napomena, ''), datum_unosa COALESCE(napomena, ''), izvor, izvor_id, datum_unosa
FROM pdv_kir WHERE 1=1` FROM pdv_kir WHERE 1=1`
args := []any{} args := []any{}
if !od.IsZero() { if !od.IsZero() {
@@ -49,14 +49,8 @@ func (r *PdvKirRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKi
var rezultat []model.PdvKir var rezultat []model.PdvKir
for redovi.Next() { for redovi.Next() {
var k model.PdvKir k, err := skenirajKir(redovi.Scan)
if err := redovi.Scan( if err != nil {
&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 {
return nil, fmt.Errorf("ntech: PdvKirRepo.Lista: scan: %w", err) return nil, fmt.Errorf("ntech: PdvKirRepo.Lista: scan: %w", err)
} }
rezultat = append(rezultat, k) 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 // DohvatiID vraća jedan zapis KIR-a po identifikatoru
func (r *PdvKirRepo) DohvatiID(ctx context.Context, id int64) (*model.PdvKir, error) { func (r *PdvKirRepo) DohvatiID(ctx context.Context, id int64) (*model.PdvKir, error) {
var k model.PdvKir red := r.db.QueryRowContext(ctx, `
err := r.db.QueryRowContext(ctx, `
SELECT id, datum_prometa, datum_knjizenja, broj_dokumenta, SELECT id, datum_prometa, datum_knjizenja, broj_dokumenta,
kupac_naziv, COALESCE(kupac_pib, ''), COALESCE(kupac_mesto, ''), kupac_naziv, COALESCE(kupac_pib, ''), COALESCE(kupac_mesto, ''),
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
osloboden_sa_pravom, osloboden_bez_prava, ukupno, osloboden_sa_pravom, osloboden_bez_prava, ukupno,
COALESCE(napomena, ''), datum_unosa COALESCE(napomena, ''), izvor, izvor_id, datum_unosa
FROM pdv_kir WHERE id = ?`, id).Scan( FROM pdv_kir WHERE id = ?`, id)
&k.ID, &k.DatumPrometa, &k.DatumKnjizenja, &k.BrojDokumenta, k, err := skenirajKir(red.Scan)
&k.KupacNaziv, &k.KupacPib, &k.KupacMesto,
&k.OsnovicaOpsta, &k.PdvOpsta, &k.OsnovicaPosebna, &k.PdvPosebna,
&k.OslobodenSaPravom, &k.OslobodenBezPrava, &k.Ukupno,
&k.Napomena, &k.DatumUnosa,
)
if err != nil { if err != nil {
return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiID: %w", err) return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiID: %w", err)
} }
return &k, nil 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 // Kreiraj dodaje novi zapis u KIR i vraća njegov id
func (r *PdvKirRepo) Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error) { func (r *PdvKirRepo) Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error) {
rez, err := r.db.ExecContext(ctx, ` 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, datum_prometa, datum_knjizenja, broj_dokumenta,
kupac_naziv, kupac_pib, kupac_mesto, kupac_naziv, kupac_pib, kupac_mesto,
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
osloboden_sa_pravom, osloboden_bez_prava, ukupno, napomena osloboden_sa_pravom, osloboden_bez_prava, ukupno, napomena, izvor, izvor_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
k.DatumPrometa, k.DatumKnjizenja, k.BrojDokumenta, k.DatumPrometa, k.DatumKnjizenja, k.BrojDokumenta,
k.KupacNaziv, k.KupacPib, k.KupacMesto, k.KupacNaziv, k.KupacPib, k.KupacMesto,
k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna, 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 { if err != nil {
return 0, fmt.Errorf("ntech: PdvKirRepo.Kreiraj: %w", err) 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 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 --- // --- KPR: knjiga primljenih računa ---
// PdvKprRepo je SQLite implementacija PdvKprRepository interfejsa // 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, ''), dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''),
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
pdv_bez_odbitka, osloboden_nabavka, ukupno, pdv_bez_odbitka, osloboden_nabavka, ukupno,
COALESCE(napomena, ''), datum_unosa COALESCE(napomena, ''), izvor, izvor_id, datum_unosa
FROM pdv_kpr WHERE 1=1` FROM pdv_kpr WHERE 1=1`
args := []any{} args := []any{}
if !od.IsZero() { 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, ''), dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''),
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
pdv_bez_odbitka, osloboden_nabavka, ukupno, pdv_bez_odbitka, osloboden_nabavka, ukupno,
COALESCE(napomena, ''), datum_unosa COALESCE(napomena, ''), izvor, izvor_id, datum_unosa
FROM pdv_kpr WHERE id = ?`, id) FROM pdv_kpr WHERE id = ?`, id)
k, err := skenirajKpr(red.Scan) k, err := skenirajKpr(red.Scan)
if err != nil { 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) { func skenirajKpr(scan func(...any) error) (model.PdvKpr, error) {
var k model.PdvKpr var k model.PdvKpr
var datumPlacanja sql.NullTime var datumPlacanja sql.NullTime
var izvorID sql.NullInt64
if err := scan( if err := scan(
&k.ID, &k.DatumPrometa, &k.DatumKnjizenja, &datumPlacanja, &k.BrojDokumenta, &k.ID, &k.DatumPrometa, &k.DatumKnjizenja, &datumPlacanja, &k.BrojDokumenta,
&k.DobavljacNaziv, &k.DobavljacPib, &k.DobavljacMesto, &k.DobavljacNaziv, &k.DobavljacPib, &k.DobavljacMesto,
&k.OsnovicaOpsta, &k.PdvOpsta, &k.OsnovicaPosebna, &k.PdvPosebna, &k.OsnovicaOpsta, &k.PdvOpsta, &k.OsnovicaPosebna, &k.PdvPosebna,
&k.PdvBezOdbitka, &k.OslobodenNabavka, &k.Ukupno, &k.PdvBezOdbitka, &k.OslobodenNabavka, &k.Ukupno,
&k.Napomena, &k.DatumUnosa, &k.Napomena, &k.Izvor, &izvorID, &k.DatumUnosa,
); err != nil { ); err != nil {
return model.PdvKpr{}, err return model.PdvKpr{}, err
} }
if datumPlacanja.Valid { if datumPlacanja.Valid {
k.DatumPlacanja = &datumPlacanja.Time k.DatumPlacanja = &datumPlacanja.Time
} }
if izvorID.Valid {
k.IzvorID = &izvorID.Int64
}
return k, nil 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, datum_prometa, datum_knjizenja, datum_placanja, broj_dokumenta,
dobavljac_naziv, dobavljac_pib, dobavljac_mesto, dobavljac_naziv, dobavljac_pib, dobavljac_mesto,
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
pdv_bez_odbitka, osloboden_nabavka, ukupno, napomena pdv_bez_odbitka, osloboden_nabavka, ukupno, napomena, izvor, izvor_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
k.DatumPrometa, k.DatumKnjizenja, datumPlacanja, k.BrojDokumenta, k.DatumPrometa, k.DatumKnjizenja, datumPlacanja, k.BrojDokumenta,
k.DobavljacNaziv, k.DobavljacPib, k.DobavljacMesto, k.DobavljacNaziv, k.DobavljacPib, k.DobavljacMesto,
k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna, 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 { if err != nil {
return 0, fmt.Errorf("ntech: PdvKprRepo.Kreiraj: %w", err) 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 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
}
+44
View File
@@ -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))
}
}
+4
View File
@@ -20,6 +20,8 @@ type PdvKir struct {
OslobodenBezPrava float64 OslobodenBezPrava float64
Ukupno float64 Ukupno float64
Napomena string Napomena string
Izvor string // "rucno" | "prodaja" | "nabavka"
IzvorID *int64 // id izvornog naloga (nil za ručni unos)
DatumUnosa time.Time DatumUnosa time.Time
} }
@@ -92,6 +94,8 @@ type PdvKpr struct {
OslobodenNabavka float64 OslobodenNabavka float64
Ukupno float64 Ukupno float64
Napomena string Napomena string
Izvor string // "rucno" | "prodaja" | "nabavka"
IzvorID *int64 // id izvorne nabavke (nil za ručni unos)
DatumUnosa time.Time DatumUnosa time.Time
} }
+8
View File
@@ -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;