- migrations/087: nova kolona cena_sa_pdv + popunjavanje postojećih - model.Artikal: polje CenaSaPdv, ispravljene CenaBezPdv/PdvIznos (neto) - sqlite/artikal.go: SELECT/Scan/INSERT/UPDATE za cena_sa_pdv (6 mesta) - magacin_forma.go: pdv_stopa iz forme, kalkulacija cena_sa_pdv, default PDV 20% - magacin.html: dinari format (25.999,00), kolona Sa PDV-om, uslovno po Moduli.pdv - magacin_forma.html: PDV dropdown + Cena sa PDV polje, uslovno po Moduli.pdv, optimizovan layout (Naziv/Jed. mere/Kategorija u 1 redu) - main.go: demo mod bez keširanja šablona (hot-reload) - model_test.go: ažurirani testovi za novo ponašanje CenaBezPdv/PdvIznos
272 lines
14 KiB
HTML
272 lines
14 KiB
HTML
{{template "base" .}}
|
|
|
|
{{define "naslov"}}{{if .Izmena}}Izmeni artikal{{else}}Novi artikal{{end}} — NTech{{end}}
|
|
|
|
{{define "sadrzaj"}}
|
|
<div style="width:100%;">
|
|
|
|
<!-- nazad dugme -->
|
|
<a href="/magacin" class="nazad-link" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML">
|
|
<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" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
|
Nazad na magacin
|
|
</a>
|
|
|
|
<div class="kartica animiraj">
|
|
<div style="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);">
|
|
{{if .Izmena}}Izmeni artikal{{else}}Novi artikal{{end}}
|
|
</span>
|
|
</div>
|
|
|
|
{{if .Greska}}
|
|
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
|
{{end}}
|
|
|
|
<form method="POST" action="{{if .Izmena}}/magacin/izmeni/{{.Artikal.ID}}{{else}}/magacin/novi{{end}}">
|
|
<div class="kolona" style="gap:14px;">
|
|
|
|
<!-- šifra i barkod -->
|
|
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
|
<div>
|
|
<label class="polje-labela">Šifra artikla</label>
|
|
<input type="text" name="sifra" id="sifra-input" value="{{.Artikal.Sifra}}"
|
|
placeholder="npr. KOMP-0001"
|
|
style="width:100%;font-family:monospace;">
|
|
<div style="font-size:11px;color:var(--tekst-slabi);margin-top:4px;">Ako ostaviš prazno, šifra se automatski dodeljuje.</div>
|
|
</div>
|
|
<div>
|
|
<label class="polje-labela">Barkod (EAN)</label>
|
|
<input type="text" name="barkod" value="{{.Artikal.Barkod}}"
|
|
placeholder="npr. 3830057592015"
|
|
style="width:100%;font-family:monospace;">
|
|
<div style="font-size:11px;color:var(--tekst-slabi);margin-top:4px;">Barkod sa pakovanja (opciono).</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- naziv / jedinica mere / kategorija — u 1 redu -->
|
|
<!-- Magacin je isključivo za proizvode (prate lager); usluge i troškovi
|
|
imaju svoje module, zato ovde tip više nije izbor -->
|
|
<input type="hidden" name="tip" value="proizvod">
|
|
<div style="display:grid;grid-template-columns:1fr 110px 1fr;gap:12px;">
|
|
<div>
|
|
<label class="polje-labela">
|
|
Naziv <span style="color:#dc2626;">*</span>
|
|
</label>
|
|
<input type="text" name="naziv" value="{{.Artikal.Naziv}}"
|
|
placeholder="npr. RAM DDR4 8GB Kingston"
|
|
style="width:100%;">
|
|
</div>
|
|
<div>
|
|
<label class="polje-labela">Jed. mere</label>
|
|
{{$jm := .Artikal.JedinicaMere}}
|
|
<select name="jedinica_mere" style="width:100%;">
|
|
<option value="kom" {{if eq $jm "kom"}}selected{{end}}>kom</option>
|
|
<option value="sat" {{if eq $jm "sat"}}selected{{end}}>sat</option>
|
|
<option value="set" {{if eq $jm "set"}}selected{{end}}>set</option>
|
|
<option value="m" {{if eq $jm "m"}}selected{{end}}>m</option>
|
|
<option value="m2" {{if eq $jm "m2"}}selected{{end}}>m²</option>
|
|
<option value="l" {{if eq $jm "l"}}selected{{end}}>l</option>
|
|
<option value="kg" {{if eq $jm "kg"}}selected{{end}}>kg</option>
|
|
<option value="pak" {{if eq $jm "pak"}}selected{{end}}>pak</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="polje-labela">Kategorija</label>
|
|
<select name="kategorija_id" style="width:100%;">
|
|
<option value="">— bez kategorije —</option>
|
|
{{range .Kategorije}}
|
|
<option value="{{.ID}}" {{if eq (printf "%d" .ID) $.KategorijaIDStr}}selected{{end}}>{{.Naziv}}</option>
|
|
{{end}}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- opis -->
|
|
<div>
|
|
<label class="polje-labela">Opis</label>
|
|
<textarea name="opis" rows="2"
|
|
placeholder="Kratak opis artikla..."
|
|
style="width:100%;resize:vertical;">{{.Artikal.Opis}}</textarea>
|
|
</div>
|
|
|
|
<!-- količina i minimum (samo za proizvode) -->
|
|
<div class="forma-grid-2" data-lager style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
|
<div>
|
|
<label class="polje-labela">Količina na stanju</label>
|
|
<input type="number" name="kolicina" value="{{.Artikal.Kolicina}}" min="0" style="width:100%;">
|
|
</div>
|
|
<div>
|
|
<label class="polje-labela">Minimalna količina</label>
|
|
<input type="number" name="kolicina_min" value="{{.Artikal.KolicinMin}}" min="0" style="width:100%;">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PDV stopa -->
|
|
{{if index .Moduli "pdv"}}
|
|
<div>
|
|
<label class="polje-labela">PDV stopa</label>
|
|
<select name="pdv_stopa" id="pdv-stopa"
|
|
style="width:100%;padding:8px 12px;background:var(--pozadina);border:0.5px solid var(--ivica);border-radius:8px;color:var(--tekst-glavni);font-size:14px;">
|
|
<option value="20" {{if eq .Artikal.PdvStopa 20.0}}selected{{end}}>Opšta stopa — 20%</option>
|
|
<option value="10" {{if eq .Artikal.PdvStopa 10.0}}selected{{end}}>Posebna stopa — 10%</option>
|
|
<option value="0" {{if eq .Artikal.PdvStopa 0.0}}selected{{end}}>Oslobođeno — 0%</option>
|
|
</select>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- cene: nabavna / prodajna / sa PDV -->
|
|
<div style="display:grid;grid-template-columns:1fr 1fr{{if index .Moduli "pdv"}} 1fr{{end}};gap:12px;">
|
|
<div>
|
|
<label class="polje-labela">Nabavna cena (din)</label>
|
|
<input type="number" id="nabavna-cena" name="nabavna_cena"
|
|
value="{{printf "%.2f" .Artikal.NabavnaCena}}" min="0" step="0.01" style="width:100%;">
|
|
</div>
|
|
<div>
|
|
<label class="polje-labela">Prodajna cena (din)</label>
|
|
<input type="number" id="prodajna-cena" name="prodajna_cena"
|
|
value="{{printf "%.2f" .Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;">
|
|
</div>
|
|
{{if index .Moduli "pdv"}}
|
|
<div>
|
|
<label class="polje-labela">Cena sa PDV-om (din)</label>
|
|
<input type="number" id="cena-sa-pdv" name="cena_sa_pdv"
|
|
value="{{printf "%.2f" .Artikal.CenaSaPdv}}" min="0" step="0.01" style="width:100%;">
|
|
</div>
|
|
{{end}}
|
|
</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>
|
|
|
|
<!-- dobavljači koji isporučuju artikal -->
|
|
{{if .Dobavljaci}}
|
|
<div>
|
|
<label class="polje-labela">Dobavljači</label>
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:6px;padding:10px 12px;border:0.5px solid var(--ivica);border-radius:8px;">
|
|
{{range .Dobavljaci}}
|
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;padding:4px 2px;">
|
|
<input type="checkbox" name="dobavljaci" value="{{.ID}}" {{if index $.IzabraniDobavljaci .ID}}checked{{end}} style="flex-shrink:0;">
|
|
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{.Naziv}}</span>
|
|
</label>
|
|
{{end}}
|
|
</div>
|
|
<div class="pomocni-tekst" style="margin-top:4px;">Određuje koje artikle dobavljač nudi pri nabavci.</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- lokacija (samo za proizvode) -->
|
|
<div data-lager>
|
|
<label class="polje-labela">Lokacija u magacinu</label>
|
|
<input type="text" name="lokacija" value="{{.Artikal.Lokacija}}"
|
|
placeholder="npr. Polica A3, Kutija 2..."
|
|
style="width:100%;">
|
|
</div>
|
|
|
|
<!-- napomena -->
|
|
<div>
|
|
<label class="polje-labela">Napomena</label>
|
|
<textarea name="napomena" rows="2"
|
|
placeholder="Interna napomena..."
|
|
style="width:100%;resize:vertical;">{{.Artikal.Napomena}}</textarea>
|
|
</div>
|
|
|
|
<!-- dugmad -->
|
|
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:6px;">
|
|
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/magacin" class="btn-sekundarno">Odustani</a>
|
|
<button type="submit" class="btn-primarno">
|
|
{{if .Izmena}}Sačuvaj izmene{{else}}Dodaj artikal{{end}}
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
// auto-predlog šifre pri promeni kategorije (samo za nov artikal i ako šifra nije ručno menjana)
|
|
var kat = document.querySelector('select[name="kategorija_id"]');
|
|
var sifra = document.getElementById('sifra-input');
|
|
var jeIzmena = {{if .Izmena}}true{{else}}false{{end}};
|
|
if (sifra) {
|
|
sifra.addEventListener('input', function () { sifra.dataset.rucno = '1'; });
|
|
}
|
|
if (kat && sifra && !jeIzmena) {
|
|
kat.addEventListener('change', function () {
|
|
if (sifra.dataset.rucno === '1' && sifra.value !== '') return;
|
|
fetch('/magacin/sledeca-sifra?kategorija=' + encodeURIComponent(kat.value))
|
|
.then(function (r) { return r.ok ? r.text() : ''; })
|
|
.then(function (t) { if (t) sifra.value = t; });
|
|
});
|
|
}
|
|
|
|
// dvosmerno povezivanje: nabavna + marža → prodajna, prodajna ↔ cena sa PDV
|
|
var nabavna = document.getElementById('nabavna-cena');
|
|
var prodajna = document.getElementById('prodajna-cena');
|
|
var saPdv = document.getElementById('cena-sa-pdv');
|
|
var pdvSel = document.getElementById('pdv-stopa');
|
|
var marza = document.querySelector('[name="marza"]');
|
|
|
|
function pdv() { return parseFloat(pdvSel.value) || 0; }
|
|
function br(el) { return parseFloat(el.value) || 0; }
|
|
|
|
// prodajna → cena sa PDV
|
|
function izProdajne() {
|
|
var p = br(prodajna);
|
|
if (p > 0) saPdv.value = (p * (1 + pdv() / 100)).toFixed(2);
|
|
izMarze();
|
|
}
|
|
// cena sa PDV → prodajna
|
|
function izSaPdv() {
|
|
var s = br(saPdv);
|
|
if (s > 0) prodajna.value = (s / (1 + pdv() / 100)).toFixed(2);
|
|
izMarze();
|
|
}
|
|
// nabavna + marža → prodajna (i onda prodajna → sa PDV)
|
|
function izMarzeKalc() {
|
|
var n = br(nabavna), m = br(marza);
|
|
if (n > 0 && marza.value !== '') {
|
|
prodajna.value = (n * (1 + m / 100)).toFixed(2);
|
|
izProdajne();
|
|
}
|
|
}
|
|
// prodajna + nabavna → marža
|
|
function izMarze() {
|
|
var n = br(nabavna), p = br(prodajna);
|
|
if (n > 0) marza.value = ((p / n - 1) * 100).toFixed(2);
|
|
}
|
|
// pri promeni PDV stope — preračunaj cena sa PDV iz prodajne
|
|
function izPdvStope() {
|
|
if (prodajna.value !== '') izProdajne();
|
|
}
|
|
|
|
if (nabavna && prodajna && saPdv && marza && pdvSel) {
|
|
prodajna.addEventListener('input', izProdajne);
|
|
saPdv.addEventListener('input', izSaPdv);
|
|
nabavna.addEventListener('input', izMarzeKalc);
|
|
marza.addEventListener('input', izMarzeKalc);
|
|
pdvSel.addEventListener('change', izPdvStope);
|
|
} else if (nabavna && prodajna && marza) {
|
|
// bez PDV modula — samo osnovna kalkulacija nabavna+marža↔prodajna
|
|
marza.addEventListener('input', function() {
|
|
var n = br(nabavna);
|
|
if (n > 0) prodajna.value = (n * (1 + br(marza) / 100)).toFixed(2);
|
|
});
|
|
nabavna.addEventListener('input', function() {
|
|
var n = br(nabavna);
|
|
if (n > 0 && marza.value !== '') prodajna.value = (n * (1 + br(marza) / 100)).toFixed(2);
|
|
});
|
|
prodajna.addEventListener('input', function() {
|
|
var n = br(nabavna);
|
|
if (n > 0) marza.value = ((br(prodajna) / n - 1) * 100).toFixed(2);
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
{{end}}
|