Compare commits

...

10 Commits

Author SHA1 Message Date
Dasko fc2743e98d docs: README — knjigovodstvene funkcije (kalkulacija, nivelacija, PDV, uvoz) 2026-06-14 17:45:35 +02:00
Dasko e3f42e64a8 Merge feature/kw-podesavanja-kalkulacija: podstavka „Kalkulacija i PDV" + ispravka mobilnih stavki nabavke 2026-06-14 17:41:41 +02:00
Dasko ae7fd7b5e2 fix(nabavka): mobilni prikaz stavki — ukloni inline display:none
Inline display:none na .stavke-kartice nadjačavao je @media iz main.css, pa se na
telefonu nije video nijedan raspored stavki. Prikaz sada vodi samo CSS klasa.
2026-06-14 17:40:54 +02:00
Dasko 4d81c576cd feat(podesavanja): podstavka „Kalkulacija i PDV" — marža + PDV stope na jednom mestu
- PdvStope handler: dodata podrazumevana marža; stranica „podesavanja-kalkulacija-pdv",
  naslov „Kalkulacija i PDV", ruta /admin/podesavanja/kalkulacija-pdv
- pdv_stope.html: sekcija „Kalkulacija" (forma marže preko /podesavanja/sacuvaj) iznad šifarnika
- marža uklonjena iz stranice „Sistem"
- sidebar: podstavka „PDV stope" → „Kalkulacija i PDV"
2026-06-14 17:40:38 +02:00
Dasko 6444e19808 Merge feature/kw-kalkulacija-prodajna: dvosmerna marža↔prodajna + status PDV obveznika 2026-06-14 17:16:58 +02:00
Dasko 6ee9eefdd0 Merge feature/kw-uvoz: uvoz robe — KPR zastavica i PPPDV 006/106 2026-06-14 17:16:58 +02:00
Dasko 100915d453 feat(nabavka): dvosmerna marža↔prodajna + poštovanje statusa PDV obveznika
- ručni unos prodajne cene preračunava maržu (izracunajMarzu): obrnuta formula
  marža = (prodajna / (nabavna × (1+pdv/100)) − 1) × 100
- kalkulacija poštuje da li je firma PDV obveznik: ako nije, prodajna se ne
  uvećava za PDV (pdvStopa = 0); PodaciFormeNabavke.PdvObveznik → JS _ntechPdvObveznik
- proširena kolona „Marža %" da se vidi cela vrednost
2026-06-14 17:16:24 +02:00
Dasko 42c74a725a feat(pdv): uvoz robe — KPR zastavica i mapiranje u PPPDV 006/106
- migracija 048: kolona uvoz na pdv_kpr (0=domaća nabavka, 1=uvoz)
- model PdvKpr.Uvoz; MapirajPPPDV(kir, kprDomace, kprUvoz) rutira uvoz u 006/106,
  domaće u 008/108; test ažuriran + uvozni scenario
- repo: KPR Lista/DohvatiID/Kreiraj čitaju i pišu uvoz
- obračun: KPR se razdvaja na domaće/uvozne; obaveza ostaje na ukupnom KPR-u
- KPR forma: kvačica „Uvoz (JCI)"; lista: oznaka UVOZ uz broj dokumenta
2026-06-14 17:16:01 +02:00
Dasko c7470ebbc9 Merge feature/kw-kalkulacija-troskovi: Faza C — marža po kategoriji/artiklu + zavisni troškovi (§5.4) 2026-06-14 16:24:31 +02:00
Dasko 803e1f6341 feat(nabavka): UI zavisnih troškova — forma i prikaz u detaljima (Faza C, celina 2)
- forma nabavke: sekcija „Zavisni troškovi" (slobodne stavke naziv+iznos,
  dodavanje/uklanjanje) + izbor metoda raspodele (po vrednosti / po količini)
- JS: kalkNabavna (kalkulativna nabavna po stavci, prati server) i preracunajSve —
  promena troška/metoda/količine/cene preračunava prodajne svih stavki
- detalji nabavke: prikaz zavisnih troškova, metoda raspodele i ukupnog iznosa
- handler DetaljiNabavke dohvata troškove (DohvatiTroskove)
2026-06-14 16:23:11 +02:00
19 changed files with 289 additions and 70 deletions
+8 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -247,7 +247,7 @@ 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/pdv-stope", h.PdvStope) 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/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}/izmeni", h.IzmeniPdvStopu)
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/aktivnost", h.PromeniAktivnostPdvStope) r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/aktivnost", h.PromeniAktivnostPdvStope)
+12 -6
View File
@@ -171,7 +171,7 @@ func (r *PdvKprRepo) Lista(ctx context.Context, od, do time.Time) ([]model.PdvKp
dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''), dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''),
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
pdv_bez_odbitka, osloboden_nabavka, ukupno, pdv_bez_odbitka, osloboden_nabavka, ukupno,
COALESCE(napomena, ''), izvor, izvor_id, datum_unosa COALESCE(napomena, ''), izvor, izvor_id, uvoz, datum_unosa
FROM pdv_kpr WHERE 1=1` FROM pdv_kpr WHERE 1=1`
args := []any{} args := []any{}
if !od.IsZero() { if !od.IsZero() {
@@ -211,7 +211,7 @@ func (r *PdvKprRepo) DohvatiID(ctx context.Context, id int64) (*model.PdvKpr, er
dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''), dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''),
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
pdv_bez_odbitka, osloboden_nabavka, ukupno, pdv_bez_odbitka, osloboden_nabavka, ukupno,
COALESCE(napomena, ''), izvor, izvor_id, datum_unosa COALESCE(napomena, ''), izvor, izvor_id, uvoz, datum_unosa
FROM pdv_kpr WHERE id = ?`, id) FROM pdv_kpr WHERE id = ?`, id)
k, err := skenirajKpr(red.Scan) k, err := skenirajKpr(red.Scan)
if err != nil { if err != nil {
@@ -225,12 +225,13 @@ func skenirajKpr(scan func(...any) error) (model.PdvKpr, error) {
var k model.PdvKpr var k model.PdvKpr
var datumPlacanja sql.NullTime var datumPlacanja sql.NullTime
var izvorID sql.NullInt64 var izvorID sql.NullInt64
var uvoz int64
if err := scan( if err := scan(
&k.ID, &k.DatumPrometa, &k.DatumKnjizenja, &datumPlacanja, &k.BrojDokumenta, &k.ID, &k.DatumPrometa, &k.DatumKnjizenja, &datumPlacanja, &k.BrojDokumenta,
&k.DobavljacNaziv, &k.DobavljacPib, &k.DobavljacMesto, &k.DobavljacNaziv, &k.DobavljacPib, &k.DobavljacMesto,
&k.OsnovicaOpsta, &k.PdvOpsta, &k.OsnovicaPosebna, &k.PdvPosebna, &k.OsnovicaOpsta, &k.PdvOpsta, &k.OsnovicaPosebna, &k.PdvPosebna,
&k.PdvBezOdbitka, &k.OslobodenNabavka, &k.Ukupno, &k.PdvBezOdbitka, &k.OslobodenNabavka, &k.Ukupno,
&k.Napomena, &k.Izvor, &izvorID, &k.DatumUnosa, &k.Napomena, &k.Izvor, &izvorID, &uvoz, &k.DatumUnosa,
); err != nil { ); err != nil {
return model.PdvKpr{}, err return model.PdvKpr{}, err
} }
@@ -240,6 +241,7 @@ func skenirajKpr(scan func(...any) error) (model.PdvKpr, error) {
if izvorID.Valid { if izvorID.Valid {
k.IzvorID = &izvorID.Int64 k.IzvorID = &izvorID.Int64
} }
k.Uvoz = uvoz != 0
return k, nil return k, nil
} }
@@ -249,18 +251,22 @@ func (r *PdvKprRepo) Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error
if k.DatumPlacanja != nil { if k.DatumPlacanja != nil {
datumPlacanja = *k.DatumPlacanja datumPlacanja = *k.DatumPlacanja
} }
uvoz := 0
if k.Uvoz {
uvoz = 1
}
rez, err := r.db.ExecContext(ctx, ` rez, err := r.db.ExecContext(ctx, `
INSERT INTO pdv_kpr ( INSERT INTO pdv_kpr (
datum_prometa, datum_knjizenja, datum_placanja, broj_dokumenta, datum_prometa, datum_knjizenja, datum_placanja, broj_dokumenta,
dobavljac_naziv, dobavljac_pib, dobavljac_mesto, dobavljac_naziv, dobavljac_pib, dobavljac_mesto,
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna, osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
pdv_bez_odbitka, osloboden_nabavka, ukupno, napomena, izvor, izvor_id pdv_bez_odbitka, osloboden_nabavka, ukupno, napomena, izvor, izvor_id, uvoz
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
k.DatumPrometa, k.DatumKnjizenja, datumPlacanja, k.BrojDokumenta, k.DatumPrometa, k.DatumKnjizenja, datumPlacanja, k.BrojDokumenta,
k.DobavljacNaziv, k.DobavljacPib, k.DobavljacMesto, k.DobavljacNaziv, k.DobavljacPib, k.DobavljacMesto,
k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna, k.OsnovicaOpsta, k.PdvOpsta, k.OsnovicaPosebna, k.PdvPosebna,
k.PdvBezOdbitka, k.OslobodenNabavka, k.Ukupno, k.Napomena, k.PdvBezOdbitka, k.OslobodenNabavka, k.Ukupno, k.Napomena,
izvorIliRucno(k.Izvor), izvorIDArg(k.IzvorID)) izvorIliRucno(k.Izvor), izvorIDArg(k.IzvorID), uvoz)
if err != nil { if err != nil {
return 0, fmt.Errorf("ntech: PdvKprRepo.Kreiraj: %w", err) return 0, fmt.Errorf("ntech: PdvKprRepo.Kreiraj: %w", err)
} }
+17
View File
@@ -31,6 +31,7 @@ type PodaciFormeNabavke struct {
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 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
} }
@@ -39,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
} }
@@ -125,6 +128,7 @@ func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) {
Dobavljaci: dobavljaci, Dobavljaci: dobavljaci,
Kategorije: kategorije, Kategorije: kategorije,
Marza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"), Marza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
PdvObveznik: h.modulUkljucen(r.Context(), "pdv"),
}) })
} }
@@ -155,6 +159,7 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
Dobavljaci: dobavljaci, Dobavljaci: dobavljaci,
Kategorije: kategorije, Kategorije: kategorije,
Marza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"), Marza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
PdvObveznik: h.modulUkljucen(r.Context(), "pdv"),
Greska: greska, Greska: greska,
}) })
return return
@@ -259,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)
@@ -277,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,
} }
+1
View File
@@ -128,6 +128,7 @@ func (h *Handler) SacuvajPdvKpr(w http.ResponseWriter, r *http.Request) {
PdvBezOdbitka: parsiraIznos(r.FormValue("pdv_bez_odbitka")), PdvBezOdbitka: parsiraIznos(r.FormValue("pdv_bez_odbitka")),
OslobodenNabavka: parsiraIznos(r.FormValue("osloboden_nabavka")), OslobodenNabavka: parsiraIznos(r.FormValue("osloboden_nabavka")),
Napomena: strings.TrimSpace(r.FormValue("napomena")), Napomena: strings.TrimSpace(r.FormValue("napomena")),
Uvoz: r.FormValue("uvoz") == "1",
} }
// datum plaćanja je opcionalan // datum plaćanja je opcionalan
if dp := parsiraDatumOpcionalno(r.FormValue("datum_placanja")); !dp.IsZero() { if dp := parsiraDatumOpcionalno(r.FormValue("datum_placanja")); !dp.IsZero() {
+14 -1
View File
@@ -59,6 +59,19 @@ func (h *Handler) PdvObracunStranica(w http.ResponseWriter, r *http.Request) {
kirSume := model.SumirajKir(kirZapisi) kirSume := model.SumirajKir(kirZapisi)
kprSume := model.SumirajKpr(kprZapisi) kprSume := model.SumirajKpr(kprZapisi)
// razdvajamo KPR na domaće i uvozne — uvoz se u PPPDV mapira u polja 006/106,
// domaća nabavka u 008/108; obračun obaveze ostaje na ukupnom KPR-u (uvozni PDV je odbitni)
var kprDomaci, kprUvozni []model.PdvKpr
for _, z := range kprZapisi {
if z.Uvoz {
kprUvozni = append(kprUvozni, z)
} else {
kprDomaci = append(kprDomaci, z)
}
}
kprDomaceSume := model.SumirajKpr(kprDomaci)
kprUvozSume := model.SumirajKpr(kprUvozni)
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "pdv-obracun" ps.Stranica = "pdv-obracun"
ps.NaslovStranice = "PDV obračun" ps.NaslovStranice = "PDV obračun"
@@ -69,6 +82,6 @@ func (h *Handler) PdvObracunStranica(w http.ResponseWriter, r *http.Request) {
KirSume: kirSume, KirSume: kirSume,
KprSume: kprSume, KprSume: kprSume,
Obracun: model.ObracunajPdv(kirSume, kprSume), Obracun: model.ObracunajPdv(kirSume, kprSume),
PPPDV: model.MapirajPPPDV(kirSume, kprSume), PPPDV: model.MapirajPPPDV(kirSume, kprDomaceSume, kprUvozSume),
}) })
} }
+16 -10
View File
@@ -19,13 +19,15 @@ var validneOznakeStope = map[string]bool{
"oslobodjeno": true, "oslobodjeno": true,
} }
// PodaciPdvStope su podaci za stranicu šifarnika PDV stopa // PodaciPdvStope su podaci za stranicu „Kalkulacija i PDV" (marža + šifarnik PDV stopa)
type PodaciPdvStope struct { type PodaciPdvStope struct {
model.PodaciStranice model.PodaciStranice
Stope []model.PdvStopa Stope []model.PdvStopa
KalkulacijaMarza string // podrazumevana marža (%) za kalkulaciju
} }
// PdvStope renderuje šifarnik PDV stopa (sve stope, uključujući arhivirane) // 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) { func (h *Handler) PdvStope(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok { if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
return return
@@ -42,9 +44,13 @@ func (h *Handler) PdvStope(w http.ResponseWriter, r *http.Request) {
} }
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podesavanja-pdv-stope" ps.Stranica = "podesavanja-kalkulacija-pdv"
ps.NaslovStranice = "PDV stope" ps.NaslovStranice = "Kalkulacija i PDV"
h.renderujTemplate(w, "pdv_stope", PodaciPdvStope{PodaciStranice: ps, Stope: stope}) 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 // parsePdvStopuForma čita i proverava polja forme; vraća popunjenu stopu i poruku o grešci
@@ -86,7 +92,7 @@ func (h *Handler) DodajPdvStopu(w http.ResponseWriter, r *http.Request) {
stopa, greska := parsePdvStopuForma(r) stopa, greska := parsePdvStopuForma(r)
if greska != "" { if greska != "" {
middleware.SetFlash(w, r, h.DB, "greska", greska) middleware.SetFlash(w, r, h.DB, "greska", greska)
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther) http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv", http.StatusSeeOther)
return return
} }
if _, err := h.PdvStopeRepo.Kreiraj(r.Context(), &stopa); err != nil { if _, err := h.PdvStopeRepo.Kreiraj(r.Context(), &stopa); err != nil {
@@ -94,7 +100,7 @@ func (h *Handler) DodajPdvStopu(w http.ResponseWriter, r *http.Request) {
return return
} }
middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je dodata.") middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je dodata.")
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther) http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv", http.StatusSeeOther)
} }
// IzmeniPdvStopu prima POST i menja postojeću stopu // IzmeniPdvStopu prima POST i menja postojeću stopu
@@ -114,7 +120,7 @@ func (h *Handler) IzmeniPdvStopu(w http.ResponseWriter, r *http.Request) {
stopa, greska := parsePdvStopuForma(r) stopa, greska := parsePdvStopuForma(r)
if greska != "" { if greska != "" {
middleware.SetFlash(w, r, h.DB, "greska", greska) middleware.SetFlash(w, r, h.DB, "greska", greska)
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther) http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv", http.StatusSeeOther)
return return
} }
stopa.ID = id stopa.ID = id
@@ -123,7 +129,7 @@ func (h *Handler) IzmeniPdvStopu(w http.ResponseWriter, r *http.Request) {
return return
} }
middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je izmenjena.") middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je izmenjena.")
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther) http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv", http.StatusSeeOther)
} }
// PromeniAktivnostPdvStope arhivira ili vraća stopu u upotrebu (toggle, bez brisanja) // PromeniAktivnostPdvStope arhivira ili vraća stopu u upotrebu (toggle, bez brisanja)
@@ -150,5 +156,5 @@ func (h *Handler) PromeniAktivnostPdvStope(w http.ResponseWriter, r *http.Reques
poruka = "PDV stopa je vraćena u upotrebu." poruka = "PDV stopa je vraćena u upotrebu."
} }
middleware.SetFlash(w, r, h.DB, "uspeh", poruka) middleware.SetFlash(w, r, h.DB, "uspeh", poruka)
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther) http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv", http.StatusSeeOther)
} }
+9 -5
View File
@@ -138,6 +138,7 @@ type PdvKpr struct {
Napomena string Napomena string
Izvor string // "rucno" | "prodaja" | "nabavka" Izvor string // "rucno" | "prodaja" | "nabavka"
IzvorID *int64 // id izvorne nabavke (nil za ručni unos) IzvorID *int64 // id izvorne nabavke (nil za ručni unos)
Uvoz bool // true = uvoz (JCI) → PPPDV 006/106; false = domaća nabavka → 008/108
DatumUnosa time.Time DatumUnosa time.Time
} }
@@ -287,9 +288,10 @@ func (p PPPDV) Polje110Apsolutno() int64 {
} }
// MapirajPPPDV preslikava zbirove KIR i KPR na polja obrasca PPPDV (cele dinare). // MapirajPPPDV preslikava zbirove KIR i KPR na polja obrasca PPPDV (cele dinare).
// ⚠ Uvoz (006/106) i PDV nadoknada poljoprivredniku (007/107) se ne prate zasebno // KPR je razdvojen: uvoz (JCI) ide u polja 006/106 (prethodni porez pri uvozu),
// pa ostaju 0 — sav odbitni PDV iz KPR pada u polje 008/108. // domaća nabavka u 008/108 (ostali prethodni porez).
func MapirajPPPDV(kir PdvKirSume, kpr PdvKprSume) PPPDV { // ⚠ PDV nadoknada poljoprivredniku (007/107) se i dalje ne prati zasebno (ostaje 0).
func MapirajPPPDV(kir PdvKirSume, kprDomace, kprUvoz PdvKprSume) PPPDV {
uDinare := func(v float64) int64 { return int64(math.Round(v)) } uDinare := func(v float64) int64 { return int64(math.Round(v)) }
p := PPPDV{ p := PPPDV{
Polje001: uDinare(kir.OslobodenSaPravom), Polje001: uDinare(kir.OslobodenSaPravom),
@@ -298,8 +300,10 @@ func MapirajPPPDV(kir PdvKirSume, kpr PdvKprSume) PPPDV {
Polje103: uDinare(kir.PdvOpsta), Polje103: uDinare(kir.PdvOpsta),
Polje004: uDinare(kir.OsnovicaPosebna), Polje004: uDinare(kir.OsnovicaPosebna),
Polje104: uDinare(kir.PdvPosebna), Polje104: uDinare(kir.PdvPosebna),
Polje008: uDinare(kpr.OsnovicaOpsta + kpr.OsnovicaPosebna), Polje006: uDinare(kprUvoz.OsnovicaOpsta + kprUvoz.OsnovicaPosebna),
Polje108: uDinare(kpr.PdvOpsta + kpr.PdvPosebna), Polje106: uDinare(kprUvoz.PdvOpsta + kprUvoz.PdvPosebna),
Polje008: uDinare(kprDomace.OsnovicaOpsta + kprDomace.OsnovicaPosebna),
Polje108: uDinare(kprDomace.PdvOpsta + kprDomace.PdvPosebna),
} }
// zbirovi se računaju iz zaokruženih polja da kolone na obrascu uvek zbiraju // zbirovi se računaju iz zaokruženih polja da kolone na obrascu uvek zbiraju
p.Polje005 = p.Polje001 + p.Polje002 + p.Polje003 + p.Polje004 p.Polje005 = p.Polje001 + p.Polje002 + p.Polje003 + p.Polje004
+20 -3
View File
@@ -108,7 +108,7 @@ func TestMapirajPPPDV(t *testing.T) {
PdvBezOdbitka: 25, // ne sme da uđe nigde u PPPDV PdvBezOdbitka: 25, // ne sme da uđe nigde u PPPDV
} }
p := MapirajPPPDV(kir, kpr) p := MapirajPPPDV(kir, kpr, PdvKprSume{})
if p.Polje001 != 300 || p.Polje002 != 101 || p.Polje003 != 1000 || p.Polje103 != 200 { if p.Polje001 != 300 || p.Polje002 != 101 || p.Polje003 != 1000 || p.Polje103 != 200 {
t.Errorf("I deo (001/002/003/103) = %d/%d/%d/%d", p.Polje001, p.Polje002, p.Polje003, p.Polje103) t.Errorf("I deo (001/002/003/103) = %d/%d/%d/%d", p.Polje001, p.Polje002, p.Polje003, p.Polje103)
@@ -120,7 +120,7 @@ func TestMapirajPPPDV(t *testing.T) {
if p.Polje005 != 1902 || p.Polje105 != 250 { if p.Polje005 != 1902 || p.Polje105 != 250 {
t.Errorf("zbir 005/105 = %d/%d, očekivano 1902/250", p.Polje005, p.Polje105) t.Errorf("zbir 005/105 = %d/%d, očekivano 1902/250", p.Polje005, p.Polje105)
} }
// uvoz i poljoprivrednik se ne prate // u ovom scenariju nema uvoza (prazan kprUvoz) ni poljoprivrednika → 006/106/007/107 = 0
if p.Polje006 != 0 || p.Polje106 != 0 || p.Polje007 != 0 || p.Polje107 != 0 { if p.Polje006 != 0 || p.Polje106 != 0 || p.Polje007 != 0 || p.Polje107 != 0 {
t.Errorf("006/106/007/107 moraju biti 0") t.Errorf("006/106/007/107 moraju biti 0")
} }
@@ -137,8 +137,25 @@ func TestMapirajPPPDV(t *testing.T) {
} }
// scenario povraćaja: izlazni < odbitni → 110 negativan, Povracaj=true // scenario povraćaja: izlazni < odbitni → 110 negativan, Povracaj=true
p2 := MapirajPPPDV(PdvKirSume{PdvOpsta: 30}, PdvKprSume{PdvOpsta: 90}) p2 := MapirajPPPDV(PdvKirSume{PdvOpsta: 30}, PdvKprSume{PdvOpsta: 90}, PdvKprSume{})
if p2.Polje110 != -60 || !p2.Povracaj || p2.Polje110Apsolutno() != 60 { if p2.Polje110 != -60 || !p2.Povracaj || p2.Polje110Apsolutno() != 60 {
t.Errorf("povraćaj: 110=%d Povracaj=%v abs=%d, očekivano -60/true/60", p2.Polje110, p2.Povracaj, p2.Polje110Apsolutno()) t.Errorf("povraćaj: 110=%d Povracaj=%v abs=%d, očekivano -60/true/60", p2.Polje110, p2.Povracaj, p2.Polje110Apsolutno())
} }
// scenario uvoza: uvozni KPR ide u 006/106, domaći u 008/108
p3 := MapirajPPPDV(
PdvKirSume{},
PdvKprSume{OsnovicaOpsta: 1000, PdvOpsta: 200}, // domaće → 008/108
PdvKprSume{OsnovicaOpsta: 500, PdvOpsta: 100, OsnovicaPosebna: 50, PdvPosebna: 5}, // uvoz → 006/106
)
if p3.Polje006 != 550 || p3.Polje106 != 105 {
t.Errorf("uvoz 006/106 = %d/%d, očekivano 550/105", p3.Polje006, p3.Polje106)
}
if p3.Polje008 != 1000 || p3.Polje108 != 200 {
t.Errorf("domaće 008/108 = %d/%d, očekivano 1000/200", p3.Polje008, p3.Polje108)
}
// 009 = 006+007+008 = 550+0+1000 = 1550; 109 = 106+107+108 = 105+0+200 = 305
if p3.Polje009 != 1550 || p3.Polje109 != 305 {
t.Errorf("zbir prethodnog 009/109 = %d/%d, očekivano 1550/305", p3.Polje009, p3.Polje109)
}
} }
+6
View File
@@ -0,0 +1,6 @@
-- Zastavica „uvoz" na zapisima knjige primljenih računa (KPR).
-- Uvoz se unosi RUČNO sa JCI (osnovica i PDV iz carinskog dokumenta drže postojeće
-- kolone osnovica_opsta / pdv_opsta). Zastavica služi da se uvozni PDV u PPPDV obrascu
-- rutira u polja 006/106 (prethodni porez pri uvozu) umesto 008/108 (ostali prethodni).
-- 0 = domaća nabavka (podrazumevano), 1 = uvoz.
ALTER TABLE pdv_kpr ADD COLUMN uvoz INTEGER NOT NULL DEFAULT 0;
+55 -4
View File
@@ -189,6 +189,9 @@ document.addEventListener('alpine:init', () => {
stavke: [{artikal_id: '', kolicina: 1, cena: 0, marza: 0, prodajna: 0}], stavke: [{artikal_id: '', kolicina: 1, cena: 0, marza: 0, prodajna: 0}],
artikliOpcije: [], artikliOpcije: [],
marzaDefault: 0, 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,
@@ -214,6 +217,7 @@ document.addEventListener('alpine:init', () => {
init() { init() {
this.artikliOpcije = window._ntechArtikli || [] this.artikliOpcije = window._ntechArtikli || []
this.marzaDefault = parseFloat(window._ntechMarza) || 0 this.marzaDefault = parseFloat(window._ntechMarza) || 0
this.pdvObveznik = window._ntechPdvObveznik === true
this.stavke.forEach(s => { s.marza = this.marzaDefault }) 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 => {
@@ -222,9 +226,12 @@ document.addEventListener('alpine:init', () => {
}, },
dodajStavku() { dodajStavku() {
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, marza: this.marzaDefault, prodajna: 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 // 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) { pdvStopa(artikalId) {
if (!this.pdvObveznik) return 0
const a = this.artikliOpcije.find(x => String(x.id) === String(artikalId)) const a = this.artikliOpcije.find(x => String(x.id) === String(artikalId))
return a ? (parseFloat(a.pdv_stopa) || 0) : 0 return a ? (parseFloat(a.pdv_stopa) || 0) : 0
}, },
@@ -238,15 +245,59 @@ document.addEventListener('alpine:init', () => {
} }
this.izracunajProdajnu(s) this.izracunajProdajnu(s)
}, },
// prodajna (sa PDV) = nabavna × (1 + marža/100) × (1 + pdvStopa/100), zaokruženo na 2 decimale // ukupan zavisni trošak nabavke
izracunajProdajnu(s) { 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 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 marza = parseFloat(s.marza) || 0
const pdv = this.pdvStopa(s.artikal_id) const pdv = this.pdvStopa(s.artikal_id)
s.prodajna = Math.round(cena * (1 + marza / 100) * (1 + pdv / 100) * 100) / 100 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)
+6 -6
View File
@@ -195,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 "podesavanja-pdv-stope") (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 "podesavanja-pdv-stope") (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 "podesavanja-pdv-stope") (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>
@@ -223,10 +223,10 @@
<span>Sistem</span> <span>Sistem</span>
<span class="nav-tooltip">Sistem</span> <span class="nav-tooltip">Sistem</span>
</a> </a>
<a href="/admin/podesavanja/pdv-stope" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-pdv-stope"}}aktivan{{end}}"> <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> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></svg>
<span>PDV stope</span> <span>Kalkulacija i PDV</span>
<span class="nav-tooltip">PDV stope</span> <span class="nav-tooltip">Kalkulacija i PDV</span>
</a> </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}}">
@@ -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;">
+54 -8
View File
@@ -16,7 +16,7 @@
{{define "sadrzaj"}} {{define "sadrzaj"}}
<!-- lista artikala kao JSON — bezbedno serijalizovana na serveru --> <!-- lista artikala kao JSON — bezbedno serijalizovana na serveru -->
<script>var _ntechArtikli = {{.ArtikliJSON}}; var _ntechMarza = {{.Marza}};</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">
@@ -87,7 +87,7 @@
<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:90px;">Marža %</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: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>
@@ -107,11 +107,12 @@
</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="izracunajProdajnu(stavka)" @input="preracunajSve()"
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;"> <td style="padding:8px 10px;">
@@ -121,6 +122,7 @@
</td> </td>
<td style="padding:8px 10px;"> <td style="padding:8px 10px;">
<input type="number" :name="'prodajna[]'" x-model="stavka.prodajna" <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);">
@@ -149,8 +151,8 @@
</table> </table>
</div> </div>
<!-- mobilne kartice stavki --> <!-- mobilne kartice stavki (prikaz/skrivanje vodi .stavke-kartice u main.css) -->
<div class="stavke-kartice" style="display:none;flex-direction:column;gap:10px;"> <div class="stavke-kartice">
<template x-for="(stavka, i) in stavke" :key="i"> <template x-for="(stavka, i) in stavke" :key="i">
<div style="border:0.5px solid var(--ivica);border-radius:8px;padding:12px;"> <div style="border:0.5px solid var(--ivica);border-radius:8px;padding:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
@@ -176,11 +178,11 @@
<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" @input="izracunajProdajnu(stavka)" 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> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
@@ -190,7 +192,7 @@
</div> </div>
<div> <div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Prodajna/kom (din)</label> <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" min="0" step="0.01" :disabled="!isMobile" style="width:100%;"> <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);">
@@ -205,6 +207,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>
+1 -1
View File
@@ -47,7 +47,7 @@
{{range .Zapisi}} {{range .Zapisi}}
<tr style="border-bottom:0.5px solid var(--ivica);"> <tr style="border-bottom:0.5px solid var(--ivica);">
<td style="padding:10px 12px;white-space:nowrap;">{{.DatumPrometa.Format "02.01.2006."}}</td> <td style="padding:10px 12px;white-space:nowrap;">{{.DatumPrometa.Format "02.01.2006."}}</td>
<td style="padding:10px 12px;">{{.BrojDokumenta}}</td> <td style="padding:10px 12px;">{{.BrojDokumenta}}{{if .Uvoz}}<div style="display:inline-block;margin-top:2px;font-size:10px;font-weight:600;color:var(--sb-akcent);border:0.5px solid var(--sb-akcent);border-radius:4px;padding:0 5px;">UVOZ</div>{{end}}</td>
<td style="padding:10px 12px;">{{.DobavljacNaziv}}{{if .DobavljacPib}}<div style="font-size:11px;color:var(--tekst-sporedni);">{{.OznakaPoreskogBroja}}: {{.DobavljacPib}}</div>{{end}}</td> <td style="padding:10px 12px;">{{.DobavljacNaziv}}{{if .DobavljacPib}}<div style="font-size:11px;color:var(--tekst-sporedni);">{{.OznakaPoreskogBroja}}: {{.DobavljacPib}}</div>{{end}}</td>
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .OsnovicaOpsta}}</td> <td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .OsnovicaOpsta}}</td>
<td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .PdvOpsta}}</td> <td style="padding:10px 12px;text-align:right;">{{printf "%.2f" .PdvOpsta}}</td>
+10
View File
@@ -102,6 +102,16 @@
<input type="text" name="napomena" style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;"> <input type="text" name="napomena" style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
</div> </div>
<div style="margin-top:14px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;color:var(--tekst-glavni);">
<input type="checkbox" name="uvoz" value="1" style="width:16px;height:16px;cursor:pointer;">
Uvoz (JCI)
</label>
<div class="pomocni-tekst" style="margin-top:4px;">
Označi ako je račun uvoz po carinskom dokumentu (JCI) — PDV ide u polja 006/106 obrasca PPPDV.
</div>
</div>
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;"> <div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;">
<a href="/pdv/kpr" class="btn-sekundarno" style="font-size:14px;padding:10px 20px;">Odustani</a> <a href="/pdv/kpr" class="btn-sekundarno" style="font-size:14px;padding:10px 20px;">Odustani</a>
<button type="submit" style="background:var(--sb-akcent);color:#fff;border:none;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">Sačuvaj</button> <button type="submit" style="background:var(--sb-akcent);color:#fff;border:none;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">Sačuvaj</button>
+26 -1
View File
@@ -1,10 +1,35 @@
{{template "base" .}} {{template "base" .}}
{{define "naslov"}}Podešavanja — PDV stope — NTech{{end}} {{define "naslov"}}Podešavanja — Kalkulacija i PDV — NTech{{end}}
{{define "sadrzaj"}} {{define "sadrzaj"}}
<div class="stranica-stack" style="width:100%;max-width:100%;"> <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 --> <!-- postojeće stope: svaki red je forma za izmenu + zasebna forma za arhiviranje -->
<div class="kartica animiraj" style="margin-bottom:16px;"> <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);"> <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
@@ -56,27 +56,6 @@
</form> </form>
</div> </div>
<div style="margin-top:16px;border-top:0.5px solid var(--ivica);padding-top:16px;">
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);margin-bottom:10px;">Kalkulacija</div>
<form method="POST" action="/podesavanja/sacuvaj">
<input type="hidden" name="_next" value="/admin/podesavanja/sistem">
<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>
<!-- panel sa listom backupa --> <!-- panel sa listom backupa -->
<div id="backup-panel" style="display:none;margin-top:16px;border-top:0.5px solid var(--ivica);padding-top:16px;"> <div id="backup-panel" style="display:none;margin-top:16px;border-top:0.5px solid var(--ivica);padding-top:16px;">
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);margin-bottom:10px;">Dostupne rezervne kopije</div> <div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);margin-bottom:10px;">Dostupne rezervne kopije</div>