Storno kroz modal + ispravka about:blank u fiskalnom tabu Prodaje
- Servis i Prodaja: storniranje naloga sad ide kroz modal — dugme 'Storniraj nalog' prvo traži potvrdu, tek onda otvara modal sa poljem za PIB/JMBG (kad je potrebno za refund); dugme 'Potvrdi storno' ostaje zaključano dok se polje ne popuni. Posle uspešnog storna modal se gasi i sadržaj stranice se osvežava (data-refresh=#sadrzaj). - Dnevni pazar (Fiskalni presek): link u sidebaru i sve tri rute (FiskalniPazar, ZakljuciFiskalniDan, FiskalniIzvestaj) sad proveravaju modul fiskalizacije, ne samo RBAC dozvolu i tehničku dostupnost PFR-a. - Prodaja: čekboks 'Prikaži fiskalni račun' se više ne nudi kad firma ne fiskalizuje, a pripremiFiskalniTab() više ne otvara prazan tab u tom slučaju — do sad je ostajao trajno na about:blank jer odredišna stranica nikad nije imala fiskalni račun da ga popuni. Dodata i odbrana za slučaj kad fiskalizacija jeste uključena ali PFR nije dostupan (zatvara osiroteli tab umesto about:blank). Detalji u docs/Greške.md (van git-a).
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"ntech/internal/config"
|
||||
ntechsqlite "ntech/internal/db/sqlite"
|
||||
"ntech/internal/fiskal"
|
||||
"ntech/internal/middleware"
|
||||
@@ -56,6 +57,12 @@ func (h *Handler) FiskalniPazar(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
podaci := PodaciFiskalniPazar{PodaciStranice: ps, Danas: time.Now().Format("2006-01-02")}
|
||||
|
||||
if !h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
podaci.Greska = "Fiskalizacija nije uključena u profilu firme — Podešavanja → Opšte → Pravni i poreski status."
|
||||
h.renderujTemplate(w, "fiskal_pazar", podaci)
|
||||
return
|
||||
}
|
||||
|
||||
klijent := h.fiskalKlijent()
|
||||
if klijent == nil {
|
||||
podaci.Greska = "Fiskalizacija nije podešena — unesi URL PFR servera u Podešavanja → Fiskalizacija."
|
||||
@@ -82,6 +89,11 @@ func (h *Handler) ZakljuciFiskalniDan(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "fiskal.zakljucenje"); !ok {
|
||||
return
|
||||
}
|
||||
if !h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije uključena u profilu firme.")
|
||||
http.Redirect(w, r, "/fiskal/pazar", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
klijent := h.fiskalKlijent()
|
||||
if klijent == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije podešena.")
|
||||
@@ -110,6 +122,11 @@ func (h *Handler) FiskalniIzvestaj(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije uključena u profilu firme.")
|
||||
http.Redirect(w, r, "/fiskal/pazar", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
klijent := h.fiskalKlijent()
|
||||
if klijent == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije podešena.")
|
||||
|
||||
@@ -226,6 +226,10 @@ document.addEventListener('alpine:init', () => {
|
||||
this.artikliOpcije = window._ntechArtikli || []
|
||||
this.klijenti = window._ntechKlijenti || []
|
||||
this.pdvObveznik = window._ntechPdvObveznik === true
|
||||
// ako firma ne fiskalizuje, ne otvaramo ni prazan tab u pripremiFiskalniTab() —
|
||||
// stranica detalja nikad neće imati FiskalniRacun da ga popuni, pa bi ostao
|
||||
// trajno na about:blank (v. docs/Greške.md)
|
||||
this.prikaziRacun = window._ntechFiskalizacija === true
|
||||
this.isMobile = window.matchMedia('(max-width: 768px)').matches
|
||||
window.matchMedia('(max-width: 768px)').addEventListener('change', e => {
|
||||
this.isMobile = e.matches
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
{{/* Knjigovodstvo — grupa: KIR/KPR/Obračun (modul „pdv") + Nivelacije (dozvola izmene cena) */}}
|
||||
{{$pdvVid := and (index .Moduli "pdv") (index .Dozvole "pdv.pregled")}}
|
||||
{{$kpoVid := and (index .Moduli "kpo") (index .Dozvole "kpo.pregled")}}
|
||||
{{$pazarVid := index .Dozvole "fiskal.pazar"}}
|
||||
{{$pazarVid := and (index .Moduli "fiskalizacija") (index .Dozvole "fiskal.pazar")}}
|
||||
{{$knjVid := or $pdvVid $kpoVid $pazarVid (index .Dozvole "artikal.izmeni")}}
|
||||
{{if $knjVid}}
|
||||
{{$knjAktivan := or (eq .Stranica "nivelacije") (eq .Stranica "kpo") (eq .Stranica "pdv-kir") (eq .Stranica "pdv-kpr") (eq .Stranica "pdv-obracun") (eq .Stranica "fiskal-pazar")}}
|
||||
|
||||
@@ -252,18 +252,58 @@
|
||||
Storno vraća sve artikle na stanje u magacinu. Nalog ostaje u evidenciji sa oznakom storniran.
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/prodaja/storno/{{.Nalog.ID}}" style="display:flex;flex-direction:column;gap:8px;align-items:flex-end;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja (opciono)" style="width:240px;">
|
||||
{{if .PotrebnaIdentKupca}}
|
||||
<input type="text" name="poreski_broj_kupca" placeholder="PIB ili JMBG kupca (obavezno za refundaciju)" required style="width:240px;">
|
||||
{{end}}
|
||||
<button type="submit" class="btn-upozorenje"
|
||||
data-potvrda="Stornirati nalog {{.Nalog.BrojNaloga}}?">
|
||||
<button type="button" class="btn-upozorenje" onclick="ntechPotvrdiPaOtvoriStorno(this, 'Stornirati nalog {{.Nalog.BrojNaloga}}?')">
|
||||
Storniraj nalog
|
||||
</button>
|
||||
<dialog class="premesti-modal" onclick="if(event.target===this)this.close()">
|
||||
<form method="dialog" class="premesti-zaglavlje">
|
||||
<h3>Storniranje naloga {{.Nalog.BrojNaloga}}</h3>
|
||||
<button type="submit" class="premesti-zatvori" aria-label="Zatvori">×</button>
|
||||
</form>
|
||||
<div style="display:flex;flex-direction:column;gap:10px;padding:16px;">
|
||||
<form method="POST" action="/prodaja/storno/{{.Nalog.ID}}" data-refresh="#sadrzaj" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<div>
|
||||
<label class="polje-labela">Razlog storniranja (opciono)</label>
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja" style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
{{if .PotrebnaIdentKupca}}
|
||||
<div>
|
||||
<label class="polje-labela">PIB ili JMBG kupca</label>
|
||||
<input type="text" id="storno-pib" name="poreski_broj_kupca" placeholder="Obavezno za fiskalni refund" required
|
||||
style="width:100%;padding:8px 12px;border:1px solid #dc2626;border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
{{end}}
|
||||
<button type="submit" id="btn-storno-potvrdi" class="btn-upozorenje" style="width:100%;{{if .PotrebnaIdentKupca}}opacity:0.4;cursor:not-allowed;{{end}}" {{if .PotrebnaIdentKupca}}disabled{{end}}>
|
||||
Potvrdi storno
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
// isti obrazac kao u Servisu (servis_detalji.html) — v. komentar tamo zašto
|
||||
// se NE koristi ntechOtvoriModal (auto-submit na zatvaranje dialoga).
|
||||
window.ntechPotvrdiPaOtvoriStorno = function (btn, poruka) {
|
||||
window._ntechPotvrdi(poruka).then(function (ok) {
|
||||
if (!ok) return;
|
||||
var d = btn.nextElementSibling;
|
||||
if (d && d.tagName === 'DIALOG') d.showModal();
|
||||
});
|
||||
};
|
||||
var pib = document.getElementById('storno-pib');
|
||||
var btnPotvrdi = document.getElementById('btn-storno-potvrdi');
|
||||
if (pib && btnPotvrdi) {
|
||||
pib.addEventListener('input', function () {
|
||||
var ok = pib.value.trim() !== '';
|
||||
btnPotvrdi.disabled = !ok;
|
||||
btnPotvrdi.style.opacity = ok ? '1' : '0.4';
|
||||
btnPotvrdi.style.cursor = ok ? 'pointer' : 'not-allowed';
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{if .FiskalniRacun}}
|
||||
@@ -296,6 +336,18 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{else}}
|
||||
<script>
|
||||
(function(){
|
||||
// fiskalizacija je pokušana (racun=1 tražen pri naplati) ali računa nema —
|
||||
// PFR nedostupan ili slično (v. FiskalGreska ispod). Prazan tab je već otvoren
|
||||
// pre redirekta — zatvori ga umesto da ostane trajno na about:blank.
|
||||
if (new URLSearchParams(location.search).get('racun') === '1') {
|
||||
var t = window.open('', 'ntech-fiskalni-tab');
|
||||
if (t) t.close();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{if .FiskalGreska}}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
var _ntechArtikli = {{.ArtikliJSON}};
|
||||
var _ntechKlijenti = {{.KlijentiJSON}};
|
||||
var _ntechPdvObveznik = {{if index .Moduli "pdv"}}true{{else}}false{{end}};
|
||||
var _ntechFiskalizacija = {{if index .Moduli "fiskalizacija"}}true{{else}}false{{end}};
|
||||
</script>
|
||||
|
||||
<div style="width: 100%" x-data="prodajaForma">
|
||||
|
||||
@@ -214,13 +214,27 @@
|
||||
f.addEventListener('submit', function(){
|
||||
var cb = f.querySelector('#prikazi-racun');
|
||||
if (!cb || !cb.checked) return;
|
||||
// otvori prazan imenovani tab SINHRONO unutar submit-a (očuvan user
|
||||
// gesture) — isti obrazac kao pripremiFiskalniTab u Prodaji (ntech.js).
|
||||
// Naplata ovde ide preko AJAX-a (data-refresh), pa se popunjava tek
|
||||
// kad MutationObserver javi da je swap gotov — a to je već van user
|
||||
// gesture-a, pa direktan window.open() u tom trenutku brauzer tretira
|
||||
// kao blokiran popup i otvara about:blank (v. docs/Greške.md).
|
||||
var tab = window.open('', 'ntech-fiskalni-tab');
|
||||
var sadrzaj = document.getElementById('sadrzaj');
|
||||
if (!sadrzaj) return;
|
||||
if (!sadrzaj) {
|
||||
if (tab) tab.close();
|
||||
return;
|
||||
}
|
||||
var observer = new MutationObserver(function(){
|
||||
observer.disconnect();
|
||||
// jedan prozor — server u istu stranicu ubacuje i povraćaj viška
|
||||
// avansa (ako postoji), da brauzer ne blokira drugi popup
|
||||
if (tab && !tab.closed) {
|
||||
tab.location.href = '/servis/{{.Nalog.ID}}/fiskalni-racun';
|
||||
} else {
|
||||
window.open('/servis/{{.Nalog.ID}}/fiskalni-racun', '_blank');
|
||||
}
|
||||
});
|
||||
observer.observe(sadrzaj.parentElement, {childList: true});
|
||||
});
|
||||
@@ -1124,19 +1138,63 @@
|
||||
Storno vraća ugrađene delove na stanje u magacinu. Nalog ostaje u evidenciji sa oznakom storniran.
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/servis/storno/{{.Nalog.ID}}" style="display:flex;flex-direction:column;gap:8px;align-items:flex-end;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja (opciono)" style="width:240px;">
|
||||
{{if .PotrebnaIdentKupca}}
|
||||
<input type="text" name="poreski_broj_kupca" placeholder="PIB ili JMBG kupca (obavezno za refundaciju)" required style="width:240px;">
|
||||
{{end}}
|
||||
<button type="submit" class="btn-upozorenje"
|
||||
data-potvrda="Stornirati nalog {{.Nalog.BrojNaloga}}?">
|
||||
<button type="button" class="btn-upozorenje" onclick="ntechPotvrdiPaOtvoriStorno(this, 'Stornirati nalog {{.Nalog.BrojNaloga}}?')">
|
||||
Storniraj nalog
|
||||
</button>
|
||||
<dialog class="premesti-modal" onclick="if(event.target===this)this.close()">
|
||||
<form method="dialog" class="premesti-zaglavlje">
|
||||
<h3>Storniranje naloga {{.Nalog.BrojNaloga}}</h3>
|
||||
<button type="submit" class="premesti-zatvori" aria-label="Zatvori">×</button>
|
||||
</form>
|
||||
<div style="display:flex;flex-direction:column;gap:10px;padding:16px;">
|
||||
<form method="POST" action="/servis/storno/{{.Nalog.ID}}" data-refresh="#sadrzaj" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<div>
|
||||
<label class="polje-labela">Razlog storniranja (opciono)</label>
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja" style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
{{if .PotrebnaIdentKupca}}
|
||||
<div>
|
||||
<label class="polje-labela">PIB ili JMBG kupca</label>
|
||||
<input type="text" id="storno-pib" name="poreski_broj_kupca" placeholder="Obavezno za fiskalni refund" required
|
||||
style="width:100%;padding:8px 12px;border:1px solid #dc2626;border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
{{end}}
|
||||
<button type="submit" id="btn-storno-potvrdi" class="btn-upozorenje" style="width:100%;{{if .PotrebnaIdentKupca}}opacity:0.4;cursor:not-allowed;{{end}}" {{if .PotrebnaIdentKupca}}disabled{{end}}>
|
||||
Potvrdi storno
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
// storno: potvrda namere PRE otvaranja modala (dvostepeni tok), pa tek onda
|
||||
// polje za PIB/JMBG (obavezno samo ako je nalog fiskalizovan — v. .PotrebnaIdentKupca).
|
||||
// NE koristi ntechOtvoriModal ovde — ta funkcija snima početno stanje forme i
|
||||
// auto-submituje na zatvaranje dialoga (za popup polja bez dugmeta Sačuvaj), što
|
||||
// bi za storno značilo da se nalog stornira i pri slučajnom zatvaranju bez klika
|
||||
// na „Potvrdi storno".
|
||||
window.ntechPotvrdiPaOtvoriStorno = function (btn, poruka) {
|
||||
window._ntechPotvrdi(poruka).then(function (ok) {
|
||||
if (!ok) return;
|
||||
var d = btn.nextElementSibling;
|
||||
if (d && d.tagName === 'DIALOG') d.showModal();
|
||||
});
|
||||
};
|
||||
var pib = document.getElementById('storno-pib');
|
||||
var btnPotvrdi = document.getElementById('btn-storno-potvrdi');
|
||||
if (pib && btnPotvrdi) {
|
||||
pib.addEventListener('input', function () {
|
||||
var ok = pib.value.trim() !== '';
|
||||
btnPotvrdi.disabled = !ok;
|
||||
btnPotvrdi.style.opacity = ok ? '1' : '0.4';
|
||||
btnPotvrdi.style.cursor = ok ? 'pointer' : 'not-allowed';
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<!-- zona za brisanje -->
|
||||
|
||||
Reference in New Issue
Block a user