feat(pdv): uvoz robe — KPR zastavica i mapiranje u PPPDV 006/106

- migracija 048: kolona uvoz na pdv_kpr (0=domaća nabavka, 1=uvoz)
- model PdvKpr.Uvoz; MapirajPPPDV(kir, kprDomace, kprUvoz) rutira uvoz u 006/106,
  domaće u 008/108; test ažuriran + uvozni scenario
- repo: KPR Lista/DohvatiID/Kreiraj čitaju i pišu uvoz
- obračun: KPR se razdvaja na domaće/uvozne; obaveza ostaje na ukupnom KPR-u
- KPR forma: kvačica „Uvoz (JCI)"; lista: oznaka UVOZ uz broj dokumenta
This commit is contained in:
2026-06-14 17:16:01 +02:00
parent c7470ebbc9
commit 42c74a725a
8 changed files with 73 additions and 16 deletions
+12 -6
View File
@@ -171,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, ''), izvor, izvor_id, datum_unosa COALESCE(napomena, ''), izvor, izvor_id, uvoz, datum_unosa
FROM pdv_kpr WHERE 1=1` FROM pdv_kpr WHERE 1=1`
args := []any{} args := []any{}
if !od.IsZero() { if !od.IsZero() {
@@ -211,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, ''), izvor, izvor_id, datum_unosa COALESCE(napomena, ''), izvor, izvor_id, uvoz, 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 {
@@ -225,12 +225,13 @@ 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 var izvorID sql.NullInt64
var uvoz int64
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.Izvor, &izvorID, &k.DatumUnosa, &k.Napomena, &k.Izvor, &izvorID, &uvoz, &k.DatumUnosa,
); err != nil { ); err != nil {
return model.PdvKpr{}, err return model.PdvKpr{}, err
} }
@@ -240,6 +241,7 @@ func skenirajKpr(scan func(...any) error) (model.PdvKpr, error) {
if izvorID.Valid { if izvorID.Valid {
k.IzvorID = &izvorID.Int64 k.IzvorID = &izvorID.Int64
} }
k.Uvoz = uvoz != 0
return k, nil return k, nil
} }
@@ -249,18 +251,22 @@ func (r *PdvKprRepo) Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error
if k.DatumPlacanja != nil { if k.DatumPlacanja != nil {
datumPlacanja = *k.DatumPlacanja datumPlacanja = *k.DatumPlacanja
} }
uvoz := 0
if k.Uvoz {
uvoz = 1
}
rez, err := r.db.ExecContext(ctx, ` rez, err := r.db.ExecContext(ctx, `
INSERT INTO pdv_kpr ( INSERT INTO pdv_kpr (
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, izvor, izvor_id pdv_bez_odbitka, osloboden_nabavka, ukupno, napomena, izvor, izvor_id, uvoz
) 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)) izvorIliRucno(k.Izvor), izvorIDArg(k.IzvorID), uvoz)
if err != nil { if err != nil {
return 0, fmt.Errorf("ntech: PdvKprRepo.Kreiraj: %w", err) return 0, fmt.Errorf("ntech: PdvKprRepo.Kreiraj: %w", err)
} }
+1
View File
@@ -128,6 +128,7 @@ func (h *Handler) SacuvajPdvKpr(w http.ResponseWriter, r *http.Request) {
PdvBezOdbitka: parsiraIznos(r.FormValue("pdv_bez_odbitka")), PdvBezOdbitka: parsiraIznos(r.FormValue("pdv_bez_odbitka")),
OslobodenNabavka: parsiraIznos(r.FormValue("osloboden_nabavka")), OslobodenNabavka: parsiraIznos(r.FormValue("osloboden_nabavka")),
Napomena: strings.TrimSpace(r.FormValue("napomena")), Napomena: strings.TrimSpace(r.FormValue("napomena")),
Uvoz: r.FormValue("uvoz") == "1",
} }
// datum plaćanja je opcionalan // datum plaćanja je opcionalan
if dp := parsiraDatumOpcionalno(r.FormValue("datum_placanja")); !dp.IsZero() { if dp := parsiraDatumOpcionalno(r.FormValue("datum_placanja")); !dp.IsZero() {
+14 -1
View File
@@ -59,6 +59,19 @@ func (h *Handler) PdvObracunStranica(w http.ResponseWriter, r *http.Request) {
kirSume := model.SumirajKir(kirZapisi) kirSume := model.SumirajKir(kirZapisi)
kprSume := model.SumirajKpr(kprZapisi) kprSume := model.SumirajKpr(kprZapisi)
// razdvajamo KPR na domaće i uvozne — uvoz se u PPPDV mapira u polja 006/106,
// domaća nabavka u 008/108; obračun obaveze ostaje na ukupnom KPR-u (uvozni PDV je odbitni)
var kprDomaci, kprUvozni []model.PdvKpr
for _, z := range kprZapisi {
if z.Uvoz {
kprUvozni = append(kprUvozni, z)
} else {
kprDomaci = append(kprDomaci, z)
}
}
kprDomaceSume := model.SumirajKpr(kprDomaci)
kprUvozSume := model.SumirajKpr(kprUvozni)
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "pdv-obracun" ps.Stranica = "pdv-obracun"
ps.NaslovStranice = "PDV obračun" ps.NaslovStranice = "PDV obračun"
@@ -69,6 +82,6 @@ func (h *Handler) PdvObracunStranica(w http.ResponseWriter, r *http.Request) {
KirSume: kirSume, KirSume: kirSume,
KprSume: kprSume, KprSume: kprSume,
Obracun: model.ObracunajPdv(kirSume, kprSume), Obracun: model.ObracunajPdv(kirSume, kprSume),
PPPDV: model.MapirajPPPDV(kirSume, kprSume), PPPDV: model.MapirajPPPDV(kirSume, kprDomaceSume, kprUvozSume),
}) })
} }
+9 -5
View File
@@ -138,6 +138,7 @@ type PdvKpr struct {
Napomena string Napomena string
Izvor string // "rucno" | "prodaja" | "nabavka" Izvor string // "rucno" | "prodaja" | "nabavka"
IzvorID *int64 // id izvorne nabavke (nil za ručni unos) IzvorID *int64 // id izvorne nabavke (nil za ručni unos)
Uvoz bool // true = uvoz (JCI) → PPPDV 006/106; false = domaća nabavka → 008/108
DatumUnosa time.Time DatumUnosa time.Time
} }
@@ -287,9 +288,10 @@ func (p PPPDV) Polje110Apsolutno() int64 {
} }
// MapirajPPPDV preslikava zbirove KIR i KPR na polja obrasca PPPDV (cele dinare). // MapirajPPPDV preslikava zbirove KIR i KPR na polja obrasca PPPDV (cele dinare).
// ⚠ Uvoz (006/106) i PDV nadoknada poljoprivredniku (007/107) se ne prate zasebno // KPR je razdvojen: uvoz (JCI) ide u polja 006/106 (prethodni porez pri uvozu),
// pa ostaju 0 — sav odbitni PDV iz KPR pada u polje 008/108. // domaća nabavka u 008/108 (ostali prethodni porez).
func MapirajPPPDV(kir PdvKirSume, kpr PdvKprSume) PPPDV { // ⚠ PDV nadoknada poljoprivredniku (007/107) se i dalje ne prati zasebno (ostaje 0).
func MapirajPPPDV(kir PdvKirSume, kprDomace, kprUvoz PdvKprSume) PPPDV {
uDinare := func(v float64) int64 { return int64(math.Round(v)) } uDinare := func(v float64) int64 { return int64(math.Round(v)) }
p := PPPDV{ p := PPPDV{
Polje001: uDinare(kir.OslobodenSaPravom), Polje001: uDinare(kir.OslobodenSaPravom),
@@ -298,8 +300,10 @@ func MapirajPPPDV(kir PdvKirSume, kpr PdvKprSume) PPPDV {
Polje103: uDinare(kir.PdvOpsta), Polje103: uDinare(kir.PdvOpsta),
Polje004: uDinare(kir.OsnovicaPosebna), Polje004: uDinare(kir.OsnovicaPosebna),
Polje104: uDinare(kir.PdvPosebna), Polje104: uDinare(kir.PdvPosebna),
Polje008: uDinare(kpr.OsnovicaOpsta + kpr.OsnovicaPosebna), Polje006: uDinare(kprUvoz.OsnovicaOpsta + kprUvoz.OsnovicaPosebna),
Polje108: uDinare(kpr.PdvOpsta + kpr.PdvPosebna), Polje106: uDinare(kprUvoz.PdvOpsta + kprUvoz.PdvPosebna),
Polje008: uDinare(kprDomace.OsnovicaOpsta + kprDomace.OsnovicaPosebna),
Polje108: uDinare(kprDomace.PdvOpsta + kprDomace.PdvPosebna),
} }
// zbirovi se računaju iz zaokruženih polja da kolone na obrascu uvek zbiraju // zbirovi se računaju iz zaokruženih polja da kolone na obrascu uvek zbiraju
p.Polje005 = p.Polje001 + p.Polje002 + p.Polje003 + p.Polje004 p.Polje005 = p.Polje001 + p.Polje002 + p.Polje003 + p.Polje004
+20 -3
View File
@@ -108,7 +108,7 @@ func TestMapirajPPPDV(t *testing.T) {
PdvBezOdbitka: 25, // ne sme da uđe nigde u PPPDV PdvBezOdbitka: 25, // ne sme da uđe nigde u PPPDV
} }
p := MapirajPPPDV(kir, kpr) p := MapirajPPPDV(kir, kpr, PdvKprSume{})
if p.Polje001 != 300 || p.Polje002 != 101 || p.Polje003 != 1000 || p.Polje103 != 200 { if p.Polje001 != 300 || p.Polje002 != 101 || p.Polje003 != 1000 || p.Polje103 != 200 {
t.Errorf("I deo (001/002/003/103) = %d/%d/%d/%d", p.Polje001, p.Polje002, p.Polje003, p.Polje103) t.Errorf("I deo (001/002/003/103) = %d/%d/%d/%d", p.Polje001, p.Polje002, p.Polje003, p.Polje103)
@@ -120,7 +120,7 @@ func TestMapirajPPPDV(t *testing.T) {
if p.Polje005 != 1902 || p.Polje105 != 250 { if p.Polje005 != 1902 || p.Polje105 != 250 {
t.Errorf("zbir 005/105 = %d/%d, očekivano 1902/250", p.Polje005, p.Polje105) t.Errorf("zbir 005/105 = %d/%d, očekivano 1902/250", p.Polje005, p.Polje105)
} }
// uvoz i poljoprivrednik se ne prate // u ovom scenariju nema uvoza (prazan kprUvoz) ni poljoprivrednika → 006/106/007/107 = 0
if p.Polje006 != 0 || p.Polje106 != 0 || p.Polje007 != 0 || p.Polje107 != 0 { if p.Polje006 != 0 || p.Polje106 != 0 || p.Polje007 != 0 || p.Polje107 != 0 {
t.Errorf("006/106/007/107 moraju biti 0") t.Errorf("006/106/007/107 moraju biti 0")
} }
@@ -137,8 +137,25 @@ func TestMapirajPPPDV(t *testing.T) {
} }
// scenario povraćaja: izlazni < odbitni → 110 negativan, Povracaj=true // scenario povraćaja: izlazni < odbitni → 110 negativan, Povracaj=true
p2 := MapirajPPPDV(PdvKirSume{PdvOpsta: 30}, PdvKprSume{PdvOpsta: 90}) p2 := MapirajPPPDV(PdvKirSume{PdvOpsta: 30}, PdvKprSume{PdvOpsta: 90}, PdvKprSume{})
if p2.Polje110 != -60 || !p2.Povracaj || p2.Polje110Apsolutno() != 60 { if p2.Polje110 != -60 || !p2.Povracaj || p2.Polje110Apsolutno() != 60 {
t.Errorf("povraćaj: 110=%d Povracaj=%v abs=%d, očekivano -60/true/60", p2.Polje110, p2.Povracaj, p2.Polje110Apsolutno()) t.Errorf("povraćaj: 110=%d Povracaj=%v abs=%d, očekivano -60/true/60", p2.Polje110, p2.Povracaj, p2.Polje110Apsolutno())
} }
// scenario uvoza: uvozni KPR ide u 006/106, domaći u 008/108
p3 := MapirajPPPDV(
PdvKirSume{},
PdvKprSume{OsnovicaOpsta: 1000, PdvOpsta: 200}, // domaće → 008/108
PdvKprSume{OsnovicaOpsta: 500, PdvOpsta: 100, OsnovicaPosebna: 50, PdvPosebna: 5}, // uvoz → 006/106
)
if p3.Polje006 != 550 || p3.Polje106 != 105 {
t.Errorf("uvoz 006/106 = %d/%d, očekivano 550/105", p3.Polje006, p3.Polje106)
}
if p3.Polje008 != 1000 || p3.Polje108 != 200 {
t.Errorf("domaće 008/108 = %d/%d, očekivano 1000/200", p3.Polje008, p3.Polje108)
}
// 009 = 006+007+008 = 550+0+1000 = 1550; 109 = 106+107+108 = 105+0+200 = 305
if p3.Polje009 != 1550 || p3.Polje109 != 305 {
t.Errorf("zbir prethodnog 009/109 = %d/%d, očekivano 1550/305", p3.Polje009, p3.Polje109)
}
} }
+6
View File
@@ -0,0 +1,6 @@
-- Zastavica „uvoz" na zapisima knjige primljenih računa (KPR).
-- Uvoz se unosi RUČNO sa JCI (osnovica i PDV iz carinskog dokumenta drže postojeće
-- kolone osnovica_opsta / pdv_opsta). Zastavica služi da se uvozni PDV u PPPDV obrascu
-- rutira u polja 006/106 (prethodni porez pri uvozu) umesto 008/108 (ostali prethodni).
-- 0 = domaća nabavka (podrazumevano), 1 = uvoz.
ALTER TABLE pdv_kpr ADD COLUMN uvoz INTEGER NOT NULL DEFAULT 0;
+1 -1
View File
@@ -47,7 +47,7 @@
{{range .Zapisi}} {{range .Zapisi}}
<tr style="border-bottom:0.5px solid var(--ivica);"> <tr style="border-bottom:0.5px solid var(--ivica);">
<td style="padding:10px 12px;white-space:nowrap;">{{.DatumPrometa.Format "02.01.2006."}}</td> <td style="padding:10px 12px;white-space:nowrap;">{{.DatumPrometa.Format "02.01.2006."}}</td>
<td style="padding:10px 12px;">{{.BrojDokumenta}}</td> <td style="padding:10px 12px;">{{.BrojDokumenta}}{{if .Uvoz}}<div style="display:inline-block;margin-top:2px;font-size:10px;font-weight:600;color:var(--sb-akcent);border:0.5px solid var(--sb-akcent);border-radius:4px;padding:0 5px;">UVOZ</div>{{end}}</td>
<td style="padding:10px 12px;">{{.DobavljacNaziv}}{{if .DobavljacPib}}<div style="font-size:11px;color:var(--tekst-sporedni);">{{.OznakaPoreskogBroja}}: {{.DobavljacPib}}</div>{{end}}</td> <td style="padding:10px 12px;">{{.DobavljacNaziv}}{{if .DobavljacPib}}<div style="font-size:11px;color:var(--tekst-sporedni);">{{.OznakaPoreskogBroja}}: {{.DobavljacPib}}</div>{{end}}</td>
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .OsnovicaOpsta}}</td> <td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .OsnovicaOpsta}}</td>
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .PdvOpsta}}</td> <td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .PdvOpsta}}</td>
+10
View File
@@ -102,6 +102,16 @@
<input type="text" name="napomena" style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;"> <input type="text" name="napomena" style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
</div> </div>
<div style="margin-top:14px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;color:var(--tekst-glavni);">
<input type="checkbox" name="uvoz" value="1" style="width:16px;height:16px;cursor:pointer;">
Uvoz (JCI)
</label>
<div class="pomocni-tekst" style="margin-top:4px;">
Označi ako je račun uvoz po carinskom dokumentu (JCI) — PDV ide u polja 006/106 obrasca PPPDV.
</div>
</div>
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;"> <div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;">
<a href="/pdv/kpr" class="btn-sekundarno" style="font-size:14px;padding:10px 20px;">Odustani</a> <a href="/pdv/kpr" class="btn-sekundarno" style="font-size:14px;padding:10px 20px;">Odustani</a>
<button type="submit" style="background:var(--sb-akcent);color:#fff;border:none;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">Sačuvaj</button> <button type="submit" style="background:var(--sb-akcent);color:#fff;border:none;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">Sačuvaj</button>