Merge feature/knjigovodstvo: knjigovodstveni modul
Profil firme i moduli, PDV evidencija (KIR/KPR) sa auto-punjenjem, PDV obračun + PP-PDV, uvoz robe (006/106), šifarnik PDV stopa, kalkulacija prodajne cene (marža po kategoriji/artiklu, zavisni troškovi, dvosmerni izračun) i nivelacija uz trag. # Conflicts: # web/templates/stranice/magacin.html # web/templates/stranice/nabavka_forma.html
This commit is contained in:
@@ -40,6 +40,12 @@ The goal is simple: everything the repair shop needs to track is located in one
|
|||||||
- Service orders — intake, status bar, costs, receipt
|
- Service orders — intake, status bar, costs, receipt
|
||||||
- Sales orders — items, calculation, receipt with company and client details
|
- Sales orders — items, calculation, receipt with company and client details
|
||||||
- Procurement — records of purchases from suppliers
|
- Procurement — records of purchases from suppliers
|
||||||
|
- Sales price calculation on procurement — markup (global, per category, per item), landed costs (customs, shipping...) allocated across items, two-way markup↔price computation; respects VAT-payer status
|
||||||
|
- Price revaluation (nivelacija) — sales price changes with an audit trail (old→new, reason, source, user)
|
||||||
|
- Company profile and modules — features toggle based on company type and VAT-payer status
|
||||||
|
- VAT records (KIR/KPR) — books of issued and received invoices, auto-filled from sales and procurement
|
||||||
|
- VAT calculation per period + mapping to the PP-PDV form; imports (customs declaration) tracked in fields 006/106
|
||||||
|
- VAT rate code list
|
||||||
- Clients and suppliers — contact database
|
- Clients and suppliers — contact database
|
||||||
- Reminders — records with deadlines
|
- Reminders — records with deadlines
|
||||||
- Reports — revenue overview, inventory status
|
- Reports — revenue overview, inventory status
|
||||||
@@ -55,7 +61,8 @@ The goal is simple: everything the repair shop needs to track is located in one
|
|||||||
|
|
||||||
### Planned
|
### Planned
|
||||||
|
|
||||||
- Fiscalization and VAT calculation (specification in Project.md)
|
- Fiscalization (ESIR/PFR) — specification in Project.md
|
||||||
|
- KPO book and double-entry bookkeeping (optional, later phase)
|
||||||
- PostgreSQL support (for multi-user environments)
|
- PostgreSQL support (for multi-user environments)
|
||||||
- WebAuthn / Passkey login (database schema is already prepared)
|
- WebAuthn / Passkey login (database schema is already prepared)
|
||||||
- Notifications (email / WhatsApp) — deferred to a later phase
|
- Notifications (email / WhatsApp) — deferred to a later phase
|
||||||
|
|||||||
+8
-1
@@ -40,6 +40,12 @@ Cilj je jednostavan: sve što servis treba da prati nalazi se na jednom mestu, b
|
|||||||
- Servisni nalozi — prijem, statusna traka, troškovi, priznanica
|
- Servisni nalozi — prijem, statusna traka, troškovi, priznanica
|
||||||
- Prodajni nalozi — stavke, obračun, priznanica sa podacima firme i klijenta
|
- Prodajni nalozi — stavke, obračun, priznanica sa podacima firme i klijenta
|
||||||
- Nabavke — evidencija nabavki od dobavljača
|
- Nabavke — evidencija nabavki od dobavljača
|
||||||
|
- Kalkulacija prodajne cene pri nabavci — marža (globalna, po kategoriji i po artiklu), zavisni troškovi (carina, prevoz...) sa raspodelom na stavke, dvosmerni izračun marža↔prodajna; poštuje status PDV obveznika
|
||||||
|
- Nivelacija — promena prodajne cene uz trag (istorija promena: stara→nova, razlog, izvor, korisnik)
|
||||||
|
- Profil firme i moduli — funkcije se uključuju prema tipu firme i statusu PDV obveznika
|
||||||
|
- PDV evidencija (KIR/KPR) — knjige izdatih i primljenih računa, automatsko punjenje iz prodaje i nabavke
|
||||||
|
- PDV obračun za period + mapiranje na obrazac PP-PDV; uvoz robe (JCI) se vodi u poljima 006/106
|
||||||
|
- Šifarnik PDV stopa
|
||||||
- Klijenti i dobavljači — baza kontakata
|
- Klijenti i dobavljači — baza kontakata
|
||||||
- Podsetnici — evidencija sa rokom
|
- Podsetnici — evidencija sa rokom
|
||||||
- Izveštaji — pregled prihoda, stanje magacina
|
- Izveštaji — pregled prihoda, stanje magacina
|
||||||
@@ -55,7 +61,8 @@ Cilj je jednostavan: sve što servis treba da prati nalazi se na jednom mestu, b
|
|||||||
|
|
||||||
### Planirano
|
### Planirano
|
||||||
|
|
||||||
- Fiskalizacija i PDV obračun (specifikacija u Project.md)
|
- Fiskalizacija (ESIR/PFR) — specifikacija u Project.md
|
||||||
|
- KPO knjiga i dvojno knjigovodstvo (opciono, kasnija faza)
|
||||||
- Podrška za PostgreSQL (za višekorisničko okruženje)
|
- Podrška za PostgreSQL (za višekorisničko okruženje)
|
||||||
- WebAuthn / Passkey prijava (šema baze je pripremljena)
|
- WebAuthn / Passkey prijava (šema baze je pripremljena)
|
||||||
- Obaveštenja (e-pošta / WhatsApp) — odloženo za kasniju fazu
|
- Obaveštenja (e-pošta / WhatsApp) — odloženo za kasniju fazu
|
||||||
|
|||||||
+44
-7
@@ -8,9 +8,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"mime"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -87,12 +87,14 @@ func main() {
|
|||||||
|
|
||||||
db, err := sqlite.OtvoriDB(putanjaBaze)
|
db, err := sqlite.OtvoriDB(putanjaBaze)
|
||||||
if err != nil {
|
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()
|
defer db.Close()
|
||||||
|
|
||||||
if err := sqlite.PokreniMigracije(db, migrFS); err != nil {
|
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")
|
slog.Info("migracije uspešno izvršene")
|
||||||
|
|
||||||
@@ -111,7 +113,8 @@ func main() {
|
|||||||
// ključ za šifrovanje TOTP tajni u mirovanju (AES-256-GCM)
|
// ključ za šifrovanje TOTP tajni u mirovanju (AES-256-GCM)
|
||||||
totpKljuc, err := ucitajTotpKljuc()
|
totpKljuc, err := ucitajTotpKljuc()
|
||||||
if err != nil {
|
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
|
// jednokratno šifruj eventualne stare TOTP tajne koje su ostale kao čist tekst
|
||||||
@@ -137,7 +140,8 @@ func main() {
|
|||||||
if os.Getenv("NTECH_ENV") == "production" {
|
if os.Getenv("NTECH_ENV") == "production" {
|
||||||
kes, err := handler.KreirajKes(templFS)
|
kes, err := handler.KreirajKes(templFS)
|
||||||
if err != nil {
|
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
|
h.Templates = kes
|
||||||
slog.Info("keš šablona kreiran", "broj", len(kes))
|
slog.Info("keš šablona kreiran", "broj", len(kes))
|
||||||
@@ -221,6 +225,20 @@ func main() {
|
|||||||
return ntechmw.RequireDozvolaMut(h.DozvoleRepo.ImaDozvolu, akcija)
|
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) {
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||||
})
|
})
|
||||||
@@ -229,6 +247,10 @@ func main() {
|
|||||||
r.Get("/admin/podesavanja/opste", h.PodesavanjaOpste)
|
r.Get("/admin/podesavanja/opste", h.PodesavanjaOpste)
|
||||||
r.Get("/admin/podesavanja/izgled", h.PodesavanjaIzgled)
|
r.Get("/admin/podesavanja/izgled", h.PodesavanjaIzgled)
|
||||||
r.Get("/admin/podesavanja/sistem", h.PodesavanjaSistem)
|
r.Get("/admin/podesavanja/sistem", h.PodesavanjaSistem)
|
||||||
|
r.Get("/admin/podesavanja/kalkulacija-pdv", 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/sacuvaj", h.SacuvajPodesavanja)
|
||||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/logo", h.OtpremiLogo)
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/logo", h.OtpremiLogo)
|
||||||
r.With(doz("podesavanja.login_pozadina")).Post("/podesavanja/login-pozadina", h.OtpremiLoginPozadinu)
|
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.Get("/podesavanja/backup", h.BackupBaze)
|
||||||
r.With(doz("backup.pokreni")).Post("/podesavanja/backup/vrati", h.VratiBackup)
|
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", h.Magacin)
|
||||||
r.Get("/magacin/novi", h.NoviArtikal)
|
r.Get("/magacin/novi", h.NoviArtikal)
|
||||||
r.With(doz("artikal.dodaj")).Post("/magacin/novi", h.SacuvajArtikal)
|
r.With(doz("artikal.dodaj")).Post("/magacin/novi", h.SacuvajArtikal)
|
||||||
@@ -244,8 +278,11 @@ func main() {
|
|||||||
r.With(doz("artikal.izmeni")).Post("/magacin/izmeni/{id}", h.SacuvajIzmenuArtikla)
|
r.With(doz("artikal.izmeni")).Post("/magacin/izmeni/{id}", h.SacuvajIzmenuArtikla)
|
||||||
r.With(doz("artikal.obrisi")).Get("/magacin/obrisi/{id}", h.ObrisiArtikal)
|
r.With(doz("artikal.obrisi")).Get("/magacin/obrisi/{id}", h.ObrisiArtikal)
|
||||||
r.With(doz("artikal.premesti")).Post("/magacin/premesti/{id}", h.PremestiArtikal)
|
r.With(doz("artikal.premesti")).Post("/magacin/premesti/{id}", h.PremestiArtikal)
|
||||||
|
r.With(doz("artikal.izmeni")).Post("/magacin/promeni-cenu/{id}", h.PromeniCenuArtikla)
|
||||||
|
r.With(doz("artikal.izmeni")).Get("/nivelacije", h.Nivelacije)
|
||||||
r.Get("/magacin/kategorije", h.Kategorije)
|
r.Get("/magacin/kategorije", h.Kategorije)
|
||||||
r.With(doz("kategorija.dodaj")).Post("/magacin/kategorije/dodaj", h.DodajKategoriju)
|
r.With(doz("kategorija.dodaj")).Post("/magacin/kategorije/dodaj", h.DodajKategoriju)
|
||||||
|
r.With(doz("kategorija.izmeni")).Post("/magacin/kategorije/izmeni/{id}", h.IzmeniKategoriju)
|
||||||
r.With(doz("kategorija.obrisi")).Get("/magacin/kategorije/obrisi/{id}", h.ObrisiKategoriju)
|
r.With(doz("kategorija.obrisi")).Get("/magacin/kategorije/obrisi/{id}", h.ObrisiKategoriju)
|
||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke", h.Nabavke)
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke", h.Nabavke)
|
||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke/nova", h.NovaNabavka)
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "nabavka.pregled")).Get("/nabavke/nova", h.NovaNabavka)
|
||||||
@@ -327,7 +364,8 @@ func main() {
|
|||||||
slog.Info("NTech pokrenut", "port", port)
|
slog.Info("NTech pokrenut", "port", port)
|
||||||
err = http.ListenAndServe(":"+port, r)
|
err = http.ListenAndServe(":"+port, r)
|
||||||
if err != nil {
|
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 +457,3 @@ func ocistiStareBackupe(folder string, max int) {
|
|||||||
_ = os.Remove(f)
|
_ = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ type ArtikalRepository interface {
|
|||||||
DohvatiID(ctx context.Context, id int64) (*model.Artikal, error)
|
DohvatiID(ctx context.Context, id int64) (*model.Artikal, error)
|
||||||
Kreiraj(ctx context.Context, a *model.Artikal) (int64, error)
|
Kreiraj(ctx context.Context, a *model.Artikal) (int64, error)
|
||||||
Izmeni(ctx context.Context, a *model.Artikal) error
|
Izmeni(ctx context.Context, a *model.Artikal) error
|
||||||
|
// AzurirajCene menja samo nabavnu i prodajnu cenu (kalkulacija pri nabavci)
|
||||||
|
AzurirajCene(ctx context.Context, id int64, nabavna, prodajna float64) error
|
||||||
PremestiKategoriju(ctx context.Context, id int64, kategorijaID *int64) error
|
PremestiKategoriju(ctx context.Context, id int64, kategorijaID *int64) error
|
||||||
Obrisi(ctx context.Context, id int64) error
|
Obrisi(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
@@ -20,7 +22,51 @@ type ArtikalRepository interface {
|
|||||||
// KategorijaRepository definiše operacije nad kategorijama
|
// KategorijaRepository definiše operacije nad kategorijama
|
||||||
type KategorijaRepository interface {
|
type KategorijaRepository interface {
|
||||||
Lista(ctx context.Context) ([]model.Kategorija, error)
|
Lista(ctx context.Context) ([]model.Kategorija, error)
|
||||||
|
DohvatiID(ctx context.Context, id int64) (*model.Kategorija, error)
|
||||||
Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error)
|
Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error)
|
||||||
|
Izmeni(ctx context.Context, k *model.Kategorija) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// NivelacijaRepository definiše operacije nad evidencijom promene prodajnih cena
|
||||||
|
type NivelacijaRepository interface {
|
||||||
|
// PromeniCenu transakciono menja prodajnu cenu artikla i upisuje nivelacioni zapis;
|
||||||
|
// vraća kreirani zapis (sa starom i novom cenom). Izvor je "rucno".
|
||||||
|
PromeniCenu(ctx context.Context, artikalID int64, novaCena float64, razlog string, korisnikID *int64) (*model.Nivelacija, error)
|
||||||
|
// Kreiraj upisuje gotov nivelacioni zapis (npr. auto-trag pri izmeni artikla)
|
||||||
|
Kreiraj(ctx context.Context, n *model.Nivelacija) (int64, error)
|
||||||
|
// Lista vraća nivelacije u periodu (po datumu); nulti datum znači bez granice
|
||||||
|
Lista(ctx context.Context, od, do time.Time) ([]model.Nivelacija, error)
|
||||||
|
// ListaZaArtikal vraća sve nivelacije jednog artikla (najnovije prvo)
|
||||||
|
ListaZaArtikal(ctx context.Context, artikalID int64) ([]model.Nivelacija, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArtikalFilter definiše parametre za filtriranje liste artikala
|
// ArtikalFilter definiše parametre za filtriranje liste artikala
|
||||||
@@ -35,7 +81,8 @@ type NabavkaRepository interface {
|
|||||||
Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error)
|
Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error)
|
||||||
DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error)
|
DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error)
|
||||||
DohvatiStavke(ctx context.Context, nabavkaID int64) ([]model.StavkaSaArtiklom, error)
|
DohvatiStavke(ctx context.Context, nabavkaID int64) ([]model.StavkaSaArtiklom, error)
|
||||||
Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke) (int64, error)
|
DohvatiTroskove(ctx context.Context, nabavkaID int64) ([]model.NabavkaTrosak, error)
|
||||||
|
Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke, troskovi []model.NabavkaTrosak) (int64, error)
|
||||||
Obrisi(ctx context.Context, id int64) error
|
Obrisi(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
SELECT
|
SELECT
|
||||||
a.id, a.kategorija_id, a.naziv, a.opis,
|
a.id, a.kategorija_id, a.naziv, a.opis,
|
||||||
a.kolicina, a.kolicina_min, a.lokacija,
|
a.kolicina, a.kolicina_min, a.lokacija,
|
||||||
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.napomena, a.datum_unosa,
|
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.marza, a.napomena, a.datum_unosa,
|
||||||
COALESCE(k.naziv, '') as kategorija_naziv
|
COALESCE(k.naziv, '') as kategorija_naziv, k.marza as kategorija_marza
|
||||||
FROM artikli a
|
FROM artikli a
|
||||||
LEFT JOIN kategorije k ON a.kategorija_id = k.id
|
LEFT JOIN kategorije k ON a.kategorija_id = k.id
|
||||||
WHERE 1=1`
|
WHERE 1=1`
|
||||||
@@ -59,12 +59,13 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var a model.ArtikalSaKategorijom
|
var a model.ArtikalSaKategorijom
|
||||||
var kategorijaID sql.NullInt64
|
var kategorijaID sql.NullInt64
|
||||||
|
var marza, katMarza sql.NullFloat64
|
||||||
|
|
||||||
err := redovi.Scan(
|
err := redovi.Scan(
|
||||||
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
||||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
||||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.Napomena, &a.DatumUnosa,
|
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa,
|
||||||
&a.KategorijaNaziv,
|
&a.KategorijaNaziv, &katMarza,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ArtikalRepo.Lista: scan: %w", err)
|
return nil, fmt.Errorf("ntech: ArtikalRepo.Lista: scan: %w", err)
|
||||||
@@ -73,6 +74,12 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
if kategorijaID.Valid {
|
if kategorijaID.Valid {
|
||||||
a.KategorijaID = &kategorijaID.Int64
|
a.KategorijaID = &kategorijaID.Int64
|
||||||
}
|
}
|
||||||
|
if marza.Valid {
|
||||||
|
a.Marza = &marza.Float64
|
||||||
|
}
|
||||||
|
if katMarza.Valid {
|
||||||
|
a.KategorijaMarza = &katMarza.Float64
|
||||||
|
}
|
||||||
|
|
||||||
a.KriticnaZaliha = a.Kolicina <= a.KolicinMin
|
a.KriticnaZaliha = a.Kolicina <= a.KolicinMin
|
||||||
|
|
||||||
@@ -86,14 +93,15 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
|||||||
func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) {
|
func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) {
|
||||||
var a model.Artikal
|
var a model.Artikal
|
||||||
var kategorijaID sql.NullInt64
|
var kategorijaID sql.NullInt64
|
||||||
|
var marza sql.NullFloat64
|
||||||
|
|
||||||
err := r.db.QueryRowContext(ctx, `
|
err := r.db.QueryRowContext(ctx, `
|
||||||
SELECT id, kategorija_id, naziv, opis, kolicina, kolicina_min,
|
SELECT id, kategorija_id, naziv, opis, kolicina, kolicina_min,
|
||||||
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, napomena, datum_unosa
|
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena, datum_unosa
|
||||||
FROM artikli WHERE id = ?`, id).Scan(
|
FROM artikli WHERE id = ?`, id).Scan(
|
||||||
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
||||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
||||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.Napomena, &a.DatumUnosa,
|
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err)
|
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err)
|
||||||
@@ -102,6 +110,9 @@ func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal,
|
|||||||
if kategorijaID.Valid {
|
if kategorijaID.Valid {
|
||||||
a.KategorijaID = &kategorijaID.Int64
|
a.KategorijaID = &kategorijaID.Int64
|
||||||
}
|
}
|
||||||
|
if marza.Valid {
|
||||||
|
a.Marza = &marza.Float64
|
||||||
|
}
|
||||||
|
|
||||||
return &a, nil
|
return &a, nil
|
||||||
}
|
}
|
||||||
@@ -111,10 +122,10 @@ func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, err
|
|||||||
rezultat, err := r.db.ExecContext(ctx, `
|
rezultat, err := r.db.ExecContext(ctx, `
|
||||||
INSERT INTO artikli
|
INSERT INTO artikli
|
||||||
(kategorija_id, naziv, opis, kolicina, kolicina_min, lokacija,
|
(kategorija_id, naziv, opis, kolicina, kolicina_min, lokacija,
|
||||||
nabavna_cena, prodajna_cena, pdv_stopa, napomena)
|
nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina, a.KolicinMin,
|
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina, a.KolicinMin,
|
||||||
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Napomena,
|
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: ArtikalRepo.Kreiraj: %w", err)
|
return 0, fmt.Errorf("ntech: ArtikalRepo.Kreiraj: %w", err)
|
||||||
@@ -134,11 +145,11 @@ func (r *ArtikalRepo) Izmeni(ctx context.Context, a *model.Artikal) error {
|
|||||||
UPDATE artikli SET
|
UPDATE artikli SET
|
||||||
kategorija_id = ?, naziv = ?, opis = ?, kolicina = ?,
|
kategorija_id = ?, naziv = ?, opis = ?, kolicina = ?,
|
||||||
kolicina_min = ?, lokacija = ?,
|
kolicina_min = ?, lokacija = ?,
|
||||||
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, napomena = ?
|
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, marza = ?, napomena = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina,
|
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina,
|
||||||
a.KolicinMin, a.Lokacija,
|
a.KolicinMin, a.Lokacija,
|
||||||
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Napomena, a.ID,
|
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena, a.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ntech: ArtikalRepo.Izmeni: %w", err)
|
return fmt.Errorf("ntech: ArtikalRepo.Izmeni: %w", err)
|
||||||
@@ -147,6 +158,16 @@ func (r *ArtikalRepo) Izmeni(ctx context.Context, a *model.Artikal) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AzurirajCene menja samo nabavnu i prodajnu cenu artikla (kalkulacija pri prijemu robe).
|
||||||
|
func (r *ArtikalRepo) AzurirajCene(ctx context.Context, id int64, nabavna, prodajna float64) error {
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
"UPDATE artikli SET nabavna_cena = ?, prodajna_cena = ? WHERE id = ?", nabavna, prodajna, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: ArtikalRepo.AzurirajCene: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// PremestiKategoriju menja samo kategoriju artikla (premeštanje u drugu kategoriju).
|
// PremestiKategoriju menja samo kategoriju artikla (premeštanje u drugu kategoriju).
|
||||||
// kategorijaID može biti nil — tada artikal ostaje bez kategorije.
|
// kategorijaID može biti nil — tada artikal ostaje bez kategorije.
|
||||||
func (r *ArtikalRepo) PremestiKategoriju(ctx context.Context, id int64, kategorijaID *int64) error {
|
func (r *ArtikalRepo) PremestiKategoriju(ctx context.Context, id int64, kategorijaID *int64) error {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func NoviDobavljacRepo(db *sql.DB) *DobavljacRepo {
|
|||||||
// Lista vraća listu dobavljača sa opcionom pretragom po nazivu
|
// Lista vraća listu dobavljača sa opcionom pretragom po nazivu
|
||||||
func (r *DobavljacRepo) Lista(ctx context.Context, pretraga string) ([]model.Dobavljac, error) {
|
func (r *DobavljacRepo) Lista(ctx context.Context, pretraga string) ([]model.Dobavljac, error) {
|
||||||
upit := `
|
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
|
FROM dobavljaci
|
||||||
WHERE 1=1`
|
WHERE 1=1`
|
||||||
|
|
||||||
@@ -43,9 +43,9 @@ func (r *DobavljacRepo) Lista(ctx context.Context, pretraga string) ([]model.Dob
|
|||||||
var rezultat []model.Dobavljac
|
var rezultat []model.Dobavljac
|
||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var d model.Dobavljac
|
var d model.Dobavljac
|
||||||
var kontaktOsoba, telefon, email, napomena sql.NullString
|
var kontaktOsoba, telefon, email, pib, mesto, napomena sql.NullString
|
||||||
err := redovi.Scan(
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: DobavljacRepo.Lista: scan: %w", err)
|
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.KontaktOsoba = kontaktOsoba.String
|
||||||
d.Telefon = telefon.String
|
d.Telefon = telefon.String
|
||||||
d.Email = email.String
|
d.Email = email.String
|
||||||
|
d.PIB = pib.String
|
||||||
|
d.Mesto = mesto.String
|
||||||
d.Napomena = napomena.String
|
d.Napomena = napomena.String
|
||||||
rezultat = append(rezultat, d)
|
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
|
// DohvatiID vraća jednog dobavljača po ID-u
|
||||||
func (r *DobavljacRepo) DohvatiID(ctx context.Context, id int64) (*model.Dobavljac, error) {
|
func (r *DobavljacRepo) DohvatiID(ctx context.Context, id int64) (*model.Dobavljac, error) {
|
||||||
var d model.Dobavljac
|
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, `
|
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(
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: DobavljacRepo.DohvatiID: %w", err)
|
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.KontaktOsoba = kontaktOsoba.String
|
||||||
d.Telefon = telefon.String
|
d.Telefon = telefon.String
|
||||||
d.Email = email.String
|
d.Email = email.String
|
||||||
|
d.PIB = pib.String
|
||||||
|
d.Mesto = mesto.String
|
||||||
d.Napomena = napomena.String
|
d.Napomena = napomena.String
|
||||||
|
|
||||||
return &d, nil
|
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
|
// Kreiraj dodaje novog dobavljača u bazu
|
||||||
func (r *DobavljacRepo) Kreiraj(ctx context.Context, d *model.Dobavljac) (int64, error) {
|
func (r *DobavljacRepo) Kreiraj(ctx context.Context, d *model.Dobavljac) (int64, error) {
|
||||||
rezultat, err := r.db.ExecContext(ctx, `
|
rezultat, err := r.db.ExecContext(ctx, `
|
||||||
INSERT INTO dobavljaci (naziv, kontakt_osoba, telefon, email, napomena)
|
INSERT INTO dobavljaci (naziv, kontakt_osoba, telefon, email, pib, mesto, napomena)
|
||||||
VALUES (?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
d.Naziv, nullString(d.KontaktOsoba), nullString(d.Telefon),
|
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 {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: DobavljacRepo.Kreiraj: %w", err)
|
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 {
|
func (r *DobavljacRepo) Izmeni(ctx context.Context, d *model.Dobavljac) error {
|
||||||
_, err := r.db.ExecContext(ctx, `
|
_, err := r.db.ExecContext(ctx, `
|
||||||
UPDATE dobavljaci SET
|
UPDATE dobavljaci SET
|
||||||
naziv = ?, kontakt_osoba = ?, telefon = ?, email = ?, napomena = ?
|
naziv = ?, kontakt_osoba = ?, telefon = ?, email = ?, pib = ?, mesto = ?, napomena = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
d.Naziv, nullString(d.KontaktOsoba), nullString(d.Telefon),
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("ntech: DobavljacRepo.Izmeni: %w", err)
|
return fmt.Errorf("ntech: DobavljacRepo.Izmeni: %w", err)
|
||||||
@@ -127,4 +131,3 @@ func (r *DobavljacRepo) Obrisi(ctx context.Context, id int64) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func NovaKategorijaRepo(db *sql.DB) *KategorijaRepo {
|
|||||||
|
|
||||||
// Lista vraća sve kategorije
|
// Lista vraća sve kategorije
|
||||||
func (r *KategorijaRepo) Lista(ctx context.Context) ([]model.Kategorija, error) {
|
func (r *KategorijaRepo) Lista(ctx context.Context) ([]model.Kategorija, error) {
|
||||||
redovi, err := r.db.QueryContext(ctx, "SELECT id, naziv, opis FROM kategorije ORDER BY naziv ASC")
|
redovi, err := r.db.QueryContext(ctx, "SELECT id, naziv, opis, marza FROM kategorije ORDER BY naziv ASC")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: %w", err)
|
return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: %w", err)
|
||||||
}
|
}
|
||||||
@@ -30,12 +30,16 @@ func (r *KategorijaRepo) Lista(ctx context.Context) ([]model.Kategorija, error)
|
|||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var k model.Kategorija
|
var k model.Kategorija
|
||||||
var opis sql.NullString
|
var opis sql.NullString
|
||||||
if err := redovi.Scan(&k.ID, &k.Naziv, &opis); err != nil {
|
var marza sql.NullFloat64
|
||||||
|
if err := redovi.Scan(&k.ID, &k.Naziv, &opis, &marza); err != nil {
|
||||||
return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: scan: %w", err)
|
return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: scan: %w", err)
|
||||||
}
|
}
|
||||||
if opis.Valid {
|
if opis.Valid {
|
||||||
k.Opis = opis.String
|
k.Opis = opis.String
|
||||||
}
|
}
|
||||||
|
if marza.Valid {
|
||||||
|
k.Marza = &marza.Float64
|
||||||
|
}
|
||||||
rezultat = append(rezultat, k)
|
rezultat = append(rezultat, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +49,8 @@ func (r *KategorijaRepo) Lista(ctx context.Context) ([]model.Kategorija, error)
|
|||||||
// Kreiraj dodaje novu kategoriju
|
// Kreiraj dodaje novu kategoriju
|
||||||
func (r *KategorijaRepo) Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error) {
|
func (r *KategorijaRepo) Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error) {
|
||||||
rezultat, err := r.db.ExecContext(ctx,
|
rezultat, err := r.db.ExecContext(ctx,
|
||||||
"INSERT INTO kategorije (naziv, opis) VALUES (?, ?)",
|
"INSERT INTO kategorije (naziv, opis, marza) VALUES (?, ?, ?)",
|
||||||
k.Naziv, k.Opis,
|
k.Naziv, k.Opis, k.Marza,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: KategorijaRepo.Kreiraj: %w", err)
|
return 0, fmt.Errorf("ntech: KategorijaRepo.Kreiraj: %w", err)
|
||||||
@@ -59,3 +63,35 @@ func (r *KategorijaRepo) Kreiraj(ctx context.Context, k *model.Kategorija) (int6
|
|||||||
|
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DohvatiID vraća jednu kategoriju po ID-u
|
||||||
|
func (r *KategorijaRepo) DohvatiID(ctx context.Context, id int64) (*model.Kategorija, error) {
|
||||||
|
var k model.Kategorija
|
||||||
|
var opis sql.NullString
|
||||||
|
var marza sql.NullFloat64
|
||||||
|
err := r.db.QueryRowContext(ctx,
|
||||||
|
"SELECT id, naziv, opis, marza FROM kategorije WHERE id = ?", id).
|
||||||
|
Scan(&k.ID, &k.Naziv, &opis, &marza)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: KategorijaRepo.DohvatiID: %w", err)
|
||||||
|
}
|
||||||
|
if opis.Valid {
|
||||||
|
k.Opis = opis.String
|
||||||
|
}
|
||||||
|
if marza.Valid {
|
||||||
|
k.Marza = &marza.Float64
|
||||||
|
}
|
||||||
|
return &k, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Izmeni ažurira naziv, opis i maržu postojeće kategorije
|
||||||
|
func (r *KategorijaRepo) Izmeni(ctx context.Context, k *model.Kategorija) error {
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
"UPDATE kategorije SET naziv = ?, opis = ?, marza = ? WHERE id = ?",
|
||||||
|
k.Naziv, k.Opis, k.Marza, k.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: KategorijaRepo.Izmeni: %w", err)
|
||||||
|
}
|
||||||
|
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
|
// 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) {
|
func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klijent, error) {
|
||||||
upit := `
|
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
|
FROM klijenti
|
||||||
WHERE 1=1`
|
WHERE 1=1`
|
||||||
|
|
||||||
@@ -44,9 +44,9 @@ func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klije
|
|||||||
var rezultat []model.Klijent
|
var rezultat []model.Klijent
|
||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var k model.Klijent
|
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(
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: KlijentRepo.Lista: scan: %w", err)
|
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.PIB = pib.String
|
||||||
k.Telefon = telefon.String
|
k.Telefon = telefon.String
|
||||||
k.Email = email.String
|
k.Email = email.String
|
||||||
|
k.Mesto = mesto.String
|
||||||
k.Napomena = napomena.String
|
k.Napomena = napomena.String
|
||||||
rezultat = append(rezultat, k)
|
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
|
// DohvatiID vraća jednog klijenta po ID-u
|
||||||
func (r *KlijentRepo) DohvatiID(ctx context.Context, id int64) (*model.Klijent, error) {
|
func (r *KlijentRepo) DohvatiID(ctx context.Context, id int64) (*model.Klijent, error) {
|
||||||
var k model.Klijent
|
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, `
|
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(
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: KlijentRepo.DohvatiID: %w", err)
|
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.PIB = pib.String
|
||||||
k.Telefon = telefon.String
|
k.Telefon = telefon.String
|
||||||
k.Email = email.String
|
k.Email = email.String
|
||||||
|
k.Mesto = mesto.String
|
||||||
k.Napomena = napomena.String
|
k.Napomena = napomena.String
|
||||||
|
|
||||||
return &k, nil
|
return &k, nil
|
||||||
@@ -97,11 +99,11 @@ func (r *KlijentRepo) Kreiraj(ctx context.Context, k *model.Klijent) (int64, err
|
|||||||
k.Tip = "fizicko"
|
k.Tip = "fizicko"
|
||||||
}
|
}
|
||||||
rezultat, err := r.db.ExecContext(ctx, `
|
rezultat, err := r.db.ExecContext(ctx, `
|
||||||
INSERT INTO klijenti (tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, napomena)
|
INSERT INTO klijenti (tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, mesto, napomena)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
k.Tip, nullString(k.Ime), nullString(k.Prezime), nullString(k.JMBG),
|
k.Tip, nullString(k.Ime), nullString(k.Prezime), nullString(k.JMBG),
|
||||||
nullString(k.NazivFirme), nullString(k.PIB), nullString(k.Telefon),
|
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 {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: KlijentRepo.Kreiraj: %w", err)
|
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, `
|
_, err := r.db.ExecContext(ctx, `
|
||||||
UPDATE klijenti SET
|
UPDATE klijenti SET
|
||||||
tip = ?, ime = ?, prezime = ?, jmbg = ?, naziv_firme = ?,
|
tip = ?, ime = ?, prezime = ?, jmbg = ?, naziv_firme = ?,
|
||||||
pib = ?, telefon = ?, email = ?, napomena = ?
|
pib = ?, telefon = ?, email = ?, mesto = ?, napomena = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
k.Tip, nullString(k.Ime), nullString(k.Prezime), nullString(k.JMBG),
|
k.Tip, nullString(k.Ime), nullString(k.Prezime), nullString(k.JMBG),
|
||||||
nullString(k.NazivFirme), nullString(k.PIB), nullString(k.Telefon),
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("ntech: KlijentRepo.Izmeni: %w", err)
|
return fmt.Errorf("ntech: KlijentRepo.Izmeni: %w", err)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func NoviNabavkaRepo(db *sql.DB) *NabavkaRepo {
|
|||||||
func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error) {
|
func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error) {
|
||||||
redovi, err := r.db.QueryContext(ctx, `
|
redovi, err := r.db.QueryContext(ctx, `
|
||||||
SELECT
|
SELECT
|
||||||
n.id, n.dobavljac_id, n.napomena, n.ukupno, n.datum,
|
n.id, n.dobavljac_id, n.napomena, n.ukupno, n.metod_raspodele, n.datum,
|
||||||
COALESCE(d.naziv, '') AS dobavljac_naziv
|
COALESCE(d.naziv, '') AS dobavljac_naziv
|
||||||
FROM nabavke n
|
FROM nabavke n
|
||||||
LEFT JOIN dobavljaci d ON n.dobavljac_id = d.id
|
LEFT JOIN dobavljaci d ON n.dobavljac_id = d.id
|
||||||
@@ -36,10 +36,10 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err
|
|||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var n model.NabavkaSaDetaljem
|
var n model.NabavkaSaDetaljem
|
||||||
var dobavljacID sql.NullInt64
|
var dobavljacID sql.NullInt64
|
||||||
var napomena sql.NullString
|
var napomena, metod sql.NullString
|
||||||
|
|
||||||
err := redovi.Scan(
|
err := redovi.Scan(
|
||||||
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &n.Datum,
|
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod, &n.Datum,
|
||||||
&n.DobavljacNaziv,
|
&n.DobavljacNaziv,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -50,6 +50,7 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err
|
|||||||
n.DobavljacID = &dobavljacID.Int64
|
n.DobavljacID = &dobavljacID.Int64
|
||||||
}
|
}
|
||||||
n.Napomena = napomena.String
|
n.Napomena = napomena.String
|
||||||
|
n.MetodRaspodele = metod.String
|
||||||
|
|
||||||
rezultat = append(rezultat, n)
|
rezultat = append(rezultat, n)
|
||||||
}
|
}
|
||||||
@@ -61,12 +62,12 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err
|
|||||||
func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error) {
|
func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error) {
|
||||||
var n model.Nabavka
|
var n model.Nabavka
|
||||||
var dobavljacID sql.NullInt64
|
var dobavljacID sql.NullInt64
|
||||||
var napomena sql.NullString
|
var napomena, metod sql.NullString
|
||||||
|
|
||||||
err := r.db.QueryRowContext(ctx, `
|
err := r.db.QueryRowContext(ctx, `
|
||||||
SELECT id, dobavljac_id, napomena, ukupno, datum
|
SELECT id, dobavljac_id, napomena, ukupno, metod_raspodele, datum
|
||||||
FROM nabavke WHERE id = ?`, id).Scan(
|
FROM nabavke WHERE id = ?`, id).Scan(
|
||||||
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &n.Datum,
|
&n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod, &n.Datum,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiID: %w", err)
|
return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiID: %w", err)
|
||||||
@@ -76,6 +77,7 @@ func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka,
|
|||||||
n.DobavljacID = &dobavljacID.Int64
|
n.DobavljacID = &dobavljacID.Int64
|
||||||
}
|
}
|
||||||
n.Napomena = napomena.String
|
n.Napomena = napomena.String
|
||||||
|
n.MetodRaspodele = metod.String
|
||||||
|
|
||||||
return &n, nil
|
return &n, nil
|
||||||
}
|
}
|
||||||
@@ -113,8 +115,33 @@ func (r *NabavkaRepo) DohvatiStavke(ctx context.Context, nabavkaID int64) ([]mod
|
|||||||
return rezultat, nil
|
return rezultat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kreiraj upisuje novu nabavku sa svim stavkama u jednoj transakciji i ažurira stanje magacina
|
// DohvatiTroskove vraća sve zavisne troškove jedne nabavke
|
||||||
func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke) (int64, error) {
|
func (r *NabavkaRepo) DohvatiTroskove(ctx context.Context, nabavkaID int64) ([]model.NabavkaTrosak, error) {
|
||||||
|
redovi, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT id, nabavka_id, naziv, iznos
|
||||||
|
FROM nabavka_troskovi
|
||||||
|
WHERE nabavka_id = ?
|
||||||
|
ORDER BY id ASC`, nabavkaID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiTroskove: %w", err)
|
||||||
|
}
|
||||||
|
defer redovi.Close()
|
||||||
|
|
||||||
|
var rezultat []model.NabavkaTrosak
|
||||||
|
for redovi.Next() {
|
||||||
|
var t model.NabavkaTrosak
|
||||||
|
if err := redovi.Scan(&t.ID, &t.NabavkaID, &t.Naziv, &t.Iznos); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiTroskove: scan: %w", err)
|
||||||
|
}
|
||||||
|
rezultat = append(rezultat, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rezultat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kreiraj upisuje novu nabavku sa svim stavkama i zavisnim troškovima u jednoj
|
||||||
|
// transakciji i ažurira stanje magacina
|
||||||
|
func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke, troskovi []model.NabavkaTrosak) (int64, error) {
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: begin: %w", err)
|
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: begin: %w", err)
|
||||||
@@ -130,9 +157,9 @@ func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []mo
|
|||||||
|
|
||||||
// upisujemo zaglavlje nabavke
|
// upisujemo zaglavlje nabavke
|
||||||
rezultat, err := tx.ExecContext(ctx, `
|
rezultat, err := tx.ExecContext(ctx, `
|
||||||
INSERT INTO nabavke (dobavljac_id, napomena, ukupno)
|
INSERT INTO nabavke (dobavljac_id, napomena, ukupno, metod_raspodele)
|
||||||
VALUES (?, ?, ?)`,
|
VALUES (?, ?, ?, ?)`,
|
||||||
nullInt64(n.DobavljacID), nullString(n.Napomena), ukupno,
|
nullInt64(n.DobavljacID), nullString(n.Napomena), ukupno, nullString(n.MetodRaspodele),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: insert nabavka: %w", err)
|
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: insert nabavka: %w", err)
|
||||||
@@ -143,6 +170,18 @@ func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []mo
|
|||||||
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: last insert id: %w", err)
|
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: last insert id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upisujemo zavisne troškove (ako ih ima)
|
||||||
|
for _, t := range troskovi {
|
||||||
|
_, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO nabavka_troskovi (nabavka_id, naziv, iznos)
|
||||||
|
VALUES (?, ?, ?)`,
|
||||||
|
nabavkaID, t.Naziv, t.Iznos,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: insert trošak: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// upisujemo svaku stavku i ažuriramo stanje artikla u magacinu
|
// upisujemo svaku stavku i ažuriramo stanje artikla u magacinu
|
||||||
for _, s := range stavke {
|
for _, s := range stavke {
|
||||||
_, err := tx.ExecContext(ctx, `
|
_, err := tx.ExecContext(ctx, `
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ntech/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NivelacijaRepo je SQLite implementacija NivelacijaRepository interfejsa
|
||||||
|
type NivelacijaRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoviNivelacijaRepo kreira novi NivelacijaRepo
|
||||||
|
func NoviNivelacijaRepo(db *sql.DB) *NivelacijaRepo {
|
||||||
|
return &NivelacijaRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrArtikalNePostoji se vraća kada se menja cena nepostojećeg artikla
|
||||||
|
var ErrArtikalNePostoji = errors.New("artikal ne postoji")
|
||||||
|
|
||||||
|
// PromeniCenu transakciono menja prodajnu cenu artikla i upisuje nivelacioni zapis.
|
||||||
|
// Stara cena se čita iz baze unutar transakcije; izvor je "rucno".
|
||||||
|
func (r *NivelacijaRepo) PromeniCenu(ctx context.Context, artikalID int64, novaCena float64, razlog string, korisnikID *int64) (*model.Nivelacija, error) {
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NivelacijaRepo.PromeniCenu: begin: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
var stara float64
|
||||||
|
err = tx.QueryRowContext(ctx, "SELECT prodajna_cena FROM artikli WHERE id = ?", artikalID).Scan(&stara)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrArtikalNePostoji
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NivelacijaRepo.PromeniCenu: čitanje cene: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = tx.ExecContext(ctx, "UPDATE artikli SET prodajna_cena = ? WHERE id = ?", novaCena, artikalID); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NivelacijaRepo.PromeniCenu: update cene: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sada := time.Now()
|
||||||
|
rez, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO nivelacije (artikal_id, stara_cena, nova_cena, razlog, izvor, korisnik_id, datum)
|
||||||
|
VALUES (?, ?, ?, ?, 'rucno', ?, ?)`,
|
||||||
|
artikalID, stara, novaCena, razlog, izvorIDArg(korisnikID), sada)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NivelacijaRepo.PromeniCenu: upis nivelacije: %w", err)
|
||||||
|
}
|
||||||
|
id, _ := rez.LastInsertId()
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NivelacijaRepo.PromeniCenu: commit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Nivelacija{
|
||||||
|
ID: id, ArtikalID: artikalID, StaraCena: stara, NovaCena: novaCena,
|
||||||
|
Razlog: razlog, Izvor: "rucno", KorisnikID: korisnikID, Datum: sada,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kreiraj upisuje gotov nivelacioni zapis (npr. auto-trag pri izmeni artikla).
|
||||||
|
func (r *NivelacijaRepo) Kreiraj(ctx context.Context, n *model.Nivelacija) (int64, error) {
|
||||||
|
izvor := n.Izvor
|
||||||
|
if izvor == "" {
|
||||||
|
izvor = "rucno"
|
||||||
|
}
|
||||||
|
datum := n.Datum
|
||||||
|
if datum.IsZero() {
|
||||||
|
datum = time.Now()
|
||||||
|
}
|
||||||
|
rez, err := r.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO nivelacije (artikal_id, stara_cena, nova_cena, razlog, izvor, korisnik_id, datum)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
n.ArtikalID, n.StaraCena, n.NovaCena, n.Razlog, izvor, izvorIDArg(n.KorisnikID), datum)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ntech: NivelacijaRepo.Kreiraj: %w", err)
|
||||||
|
}
|
||||||
|
id, err := rez.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ntech: NivelacijaRepo.Kreiraj: id: %w", err)
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lista vraća nivelacije u periodu (po datumu); nulti datum znači bez granice.
|
||||||
|
func (r *NivelacijaRepo) Lista(ctx context.Context, od, do time.Time) ([]model.Nivelacija, error) {
|
||||||
|
upit := nivelacijaSelect + " WHERE 1=1"
|
||||||
|
args := []any{}
|
||||||
|
if !od.IsZero() {
|
||||||
|
upit += " AND n.datum >= ?"
|
||||||
|
args = append(args, od)
|
||||||
|
}
|
||||||
|
if !do.IsZero() {
|
||||||
|
upit += " AND n.datum <= ?"
|
||||||
|
args = append(args, do)
|
||||||
|
}
|
||||||
|
upit += " ORDER BY n.datum DESC, n.id DESC"
|
||||||
|
return r.upitVisestruko(ctx, upit, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListaZaArtikal vraća sve nivelacije jednog artikla (najnovije prvo).
|
||||||
|
func (r *NivelacijaRepo) ListaZaArtikal(ctx context.Context, artikalID int64) ([]model.Nivelacija, error) {
|
||||||
|
upit := nivelacijaSelect + " WHERE n.artikal_id = ? ORDER BY n.datum DESC, n.id DESC"
|
||||||
|
return r.upitVisestruko(ctx, upit, artikalID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nivelacijaSelect je zajednički SELECT sa JOIN-ovima na artikle i korisnike (za prikaz).
|
||||||
|
const nivelacijaSelect = `
|
||||||
|
SELECT n.id, n.artikal_id, COALESCE(a.naziv, ''), n.stara_cena, n.nova_cena,
|
||||||
|
COALESCE(n.razlog, ''), n.izvor, n.korisnik_id, COALESCE(k.korisnicko_ime, ''),
|
||||||
|
n.datum, n.datum_unosa
|
||||||
|
FROM nivelacije n
|
||||||
|
LEFT JOIN artikli a ON a.id = n.artikal_id
|
||||||
|
LEFT JOIN korisnici k ON k.id = n.korisnik_id`
|
||||||
|
|
||||||
|
// upitVisestruko izvršava SELECT i skenira sve redove u listu nivelacija.
|
||||||
|
func (r *NivelacijaRepo) upitVisestruko(ctx context.Context, upit string, args ...any) ([]model.Nivelacija, error) {
|
||||||
|
redovi, err := r.db.QueryContext(ctx, upit, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NivelacijaRepo: %w", err)
|
||||||
|
}
|
||||||
|
defer redovi.Close()
|
||||||
|
|
||||||
|
var rezultat []model.Nivelacija
|
||||||
|
for redovi.Next() {
|
||||||
|
var n model.Nivelacija
|
||||||
|
var korisnikID sql.NullInt64
|
||||||
|
if err := redovi.Scan(
|
||||||
|
&n.ID, &n.ArtikalID, &n.ArtikalNaziv, &n.StaraCena, &n.NovaCena,
|
||||||
|
&n.Razlog, &n.Izvor, &korisnikID, &n.KorisnikIme, &n.Datum, &n.DatumUnosa,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NivelacijaRepo: scan: %w", err)
|
||||||
|
}
|
||||||
|
if korisnikID.Valid {
|
||||||
|
n.KorisnikID = &korisnikID.Int64
|
||||||
|
}
|
||||||
|
rezultat = append(rezultat, n)
|
||||||
|
}
|
||||||
|
if err := redovi.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: NivelacijaRepo: %w", err)
|
||||||
|
}
|
||||||
|
return rezultat, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
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, uvoz, 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, uvoz, 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
|
||||||
|
var uvoz int64
|
||||||
|
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, &uvoz, &k.DatumUnosa,
|
||||||
|
); err != nil {
|
||||||
|
return model.PdvKpr{}, err
|
||||||
|
}
|
||||||
|
if datumPlacanja.Valid {
|
||||||
|
k.DatumPlacanja = &datumPlacanja.Time
|
||||||
|
}
|
||||||
|
if izvorID.Valid {
|
||||||
|
k.IzvorID = &izvorID.Int64
|
||||||
|
}
|
||||||
|
k.Uvoz = uvoz != 0
|
||||||
|
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
|
||||||
|
}
|
||||||
|
uvoz := 0
|
||||||
|
if k.Uvoz {
|
||||||
|
uvoz = 1
|
||||||
|
}
|
||||||
|
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, uvoz
|
||||||
|
) 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), uvoz)
|
||||||
|
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
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -85,7 +86,15 @@ func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dobavljac, greska := parseFormuDobavljaca(r)
|
dobavljac, greska := parseFormuDobavljaca(r)
|
||||||
|
jeAjax := r.Header.Get("X-Requested-With") == "fetch"
|
||||||
if greska != "" {
|
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)
|
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||||
ps.Stranica = "dobavljaci"
|
ps.Stranica = "dobavljaci"
|
||||||
@@ -99,11 +108,19 @@ func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
http.Error(w, "Greška pri čuvanju dobavljača", http.StatusInternalServerError)
|
||||||
return
|
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)
|
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")),
|
KontaktOsoba: strings.TrimSpace(r.FormValue("kontakt_osoba")),
|
||||||
Telefon: strings.TrimSpace(r.FormValue("telefon")),
|
Telefon: strings.TrimSpace(r.FormValue("telefon")),
|
||||||
Email: email,
|
Email: email,
|
||||||
|
PIB: strings.TrimSpace(r.FormValue("pib")),
|
||||||
|
Mesto: strings.TrimSpace(r.FormValue("mesto")),
|
||||||
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
||||||
}, ""
|
}, ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"ntech/internal/config"
|
||||||
"ntech/internal/db"
|
"ntech/internal/db"
|
||||||
"ntech/internal/db/sqlite"
|
"ntech/internal/db/sqlite"
|
||||||
"ntech/internal/middleware"
|
"ntech/internal/middleware"
|
||||||
@@ -34,6 +36,10 @@ type Handler struct {
|
|||||||
PokusajiRepo db.PokusajiPrijaveRepository
|
PokusajiRepo db.PokusajiPrijaveRepository
|
||||||
LoginIstorijsaRepo db.LoginIstorijsaRepository
|
LoginIstorijsaRepo db.LoginIstorijsaRepository
|
||||||
DozvoleRepo db.DozvoleRepository
|
DozvoleRepo db.DozvoleRepository
|
||||||
|
PdvStopeRepo db.PdvStopaRepository
|
||||||
|
PdvKirRepo db.PdvKirRepository
|
||||||
|
PdvKprRepo db.PdvKprRepository
|
||||||
|
NivelacijaRepo db.NivelacijaRepository
|
||||||
Verzija string
|
Verzija string
|
||||||
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
|
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
|
||||||
Templates map[string]*template.Template
|
Templates map[string]*template.Template
|
||||||
@@ -93,6 +99,10 @@ func Novi(baza *sql.DB, totpKljuc []byte) *Handler {
|
|||||||
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
|
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
|
||||||
LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza),
|
LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza),
|
||||||
DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()),
|
DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()),
|
||||||
|
PdvStopeRepo: sqlite.NoviPdvStopaRepo(baza),
|
||||||
|
PdvKirRepo: sqlite.NoviPdvKirRepo(baza),
|
||||||
|
PdvKprRepo: sqlite.NoviPdvKprRepo(baza),
|
||||||
|
NivelacijaRepo: sqlite.NoviNivelacijaRepo(baza),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +128,20 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
|
|||||||
h.PokusajiRepo = sqlite.NoviPokusajiPrijaveRepo(novaDB)
|
h.PokusajiRepo = sqlite.NoviPokusajiPrijaveRepo(novaDB)
|
||||||
h.LoginIstorijsaRepo = sqlite.NoviLoginIstorijsaRepo(novaDB)
|
h.LoginIstorijsaRepo = sqlite.NoviLoginIstorijsaRepo(novaDB)
|
||||||
h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije())
|
h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije())
|
||||||
|
h.PdvStopeRepo = sqlite.NoviPdvStopaRepo(novaDB)
|
||||||
|
h.PdvKirRepo = sqlite.NoviPdvKirRepo(novaDB)
|
||||||
|
h.PdvKprRepo = sqlite.NoviPdvKprRepo(novaDB)
|
||||||
|
h.NivelacijaRepo = sqlite.NoviNivelacijaRepo(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.
|
// zahtevajDozvolu vraća prijavljenog korisnika ako njegova uloga sme da izvrši akciju.
|
||||||
@@ -159,6 +183,8 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
|||||||
ps.CsrfToken = middleware.CsrfToken(r.Context())
|
ps.CsrfToken = middleware.CsrfToken(r.Context())
|
||||||
ps.AssetV = h.AssetV
|
ps.AssetV = h.AssetV
|
||||||
ps.Flash = middleware.GetFlash(r, h.DB)
|
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:
|
// logika pozadine:
|
||||||
// - lična pozadina → uvek se prikazuje i forsira tamnu temu, bez obzira na KoristiLokalnuTemu
|
// - lična pozadina → uvek se prikazuje i forsira tamnu temu, bez obzira na KoristiLokalnuTemu
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"ntech/internal/db/sqlite"
|
"ntech/internal/db/sqlite"
|
||||||
"ntech/internal/model"
|
"ntech/internal/model"
|
||||||
@@ -64,6 +65,7 @@ func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) {
|
|||||||
k := &model.Kategorija{
|
k := &model.Kategorija{
|
||||||
Naziv: naziv,
|
Naziv: naziv,
|
||||||
Opis: r.FormValue("opis"),
|
Opis: r.FormValue("opis"),
|
||||||
|
Marza: parsirajMarzu(r.FormValue("marza")),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.KategorijeRepo.Kreiraj(r.Context(), k); err != nil {
|
if _, err := h.KategorijeRepo.Kreiraj(r.Context(), k); err != nil {
|
||||||
@@ -74,6 +76,56 @@ func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/magacin/kategorije?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/magacin/kategorije?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IzmeniKategoriju prima POST i ažurira naziv, opis i maržu postojeće kategorije
|
||||||
|
func (h *Handler) IzmeniKategoriju(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, ok := h.zahtevajDozvolu(w, r, "kategorija.izmeni"); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Neispravan ID kategorije", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
naziv := r.FormValue("naziv")
|
||||||
|
if naziv == "" {
|
||||||
|
http.Redirect(w, r, "/magacin/kategorije", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
k := &model.Kategorija{
|
||||||
|
ID: id,
|
||||||
|
Naziv: naziv,
|
||||||
|
Opis: r.FormValue("opis"),
|
||||||
|
Marza: parsirajMarzu(r.FormValue("marza")),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.KategorijeRepo.Izmeni(r.Context(), k); err != nil {
|
||||||
|
http.Error(w, "Greška pri čuvanju izmene kategorije", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/magacin/kategorije?sacuvano=1", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsirajMarzu pretvara tekst iz forme u *float64; prazno/neispravno → nil (NULL u bazi)
|
||||||
|
func parsirajMarzu(s string) *float64 {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil || v < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
// ObrisiKategoriju briše kategoriju po ID-u
|
// ObrisiKategoriju briše kategoriju po ID-u
|
||||||
func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, ok := h.zahtevajDozvolu(w, r, "kategorija.obrisi"); !ok {
|
if _, ok := h.zahtevajDozvolu(w, r, "kategorija.obrisi"); !ok {
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ var saSidebar = []string{
|
|||||||
"magacin", "magacin_forma",
|
"magacin", "magacin_forma",
|
||||||
"nabavke", "nabavka_forma", "nabavka_detalji",
|
"nabavke", "nabavka_forma", "nabavka_detalji",
|
||||||
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
|
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
|
||||||
|
"pdv_stope",
|
||||||
|
"pdv_kir", "pdv_kir_forma",
|
||||||
|
"pdv_kpr", "pdv_kpr_forma",
|
||||||
|
"pdv_obracun",
|
||||||
|
"nivelacije",
|
||||||
"podsetnici", "podsetnik_forma",
|
"podsetnici", "podsetnik_forma",
|
||||||
"profil_tema",
|
"profil_tema",
|
||||||
"prodaja", "prodaja_detalji", "prodaja_forma",
|
"prodaja", "prodaja_detalji", "prodaja_forma",
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ func parseFormuKlijenta(r *http.Request) (model.Klijent, string) {
|
|||||||
PIB: strings.TrimSpace(r.FormValue("pib")),
|
PIB: strings.TrimSpace(r.FormValue("pib")),
|
||||||
Telefon: strings.TrimSpace(r.FormValue("telefon")),
|
Telefon: strings.TrimSpace(r.FormValue("telefon")),
|
||||||
Email: email,
|
Email: email,
|
||||||
|
Mesto: strings.TrimSpace(r.FormValue("mesto")),
|
||||||
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
||||||
}, ""
|
}, ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -182,12 +183,32 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stara prodajna cena — za nivelacioni trag ako se promeni kroz izmenu
|
||||||
|
var staraCena float64
|
||||||
|
if stari, e := h.Artikli.DohvatiID(r.Context(), id); e == nil {
|
||||||
|
staraCena = stari.ProdajnaCena
|
||||||
|
}
|
||||||
|
|
||||||
artikal.ID = id
|
artikal.ID = id
|
||||||
if err := h.Artikli.Izmeni(r.Context(), &artikal); err != nil {
|
if err := h.Artikli.Izmeni(r.Context(), &artikal); err != nil {
|
||||||
http.Error(w, "Greška pri čuvanju izmene", http.StatusInternalServerError)
|
http.Error(w, "Greška pri čuvanju izmene", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ako se prodajna cena promenila, automatski upiši nivelacioni zapis (izvor "izmena")
|
||||||
|
if razlika := artikal.ProdajnaCena - staraCena; razlika > 0.005 || razlika < -0.005 {
|
||||||
|
korisnikID := &k.ID
|
||||||
|
if _, e := h.NivelacijaRepo.Kreiraj(r.Context(), &model.Nivelacija{
|
||||||
|
ArtikalID: id,
|
||||||
|
StaraCena: staraCena,
|
||||||
|
NovaCena: artikal.ProdajnaCena,
|
||||||
|
Izvor: "izmena",
|
||||||
|
KorisnikID: korisnikID,
|
||||||
|
}); e != nil {
|
||||||
|
slog.Error("auto-nivelacija pri izmeni artikla nije upisana", "artikal_id", id, "error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/magacin?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/magacin?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +249,15 @@ func parseFormuArtikla(r *http.Request) (model.Artikal, string) {
|
|||||||
artikal.ProdajnaCena = v
|
artikal.ProdajnaCena = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// marža (%) je opciona; prazno polje ostaje NULL (artikal nasleđuje maržu kategorije/globalnu)
|
||||||
|
if m := r.FormValue("marza"); m != "" {
|
||||||
|
v, err := strconv.ParseFloat(m, 64)
|
||||||
|
if err != nil || v < 0 {
|
||||||
|
return artikal, "Marža mora biti pozitivan broj."
|
||||||
|
}
|
||||||
|
artikal.Marza = &v
|
||||||
|
}
|
||||||
|
|
||||||
if katID := r.FormValue("kategorija_id"); katID != "" {
|
if katID := r.FormValue("kategorija_id"); katID != "" {
|
||||||
id, err := strconv.ParseInt(katID, 10, 64)
|
id, err := strconv.ParseInt(katID, 10, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
+143
-12
@@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -29,6 +30,8 @@ type PodaciFormeNabavke struct {
|
|||||||
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
|
Dobavljaci []model.Dobavljac
|
||||||
Kategorije []model.Kategorija // za dropdown u modalu novog artikla
|
Kategorije []model.Kategorija // za dropdown u modalu novog artikla
|
||||||
|
Marza string // podrazumevana marža (%) za kalkulaciju
|
||||||
|
PdvObveznik bool // da li firma obračunava PDV (utiče na prodajnu cenu u kalkulaciji)
|
||||||
Greska string
|
Greska string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +40,8 @@ type PodaciDetaljiNabavke struct {
|
|||||||
model.PodaciStranice
|
model.PodaciStranice
|
||||||
Nabavka model.Nabavka
|
Nabavka model.Nabavka
|
||||||
Stavke []model.StavkaSaArtiklom
|
Stavke []model.StavkaSaArtiklom
|
||||||
|
Troskovi []model.NabavkaTrosak
|
||||||
|
UkupanTrosak float64
|
||||||
DobavljacNaziv string
|
DobavljacNaziv string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,10 +50,16 @@ func artikalUJSON(artikli []model.ArtikalSaKategorijom) template.JS {
|
|||||||
type stavka struct {
|
type stavka struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Naziv string `json:"naziv"`
|
Naziv string `json:"naziv"`
|
||||||
|
PdvStopa float64 `json:"pdv_stopa"`
|
||||||
|
Marza *float64 `json:"marza"` // marža artikla; null = nije postavljeno
|
||||||
|
KategorijaMarza *float64 `json:"kategorija_marza"` // marža kategorije; fallback ako artikal nema
|
||||||
}
|
}
|
||||||
lista := make([]stavka, 0, len(artikli))
|
lista := make([]stavka, 0, len(artikli))
|
||||||
for _, a := range artikli {
|
for _, a := range artikli {
|
||||||
lista = append(lista, stavka{ID: a.ID, Naziv: a.Naziv})
|
lista = append(lista, stavka{
|
||||||
|
ID: a.ID, Naziv: a.Naziv, PdvStopa: a.PdvStopa,
|
||||||
|
Marza: a.Marza, KategorijaMarza: a.KategorijaMarza,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(lista)
|
b, _ := json.Marshal(lista)
|
||||||
return template.JS(b)
|
return template.JS(b)
|
||||||
@@ -116,12 +127,15 @@ func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) {
|
|||||||
ArtikliJSON: artikalUJSON(artikli),
|
ArtikliJSON: artikalUJSON(artikli),
|
||||||
Dobavljaci: dobavljaci,
|
Dobavljaci: dobavljaci,
|
||||||
Kategorije: kategorije,
|
Kategorije: kategorije,
|
||||||
|
Marza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
||||||
|
PdvObveznik: h.modulUkljucen(r.Context(), "pdv"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SacuvajNabavku prima POST formu, parsira stavke i upisuje nabavku u bazu
|
// SacuvajNabavku prima POST formu, parsira stavke i upisuje nabavku u bazu
|
||||||
func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, ok := h.zahtevajDozvolu(w, r, "nabavka.dodaj"); !ok {
|
k, ok := h.zahtevajDozvolu(w, r, "nabavka.dodaj")
|
||||||
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
@@ -129,7 +143,7 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nabavka, stavke, greska := parseFormuNabavke(r)
|
nabavka, stavke, troskovi, greska := parseFormuNabavke(r)
|
||||||
if greska != "" {
|
if greska != "" {
|
||||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
artikli, _ := h.Artikli.Lista(r.Context(), db.ArtikalFilter{})
|
artikli, _ := h.Artikli.Lista(r.Context(), db.ArtikalFilter{})
|
||||||
@@ -144,17 +158,89 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
ArtikliJSON: artikalUJSON(artikli),
|
ArtikliJSON: artikalUJSON(artikli),
|
||||||
Dobavljaci: dobavljaci,
|
Dobavljaci: dobavljaci,
|
||||||
Kategorije: kategorije,
|
Kategorije: kategorije,
|
||||||
|
Marza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
||||||
|
PdvObveznik: h.modulUkljucen(r.Context(), "pdv"),
|
||||||
Greska: greska,
|
Greska: greska,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := h.NabavkeRepo.Kreiraj(r.Context(), &nabavka, stavke)
|
id, err := h.NabavkeRepo.Kreiraj(r.Context(), &nabavka, stavke, troskovi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Greška pri čuvanju nabavke", http.StatusInternalServerError)
|
http.Error(w, "Greška pri čuvanju nabavke", http.StatusInternalServerError)
|
||||||
return
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// kalkulacija: ažuriraj nabavnu i prodajnu cenu artikla iz forme + nivelacioni trag.
|
||||||
|
// prodajna[] je paralelni niz uz stavke (isti redosled kao artikal_id[]).
|
||||||
|
prodajne := r.Form["prodajna[]"]
|
||||||
|
var korisnikID *int64
|
||||||
|
if k != nil {
|
||||||
|
korisnikID = &k.ID
|
||||||
|
}
|
||||||
|
// kalkulativna nabavna cena po stavci (fakturna + raspodeljeni zavisni trošak) —
|
||||||
|
// računa se na serveru; bez troškova je jednaka čistoj ceni po komadu.
|
||||||
|
var ukupanTrosak float64
|
||||||
|
for _, t := range troskovi {
|
||||||
|
ukupanTrosak += t.Iznos
|
||||||
|
}
|
||||||
|
kalkNabavna := model.RasporediTroskove(stavke, ukupanTrosak, nabavka.MetodRaspodele)
|
||||||
|
for i, s := range stavke {
|
||||||
|
if i >= len(prodajne) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
prodajna, e := strconv.ParseFloat(strings.TrimSpace(prodajne[i]), 64)
|
||||||
|
if e != nil || prodajna <= 0 {
|
||||||
|
continue // prazno/nula ne sme da pregazi postojeću cenu
|
||||||
|
}
|
||||||
|
// stara prodajna cena — za nivelacioni zapis
|
||||||
|
var staraProdajna float64
|
||||||
|
if a, e := h.Artikli.DohvatiID(r.Context(), s.ArtikalID); e == nil {
|
||||||
|
staraProdajna = a.ProdajnaCena
|
||||||
|
}
|
||||||
|
if e := h.Artikli.AzurirajCene(r.Context(), s.ArtikalID, kalkNabavna[i], prodajna); e != nil {
|
||||||
|
slog.Error("kalkulacija: ažuriranje cena nije uspelo", "artikal_id", s.ArtikalID, "error", e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if razlika := prodajna - staraProdajna; razlika > 0.005 || razlika < -0.005 {
|
||||||
|
if _, e := h.NivelacijaRepo.Kreiraj(r.Context(), &model.Nivelacija{
|
||||||
|
ArtikalID: s.ArtikalID,
|
||||||
|
StaraCena: staraProdajna,
|
||||||
|
NovaCena: prodajna,
|
||||||
|
Izvor: "kalkulacija",
|
||||||
|
KorisnikID: korisnikID,
|
||||||
|
}); e != nil {
|
||||||
|
slog.Error("kalkulacija: nivelacija nije upisana", "artikal_id", s.ArtikalID, "error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +264,12 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
troskovi, err := h.NabavkeRepo.DohvatiTroskove(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Greška pri učitavanju troškova", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||||
@@ -196,10 +288,16 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
|
|||||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||||
ps.Stranica = "nabavke"
|
ps.Stranica = "nabavke"
|
||||||
ps.NaslovStranice = "Detalji nabavke"
|
ps.NaslovStranice = "Detalji nabavke"
|
||||||
|
var ukupanTrosak float64
|
||||||
|
for _, t := range troskovi {
|
||||||
|
ukupanTrosak += t.Iznos
|
||||||
|
}
|
||||||
podaci := PodaciDetaljiNabavke{
|
podaci := PodaciDetaljiNabavke{
|
||||||
PodaciStranice: ps,
|
PodaciStranice: ps,
|
||||||
Nabavka: *nabavka,
|
Nabavka: *nabavka,
|
||||||
Stavke: stavke,
|
Stavke: stavke,
|
||||||
|
Troskovi: troskovi,
|
||||||
|
UkupanTrosak: ukupanTrosak,
|
||||||
DobavljacNaziv: dobavljacNaziv,
|
DobavljacNaziv: dobavljacNaziv,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,12 +319,17 @@ func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Greška pri brisanju nabavke", http.StatusInternalServerError)
|
http.Error(w, "Greška pri brisanju nabavke", http.StatusInternalServerError)
|
||||||
return
|
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)
|
http.Redirect(w, r, "/nabavke?obrisan=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseFormuNabavke čita zaglavlje i stavke iz HTTP forme i vraća model i eventualnu grešku
|
// parseFormuNabavke čita zaglavlje, stavke i zavisne troškove iz HTTP forme
|
||||||
func parseFormuNabavke(r *http.Request) (model.Nabavka, []model.StavkaNabavke, string) {
|
// i vraća model, stavke, troškove i eventualnu grešku
|
||||||
|
func parseFormuNabavke(r *http.Request) (model.Nabavka, []model.StavkaNabavke, []model.NabavkaTrosak, string) {
|
||||||
var nabavka model.Nabavka
|
var nabavka model.Nabavka
|
||||||
|
|
||||||
// opcioni dobavljač
|
// opcioni dobavljač
|
||||||
@@ -244,28 +347,28 @@ func parseFormuNabavke(r *http.Request) (model.Nabavka, []model.StavkaNabavke, s
|
|||||||
cene := r.Form["cena_po_komadu[]"]
|
cene := r.Form["cena_po_komadu[]"]
|
||||||
|
|
||||||
if len(artikalIDovi) == 0 {
|
if len(artikalIDovi) == 0 {
|
||||||
return nabavka, nil, "Nabavka mora imati najmanje jednu stavku."
|
return nabavka, nil, nil, "Nabavka mora imati najmanje jednu stavku."
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(artikalIDovi) != len(kolicine) || len(artikalIDovi) != len(cene) {
|
if len(artikalIDovi) != len(kolicine) || len(artikalIDovi) != len(cene) {
|
||||||
return nabavka, nil, "Greška u podacima forme — broj stavki nije ispravan."
|
return nabavka, nil, nil, "Greška u podacima forme — broj stavki nije ispravan."
|
||||||
}
|
}
|
||||||
|
|
||||||
var stavke []model.StavkaNabavke
|
var stavke []model.StavkaNabavke
|
||||||
for i := range artikalIDovi {
|
for i := range artikalIDovi {
|
||||||
artikalID, err := strconv.ParseInt(strings.TrimSpace(artikalIDovi[i]), 10, 64)
|
artikalID, err := strconv.ParseInt(strings.TrimSpace(artikalIDovi[i]), 10, 64)
|
||||||
if err != nil || artikalID <= 0 {
|
if err != nil || artikalID <= 0 {
|
||||||
return nabavka, nil, "Neispravan artikal u stavci."
|
return nabavka, nil, nil, "Neispravan artikal u stavci."
|
||||||
}
|
}
|
||||||
|
|
||||||
kolicina, err := strconv.Atoi(strings.TrimSpace(kolicine[i]))
|
kolicina, err := strconv.Atoi(strings.TrimSpace(kolicine[i]))
|
||||||
if err != nil || kolicina <= 0 {
|
if err != nil || kolicina <= 0 {
|
||||||
return nabavka, nil, "Količina mora biti pozitivan broj."
|
return nabavka, nil, nil, "Količina mora biti pozitivan broj."
|
||||||
}
|
}
|
||||||
|
|
||||||
cena, err := strconv.ParseFloat(strings.TrimSpace(cene[i]), 64)
|
cena, err := strconv.ParseFloat(strings.TrimSpace(cene[i]), 64)
|
||||||
if err != nil || cena < 0 {
|
if err != nil || cena < 0 {
|
||||||
return nabavka, nil, "Cena mora biti pozitivan broj."
|
return nabavka, nil, nil, "Cena mora biti pozitivan broj."
|
||||||
}
|
}
|
||||||
|
|
||||||
stavke = append(stavke, model.StavkaNabavke{
|
stavke = append(stavke, model.StavkaNabavke{
|
||||||
@@ -275,7 +378,35 @@ func parseFormuNabavke(r *http.Request) (model.Nabavka, []model.StavkaNabavke, s
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nabavka, stavke, ""
|
// zavisni troškovi (opcioni, paralelni nizovi); prazni redovi se preskaču
|
||||||
|
naziviT := r.Form["trosak_naziv[]"]
|
||||||
|
iznosiT := r.Form["trosak_iznos[]"]
|
||||||
|
var troskovi []model.NabavkaTrosak
|
||||||
|
for i := range naziviT {
|
||||||
|
naziv := strings.TrimSpace(naziviT[i])
|
||||||
|
if naziv == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var iznos float64
|
||||||
|
if i < len(iznosiT) {
|
||||||
|
iznos, _ = strconv.ParseFloat(strings.TrimSpace(iznosiT[i]), 64)
|
||||||
|
}
|
||||||
|
if iznos <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
troskovi = append(troskovi, model.NabavkaTrosak{Naziv: naziv, Iznos: iznos})
|
||||||
|
}
|
||||||
|
|
||||||
|
// metod raspodele je bitan samo ako ima troškova; default je po vrednosti
|
||||||
|
if len(troskovi) > 0 {
|
||||||
|
metod := r.FormValue("metod_raspodele")
|
||||||
|
if metod != "kolicina" {
|
||||||
|
metod = "vrednost"
|
||||||
|
}
|
||||||
|
nabavka.MetodRaspodele = metod
|
||||||
|
}
|
||||||
|
|
||||||
|
return nabavka, stavke, troskovi, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderujFormuNabavke renderuje HTML šablon forme za unos nove nabavke
|
// renderujFormuNabavke renderuje HTML šablon forme za unos nove nabavke
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"ntech/internal/db/sqlite"
|
||||||
|
"ntech/internal/middleware"
|
||||||
|
"ntech/internal/model"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PodaciNivelacije su podaci za pregled istorije promene cena
|
||||||
|
type PodaciNivelacije struct {
|
||||||
|
model.PodaciStranice
|
||||||
|
Zapisi []model.Nivelacija
|
||||||
|
Od string
|
||||||
|
Do string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nivelacije renderuje istoriju promena prodajnih cena za izabrani period
|
||||||
|
func (h *Handler) Nivelacije(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, ok := h.zahtevajDozvolu(w, r, "artikal.izmeni"); !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.NivelacijaRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Greška pri učitavanju nivelacija", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||||
|
ps.Stranica = "nivelacije"
|
||||||
|
ps.NaslovStranice = "Nivelacije — promene cena"
|
||||||
|
h.renderujTemplate(w, "nivelacije", PodaciNivelacije{
|
||||||
|
PodaciStranice: ps,
|
||||||
|
Zapisi: zapisi,
|
||||||
|
Od: odStr,
|
||||||
|
Do: doStr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromeniCenuArtikla menja prodajnu cenu artikla i upisuje nivelacioni zapis (izvor "rucno").
|
||||||
|
func (h *Handler) PromeniCenuArtikla(w http.ResponseWriter, r *http.Request) {
|
||||||
|
k, ok := h.zahtevajDozvolu(w, r, "artikal.izmeni")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Neispravan ID artikla", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
novaCena := parsiraIznos(r.FormValue("nova_cena"))
|
||||||
|
razlog := strings.TrimSpace(r.FormValue("razlog"))
|
||||||
|
if novaCena <= 0 {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", "Nova cena mora biti veća od nule.")
|
||||||
|
http.Redirect(w, r, "/magacin", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
korisnikID := &k.ID
|
||||||
|
_, err = h.NivelacijaRepo.PromeniCenu(r.Context(), id, novaCena, razlog, korisnikID)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sqlite.ErrArtikalNePostoji):
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", "Artikal nije pronađen.")
|
||||||
|
case err != nil:
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri promeni cene.")
|
||||||
|
default:
|
||||||
|
middleware.SetFlash(w, r, h.DB, "uspeh", "Prodajna cena je izmenjena.")
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/magacin", 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,165 @@
|
|||||||
|
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")),
|
||||||
|
Uvoz: r.FormValue("uvoz") == "1",
|
||||||
|
}
|
||||||
|
// 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,87 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
// razdvajamo KPR na domaće i uvozne — uvoz se u PPPDV mapira u polja 006/106,
|
||||||
|
// domaća nabavka u 008/108; obračun obaveze ostaje na ukupnom KPR-u (uvozni PDV je odbitni)
|
||||||
|
var kprDomaci, kprUvozni []model.PdvKpr
|
||||||
|
for _, z := range kprZapisi {
|
||||||
|
if z.Uvoz {
|
||||||
|
kprUvozni = append(kprUvozni, z)
|
||||||
|
} else {
|
||||||
|
kprDomaci = append(kprDomaci, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kprDomaceSume := model.SumirajKpr(kprDomaci)
|
||||||
|
kprUvozSume := model.SumirajKpr(kprUvozni)
|
||||||
|
|
||||||
|
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||||
|
ps.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, kprDomaceSume, kprUvozSume),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
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 „Kalkulacija i PDV" (marža + šifarnik PDV stopa)
|
||||||
|
type PodaciPdvStope struct {
|
||||||
|
model.PodaciStranice
|
||||||
|
Stope []model.PdvStopa
|
||||||
|
KalkulacijaMarza string // podrazumevana marža (%) za kalkulaciju
|
||||||
|
}
|
||||||
|
|
||||||
|
// PdvStope renderuje stranicu „Kalkulacija i PDV": podešavanje podrazumevane marže
|
||||||
|
// i š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-kalkulacija-pdv"
|
||||||
|
ps.NaslovStranice = "Kalkulacija i PDV"
|
||||||
|
h.renderujTemplate(w, "pdv_stope", PodaciPdvStope{
|
||||||
|
PodaciStranice: ps,
|
||||||
|
Stope: stope,
|
||||||
|
KalkulacijaMarza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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/kalkulacija-pdv", 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/kalkulacija-pdv", 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/kalkulacija-pdv", 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/kalkulacija-pdv", 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/kalkulacija-pdv", http.StatusSeeOther)
|
||||||
|
}
|
||||||
@@ -30,6 +30,11 @@ type PodaciPodesavanja struct {
|
|||||||
PIB string
|
PIB string
|
||||||
LogoTip string
|
LogoTip string
|
||||||
LogoPutanja 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
|
Sacuvano bool
|
||||||
Verzija string
|
Verzija string
|
||||||
LogoGreska string
|
LogoGreska string
|
||||||
@@ -37,6 +42,7 @@ type PodaciPodesavanja struct {
|
|||||||
Backupi []BackupInfo
|
Backupi []BackupInfo
|
||||||
BackupIntervalSati string
|
BackupIntervalSati string
|
||||||
BackupBrojKopija string
|
BackupBrojKopija string
|
||||||
|
KalkulacijaMarza string
|
||||||
LoginPozadina string
|
LoginPozadina string
|
||||||
LoginPozadinaOpacity string
|
LoginPozadinaOpacity string
|
||||||
LoginPozadinaBlurPozadine string
|
LoginPozadinaBlurPozadine string
|
||||||
@@ -234,6 +240,11 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
|||||||
"telefon": r.FormValue("telefon"),
|
"telefon": r.FormValue("telefon"),
|
||||||
"pib": r.FormValue("pib"),
|
"pib": r.FormValue("pib"),
|
||||||
"logo_tip": r.FormValue("logo_tip"),
|
"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 {
|
for kljuc, vrednost := range polja {
|
||||||
@@ -278,6 +289,20 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// podrazumevana marža za kalkulaciju (procenat, 0–1000)
|
||||||
|
if v := strings.TrimSpace(r.FormValue("kalkulacija_marza")); v != "" {
|
||||||
|
marza, err := strconv.ParseFloat(strings.Replace(v, ",", ".", 1), 64)
|
||||||
|
if err != nil || marza < 0 || marza > 1000 {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", "Marža mora biti broj između 0 i 1000.")
|
||||||
|
http.Redirect(w, r, sledeci, http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "kalkulacija_marza", strconv.FormatFloat(marza, 'f', -1, 64)); err != nil {
|
||||||
|
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, sledeci+"?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, sledeci+"?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,6 +626,10 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
|
|||||||
PIB: podesavanja["pib"],
|
PIB: podesavanja["pib"],
|
||||||
LogoTip: podesavanja["logo_tip"],
|
LogoTip: podesavanja["logo_tip"],
|
||||||
LogoPutanja: podesavanja["logo_putanja"],
|
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",
|
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||||
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
||||||
Verzija: h.Verzija,
|
Verzija: h.Verzija,
|
||||||
@@ -613,6 +642,7 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
|
|||||||
LoginPozadinaZatamnjenjeKartice: vrednostIliDefault(podesavanja, "login_pozadina_zatamnjenje_kartice", "0"),
|
LoginPozadinaZatamnjenjeKartice: vrednostIliDefault(podesavanja, "login_pozadina_zatamnjenje_kartice", "0"),
|
||||||
BackupIntervalSati: vrednostIliDefault(podesavanja, "backup_interval_sati", "24"),
|
BackupIntervalSati: vrednostIliDefault(podesavanja, "backup_interval_sati", "24"),
|
||||||
BackupBrojKopija: vrednostIliDefault(podesavanja, "backup_broj_kopija", "7"),
|
BackupBrojKopija: vrednostIliDefault(podesavanja, "backup_broj_kopija", "7"),
|
||||||
|
KalkulacijaMarza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,4 +687,3 @@ func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
|
|||||||
podaci.Stranica = "podesavanja-sistem"
|
podaci.Stranica = "podesavanja-sistem"
|
||||||
h.renderujTemplate(w, "podesavanja_sistem", podaci)
|
h.renderujTemplate(w, "podesavanja_sistem", podaci)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,22 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
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)
|
http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError)
|
||||||
return
|
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)
|
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)
|
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||||
return
|
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.")
|
middleware.SetFlash(w, r, h.DB, "uspeh", "Prodajni nalog je storniran.")
|
||||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ var sveAkcije = []string{
|
|||||||
"artikal.premesti",
|
"artikal.premesti",
|
||||||
"kategorija.pregled",
|
"kategorija.pregled",
|
||||||
"kategorija.dodaj",
|
"kategorija.dodaj",
|
||||||
|
"kategorija.izmeni",
|
||||||
"kategorija.obrisi",
|
"kategorija.obrisi",
|
||||||
"nabavka.pregled",
|
"nabavka.pregled",
|
||||||
"nabavka.dodaj",
|
"nabavka.dodaj",
|
||||||
@@ -38,6 +39,9 @@ var sveAkcije = []string{
|
|||||||
"backup.pokreni",
|
"backup.pokreni",
|
||||||
"tema.lokalno",
|
"tema.lokalno",
|
||||||
"dashboard.prihod",
|
"dashboard.prihod",
|
||||||
|
"pdv.pregled",
|
||||||
|
"pdv.dodaj",
|
||||||
|
"pdv.obrisi",
|
||||||
}
|
}
|
||||||
|
|
||||||
// SveAkcije vraća listu svih poznatih akcija — koristi se pri inicijalizaciji baze i resetu
|
// SveAkcije vraća listu svih poznatih akcija — koristi se pri inicijalizaciji baze i resetu
|
||||||
@@ -59,7 +63,7 @@ func ImaDozvolu(uloga, akcija string) bool {
|
|||||||
"artikal.obrisi", "artikal.premesti":
|
"artikal.obrisi", "artikal.premesti":
|
||||||
return true
|
return true
|
||||||
// kategorija
|
// kategorija
|
||||||
case "kategorija.pregled", "kategorija.dodaj", "kategorija.obrisi":
|
case "kategorija.pregled", "kategorija.dodaj", "kategorija.izmeni", "kategorija.obrisi":
|
||||||
return true
|
return true
|
||||||
// nabavka
|
// nabavka
|
||||||
case "nabavka.pregled", "nabavka.dodaj", "nabavka.obrisi":
|
case "nabavka.pregled", "nabavka.dodaj", "nabavka.obrisi":
|
||||||
@@ -95,6 +99,9 @@ func ImaDozvolu(uloga, akcija string) bool {
|
|||||||
// dashboard — prihod samo admin+
|
// dashboard — prihod samo admin+
|
||||||
case "dashboard.prihod":
|
case "dashboard.prihod":
|
||||||
return true
|
return true
|
||||||
|
// PDV evidencija (KIR/KPR) — administrativno, radnik nema
|
||||||
|
case "pdv.pregled", "pdv.dodaj", "pdv.obrisi":
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return false
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ type Artikal struct {
|
|||||||
NabavnaCena float64
|
NabavnaCena float64
|
||||||
ProdajnaCena float64
|
ProdajnaCena float64
|
||||||
PdvStopa float64
|
PdvStopa float64
|
||||||
|
Marza *float64 // podrazumevana marža (%) za kalkulaciju; NULL = nije postavljeno
|
||||||
Napomena string
|
Napomena string
|
||||||
DatumUnosa time.Time
|
DatumUnosa time.Time
|
||||||
}
|
}
|
||||||
@@ -33,11 +34,13 @@ type Kategorija struct {
|
|||||||
ID int64
|
ID int64
|
||||||
Naziv string
|
Naziv string
|
||||||
Opis string
|
Opis string
|
||||||
|
Marza *float64 // podrazumevana marža (%) za artikle ove kategorije; NULL = nije postavljeno
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArtikalSaKategorijom je artikal sa nazivom kategorije — za prikaz u tabeli
|
// ArtikalSaKategorijom je artikal sa nazivom kategorije — za prikaz u tabeli
|
||||||
type ArtikalSaKategorijom struct {
|
type ArtikalSaKategorijom struct {
|
||||||
Artikal
|
Artikal
|
||||||
KategorijaNaziv string
|
KategorijaNaziv string
|
||||||
|
KategorijaMarza *float64 // marža kategorije; za fallback predloga marže pri nabavci
|
||||||
KriticnaZaliha bool
|
KriticnaZaliha bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ type Dobavljac struct {
|
|||||||
KontaktOsoba string
|
KontaktOsoba string
|
||||||
Telefon string
|
Telefon string
|
||||||
Email string
|
Email string
|
||||||
|
PIB string
|
||||||
|
Mesto string
|
||||||
Napomena string
|
Napomena string
|
||||||
DatumUnosa time.Time
|
DatumUnosa time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Klijent struct {
|
|||||||
PIB string
|
PIB string
|
||||||
Telefon string
|
Telefon string
|
||||||
Email string
|
Email string
|
||||||
|
Mesto string
|
||||||
Napomena string
|
Napomena string
|
||||||
DatumUnosa time.Time
|
DatumUnosa time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Nabavka predstavlja zaglavlje jedne nabavke
|
// Nabavka predstavlja zaglavlje jedne nabavke
|
||||||
type Nabavka struct {
|
type Nabavka struct {
|
||||||
@@ -8,9 +11,18 @@ type Nabavka struct {
|
|||||||
DobavljacID *int64
|
DobavljacID *int64
|
||||||
Napomena string
|
Napomena string
|
||||||
Ukupno float64
|
Ukupno float64
|
||||||
|
MetodRaspodele string // "vrednost" ili "kolicina"; prazno = nema zavisnih troškova
|
||||||
Datum time.Time
|
Datum time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NabavkaTrosak je jedna stavka zavisnih troškova nabavke (npr. prevoz, carina)
|
||||||
|
type NabavkaTrosak struct {
|
||||||
|
ID int64
|
||||||
|
NabavkaID int64
|
||||||
|
Naziv string
|
||||||
|
Iznos float64
|
||||||
|
}
|
||||||
|
|
||||||
// StavkaNabavke predstavlja jednu liniju (artikal) unutar nabavke
|
// StavkaNabavke predstavlja jednu liniju (artikal) unutar nabavke
|
||||||
type StavkaNabavke struct {
|
type StavkaNabavke struct {
|
||||||
ID int64
|
ID int64
|
||||||
@@ -21,6 +33,44 @@ type StavkaNabavke struct {
|
|||||||
Ukupno float64
|
Ukupno float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RasporediTroskove raspodeljuje ukupan zavisni trošak na stavke nabavke i vraća
|
||||||
|
// kalkulativnu nabavnu cenu po komadu za svaku stavku (isti redosled kao ulazne stavke).
|
||||||
|
// metod "kolicina" deli trošak po broju komada; svaka druga vrednost (uklj. "vrednost")
|
||||||
|
// deli po nabavnoj vrednosti stavke (količina × cena).
|
||||||
|
// Ako nema troška ili je osnovica nula, vraća nepromenjenu cenu po komadu.
|
||||||
|
func RasporediTroskove(stavke []StavkaNabavke, ukupanTrosak float64, metod string) []float64 {
|
||||||
|
kalk := make([]float64, len(stavke))
|
||||||
|
|
||||||
|
// osnovica raspodele po izabranom metodu
|
||||||
|
var osnovica float64
|
||||||
|
for _, s := range stavke {
|
||||||
|
if metod == "kolicina" {
|
||||||
|
osnovica += float64(s.Kolicina)
|
||||||
|
} else {
|
||||||
|
osnovica += float64(s.Kolicina) * s.CenaPoKomadu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, s := range stavke {
|
||||||
|
kalk[i] = s.CenaPoKomadu
|
||||||
|
// bez troška, bez osnovice ili bez količine — nema šta da se raspodeli
|
||||||
|
if ukupanTrosak <= 0 || osnovica <= 0 || s.Kolicina == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var udeo float64
|
||||||
|
if metod == "kolicina" {
|
||||||
|
udeo = float64(s.Kolicina) / osnovica
|
||||||
|
} else {
|
||||||
|
udeo = (float64(s.Kolicina) * s.CenaPoKomadu) / osnovica
|
||||||
|
}
|
||||||
|
trosakPoKomadu := (ukupanTrosak * udeo) / float64(s.Kolicina)
|
||||||
|
// zaokruženo na 2 decimale — kalkulativna nabavna je cena koja se čuva
|
||||||
|
kalk[i] = math.Round((s.CenaPoKomadu+trosakPoKomadu)*100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return kalk
|
||||||
|
}
|
||||||
|
|
||||||
// NabavkaSaDetaljem je nabavka sa nazivom dobavljača — za prikaz u listi
|
// NabavkaSaDetaljem je nabavka sa nazivom dobavljača — za prikaz u listi
|
||||||
type NabavkaSaDetaljem struct {
|
type NabavkaSaDetaljem struct {
|
||||||
Nabavka
|
Nabavka
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// proverava raspodelu zavisnih troškova na stavke nabavke po oba metoda
|
||||||
|
func TestRasporediTroskove(t *testing.T) {
|
||||||
|
// dve stavke: A = 10×100 (vrednost 1000), B = 10×200 (vrednost 2000)
|
||||||
|
stavke := []StavkaNabavke{
|
||||||
|
{Kolicina: 10, CenaPoKomadu: 100},
|
||||||
|
{Kolicina: 10, CenaPoKomadu: 200},
|
||||||
|
}
|
||||||
|
|
||||||
|
slucajevi := []struct {
|
||||||
|
naziv string
|
||||||
|
stavke []StavkaNabavke
|
||||||
|
trosak float64
|
||||||
|
metod string
|
||||||
|
ocekuje []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
naziv: "bez troška — cena nepromenjena",
|
||||||
|
stavke: stavke,
|
||||||
|
trosak: 0,
|
||||||
|
metod: "vrednost",
|
||||||
|
ocekuje: []float64{100, 200},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// trošak 300 po vrednosti: A nosi 1/3 (100 → 10/kom), B nosi 2/3 (200 → 20/kom)
|
||||||
|
naziv: "po vrednosti",
|
||||||
|
stavke: stavke,
|
||||||
|
trosak: 300,
|
||||||
|
metod: "vrednost",
|
||||||
|
ocekuje: []float64{110, 220},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// trošak 300 po količini: svaka stavka 1/2 (150 → 15/kom)
|
||||||
|
naziv: "po količini",
|
||||||
|
stavke: stavke,
|
||||||
|
trosak: 300,
|
||||||
|
metod: "kolicina",
|
||||||
|
ocekuje: []float64{115, 215},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// osnovica po vrednosti je 0 (sve cene 0) — nema raspodele
|
||||||
|
naziv: "nulta osnovica vrednosti",
|
||||||
|
stavke: []StavkaNabavke{{Kolicina: 5, CenaPoKomadu: 0}},
|
||||||
|
trosak: 100,
|
||||||
|
metod: "vrednost",
|
||||||
|
ocekuje: []float64{0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// stavka bez količine se preskače (bez deljenja nulom)
|
||||||
|
naziv: "količina nula",
|
||||||
|
stavke: []StavkaNabavke{{Kolicina: 0, CenaPoKomadu: 50}},
|
||||||
|
trosak: 100,
|
||||||
|
metod: "kolicina",
|
||||||
|
ocekuje: []float64{50},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range slucajevi {
|
||||||
|
t.Run(s.naziv, func(t *testing.T) {
|
||||||
|
dobijeno := RasporediTroskove(s.stavke, s.trosak, s.metod)
|
||||||
|
if len(dobijeno) != len(s.ocekuje) {
|
||||||
|
t.Fatalf("dužina = %d, očekivano %d", len(dobijeno), len(s.ocekuje))
|
||||||
|
}
|
||||||
|
for i := range s.ocekuje {
|
||||||
|
if dobijeno[i] != s.ocekuje[i] {
|
||||||
|
t.Errorf("stavka %d: dobijeno %.2f, očekivano %.2f", i, dobijeno[i], s.ocekuje[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Nivelacija je zapis o promeni prodajne cene artikla (revizioni trag).
|
||||||
|
// Razlika i procenat se izvode iz stare i nove cene (ne čuvaju se u bazi).
|
||||||
|
type Nivelacija struct {
|
||||||
|
ID int64
|
||||||
|
ArtikalID int64
|
||||||
|
ArtikalNaziv string // iz JOIN-a, radi prikaza; nije kolona u nivelacije
|
||||||
|
StaraCena float64
|
||||||
|
NovaCena float64
|
||||||
|
Razlog string
|
||||||
|
Izvor string // "rucno" | "izmena" | "kalkulacija"
|
||||||
|
KorisnikID *int64
|
||||||
|
KorisnikIme string // iz JOIN-a, radi prikaza
|
||||||
|
Datum time.Time
|
||||||
|
DatumUnosa time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Razlika vraća apsolutnu promenu cene (nova − stara); negativna znači pojeftinjenje.
|
||||||
|
func (n Nivelacija) Razlika() float64 {
|
||||||
|
return n.NovaCena - n.StaraCena
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procenat vraća procentualnu promenu u odnosu na staru cenu (0 ako je stara cena 0).
|
||||||
|
func (n Nivelacija) Procenat() float64 {
|
||||||
|
if n.StaraCena == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (n.NovaCena - n.StaraCena) / n.StaraCena * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poskupljenje vraća true kada je nova cena veća od stare.
|
||||||
|
func (n Nivelacija) Poskupljenje() bool {
|
||||||
|
return n.NovaCena > n.StaraCena
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNivelacija(t *testing.T) {
|
||||||
|
// poskupljenje: 100 → 120 = +20, +20%
|
||||||
|
n := Nivelacija{StaraCena: 100, NovaCena: 120}
|
||||||
|
if !blizu(n.Razlika(), 20) || !blizu(n.Procenat(), 20) || !n.Poskupljenje() {
|
||||||
|
t.Errorf("razlika=%v procenat=%v poskupljenje=%v, očekivano 20/20/true", n.Razlika(), n.Procenat(), n.Poskupljenje())
|
||||||
|
}
|
||||||
|
|
||||||
|
// pojeftinjenje: 200 → 150 = -50, -25%
|
||||||
|
n2 := Nivelacija{StaraCena: 200, NovaCena: 150}
|
||||||
|
if !blizu(n2.Razlika(), -50) || !blizu(n2.Procenat(), -25) || n2.Poskupljenje() {
|
||||||
|
t.Errorf("razlika=%v procenat=%v poskupljenje=%v, očekivano -50/-25/false", n2.Razlika(), n2.Procenat(), n2.Poskupljenje())
|
||||||
|
}
|
||||||
|
|
||||||
|
// stara cena 0 → procenat 0 (bez deljenja nulom)
|
||||||
|
n3 := Nivelacija{StaraCena: 0, NovaCena: 80}
|
||||||
|
if !blizu(n3.Procenat(), 0) {
|
||||||
|
t.Errorf("procenat=%v, očekivano 0 kada je stara cena 0", n3.Procenat())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
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)
|
||||||
|
Uvoz bool // true = uvoz (JCI) → PPPDV 006/106; false = domaća nabavka → 008/108
|
||||||
|
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).
|
||||||
|
// KPR je razdvojen: uvoz (JCI) ide u polja 006/106 (prethodni porez pri uvozu),
|
||||||
|
// domaća nabavka u 008/108 (ostali prethodni porez).
|
||||||
|
// ⚠ PDV nadoknada poljoprivredniku (007/107) se i dalje ne prati zasebno (ostaje 0).
|
||||||
|
func MapirajPPPDV(kir PdvKirSume, kprDomace, kprUvoz PdvKprSume) PPPDV {
|
||||||
|
uDinare := func(v float64) int64 { return int64(math.Round(v)) }
|
||||||
|
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),
|
||||||
|
Polje006: uDinare(kprUvoz.OsnovicaOpsta + kprUvoz.OsnovicaPosebna),
|
||||||
|
Polje106: uDinare(kprUvoz.PdvOpsta + kprUvoz.PdvPosebna),
|
||||||
|
Polje008: uDinare(kprDomace.OsnovicaOpsta + kprDomace.OsnovicaPosebna),
|
||||||
|
Polje108: uDinare(kprDomace.PdvOpsta + kprDomace.PdvPosebna),
|
||||||
|
}
|
||||||
|
// zbirovi se računaju iz zaokruženih polja da kolone na obrascu uvek zbiraju
|
||||||
|
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,161 @@
|
|||||||
|
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, PdvKprSume{})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
// u ovom scenariju nema uvoza (prazan kprUvoz) ni poljoprivrednika → 006/106/007/107 = 0
|
||||||
|
if p.Polje006 != 0 || p.Polje106 != 0 || p.Polje007 != 0 || p.Polje107 != 0 {
|
||||||
|
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}, PdvKprSume{})
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
// scenario uvoza: uvozni KPR ide u 006/106, domaći u 008/108
|
||||||
|
p3 := MapirajPPPDV(
|
||||||
|
PdvKirSume{},
|
||||||
|
PdvKprSume{OsnovicaOpsta: 1000, PdvOpsta: 200}, // domaće → 008/108
|
||||||
|
PdvKprSume{OsnovicaOpsta: 500, PdvOpsta: 100, OsnovicaPosebna: 50, PdvPosebna: 5}, // uvoz → 006/106
|
||||||
|
)
|
||||||
|
if p3.Polje006 != 550 || p3.Polje106 != 105 {
|
||||||
|
t.Errorf("uvoz 006/106 = %d/%d, očekivano 550/105", p3.Polje006, p3.Polje106)
|
||||||
|
}
|
||||||
|
if p3.Polje008 != 1000 || p3.Polje108 != 200 {
|
||||||
|
t.Errorf("domaće 008/108 = %d/%d, očekivano 1000/200", p3.Polje008, p3.Polje108)
|
||||||
|
}
|
||||||
|
// 009 = 006+007+008 = 550+0+1000 = 1550; 109 = 106+107+108 = 105+0+200 = 305
|
||||||
|
if p3.Polje009 != 1550 || p3.Polje109 != 305 {
|
||||||
|
t.Errorf("zbir prethodnog 009/109 = %d/%d, očekivano 1550/305", p3.Polje009, p3.Polje109)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ type PodaciStranice struct {
|
|||||||
CsrfToken string // CSRF zaštitni token za forme
|
CsrfToken string // CSRF zaštitni token za forme
|
||||||
AssetV string // verzija statičkih fajlova (cache-busting za CSS/JS)
|
AssetV string // verzija statičkih fajlova (cache-busting za CSS/JS)
|
||||||
Dozvole map[string]bool // mapa akcija → dozvoljeno/nije
|
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
|
Flash *FlashPoruka // jednokratna poruka nakon redirecta
|
||||||
// app pozadina — popunjava se iz podešavanja za sve stranice
|
// app pozadina — popunjava se iz podešavanja za sve stranice
|
||||||
AppPozadina string
|
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;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Nivelacije (Faza A kalkulacije/nivelacije): trag svake promene prodajne cene artikla.
|
||||||
|
-- Razlika (nova − stara) se izvodi u programu, ne čuva se. Izvor razlikuje način promene:
|
||||||
|
-- 'rucno' (posebna akcija sa razlogom), 'izmena' (auto pri izmeni artikla), 'kalkulacija' (kasnije).
|
||||||
|
CREATE TABLE IF NOT EXISTS nivelacije (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
artikal_id INTEGER NOT NULL REFERENCES artikli(id) ON DELETE CASCADE,
|
||||||
|
stara_cena REAL NOT NULL,
|
||||||
|
nova_cena REAL NOT NULL,
|
||||||
|
razlog TEXT,
|
||||||
|
izvor TEXT NOT NULL DEFAULT 'rucno',
|
||||||
|
korisnik_id INTEGER REFERENCES korisnici(id) ON DELETE SET NULL,
|
||||||
|
datum DATE NOT NULL, -- poslovni datum promene
|
||||||
|
datum_unosa DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nivelacije_artikal ON nivelacije(artikal_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nivelacije_datum ON nivelacije(datum);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Dodaje podrazumevanu maržu (%) na artikle i kategorije za kalkulaciju
|
||||||
|
-- prodajne cene pri nabavci. Kolone su NULL kada marža nije postavljena —
|
||||||
|
-- tako se „nije postavljeno" razlikuje od svesno unete marže 0%.
|
||||||
|
-- Redosled izvođenja marže: artikal.marza → kategorija.marza → globalna kalkulacija_marza.
|
||||||
|
ALTER TABLE artikli ADD COLUMN marza REAL;
|
||||||
|
ALTER TABLE kategorije ADD COLUMN marza REAL;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Zavisni troškovi nabavke (carina, prevoz, špedicija…) i metod njihove raspodele.
|
||||||
|
-- Troškovi se raspodeljuju na stavke i ulaze u kalkulativnu nabavnu cenu, iz koje
|
||||||
|
-- se računa prodajna. Slobodne stavke (naziv + iznos), više redova po nabavci.
|
||||||
|
|
||||||
|
-- metod raspodele po nabavci: 'vrednost' (po nabavnoj vrednosti stavke) ili 'kolicina'.
|
||||||
|
-- NULL/prazno = nema zavisnih troškova (kalkulacija kao u Fazi B).
|
||||||
|
ALTER TABLE nabavke ADD COLUMN metod_raspodele TEXT;
|
||||||
|
|
||||||
|
-- pojedinačne stavke zavisnih troškova; brišu se zajedno sa nabavkom (CASCADE)
|
||||||
|
CREATE TABLE IF NOT EXISTS nabavka_troskovi (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nabavka_id INTEGER NOT NULL REFERENCES nabavke(id) ON DELETE CASCADE,
|
||||||
|
naziv TEXT NOT NULL,
|
||||||
|
iznos REAL NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nabavka_troskovi_nabavka ON nabavka_troskovi(nabavka_id);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Zastavica „uvoz" na zapisima knjige primljenih računa (KPR).
|
||||||
|
-- Uvoz se unosi RUČNO sa JCI (osnovica i PDV iz carinskog dokumenta drže postojeće
|
||||||
|
-- kolone osnovica_opsta / pdv_opsta). Zastavica služi da se uvozni PDV u PPPDV obrascu
|
||||||
|
-- rutira u polja 006/106 (prethodni porez pri uvozu) umesto 008/108 (ostali prethodni).
|
||||||
|
-- 0 = domaća nabavka (podrazumevano), 1 = uvoz.
|
||||||
|
ALTER TABLE pdv_kpr ADD COLUMN uvoz INTEGER NOT NULL DEFAULT 0;
|
||||||
@@ -327,7 +327,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.kartica:hover {
|
.kartica:hover {
|
||||||
transform: translateY(-4px);
|
transform: scale(1.02);
|
||||||
box-shadow: var(--senka);
|
box-shadow: var(--senka);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+159
-2
@@ -186,27 +186,118 @@ document.addEventListener('alpine:init', () => {
|
|||||||
|
|
||||||
// forma za nabavku
|
// forma za nabavku
|
||||||
Alpine.data('nabavkaForma', () => ({
|
Alpine.data('nabavkaForma', () => ({
|
||||||
stavke: [{artikal_id: '', kolicina: 1, cena: 0}],
|
stavke: [{artikal_id: '', kolicina: 1, cena: 0, marza: 0, prodajna: 0}],
|
||||||
artikliOpcije: [],
|
artikliOpcije: [],
|
||||||
|
marzaDefault: 0,
|
||||||
|
troskovi: [], // zavisni troškovi {naziv, iznos}
|
||||||
|
metodRaspodele: 'vrednost', // 'vrednost' ili 'kolicina'
|
||||||
|
pdvObveznik: true, // da li firma obračunava PDV (utiče na prodajnu cenu)
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
modal: false,
|
modal: false,
|
||||||
modalUcitavanje: false,
|
modalUcitavanje: false,
|
||||||
modalGreska: '',
|
modalGreska: '',
|
||||||
modalNaziv: '',
|
modalNaziv: '',
|
||||||
modalKategorijaID: '',
|
modalKategorijaID: '',
|
||||||
|
modalOpis: '',
|
||||||
|
modalKolicina: '',
|
||||||
|
modalKolicinaMin: '',
|
||||||
modalCena: '',
|
modalCena: '',
|
||||||
|
modalLokacija: '',
|
||||||
|
modalNapomena: '',
|
||||||
|
modalDob: false,
|
||||||
|
modalDobUcitavanje: false,
|
||||||
|
modalDobGreska: '',
|
||||||
|
modalDobNaziv: '',
|
||||||
|
modalDobKontakt: '',
|
||||||
|
modalDobTelefon: '',
|
||||||
|
modalDobEmail: '',
|
||||||
|
modalDobPib: '',
|
||||||
|
modalDobMesto: '',
|
||||||
|
modalDobNapomena: '',
|
||||||
init() {
|
init() {
|
||||||
this.artikliOpcije = window._ntechArtikli || []
|
this.artikliOpcije = window._ntechArtikli || []
|
||||||
|
this.marzaDefault = parseFloat(window._ntechMarza) || 0
|
||||||
|
this.pdvObveznik = window._ntechPdvObveznik === true
|
||||||
|
this.stavke.forEach(s => { s.marza = this.marzaDefault })
|
||||||
this.isMobile = window.matchMedia('(max-width: 768px)').matches
|
this.isMobile = window.matchMedia('(max-width: 768px)').matches
|
||||||
window.matchMedia('(max-width: 768px)').addEventListener('change', e => {
|
window.matchMedia('(max-width: 768px)').addEventListener('change', e => {
|
||||||
this.isMobile = e.matches
|
this.isMobile = e.matches
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dodajStavku() {
|
dodajStavku() {
|
||||||
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0})
|
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, marza: this.marzaDefault, prodajna: 0})
|
||||||
|
this.preracunajSve()
|
||||||
|
},
|
||||||
|
// PDV stopa izabranog artikla (iz JSON liste) — za obračun prodajne cene.
|
||||||
|
// Ako firma nije PDV obveznik, PDV se ne dodaje na prodajnu cenu (stopa = 0).
|
||||||
|
pdvStopa(artikalId) {
|
||||||
|
if (!this.pdvObveznik) return 0
|
||||||
|
const a = this.artikliOpcije.find(x => String(x.id) === String(artikalId))
|
||||||
|
return a ? (parseFloat(a.pdv_stopa) || 0) : 0
|
||||||
|
},
|
||||||
|
// pri izboru artikla predloži maržu: artikal → kategorija → globalna, pa izračunaj prodajnu
|
||||||
|
izaberiArtikal(s) {
|
||||||
|
const a = this.artikliOpcije.find(x => String(x.id) === String(s.artikal_id))
|
||||||
|
if (a) {
|
||||||
|
if (a.marza != null) s.marza = a.marza
|
||||||
|
else if (a.kategorija_marza != null) s.marza = a.kategorija_marza
|
||||||
|
else s.marza = this.marzaDefault
|
||||||
|
}
|
||||||
|
this.izracunajProdajnu(s)
|
||||||
|
},
|
||||||
|
// ukupan zavisni trošak nabavke
|
||||||
|
ukupanTrosak() {
|
||||||
|
return this.troskovi.reduce((z, t) => z + (parseFloat(t.iznos) || 0), 0)
|
||||||
|
},
|
||||||
|
// osnovica raspodele po izabranom metodu (zbir po svim stavkama)
|
||||||
|
osnovicaRaspodele() {
|
||||||
|
return this.stavke.reduce((z, s) => {
|
||||||
|
const kol = parseFloat(s.kolicina) || 0
|
||||||
|
return z + (this.metodRaspodele === 'kolicina' ? kol : kol * (parseFloat(s.cena) || 0))
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
|
// kalkulativna nabavna cena po komadu (fakturna + raspodeljeni trošak) — isto kao server
|
||||||
|
kalkNabavna(s) {
|
||||||
|
const cena = parseFloat(s.cena) || 0
|
||||||
|
const kol = parseFloat(s.kolicina) || 0
|
||||||
|
const trosak = this.ukupanTrosak()
|
||||||
|
const osn = this.osnovicaRaspodele()
|
||||||
|
if (trosak <= 0 || osn <= 0 || kol <= 0) return cena
|
||||||
|
const baza = this.metodRaspodele === 'kolicina' ? kol : kol * cena
|
||||||
|
const trosakPoKomadu = (trosak * (baza / osn)) / kol
|
||||||
|
return Math.round((cena + trosakPoKomadu) * 100) / 100
|
||||||
|
},
|
||||||
|
// prodajna (sa PDV) = kalkulativna nabavna × (1 + marža/100) × (1 + pdvStopa/100)
|
||||||
|
izracunajProdajnu(s) {
|
||||||
|
const nabavna = this.kalkNabavna(s)
|
||||||
|
const marza = parseFloat(s.marza) || 0
|
||||||
|
const pdv = this.pdvStopa(s.artikal_id)
|
||||||
|
s.prodajna = Math.round(nabavna * (1 + marza / 100) * (1 + pdv / 100) * 100) / 100
|
||||||
|
},
|
||||||
|
// obrnuti smer: iz ručno unete prodajne cene izvedi maržu (%)
|
||||||
|
// marža = (prodajna / (nabavna × (1 + pdv/100)) − 1) × 100
|
||||||
|
izracunajMarzu(s) {
|
||||||
|
const nabavna = this.kalkNabavna(s)
|
||||||
|
const pdv = this.pdvStopa(s.artikal_id)
|
||||||
|
const osnovica = nabavna * (1 + pdv / 100)
|
||||||
|
if (osnovica <= 0) return // bez nabavne cene marža se ne može izvesti
|
||||||
|
const prodajna = parseFloat(s.prodajna) || 0
|
||||||
|
s.marza = Math.round(((prodajna / osnovica) - 1) * 100 * 100) / 100
|
||||||
|
},
|
||||||
|
// raspodela zavisi od svih stavki — promena troška/metoda/količine/cene preračunava sve
|
||||||
|
preracunajSve() {
|
||||||
|
this.stavke.forEach(s => this.izracunajProdajnu(s))
|
||||||
|
},
|
||||||
|
dodajTrosak() {
|
||||||
|
this.troskovi.push({naziv: '', iznos: 0})
|
||||||
|
},
|
||||||
|
ukloniTrosak(i) {
|
||||||
|
this.troskovi.splice(i, 1)
|
||||||
|
this.preracunajSve()
|
||||||
},
|
},
|
||||||
ukloniStavku(i) {
|
ukloniStavku(i) {
|
||||||
if (this.stavke.length > 1) this.stavke.splice(i, 1)
|
if (this.stavke.length > 1) this.stavke.splice(i, 1)
|
||||||
|
this.preracunajSve()
|
||||||
},
|
},
|
||||||
ukupnoStavke(s) {
|
ukupnoStavke(s) {
|
||||||
return (parseFloat(s.kolicina) * parseFloat(s.cena) || 0).toFixed(2)
|
return (parseFloat(s.kolicina) * parseFloat(s.cena) || 0).toFixed(2)
|
||||||
@@ -219,7 +310,12 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.modalGreska = ''
|
this.modalGreska = ''
|
||||||
this.modalNaziv = ''
|
this.modalNaziv = ''
|
||||||
this.modalKategorijaID = ''
|
this.modalKategorijaID = ''
|
||||||
|
this.modalOpis = ''
|
||||||
|
this.modalKolicina = ''
|
||||||
|
this.modalKolicinaMin = ''
|
||||||
this.modalCena = ''
|
this.modalCena = ''
|
||||||
|
this.modalLokacija = ''
|
||||||
|
this.modalNapomena = ''
|
||||||
this.$nextTick(() => this.$refs.modalNazivInput && this.$refs.modalNazivInput.focus())
|
this.$nextTick(() => this.$refs.modalNazivInput && this.$refs.modalNazivInput.focus())
|
||||||
},
|
},
|
||||||
zatvoriModal() {
|
zatvoriModal() {
|
||||||
@@ -235,7 +331,12 @@ document.addEventListener('alpine:init', () => {
|
|||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
params.append('naziv', this.modalNaziv.trim())
|
params.append('naziv', this.modalNaziv.trim())
|
||||||
if (this.modalKategorijaID) params.append('kategorija_id', this.modalKategorijaID)
|
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)
|
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 || '')
|
params.append('_csrf', document.querySelector('meta[name=csrf-token]')?.content || '')
|
||||||
try {
|
try {
|
||||||
const odgovor = await fetch('/magacin/novi', {
|
const odgovor = await fetch('/magacin/novi', {
|
||||||
@@ -255,6 +356,62 @@ document.addEventListener('alpine:init', () => {
|
|||||||
} finally {
|
} finally {
|
||||||
this.modalUcitavanje = false
|
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,54 @@
|
|||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{/* Knjigovodstvo — grupa: KIR/KPR/Obračun (modul „pdv") + Nivelacije (dozvola izmene cena) */}}
|
||||||
|
{{$pdvVid := and (index .Moduli "pdv") (index .Dozvole "pdv.pregled")}}
|
||||||
|
{{$knjVid := or $pdvVid (index .Dozvole "artikal.izmeni")}}
|
||||||
|
{{if $knjVid}}
|
||||||
|
{{$knjAktivan := or (eq .Stranica "nivelacije") (eq .Stranica "pdv-kir") (eq .Stranica "pdv-kpr") (eq .Stranica "pdv-obracun")}}
|
||||||
|
<div>
|
||||||
|
<button type="button" data-podmeni-dugme
|
||||||
|
class="nav-stavka {{if $knjAktivan}}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"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
||||||
|
<span>Knjigovodstvo</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 $knjAktivan}}rotate(180deg){{else}}rotate(0deg){{end}}">
|
||||||
|
<polyline points="6 9 12 15 18 9"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-tooltip">Knjigovodstvo</span>
|
||||||
|
</button>
|
||||||
|
<div class="nav-podmeni {{if $knjAktivan}}otvoren{{end}}">
|
||||||
|
{{if index .Dozvole "artikal.izmeni"}}
|
||||||
|
<a href="/nivelacije" class="nav-stavka nav-podstavka {{if eq .Stranica "nivelacije"}}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="12" y1="20" x2="12" y2="10"/><line x1="18" y1="20" x2="18" y2="4"/><line x1="6" y1="20" x2="6" y2="16"/></svg>
|
||||||
|
<span>Nivelacije</span>
|
||||||
|
<span class="nav-tooltip">Nivelacije — promene cena</span>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{if $pdvVid}}
|
||||||
|
<a href="/pdv/kir" class="nav-stavka nav-podstavka {{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 nav-podstavka {{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 nav-podstavka {{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>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<div class="nav-separator"></div>
|
<div class="nav-separator"></div>
|
||||||
<div class="nav-oznaka">Nalog</div>
|
<div class="nav-oznaka">Nalog</div>
|
||||||
|
|
||||||
@@ -147,19 +195,19 @@
|
|||||||
{{if index .Dozvole "podesavanja.pregled"}}
|
{{if index .Dozvole "podesavanja.pregled"}}
|
||||||
<div>
|
<div>
|
||||||
<button type="button" data-podmeni-dugme
|
<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-kalkulacija-pdv") (eq .Stranica "dozvole")}}aktivan{{end}}"
|
||||||
style="width:100%;background:none;border:none;cursor:pointer;">
|
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>
|
<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>Podešavanja</span>
|
||||||
<span class="nav-strelica">
|
<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"
|
<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-kalkulacija-pdv") (eq .Stranica "dozvole")}}rotate(180deg){{else}}rotate(0deg){{end}}">
|
||||||
<polyline points="6 9 12 15 18 9"/>
|
<polyline points="6 9 12 15 18 9"/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="nav-tooltip">Podešavanja</span>
|
<span class="nav-tooltip">Podešavanja</span>
|
||||||
</button>
|
</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-kalkulacija-pdv") (eq .Stranica "dozvole")}}otvoren{{end}}">
|
||||||
<a href="/admin/podesavanja/opste" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-opste"}}aktivan{{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>
|
<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>
|
<span>Opšte</span>
|
||||||
@@ -175,6 +223,11 @@
|
|||||||
<span>Sistem</span>
|
<span>Sistem</span>
|
||||||
<span class="nav-tooltip">Sistem</span>
|
<span class="nav-tooltip">Sistem</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/admin/podesavanja/kalkulacija-pdv" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-kalkulacija-pdv"}}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>Kalkulacija i PDV</span>
|
||||||
|
<span class="nav-tooltip">Kalkulacija i PDV</span>
|
||||||
|
</a>
|
||||||
{{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}}
|
{{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}}
|
||||||
<a href="/admin/dozvole" class="nav-stavka nav-podstavka {{if eq .Stranica "dozvole"}}aktivan{{end}}">
|
<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>
|
<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%;">
|
style="width:100%;">
|
||||||
</div>
|
</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 -->
|
<!-- napomena -->
|
||||||
<div>
|
<div>
|
||||||
<label class="polje-labela">Napomena</label>
|
<label class="polje-labela">Napomena</label>
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
<input type="text" name="opis" placeholder="Kratak opis kategorije..."
|
<input type="text" name="opis" placeholder="Kratak opis kategorije..."
|
||||||
style="width:100%;">
|
style="width:100%;">
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Marža (%)</label>
|
||||||
|
<input type="number" name="marza" min="0" step="0.01"
|
||||||
|
placeholder="prazno = globalna marža" style="width:100%;">
|
||||||
|
</div>
|
||||||
<div style="display:flex;justify-content:flex-end;">
|
<div style="display:flex;justify-content:flex-end;">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
style="padding:8px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">
|
style="padding:8px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">
|
||||||
@@ -76,6 +81,38 @@
|
|||||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">{{.Opis}}</div>
|
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">{{.Opis}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
{{if .Marza}}
|
||||||
|
<span style="font-size:12px;color:var(--tekst-sporedni);white-space:nowrap;">marža {{.Marza}}%</span>
|
||||||
|
{{end}}
|
||||||
|
{{if index $.Dozvole "kategorija.izmeni"}}
|
||||||
|
<button type="button" class="btn-primarno-malo" onclick="this.nextElementSibling.showModal()">Izmeni</button>
|
||||||
|
{{/* nativni modal — isti obrazac kao u magacinu (top layer, centriran) */}}
|
||||||
|
<dialog id="kat-{{.ID}}" class="premesti-modal" onclick="if(event.target===this)this.close()">
|
||||||
|
<form method="dialog" class="premesti-zaglavlje">
|
||||||
|
<h3>Izmeni kategoriju</h3>
|
||||||
|
<button type="submit" class="premesti-zatvori" aria-label="Zatvori">×</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/magacin/kategorije/izmeni/{{.ID}}" style="display:flex;flex-direction:column;gap:12px;padding:16px;">
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Naziv <span style="color:#dc2626;">*</span></label>
|
||||||
|
<input type="text" name="naziv" value="{{.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>
|
||||||
|
<label class="polje-labela">Opis</label>
|
||||||
|
<input type="text" name="opis" value="{{.Opis}}"
|
||||||
|
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">Marža (%)</label>
|
||||||
|
<input type="number" name="marza" min="0" step="0.01" value="{{if .Marza}}{{.Marza}}{{end}}"
|
||||||
|
placeholder="prazno = globalna marža"
|
||||||
|
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>
|
||||||
|
<button type="submit" class="btn-primarno" style="align-self:flex-end;">Sačuvaj</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
{{end}}
|
||||||
{{if index $.Dozvole "kategorija.obrisi"}}
|
{{if index $.Dozvole "kategorija.obrisi"}}
|
||||||
<a href="/magacin/kategorije/obrisi/{{.ID}}" class="btn-obrisi-malo"
|
<a href="/magacin/kategorije/obrisi/{{.ID}}" class="btn-obrisi-malo"
|
||||||
data-potvrda="Da li ste sigurni da želite da obrišete ovu kategoriju?">
|
data-potvrda="Da li ste sigurni da želite da obrišete ovu kategoriju?">
|
||||||
|
|||||||
@@ -117,6 +117,11 @@
|
|||||||
<input type="text" name="email" value="{{.Klijent.Email}}"
|
<input type="text" name="email" value="{{.Klijent.Email}}"
|
||||||
placeholder="npr. marko@example.com">
|
placeholder="npr. marko@example.com">
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Mesto / grad</label>
|
||||||
|
<input type="text" name="mesto" value="{{.Klijent.Mesto}}"
|
||||||
|
placeholder="npr. Niš">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">
|
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">
|
||||||
Izmeni
|
Izmeni
|
||||||
</a>
|
</a>
|
||||||
|
{{template "promeniCenuMeni" (dict "ID" .ID "Cena" .ProdajnaCena)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if index $.Dozvole "artikal.premesti"}}{{if $.Kategorije}}
|
{{if index $.Dozvole "artikal.premesti"}}{{if $.Kategorije}}
|
||||||
{{template "premestiMeni" (dict "ID" .ID "Kategorije" $.Kategorije "Prefiks" "tab")}}
|
{{template "premestiMeni" (dict "ID" .ID "Kategorije" $.Kategorije "Prefiks" "tab")}}
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
<div class="magacin-kartice">
|
<div class="magacin-kartice">
|
||||||
{{range .Artikli}}
|
{{range .Artikli}}
|
||||||
<div class="kartica magacin-kartica animiraj">
|
<div class="kartica magacin-kartica animiraj">
|
||||||
<div class="red-izmedju" style="align-items:flex-start;gap:12px;margin-bottom:10px;">
|
<div class="red-izmedju" style="align-items:flex-start;gap:12px;margin-bottom:10px;flex-wrap:wrap;">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">{{.Naziv}}</div>
|
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">{{.Naziv}}</div>
|
||||||
{{if .KategorijaNaziv}}
|
{{if .KategorijaNaziv}}
|
||||||
@@ -122,6 +123,7 @@
|
|||||||
<div style="display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;">
|
<div style="display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;">
|
||||||
{{if index $.Dozvole "artikal.izmeni"}}
|
{{if index $.Dozvole "artikal.izmeni"}}
|
||||||
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
|
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
|
||||||
|
{{template "promeniCenuMeni" (dict "ID" .ID "Cena" .ProdajnaCena)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if index $.Dozvole "artikal.premesti"}}{{if $.Kategorije}}
|
{{if index $.Dozvole "artikal.premesti"}}{{if $.Kategorije}}
|
||||||
{{template "premestiMeni" (dict "ID" .ID "Kategorije" $.Kategorije "Prefiks" "kart")}}
|
{{template "premestiMeni" (dict "ID" .ID "Kategorije" $.Kategorije "Prefiks" "kart")}}
|
||||||
@@ -179,3 +181,29 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "promeniCenuMeni"}}
|
||||||
|
<button type="button" class="btn-primarno-malo" style="align-self:center;"
|
||||||
|
onclick="this.nextElementSibling.showModal()">Promeni cenu</button>
|
||||||
|
{{/* nativni modal — isti obrazac kao premesti (top layer, centriran) */}}
|
||||||
|
<dialog id="cena-{{.ID}}" class="premesti-modal" onclick="if(event.target===this)this.close()">
|
||||||
|
<form method="dialog" class="premesti-zaglavlje">
|
||||||
|
<h3>Promeni prodajnu cenu</h3>
|
||||||
|
<button type="submit" class="premesti-zatvori" aria-label="Zatvori">×</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/magacin/promeni-cenu/{{.ID}}" style="display:flex;flex-direction:column;gap:12px;padding:16px;">
|
||||||
|
<div style="font-size:13px;color:var(--tekst-sporedni);">Trenutna cena: <strong>{{printf "%.0f" .Cena}} din</strong></div>
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Nova cena (din)</label>
|
||||||
|
<input type="number" name="nova_cena" min="0" step="0.01" value="{{printf "%.2f" .Cena}}" 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">Razlog (opciono)</label>
|
||||||
|
<input type="text" name="razlog" placeholder="npr. promena nabavne cene"
|
||||||
|
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>
|
||||||
|
<button type="submit" class="btn-primarno" style="align-self:flex-end;">Sačuvaj cenu</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
{{end}}
|
||||||
|
|||||||
@@ -71,6 +71,13 @@
|
|||||||
<input type="number" name="prodajna_cena" value="{{.Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;">
|
<input type="number" name="prodajna_cena" value="{{.Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- marža za kalkulaciju; prazno = nasleđuje maržu kategorije ili globalnu -->
|
||||||
|
<div>
|
||||||
|
<label class="polje-labela">Marža (%)</label>
|
||||||
|
<input type="number" name="marza" value="{{if .Artikal.Marza}}{{.Artikal.Marza}}{{end}}" min="0" step="0.01" style="width:100%;"
|
||||||
|
placeholder="prazno = po kategoriji / globalna">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- lokacija -->
|
<!-- lokacija -->
|
||||||
<div>
|
<div>
|
||||||
<label class="polje-labela">Lokacija u magacinu</label>
|
<label class="polje-labela">Lokacija u magacinu</label>
|
||||||
|
|||||||
@@ -122,6 +122,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- zavisni troškovi -->
|
||||||
|
{{if .Troskovi}}
|
||||||
|
<div class="kartica detalji-kartica animiraj" style="padding:0;overflow:hidden;">
|
||||||
|
<div style="padding:14px 16px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
|
||||||
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Zavisni troškovi</span>
|
||||||
|
<span class="pomocni-tekst">
|
||||||
|
Raspodela: {{if eq .Nabavka.MetodRaspodele "kolicina"}}po količini{{else}}po vrednosti stavke{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="padding:8px 16px;">
|
||||||
|
{{range .Troskovi}}
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<span style="font-size:14px;color:var(--tekst-glavni);">{{.Naziv}}</span>
|
||||||
|
<span style="font-size:14px;color:var(--tekst-sporedni);">{{printf "%.2f" .Iznos}} din</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;">
|
||||||
|
<span style="font-size:13px;font-weight:500;color:var(--tekst-sporedni);">Ukupno troškovi:</span>
|
||||||
|
<span style="font-size:15px;font-weight:600;color:var(--tekst-glavni);">{{printf "%.2f" .UkupanTrosak}} din</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- zona za brisanje -->
|
<!-- zona za brisanje -->
|
||||||
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
|
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
|
||||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||||
|
|||||||
@@ -8,13 +8,15 @@
|
|||||||
.forma-kartica:nth-child(1) { animation-delay: 0.04s; }
|
.forma-kartica:nth-child(1) { animation-delay: 0.04s; }
|
||||||
.forma-kartica:nth-child(2) { animation-delay: 0.12s; }
|
.forma-kartica:nth-child(2) { animation-delay: 0.12s; }
|
||||||
.modal-sadrzaj { animation: modalIn 0.25s ease forwards; }
|
.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>
|
</style>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "sadrzaj"}}
|
{{define "sadrzaj"}}
|
||||||
|
|
||||||
<!-- lista artikala kao JSON — bezbedno serijalizovana na serveru -->
|
<!-- lista artikala kao JSON — bezbedno serijalizovana na serveru -->
|
||||||
<script>var _ntechArtikli = {{.ArtikliJSON}};</script>
|
<script>var _ntechArtikli = {{.ArtikliJSON}}; var _ntechMarza = {{.Marza}}; var _ntechPdvObveznik = {{.PdvObveznik}};</script>
|
||||||
|
|
||||||
<div style="width:100%;" x-data="nabavkaForma">
|
<div style="width:100%;" x-data="nabavkaForma">
|
||||||
|
|
||||||
@@ -37,8 +39,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="kolona" style="gap:14px;">
|
<div class="kolona" style="gap:14px;">
|
||||||
<div>
|
<div>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
<label class="polje-labela">Dobavljač</label>
|
<label class="polje-labela">Dobavljač</label>
|
||||||
<select name="dobavljac_id" style="width:100%;">
|
<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>
|
<option value="">— bez dobavljača —</option>
|
||||||
{{range .Dobavljaci}}
|
{{range .Dobavljaci}}
|
||||||
<option value="{{.ID}}">{{.Naziv}}</option>
|
<option value="{{.ID}}">{{.Naziv}}</option>
|
||||||
@@ -78,6 +87,8 @@
|
|||||||
<th style="padding:8px 10px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Artikal</th>
|
<th style="padding:8px 10px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Artikal</th>
|
||||||
<th style="padding:8px 10px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:90px;">Količina</th>
|
<th style="padding:8px 10px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:90px;">Količina</th>
|
||||||
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:130px;">Cena/kom (din)</th>
|
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:130px;">Cena/kom (din)</th>
|
||||||
|
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:120px;">Marža %</th>
|
||||||
|
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:130px;">Prodajna/kom (din)</th>
|
||||||
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:110px;">Ukupno</th>
|
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:110px;">Ukupno</th>
|
||||||
<th style="width:40px;"></th>
|
<th style="width:40px;"></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -87,7 +98,7 @@
|
|||||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
<td style="padding:8px 10px;">
|
<td style="padding:8px 10px;">
|
||||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
||||||
:disabled="isMobile" style="width:100%;">
|
@change="izaberiArtikal(stavka)" :disabled="isMobile" style="width:100%;">
|
||||||
<option value="">— odaberi artikal —</option>
|
<option value="">— odaberi artikal —</option>
|
||||||
<template x-for="a in artikliOpcije" :key="a.id">
|
<template x-for="a in artikliOpcije" :key="a.id">
|
||||||
<option :value="a.id" x-text="a.naziv"></option>
|
<option :value="a.id" x-text="a.naziv"></option>
|
||||||
@@ -96,10 +107,22 @@
|
|||||||
</td>
|
</td>
|
||||||
<td style="padding:8px 10px;">
|
<td style="padding:8px 10px;">
|
||||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina"
|
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina"
|
||||||
|
@input="preracunajSve()"
|
||||||
min="1" :disabled="isMobile" style="width:100%;text-align:center;">
|
min="1" :disabled="isMobile" style="width:100%;text-align:center;">
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:8px 10px;">
|
<td style="padding:8px 10px;">
|
||||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena"
|
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena"
|
||||||
|
@input="preracunajSve()"
|
||||||
|
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
|
||||||
|
</td>
|
||||||
|
<td style="padding:8px 10px;">
|
||||||
|
<input type="number" :name="'marza[]'" x-model="stavka.marza"
|
||||||
|
@input="izracunajProdajnu(stavka)"
|
||||||
|
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
|
||||||
|
</td>
|
||||||
|
<td style="padding:8px 10px;">
|
||||||
|
<input type="number" :name="'prodajna[]'" x-model="stavka.prodajna"
|
||||||
|
@input="izracunajMarzu(stavka)"
|
||||||
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
|
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:8px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
<td style="padding:8px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||||
@@ -118,7 +141,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr style="border-top:0.5px solid var(--ivica);">
|
<tr style="border-top:0.5px solid var(--ivica);">
|
||||||
<td colspan="3" style="padding:10px 10px;text-align:right;font-size:13px;color:var(--tekst-sporedni);font-weight:500;">Ukupno:</td>
|
<td colspan="5" style="padding:10px 10px;text-align:right;font-size:13px;color:var(--tekst-sporedni);font-weight:500;">Ukupno:</td>
|
||||||
<td style="padding:10px 10px;text-align:right;font-size:15px;font-weight:600;color:var(--tekst-glavni);">
|
<td style="padding:10px 10px;text-align:right;font-size:15px;font-weight:600;color:var(--tekst-glavni);">
|
||||||
<span x-text="ukupnoSvega() + ' din'"></span>
|
<span x-text="ukupnoSvega() + ' din'"></span>
|
||||||
</td>
|
</td>
|
||||||
@@ -146,7 +169,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
|
||||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
||||||
:disabled="!isMobile" style="width:100%;">
|
@change="izaberiArtikal(stavka)" :disabled="!isMobile" style="width:100%;">
|
||||||
<option value="">— odaberi artikal —</option>
|
<option value="">— odaberi artikal —</option>
|
||||||
<template x-for="a in artikliOpcije" :key="a.id">
|
<template x-for="a in artikliOpcije" :key="a.id">
|
||||||
<option :value="a.id" x-text="a.naziv"></option>
|
<option :value="a.id" x-text="a.naziv"></option>
|
||||||
@@ -156,11 +179,21 @@
|
|||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Količina</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Količina</label>
|
||||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" min="1" :disabled="!isMobile" style="width:100%;">
|
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" @input="preracunajSve()" min="1" :disabled="!isMobile" style="width:100%;">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label>
|
||||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
|
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" @input="preracunajSve()" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||||
|
<div>
|
||||||
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Marža %</label>
|
||||||
|
<input type="number" :name="'marza[]'" x-model="stavka.marza" @input="izracunajProdajnu(stavka)" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Prodajna/kom (din)</label>
|
||||||
|
<input type="number" :name="'prodajna[]'" x-model="stavka.prodajna" @input="izracunajMarzu(stavka)" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
<div style="text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||||
@@ -175,6 +208,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- zavisni troškovi -->
|
||||||
|
<div class="kartica forma-kartica animiraj" style="margin-bottom:16px;">
|
||||||
|
<div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
|
||||||
|
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Zavisni troškovi</span>
|
||||||
|
<button type="button" @click="dodajTrosak()" class="btn-primarno" style="font-size:13px;padding:6px 14px;">
|
||||||
|
+ Dodaj trošak
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="troskovi.length > 0">
|
||||||
|
<div class="kolona" style="gap:10px;">
|
||||||
|
<!-- metod raspodele -->
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
||||||
|
<label class="polje-labela" style="margin:0;">Raspodela na stavke:</label>
|
||||||
|
<select name="metod_raspodele" x-model="metodRaspodele" @change="preracunajSve()" style="max-width:240px;">
|
||||||
|
<option value="vrednost">po vrednosti stavke</option>
|
||||||
|
<option value="kolicina">po količini</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- redovi troškova -->
|
||||||
|
<template x-for="(t, i) in troskovi" :key="i">
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
|
<input type="text" :name="'trosak_naziv[]'" x-model="t.naziv"
|
||||||
|
placeholder="npr. Prevoz, Carina, Špedicija" style="flex:1;">
|
||||||
|
<input type="number" :name="'trosak_iznos[]'" x-model="t.iznos"
|
||||||
|
@input="preracunajSve()" min="0" step="0.01"
|
||||||
|
placeholder="iznos (din)" style="width:140px;text-align:right;">
|
||||||
|
<button type="button" @click="ukloniTrosak(i)"
|
||||||
|
style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:18px;line-height:1;padding:2px 6px;border-radius:4px;"
|
||||||
|
title="Ukloni trošak">×</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div style="text-align:right;font-size:13px;color:var(--tekst-sporedni);">
|
||||||
|
Ukupno troškovi: <strong x-text="ukupanTrosak().toFixed(2) + ' din'"></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="troskovi.length === 0">
|
||||||
|
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||||
|
Nema zavisnih troškova. Dodaj trošak (prevoz, carina…) — raspodeliće se na stavke i ući u kalkulativnu nabavnu cenu.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- dugmad forme -->
|
<!-- dugmad forme -->
|
||||||
<div style="display:flex;justify-content:flex-end;gap:10px;">
|
<div style="display:flex;justify-content:flex-end;gap:10px;">
|
||||||
<a href="/nabavke" class="btn-sekundarno">Odustani</a>
|
<a href="/nabavke" class="btn-sekundarno">Odustani</a>
|
||||||
@@ -188,10 +265,10 @@
|
|||||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||||
x-transition:leave="transition ease-in duration-150"
|
x-transition:leave="transition ease-in duration-150"
|
||||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
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()">
|
@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);">
|
<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>
|
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Novi artikal</span>
|
||||||
@@ -222,12 +299,44 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label class="polje-labela">Prodajna cena (din)</label>
|
<label class="polje-labela">Prodajna cena (din)</label>
|
||||||
<input type="number" x-model="modalCena" min="0" step="0.01"
|
<input type="number" x-model="modalCena" min="0" step="0.01"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
style="width:100%;">
|
style="width:100%;">
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;padding-top:14px;border-top:0.5px solid var(--ivica);">
|
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;padding-top:14px;border-top:0.5px solid var(--ivica);">
|
||||||
@@ -241,5 +350,76 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "naslov"}}Nivelacije — promene cena — NTech{{end}}
|
||||||
|
|
||||||
|
{{define "sadrzaj"}}
|
||||||
|
<div class="stranica-stack" style="width:100%;max-width:100%;">
|
||||||
|
|
||||||
|
<!-- filter perioda -->
|
||||||
|
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||||
|
<form method="GET" action="/nivelacije" 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="/nivelacije" class="nazad-link" style="margin-bottom:0;">Poništi filter</a>{{end}}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- istorija nivelacija -->
|
||||||
|
<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:760px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<th style="padding:10px 12px;">Datum</th>
|
||||||
|
<th style="padding:10px 12px;">Artikal</th>
|
||||||
|
<th style="padding:10px 12px;text-align:right;">Stara cena</th>
|
||||||
|
<th style="padding:10px 12px;text-align:right;">Nova cena</th>
|
||||||
|
<th style="padding:10px 12px;text-align:right;">Razlika</th>
|
||||||
|
<th style="padding:10px 12px;">Izvor</th>
|
||||||
|
<th style="padding:10px 12px;">Razlog</th>
|
||||||
|
<th style="padding:10px 12px;">Korisnik</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Zapisi}}
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<td style="padding:10px 12px;white-space:nowrap;">{{.Datum.Format "02.01.2006."}}</td>
|
||||||
|
<td style="padding:10px 12px;">{{.ArtikalNaziv}}</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;color:var(--tekst-sporedni);">{{printf "%.2f" .StaraCena}}</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;font-weight:500;">{{printf "%.2f" .NovaCena}}</td>
|
||||||
|
<td style="padding:10px 12px;text-align:right;white-space:nowrap;color:{{if .Poskupljenje}}var(--greska){{else}}var(--uspeh){{end}};">
|
||||||
|
{{if .Poskupljenje}}+{{end}}{{printf "%.2f" .Razlika}}
|
||||||
|
<div style="font-size:11px;">{{if .Poskupljenje}}+{{end}}{{printf "%.1f" .Procenat}}%</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 12px;">
|
||||||
|
{{if eq .Izvor "rucno"}}<span title="Posebna akcija „Promeni cenu“">ručno</span>
|
||||||
|
{{else if eq .Izvor "izmena"}}<span title="Promenjeno kroz izmenu artikla">izmena</span>
|
||||||
|
{{else if eq .Izvor "kalkulacija"}}<span title="Iz kalkulacije pri prijemu robe">kalkulacija</span>
|
||||||
|
{{else}}{{.Izvor}}{{end}}
|
||||||
|
</td>
|
||||||
|
<td style="padding:10px 12px;color:var(--tekst-sporedni);">{{if .Razlog}}{{.Razlog}}{{else}}—{{end}}</td>
|
||||||
|
<td style="padding:10px 12px;color:var(--tekst-sporedni);">{{if .KorisnikIme}}{{.KorisnikIme}}{{else}}—{{end}}</td>
|
||||||
|
</tr>
|
||||||
|
{{else}}
|
||||||
|
<tr><td colspan="8" style="padding:28px;text-align:center;color:var(--tekst-sporedni);">Nema zabeleženih promena cena u izabranom periodu.</td></tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</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}}{{if .Uvoz}}<div style="display:inline-block;margin-top:2px;font-size:10px;font-weight:600;color:var(--sb-akcent);border:0.5px solid var(--sb-akcent);border-radius:4px;padding:0 5px;">UVOZ</div>{{end}}</td>
|
||||||
|
<td style="padding:10px 12px;">{{.DobavljacNaziv}}{{if .DobavljacPib}}<div style="font-size:11px;color:var(--tekst-sporedni);">{{.OznakaPoreskogBroja}}: {{.DobavljacPib}}</div>{{end}}</td>
|
||||||
|
<td style="padding:10px 12px;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,136 @@
|
|||||||
|
{{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="margin-top:14px;">
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;color:var(--tekst-glavni);">
|
||||||
|
<input type="checkbox" name="uvoz" value="1" style="width:16px;height:16px;cursor:pointer;">
|
||||||
|
Uvoz (JCI)
|
||||||
|
</label>
|
||||||
|
<div class="pomocni-tekst" style="margin-top:4px;">
|
||||||
|
Označi ako je račun uvoz po carinskom dokumentu (JCI) — PDV ide u polja 006/106 obrasca PPPDV.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;">
|
||||||
|
<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,115 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "naslov"}}Podešavanja — Kalkulacija i PDV — NTech{{end}}
|
||||||
|
|
||||||
|
{{define "sadrzaj"}}
|
||||||
|
<div class="stranica-stack" style="width:100%;max-width:100%;">
|
||||||
|
|
||||||
|
<!-- kalkulacija: podrazumevana marža za formiranje prodajne cene pri nabavci -->
|
||||||
|
<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="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Kalkulacija</span>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="/podesavanja/sacuvaj">
|
||||||
|
<input type="hidden" name="_next" value="/admin/podesavanja/kalkulacija-pdv">
|
||||||
|
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-end;">
|
||||||
|
<div>
|
||||||
|
<label for="kalkulacija_marza" class="polje-labela">Podrazumevana marža (%)</label>
|
||||||
|
<input type="number" id="kalkulacija_marza" name="kalkulacija_marza" min="0" max="1000" step="0.01" value="{{.KalkulacijaMarza}}"
|
||||||
|
style="width:140px;padding:8px 12px;background:var(--pozadina);border:0.5px solid var(--ivica);border-radius:8px;color:var(--tekst-glavni);font-size:14px;">
|
||||||
|
</div>
|
||||||
|
<button type="submit"
|
||||||
|
style="padding:9px 18px;background:var(--sb-akcent);border:none;border-radius:8px;color:#fff;font-size:13px;font-weight:500;cursor:pointer;">
|
||||||
|
Sačuvaj
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:8px;">
|
||||||
|
Početna marža za formiranje prodajne cene pri nabavci. Po stavci je možeš promeniti.
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
</div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
|
|
||||||
{{if .LogoGreska}}
|
{{if .LogoGreska}}
|
||||||
@@ -135,4 +223,37 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{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}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user