feat(pdv): KIR/KPR evidencija — migracija, model i repozitorijum (Faza 2a)

Tabele pdv_kir i pdv_kpr (iznosi po vrsti stope), modeli PdvKir/PdvKpr,
repozitorijumi sa filterom perioda i integracioni test (datum round-trip,
nullable datum plaćanja).
This commit is contained in:
2026-06-13 21:22:48 +02:00
parent d06a353a52
commit 26c829fef3
6 changed files with 485 additions and 0 deletions
+16
View File
@@ -32,6 +32,22 @@ type PdvStopaRepository interface {
PostaviAktivnu(ctx context.Context, id int64, aktivna bool) error PostaviAktivnu(ctx context.Context, id int64, aktivna bool) error
} }
// PdvKirRepository definiše operacije nad knjigom izdatih računa (KIR)
type PdvKirRepository interface {
Lista(ctx context.Context, od, do time.Time) ([]model.PdvKir, error)
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
}
// PdvKprRepository definiše operacije nad knjigom primljenih računa (KPR)
type PdvKprRepository interface {
Lista(ctx context.Context, od, do time.Time) ([]model.PdvKpr, error)
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
}
// ArtikalFilter definiše parametre za filtriranje liste artikala // ArtikalFilter definiše parametre za filtriranje liste artikala
type ArtikalFilter struct { type ArtikalFilter struct {
Pretraga string Pretraga string
+243
View File
@@ -0,0 +1,243 @@
package sqlite
import (
"context"
"database/sql"
"fmt"
"time"
"ntech/internal/model"
)
// --- KIR: knjiga izdatih računa ---
// PdvKirRepo je SQLite implementacija PdvKirRepository interfejsa
type PdvKirRepo struct {
db *sql.DB
}
// NoviPdvKirRepo kreira novi PdvKirRepo
func NoviPdvKirRepo(db *sql.DB) *PdvKirRepo {
return &PdvKirRepo{db: db}
}
// Lista vraća zapise KIR-a u zadatom periodu (po datumu prometa); nulti datum znači bez granice
func (r *PdvKirRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKir, error) {
upit := `
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 1=1`
args := []any{}
if !od.IsZero() {
upit += " AND datum_prometa >= ?"
args = append(args, od)
}
if !do.IsZero() {
upit += " AND datum_prometa <= ?"
args = append(args, do)
}
upit += " ORDER BY datum_prometa ASC, id ASC"
redovi, err := r.db.QueryContext(ctx, upit, args...)
if err != nil {
return nil, fmt.Errorf("ntech: PdvKirRepo.Lista: %w", err)
}
defer redovi.Close()
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 {
return nil, fmt.Errorf("ntech: PdvKirRepo.Lista: scan: %w", err)
}
rezultat = append(rezultat, k)
}
if err := redovi.Err(); err != nil {
return nil, fmt.Errorf("ntech: PdvKirRepo.Lista: %w", err)
}
return rezultat, nil
}
// 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, `
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,
)
if err != nil {
return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiID: %w", err)
}
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, `
INSERT INTO pdv_kir (
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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)
if err != nil {
return 0, fmt.Errorf("ntech: PdvKirRepo.Kreiraj: %w", err)
}
id, err := rez.LastInsertId()
if err != nil {
return 0, fmt.Errorf("ntech: PdvKirRepo.Kreiraj: id: %w", err)
}
return id, nil
}
// Obrisi briše zapis KIR-a
func (r *PdvKirRepo) Obrisi(ctx context.Context, id int64) error {
if _, err := r.db.ExecContext(ctx, "DELETE FROM pdv_kir WHERE id = ?", id); err != nil {
return fmt.Errorf("ntech: PdvKirRepo.Obrisi: %w", err)
}
return nil
}
// --- KPR: knjiga primljenih računa ---
// PdvKprRepo je SQLite implementacija PdvKprRepository interfejsa
type PdvKprRepo struct {
db *sql.DB
}
// NoviPdvKprRepo kreira novi PdvKprRepo
func NoviPdvKprRepo(db *sql.DB) *PdvKprRepo {
return &PdvKprRepo{db: db}
}
// Lista vraća zapise KPR-a u zadatom periodu (po datumu prometa); nulti datum znači bez granice
func (r *PdvKprRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKpr, error) {
upit := `
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, ''), datum_unosa
FROM pdv_kpr WHERE 1=1`
args := []any{}
if !od.IsZero() {
upit += " AND datum_prometa >= ?"
args = append(args, od)
}
if !do.IsZero() {
upit += " AND datum_prometa <= ?"
args = append(args, do)
}
upit += " ORDER BY datum_prometa ASC, id ASC"
redovi, err := r.db.QueryContext(ctx, upit, args...)
if err != nil {
return nil, fmt.Errorf("ntech: PdvKprRepo.Lista: %w", err)
}
defer redovi.Close()
var rezultat []model.PdvKpr
for redovi.Next() {
k, err := skenirajKpr(redovi.Scan)
if err != nil {
return nil, fmt.Errorf("ntech: PdvKprRepo.Lista: scan: %w", err)
}
rezultat = append(rezultat, k)
}
if err := redovi.Err(); err != nil {
return nil, fmt.Errorf("ntech: PdvKprRepo.Lista: %w", err)
}
return rezultat, nil
}
// DohvatiID vraća jedan zapis KPR-a po identifikatoru
func (r *PdvKprRepo) DohvatiID(ctx context.Context, id int64) (*model.PdvKpr, error) {
red := r.db.QueryRowContext(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, ''), datum_unosa
FROM pdv_kpr WHERE id = ?`, id)
k, err := skenirajKpr(red.Scan)
if err != nil {
return nil, fmt.Errorf("ntech: PdvKprRepo.DohvatiID: %w", err)
}
return &k, nil
}
// skenirajKpr čita jedan red KPR-a; datum_placanja je nullable pa ide preko sql.NullTime
func skenirajKpr(scan func(...any) error) (model.PdvKpr, error) {
var k model.PdvKpr
var datumPlacanja sql.NullTime
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,
); err != nil {
return model.PdvKpr{}, err
}
if datumPlacanja.Valid {
k.DatumPlacanja = &datumPlacanja.Time
}
return k, nil
}
// Kreiraj dodaje novi zapis u KPR i vraća njegov id
func (r *PdvKprRepo) Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error) {
var datumPlacanja any
if k.DatumPlacanja != nil {
datumPlacanja = *k.DatumPlacanja
}
rez, err := r.db.ExecContext(ctx, `
INSERT INTO pdv_kpr (
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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)
if err != nil {
return 0, fmt.Errorf("ntech: PdvKprRepo.Kreiraj: %w", err)
}
id, err := rez.LastInsertId()
if err != nil {
return 0, fmt.Errorf("ntech: PdvKprRepo.Kreiraj: id: %w", err)
}
return id, nil
}
// Obrisi briše zapis KPR-a
func (r *PdvKprRepo) Obrisi(ctx context.Context, id int64) error {
if _, err := r.db.ExecContext(ctx, "DELETE FROM pdv_kpr WHERE id = ?", id); err != nil {
return fmt.Errorf("ntech: PdvKprRepo.Obrisi: %w", err)
}
return nil
}
+129
View File
@@ -0,0 +1,129 @@
package sqlite
import (
"context"
"testing"
"time"
"ntech/internal/model"
)
// istiDan poredi datume po godini/mesecu/danu (izbegava razlike u vremenskoj zoni/času)
func istiDan(a, b time.Time) bool {
ay, am, ad := a.Date()
by, bm, bd := b.Date()
return ay == by && am == bm && ad == bd
}
func TestPdvKirRepo(t *testing.T) {
db := testDB(t)
repo := NoviPdvKirRepo(db)
ctx := context.Background()
dp := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
dk := time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)
id, err := repo.Kreiraj(ctx, &model.PdvKir{
DatumPrometa: dp, DatumKnjizenja: dk, BrojDokumenta: "R-1",
KupacNaziv: "Kupac doo", KupacPib: "123456789", KupacMesto: "Niš",
OsnovicaOpsta: 100, PdvOpsta: 20, Ukupno: 120,
})
if err != nil {
t.Fatalf("Kreiraj: %v", err)
}
t.Run("datum i iznosi round-trip", func(t *testing.T) {
k, err := repo.DohvatiID(ctx, id)
if err != nil {
t.Fatalf("DohvatiID: %v", err)
}
if !istiDan(k.DatumPrometa, dp) {
t.Errorf("datum_prometa = %v, očekivano %v", k.DatumPrometa, dp)
}
if !istiDan(k.DatumKnjizenja, dk) {
t.Errorf("datum_knjizenja = %v, očekivano %v", k.DatumKnjizenja, dk)
}
if k.KupacNaziv != "Kupac doo" || k.KupacPib != "123456789" || k.KupacMesto != "Niš" {
t.Errorf("kupac podaci ne odgovaraju: %+v", k)
}
if k.OsnovicaOpsta != 100 || k.PdvOpsta != 20 || k.Ukupno != 120 {
t.Errorf("iznosi ne odgovaraju: %+v", k)
}
})
t.Run("filter perioda", func(t *testing.T) {
uPeriodu, err := repo.Lista(ctx, time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC))
if err != nil {
t.Fatalf("Lista u periodu: %v", err)
}
if len(uPeriodu) != 1 {
t.Errorf("u junu očekivano 1 zapis, dobijeno %d", len(uPeriodu))
}
vanPerioda, err := repo.Lista(ctx, time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC))
if err != nil {
t.Fatalf("Lista van perioda: %v", err)
}
if len(vanPerioda) != 0 {
t.Errorf("u julu očekivano 0 zapisa, dobijeno %d", len(vanPerioda))
}
})
t.Run("brisanje", func(t *testing.T) {
if err := repo.Obrisi(ctx, id); err != nil {
t.Fatalf("Obrisi: %v", err)
}
sve, _ := repo.Lista(ctx, time.Time{}, time.Time{})
if len(sve) != 0 {
t.Errorf("posle brisanja očekivano 0, dobijeno %d", len(sve))
}
})
}
func TestPdvKprRepo(t *testing.T) {
db := testDB(t)
repo := NoviPdvKprRepo(db)
ctx := context.Background()
dp := time.Date(2026, 6, 10, 0, 0, 0, 0, time.UTC)
dpl := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
t.Run("sa datumom plaćanja", func(t *testing.T) {
id, err := repo.Kreiraj(ctx, &model.PdvKpr{
DatumPrometa: dp, DatumKnjizenja: dp, DatumPlacanja: &dpl, BrojDokumenta: "U-1",
DobavljacNaziv: "Dobavljač doo", OsnovicaOpsta: 200, PdvOpsta: 40, Ukupno: 240,
})
if err != nil {
t.Fatalf("Kreiraj: %v", err)
}
k, err := repo.DohvatiID(ctx, id)
if err != nil {
t.Fatalf("DohvatiID: %v", err)
}
if k.DatumPlacanja == nil {
t.Fatalf("datum_placanja je nil, očekivan %v", dpl)
}
if !istiDan(*k.DatumPlacanja, dpl) {
t.Errorf("datum_placanja = %v, očekivano %v", *k.DatumPlacanja, dpl)
}
if k.OsnovicaOpsta != 200 || k.PdvOpsta != 40 {
t.Errorf("iznosi ne odgovaraju: %+v", k)
}
})
t.Run("bez datuma plaćanja (NULL)", func(t *testing.T) {
id, err := repo.Kreiraj(ctx, &model.PdvKpr{
DatumPrometa: dp, DatumKnjizenja: dp, DatumPlacanja: nil, BrojDokumenta: "U-2",
DobavljacNaziv: "Dobavljač 2", OsnovicaPosebna: 50, PdvPosebna: 5, Ukupno: 55,
})
if err != nil {
t.Fatalf("Kreiraj: %v", err)
}
k, err := repo.DohvatiID(ctx, id)
if err != nil {
t.Fatalf("DohvatiID: %v", err)
}
if k.DatumPlacanja != nil {
t.Errorf("datum_placanja = %v, očekivan nil", *k.DatumPlacanja)
}
})
}
+6
View File
@@ -36,6 +36,8 @@ type Handler struct {
LoginIstorijsaRepo db.LoginIstorijsaRepository LoginIstorijsaRepo db.LoginIstorijsaRepository
DozvoleRepo db.DozvoleRepository DozvoleRepo db.DozvoleRepository
PdvStopeRepo db.PdvStopaRepository PdvStopeRepo db.PdvStopaRepository
PdvKirRepo db.PdvKirRepository
PdvKprRepo db.PdvKprRepository
Verzija string Verzija string
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju) AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
Templates map[string]*template.Template Templates map[string]*template.Template
@@ -96,6 +98,8 @@ func Novi(baza *sql.DB, totpKljuc []byte) *Handler {
LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza), LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza),
DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()), DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()),
PdvStopeRepo: sqlite.NoviPdvStopaRepo(baza), PdvStopeRepo: sqlite.NoviPdvStopaRepo(baza),
PdvKirRepo: sqlite.NoviPdvKirRepo(baza),
PdvKprRepo: sqlite.NoviPdvKprRepo(baza),
} }
} }
@@ -122,6 +126,8 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
h.LoginIstorijsaRepo = sqlite.NoviLoginIstorijsaRepo(novaDB) h.LoginIstorijsaRepo = sqlite.NoviLoginIstorijsaRepo(novaDB)
h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije()) h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije())
h.PdvStopeRepo = sqlite.NoviPdvStopaRepo(novaDB) h.PdvStopeRepo = sqlite.NoviPdvStopaRepo(novaDB)
h.PdvKirRepo = sqlite.NoviPdvKirRepo(novaDB)
h.PdvKprRepo = sqlite.NoviPdvKprRepo(novaDB)
} }
// zahtevajDozvolu vraća prijavljenog korisnika ako njegova uloga sme da izvrši akciju. // zahtevajDozvolu vraća prijavljenog korisnika ako njegova uloga sme da izvrši akciju.
+45
View File
@@ -0,0 +1,45 @@
package model
import "time"
// PdvKir je jedan zapis u knjizi izdatih računa (izlazni PDV).
// Iznosi se vode po vrsti stope (opšta/posebna) — vidi migraciju 041.
type PdvKir struct {
ID int64
DatumPrometa time.Time
DatumKnjizenja time.Time
BrojDokumenta string
KupacNaziv string
KupacPib string
KupacMesto string
OsnovicaOpsta float64
PdvOpsta float64
OsnovicaPosebna float64
PdvPosebna float64
OslobodenSaPravom float64
OslobodenBezPrava float64
Ukupno float64
Napomena string
DatumUnosa time.Time
}
// PdvKpr je jedan zapis u knjizi primljenih računa (ulazni PDV).
type PdvKpr struct {
ID int64
DatumPrometa time.Time
DatumKnjizenja time.Time
DatumPlacanja *time.Time // može biti prazan
BrojDokumenta string
DobavljacNaziv string
DobavljacPib string
DobavljacMesto string
OsnovicaOpsta float64
PdvOpsta float64
OsnovicaPosebna float64
PdvPosebna float64
PdvBezOdbitka float64
OslobodenNabavka float64
Ukupno float64
Napomena string
DatumUnosa time.Time
}
+46
View File
@@ -0,0 +1,46 @@
-- PDV evidencija (Faza 2a knjigovodstvenog modula): knjiga izdatih računa (KIR)
-- i knjiga primljenih računa (KPR). Iznosi se vode po VRSTI stope (opšta/posebna),
-- ne po procentu — promena stope zakonom ne razbija kolone.
-- Napomena: ovo je osnovna knjiga; pun POPDV obrazac (uvoz, avansi, poljoprivreda)
-- dolazi u kasnijoj iteraciji.
-- Knjiga izdatih računa (izlazni PDV).
CREATE TABLE IF NOT EXISTS pdv_kir (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datum_prometa DATE NOT NULL, -- datum prometa / izdavanja računa
datum_knjizenja DATE NOT NULL, -- datum knjiženja u KIR
broj_dokumenta TEXT NOT NULL, -- broj računa ili drugog dokumenta
kupac_naziv TEXT NOT NULL,
kupac_pib TEXT, -- PIB ili JMBG kupca
kupac_mesto TEXT,
osnovica_opsta REAL NOT NULL DEFAULT 0, -- osnovica oporeziva opštom stopom
pdv_opsta REAL NOT NULL DEFAULT 0, -- obračunati PDV po opštoj stopi
osnovica_posebna REAL NOT NULL DEFAULT 0, -- osnovica oporeziva posebnom stopom
pdv_posebna REAL NOT NULL DEFAULT 0, -- obračunati PDV po posebnoj stopi
osloboden_sa_pravom REAL NOT NULL DEFAULT 0, -- oslobođen promet sa pravom na odbitak
osloboden_bez_prava REAL NOT NULL DEFAULT 0, -- oslobođen promet bez prava na odbitak
ukupno REAL NOT NULL DEFAULT 0, -- ukupna naknada sa PDV
napomena TEXT,
datum_unosa DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Knjiga primljenih računa (ulazni PDV).
CREATE TABLE IF NOT EXISTS pdv_kpr (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datum_prometa DATE NOT NULL, -- datum prometa / prijema računa
datum_knjizenja DATE NOT NULL, -- datum knjiženja u KPR
datum_placanja DATE, -- datum plaćanja (može biti prazan)
broj_dokumenta TEXT NOT NULL,
dobavljac_naziv TEXT NOT NULL,
dobavljac_pib TEXT,
dobavljac_mesto TEXT,
osnovica_opsta REAL NOT NULL DEFAULT 0,
pdv_opsta REAL NOT NULL DEFAULT 0, -- PDV koji se može odbiti (opšta)
osnovica_posebna REAL NOT NULL DEFAULT 0,
pdv_posebna REAL NOT NULL DEFAULT 0, -- PDV koji se može odbiti (posebna)
pdv_bez_odbitka REAL NOT NULL DEFAULT 0, -- PDV bez prava na odbitak
osloboden_nabavka REAL NOT NULL DEFAULT 0, -- nabavka bez PDV / oslobođena
ukupno REAL NOT NULL DEFAULT 0,
napomena TEXT,
datum_unosa DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);