Merge feature/kw-pdv-evidencija: PDV evidencija (Faza 2 — KIR/KPR, auto-punjenje, obračun, PPPDV)
This commit is contained in:
+41
-7
@@ -8,9 +8,9 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -87,12 +87,14 @@ func main() {
|
||||
|
||||
db, err := sqlite.OtvoriDB(putanjaBaze)
|
||||
if err != nil {
|
||||
slog.Error("Greška pri otvaranju baze", "error", err); os.Exit(1)
|
||||
slog.Error("Greška pri otvaranju baze", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := sqlite.PokreniMigracije(db, migrFS); err != nil {
|
||||
slog.Error("Greška pri migracijama", "error", err); os.Exit(1)
|
||||
slog.Error("Greška pri migracijama", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
slog.Info("migracije uspešno izvršene")
|
||||
|
||||
@@ -111,7 +113,8 @@ func main() {
|
||||
// ključ za šifrovanje TOTP tajni u mirovanju (AES-256-GCM)
|
||||
totpKljuc, err := ucitajTotpKljuc()
|
||||
if err != nil {
|
||||
slog.Error("Greška pri učitavanju ključa za TOTP", "error", err); os.Exit(1)
|
||||
slog.Error("Greška pri učitavanju ključa za TOTP", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// jednokratno šifruj eventualne stare TOTP tajne koje su ostale kao čist tekst
|
||||
@@ -137,7 +140,8 @@ func main() {
|
||||
if os.Getenv("NTECH_ENV") == "production" {
|
||||
kes, err := handler.KreirajKes(templFS)
|
||||
if err != nil {
|
||||
slog.Error("Greška pri kreiranju keša šablona", "error", err); os.Exit(1)
|
||||
slog.Error("Greška pri kreiranju keša šablona", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
h.Templates = kes
|
||||
slog.Info("keš šablona kreiran", "broj", len(kes))
|
||||
@@ -221,6 +225,20 @@ func main() {
|
||||
return ntechmw.RequireDozvolaMut(h.DozvoleRepo.ImaDozvolu, akcija)
|
||||
}
|
||||
|
||||
// modul vraća middleware koji propušta zahtev samo ako je zakonski modul
|
||||
// uključen za firmu (profil firme). Sloj IZNAD RBAC-a — zahtev mora proći
|
||||
// i „modul uključen" (ovo) i „korisnik sme" (doz/zahtevajDozvolu).
|
||||
proveriModul := func(ctx context.Context, m string) bool {
|
||||
pod, err := sqlite.DohvatiSvaPodesavanja(ctx, h.DB)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return config.ModulUkljucen(pod, m)
|
||||
}
|
||||
modul := func(m string) func(http.Handler) http.Handler {
|
||||
return ntechmw.RequireModul(proveriModul, m)
|
||||
}
|
||||
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||
})
|
||||
@@ -229,6 +247,10 @@ func main() {
|
||||
r.Get("/admin/podesavanja/opste", h.PodesavanjaOpste)
|
||||
r.Get("/admin/podesavanja/izgled", h.PodesavanjaIzgled)
|
||||
r.Get("/admin/podesavanja/sistem", h.PodesavanjaSistem)
|
||||
r.Get("/admin/podesavanja/pdv-stope", h.PdvStope)
|
||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/dodaj", h.DodajPdvStopu)
|
||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/izmeni", h.IzmeniPdvStopu)
|
||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/aktivnost", h.PromeniAktivnostPdvStope)
|
||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/sacuvaj", h.SacuvajPodesavanja)
|
||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/logo", h.OtpremiLogo)
|
||||
r.With(doz("podesavanja.login_pozadina")).Post("/podesavanja/login-pozadina", h.OtpremiLoginPozadinu)
|
||||
@@ -237,6 +259,18 @@ func main() {
|
||||
|
||||
r.Get("/podesavanja/backup", h.BackupBaze)
|
||||
r.With(doz("backup.pokreni")).Post("/podesavanja/backup/vrati", h.VratiBackup)
|
||||
|
||||
// PDV evidencija — KIR (knjiga izdatih računa). Dostupno samo kada je modul
|
||||
// „pdv" uključen za firmu (RequireModul), uz RBAC dozvolu pdv.*.
|
||||
r.With(modul("pdv")).Get("/pdv/kir", h.PdvKir)
|
||||
r.With(modul("pdv")).Get("/pdv/kir/nova", h.NoviPdvKir)
|
||||
r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kir/nova", h.SacuvajPdvKir)
|
||||
r.With(modul("pdv"), doz("pdv.obrisi")).Post("/pdv/kir/obrisi/{id}", h.ObrisiPdvKir)
|
||||
r.With(modul("pdv")).Get("/pdv/kpr", h.PdvKpr)
|
||||
r.With(modul("pdv")).Get("/pdv/kpr/nova", h.NoviPdvKpr)
|
||||
r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kpr/nova", h.SacuvajPdvKpr)
|
||||
r.With(modul("pdv"), doz("pdv.obrisi")).Post("/pdv/kpr/obrisi/{id}", h.ObrisiPdvKpr)
|
||||
r.With(modul("pdv")).Get("/pdv/obracun", h.PdvObracunStranica)
|
||||
r.Get("/magacin", h.Magacin)
|
||||
r.Get("/magacin/novi", h.NoviArtikal)
|
||||
r.With(doz("artikal.dodaj")).Post("/magacin/novi", h.SacuvajArtikal)
|
||||
@@ -327,7 +361,8 @@ func main() {
|
||||
slog.Info("NTech pokrenut", "port", port)
|
||||
err = http.ListenAndServe(":"+port, r)
|
||||
if err != nil {
|
||||
slog.Error("port je zauzet ili nije dostupan", "port", port); os.Exit(1)
|
||||
slog.Error("port je zauzet ili nije dostupan", "port", port)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,4 +454,3 @@ func ocistiStareBackupe(folder string, max int) {
|
||||
_ = os.Remove(f)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package config
|
||||
|
||||
// Ovaj fajl izvodi iz profila firme (vidi Project.md §2, §4) koji su zakonski
|
||||
// moduli uključeni za datu firmu. Profil se čuva kao key-value u tabeli
|
||||
// `podesavanja`, pa helper prima već učitanu mapu (čista funkcija — lako se
|
||||
// testira bez baze). Ovo je sloj IZNAD RBAC-a: „da li firma uopšte koristi
|
||||
// modul", nezavisno od „da li korisnik sme".
|
||||
|
||||
// Ključevi profila firme u tabeli `podesavanja`.
|
||||
const (
|
||||
KljucPravniOblik = "firma_pravni_oblik"
|
||||
KljucPdvObveznik = "firma_pdv_obveznik"
|
||||
KljucFiskalizacija = "firma_fiskalizacija"
|
||||
KljucRezim = "firma_rezim"
|
||||
)
|
||||
|
||||
// Nazivi modula koje program pali/gasi prema profilu firme.
|
||||
const (
|
||||
ModulPdv = "pdv"
|
||||
ModulFiskalizacija = "fiskalizacija"
|
||||
ModulKpo = "kpo"
|
||||
ModulDvojno = "dvojno"
|
||||
)
|
||||
|
||||
// ModulUkljucen vraća da li je dati zakonski modul aktivan za firmu, na osnovu
|
||||
// profila firme. Režim „samo evidencija" gasi ceo zakonski sloj — tada nijedan
|
||||
// modul nije uključen, bez obzira na ostale prekidače.
|
||||
func ModulUkljucen(podesavanja map[string]string, modul string) bool {
|
||||
// podrazumevano (ključ još ne postoji) je „samo evidencija" — najbezbednije
|
||||
// stanje: stara instalacija ne počinje da se ponaša kao poreski obveznik.
|
||||
rezim := podesavanja[KljucRezim]
|
||||
if rezim == "" {
|
||||
rezim = "samo_evidencija"
|
||||
}
|
||||
if rezim != "pun" {
|
||||
return false
|
||||
}
|
||||
|
||||
switch modul {
|
||||
case ModulFiskalizacija:
|
||||
// fiskalizacija je nezavisna od pravnog oblika — zaseban prekidač
|
||||
// „izdaje li račune građanima" (Project.md §3, napomena *).
|
||||
return podesavanja[KljucFiskalizacija] == "da"
|
||||
case ModulPdv:
|
||||
// PDV evidencija se vodi kad je firma u sistemu PDV-a.
|
||||
return podesavanja[KljucPdvObveznik] == "da"
|
||||
case ModulKpo:
|
||||
// KPO (knjiga o ostvarenom prometu) je samo za paušalce.
|
||||
return podesavanja[KljucPravniOblik] == "pausalac"
|
||||
case ModulDvojno:
|
||||
// dvojno knjigovodstvo je za DOO (za preduzetnike je buduća podela, Project.md §8).
|
||||
return podesavanja[KljucPravniOblik] == "doo"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// SviModuli vraća mapu svih poznatih modula → da li su uključeni za dati profil firme.
|
||||
// Koristi se da šabloni uslovno prikazuju stavke menija (analogno mapi Dozvole).
|
||||
func SviModuli(podesavanja map[string]string) map[string]bool {
|
||||
moduli := []string{ModulPdv, ModulFiskalizacija, ModulKpo, ModulDvojno}
|
||||
m := make(map[string]bool, len(moduli))
|
||||
for _, modul := range moduli {
|
||||
m[modul] = ModulUkljucen(podesavanja, modul)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestModulUkljucen(t *testing.T) {
|
||||
// pun je profil punog režima sa svim prekidačima upaljenim i pravnim oblikom doo;
|
||||
// pojedinačni testovi ga prepravljaju po potrebi preko pomoćne funkcije izmeni.
|
||||
pun := map[string]string{
|
||||
KljucRezim: "pun",
|
||||
KljucPdvObveznik: "da",
|
||||
KljucFiskalizacija: "da",
|
||||
KljucPravniOblik: "doo",
|
||||
}
|
||||
// izmeni vraća kopiju mape sa promenjenim/dodatim ključem (da testovi ne dele stanje)
|
||||
izmeni := func(osnova map[string]string, kljuc, vrednost string) map[string]string {
|
||||
kopija := make(map[string]string, len(osnova)+1)
|
||||
maps.Copy(kopija, osnova)
|
||||
kopija[kljuc] = vrednost
|
||||
return kopija
|
||||
}
|
||||
|
||||
testovi := []struct {
|
||||
naziv string
|
||||
podesavanja map[string]string
|
||||
modul string
|
||||
ocekivano bool
|
||||
}{
|
||||
// master prekidač: režim „samo evidencija" gasi sve
|
||||
{"prazna mapa — pdv ugašen (default samo_evidencija)", map[string]string{}, ModulPdv, false},
|
||||
{"prazna mapa — fiskalizacija ugašena", map[string]string{}, ModulFiskalizacija, false},
|
||||
{"prazna mapa — kpo ugašen", map[string]string{}, ModulKpo, false},
|
||||
{"prazna mapa — dvojno ugašeno", map[string]string{}, ModulDvojno, false},
|
||||
{"samo evidencija gasi pdv iako je obveznik", izmeni(pun, KljucRezim, "samo_evidencija"), ModulPdv, false},
|
||||
{"samo evidencija gasi fiskalizaciju iako je upaljena", izmeni(pun, KljucRezim, "samo_evidencija"), ModulFiskalizacija, false},
|
||||
|
||||
// fiskalizacija — zaseban prekidač, nezavisan od oblika
|
||||
{"pun + fiskalizacija da", pun, ModulFiskalizacija, true},
|
||||
{"pun + fiskalizacija ne", izmeni(pun, KljucFiskalizacija, "ne"), ModulFiskalizacija, false},
|
||||
|
||||
// pdv — vezan isključivo za prekidač obveznika
|
||||
{"pun + obveznik da", pun, ModulPdv, true},
|
||||
{"pun + obveznik ne (čak i doo)", izmeni(pun, KljucPdvObveznik, "ne"), ModulPdv, false},
|
||||
|
||||
// kpo — samo paušalac
|
||||
{"pun + paušalac → kpo", izmeni(pun, KljucPravniOblik, "pausalac"), ModulKpo, true},
|
||||
{"pun + doo → nema kpo", pun, ModulKpo, false},
|
||||
{"pun + preduzetnik → nema kpo", izmeni(pun, KljucPravniOblik, "preduzetnik_knjige"), ModulKpo, false},
|
||||
|
||||
// dvojno — samo doo
|
||||
{"pun + doo → dvojno", pun, ModulDvojno, true},
|
||||
{"pun + paušalac → nema dvojno", izmeni(pun, KljucPravniOblik, "pausalac"), ModulDvojno, false},
|
||||
{"pun + preduzetnik → nema dvojno (buduća podela)", izmeni(pun, KljucPravniOblik, "preduzetnik_knjige"), ModulDvojno, false},
|
||||
|
||||
// nepoznat modul
|
||||
{"nepoznat modul → false", pun, "nepostojeci", false},
|
||||
}
|
||||
|
||||
for _, tt := range testovi {
|
||||
t.Run(tt.naziv, func(t *testing.T) {
|
||||
if got := ModulUkljucen(tt.podesavanja, tt.modul); got != tt.ocekivano {
|
||||
t.Errorf("ModulUkljucen(%q) = %v, očekivano %v", tt.modul, got, tt.ocekivano)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSviModuli(t *testing.T) {
|
||||
// pun režim, doo, PDV obveznik, fiskalizacija → pdv, fiskalizacija, dvojno uključeni; kpo ne
|
||||
pod := map[string]string{
|
||||
KljucRezim: "pun",
|
||||
KljucPdvObveznik: "da",
|
||||
KljucFiskalizacija: "da",
|
||||
KljucPravniOblik: "doo",
|
||||
}
|
||||
m := SviModuli(pod)
|
||||
if len(m) != 4 {
|
||||
t.Fatalf("SviModuli vraća %d modula, očekivano 4", len(m))
|
||||
}
|
||||
ocek := map[string]bool{ModulPdv: true, ModulFiskalizacija: true, ModulKpo: false, ModulDvojno: true}
|
||||
for modul, want := range ocek {
|
||||
if m[modul] != want {
|
||||
t.Errorf("SviModuli[%q] = %v, očekivano %v", modul, m[modul], want)
|
||||
}
|
||||
}
|
||||
|
||||
// samo evidencija → svi ugašeni
|
||||
prazna := SviModuli(map[string]string{KljucRezim: "samo_evidencija"})
|
||||
for modul, ukljucen := range prazna {
|
||||
if ukljucen {
|
||||
t.Errorf("u režimu samo_evidencija modul %q ne sme biti uključen", modul)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,35 @@ type KategorijaRepository interface {
|
||||
Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error)
|
||||
}
|
||||
|
||||
// PdvStopaRepository definiše operacije nad šifarnikom PDV stopa
|
||||
type PdvStopaRepository interface {
|
||||
Lista(ctx context.Context, samoAktivne bool) ([]model.PdvStopa, error)
|
||||
DohvatiID(ctx context.Context, id int64) (*model.PdvStopa, error)
|
||||
Kreiraj(ctx context.Context, s *model.PdvStopa) (int64, error)
|
||||
Izmeni(ctx context.Context, s *model.PdvStopa) 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
|
||||
// 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)
|
||||
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
|
||||
// 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
|
||||
type ArtikalFilter struct {
|
||||
Pretraga string
|
||||
@@ -117,8 +146,8 @@ type SesijeRepository interface {
|
||||
|
||||
// PodsetnikFilter definiše parametre za filtriranje liste podsetnika
|
||||
type PodsetnikFilter struct {
|
||||
SamoAktivni bool // true = samo nezavršeni; false = svi
|
||||
KorisnikID *int64 // ako nije nil — samo podsetnici tog korisnika
|
||||
SamoAktivni bool // true = samo nezavršeni; false = svi
|
||||
KorisnikID *int64 // ako nije nil — samo podsetnici tog korisnika
|
||||
}
|
||||
|
||||
// PokusajiPrijaveRepository definiše operacije nad evidencijom pokušaja prijave
|
||||
|
||||
@@ -21,7 +21,7 @@ func NoviDobavljacRepo(db *sql.DB) *DobavljacRepo {
|
||||
// Lista vraća listu dobavljača sa opcionom pretragom po nazivu
|
||||
func (r *DobavljacRepo) Lista(ctx context.Context, pretraga string) ([]model.Dobavljac, error) {
|
||||
upit := `
|
||||
SELECT id, naziv, kontakt_osoba, telefon, email, napomena, datum_unosa
|
||||
SELECT id, naziv, kontakt_osoba, telefon, email, pib, mesto, napomena, datum_unosa
|
||||
FROM dobavljaci
|
||||
WHERE 1=1`
|
||||
|
||||
@@ -43,9 +43,9 @@ func (r *DobavljacRepo) Lista(ctx context.Context, pretraga string) ([]model.Dob
|
||||
var rezultat []model.Dobavljac
|
||||
for redovi.Next() {
|
||||
var d model.Dobavljac
|
||||
var kontaktOsoba, telefon, email, napomena sql.NullString
|
||||
var kontaktOsoba, telefon, email, pib, mesto, napomena sql.NullString
|
||||
err := redovi.Scan(
|
||||
&d.ID, &d.Naziv, &kontaktOsoba, &telefon, &email, &napomena, &d.DatumUnosa,
|
||||
&d.ID, &d.Naziv, &kontaktOsoba, &telefon, &email, &pib, &mesto, &napomena, &d.DatumUnosa,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: DobavljacRepo.Lista: scan: %w", err)
|
||||
@@ -53,6 +53,8 @@ func (r *DobavljacRepo) Lista(ctx context.Context, pretraga string) ([]model.Dob
|
||||
d.KontaktOsoba = kontaktOsoba.String
|
||||
d.Telefon = telefon.String
|
||||
d.Email = email.String
|
||||
d.PIB = pib.String
|
||||
d.Mesto = mesto.String
|
||||
d.Napomena = napomena.String
|
||||
rezultat = append(rezultat, d)
|
||||
}
|
||||
@@ -63,12 +65,12 @@ func (r *DobavljacRepo) Lista(ctx context.Context, pretraga string) ([]model.Dob
|
||||
// DohvatiID vraća jednog dobavljača po ID-u
|
||||
func (r *DobavljacRepo) DohvatiID(ctx context.Context, id int64) (*model.Dobavljac, error) {
|
||||
var d model.Dobavljac
|
||||
var kontaktOsoba, telefon, email, napomena sql.NullString
|
||||
var kontaktOsoba, telefon, email, pib, mesto, napomena sql.NullString
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, naziv, kontakt_osoba, telefon, email, napomena, datum_unosa
|
||||
SELECT id, naziv, kontakt_osoba, telefon, email, pib, mesto, napomena, datum_unosa
|
||||
FROM dobavljaci WHERE id = ?`, id).Scan(
|
||||
&d.ID, &d.Naziv, &kontaktOsoba, &telefon, &email, &napomena, &d.DatumUnosa,
|
||||
&d.ID, &d.Naziv, &kontaktOsoba, &telefon, &email, &pib, &mesto, &napomena, &d.DatumUnosa,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: DobavljacRepo.DohvatiID: %w", err)
|
||||
@@ -77,6 +79,8 @@ func (r *DobavljacRepo) DohvatiID(ctx context.Context, id int64) (*model.Dobavlj
|
||||
d.KontaktOsoba = kontaktOsoba.String
|
||||
d.Telefon = telefon.String
|
||||
d.Email = email.String
|
||||
d.PIB = pib.String
|
||||
d.Mesto = mesto.String
|
||||
d.Napomena = napomena.String
|
||||
|
||||
return &d, nil
|
||||
@@ -85,10 +89,10 @@ func (r *DobavljacRepo) DohvatiID(ctx context.Context, id int64) (*model.Dobavlj
|
||||
// Kreiraj dodaje novog dobavljača u bazu
|
||||
func (r *DobavljacRepo) Kreiraj(ctx context.Context, d *model.Dobavljac) (int64, error) {
|
||||
rezultat, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO dobavljaci (naziv, kontakt_osoba, telefon, email, napomena)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
INSERT INTO dobavljaci (naziv, kontakt_osoba, telefon, email, pib, mesto, napomena)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
d.Naziv, nullString(d.KontaktOsoba), nullString(d.Telefon),
|
||||
nullString(d.Email), nullString(d.Napomena),
|
||||
nullString(d.Email), nullString(d.PIB), nullString(d.Mesto), nullString(d.Napomena),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: DobavljacRepo.Kreiraj: %w", err)
|
||||
@@ -106,10 +110,10 @@ func (r *DobavljacRepo) Kreiraj(ctx context.Context, d *model.Dobavljac) (int64,
|
||||
func (r *DobavljacRepo) Izmeni(ctx context.Context, d *model.Dobavljac) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE dobavljaci SET
|
||||
naziv = ?, kontakt_osoba = ?, telefon = ?, email = ?, napomena = ?
|
||||
naziv = ?, kontakt_osoba = ?, telefon = ?, email = ?, pib = ?, mesto = ?, napomena = ?
|
||||
WHERE id = ?`,
|
||||
d.Naziv, nullString(d.KontaktOsoba), nullString(d.Telefon),
|
||||
nullString(d.Email), nullString(d.Napomena), d.ID,
|
||||
nullString(d.Email), nullString(d.PIB), nullString(d.Mesto), nullString(d.Napomena), d.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: DobavljacRepo.Izmeni: %w", err)
|
||||
@@ -127,4 +131,3 @@ func (r *DobavljacRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ func NoviKlijentRepo(db *sql.DB) *KlijentRepo {
|
||||
// Lista vraća listu klijenata sa opcionom pretragom po imenu, prezimenu ili nazivu firme
|
||||
func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klijent, error) {
|
||||
upit := `
|
||||
SELECT id, tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, napomena, datum_unosa
|
||||
SELECT id, tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti
|
||||
WHERE 1=1`
|
||||
|
||||
@@ -44,9 +44,9 @@ func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klije
|
||||
var rezultat []model.Klijent
|
||||
for redovi.Next() {
|
||||
var k model.Klijent
|
||||
var ime, prezime, jmbg, nazivFirme, pib, telefon, email, napomena sql.NullString
|
||||
var ime, prezime, jmbg, nazivFirme, pib, telefon, email, mesto, napomena sql.NullString
|
||||
err := redovi.Scan(
|
||||
&k.ID, &k.Tip, &ime, &prezime, &jmbg, &nazivFirme, &pib, &telefon, &email, &napomena, &k.DatumUnosa,
|
||||
&k.ID, &k.Tip, &ime, &prezime, &jmbg, &nazivFirme, &pib, &telefon, &email, &mesto, &napomena, &k.DatumUnosa,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: KlijentRepo.Lista: scan: %w", err)
|
||||
@@ -58,6 +58,7 @@ func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klije
|
||||
k.PIB = pib.String
|
||||
k.Telefon = telefon.String
|
||||
k.Email = email.String
|
||||
k.Mesto = mesto.String
|
||||
k.Napomena = napomena.String
|
||||
rezultat = append(rezultat, k)
|
||||
}
|
||||
@@ -68,12 +69,12 @@ func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klije
|
||||
// DohvatiID vraća jednog klijenta po ID-u
|
||||
func (r *KlijentRepo) DohvatiID(ctx context.Context, id int64) (*model.Klijent, error) {
|
||||
var k model.Klijent
|
||||
var ime, prezime, jmbg, nazivFirme, pib, telefon, email, napomena sql.NullString
|
||||
var ime, prezime, jmbg, nazivFirme, pib, telefon, email, mesto, napomena sql.NullString
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, napomena, datum_unosa
|
||||
SELECT id, tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti WHERE id = ?`, id).Scan(
|
||||
&k.ID, &k.Tip, &ime, &prezime, &jmbg, &nazivFirme, &pib, &telefon, &email, &napomena, &k.DatumUnosa,
|
||||
&k.ID, &k.Tip, &ime, &prezime, &jmbg, &nazivFirme, &pib, &telefon, &email, &mesto, &napomena, &k.DatumUnosa,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: KlijentRepo.DohvatiID: %w", err)
|
||||
@@ -86,6 +87,7 @@ func (r *KlijentRepo) DohvatiID(ctx context.Context, id int64) (*model.Klijent,
|
||||
k.PIB = pib.String
|
||||
k.Telefon = telefon.String
|
||||
k.Email = email.String
|
||||
k.Mesto = mesto.String
|
||||
k.Napomena = napomena.String
|
||||
|
||||
return &k, nil
|
||||
@@ -97,11 +99,11 @@ func (r *KlijentRepo) Kreiraj(ctx context.Context, k *model.Klijent) (int64, err
|
||||
k.Tip = "fizicko"
|
||||
}
|
||||
rezultat, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO klijenti (tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, napomena)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
INSERT INTO klijenti (tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, mesto, napomena)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
k.Tip, nullString(k.Ime), nullString(k.Prezime), nullString(k.JMBG),
|
||||
nullString(k.NazivFirme), nullString(k.PIB), nullString(k.Telefon),
|
||||
nullString(k.Email), nullString(k.Napomena),
|
||||
nullString(k.Email), nullString(k.Mesto), nullString(k.Napomena),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: KlijentRepo.Kreiraj: %w", err)
|
||||
@@ -123,11 +125,11 @@ func (r *KlijentRepo) Izmeni(ctx context.Context, k *model.Klijent) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE klijenti SET
|
||||
tip = ?, ime = ?, prezime = ?, jmbg = ?, naziv_firme = ?,
|
||||
pib = ?, telefon = ?, email = ?, napomena = ?
|
||||
pib = ?, telefon = ?, email = ?, mesto = ?, napomena = ?
|
||||
WHERE id = ?`,
|
||||
k.Tip, nullString(k.Ime), nullString(k.Prezime), nullString(k.JMBG),
|
||||
nullString(k.NazivFirme), nullString(k.PIB), nullString(k.Telefon),
|
||||
nullString(k.Email), nullString(k.Napomena), k.ID,
|
||||
nullString(k.Email), nullString(k.Mesto), nullString(k.Napomena), k.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: KlijentRepo.Izmeni: %w", err)
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
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, ''), izvor, izvor_id, 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() {
|
||||
k, err := skenirajKir(redovi.Scan)
|
||||
if 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) {
|
||||
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, ''), 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, `
|
||||
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, 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,
|
||||
izvorIliRucno(k.Izvor), izvorIDArg(k.IzvorID))
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
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, ''), izvor, izvor_id, 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, ''), izvor, izvor_id, 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
|
||||
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.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
|
||||
}
|
||||
|
||||
// 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, 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,
|
||||
izvorIliRucno(k.Izvor), izvorIDArg(k.IzvorID))
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// PdvStopaRepo je SQLite implementacija PdvStopaRepository interfejsa
|
||||
type PdvStopaRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NoviPdvStopaRepo kreira novi PdvStopaRepo
|
||||
func NoviPdvStopaRepo(db *sql.DB) *PdvStopaRepo {
|
||||
return &PdvStopaRepo{db: db}
|
||||
}
|
||||
|
||||
// Lista vraća stope iz šifarnika; ako je samoAktivne true, izostavlja arhivirane
|
||||
func (r *PdvStopaRepo) Lista(ctx context.Context, samoAktivne bool) ([]model.PdvStopa, error) {
|
||||
upit := `
|
||||
SELECT id, naziv, stopa, oznaka, aktivna, redosled, datum_unosa
|
||||
FROM pdv_stope
|
||||
WHERE 1=1`
|
||||
if samoAktivne {
|
||||
upit += " AND aktivna = 1"
|
||||
}
|
||||
upit += " ORDER BY redosled ASC, stopa DESC"
|
||||
|
||||
redovi, err := r.db.QueryContext(ctx, upit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: PdvStopaRepo.Lista: %w", err)
|
||||
}
|
||||
defer redovi.Close()
|
||||
|
||||
var rezultat []model.PdvStopa
|
||||
for redovi.Next() {
|
||||
var s model.PdvStopa
|
||||
if err := redovi.Scan(&s.ID, &s.Naziv, &s.Stopa, &s.Oznaka, &s.Aktivna, &s.Redosled, &s.DatumUnosa); err != nil {
|
||||
return nil, fmt.Errorf("ntech: PdvStopaRepo.Lista: scan: %w", err)
|
||||
}
|
||||
rezultat = append(rezultat, s)
|
||||
}
|
||||
if err := redovi.Err(); err != nil {
|
||||
return nil, fmt.Errorf("ntech: PdvStopaRepo.Lista: %w", err)
|
||||
}
|
||||
return rezultat, nil
|
||||
}
|
||||
|
||||
// DohvatiID vraća jednu stopu po identifikatoru
|
||||
func (r *PdvStopaRepo) DohvatiID(ctx context.Context, id int64) (*model.PdvStopa, error) {
|
||||
var s model.PdvStopa
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, naziv, stopa, oznaka, aktivna, redosled, datum_unosa
|
||||
FROM pdv_stope WHERE id = ?`, id).
|
||||
Scan(&s.ID, &s.Naziv, &s.Stopa, &s.Oznaka, &s.Aktivna, &s.Redosled, &s.DatumUnosa)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: PdvStopaRepo.DohvatiID: %w", err)
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// Kreiraj dodaje novu stopu i vraća njen id
|
||||
func (r *PdvStopaRepo) Kreiraj(ctx context.Context, s *model.PdvStopa) (int64, error) {
|
||||
rez, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO pdv_stope (naziv, stopa, oznaka, aktivna, redosled)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
s.Naziv, s.Stopa, s.Oznaka, s.Aktivna, s.Redosled)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: PdvStopaRepo.Kreiraj: %w", err)
|
||||
}
|
||||
id, err := rez.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: PdvStopaRepo.Kreiraj: id: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Izmeni menja podatke postojeće stope (osim datuma unosa)
|
||||
func (r *PdvStopaRepo) Izmeni(ctx context.Context, s *model.PdvStopa) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE pdv_stope
|
||||
SET naziv = ?, stopa = ?, oznaka = ?, aktivna = ?, redosled = ?
|
||||
WHERE id = ?`,
|
||||
s.Naziv, s.Stopa, s.Oznaka, s.Aktivna, s.Redosled, s.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: PdvStopaRepo.Izmeni: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostaviAktivnu arhivira (false) ili vraća u upotrebu (true) stopu, bez brisanja
|
||||
func (r *PdvStopaRepo) PostaviAktivnu(ctx context.Context, id int64, aktivna bool) error {
|
||||
_, err := r.db.ExecContext(ctx, "UPDATE pdv_stope SET aktivna = ? WHERE id = ?", aktivna, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: PdvStopaRepo.PostaviAktivnu: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
func TestPdvStopaRepo(t *testing.T) {
|
||||
db := testDB(t)
|
||||
repo := NoviPdvStopaRepo(db)
|
||||
ctx := context.Background()
|
||||
|
||||
// migracija 040 seeduje 3 stope; redosled ASC → opšta(20), posebna(10), oslobođeno(0)
|
||||
t.Run("seed iz migracije", func(t *testing.T) {
|
||||
stope, err := repo.Lista(ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Lista: %v", err)
|
||||
}
|
||||
if len(stope) != 3 {
|
||||
t.Fatalf("očekivano 3 seed stope, dobijeno %d", len(stope))
|
||||
}
|
||||
if stope[0].Stopa != 20 || stope[0].Oznaka != "opsta" {
|
||||
t.Errorf("prva stopa = %v %q, očekivano 20 opsta", stope[0].Stopa, stope[0].Oznaka)
|
||||
}
|
||||
if stope[2].Stopa != 0 || stope[2].Oznaka != "oslobodjeno" {
|
||||
t.Errorf("treća stopa = %v %q, očekivano 0 oslobodjeno", stope[2].Stopa, stope[2].Oznaka)
|
||||
}
|
||||
})
|
||||
|
||||
var novaID int64
|
||||
|
||||
t.Run("kreiraj i dohvati", func(t *testing.T) {
|
||||
id, err := repo.Kreiraj(ctx, &model.PdvStopa{
|
||||
Naziv: "Test stopa", Stopa: 11.5, Oznaka: "posebna", Aktivna: true, Redosled: 9,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Kreiraj: %v", err)
|
||||
}
|
||||
novaID = id
|
||||
s, err := repo.DohvatiID(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("DohvatiID: %v", err)
|
||||
}
|
||||
if s.Naziv != "Test stopa" || s.Stopa != 11.5 || s.Oznaka != "posebna" || !s.Aktivna || s.Redosled != 9 {
|
||||
t.Errorf("dohvaćeno %+v, ne poklapa se sa unetim", s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("izmeni", func(t *testing.T) {
|
||||
err := repo.Izmeni(ctx, &model.PdvStopa{
|
||||
ID: novaID, Naziv: "Izmenjena", Stopa: 12, Oznaka: "opsta", Aktivna: true, Redosled: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Izmeni: %v", err)
|
||||
}
|
||||
s, err := repo.DohvatiID(ctx, novaID)
|
||||
if err != nil {
|
||||
t.Fatalf("DohvatiID posle izmene: %v", err)
|
||||
}
|
||||
if s.Naziv != "Izmenjena" || s.Stopa != 12 || s.Oznaka != "opsta" || s.Redosled != 5 {
|
||||
t.Errorf("posle izmene %+v, ne poklapa se sa izmenom", s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("arhiviranje izostavlja iz aktivnih ali zadržava u punoj listi", func(t *testing.T) {
|
||||
if err := repo.PostaviAktivnu(ctx, novaID, false); err != nil {
|
||||
t.Fatalf("PostaviAktivnu: %v", err)
|
||||
}
|
||||
aktivne, err := repo.Lista(ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Lista aktivne: %v", err)
|
||||
}
|
||||
for _, s := range aktivne {
|
||||
if s.ID == novaID {
|
||||
t.Errorf("arhivirana stopa %d ne sme biti među aktivnima", novaID)
|
||||
}
|
||||
}
|
||||
sve, err := repo.Lista(ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Lista sve: %v", err)
|
||||
}
|
||||
nadjena := false
|
||||
for _, s := range sve {
|
||||
if s.ID == novaID {
|
||||
nadjena = true
|
||||
if s.Aktivna {
|
||||
t.Errorf("stopa %d treba da bude aktivna=false posle arhiviranja", novaID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !nadjena {
|
||||
t.Errorf("arhivirana stopa %d mora ostati u punoj listi (ne briše se)", novaID)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -85,7 +86,15 @@ func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
dobavljac, greska := parseFormuDobavljaca(r)
|
||||
jeAjax := r.Header.Get("X-Requested-With") == "fetch"
|
||||
if greska != "" {
|
||||
// fetch zahtev (iz modala u nabavci) dobija JSON grešku
|
||||
if jeAjax {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, `{"greska":%q}`, greska)
|
||||
return
|
||||
}
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "dobavljaci"
|
||||
@@ -99,11 +108,19 @@ func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.DobavljaciRepo.Kreiraj(r.Context(), &dobavljac); err != nil {
|
||||
id, err := h.DobavljaciRepo.Kreiraj(r.Context(), &dobavljac)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri čuvanju dobavljača", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// fetch zahtev (iz modala) dobija JSON sa ID-em i nazivom novog dobavljača
|
||||
if jeAjax {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"id":%d,"naziv":%q}`, id, dobavljac.Naziv)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/dobavljaci?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -214,6 +231,8 @@ func parseFormuDobavljaca(r *http.Request) (model.Dobavljac, string) {
|
||||
KontaktOsoba: strings.TrimSpace(r.FormValue("kontakt_osoba")),
|
||||
Telefon: strings.TrimSpace(r.FormValue("telefon")),
|
||||
Email: email,
|
||||
PIB: strings.TrimSpace(r.FormValue("pib")),
|
||||
Mesto: strings.TrimSpace(r.FormValue("mesto")),
|
||||
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
||||
}, ""
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"ntech/internal/config"
|
||||
"ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
@@ -34,6 +36,9 @@ type Handler struct {
|
||||
PokusajiRepo db.PokusajiPrijaveRepository
|
||||
LoginIstorijsaRepo db.LoginIstorijsaRepository
|
||||
DozvoleRepo db.DozvoleRepository
|
||||
PdvStopeRepo db.PdvStopaRepository
|
||||
PdvKirRepo db.PdvKirRepository
|
||||
PdvKprRepo db.PdvKprRepository
|
||||
Verzija string
|
||||
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
|
||||
Templates map[string]*template.Template
|
||||
@@ -93,6 +98,9 @@ func Novi(baza *sql.DB, totpKljuc []byte) *Handler {
|
||||
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
|
||||
LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza),
|
||||
DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()),
|
||||
PdvStopeRepo: sqlite.NoviPdvStopaRepo(baza),
|
||||
PdvKirRepo: sqlite.NoviPdvKirRepo(baza),
|
||||
PdvKprRepo: sqlite.NoviPdvKprRepo(baza),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +126,19 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
|
||||
h.PokusajiRepo = sqlite.NoviPokusajiPrijaveRepo(novaDB)
|
||||
h.LoginIstorijsaRepo = sqlite.NoviLoginIstorijsaRepo(novaDB)
|
||||
h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije())
|
||||
h.PdvStopeRepo = sqlite.NoviPdvStopaRepo(novaDB)
|
||||
h.PdvKirRepo = sqlite.NoviPdvKirRepo(novaDB)
|
||||
h.PdvKprRepo = sqlite.NoviPdvKprRepo(novaDB)
|
||||
}
|
||||
|
||||
// modulUkljucen vraća da li je zakonski modul (npr. „pdv") uključen za firmu prema profilu.
|
||||
// Koristi se pri automatskom punjenju KIR/KPR iz prodaje/nabavke.
|
||||
func (h *Handler) modulUkljucen(ctx context.Context, modul string) bool {
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(ctx, h.DB)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return config.ModulUkljucen(podesavanja, modul)
|
||||
}
|
||||
|
||||
// zahtevajDozvolu vraća prijavljenog korisnika ako njegova uloga sme da izvrši akciju.
|
||||
@@ -159,6 +180,8 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
||||
ps.CsrfToken = middleware.CsrfToken(r.Context())
|
||||
ps.AssetV = h.AssetV
|
||||
ps.Flash = middleware.GetFlash(r, h.DB)
|
||||
// uključeni zakonski moduli prema profilu firme — šabloni ih koriste za uslovni meni
|
||||
ps.Moduli = config.SviModuli(podesavanja)
|
||||
|
||||
// logika pozadine:
|
||||
// - lična pozadina → uvek se prikazuje i forsira tamnu temu, bez obzira na KoristiLokalnuTemu
|
||||
|
||||
@@ -25,6 +25,10 @@ var saSidebar = []string{
|
||||
"magacin", "magacin_forma",
|
||||
"nabavke", "nabavka_forma", "nabavka_detalji",
|
||||
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
|
||||
"pdv_stope",
|
||||
"pdv_kir", "pdv_kir_forma",
|
||||
"pdv_kpr", "pdv_kpr_forma",
|
||||
"pdv_obracun",
|
||||
"podsetnici", "podsetnik_forma",
|
||||
"profil_tema",
|
||||
"prodaja", "prodaja_detalji", "prodaja_forma",
|
||||
|
||||
@@ -249,6 +249,7 @@ func parseFormuKlijenta(r *http.Request) (model.Klijent, string) {
|
||||
PIB: strings.TrimSpace(r.FormValue("pib")),
|
||||
Telefon: strings.TrimSpace(r.FormValue("telefon")),
|
||||
Email: email,
|
||||
Mesto: strings.TrimSpace(r.FormValue("mesto")),
|
||||
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
||||
}, ""
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -26,7 +27,7 @@ type PodaciNabavki struct {
|
||||
type PodaciFormeNabavke struct {
|
||||
model.PodaciStranice
|
||||
Artikli []model.ArtikalSaKategorijom
|
||||
ArtikliJSON template.JS // JSON niz artikala za Alpine.js — bezbedan za umetanje u <script>
|
||||
ArtikliJSON template.JS // JSON niz artikala za Alpine.js — bezbedan za umetanje u <script>
|
||||
Dobavljaci []model.Dobavljac
|
||||
Kategorije []model.Kategorija // za dropdown u modalu novog artikla
|
||||
Greska string
|
||||
@@ -155,6 +156,32 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// automatski zavedi u KPR ako je firma PDV obveznik; PDV se izvodi iz stope artikla
|
||||
if h.modulUkljucen(r.Context(), "pdv") {
|
||||
var stavkePdv []model.NabavkaStavkaPdv
|
||||
for _, s := range stavke {
|
||||
var stopa float64
|
||||
if a, e := h.Artikli.DohvatiID(r.Context(), s.ArtikalID); e == nil {
|
||||
stopa = a.PdvStopa
|
||||
}
|
||||
stavkePdv = append(stavkePdv, model.NabavkaStavkaPdv{
|
||||
Osnovica: float64(s.Kolicina) * s.CenaPoKomadu,
|
||||
PdvStopa: stopa,
|
||||
})
|
||||
}
|
||||
naziv, pib, mesto := "Nepoznat dobavljač", "", ""
|
||||
if nabavka.DobavljacID != nil {
|
||||
if d, e := h.DobavljaciRepo.DohvatiID(r.Context(), *nabavka.DobavljacID); e == nil {
|
||||
naziv, pib, mesto = d.Naziv, d.PIB, d.Mesto
|
||||
}
|
||||
}
|
||||
nabavka.ID = id
|
||||
kpr := model.KprIzNabavke(nabavka, naziv, pib, mesto, stavkePdv)
|
||||
if _, e := h.PdvKprRepo.Kreiraj(r.Context(), &kpr); e != nil {
|
||||
slog.Error("auto-upis u KPR nije uspeo", "nabavka_id", id, "error", e)
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -221,6 +248,10 @@ func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Greška pri brisanju nabavke", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// ukloni vezani auto-KPR zapis (ako ga je ova nabavka kreirala)
|
||||
if err := h.PdvKprRepo.ObrisiPoIzvoru(r.Context(), "nabavka", id); err != nil {
|
||||
slog.Error("brisanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/nabavke?obrisan=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// PodaciPdvKir su podaci za pregled knjige izdatih računa
|
||||
type PodaciPdvKir struct {
|
||||
model.PodaciStranice
|
||||
Zapisi []model.PdvKir
|
||||
Sume model.PdvKirSume
|
||||
Od string // filter perioda (YYYY-MM-DD), prazno = bez granice
|
||||
Do string
|
||||
}
|
||||
|
||||
// PodaciPdvKirForma su podaci za formu unosa zapisa KIR
|
||||
type PodaciPdvKirForma struct {
|
||||
model.PodaciStranice
|
||||
Greska string
|
||||
Danas string // podrazumevani datum u formi
|
||||
Klijenti []model.Klijent // za izbor kupca iz postojećih klijenata
|
||||
}
|
||||
|
||||
// parsiraDatumOpcionalno vraća datum iz YYYY-MM-DD; prazan string daje nulti datum (bez filtera)
|
||||
func parsiraDatumOpcionalno(s string) time.Time {
|
||||
t, err := time.Parse("2006-01-02", strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// parsiraIznos čita decimalni broj iz forme (prihvata i zarez); prazno/neispravno daje 0
|
||||
func parsiraIznos(s string) float64 {
|
||||
v, err := strconv.ParseFloat(strings.TrimSpace(strings.Replace(s, ",", ".", 1)), 64)
|
||||
if err != nil || v < 0 {
|
||||
return 0
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// PdvKir renderuje pregled knjige izdatih računa sa sumama po stopama
|
||||
func (h *Handler) PdvKir(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.pregled"); !ok {
|
||||
return
|
||||
}
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
odStr := r.URL.Query().Get("od")
|
||||
doStr := r.URL.Query().Get("do")
|
||||
zapisi, err := h.PdvKirRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr))
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju knjige izdatih računa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "pdv-kir"
|
||||
ps.NaslovStranice = "KIR — knjiga izdatih računa"
|
||||
h.renderujTemplate(w, "pdv_kir", PodaciPdvKir{
|
||||
PodaciStranice: ps,
|
||||
Zapisi: zapisi,
|
||||
Sume: model.SumirajKir(zapisi),
|
||||
Od: odStr,
|
||||
Do: doStr,
|
||||
})
|
||||
}
|
||||
|
||||
// NoviPdvKir prikazuje praznu formu za unos zapisa u KIR
|
||||
func (h *Handler) NoviPdvKir(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.dodaj"); !ok {
|
||||
return
|
||||
}
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// klijenti za izbor kupca; greška se ne prekida (forma radi i sa ručnim unosom)
|
||||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "pdv-kir"
|
||||
ps.NaslovStranice = "Novi izlazni račun (KIR)"
|
||||
h.renderujTemplate(w, "pdv_kir_forma", PodaciPdvKirForma{
|
||||
PodaciStranice: ps,
|
||||
Danas: time.Now().Format("2006-01-02"),
|
||||
Klijenti: klijenti,
|
||||
})
|
||||
}
|
||||
|
||||
// SacuvajPdvKir prima POST i upisuje novi zapis u KIR
|
||||
func (h *Handler) SacuvajPdvKir(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.dodaj"); !ok {
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
datumPrometa, e1 := time.Parse("2006-01-02", strings.TrimSpace(r.FormValue("datum_prometa")))
|
||||
datumKnjizenja, e2 := time.Parse("2006-01-02", strings.TrimSpace(r.FormValue("datum_knjizenja")))
|
||||
brojDokumenta := strings.TrimSpace(r.FormValue("broj_dokumenta"))
|
||||
kupacNaziv := strings.TrimSpace(r.FormValue("kupac_naziv"))
|
||||
|
||||
greska := ""
|
||||
switch {
|
||||
case e1 != nil:
|
||||
greska = "Datum prometa je obavezan i mora biti ispravan."
|
||||
case e2 != nil:
|
||||
greska = "Datum knjiženja je obavezan i mora biti ispravan."
|
||||
case brojDokumenta == "":
|
||||
greska = "Broj dokumenta je obavezan."
|
||||
case kupacNaziv == "":
|
||||
greska = "Naziv kupca je obavezan."
|
||||
}
|
||||
if greska != "" {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", greska)
|
||||
http.Redirect(w, r, "/pdv/kir/nova", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
z := model.PdvKir{
|
||||
DatumPrometa: datumPrometa,
|
||||
DatumKnjizenja: datumKnjizenja,
|
||||
BrojDokumenta: brojDokumenta,
|
||||
KupacNaziv: kupacNaziv,
|
||||
KupacPib: strings.TrimSpace(r.FormValue("kupac_pib")),
|
||||
KupacMesto: strings.TrimSpace(r.FormValue("kupac_mesto")),
|
||||
OsnovicaOpsta: parsiraIznos(r.FormValue("osnovica_opsta")),
|
||||
PdvOpsta: parsiraIznos(r.FormValue("pdv_opsta")),
|
||||
OsnovicaPosebna: parsiraIznos(r.FormValue("osnovica_posebna")),
|
||||
PdvPosebna: parsiraIznos(r.FormValue("pdv_posebna")),
|
||||
OslobodenSaPravom: parsiraIznos(r.FormValue("osloboden_sa_pravom")),
|
||||
OslobodenBezPrava: parsiraIznos(r.FormValue("osloboden_bez_prava")),
|
||||
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
||||
}
|
||||
// ukupna naknada sa PDV — zbir svih osnovica, PDV-a i oslobođenog prometa (računa server)
|
||||
z.Ukupno = z.OsnovicaOpsta + z.PdvOpsta + z.OsnovicaPosebna + z.PdvPosebna +
|
||||
z.OslobodenSaPravom + z.OslobodenBezPrava
|
||||
|
||||
if _, err := h.PdvKirRepo.Kreiraj(r.Context(), &z); err != nil {
|
||||
http.Error(w, "Greška pri čuvanju zapisa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Izlazni račun je dodat u KIR.")
|
||||
http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ObrisiPdvKir briše zapis iz KIR
|
||||
func (h *Handler) ObrisiPdvKir(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.obrisi"); !ok {
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID zapisa", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.PdvKirRepo.Obrisi(r.Context(), id); err != nil {
|
||||
http.Error(w, "Greška pri brisanju zapisa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Zapis je obrisan iz KIR.")
|
||||
http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// PodaciPdvKpr su podaci za pregled knjige primljenih računa
|
||||
type PodaciPdvKpr struct {
|
||||
model.PodaciStranice
|
||||
Zapisi []model.PdvKpr
|
||||
Sume model.PdvKprSume
|
||||
Od string
|
||||
Do string
|
||||
}
|
||||
|
||||
// PodaciPdvKprForma su podaci za formu unosa zapisa KPR
|
||||
type PodaciPdvKprForma struct {
|
||||
model.PodaciStranice
|
||||
Greska string
|
||||
Danas string
|
||||
Dobavljaci []model.Dobavljac // za izbor dobavljača iz postojećih
|
||||
}
|
||||
|
||||
// PdvKpr renderuje pregled knjige primljenih računa sa sumama po stopama
|
||||
func (h *Handler) PdvKpr(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.pregled"); !ok {
|
||||
return
|
||||
}
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
odStr := r.URL.Query().Get("od")
|
||||
doStr := r.URL.Query().Get("do")
|
||||
zapisi, err := h.PdvKprRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr))
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju knjige primljenih računa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "pdv-kpr"
|
||||
ps.NaslovStranice = "KPR — knjiga primljenih računa"
|
||||
h.renderujTemplate(w, "pdv_kpr", PodaciPdvKpr{
|
||||
PodaciStranice: ps,
|
||||
Zapisi: zapisi,
|
||||
Sume: model.SumirajKpr(zapisi),
|
||||
Od: odStr,
|
||||
Do: doStr,
|
||||
})
|
||||
}
|
||||
|
||||
// NoviPdvKpr prikazuje praznu formu za unos zapisa u KPR
|
||||
func (h *Handler) NoviPdvKpr(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.dodaj"); !ok {
|
||||
return
|
||||
}
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
dobavljaci, _ := h.DobavljaciRepo.Lista(r.Context(), "")
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "pdv-kpr"
|
||||
ps.NaslovStranice = "Novi ulazni račun (KPR)"
|
||||
h.renderujTemplate(w, "pdv_kpr_forma", PodaciPdvKprForma{
|
||||
PodaciStranice: ps,
|
||||
Danas: time.Now().Format("2006-01-02"),
|
||||
Dobavljaci: dobavljaci,
|
||||
})
|
||||
}
|
||||
|
||||
// SacuvajPdvKpr prima POST i upisuje novi zapis u KPR
|
||||
func (h *Handler) SacuvajPdvKpr(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.dodaj"); !ok {
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
datumPrometa, e1 := time.Parse("2006-01-02", strings.TrimSpace(r.FormValue("datum_prometa")))
|
||||
datumKnjizenja, e2 := time.Parse("2006-01-02", strings.TrimSpace(r.FormValue("datum_knjizenja")))
|
||||
brojDokumenta := strings.TrimSpace(r.FormValue("broj_dokumenta"))
|
||||
dobavljacNaziv := strings.TrimSpace(r.FormValue("dobavljac_naziv"))
|
||||
|
||||
greska := ""
|
||||
switch {
|
||||
case e1 != nil:
|
||||
greska = "Datum prometa je obavezan i mora biti ispravan."
|
||||
case e2 != nil:
|
||||
greska = "Datum knjiženja je obavezan i mora biti ispravan."
|
||||
case brojDokumenta == "":
|
||||
greska = "Broj dokumenta je obavezan."
|
||||
case dobavljacNaziv == "":
|
||||
greska = "Naziv dobavljača je obavezan."
|
||||
}
|
||||
if greska != "" {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", greska)
|
||||
http.Redirect(w, r, "/pdv/kpr/nova", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
z := model.PdvKpr{
|
||||
DatumPrometa: datumPrometa,
|
||||
DatumKnjizenja: datumKnjizenja,
|
||||
BrojDokumenta: brojDokumenta,
|
||||
DobavljacNaziv: dobavljacNaziv,
|
||||
DobavljacPib: strings.TrimSpace(r.FormValue("dobavljac_pib")),
|
||||
DobavljacMesto: strings.TrimSpace(r.FormValue("dobavljac_mesto")),
|
||||
OsnovicaOpsta: parsiraIznos(r.FormValue("osnovica_opsta")),
|
||||
PdvOpsta: parsiraIznos(r.FormValue("pdv_opsta")),
|
||||
OsnovicaPosebna: parsiraIznos(r.FormValue("osnovica_posebna")),
|
||||
PdvPosebna: parsiraIznos(r.FormValue("pdv_posebna")),
|
||||
PdvBezOdbitka: parsiraIznos(r.FormValue("pdv_bez_odbitka")),
|
||||
OslobodenNabavka: parsiraIznos(r.FormValue("osloboden_nabavka")),
|
||||
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
||||
}
|
||||
// datum plaćanja je opcionalan
|
||||
if dp := parsiraDatumOpcionalno(r.FormValue("datum_placanja")); !dp.IsZero() {
|
||||
z.DatumPlacanja = &dp
|
||||
}
|
||||
// ukupna vrednost računa — zbir osnovica, PDV-a i oslobođene nabavke (računa server)
|
||||
z.Ukupno = z.OsnovicaOpsta + z.PdvOpsta + z.OsnovicaPosebna + z.PdvPosebna +
|
||||
z.PdvBezOdbitka + z.OslobodenNabavka
|
||||
|
||||
if _, err := h.PdvKprRepo.Kreiraj(r.Context(), &z); err != nil {
|
||||
http.Error(w, "Greška pri čuvanju zapisa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Ulazni račun je dodat u KPR.")
|
||||
http.Redirect(w, r, "/pdv/kpr", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ObrisiPdvKpr briše zapis iz KPR
|
||||
func (h *Handler) ObrisiPdvKpr(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.obrisi"); !ok {
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID zapisa", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.PdvKprRepo.Obrisi(r.Context(), id); err != nil {
|
||||
http.Error(w, "Greška pri brisanju zapisa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Zapis je obrisan iz KPR.")
|
||||
http.Redirect(w, r, "/pdv/kpr", http.StatusSeeOther)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// PodaciPdvObracun su podaci za stranicu obračuna PDV za period
|
||||
type PodaciPdvObracun struct {
|
||||
model.PodaciStranice
|
||||
Od string
|
||||
Do string
|
||||
KirSume model.PdvKirSume
|
||||
KprSume model.PdvKprSume
|
||||
Obracun model.PdvObracun
|
||||
PPPDV model.PPPDV
|
||||
}
|
||||
|
||||
// PdvObracunStranica računa obavezu PDV za izabrani period.
|
||||
// Kada period nije zadat, podrazumevano se uzima tekući mesec.
|
||||
func (h *Handler) PdvObracunStranica(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "pdv.pregled"); !ok {
|
||||
return
|
||||
}
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
odStr := r.URL.Query().Get("od")
|
||||
doStr := r.URL.Query().Get("do")
|
||||
// podrazumevani period: tekući mesec (od prvog do poslednjeg dana)
|
||||
if odStr == "" && doStr == "" {
|
||||
sada := time.Now()
|
||||
prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
|
||||
poslednji := prvi.AddDate(0, 1, -1)
|
||||
odStr = prvi.Format("2006-01-02")
|
||||
doStr = poslednji.Format("2006-01-02")
|
||||
}
|
||||
|
||||
od := parsiraDatumOpcionalno(odStr)
|
||||
do := parsiraDatumOpcionalno(doStr)
|
||||
|
||||
kirZapisi, err := h.PdvKirRepo.Lista(r.Context(), od, do)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju knjige izdatih računa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
kprZapisi, err := h.PdvKprRepo.Lista(r.Context(), od, do)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju knjige primljenih računa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
kirSume := model.SumirajKir(kirZapisi)
|
||||
kprSume := model.SumirajKpr(kprZapisi)
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "pdv-obracun"
|
||||
ps.NaslovStranice = "PDV obračun"
|
||||
h.renderujTemplate(w, "pdv_obracun", PodaciPdvObracun{
|
||||
PodaciStranice: ps,
|
||||
Od: odStr,
|
||||
Do: doStr,
|
||||
KirSume: kirSume,
|
||||
KprSume: kprSume,
|
||||
Obracun: model.ObracunajPdv(kirSume, kprSume),
|
||||
PPPDV: model.MapirajPPPDV(kirSume, kprSume),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// validneOznakeStope su dozvoljene oznake PDV stope (vrsta po zakonu)
|
||||
var validneOznakeStope = map[string]bool{
|
||||
"opsta": true,
|
||||
"posebna": true,
|
||||
"oslobodjeno": true,
|
||||
}
|
||||
|
||||
// PodaciPdvStope su podaci za stranicu šifarnika PDV stopa
|
||||
type PodaciPdvStope struct {
|
||||
model.PodaciStranice
|
||||
Stope []model.PdvStopa
|
||||
}
|
||||
|
||||
// PdvStope renderuje šifarnik PDV stopa (sve stope, uključujući arhivirane)
|
||||
func (h *Handler) PdvStope(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
|
||||
return
|
||||
}
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
stope, err := h.PdvStopeRepo.Lista(r.Context(), false)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju PDV stopa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "podesavanja-pdv-stope"
|
||||
ps.NaslovStranice = "PDV stope"
|
||||
h.renderujTemplate(w, "pdv_stope", PodaciPdvStope{PodaciStranice: ps, Stope: stope})
|
||||
}
|
||||
|
||||
// parsePdvStopuForma čita i proverava polja forme; vraća popunjenu stopu i poruku o grešci
|
||||
func parsePdvStopuForma(r *http.Request) (model.PdvStopa, string) {
|
||||
naziv := strings.TrimSpace(r.FormValue("naziv"))
|
||||
oznaka := strings.TrimSpace(r.FormValue("oznaka"))
|
||||
stopaTekst := strings.TrimSpace(strings.Replace(r.FormValue("stopa"), ",", ".", 1))
|
||||
redosled, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("redosled")))
|
||||
|
||||
if naziv == "" {
|
||||
return model.PdvStopa{}, "Naziv stope je obavezan."
|
||||
}
|
||||
if !validneOznakeStope[oznaka] {
|
||||
return model.PdvStopa{}, "Oznaka mora biti opšta, posebna ili oslobođeno."
|
||||
}
|
||||
stopa, err := strconv.ParseFloat(stopaTekst, 64)
|
||||
if err != nil || stopa < 0 || stopa > 100 {
|
||||
return model.PdvStopa{}, "Stopa mora biti broj između 0 i 100."
|
||||
}
|
||||
|
||||
return model.PdvStopa{
|
||||
Naziv: naziv,
|
||||
Stopa: stopa,
|
||||
Oznaka: oznaka,
|
||||
Aktivna: true,
|
||||
Redosled: redosled,
|
||||
}, ""
|
||||
}
|
||||
|
||||
// DodajPdvStopu prima POST i upisuje novu stopu u šifarnik
|
||||
func (h *Handler) DodajPdvStopu(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
stopa, greska := parsePdvStopuForma(r)
|
||||
if greska != "" {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", greska)
|
||||
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if _, err := h.PdvStopeRepo.Kreiraj(r.Context(), &stopa); err != nil {
|
||||
http.Error(w, "Greška pri čuvanju PDV stope", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je dodata.")
|
||||
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// IzmeniPdvStopu prima POST i menja postojeću stopu
|
||||
func (h *Handler) IzmeniPdvStopu(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID stope", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
stopa, greska := parsePdvStopuForma(r)
|
||||
if greska != "" {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", greska)
|
||||
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
stopa.ID = id
|
||||
if err := h.PdvStopeRepo.Izmeni(r.Context(), &stopa); err != nil {
|
||||
http.Error(w, "Greška pri izmeni PDV stope", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je izmenjena.")
|
||||
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// PromeniAktivnostPdvStope arhivira ili vraća stopu u upotrebu (toggle, bez brisanja)
|
||||
func (h *Handler) PromeniAktivnostPdvStope(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID stope", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
postojeca, err := h.PdvStopeRepo.DohvatiID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "PDV stopa nije pronađena", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err := h.PdvStopeRepo.PostaviAktivnu(r.Context(), id, !postojeca.Aktivna); err != nil {
|
||||
http.Error(w, "Greška pri promeni statusa PDV stope", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
poruka := "PDV stopa je arhivirana."
|
||||
if !postojeca.Aktivna {
|
||||
poruka = "PDV stopa je vraćena u upotrebu."
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", poruka)
|
||||
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||
}
|
||||
@@ -23,25 +23,30 @@ import (
|
||||
// PodaciPodesavanja su podaci za stranicu podešavanja
|
||||
type PodaciPodesavanja struct {
|
||||
model.PodaciStranice
|
||||
NazivFirme string
|
||||
Podnazlov string
|
||||
Adresa string
|
||||
Telefon string
|
||||
PIB string
|
||||
LogoTip string
|
||||
LogoPutanja string
|
||||
Sacuvano bool
|
||||
Verzija string
|
||||
LogoGreska string
|
||||
BackupVracen bool
|
||||
Backupi []BackupInfo
|
||||
BackupIntervalSati string
|
||||
BackupBrojKopija string
|
||||
LoginPozadina string
|
||||
LoginPozadinaOpacity string
|
||||
LoginPozadinaBlurPozadine string
|
||||
LoginPozadinaBlurKartice string
|
||||
LoginPozadinaZatamnjenjeKartice string
|
||||
NazivFirme string
|
||||
Podnazlov string
|
||||
Adresa string
|
||||
Telefon string
|
||||
PIB string
|
||||
LogoTip string
|
||||
LogoPutanja string
|
||||
// profil firme — pravni/poreski status (Faza 0); određuje koji se zakonski moduli pale
|
||||
FirmaPravniOblik string
|
||||
FirmaPdvObveznik string
|
||||
FirmaFiskalizacija string
|
||||
FirmaRezim string
|
||||
Sacuvano bool
|
||||
Verzija string
|
||||
LogoGreska string
|
||||
BackupVracen bool
|
||||
Backupi []BackupInfo
|
||||
BackupIntervalSati string
|
||||
BackupBrojKopija string
|
||||
LoginPozadina string
|
||||
LoginPozadinaOpacity string
|
||||
LoginPozadinaBlurPozadine string
|
||||
LoginPozadinaBlurKartice string
|
||||
LoginPozadinaZatamnjenjeKartice string
|
||||
}
|
||||
|
||||
// BackupInfo opisuje jedan backup fajl
|
||||
@@ -69,19 +74,19 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
ps.Stranica = "podesavanja"
|
||||
ps.NaslovStranice = "Podešavanja"
|
||||
podaci := PodaciPodesavanja{
|
||||
PodaciStranice: ps,
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
Adresa: podesavanja["adresa"],
|
||||
Telefon: podesavanja["telefon"],
|
||||
PIB: podesavanja["pib"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
||||
Verzija: h.Verzija,
|
||||
LogoGreska: r.URL.Query().Get("logo_greska"),
|
||||
Backupi: ucitajListuBackupa(),
|
||||
PodaciStranice: ps,
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
Adresa: podesavanja["adresa"],
|
||||
Telefon: podesavanja["telefon"],
|
||||
PIB: podesavanja["pib"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
||||
Verzija: h.Verzija,
|
||||
LogoGreska: r.URL.Query().Get("logo_greska"),
|
||||
Backupi: ucitajListuBackupa(),
|
||||
LoginPozadina: podesavanja["login_pozadina"],
|
||||
LoginPozadinaOpacity: vrednostIliDefault(podesavanja, "login_pozadina_opacity", "50"),
|
||||
LoginPozadinaBlurPozadine: vrednostIliDefault(podesavanja, "login_pozadina_blur_pozadine", "0"),
|
||||
@@ -234,6 +239,11 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
"telefon": r.FormValue("telefon"),
|
||||
"pib": r.FormValue("pib"),
|
||||
"logo_tip": r.FormValue("logo_tip"),
|
||||
// profil firme (Faza 0) — radio dugmad uvek šalju vrednost, pa se uredno čuvaju
|
||||
"firma_pravni_oblik": r.FormValue("firma_pravni_oblik"),
|
||||
"firma_pdv_obveznik": r.FormValue("firma_pdv_obveznik"),
|
||||
"firma_fiskalizacija": r.FormValue("firma_fiskalizacija"),
|
||||
"firma_rezim": r.FormValue("firma_rezim"),
|
||||
}
|
||||
|
||||
for kljuc, vrednost := range polja {
|
||||
@@ -566,10 +576,10 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
for kljuc, vrednost := range map[string]string{
|
||||
"login_pozadina_blur_pozadine": blurPozadineStr,
|
||||
"login_pozadina_blur_kartice": blurKarticeStr,
|
||||
"login_pozadina_opacity": opacityStr,
|
||||
"login_pozadina_zatamnjenje_kartice": zatamnjenjeKarticeStr,
|
||||
"login_pozadina_blur_pozadine": blurPozadineStr,
|
||||
"login_pozadina_blur_kartice": blurKarticeStr,
|
||||
"login_pozadina_opacity": opacityStr,
|
||||
"login_pozadina_zatamnjenje_kartice": zatamnjenjeKarticeStr,
|
||||
} {
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
|
||||
slog.Error("greška pri čuvanju stila login pozadine", "kljuc", kljuc, "error", err)
|
||||
@@ -593,19 +603,23 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
|
||||
ps.Stranica = "podesavanja"
|
||||
ps.NaslovStranice = naslov
|
||||
return PodaciPodesavanja{
|
||||
PodaciStranice: ps,
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
Adresa: podesavanja["adresa"],
|
||||
Telefon: podesavanja["telefon"],
|
||||
PIB: podesavanja["pib"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
||||
Verzija: h.Verzija,
|
||||
LogoGreska: r.URL.Query().Get("logo_greska"),
|
||||
Backupi: ucitajListuBackupa(),
|
||||
PodaciStranice: ps,
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
Adresa: podesavanja["adresa"],
|
||||
Telefon: podesavanja["telefon"],
|
||||
PIB: podesavanja["pib"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
FirmaPravniOblik: vrednostIliDefault(podesavanja, "firma_pravni_oblik", "pausalac"),
|
||||
FirmaPdvObveznik: vrednostIliDefault(podesavanja, "firma_pdv_obveznik", "ne"),
|
||||
FirmaFiskalizacija: vrednostIliDefault(podesavanja, "firma_fiskalizacija", "ne"),
|
||||
FirmaRezim: vrednostIliDefault(podesavanja, "firma_rezim", "samo_evidencija"),
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
||||
Verzija: h.Verzija,
|
||||
LogoGreska: r.URL.Query().Get("logo_greska"),
|
||||
Backupi: ucitajListuBackupa(),
|
||||
LoginPozadina: podesavanja["login_pozadina"],
|
||||
LoginPozadinaOpacity: vrednostIliDefault(podesavanja, "login_pozadina_opacity", "50"),
|
||||
LoginPozadinaBlurPozadine: vrednostIliDefault(podesavanja, "login_pozadina_blur_pozadine", "0"),
|
||||
@@ -657,4 +671,3 @@ func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
|
||||
podaci.Stranica = "podesavanja-sistem"
|
||||
h.renderujTemplate(w, "podesavanja_sistem", podaci)
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,22 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// automatski zavedi u KIR ako je firma PDV obveznik i prodaja je na klijenta (B2B faktura).
|
||||
// Maloprodaja građanima (bez klijenta) ide zbirno preko fiskalizacije (Faza 3) — preskače se.
|
||||
if nalog.KlijentID != nil && h.modulUkljucen(r.Context(), "pdv") {
|
||||
if klijent, e := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID); e == nil {
|
||||
nalog.ID = id
|
||||
pib := klijent.PIB
|
||||
if klijent.Tip != "pravno" {
|
||||
pib = klijent.JMBG
|
||||
}
|
||||
kir := model.KirIzProdaje(nalog, stavke, klijent.PunoIme(), pib, klijent.Mesto)
|
||||
if _, e := h.PdvKirRepo.Kreiraj(r.Context(), &kir); e != nil {
|
||||
slog.Error("auto-upis u KIR nije uspeo", "prodaja_id", id, "error", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -319,6 +335,10 @@ func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// ukloni vezani auto-KIR zapis (ako ga je ova prodaja kreirala)
|
||||
if err := h.PdvKirRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); err != nil {
|
||||
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/prodaja?obrisan=1", http.StatusSeeOther)
|
||||
}
|
||||
@@ -408,6 +428,10 @@ func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// stornirana prodaja ne ulazi u PDV — ukloni vezani auto-KIR zapis
|
||||
if err := h.PdvKirRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); err != nil {
|
||||
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Prodajni nalog je storniran.")
|
||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ var sveAkcije = []string{
|
||||
"backup.pokreni",
|
||||
"tema.lokalno",
|
||||
"dashboard.prihod",
|
||||
"pdv.pregled",
|
||||
"pdv.dodaj",
|
||||
"pdv.obrisi",
|
||||
}
|
||||
|
||||
// SveAkcije vraća listu svih poznatih akcija — koristi se pri inicijalizaciji baze i resetu
|
||||
@@ -95,6 +98,9 @@ func ImaDozvolu(uloga, akcija string) bool {
|
||||
// dashboard — prihod samo admin+
|
||||
case "dashboard.prihod":
|
||||
return true
|
||||
// PDV evidencija (KIR/KPR) — administrativno, radnik nema
|
||||
case "pdv.pregled", "pdv.dodaj", "pdv.obrisi":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RequireModul je chi middleware koji propušta zahtev samo ako je traženi zakonski
|
||||
// modul uključen za firmu (prema profilu firme iz podešavanja). Ovo je sloj IZNAD
|
||||
// RBAC-a: „da li firma uopšte koristi modul", nezavisno od „da li korisnik sme"
|
||||
// (RequireDozvola). Zahtev mora proći oba sloja.
|
||||
//
|
||||
// Provera se prosleđuje kao funkcija (proveri) da paket middleware ne zavisi od
|
||||
// config/sqlite — isti obrazac kao RequireDozvola. U praksi je to closure koja
|
||||
// učita podešavanja i pozove config.ModulUkljucen.
|
||||
func RequireModul(proveri func(ctx context.Context, modul string) bool, modul string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !proveri(r.Context(), modul) {
|
||||
postaviFlashGresku(w, "Ovaj modul nije uključen za vašu firmu.")
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ type Dobavljac struct {
|
||||
KontaktOsoba string
|
||||
Telefon string
|
||||
Email string
|
||||
PIB string
|
||||
Mesto string
|
||||
Napomena string
|
||||
DatumUnosa time.Time
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ type Klijent struct {
|
||||
PIB string
|
||||
Telefon string
|
||||
Email string
|
||||
Mesto string
|
||||
Napomena string
|
||||
DatumUnosa time.Time
|
||||
}
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"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
|
||||
Izvor string // "rucno" | "prodaja" | "nabavka"
|
||||
IzvorID *int64 // id izvornog naloga (nil za ručni unos)
|
||||
DatumUnosa time.Time
|
||||
}
|
||||
|
||||
// OslobodenUkupno vraća zbir oslobođenog prometa (sa i bez prava na odbitak).
|
||||
func (k PdvKir) OslobodenUkupno() float64 {
|
||||
return k.OslobodenSaPravom + k.OslobodenBezPrava
|
||||
}
|
||||
|
||||
// OznakaPoreskogBroja vraća „JMBG" ako uneti broj ima 13 cifara (fizičko lice),
|
||||
// inače „PIB" (pravno lice / preduzetnik — PIB ima 9 cifara).
|
||||
func (k PdvKir) OznakaPoreskogBroja() string {
|
||||
cifre := 0
|
||||
for _, r := range k.KupacPib {
|
||||
if r >= '0' && r <= '9' {
|
||||
cifre++
|
||||
}
|
||||
}
|
||||
if cifre == 13 {
|
||||
return "JMBG"
|
||||
}
|
||||
return "PIB"
|
||||
}
|
||||
|
||||
// PdvKirSume su zbirovi kolona KIR-a (za red „ukupno" u pregledu knjige).
|
||||
type PdvKirSume struct {
|
||||
OsnovicaOpsta float64
|
||||
PdvOpsta float64
|
||||
OsnovicaPosebna float64
|
||||
PdvPosebna float64
|
||||
OslobodenSaPravom float64
|
||||
OslobodenBezPrava float64
|
||||
Ukupno float64
|
||||
}
|
||||
|
||||
// OslobodenUkupno vraća zbir oslobođenog prometa (sa i bez prava na odbitak).
|
||||
func (s PdvKirSume) OslobodenUkupno() float64 {
|
||||
return s.OslobodenSaPravom + s.OslobodenBezPrava
|
||||
}
|
||||
|
||||
// SumirajKir sabira sve kolone iz liste KIR zapisa.
|
||||
func SumirajKir(zapisi []PdvKir) PdvKirSume {
|
||||
var s PdvKirSume
|
||||
for _, z := range zapisi {
|
||||
s.OsnovicaOpsta += z.OsnovicaOpsta
|
||||
s.PdvOpsta += z.PdvOpsta
|
||||
s.OsnovicaPosebna += z.OsnovicaPosebna
|
||||
s.PdvPosebna += z.PdvPosebna
|
||||
s.OslobodenSaPravom += z.OslobodenSaPravom
|
||||
s.OslobodenBezPrava += z.OslobodenBezPrava
|
||||
s.Ukupno += z.Ukupno
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// KirIzProdaje gradi KIR zapis iz prodaje: stavke se grupišu po PDV stopi
|
||||
// (20→opšta, 10→posebna, ostalo→oslobođeno). CenaPoKomadu je prodajna cena SA PDV,
|
||||
// pa se osnovica izvodi deljenjem sa (1 + stopa/100).
|
||||
func KirIzProdaje(nalog ProdajniNalog, stavke []StavkaProdaje, kupacNaziv, kupacPib, kupacMesto string) PdvKir {
|
||||
id := nalog.ID
|
||||
k := PdvKir{
|
||||
DatumPrometa: nalog.Datum,
|
||||
DatumKnjizenja: nalog.Datum,
|
||||
BrojDokumenta: nalog.BrojNaloga,
|
||||
KupacNaziv: kupacNaziv,
|
||||
KupacPib: kupacPib,
|
||||
KupacMesto: kupacMesto,
|
||||
Izvor: "prodaja",
|
||||
IzvorID: &id,
|
||||
}
|
||||
for _, s := range stavke {
|
||||
ukupnoLinija := float64(s.Kolicina) * s.CenaPoKomadu
|
||||
osnovica := ukupnoLinija
|
||||
if s.PdvStopa > 0 {
|
||||
osnovica = ukupnoLinija / (1 + s.PdvStopa/100)
|
||||
}
|
||||
pdv := ukupnoLinija - osnovica
|
||||
switch s.PdvStopa {
|
||||
case 20:
|
||||
k.OsnovicaOpsta += osnovica
|
||||
k.PdvOpsta += pdv
|
||||
case 10:
|
||||
k.OsnovicaPosebna += osnovica
|
||||
k.PdvPosebna += pdv
|
||||
default:
|
||||
// 0% / oslobođeno — osnovica bez PDV-a u oslobođen promet sa pravom na odbitak
|
||||
k.OslobodenSaPravom += osnovica
|
||||
}
|
||||
k.Ukupno += ukupnoLinija
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
// 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
|
||||
Izvor string // "rucno" | "prodaja" | "nabavka"
|
||||
IzvorID *int64 // id izvorne nabavke (nil za ručni unos)
|
||||
DatumUnosa time.Time
|
||||
}
|
||||
|
||||
// OznakaPoreskogBroja vraća „JMBG" za 13-cifreni broj, inače „PIB" (dobavljači su obično firme).
|
||||
func (k PdvKpr) OznakaPoreskogBroja() string {
|
||||
cifre := 0
|
||||
for _, r := range k.DobavljacPib {
|
||||
if r >= '0' && r <= '9' {
|
||||
cifre++
|
||||
}
|
||||
}
|
||||
if cifre == 13 {
|
||||
return "JMBG"
|
||||
}
|
||||
return "PIB"
|
||||
}
|
||||
|
||||
// PdvKprSume su zbirovi kolona KPR-a (za red „ukupno" u pregledu knjige).
|
||||
type PdvKprSume struct {
|
||||
OsnovicaOpsta float64
|
||||
PdvOpsta float64
|
||||
OsnovicaPosebna float64
|
||||
PdvPosebna float64
|
||||
PdvBezOdbitka float64
|
||||
OslobodenNabavka float64
|
||||
Ukupno float64
|
||||
}
|
||||
|
||||
// SumirajKpr sabira sve kolone iz liste KPR zapisa.
|
||||
func SumirajKpr(zapisi []PdvKpr) PdvKprSume {
|
||||
var s PdvKprSume
|
||||
for _, z := range zapisi {
|
||||
s.OsnovicaOpsta += z.OsnovicaOpsta
|
||||
s.PdvOpsta += z.PdvOpsta
|
||||
s.OsnovicaPosebna += z.OsnovicaPosebna
|
||||
s.PdvPosebna += z.PdvPosebna
|
||||
s.PdvBezOdbitka += z.PdvBezOdbitka
|
||||
s.OslobodenNabavka += z.OslobodenNabavka
|
||||
s.Ukupno += z.Ukupno
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// NabavkaStavkaPdv je jedna stavka nabavke sa pripadajućom PDV stopom (iz artikla).
|
||||
type NabavkaStavkaPdv struct {
|
||||
Osnovica float64 // nabavna vrednost stavke (cena × količina) — tretira se kao osnovica bez PDV
|
||||
PdvStopa float64
|
||||
}
|
||||
|
||||
// KprIzNabavke gradi KPR zapis iz nabavke. PDV se izvodi iz stope artikla po stavci
|
||||
// (⚠ aproksimacija: nabavna cena = osnovica bez PDV; stvaran PDV sa računa dobavljača može
|
||||
// se razlikovati). Grupiše po stopi (20→opšta, 10→posebna, ostalo→oslobođena nabavka).
|
||||
// Broj dokumenta je sintetički ("NAB-<id>") jer nabavka ne čuva broj računa dobavljača.
|
||||
func KprIzNabavke(nabavka Nabavka, dobavljacNaziv, dobavljacPib, dobavljacMesto string, stavke []NabavkaStavkaPdv) PdvKpr {
|
||||
id := nabavka.ID
|
||||
k := PdvKpr{
|
||||
DatumPrometa: nabavka.Datum,
|
||||
DatumKnjizenja: nabavka.Datum,
|
||||
BrojDokumenta: fmt.Sprintf("NAB-%d", id),
|
||||
DobavljacNaziv: dobavljacNaziv,
|
||||
DobavljacPib: dobavljacPib,
|
||||
DobavljacMesto: dobavljacMesto,
|
||||
Napomena: nabavka.Napomena,
|
||||
Izvor: "nabavka",
|
||||
IzvorID: &id,
|
||||
}
|
||||
for _, s := range stavke {
|
||||
pdv := s.Osnovica * s.PdvStopa / 100
|
||||
switch s.PdvStopa {
|
||||
case 20:
|
||||
k.OsnovicaOpsta += s.Osnovica
|
||||
k.PdvOpsta += pdv
|
||||
case 10:
|
||||
k.OsnovicaPosebna += s.Osnovica
|
||||
k.PdvPosebna += pdv
|
||||
default:
|
||||
k.OslobodenNabavka += s.Osnovica
|
||||
}
|
||||
k.Ukupno += s.Osnovica + pdv
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
// PdvObracun je rezultat obračuna PDV za period: izlazni (dugovani) PDV iz KIR,
|
||||
// prethodni (odbitni) PDV iz KPR i konačna obaveza. Pozitivna obaveza znači iznos
|
||||
// za uplatu, negativna iznos za povraćaj / prenos poreskog kredita u naredni period.
|
||||
type PdvObracun struct {
|
||||
// izlazni (dugovani) PDV — iz KIR, po stopama
|
||||
IzlazniPdvOpsta float64
|
||||
IzlazniPdvPosebna float64
|
||||
IzlazniPdvUkupno float64
|
||||
// prethodni (odbitni) PDV — iz KPR, po stopama (bez PDV bez prava na odbitak)
|
||||
OdbitniPdvOpsta float64
|
||||
OdbitniPdvPosebna float64
|
||||
OdbitniPdvUkupno float64
|
||||
// obaveza = izlazni − odbitni (>0 za uplatu, <0 za povraćaj/prenos)
|
||||
Obaveza float64
|
||||
}
|
||||
|
||||
// ZaUplatu vraća true kada postoji obaveza za uplatu (izlazni PDV veći od odbitnog).
|
||||
func (o PdvObracun) ZaUplatu() bool {
|
||||
return o.Obaveza > 0
|
||||
}
|
||||
|
||||
// ObavezaApsolutna vraća iznos obaveze bez predznaka (za prikaz povraćaja kao pozitivan broj).
|
||||
func (o PdvObracun) ObavezaApsolutna() float64 {
|
||||
if o.Obaveza < 0 {
|
||||
return -o.Obaveza
|
||||
}
|
||||
return o.Obaveza
|
||||
}
|
||||
|
||||
// PPPDV su vrednosti polja zvaničnog obrasca poreske prijave PDV (PPPDV).
|
||||
// Iznosi su u celim dinarima (obrazac ne dozvoljava decimale). Brojevi polja
|
||||
// odgovaraju obrascu: 001–005/103–105 (promet i izlazni PDV), 006–009/106–109
|
||||
// (prethodni/odbitni porez), 110 (obaveza = 105 − 109), Povracaj (polje 11).
|
||||
type PPPDV struct {
|
||||
// I. promet dobara i usluga
|
||||
Polje001 int64 // oslobođen sa pravom na odbitak (naknada)
|
||||
Polje002 int64 // oslobođen bez prava na odbitak (naknada)
|
||||
Polje003 int64 // opšta stopa — naknada bez PDV
|
||||
Polje103 int64 // opšta stopa — obračunati PDV
|
||||
Polje004 int64 // posebna stopa — naknada bez PDV
|
||||
Polje104 int64 // posebna stopa — obračunati PDV
|
||||
Polje005 int64 // zbir naknada (1+2+3+4)
|
||||
Polje105 int64 // zbir izlaznog PDV (103+104)
|
||||
// II. prethodni porez
|
||||
Polje006 int64 // prethodni porez pri uvozu — naknada (ne prati se → 0)
|
||||
Polje106 int64 // prethodni porez pri uvozu — PDV (ne prati se → 0)
|
||||
Polje007 int64 // PDV nadoknada poljoprivredniku — naknada (ne prati se → 0)
|
||||
Polje107 int64 // PDV nadoknada poljoprivredniku — PDV (ne prati se → 0)
|
||||
Polje008 int64 // ostali prethodni porez — naknada
|
||||
Polje108 int64 // ostali prethodni porez — PDV (odbitni)
|
||||
Polje009 int64 // zbir naknada (6+7+8)
|
||||
Polje109 int64 // zbir prethodnog poreza (106+107+108)
|
||||
// III. poreska obaveza
|
||||
Polje110 int64 // iznos PDV u periodu (105 − 109); >0 za uplatu, <0 za povraćaj
|
||||
Povracaj bool // polje 11: true kada je 110 negativan (iznos za povraćaj)
|
||||
}
|
||||
|
||||
// Polje110Apsolutno vraća iznos polja 110 bez predznaka (za prikaz povraćaja).
|
||||
func (p PPPDV) Polje110Apsolutno() int64 {
|
||||
if p.Polje110 < 0 {
|
||||
return -p.Polje110
|
||||
}
|
||||
return p.Polje110
|
||||
}
|
||||
|
||||
// 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
|
||||
// pa ostaju 0 — sav odbitni PDV iz KPR pada u polje 008/108.
|
||||
func MapirajPPPDV(kir PdvKirSume, kpr PdvKprSume) PPPDV {
|
||||
uDinare := func(v float64) int64 { return int64(math.Round(v)) }
|
||||
p := PPPDV{
|
||||
Polje001: uDinare(kir.OslobodenSaPravom),
|
||||
Polje002: uDinare(kir.OslobodenBezPrava),
|
||||
Polje003: uDinare(kir.OsnovicaOpsta),
|
||||
Polje103: uDinare(kir.PdvOpsta),
|
||||
Polje004: uDinare(kir.OsnovicaPosebna),
|
||||
Polje104: uDinare(kir.PdvPosebna),
|
||||
Polje008: uDinare(kpr.OsnovicaOpsta + kpr.OsnovicaPosebna),
|
||||
Polje108: uDinare(kpr.PdvOpsta + kpr.PdvPosebna),
|
||||
}
|
||||
// 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.Polje105 = p.Polje103 + p.Polje104
|
||||
p.Polje009 = p.Polje006 + p.Polje007 + p.Polje008
|
||||
p.Polje109 = p.Polje106 + p.Polje107 + p.Polje108
|
||||
p.Polje110 = p.Polje105 - p.Polje109
|
||||
p.Povracaj = p.Polje110 < 0
|
||||
return p
|
||||
}
|
||||
|
||||
// ObracunajPdv računa obavezu PDV iz zbirova KIR i KPR za isti period.
|
||||
// PdvBezOdbitka iz KPR se namerno NE računa u odbitni PDV — to je PDV za koji
|
||||
// ne postoji pravo na odbitak prethodnog poreza.
|
||||
func ObracunajPdv(kir PdvKirSume, kpr PdvKprSume) PdvObracun {
|
||||
o := PdvObracun{
|
||||
IzlazniPdvOpsta: kir.PdvOpsta,
|
||||
IzlazniPdvPosebna: kir.PdvPosebna,
|
||||
OdbitniPdvOpsta: kpr.PdvOpsta,
|
||||
OdbitniPdvPosebna: kpr.PdvPosebna,
|
||||
}
|
||||
o.IzlazniPdvUkupno = o.IzlazniPdvOpsta + o.IzlazniPdvPosebna
|
||||
o.OdbitniPdvUkupno = o.OdbitniPdvOpsta + o.OdbitniPdvPosebna
|
||||
o.Obaveza = o.IzlazniPdvUkupno - o.OdbitniPdvUkupno
|
||||
return o
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func blizu(a, b float64) bool { return math.Abs(a-b) < 0.01 }
|
||||
|
||||
func TestKirIzProdaje(t *testing.T) {
|
||||
nalog := ProdajniNalog{ID: 5, BrojNaloga: "P-1", Datum: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)}
|
||||
stavke := []StavkaProdaje{
|
||||
// 20%: 2 × 120 = 240 (osnovica 200, PDV 40)
|
||||
{Kolicina: 2, CenaPoKomadu: 120, PdvStopa: 20},
|
||||
// 10%: 1 × 110 = 110 (osnovica 100, PDV 10)
|
||||
{Kolicina: 1, CenaPoKomadu: 110, PdvStopa: 10},
|
||||
// 0%: 1 × 50 = 50 (oslobođeno, bez PDV)
|
||||
{Kolicina: 1, CenaPoKomadu: 50, PdvStopa: 0},
|
||||
}
|
||||
|
||||
k := KirIzProdaje(nalog, stavke, "Kupac doo", "123456789", "Niš")
|
||||
|
||||
if k.Izvor != "prodaja" || k.IzvorID == nil || *k.IzvorID != 5 {
|
||||
t.Errorf("izvor=%q izvor_id=%v, očekivano prodaja/5", k.Izvor, k.IzvorID)
|
||||
}
|
||||
if k.BrojDokumenta != "P-1" || k.KupacNaziv != "Kupac doo" || k.KupacPib != "123456789" {
|
||||
t.Errorf("zaglavlje ne odgovara: %+v", k)
|
||||
}
|
||||
if !blizu(k.OsnovicaOpsta, 200) || !blizu(k.PdvOpsta, 40) {
|
||||
t.Errorf("opšta: osnovica=%v pdv=%v, očekivano 200/40", k.OsnovicaOpsta, k.PdvOpsta)
|
||||
}
|
||||
if !blizu(k.OsnovicaPosebna, 100) || !blizu(k.PdvPosebna, 10) {
|
||||
t.Errorf("posebna: osnovica=%v pdv=%v, očekivano 100/10", k.OsnovicaPosebna, k.PdvPosebna)
|
||||
}
|
||||
if !blizu(k.OslobodenSaPravom, 50) {
|
||||
t.Errorf("oslobođeno=%v, očekivano 50", k.OslobodenSaPravom)
|
||||
}
|
||||
if !blizu(k.Ukupno, 400) {
|
||||
t.Errorf("ukupno=%v, očekivano 400 (240+110+50)", k.Ukupno)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKprIzNabavke(t *testing.T) {
|
||||
nabavka := Nabavka{ID: 3, Napomena: "test", Datum: time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)}
|
||||
stavke := []NabavkaStavkaPdv{
|
||||
{Osnovica: 200, PdvStopa: 20}, // PDV 40
|
||||
{Osnovica: 100, PdvStopa: 10}, // PDV 10
|
||||
{Osnovica: 50, PdvStopa: 0}, // oslobođena nabavka
|
||||
}
|
||||
|
||||
k := KprIzNabavke(nabavka, "Dobavljač doo", "987654321", "Beograd", stavke)
|
||||
|
||||
if k.Izvor != "nabavka" || k.IzvorID == nil || *k.IzvorID != 3 {
|
||||
t.Errorf("izvor=%q izvor_id=%v, očekivano nabavka/3", k.Izvor, k.IzvorID)
|
||||
}
|
||||
if k.BrojDokumenta != "NAB-3" || k.DobavljacNaziv != "Dobavljač doo" || k.DobavljacPib != "987654321" {
|
||||
t.Errorf("zaglavlje ne odgovara: %+v", k)
|
||||
}
|
||||
if !blizu(k.OsnovicaOpsta, 200) || !blizu(k.PdvOpsta, 40) {
|
||||
t.Errorf("opšta: osnovica=%v pdv=%v, očekivano 200/40", k.OsnovicaOpsta, k.PdvOpsta)
|
||||
}
|
||||
if !blizu(k.OsnovicaPosebna, 100) || !blizu(k.PdvPosebna, 10) {
|
||||
t.Errorf("posebna: osnovica=%v pdv=%v, očekivano 100/10", k.OsnovicaPosebna, k.PdvPosebna)
|
||||
}
|
||||
if !blizu(k.OslobodenNabavka, 50) {
|
||||
t.Errorf("oslobođena nabavka=%v, očekivano 50", k.OslobodenNabavka)
|
||||
}
|
||||
if !blizu(k.Ukupno, 400) {
|
||||
t.Errorf("ukupno=%v, očekivano 400 (240+110+50)", k.Ukupno)
|
||||
}
|
||||
}
|
||||
|
||||
func TestObracunajPdv(t *testing.T) {
|
||||
// izlazni: 200 (opšta) + 50 (posebna) = 250; odbitni: 80 + 20 = 100; obaveza = 150
|
||||
kir := PdvKirSume{PdvOpsta: 200, PdvPosebna: 50}
|
||||
kpr := PdvKprSume{PdvOpsta: 80, PdvPosebna: 20, PdvBezOdbitka: 30}
|
||||
|
||||
o := ObracunajPdv(kir, kpr)
|
||||
|
||||
if !blizu(o.IzlazniPdvUkupno, 250) {
|
||||
t.Errorf("izlazni=%v, očekivano 250", o.IzlazniPdvUkupno)
|
||||
}
|
||||
// PdvBezOdbitka (30) ne sme da uđe u odbitni PDV
|
||||
if !blizu(o.OdbitniPdvUkupno, 100) {
|
||||
t.Errorf("odbitni=%v, očekivano 100 (bez PdvBezOdbitka)", o.OdbitniPdvUkupno)
|
||||
}
|
||||
if !blizu(o.Obaveza, 150) || !o.ZaUplatu() {
|
||||
t.Errorf("obaveza=%v ZaUplatu=%v, očekivano 150/true", o.Obaveza, o.ZaUplatu())
|
||||
}
|
||||
|
||||
// kada je odbitni veći od izlaznog → negativna obaveza (povraćaj/prenos), ZaUplatu=false
|
||||
o2 := ObracunajPdv(PdvKirSume{PdvOpsta: 40}, PdvKprSume{PdvOpsta: 100})
|
||||
if !blizu(o2.Obaveza, -60) || o2.ZaUplatu() {
|
||||
t.Errorf("obaveza=%v ZaUplatu=%v, očekivano -60/false", o2.Obaveza, o2.ZaUplatu())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapirajPPPDV(t *testing.T) {
|
||||
kir := PdvKirSume{
|
||||
OsnovicaOpsta: 1000.4, PdvOpsta: 200.08, // 003=1000, 103=200
|
||||
OsnovicaPosebna: 500.5, PdvPosebna: 50.05, // 004=501 (round), 104=50
|
||||
OslobodenSaPravom: 300.2, OslobodenBezPrava: 100.9, // 001=300, 002=101
|
||||
}
|
||||
kpr := PdvKprSume{
|
||||
OsnovicaOpsta: 400, PdvOpsta: 80,
|
||||
OsnovicaPosebna: 100, PdvPosebna: 10,
|
||||
PdvBezOdbitka: 25, // ne sme da uđe nigde u PPPDV
|
||||
}
|
||||
|
||||
p := MapirajPPPDV(kir, kpr)
|
||||
|
||||
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)
|
||||
}
|
||||
if p.Polje004 != 501 || p.Polje104 != 50 {
|
||||
t.Errorf("posebna 004/104 = %d/%d, očekivano 501/50", p.Polje004, p.Polje104)
|
||||
}
|
||||
// 005 = 300+101+1000+501 = 1902; 105 = 200+50 = 250
|
||||
if p.Polje005 != 1902 || p.Polje105 != 250 {
|
||||
t.Errorf("zbir 005/105 = %d/%d, očekivano 1902/250", p.Polje005, p.Polje105)
|
||||
}
|
||||
// uvoz i poljoprivrednik se ne prate
|
||||
if p.Polje006 != 0 || p.Polje106 != 0 || p.Polje007 != 0 || p.Polje107 != 0 {
|
||||
t.Errorf("006/106/007/107 moraju biti 0")
|
||||
}
|
||||
// 008 = 400+100 = 500; 108 = 80+10 = 90 (PdvBezOdbitka 25 izostavljen)
|
||||
if p.Polje008 != 500 || p.Polje108 != 90 {
|
||||
t.Errorf("ostali prethodni 008/108 = %d/%d, očekivano 500/90", p.Polje008, p.Polje108)
|
||||
}
|
||||
if p.Polje009 != 500 || p.Polje109 != 90 {
|
||||
t.Errorf("zbir prethodnog 009/109 = %d/%d, očekivano 500/90", p.Polje009, p.Polje109)
|
||||
}
|
||||
// 110 = 250 − 90 = 160 (za uplatu)
|
||||
if p.Polje110 != 160 || p.Povracaj {
|
||||
t.Errorf("110=%d Povracaj=%v, očekivano 160/false", p.Polje110, p.Povracaj)
|
||||
}
|
||||
|
||||
// scenario povraćaja: izlazni < odbitni → 110 negativan, Povracaj=true
|
||||
p2 := MapirajPPPDV(PdvKirSume{PdvOpsta: 30}, PdvKprSume{PdvOpsta: 90})
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// PdvStopa je jedna stavka u šifarniku PDV stopa (Faza 1 knjigovodstvenog modula).
|
||||
// Stope su podatak, ne hardkod — nova ili izmenjena stopa bez diranja koda.
|
||||
type PdvStopa struct {
|
||||
ID int64
|
||||
Naziv string // npr. "Opšta stopa"
|
||||
Stopa float64 // procenat, npr. 20.0
|
||||
Oznaka string // "opsta" | "posebna" | "oslobodjeno"
|
||||
Aktivna bool // false = arhivirana (ne nudi se u listama, ali stari zapisi ostaju ispravni)
|
||||
Redosled int // redosled prikaza
|
||||
DatumUnosa time.Time
|
||||
}
|
||||
@@ -39,11 +39,12 @@ type PodaciStranice struct {
|
||||
LogoTip string // "sa_nazivom", "bez_naziva", "slika"
|
||||
LogoPutanja string // putanja do slike, koristi se samo kada je LogoTip "slika"
|
||||
Korisnik string
|
||||
KorisnikIme string // korisničko ime prijavljenog korisnika
|
||||
KorisnikIme string // korisničko ime prijavljenog korisnika
|
||||
KorisnikUloga string // uloga: "superadmin", "admin", "radnik"
|
||||
CsrfToken string // CSRF zaštitni token za forme
|
||||
AssetV string // verzija statičkih fajlova (cache-busting za CSS/JS)
|
||||
Dozvole map[string]bool // mapa akcija → dozvoljeno/nije
|
||||
Moduli map[string]bool // mapa zakonskih modula → uključen za firmu (profil firme)
|
||||
Flash *FlashPoruka // jednokratna poruka nakon redirecta
|
||||
// app pozadina — popunjava se iz podešavanja za sve stranice
|
||||
AppPozadina string
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Šifarnik PDV stopa (Faza 1 knjigovodstvenog modula).
|
||||
-- Stope se čuvaju kao podatak, ne hardkod — nova ili izmenjena stopa bez diranja koda.
|
||||
-- Početne vrednosti po važećem zakonu (jun 2026): opšta 20%, posebna 10%, oslobođeno 0%.
|
||||
CREATE TABLE IF NOT EXISTS pdv_stope (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
naziv TEXT NOT NULL, -- npr. "Opšta stopa"
|
||||
stopa REAL NOT NULL, -- procenat, npr. 20.0
|
||||
oznaka TEXT NOT NULL, -- "opsta" | "posebna" | "oslobodjeno"
|
||||
aktivna INTEGER NOT NULL DEFAULT 1, -- 1 = u upotrebi, 0 = arhivirana
|
||||
redosled INTEGER NOT NULL DEFAULT 0, -- redosled prikaza u listama
|
||||
datum_unosa DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Seed početnih stopa samo ako je tabela prazna (idempotentno, ne gazi ručne izmene).
|
||||
INSERT INTO pdv_stope (naziv, stopa, oznaka, redosled)
|
||||
SELECT naziv, stopa, oznaka, redosled FROM (
|
||||
SELECT 'Opšta stopa' AS naziv, 20.0 AS stopa, 'opsta' AS oznaka, 1 AS redosled
|
||||
UNION ALL SELECT 'Posebna stopa', 10.0, 'posebna', 2
|
||||
UNION ALL SELECT 'Oslobođeno (0%)', 0.0, 'oslobodjeno', 3
|
||||
)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM pdv_stope);
|
||||
@@ -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
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Dodaje mesto/grad u klijente. Koristi se pri izboru klijenta u KIR formi
|
||||
-- (popunjava mesto kupca), a korisno je i samo po sebi za adresu klijenta.
|
||||
ALTER TABLE klijenti ADD COLUMN mesto TEXT;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Dodaje PIB i mesto/grad u dobavljače. PIB dobavljača je obavezan podatak za
|
||||
-- knjigu primljenih računa (KPR) i POPDV (evidencija prethodnog/odbitnog PDV-a).
|
||||
ALTER TABLE dobavljaci ADD COLUMN pib TEXT;
|
||||
ALTER TABLE dobavljaci ADD COLUMN mesto TEXT;
|
||||
@@ -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;
|
||||
@@ -194,7 +194,22 @@ document.addEventListener('alpine:init', () => {
|
||||
modalGreska: '',
|
||||
modalNaziv: '',
|
||||
modalKategorijaID: '',
|
||||
modalOpis: '',
|
||||
modalKolicina: '',
|
||||
modalKolicinaMin: '',
|
||||
modalCena: '',
|
||||
modalLokacija: '',
|
||||
modalNapomena: '',
|
||||
modalDob: false,
|
||||
modalDobUcitavanje: false,
|
||||
modalDobGreska: '',
|
||||
modalDobNaziv: '',
|
||||
modalDobKontakt: '',
|
||||
modalDobTelefon: '',
|
||||
modalDobEmail: '',
|
||||
modalDobPib: '',
|
||||
modalDobMesto: '',
|
||||
modalDobNapomena: '',
|
||||
init() {
|
||||
this.artikliOpcije = window._ntechArtikli || []
|
||||
this.isMobile = window.matchMedia('(max-width: 768px)').matches
|
||||
@@ -219,7 +234,12 @@ document.addEventListener('alpine:init', () => {
|
||||
this.modalGreska = ''
|
||||
this.modalNaziv = ''
|
||||
this.modalKategorijaID = ''
|
||||
this.modalOpis = ''
|
||||
this.modalKolicina = ''
|
||||
this.modalKolicinaMin = ''
|
||||
this.modalCena = ''
|
||||
this.modalLokacija = ''
|
||||
this.modalNapomena = ''
|
||||
this.$nextTick(() => this.$refs.modalNazivInput && this.$refs.modalNazivInput.focus())
|
||||
},
|
||||
zatvoriModal() {
|
||||
@@ -235,7 +255,12 @@ document.addEventListener('alpine:init', () => {
|
||||
const params = new URLSearchParams()
|
||||
params.append('naziv', this.modalNaziv.trim())
|
||||
if (this.modalKategorijaID) params.append('kategorija_id', this.modalKategorijaID)
|
||||
params.append('opis', this.modalOpis.trim())
|
||||
if (this.modalKolicina) params.append('kolicina', this.modalKolicina)
|
||||
if (this.modalKolicinaMin) params.append('kolicina_min', this.modalKolicinaMin)
|
||||
if (this.modalCena) params.append('prodajna_cena', this.modalCena)
|
||||
params.append('lokacija', this.modalLokacija.trim())
|
||||
params.append('napomena', this.modalNapomena.trim())
|
||||
params.append('_csrf', document.querySelector('meta[name=csrf-token]')?.content || '')
|
||||
try {
|
||||
const odgovor = await fetch('/magacin/novi', {
|
||||
@@ -255,6 +280,62 @@ document.addEventListener('alpine:init', () => {
|
||||
} finally {
|
||||
this.modalUcitavanje = false
|
||||
}
|
||||
},
|
||||
otvoriModalDobavljac() {
|
||||
this.modalDob = true
|
||||
this.modalDobGreska = ''
|
||||
this.modalDobNaziv = ''
|
||||
this.modalDobKontakt = ''
|
||||
this.modalDobTelefon = ''
|
||||
this.modalDobEmail = ''
|
||||
this.modalDobPib = ''
|
||||
this.modalDobMesto = ''
|
||||
this.modalDobNapomena = ''
|
||||
this.$nextTick(() => this.$refs.modalDobNazivInput && this.$refs.modalDobNazivInput.focus())
|
||||
},
|
||||
zatvoriModalDobavljac() {
|
||||
this.modalDob = false
|
||||
},
|
||||
async sacuvajDobavljaca() {
|
||||
if (!this.modalDobNaziv.trim()) {
|
||||
this.modalDobGreska = 'Naziv dobavljača je obavezan.'
|
||||
return
|
||||
}
|
||||
this.modalDobUcitavanje = true
|
||||
this.modalDobGreska = ''
|
||||
const params = new URLSearchParams()
|
||||
params.append('naziv', this.modalDobNaziv.trim())
|
||||
params.append('kontakt_osoba', this.modalDobKontakt.trim())
|
||||
params.append('telefon', this.modalDobTelefon.trim())
|
||||
params.append('email', this.modalDobEmail.trim())
|
||||
params.append('pib', this.modalDobPib.trim())
|
||||
params.append('mesto', this.modalDobMesto.trim())
|
||||
params.append('napomena', this.modalDobNapomena.trim())
|
||||
params.append('_csrf', document.querySelector('meta[name=csrf-token]')?.content || '')
|
||||
try {
|
||||
const odgovor = await fetch('/dobavljaci/novi', {
|
||||
method: 'POST',
|
||||
headers: {'X-Requested-With': 'fetch', 'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: params
|
||||
})
|
||||
if (!odgovor.ok) {
|
||||
const greska = await odgovor.json().catch(() => null)
|
||||
this.modalDobGreska = (greska && greska.greska) || 'Greška pri čuvanju dobavljača. Pokušajte ponovo.'
|
||||
return
|
||||
}
|
||||
const novi = await odgovor.json()
|
||||
// dodaj novog dobavljača u padajuću listu i odmah ga izaberi
|
||||
const opcija = document.createElement('option')
|
||||
opcija.value = novi.id
|
||||
opcija.textContent = novi.naziv
|
||||
this.$refs.selDobavljac.appendChild(opcija)
|
||||
this.$refs.selDobavljac.value = novi.id
|
||||
this.zatvoriModalDobavljac()
|
||||
} catch {
|
||||
this.modalDobGreska = 'Greška pri komunikaciji sa serverom.'
|
||||
} finally {
|
||||
this.modalDobUcitavanje = false
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
@@ -93,6 +93,25 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{/* PDV evidencija — vidljivo samo kada je modul „pdv" uključen za firmu (profil firme) i korisnik ima dozvolu */}}
|
||||
{{if and (index .Moduli "pdv") (index .Dozvole "pdv.pregled")}}
|
||||
<a href="/pdv/kir" class="nav-stavka {{if eq .Stranica "pdv-kir"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
<span>KIR</span>
|
||||
<span class="nav-tooltip">KIR — knjiga izdatih računa</span>
|
||||
</a>
|
||||
<a href="/pdv/kpr" class="nav-stavka {{if eq .Stranica "pdv-kpr"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="16" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||||
<span>KPR</span>
|
||||
<span class="nav-tooltip">KPR — knjiga primljenih računa</span>
|
||||
</a>
|
||||
<a href="/pdv/obracun" class="nav-stavka {{if eq .Stranica "pdv-obracun"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="10" y2="10"/><line x1="14" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="10" y2="14"/><line x1="14" y1="14" x2="16" y2="14"/><line x1="8" y1="18" x2="16" y2="18"/></svg>
|
||||
<span>Obračun</span>
|
||||
<span class="nav-tooltip">PDV obračun za period</span>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
<div class="nav-separator"></div>
|
||||
<div class="nav-oznaka">Nalog</div>
|
||||
|
||||
@@ -147,19 +166,19 @@
|
||||
{{if index .Dozvole "podesavanja.pregled"}}
|
||||
<div>
|
||||
<button type="button" data-podmeni-dugme
|
||||
class="nav-stavka {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "dozvole")}}aktivan{{end}}"
|
||||
class="nav-stavka {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-pdv-stope") (eq .Stranica "dozvole")}}aktivan{{end}}"
|
||||
style="width:100%;background:none;border:none;cursor:pointer;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06-.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
<span>Podešavanja</span>
|
||||
<span class="nav-strelica">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
style="transition:transform 0.2s;transform:{{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "dozvole")}}rotate(180deg){{else}}rotate(0deg){{end}}">
|
||||
style="transition:transform 0.2s;transform:{{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-pdv-stope") (eq .Stranica "dozvole")}}rotate(180deg){{else}}rotate(0deg){{end}}">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-tooltip">Podešavanja</span>
|
||||
</button>
|
||||
<div class="nav-podmeni {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "dozvole")}}otvoren{{end}}">
|
||||
<div class="nav-podmeni {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-pdv-stope") (eq .Stranica "dozvole")}}otvoren{{end}}">
|
||||
<a href="/admin/podesavanja/opste" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-opste"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M5 21V7a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v14"/><path d="M9 21v-5h6v5"/><path d="M9 9h.01M15 9h.01M9 13h.01M15 13h.01"/></svg>
|
||||
<span>Opšte</span>
|
||||
@@ -175,6 +194,11 @@
|
||||
<span>Sistem</span>
|
||||
<span class="nav-tooltip">Sistem</span>
|
||||
</a>
|
||||
<a href="/admin/podesavanja/pdv-stope" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-pdv-stope"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></svg>
|
||||
<span>PDV stope</span>
|
||||
<span class="nav-tooltip">PDV stope</span>
|
||||
</a>
|
||||
{{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}}
|
||||
<a href="/admin/dozvole" class="nav-stavka nav-podstavka {{if eq .Stranica "dozvole"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
|
||||
@@ -59,6 +59,20 @@
|
||||
style="width:100%;">
|
||||
</div>
|
||||
|
||||
<!-- PIB i mesto (za KPR / POPDV) -->
|
||||
<div class="forma-grid-2">
|
||||
<div>
|
||||
<label class="polje-labela">PIB</label>
|
||||
<input type="text" name="pib" value="{{.Dobavljac.PIB}}"
|
||||
placeholder="npr. 123456789" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Mesto / grad</label>
|
||||
<input type="text" name="mesto" value="{{.Dobavljac.Mesto}}"
|
||||
placeholder="npr. Beograd" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- napomena -->
|
||||
<div>
|
||||
<label class="polje-labela">Napomena</label>
|
||||
|
||||
@@ -117,6 +117,11 @@
|
||||
<input type="text" name="email" value="{{.Klijent.Email}}"
|
||||
placeholder="npr. marko@example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Mesto / grad</label>
|
||||
<input type="text" name="mesto" value="{{.Klijent.Mesto}}"
|
||||
placeholder="npr. Niš">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
.forma-kartica:nth-child(1) { animation-delay: 0.04s; }
|
||||
.forma-kartica:nth-child(2) { animation-delay: 0.12s; }
|
||||
.modal-sadrzaj { animation: modalIn 0.25s ease forwards; }
|
||||
/* centriranje preko klase (ne inline) — x-show menja inline display, pa bi obrisao flex i modal bi pao u ćošak */
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 50; display: flex; align-items: center; justify-content: center; padding: 16px; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
@@ -37,8 +39,15 @@
|
||||
</div>
|
||||
<div class="kolona" style="gap:14px;">
|
||||
<div>
|
||||
<label class="polje-labela">Dobavljač</label>
|
||||
<select name="dobavljac_id" style="width:100%;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<label class="polje-labela">Dobavljač</label>
|
||||
<button type="button" @click="otvoriModalDobavljac()"
|
||||
style="padding:6px 14px;background:var(--kartica);color:var(--tekst-sporedni);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;margin-bottom:6px;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background='var(--kartica)'">
|
||||
+ Novi dobavljač
|
||||
</button>
|
||||
</div>
|
||||
<select name="dobavljac_id" x-ref="selDobavljac" style="width:100%;">
|
||||
<option value="">— bez dobavljača —</option>
|
||||
{{range .Dobavljaci}}
|
||||
<option value="{{.ID}}">{{.Naziv}}</option>
|
||||
@@ -187,10 +196,10 @@
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:50;display:flex;align-items:center;justify-content:center;padding:16px;"
|
||||
class="modal-overlay"
|
||||
@click.self="zatvoriModal()" @keydown.escape.window="zatvoriModal()">
|
||||
|
||||
<div class="kartica modal-sadrzaj" style="width:100%;max-width:440px;padding:24px;">
|
||||
<div class="kartica modal-sadrzaj" style="width:100%;max-width:480px;padding:24px;max-height:90vh;overflow-y:auto;">
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Novi artikal</span>
|
||||
@@ -221,12 +230,44 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="polje-labela">Opis</label>
|
||||
<textarea x-model="modalOpis" rows="2"
|
||||
placeholder="Kratak opis artikla..."
|
||||
style="width:100%;resize:vertical;"></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="polje-labela">Količina na stanju</label>
|
||||
<input type="number" x-model="modalKolicina" min="0" placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Minimalna količina</label>
|
||||
<input type="number" x-model="modalKolicinaMin" min="0" placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="polje-labela">Prodajna cena (din)</label>
|
||||
<input type="number" x-model="modalCena" min="0" step="0.01"
|
||||
placeholder="0"
|
||||
style="width:100%;">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="polje-labela">Lokacija u magacinu</label>
|
||||
<input type="text" x-model="modalLokacija"
|
||||
placeholder="npr. Regal A3, polica 2"
|
||||
style="width:100%;">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="polje-labela">Napomena</label>
|
||||
<textarea x-model="modalNapomena" rows="2"
|
||||
placeholder="Interna napomena o artiklu..."
|
||||
style="width:100%;resize:vertical;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;padding-top:14px;border-top:0.5px solid var(--ivica);">
|
||||
@@ -240,5 +281,76 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal: novi dobavljač -->
|
||||
<div x-show="modalDob" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
class="modal-overlay"
|
||||
@click.self="zatvoriModalDobavljac()" @keydown.escape.window="zatvoriModalDobavljac()">
|
||||
|
||||
<div class="kartica modal-sadrzaj" style="width:100%;max-width:480px;padding:24px;max-height:90vh;overflow-y:auto;">
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Novi dobavljač</span>
|
||||
<button type="button" @click="zatvoriModalDobavljac()" aria-label="Zatvori"
|
||||
style="background:none;border:none;cursor:pointer;color:var(--tekst-sporedni);font-size:20px;line-height:1;padding:2px 6px;border-radius:4px;transition:background 0.15s;">×</button>
|
||||
</div>
|
||||
|
||||
<div x-show="modalDobGreska" class="poruka-greska greska-animacija" x-text="modalDobGreska"></div>
|
||||
|
||||
<div class="kolona" style="gap:14px;">
|
||||
<div>
|
||||
<label class="polje-labela">Naziv <span style="color:#dc2626;">*</span></label>
|
||||
<input type="text" x-model="modalDobNaziv" x-ref="modalDobNazivInput"
|
||||
placeholder="npr. TechDistrib d.o.o." style="width:100%;"
|
||||
@keydown.enter.prevent="sacuvajDobavljaca()">
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="polje-labela">Kontakt osoba</label>
|
||||
<input type="text" x-model="modalDobKontakt" placeholder="npr. Marko Petrović" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Telefon</label>
|
||||
<input type="text" x-model="modalDobTelefon" placeholder="npr. 011 123 4567" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="polje-labela">E-pošta</label>
|
||||
<input type="text" x-model="modalDobEmail" placeholder="npr. nabavka@techdistrib.rs" style="width:100%;">
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="polje-labela">PIB</label>
|
||||
<input type="text" x-model="modalDobPib" placeholder="npr. 123456789" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Mesto / grad</label>
|
||||
<input type="text" x-model="modalDobMesto" placeholder="npr. Beograd" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="polje-labela">Napomena</label>
|
||||
<textarea x-model="modalDobNapomena" rows="2"
|
||||
placeholder="Interna napomena o dobavljaču..."
|
||||
style="width:100%;resize:vertical;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;padding-top:14px;border-top:0.5px solid var(--ivica);">
|
||||
<button type="button" @click="zatvoriModalDobavljac()" class="btn-sekundarno">Odustani</button>
|
||||
<button type="button" @click="sacuvajDobavljaca()" :disabled="modalDobUcitavanje" class="btn-primarno"
|
||||
:style="modalDobUcitavanje ? 'opacity:0.6;cursor:not-allowed' : ''">
|
||||
<span x-text="modalDobUcitavanje ? 'Čuvanje...' : 'Dodaj dobavljača'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}KIR — knjiga izdatih računa — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="stranica-stack" style="width:100%;max-width:100%;">
|
||||
|
||||
<!-- zaglavlje: filter perioda + dugme za novi zapis -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap;">
|
||||
<form method="GET" action="/pdv/kir" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
|
||||
<div>
|
||||
<label class="polje-labela">Od datuma</label>
|
||||
<input type="date" name="od" value="{{.Od}}" style="padding:8px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Do datuma</label>
|
||||
<input type="date" name="do" value="{{.Do}}" style="padding:8px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<button type="submit" style="padding:8px 16px;background:var(--sb-aktivan);color:var(--tekst-jak);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;">Prikaži</button>
|
||||
{{if or .Od .Do}}<a href="/pdv/kir" class="nazad-link" style="margin-bottom:0;">Poništi filter</a>{{end}}
|
||||
</form>
|
||||
<a href="/pdv/kir/nova" class="btn-primarno" style="font-size:13px;padding:8px 16px;white-space:nowrap;">+ Nov izlazni račun</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- knjiga -->
|
||||
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:900px;">
|
||||
<thead>
|
||||
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
|
||||
<th style="padding:10px 12px;">Datum prometa</th>
|
||||
<th style="padding:10px 12px;">Broj dok.</th>
|
||||
<th style="padding:10px 12px;">Kupac</th>
|
||||
<th style="padding:10px 12px;text-align:right;">Osn. 20%</th>
|
||||
<th style="padding:10px 12px;text-align:right;">PDV 20%</th>
|
||||
<th style="padding:10px 12px;text-align:right;">Osn. 10%</th>
|
||||
<th style="padding:10px 12px;text-align:right;">PDV 10%</th>
|
||||
<th style="padding:10px 12px;text-align:right;">Oslobođeno</th>
|
||||
<th style="padding:10px 12px;text-align:right;">Ukupno</th>
|
||||
<th style="padding:10px 12px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Zapisi}}
|
||||
<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;">{{.BrojDokumenta}}</td>
|
||||
<td style="padding:10px 12px;">{{.KupacNaziv}}{{if .KupacPib}}<div style="font-size:11px;color:var(--tekst-sporedni);">{{.OznakaPoreskogBroja}}: {{.KupacPib}}</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" .PdvOpsta}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .OsnovicaPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .PdvPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .OslobodenUkupno}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;font-weight:500;">{{printf "%.2f" .Ukupno}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;white-space:nowrap;">
|
||||
{{if eq .Izvor "rucno"}}
|
||||
<form method="POST" action="/pdv/kir/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit" class="btn-obrisi-malo" data-potvrda="Obrisati ovaj zapis iz KIR?">Obriši</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<span style="font-size:11px;color:var(--tekst-sporedni);" title="Zavedeno automatski — upravlja se preko prodaje (storniraj/obriši prodaju)">iz prodaje</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="10" style="padding:28px;text-align:center;color:var(--tekst-sporedni);">Nema zapisa u izabranom periodu. <a href="/pdv/kir/nova" style="color:var(--sb-akcent);">Dodaj prvi.</a></td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
{{if .Zapisi}}
|
||||
<tfoot>
|
||||
<tr style="border-top:0.5px solid var(--ivica);font-weight:500;background:var(--pozadina);">
|
||||
<td style="padding:10px 12px;" colspan="3">UKUPNO ({{len .Zapisi}})</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.OsnovicaOpsta}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.PdvOpsta}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.OsnovicaPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.PdvPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.OslobodenUkupno}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.Ukupno}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,123 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Novi izlazni račun (KIR) — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="stranica-stack" style="width:100%;max-width:760px;">
|
||||
|
||||
<a href="/pdv/kir" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na KIR
|
||||
</a>
|
||||
|
||||
<form method="POST" action="/pdv/kir/nova">
|
||||
<!-- dokument -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">Dokument</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;">
|
||||
<div>
|
||||
<label class="polje-labela">Datum prometa</label>
|
||||
<input type="date" name="datum_prometa" value="{{.Danas}}" required 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>
|
||||
<label class="polje-labela">Datum knjiženja</label>
|
||||
<input type="date" name="datum_knjizenja" value="{{.Danas}}" required 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 style="grid-column:1 / -1;">
|
||||
<label class="polje-labela">Broj dokumenta</label>
|
||||
<input type="text" name="broj_dokumenta" required 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;" placeholder="npr. R-2026/001">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- kupac -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">Kupac</div>
|
||||
<div class="kolona" style="gap:14px;">
|
||||
{{if .Klijenti}}
|
||||
<div>
|
||||
<label class="polje-labela">Izaberi iz klijenata (opciono)</label>
|
||||
<select onchange="popuniKupca(this)" 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;">
|
||||
<option value="">— ručni unos —</option>
|
||||
{{range .Klijenti}}
|
||||
<option data-naziv="{{.PunoIme}}" data-pib="{{if eq .Tip "pravno"}}{{.PIB}}{{else}}{{.JMBG}}{{end}}" data-mesto="{{.Mesto}}">{{.PunoIme}}{{if .Mesto}} — {{.Mesto}}{{end}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<div class="pomocni-tekst" style="font-size:12px;margin-top:4px;">Izbor popunjava naziv i PIB/JMBG; možeš ih i ručno izmeniti.</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<label class="polje-labela">Naziv kupca</label>
|
||||
<input type="text" name="kupac_naziv" required 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 style="display:grid;grid-template-columns:1fr 1fr;gap:14px;">
|
||||
<div>
|
||||
<label class="polje-labela">PIB / JMBG</label>
|
||||
<input type="text" name="kupac_pib" 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>
|
||||
<label class="polje-labela">Mesto</label>
|
||||
<input type="text" name="kupac_mesto" 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>
|
||||
</div>
|
||||
|
||||
<!-- iznosi po stopama -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:6px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">Iznosi po stopama</div>
|
||||
<div class="pomocni-tekst" style="font-size:12px;margin-bottom:14px;">Ostavite 0 gde nema prometa. „Ukupno" se računa automatski kao zbir osnovica, PDV-a i oslobođenog prometa.</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;">
|
||||
<div>
|
||||
<label class="polje-labela">Osnovica opšta (20%)</label>
|
||||
<input type="number" step="0.01" min="0" name="osnovica_opsta" value="0" 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>
|
||||
<label class="polje-labela">PDV opšta (20%)</label>
|
||||
<input type="number" step="0.01" min="0" name="pdv_opsta" value="0" 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>
|
||||
<label class="polje-labela">Osnovica posebna (10%)</label>
|
||||
<input type="number" step="0.01" min="0" name="osnovica_posebna" value="0" 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>
|
||||
<label class="polje-labela">PDV posebna (10%)</label>
|
||||
<input type="number" step="0.01" min="0" name="pdv_posebna" value="0" 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>
|
||||
<label class="polje-labela">Oslobođeno sa pravom na odbitak</label>
|
||||
<input type="number" step="0.01" min="0" name="osloboden_sa_pravom" value="0" 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>
|
||||
<label class="polje-labela">Oslobođeno bez prava na odbitak</label>
|
||||
<input type="number" step="0.01" min="0" name="osloboden_bez_prava" value="0" 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 class="polje-labela">Napomena</label>
|
||||
<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 style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;">
|
||||
<a href="/pdv/kir" 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>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Izbor klijenta iz padajuće liste popunjava polja kupca (naziv, PIB/JMBG, mesto).
|
||||
// KIR i dalje čuva kupca kao tekst — klijent samo popuni polja, koja se mogu i ručno menjati.
|
||||
function popuniKupca(sel) {
|
||||
var o = sel.options[sel.selectedIndex];
|
||||
var naziv = o.getAttribute('data-naziv');
|
||||
if (!naziv) return; // „— ručni unos —"
|
||||
var f = sel.closest('form');
|
||||
f.querySelector('input[name="kupac_naziv"]').value = naziv;
|
||||
f.querySelector('input[name="kupac_pib"]').value = o.getAttribute('data-pib') || '';
|
||||
f.querySelector('input[name="kupac_mesto"]').value = o.getAttribute('data-mesto') || '';
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,93 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}KPR — knjiga primljenih računa — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="stranica-stack" style="width:100%;max-width:100%;">
|
||||
|
||||
<!-- zaglavlje: filter perioda + dugme za novi zapis -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap;">
|
||||
<form method="GET" action="/pdv/kpr" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
|
||||
<div>
|
||||
<label class="polje-labela">Od datuma</label>
|
||||
<input type="date" name="od" value="{{.Od}}" style="padding:8px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Do datuma</label>
|
||||
<input type="date" name="do" value="{{.Do}}" style="padding:8px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<button type="submit" style="padding:8px 16px;background:var(--sb-aktivan);color:var(--tekst-jak);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;">Prikaži</button>
|
||||
{{if or .Od .Do}}<a href="/pdv/kpr" class="nazad-link" style="margin-bottom:0;">Poništi filter</a>{{end}}
|
||||
</form>
|
||||
<a href="/pdv/kpr/nova" class="btn-primarno" style="font-size:13px;padding:8px 16px;white-space:nowrap;">+ Nov ulazni račun</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- knjiga -->
|
||||
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:980px;">
|
||||
<thead>
|
||||
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
|
||||
<th style="padding:10px 12px;">Datum prometa</th>
|
||||
<th style="padding:10px 12px;">Broj dok.</th>
|
||||
<th style="padding:10px 12px;">Dobavljač</th>
|
||||
<th style="padding:10px 12px;text-align:right;">Osn. 20%</th>
|
||||
<th style="padding:10px 12px;text-align:right;">PDV 20%</th>
|
||||
<th style="padding:10px 12px;text-align:right;">Osn. 10%</th>
|
||||
<th style="padding:10px 12px;text-align:right;">PDV 10%</th>
|
||||
<th style="padding:10px 12px;text-align:right;">PDV bez odb.</th>
|
||||
<th style="padding:10px 12px;text-align:right;">Oslob. nab.</th>
|
||||
<th style="padding:10px 12px;text-align:right;">Ukupno</th>
|
||||
<th style="padding:10px 12px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Zapisi}}
|
||||
<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;">{{.BrojDokumenta}}</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" .PdvOpsta}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .OsnovicaPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .PdvPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .PdvBezOdbitka}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .OslobodenNabavka}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;font-weight:500;">{{printf "%.2f" .Ukupno}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;white-space:nowrap;">
|
||||
{{if eq .Izvor "rucno"}}
|
||||
<form method="POST" action="/pdv/kpr/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit" class="btn-obrisi-malo" data-potvrda="Obrisati ovaj zapis iz KPR?">Obriši</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<span style="font-size:11px;color:var(--tekst-sporedni);" title="Zavedeno automatski — upravlja se preko nabavke (obriši nabavku)">iz nabavke</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="11" style="padding:28px;text-align:center;color:var(--tekst-sporedni);">Nema zapisa u izabranom periodu. <a href="/pdv/kpr/nova" style="color:var(--sb-akcent);">Dodaj prvi.</a></td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
{{if .Zapisi}}
|
||||
<tfoot>
|
||||
<tr style="border-top:0.5px solid var(--ivica);font-weight:500;background:var(--pozadina);">
|
||||
<td style="padding:10px 12px;" colspan="3">UKUPNO ({{len .Zapisi}})</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.OsnovicaOpsta}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.PdvOpsta}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.OsnovicaPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.PdvPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.PdvBezOdbitka}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.OslobodenNabavka}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Sume.Ukupno}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,126 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Novi ulazni račun (KPR) — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="stranica-stack" style="width:100%;max-width:760px;">
|
||||
|
||||
<a href="/pdv/kpr" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na KPR
|
||||
</a>
|
||||
|
||||
<form method="POST" action="/pdv/kpr/nova">
|
||||
<!-- dokument -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">Dokument</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;">
|
||||
<div>
|
||||
<label class="polje-labela">Datum prometa</label>
|
||||
<input type="date" name="datum_prometa" value="{{.Danas}}" required 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>
|
||||
<label class="polje-labela">Datum knjiženja</label>
|
||||
<input type="date" name="datum_knjizenja" value="{{.Danas}}" required 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>
|
||||
<label class="polje-labela">Datum plaćanja (opciono)</label>
|
||||
<input type="date" name="datum_placanja" 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>
|
||||
<label class="polje-labela">Broj dokumenta</label>
|
||||
<input type="text" name="broj_dokumenta" required 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;" placeholder="npr. 2026-1234">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- dobavljač -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">Dobavljač</div>
|
||||
<div class="kolona" style="gap:14px;">
|
||||
{{if .Dobavljaci}}
|
||||
<div>
|
||||
<label class="polje-labela">Izaberi iz dobavljača (opciono)</label>
|
||||
<select onchange="popuniDobavljaca(this)" 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;">
|
||||
<option value="">— ručni unos —</option>
|
||||
{{range .Dobavljaci}}
|
||||
<option data-naziv="{{.Naziv}}" data-pib="{{.PIB}}" data-mesto="{{.Mesto}}">{{.Naziv}}{{if .Mesto}} — {{.Mesto}}{{end}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<div class="pomocni-tekst" style="font-size:12px;margin-top:4px;">Izbor popunjava naziv, PIB i mesto; možeš ih i ručno izmeniti.</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<label class="polje-labela">Naziv dobavljača</label>
|
||||
<input type="text" name="dobavljac_naziv" required 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 style="display:grid;grid-template-columns:1fr 1fr;gap:14px;">
|
||||
<div>
|
||||
<label class="polje-labela">PIB</label>
|
||||
<input type="text" name="dobavljac_pib" 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>
|
||||
<label class="polje-labela">Mesto</label>
|
||||
<input type="text" name="dobavljac_mesto" 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>
|
||||
</div>
|
||||
|
||||
<!-- iznosi po stopama -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:6px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">Iznosi po stopama</div>
|
||||
<div class="pomocni-tekst" style="font-size:12px;margin-bottom:14px;">PDV (opšta/posebna) je iznos koji se može odbiti. „Ukupno" se računa automatski.</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;">
|
||||
<div>
|
||||
<label class="polje-labela">Osnovica opšta (20%)</label>
|
||||
<input type="number" step="0.01" min="0" name="osnovica_opsta" value="0" 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>
|
||||
<label class="polje-labela">PDV opšta (20%, odbitni)</label>
|
||||
<input type="number" step="0.01" min="0" name="pdv_opsta" value="0" 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>
|
||||
<label class="polje-labela">Osnovica posebna (10%)</label>
|
||||
<input type="number" step="0.01" min="0" name="osnovica_posebna" value="0" 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>
|
||||
<label class="polje-labela">PDV posebna (10%, odbitni)</label>
|
||||
<input type="number" step="0.01" min="0" name="pdv_posebna" value="0" 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>
|
||||
<label class="polje-labela">PDV bez prava na odbitak</label>
|
||||
<input type="number" step="0.01" min="0" name="pdv_bez_odbitka" value="0" 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>
|
||||
<label class="polje-labela">Oslobođena nabavka / bez PDV</label>
|
||||
<input type="number" step="0.01" min="0" name="osloboden_nabavka" value="0" 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 class="polje-labela">Napomena</label>
|
||||
<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 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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Izbor dobavljača iz padajuće liste popunjava polja (naziv, PIB, mesto).
|
||||
function popuniDobavljaca(sel) {
|
||||
var o = sel.options[sel.selectedIndex];
|
||||
var naziv = o.getAttribute('data-naziv');
|
||||
if (!naziv) return; // „— ručni unos —"
|
||||
var f = sel.closest('form');
|
||||
f.querySelector('input[name="dobavljac_naziv"]').value = naziv;
|
||||
f.querySelector('input[name="dobavljac_pib"]').value = o.getAttribute('data-pib') || '';
|
||||
f.querySelector('input[name="dobavljac_mesto"]').value = o.getAttribute('data-mesto') || '';
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,200 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}PDV obračun — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="stranica-stack" style="width:100%;max-width:100%;">
|
||||
|
||||
<!-- izbor perioda -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<form method="GET" action="/pdv/obracun" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
|
||||
<div>
|
||||
<label class="polje-labela">Od datuma</label>
|
||||
<input type="date" name="od" value="{{.Od}}" style="padding:8px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Do datuma</label>
|
||||
<input type="date" name="do" value="{{.Do}}" style="padding:8px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<button type="submit" style="padding:8px 16px;background:var(--sb-aktivan);color:var(--tekst-jak);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;">Prikaži</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- obračun po stopama -->
|
||||
<div class="kartica animiraj" style="padding:0;overflow:hidden;margin-bottom:16px;">
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:560px;">
|
||||
<thead>
|
||||
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
|
||||
<th style="padding:10px 12px;"></th>
|
||||
<th style="padding:10px 12px;text-align:right;">Osnovica</th>
|
||||
<th style="padding:10px 12px;text-align:right;">PDV</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- izlazni (dugovani) PDV — iz KIR -->
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:10px 12px;font-weight:500;" colspan="3">Izlazni (dugovani) PDV — KIR</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:10px 12px;padding-left:24px;">Opšta stopa (20%)</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .KirSume.OsnovicaOpsta}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.IzlazniPdvOpsta}}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:10px 12px;padding-left:24px;">Posebna stopa (10%)</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .KirSume.OsnovicaPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.IzlazniPdvPosebna}}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:10px 12px;padding-left:24px;color:var(--tekst-sporedni);">Oslobođen promet</td>
|
||||
<td style="padding:10px 12px;text-align:right;color:var(--tekst-sporedni);">{{printf "%.2f" .KirSume.OslobodenUkupno}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;color:var(--tekst-sporedni);">—</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);font-weight:500;background:var(--pozadina);">
|
||||
<td style="padding:10px 12px;">Ukupno izlazni PDV</td>
|
||||
<td style="padding:10px 12px;"></td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.IzlazniPdvUkupno}}</td>
|
||||
</tr>
|
||||
|
||||
<!-- odbitni (prethodni) PDV — iz KPR -->
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:10px 12px;font-weight:500;padding-top:18px;" colspan="3">Odbitni (prethodni) PDV — KPR</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:10px 12px;padding-left:24px;">Opšta stopa (20%)</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .KprSume.OsnovicaOpsta}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.OdbitniPdvOpsta}}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:10px 12px;padding-left:24px;">Posebna stopa (10%)</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .KprSume.OsnovicaPosebna}}</td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.OdbitniPdvPosebna}}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:10px 12px;padding-left:24px;color:var(--tekst-sporedni);">PDV bez prava na odbitak</td>
|
||||
<td style="padding:10px 12px;text-align:right;color:var(--tekst-sporedni);">—</td>
|
||||
<td style="padding:10px 12px;text-align:right;color:var(--tekst-sporedni);">{{printf "%.2f" .KprSume.PdvBezOdbitka}}</td>
|
||||
</tr>
|
||||
<tr style="font-weight:500;background:var(--pozadina);">
|
||||
<td style="padding:10px 12px;">Ukupno odbitni PDV</td>
|
||||
<td style="padding:10px 12px;"></td>
|
||||
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .Obracun.OdbitniPdvUkupno}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- rezultat: obaveza za uplatu / povraćaj -->
|
||||
<div class="kartica animiraj" style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;
|
||||
border-left:4px solid {{if .Obracun.ZaUplatu}}var(--greska){{else}}var(--uspeh){{end}};">
|
||||
<div>
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||
{{if .Obracun.ZaUplatu}}PDV obaveza za uplatu{{else}}PDV za povraćaj / prenos u naredni period{{end}}
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">izlazni − odbitni PDV</div>
|
||||
</div>
|
||||
<div style="font-size:24px;font-weight:600;color:{{if .Obracun.ZaUplatu}}var(--greska){{else}}var(--uspeh){{end}};white-space:nowrap;">
|
||||
{{printf "%.2f" .Obracun.ObavezaApsolutna}} RSD
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PPPDV — prikaz polja zvanične poreske prijave PDV -->
|
||||
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
|
||||
<div style="padding:16px 16px 0;">
|
||||
<div style="font-size:15px;font-weight:600;color:var(--tekst-glavni);">PPPDV — poreska prijava PDV</div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">
|
||||
Prikaz polja obrasca za izabrani period, u celim dinarima.
|
||||
⚠ Uvoz (006/106) i PDV nadoknada poljoprivredniku (007/107) se ne vode zasebno u programu.
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x:auto;margin-top:12px;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:560px;">
|
||||
<thead>
|
||||
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
|
||||
<th style="padding:10px 12px;width:48px;">Polje</th>
|
||||
<th style="padding:10px 12px;">Opis</th>
|
||||
<th style="padding:10px 12px;text-align:right;">Naknada bez PDV</th>
|
||||
<th style="padding:10px 12px;text-align:right;">PDV</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);"><td style="padding:8px 12px;font-weight:500;" colspan="4">I. Promet dobara i usluga</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:8px 12px;color:var(--tekst-sporedni);">001</td>
|
||||
<td style="padding:8px 12px;">Oslobođen sa pravom na odbitak</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje001}}</td>
|
||||
<td style="padding:8px 12px;text-align:right;color:var(--tekst-sporedni);">—</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:8px 12px;color:var(--tekst-sporedni);">002</td>
|
||||
<td style="padding:8px 12px;">Oslobođen bez prava na odbitak</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje002}}</td>
|
||||
<td style="padding:8px 12px;text-align:right;color:var(--tekst-sporedni);">—</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:8px 12px;color:var(--tekst-sporedni);">003<div style="font-size:11px;">103</div></td>
|
||||
<td style="padding:8px 12px;">Promet po opštoj stopi (20%)</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje003}}</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje103}}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:8px 12px;color:var(--tekst-sporedni);">004<div style="font-size:11px;">104</div></td>
|
||||
<td style="padding:8px 12px;">Promet po posebnoj stopi (10%)</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje004}}</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje104}}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);font-weight:500;background:var(--pozadina);">
|
||||
<td style="padding:8px 12px;">005<div style="font-size:11px;">105</div></td>
|
||||
<td style="padding:8px 12px;">ZBIR (1+2+3+4)</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje005}}</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje105}}</td>
|
||||
</tr>
|
||||
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);"><td style="padding:8px 12px;font-weight:500;padding-top:14px;" colspan="4">II. Prethodni porez</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);color:var(--tekst-sporedni);">
|
||||
<td style="padding:8px 12px;">006<div style="font-size:11px;">106</div></td>
|
||||
<td style="padding:8px 12px;">Prethodni porez pri uvozu</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje006}}</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje106}}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);color:var(--tekst-sporedni);">
|
||||
<td style="padding:8px 12px;">007<div style="font-size:11px;">107</div></td>
|
||||
<td style="padding:8px 12px;">PDV nadoknada poljoprivredniku</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje007}}</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje107}}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:8px 12px;color:var(--tekst-sporedni);">008<div style="font-size:11px;">108</div></td>
|
||||
<td style="padding:8px 12px;">Ostali prethodni porez (domaće nabavke)</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje008}}</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje108}}</td>
|
||||
</tr>
|
||||
<tr style="font-weight:500;background:var(--pozadina);">
|
||||
<td style="padding:8px 12px;">009<div style="font-size:11px;">109</div></td>
|
||||
<td style="padding:8px 12px;">ZBIR (6+7+8)</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje009}}</td>
|
||||
<td style="padding:8px 12px;text-align:right;">{{.PPPDV.Polje109}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style="border-top:0.5px solid var(--ivica);font-weight:600;">
|
||||
<td style="padding:10px 12px;">110</td>
|
||||
<td style="padding:10px 12px;" colspan="2">
|
||||
Iznos PDV u periodu (105 − 109)
|
||||
{{if .PPPDV.Povracaj}}<span style="color:var(--uspeh);"> — za povraćaj</span>{{else}}<span style="color:var(--greska);"> — za uplatu</span>{{end}}
|
||||
</td>
|
||||
<td style="padding:10px 12px;text-align:right;color:{{if .PPPDV.Povracaj}}var(--uspeh){{else}}var(--greska){{end}};">{{.PPPDV.Polje110Apsolutno}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 12px;color:var(--tekst-sporedni);">11</td>
|
||||
<td style="padding:8px 12px;" colspan="3">Povraćaj: <strong>{{if .PPPDV.Povracaj}}DA{{else}}NE{{end}}</strong></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,90 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Podešavanja — PDV stope — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="stranica-stack" style="width:100%;max-width:100%;">
|
||||
|
||||
<!-- postojeće stope: svaki red je forma za izmenu + zasebna forma za arhiviranje -->
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></svg>
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Šifarnik PDV stopa</span>
|
||||
</div>
|
||||
|
||||
<div class="pomocni-tekst" style="font-size:12px;margin-bottom:14px;">
|
||||
Stope se koriste u kalkulaciji cena i PDV evidenciji. Stopa koja je negde upotrebljena se ne briše — arhivira se (nestaje iz padajućih lista, a postojeći zapisi ostaju ispravni).
|
||||
</div>
|
||||
|
||||
{{if not .Stope}}
|
||||
<div class="pomocni-tekst">Nema unetih PDV stopa.</div>
|
||||
{{end}}
|
||||
|
||||
{{range .Stope}}
|
||||
<div style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;padding:10px 0;border-bottom:0.5px solid var(--ivica);{{if not .Aktivna}}opacity:0.5;{{end}}">
|
||||
<form method="POST" action="/podesavanja/pdv-stope/{{.ID}}/izmeni" style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;flex:1;">
|
||||
<div style="flex:2;min-width:140px;">
|
||||
<label class="polje-labela">Naziv</label>
|
||||
<input type="text" name="naziv" value="{{.Naziv}}" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<div style="width:80px;">
|
||||
<label class="polje-labela">Stopa %</label>
|
||||
<input type="text" name="stopa" value="{{printf "%g" .Stopa}}" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<div style="width:150px;">
|
||||
<label class="polje-labela">Vrsta</label>
|
||||
<select name="oznaka" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
<option value="opsta" {{if eq .Oznaka "opsta"}}selected{{end}}>Opšta</option>
|
||||
<option value="posebna" {{if eq .Oznaka "posebna"}}selected{{end}}>Posebna</option>
|
||||
<option value="oslobodjeno" {{if eq .Oznaka "oslobodjeno"}}selected{{end}}>Oslobođeno</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="width:80px;">
|
||||
<label class="polje-labela">Redosled</label>
|
||||
<input type="text" name="redosled" value="{{.Redosled}}" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<button type="submit" style="padding:7px 14px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;">Sačuvaj</button>
|
||||
</form>
|
||||
<form method="POST" action="/podesavanja/pdv-stope/{{.ID}}/aktivnost">
|
||||
<button type="submit" style="padding:7px 14px;background:var(--sb-aktivan);color:var(--tekst-jak);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;">{{if .Aktivna}}Arhiviraj{{else}}Vrati{{end}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- dodavanje nove stope -->
|
||||
<form method="POST" action="/podesavanja/pdv-stope/dodaj">
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Nova stopa</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;">
|
||||
<div style="flex:2;min-width:140px;">
|
||||
<label class="polje-labela">Naziv</label>
|
||||
<input type="text" name="naziv" placeholder="npr. Opšta stopa" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<div style="width:80px;">
|
||||
<label class="polje-labela">Stopa %</label>
|
||||
<input type="text" name="stopa" placeholder="20" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<div style="width:150px;">
|
||||
<label class="polje-labela">Vrsta</label>
|
||||
<select name="oznaka" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
<option value="opsta">Opšta</option>
|
||||
<option value="posebna">Posebna</option>
|
||||
<option value="oslobodjeno">Oslobođeno</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="width:80px;">
|
||||
<label class="polje-labela">Redosled</label>
|
||||
<input type="text" name="redosled" placeholder="0" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
<button type="submit" style="padding:7px 16px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap;">Dodaj</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -117,6 +117,94 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- profil firme: pravni/poreski status (Faza 0) — određuje koji se zakonski moduli pale -->
|
||||
<form method="POST" action="/podesavanja/sacuvaj">
|
||||
<input type="hidden" name="_next" value="/admin/podesavanja/opste">
|
||||
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Pravni i poreski status</span>
|
||||
</div>
|
||||
|
||||
<div class="kolona" style="gap:18px;">
|
||||
|
||||
<!-- pravni oblik -->
|
||||
<div>
|
||||
<label class="polje-labela">Pravni oblik</label>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .FirmaPravniOblik "pausalac"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="firma_pravni_oblik" value="pausalac" {{if eq .FirmaPravniOblik "pausalac"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Paušalac</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .FirmaPravniOblik "preduzetnik_knjige"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="firma_pravni_oblik" value="preduzetnik_knjige" {{if eq .FirmaPravniOblik "preduzetnik_knjige"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Preduzetnik (knjige)</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .FirmaPravniOblik "doo"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="firma_pravni_oblik" value="doo" {{if eq .FirmaPravniOblik "doo"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">DOO</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- režim rada -->
|
||||
<div>
|
||||
<label class="polje-labela">Režim rada</label>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .FirmaRezim "pun"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="firma_rezim" value="pun" {{if eq .FirmaRezim "pun"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Pun (vodi poslovne knjige)</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .FirmaRezim "samo_evidencija"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="firma_rezim" value="samo_evidencija" {{if eq .FirmaRezim "samo_evidencija"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Samo evidencija</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pomocni-tekst" style="font-size:12px;margin-top:6px;">„Samo evidencija" gasi ceo zakonski sloj — program vodi servis i zalihe, ali ne izdaje fiskalne ni poreske dokumente.</div>
|
||||
</div>
|
||||
|
||||
<!-- PDV obveznik -->
|
||||
<div>
|
||||
<label class="polje-labela">U sistemu PDV-a</label>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .FirmaPdvObveznik "da"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="firma_pdv_obveznik" value="da" {{if eq .FirmaPdvObveznik "da"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Da</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .FirmaPdvObveznik "ne"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="firma_pdv_obveznik" value="ne" {{if eq .FirmaPdvObveznik "ne"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Ne</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- fiskalizacija — zasivljena i forsirana na „Ne" kad je režim „samo evidencija" (vidi skriptu na dnu) -->
|
||||
<div id="fiskalizacija-grupa">
|
||||
<label class="polje-labela">Izdaje fiskalne račune (promet građanima)</label>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .FirmaFiskalizacija "da"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="firma_fiskalizacija" value="da" {{if eq .FirmaFiskalizacija "da"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Da</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .FirmaFiskalizacija "ne"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="firma_fiskalizacija" value="ne" {{if eq .FirmaFiskalizacija "ne"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Ne</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:20px;">
|
||||
<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;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
Sačuvaj status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
{{if .LogoGreska}}
|
||||
@@ -135,4 +223,37 @@
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<script>
|
||||
// Fiskalizacija ima smisla samo u punom režimu. Kad je „samo evidencija", grupa se
|
||||
// zasivi i forsira na „Ne". Ne koristimo disabled (jer disabled polje se ne šalje u
|
||||
// POST pa bi stara vrednost ostala u bazi) — umesto toga pointer-events:none, a radio
|
||||
// „Ne" ostaje aktivan i čekiran, pa se uredno sačuva „ne".
|
||||
(function() {
|
||||
var rezim = document.querySelectorAll('input[name="firma_rezim"]');
|
||||
var grupa = document.getElementById('fiskalizacija-grupa');
|
||||
var fisk = document.querySelectorAll('input[name="firma_fiskalizacija"]');
|
||||
if (!grupa || !rezim.length) return;
|
||||
|
||||
function azuriraj() {
|
||||
var izabran = document.querySelector('input[name="firma_rezim"]:checked');
|
||||
var samoEvidencija = izabran && izabran.value === 'samo_evidencija';
|
||||
grupa.style.opacity = samoEvidencija ? '0.45' : '';
|
||||
grupa.style.pointerEvents = samoEvidencija ? 'none' : '';
|
||||
if (samoEvidencija) {
|
||||
fisk.forEach(function(r) {
|
||||
r.checked = (r.value === 'ne');
|
||||
var lab = r.closest('label');
|
||||
if (lab) {
|
||||
lab.style.borderColor = r.checked ? 'var(--sb-akcent)' : 'var(--ivica)';
|
||||
lab.style.background = r.checked ? 'var(--pozadina)' : 'transparent';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rezim.forEach(function(r) { r.addEventListener('change', azuriraj); });
|
||||
azuriraj();
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user