Popis: zakonske kolone (nabavna cena, vrednost, manjak/višak), tip popisa, datum i sačinilac

This commit is contained in:
2026-06-30 15:36:22 +02:00
parent c583472e3e
commit 28f7e30a3d
2 changed files with 185 additions and 35 deletions
+23 -6
View File
@@ -316,9 +316,11 @@ func (h *Handler) StanjeZalihaIzvestaj(w http.ResponseWriter, r *http.Request) {
// PodaciPopisa su podaci za stranicu popisa
type PodaciPopisa struct {
model.PodaciStranice
Artikli []model.ArtikalSaKategorijom
Sacuvano bool
Greska string
Artikli []model.ArtikalSaKategorijom
Sacuvano bool
Greska string
TipPopisa string // godišnji | vanredni | preuzimanje
DatumPopisa string // formatiran datum za prikaz
}
// Popis prikazuje formu za unos stvarnog stanja (inventuru)
@@ -327,7 +329,7 @@ func (h *Handler) Popis(w http.ResponseWriter, r *http.Request) {
return
}
artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{})
artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{Tip: model.TipProizvod})
if err != nil {
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
return
@@ -342,6 +344,8 @@ func (h *Handler) Popis(w http.ResponseWriter, r *http.Request) {
PodaciStranice: ps,
Artikli: artikli,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
TipPopisa: "godišnji",
DatumPopisa: time.Now().Format("02.01.2006."),
})
}
@@ -357,15 +361,26 @@ func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
return
}
artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{})
artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{Tip: model.TipProizvod})
if err != nil {
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
return
}
tipPopisa := r.FormValue("tip_popisa")
if tipPopisa == "" {
tipPopisa = "godišnji"
}
napomena := r.FormValue("napomena")
if napomena == "" {
napomena = "Godišnji popis"
switch tipPopisa {
case "vanredni":
napomena = "Vanredni popis"
case "preuzimanje":
napomena = "Popis pri preuzimanju dužnosti"
default:
napomena = "Godišnji popis"
}
}
var greskaBroj int
@@ -413,6 +428,8 @@ func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
PodaciStranice: ps,
Artikli: artikli,
Greska: fmt.Sprintf("Došlo je do greške pri upisivanju %d artikala.", greskaBroj),
TipPopisa: tipPopisa,
DatumPopisa: time.Now().Format("02.01.2006."),
})
return
}
+162 -29
View File
@@ -5,7 +5,7 @@
{{define "dodatni-css"}}
<style>
.popis-input {
width: 80px;
width: 90px;
text-align: right;
font-family: monospace;
font-size: 14px;
@@ -15,7 +15,7 @@
border-color: #f97316;
background: rgba(249,115,22,0.06);
}
.popis-razlika {
.popis-razlika, .popis-vrednost-razlika {
font-family: monospace;
font-size: 13px;
font-weight: 600;
@@ -23,6 +23,17 @@
text-align: right;
}
.red-kriticno td { background: rgba(220,38,38,0.04); }
.popis-suma {
display: flex;
gap: 24px;
flex-wrap: wrap;
font-size: 13px;
}
.popis-suma-stavka { display: flex; flex-direction: column; gap: 2px; }
.popis-suma-labela { color: var(--tekst-slabi); font-size: 12px; }
.popis-suma-vrednost { font-weight: 600; font-family: monospace; font-size: 14px; }
.boja-manjak { color: #dc2626; }
.boja-visak { color: #22c55e; }
</style>
{{end}}
@@ -40,11 +51,15 @@
<div class="kartica animiraj" style="padding:14px 18px;">
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
<div style="flex:1;min-width:260px;">
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);margin-bottom:4px;">Godišnji popis magacina</div>
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);margin-bottom:4px;">Popis magacina</div>
<div style="font-size:13px;color:var(--tekst-slabi);line-height:1.6;">
Unesite stvarno prebrojano stanje za svaki artikal. Polja sa razlikom biće istaknuta narandžasto.
Samo artikli sa izmenjenom količinom biće korigovani — ostali ostaju nepromenjeni.
</div>
<div style="font-size:12px;color:var(--tekst-slabi);margin-top:6px;">
Datum popisa: <strong style="color:var(--tekst-glavni);">{{.DatumPopisa}}</strong>
&nbsp;|&nbsp; Sačinio: <strong style="color:var(--tekst-glavni);">{{.KorisnikIme}}</strong>
</div>
</div>
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/izvestaji" class="btn-sekundarno" style="white-space:nowrap;">← Izveštaji</a>
</div>
@@ -58,9 +73,17 @@
<div style="padding:12px 20px;border-bottom:0.5px solid var(--ivica);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Artikli</span>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<div>
<label class="polje-labela" style="display:inline;margin-right:6px;">Tip:</label>
<select name="tip_popisa" id="tip-popisa" style="width:auto;height:auto;font-size:13px;padding:5px 28px 5px 8px;" onchange="azurirajNapomenu()">
<option value="godišnji"{{if eq .TipPopisa "godišnji"}} selected{{end}}>Godišnji</option>
<option value="vanredni"{{if eq .TipPopisa "vanredni"}} selected{{end}}>Vanredni</option>
<option value="preuzimanje"{{if eq .TipPopisa "preuzimanje"}} selected{{end}}>Preuzimanje dužnosti</option>
</select>
</div>
<div>
<label class="polje-labela" style="display:inline;margin-right:6px;">Napomena:</label>
<input type="text" name="napomena" value="Godišnji popis" style="width:200px;font-size:13px;padding:5px 8px;">
<input type="text" name="napomena" id="napomena-polje" value="Godišnji popis" style="width:220px;font-size:13px;padding:5px 8px;">
</div>
<button type="submit" class="btn-primarno">Sačuvaj popis</button>
</div>
@@ -72,15 +95,19 @@
</div>
{{else}}
<div style="overflow-x:auto;">
<table class="tabela">
<table class="tabela" id="popis-tabela">
<thead>
<tr>
<th>Artikal</th>
<th>Šifra</th>
<th>Kategorija</th>
<th style="text-align:right;">Knjižno stanje</th>
<th style="text-align:right;">Stvarno stanje</th>
<th style="text-align:right;">Razlika</th>
<th style="text-align:right;">JM</th>
<th style="text-align:right;">Nab. cena</th>
<th style="text-align:right;">Knj. kol.</th>
<th style="text-align:right;">Knj. vrednost</th>
<th style="text-align:right;">Stvarno kol.</th>
<th style="text-align:right;">Razlika kol.</th>
<th style="text-align:right;">Razlika vrednost</th>
</tr>
</thead>
<tbody>
@@ -89,7 +116,10 @@
<td style="font-weight:500;">{{.Naziv}}</td>
<td style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
<td style="font-size:12px;color:var(--tekst-slabi);">{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}</td>
<td style="text-align:right;font-size:12px;color:var(--tekst-slabi);">{{if .JedinicaMere}}{{.JedinicaMere}}{{else}}kom{{end}}</td>
<td style="text-align:right;font-family:monospace;font-size:13px;">{{dinariCeli .NabavnaCena}}</td>
<td style="text-align:right;font-family:monospace;font-weight:600;" id="knjizno-{{.ID}}">{{.Kolicina}}</td>
<td style="text-align:right;font-family:monospace;font-size:13px;" id="knjvred-{{.ID}}"></td>
<td style="text-align:right;">
<input type="number"
class="popis-input"
@@ -98,18 +128,54 @@
value="{{.Kolicina}}"
min="0"
data-knjizno="{{.Kolicina}}"
data-cena="{{printf "%.4f" .NabavnaCena}}"
data-id="{{.ID}}"
oninput="azurirajRazliku(this)">
oninput="azurirajRed(this)">
</td>
<td style="text-align:right;">
<span class="popis-razlika" id="razlika-{{.ID}}"></span>
</td>
<td style="text-align:right;">
<span class="popis-vrednost-razlika" id="vrazlika-{{.ID}}"></span>
</td>
</tr>
{{end}}
</tbody>
<tfoot>
<tr style="font-weight:600;background:var(--pozadina);">
<td colspan="5" style="text-align:right;font-size:12px;color:var(--tekst-slabi);">UKUPNO:</td>
<td style="text-align:right;font-family:monospace;" id="suma-knjizno-kol"></td>
<td style="text-align:right;font-family:monospace;" id="suma-knjizno-vred"></td>
<td style="text-align:right;font-family:monospace;" id="suma-stvarno-kol"></td>
<td style="text-align:right;font-family:monospace;" id="suma-razlika-kol"></td>
<td style="text-align:right;font-family:monospace;" id="suma-razlika-vred"></td>
</tr>
</tfoot>
</table>
</div>
<!-- sumarni pregled manjkova/viškova -->
<div style="padding:14px 20px;border-top:0.5px solid var(--ivica);">
<div class="popis-suma">
<div class="popis-suma-stavka">
<span class="popis-suma-labela">Manjkovi (kom)</span>
<span class="popis-suma-vrednost boja-manjak" id="sum-manjak-kol">0</span>
</div>
<div class="popis-suma-stavka">
<span class="popis-suma-labela">Manjkovi (vrednost)</span>
<span class="popis-suma-vrednost boja-manjak" id="sum-manjak-vred">0,00</span>
</div>
<div class="popis-suma-stavka">
<span class="popis-suma-labela">Viškovi (kom)</span>
<span class="popis-suma-vrednost boja-visak" id="sum-visak-kol">0</span>
</div>
<div class="popis-suma-stavka">
<span class="popis-suma-labela">Viškovi (vrednost)</span>
<span class="popis-suma-vrednost boja-visak" id="sum-visak-vred">0,00</span>
</div>
</div>
</div>
<!-- dugme na dnu -->
<div style="padding:14px 20px;border-top:0.5px solid var(--ivica);display:flex;justify-content:flex-end;">
<button type="submit" class="btn-primarno">Sačuvaj popis</button>
@@ -121,33 +187,100 @@
</div>
<script>
function azurirajRazliku(input) {
var id = input.dataset.id;
var napomePoTipu = {
"godišnji": "Godišnji popis",
"vanredni": "Vanredni popis",
"preuzimanje": "Popis pri preuzimanju dužnosti"
};
function azurirajNapomenu() {
var tip = document.getElementById('tip-popisa').value;
document.getElementById('napomena-polje').value = napomePoTipu[tip] || "Godišnji popis";
}
function fmt(n) {
return n.toLocaleString('sr-RS', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function azurirajRed(input) {
var id = input.dataset.id;
var knjizno = parseInt(input.dataset.knjizno, 10);
var cena = parseFloat(input.dataset.cena) || 0;
var stvarno = parseInt(input.value, 10);
var span = document.getElementById('razlika-' + id);
var spanKol = document.getElementById('razlika-' + id);
var spanVred = document.getElementById('vrazlika-' + id);
if (isNaN(stvarno)) {
span.textContent = '—';
span.style.color = '';
spanKol.textContent = '—'; spanVred.textContent = '—';
spanKol.style.color = ''; spanVred.style.color = '';
input.classList.remove('izmenjeno');
return;
}
var razlika = stvarno - knjizno;
if (razlika === 0) {
span.textContent = '—';
span.style.color = '';
input.classList.remove('izmenjeno');
} else if (razlika > 0) {
span.textContent = '+' + razlika;
span.style.color = '#22c55e';
input.classList.add('izmenjeno');
} else {
span.textContent = razlika;
span.style.color = '#dc2626';
input.classList.add('izmenjeno');
var razlika = stvarno - knjizno;
if (razlika === 0) {
spanKol.textContent = '—'; spanVred.textContent = '—';
spanKol.style.color = ''; spanVred.style.color = '';
input.classList.remove('izmenjeno');
} else {
var boja = razlika > 0 ? '#22c55e' : '#dc2626';
spanKol.textContent = (razlika > 0 ? '+' : '') + razlika;
spanVred.textContent = (razlika > 0 ? '+' : '') + fmt(razlika * cena);
spanKol.style.color = boja; spanVred.style.color = boja;
input.classList.add('izmenjeno');
}
}
azurirajSumu();
}
function azurirajSumu() {
var inputs = document.querySelectorAll('.popis-input');
var knjiznoKol = 0, knjiznoVred = 0, stvarnoKol = 0;
var manjKol = 0, manjVred = 0, visakKol = 0, visakVred = 0;
inputs.forEach(function(inp) {
var kn = parseInt(inp.dataset.knjizno, 10) || 0;
var cena = parseFloat(inp.dataset.cena) || 0;
var st = parseInt(inp.value, 10);
if (isNaN(st)) st = kn;
knjiznoKol += kn;
knjiznoVred += kn * cena;
stvarnoKol += st;
var r = st - kn;
if (r < 0) { manjKol += Math.abs(r); manjVred += Math.abs(r) * cena; }
if (r > 0) { visakKol += r; visakVred += r * cena; }
});
var razlikaKol = stvarnoKol - knjiznoKol;
var razlikaVred = visakVred - manjVred;
var el = function(id) { return document.getElementById(id); };
if (el('suma-knjizno-kol')) el('suma-knjizno-kol').textContent = knjiznoKol;
if (el('suma-knjizno-vred')) el('suma-knjizno-vred').textContent = fmt(knjiznoVred);
if (el('suma-stvarno-kol')) el('suma-stvarno-kol').textContent = stvarnoKol;
if (el('suma-razlika-kol')) {
el('suma-razlika-kol').textContent = (razlikaKol >= 0 ? '+' : '') + razlikaKol;
el('suma-razlika-kol').style.color = razlikaKol === 0 ? '' : (razlikaKol > 0 ? '#22c55e' : '#dc2626');
}
if (el('suma-razlika-vred')) {
el('suma-razlika-vred').textContent = (razlikaVred >= 0 ? '+' : '') + fmt(razlikaVred);
el('suma-razlika-vred').style.color = razlikaVred === 0 ? '' : (razlikaVred > 0 ? '#22c55e' : '#dc2626');
}
if (el('sum-manjak-kol')) el('sum-manjak-kol').textContent = manjKol;
if (el('sum-manjak-vred')) el('sum-manjak-vred').textContent = fmt(manjVred);
if (el('sum-visak-kol')) el('sum-visak-kol').textContent = visakKol;
if (el('sum-visak-vred')) el('sum-visak-vred').textContent = fmt(visakVred);
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.popis-input').forEach(function(inp) {
var id = inp.dataset.id;
var kn = parseInt(inp.dataset.knjizno, 10) || 0;
var cena = parseFloat(inp.dataset.cena) || 0;
var el = document.getElementById('knjvred-' + id);
if (el) el.textContent = fmt(kn * cena);
});
azurirajSumu();
});
</script>
{{end}}