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
This commit is contained in:
2026-06-14 17:16:24 +02:00
parent c7470ebbc9
commit 100915d453
3 changed files with 22 additions and 4 deletions
+3
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
} }
@@ -127,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"),
}) })
} }
@@ -157,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
+15 -1
View File
@@ -191,6 +191,7 @@ document.addEventListener('alpine:init', () => {
marzaDefault: 0, marzaDefault: 0,
troskovi: [], // zavisni troškovi {naziv, iznos} troskovi: [], // zavisni troškovi {naziv, iznos}
metodRaspodele: 'vrednost', // 'vrednost' ili 'kolicina' 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,
@@ -216,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 => {
@@ -226,8 +228,10 @@ document.addEventListener('alpine:init', () => {
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() 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
}, },
@@ -270,6 +274,16 @@ document.addEventListener('alpine:init', () => {
const pdv = this.pdvStopa(s.artikal_id) const pdv = this.pdvStopa(s.artikal_id)
s.prodajna = Math.round(nabavna * (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 // raspodela zavisi od svih stavki — promena troška/metoda/količine/cene preračunava sve
preracunajSve() { preracunajSve() {
this.stavke.forEach(s => this.izracunajProdajnu(s)) this.stavke.forEach(s => this.izracunajProdajnu(s))
+4 -3
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>
@@ -122,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);">
@@ -191,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);">