Prodaja: skener barkoda, prilagodjeni dropdown artikala, ispravka cena sa/bez PDV-a po rezimu
This commit is contained in:
@@ -197,7 +197,8 @@ func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavk
|
||||
}
|
||||
|
||||
// provera stanja, smanjenje i insert stavki
|
||||
for _, s := range stavke {
|
||||
for i := range stavke {
|
||||
s := stavke[i]
|
||||
var naziv, tip string
|
||||
var stanjePre int
|
||||
err := tx.QueryRowContext(ctx,
|
||||
@@ -230,14 +231,18 @@ func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavk
|
||||
}
|
||||
ukupnoStavke := float64(s.Kolicina) * cenaPoslePopusta
|
||||
|
||||
// CenaPoKomadu je neto (bez PDV); PDV se dodaje naviše.
|
||||
// cena_bez_pdv/pdv_iznos moraju biti diskontovani da ostanu konzistentni sa ukupno.
|
||||
// CenaPoKomadu je cena za naplatu: kod punog PDV obveznika bruto (sa PDV-om,
|
||||
// PDV se izdvaja naniže), kod firme na evidenciji nema PDV-a (PdvStopa=0, pa je bez razlike).
|
||||
cenaBezPdv := s.CenaBezPdv
|
||||
pdvIznos := s.PdvIznos
|
||||
if cenaBezPdv == 0 {
|
||||
cenaBezPdv = cenaPoslePopusta
|
||||
pdvIznos = cenaBezPdv * s.PdvStopa / 100
|
||||
cenaBezPdv = cenaPoslePopusta / (1 + s.PdvStopa/100)
|
||||
pdvIznos = cenaPoslePopusta - cenaBezPdv
|
||||
}
|
||||
// vraćamo izračunate vrednosti u slajs pozivaoca (npr. za auto-upis u KIR posle Kreiraj)
|
||||
stavke[i].CenaBezPdv = cenaBezPdv
|
||||
stavke[i].PdvIznos = pdvIznos
|
||||
stavke[i].Ukupno = ukupnoStavke
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO stavke_prodaje
|
||||
(nalog_id, artikal_id, kolicina, cena_po_komadu, popust_procenat, ukupno, pdv_stopa, pdv_iznos, cena_bez_pdv)
|
||||
@@ -443,9 +448,10 @@ func (r *ProdajaRepo) DnevniPrometMaloprodaje(ctx context.Context, datum string)
|
||||
return p, fmt.Errorf("ntech: ProdajaRepo.DnevniPrometMaloprodaje: count: %w", err)
|
||||
}
|
||||
|
||||
// stavke grupisane po PDV stopi
|
||||
// stavke grupisane po PDV stopi — osnovica i PDV se čitaju iz već izračunatih
|
||||
// cena_bez_pdv/pdv_iznos kolona (ukupno je bruto iznos za naplatu, ne osnovica)
|
||||
redovi, err := r.db.QueryContext(ctx, `
|
||||
SELECT s.pdv_stopa, SUM(s.ukupno), SUM(s.ukupno * s.pdv_stopa / 100.0)
|
||||
SELECT s.pdv_stopa, SUM(s.cena_bez_pdv * s.kolicina), SUM(s.pdv_iznos * s.kolicina)
|
||||
FROM stavke_prodaje s
|
||||
JOIN prodajni_nalozi p ON p.id = s.nalog_id
|
||||
WHERE p.klijent_id IS NULL AND p.stornirano = 0 AND DATE(p.datum) = ?
|
||||
|
||||
@@ -81,7 +81,8 @@ func TestProdajaKreiraj_NedovoljnoStanja(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestProdajaKreiraj_PdvAutoKalkulacija: kada CenaBezPdv=0 a PdvStopa>0,
|
||||
// cena_bez_pdv i pdv_iznos se automatski računaju iz neto cene (CenaPoKomadu je neto).
|
||||
// cena_bez_pdv i pdv_iznos se automatski izdvajaju naniže iz bruto cene
|
||||
// (CenaPoKomadu je kod punog PDV obveznika cena za naplatu, sa PDV-om).
|
||||
func TestProdajaKreiraj_PdvAutoKalkulacija(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
baza := testDB(t)
|
||||
@@ -92,13 +93,14 @@ func TestProdajaKreiraj_PdvAutoKalkulacija(t *testing.T) {
|
||||
Naziv: "Tastatura", Tip: model.TipProizvod, Kolicina: 10,
|
||||
})
|
||||
|
||||
// neto cena 1000, PDV 20% → pdv_iznos = 200
|
||||
_, err := prodRepo.Kreiraj(ctx, &model.ProdajniNalog{
|
||||
BrojNaloga: "PR-PDV-001", Ukupno: 1000, NacinPlacanja: "gotovina", Datum: time.Now(),
|
||||
}, []model.StavkaProdaje{
|
||||
{ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 1000, PdvStopa: 20},
|
||||
// bruto cena 1200, PDV 20% → cena_bez_pdv = 1000, pdv_iznos = 200
|
||||
stavke := []model.StavkaProdaje{
|
||||
{ArtikalID: artID, Kolicina: 1, CenaPoKomadu: 1200, PdvStopa: 20},
|
||||
// CenaBezPdv=0 namerno — treba da se auto-izračuna
|
||||
}, nil)
|
||||
}
|
||||
_, err := prodRepo.Kreiraj(ctx, &model.ProdajniNalog{
|
||||
BrojNaloga: "PR-PDV-001", Ukupno: 1200, NacinPlacanja: "gotovina", Datum: time.Now(),
|
||||
}, stavke, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Kreiraj: %v", err)
|
||||
}
|
||||
|
||||
@@ -105,8 +105,9 @@ func (k *PdvKir) DodajNeto(osnovica, stopa float64) {
|
||||
}
|
||||
|
||||
// KirIzProdaje gradi KIR zapis iz prodaje: stavke se grupišu po PDV stopi
|
||||
// (20→opšta, 10→posebna, ostalo→oslobođeno). CenaPoKomadu je prodajna cena BEZ PDV
|
||||
// (neto), PDV se dodaje naviše — isti obrazac kao kod servisnih radova/delova.
|
||||
// (20→opšta, 10→posebna, ostalo→oslobođeno). Koristi već izračunatu CenaBezPdv
|
||||
// (neto, popust je već uračunat pri upisu stavke — v. ProdajaRepo.Kreiraj), jer je
|
||||
// CenaPoKomadu kod punog PDV obveznika bruto (cena za naplatu), ne neto.
|
||||
// DatumPrometa je datum same prodaje (nalog.Datum); DatumKnjizenja je datum kada se
|
||||
// ovaj KIR zapis stvarno upisuje u knjigu — za prodaju upisanu odmah to je isti trenutak,
|
||||
// ali za naknadni backfill starih naloga upis kasni za periodom pa se prosleđuje posebno.
|
||||
@@ -123,11 +124,7 @@ func KirIzProdaje(nalog ProdajniNalog, stavke []StavkaProdaje, kupacNaziv, kupac
|
||||
IzvorID: &id,
|
||||
}
|
||||
for _, s := range stavke {
|
||||
cenaPoslePopusta := s.CenaPoKomadu
|
||||
if s.PopustProcenat > 0 {
|
||||
cenaPoslePopusta = cenaPoslePopusta * (1 - s.PopustProcenat/100)
|
||||
}
|
||||
k.DodajNeto(float64(s.Kolicina)*cenaPoslePopusta, s.PdvStopa)
|
||||
k.DodajNeto(float64(s.Kolicina)*s.CenaBezPdv, s.PdvStopa)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ func blizu(a, b float64) bool { return math.Abs(a-b) < 0.01 }
|
||||
func TestKirIzProdaje(t *testing.T) {
|
||||
nalog := ProdajniNalog{ID: 5, BrojNaloga: "P-1", Datum: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)}
|
||||
stavke := []StavkaProdaje{
|
||||
// 20%: CenaPoKomadu je NETO → osnovica 2×120=240, PDV 240×20%=48
|
||||
{Kolicina: 2, CenaPoKomadu: 120, PdvStopa: 20},
|
||||
// 20%: CenaBezPdv je već izračunata neto vrednost (v. ProdajaRepo.Kreiraj) → osnovica 2×120=240, PDV 240×20%=48
|
||||
{Kolicina: 2, CenaBezPdv: 120, PdvStopa: 20},
|
||||
// 10%: osnovica 1×110=110, PDV 110×10%=11
|
||||
{Kolicina: 1, CenaPoKomadu: 110, PdvStopa: 10},
|
||||
{Kolicina: 1, CenaBezPdv: 110, PdvStopa: 10},
|
||||
// 0%: 1 × 50 = 50 (oslobođeno, bez PDV)
|
||||
{Kolicina: 1, CenaPoKomadu: 50, PdvStopa: 0},
|
||||
{Kolicina: 1, CenaBezPdv: 50, PdvStopa: 0},
|
||||
}
|
||||
|
||||
k := KirIzProdaje(nalog, stavke, "Kupac doo", "123456789", "Niš", nalog.Datum)
|
||||
|
||||
+89
-8
@@ -136,7 +136,7 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
// forma za prodaju
|
||||
Alpine.data('prodajaForma', () => ({
|
||||
stavke: [{artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: 20, popust: 0, kategorija_naziv: ''}],
|
||||
stavke: [{artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: 20, popust: 0, kategorija_naziv: '', _naziv: '', _pretraga: '', _prikaziListu: false, _limit: 30}],
|
||||
artikliOpcije: [],
|
||||
pretragaArtikal: '',
|
||||
pdvObveznik: true,
|
||||
@@ -225,31 +225,89 @@ document.addEventListener('alpine:init', () => {
|
||||
this.isMobile = e.matches
|
||||
})
|
||||
},
|
||||
get filtriraniArtikli() {
|
||||
const q = this.pretragaArtikal.trim().toLowerCase()
|
||||
if (!q) return this.artikliOpcije.slice(0, 50)
|
||||
// _filtriraniArtikliSvi vraća SVE opcije koje odgovaraju pretrazi jedne stavke (bez ograničenja),
|
||||
// koristi ga i filtriraniArtikliZaStavku (za prikaz) i imaJosArtikala (da zna da li dogruzati još)
|
||||
_filtriraniArtikliSvi(stavka) {
|
||||
const q = (stavka._pretraga || '').trim().toLowerCase()
|
||||
if (!q) return this.artikliOpcije
|
||||
return this.artikliOpcije.filter(a =>
|
||||
(a.naziv || '').toLowerCase().includes(q) ||
|
||||
(a.sifra || '').toLowerCase().includes(q) ||
|
||||
(a.barkod || '').toLowerCase().includes(q)
|
||||
).slice(0, 30)
|
||||
)
|
||||
},
|
||||
// filtriraniArtikliZaStavku vraća opcije za prilagođeni dropdown izbora artikla jedne stavke
|
||||
// (svaka stavka ima svoju nezavisnu pretragu i limit, poput pretrage klijenta)
|
||||
filtriraniArtikliZaStavku(stavka) {
|
||||
return this._filtriraniArtikliSvi(stavka).slice(0, stavka._limit || 30)
|
||||
},
|
||||
imaJosArtikala(stavka) {
|
||||
return (stavka._limit || 30) < this._filtriraniArtikliSvi(stavka).length
|
||||
},
|
||||
ucitajViseArtikala(stavka, e) {
|
||||
const el = e.target
|
||||
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 40 && this.imaJosArtikala(stavka)) {
|
||||
stavka._limit = (stavka._limit || 30) + 30
|
||||
}
|
||||
},
|
||||
dodajStavku() {
|
||||
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: this.pdvObveznik ? 20 : 0, popust: 0, kategorija_naziv: ''})
|
||||
// ne dozvoli gomilanje praznih redova — ako poslednja stavka nema izabran artikal, samo je otvori
|
||||
const poslednja = this.stavke[this.stavke.length - 1]
|
||||
if (poslednja && !poslednja.artikal_id) {
|
||||
poslednja._prikaziListu = true
|
||||
return
|
||||
}
|
||||
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: this.pdvObveznik ? 20 : 0, popust: 0, kategorija_naziv: '', _naziv: '', _pretraga: '', _prikaziListu: false, _limit: 30})
|
||||
},
|
||||
ukloniStavku(i) {
|
||||
if (this.stavke.length > 1) this.stavke.splice(i, 1)
|
||||
},
|
||||
izaberiArtikal(stavka, a) {
|
||||
stavka.artikal_id = a.id
|
||||
stavka._naziv = a.naziv
|
||||
stavka._pretraga = ''
|
||||
stavka._prikaziListu = false
|
||||
stavka._limit = 30
|
||||
this.popuniCenu(stavka)
|
||||
},
|
||||
promeniArtikal(stavka) {
|
||||
stavka.artikal_id = ''
|
||||
stavka._naziv = ''
|
||||
stavka._pretraga = ''
|
||||
stavka._limit = 30
|
||||
stavka.kategorija_naziv = ''
|
||||
stavka.cena = 0
|
||||
stavka.cena_sa_pdv = 0
|
||||
stavka.pdv_stopa = this.pdvObveznik ? 20 : 0
|
||||
},
|
||||
dodajPoBarkodu() {
|
||||
const kod = this.pretragaArtikal.trim()
|
||||
if (!kod) return
|
||||
const a = this.artikliOpcije.find(x => x.barkod && x.barkod === kod)
|
||||
if (!a) return
|
||||
this.pretragaArtikal = ''
|
||||
const postojeca = this.stavke.find(s => s.artikal_id == a.id)
|
||||
if (postojeca) {
|
||||
postojeca.kolicina = (parseInt(postojeca.kolicina) || 0) + 1
|
||||
return
|
||||
}
|
||||
const prazna = this.stavke.find(s => !s.artikal_id)
|
||||
const stavka = prazna || this.stavke[this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: this.pdvObveznik ? 20 : 0, popust: 0, kategorija_naziv: '', _naziv: '', _pretraga: '', _prikaziListu: false, _limit: 30}) - 1]
|
||||
this.izaberiArtikal(stavka, a)
|
||||
},
|
||||
popuniCenu(stavka) {
|
||||
const a = this.artikliOpcije.find(x => x.id == stavka.artikal_id)
|
||||
if (a) {
|
||||
stavka.cena_sa_pdv = a.cena_sa_pdv || 0
|
||||
stavka.kategorija_naziv = a.kategorija_naziv || ''
|
||||
stavka._naziv = a.naziv
|
||||
if (this.pdvObveznik) {
|
||||
stavka.cena = a.cena || 0
|
||||
// pun PDV obveznik naplaćuje cenu sa PDV-om (bruto) — PDV se izdvaja naniže
|
||||
stavka.cena = a.cena_sa_pdv || a.cena || 0
|
||||
stavka.pdv_stopa = a.pdv_stopa !== undefined ? a.pdv_stopa : 20
|
||||
} else {
|
||||
stavka.cena = a.cena_sa_pdv || a.cena || 0
|
||||
// firma na evidenciji ne obračunava PDV — naplaćuje se čista prodajna cena
|
||||
stavka.cena = a.cena || 0
|
||||
stavka.pdv_stopa = 0
|
||||
}
|
||||
}
|
||||
@@ -271,6 +329,9 @@ document.addEventListener('alpine:init', () => {
|
||||
imaPrekoracenja() {
|
||||
return this.stavke.some((_, i) => this.prekoracenje(i))
|
||||
},
|
||||
imaPraznihStavki() {
|
||||
return this.stavke.some(s => !s.artikal_id)
|
||||
},
|
||||
ukupnoStavke(s) {
|
||||
const cena = parseFloat(s.cena) || 0
|
||||
const popust = parseFloat(s.popust) || 0
|
||||
@@ -285,6 +346,26 @@ document.addEventListener('alpine:init', () => {
|
||||
return z + (parseFloat(s.kolicina) * cenaPosle || 0)
|
||||
}, 0).toFixed(2)
|
||||
},
|
||||
// pdvIznosStavke vraća PDV sadržan u ceni po komadu (cena je bruto — PDV se izdvaja naniže)
|
||||
pdvIznosStavke(s) {
|
||||
const cena = parseFloat(s.cena) || 0
|
||||
const stopa = parseFloat(s.pdv_stopa) || 0
|
||||
if (!stopa) return 0
|
||||
return cena - cena / (1 + stopa / 100)
|
||||
},
|
||||
osnovicaSvega() {
|
||||
return this.stavke.reduce((z, s) => {
|
||||
const cena = parseFloat(s.cena) || 0
|
||||
const popust = parseFloat(s.popust) || 0
|
||||
const cenaPosle = popust > 0 ? cena * (1 - popust/100) : cena
|
||||
const stopa = parseFloat(s.pdv_stopa) || 0
|
||||
const neto = stopa > 0 ? cenaPosle / (1 + stopa / 100) : cenaPosle
|
||||
return z + (parseFloat(s.kolicina) * neto || 0)
|
||||
}, 0).toFixed(2)
|
||||
},
|
||||
pdvSvega() {
|
||||
return (parseFloat(this.ukupnoSvega()) - parseFloat(this.osnovicaSvega())).toFixed(2)
|
||||
},
|
||||
// fmtDin formatira broj sa separatorom hiljada (tačka) i 2 decimale (zarez): 1234567.5 → "1.234.567,50"
|
||||
fmtDin(v) {
|
||||
const n = parseFloat(v) || 0
|
||||
|
||||
@@ -112,19 +112,19 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- pretraga artikala -->
|
||||
<!-- pretraga artikala / skener barkoda -->
|
||||
<div style="margin-bottom:12px;">
|
||||
<input type="text" x-model="pretragaArtikal" placeholder="Pretraži artikle po nazivu, šifri ili barkodu..." style="width:100%;">
|
||||
<input type="text" x-model="pretragaArtikal" @keydown.enter.prevent="dodajPoBarkodu()" placeholder="Pretraži artikle po nazivu, šifri, barkodu ili skeniraj barkod..." autocomplete="off" style="width:100%;">
|
||||
</div>
|
||||
|
||||
<!-- desktop tabela stavki -->
|
||||
<div class="stavke-tabela-wrapper" style="overflow-x: auto">
|
||||
<div class="stavke-tabela-wrapper">
|
||||
<table class="tabela">
|
||||
<thead>
|
||||
<tr style="border-bottom: 0.5px solid var(--ivica)">
|
||||
<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;">Kol.</th>
|
||||
<th style="padding: 8px 10px; text-align: right; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni); width: 130px;">Cena/kom</th>
|
||||
<th style="padding: 8px 10px; text-align: right; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni); width: 130px;">Cena/kom{{if index .Moduli "pdv"}} (sa PDV){{end}}</th>
|
||||
<th style="padding: 8px 10px; text-align: right; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni); width: 80px;">Popust%</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>
|
||||
@@ -133,15 +133,37 @@
|
||||
<tbody>
|
||||
<template x-for="(stavka, i) in stavke" :key="i">
|
||||
<tr style="border-bottom: 0.5px solid var(--ivica)">
|
||||
<td style="padding: 8px 10px;">
|
||||
<td style="padding: 8px 10px; position: relative;">
|
||||
<input type="hidden" :name="'pdv_stopa[]'" :value="stavka.pdv_stopa">
|
||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id" @change="popuniCenu(stavka)" :disabled="isMobile" style="width: 100%">
|
||||
<option value="">— odaberi artikal —</option>
|
||||
<template x-for="a in filtriraniArtikli" :key="a.id">
|
||||
<option :value="a.id" x-text="a.naziv + (a.kategorija_naziv ? ' [' + a.kategorija_naziv + ']' : '')"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div x-show="stavka.kategorija_naziv" style="font-size:11px;color:var(--tekst-sporedni);margin-top:2px;" x-text="stavka.kategorija_naziv"></div>
|
||||
<input type="hidden" :name="'artikal_id[]'" :value="stavka.artikal_id" :disabled="isMobile">
|
||||
<template x-if="stavka.artikal_id">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:6px;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" x-text="stavka._naziv"></span>
|
||||
<button type="button" @click="promeniArtikal(stavka)" :disabled="isMobile" title="Promeni artikal"
|
||||
style="background:none;border:none;cursor:pointer;color:var(--tekst-sporedni);font-size:13px;flex-shrink:0;padding:0;">✎</button>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!stavka.artikal_id">
|
||||
<div @click.outside="stavka._prikaziListu = false">
|
||||
<input type="text" x-model="stavka._pretraga" @focus="stavka._prikaziListu = true" @input="stavka._prikaziListu = true"
|
||||
placeholder="Pretraži artikal..." autocomplete="off" :disabled="isMobile" style="width: 100%">
|
||||
<div x-show="stavka._prikaziListu && filtriraniArtikliZaStavku(stavka).length" x-cloak
|
||||
@scroll="ucitajViseArtikala(stavka, $event)"
|
||||
style="position: absolute; z-index: 40; left: 8px; right: 8px; margin-top: 4px; background: var(--pozadina); border: 0.5px solid var(--ivica); border-radius: 10px; max-height: 240px; overflow: auto; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);">
|
||||
<template x-for="a in filtriraniArtikliZaStavku(stavka)" :key="a.id">
|
||||
<div @click="izaberiArtikal(stavka, a)" style="display:flex;justify-content:space-between;gap:10px;padding:8px 12px;cursor:pointer;border-bottom:0.5px solid var(--ivica);">
|
||||
<div style="min-width:0;">
|
||||
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" x-text="a.naziv"></div>
|
||||
<div style="font-size:11px;color:var(--tekst-sporedni);" x-text="a.kategorija_naziv"></div>
|
||||
</div>
|
||||
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);white-space:nowrap;" x-text="fmtDin(pdvObveznik ? a.cena_sa_pdv : a.cena) + ' din'"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="imaJosArtikala(stavka)" style="padding: 6px 12px; text-align: center; font-size: 11px; color: var(--tekst-sporedni)">Skrolujte za još rezultata...</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="stavka.kategorija_naziv" style="font-size:11px;color:var(--tekst-sporedni);background:var(--pozadina);padding:2px 8px;border-radius:4px;display:inline-block;margin-top:4px;" x-text="stavka.kategorija_naziv"></div>
|
||||
</td>
|
||||
<td style="padding: 8px 10px">
|
||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" min="1" :disabled="isMobile"
|
||||
@@ -151,9 +173,9 @@
|
||||
<div x-show="prekoracenje(i)" style="font-size: 11px; color: #dc2626; margin-top: 1px; text-align: center; white-space: nowrap;">Prekoračena količina</div>
|
||||
</td>
|
||||
<td style="padding: 8px 10px">
|
||||
<div style="display:flex;flex-direction:column;gap:2px;">
|
||||
<div style="display:flex;flex-direction:column;gap:4px;align-items:flex-end;">
|
||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="isMobile" style="width: 100%; text-align: right" />
|
||||
<span x-show="pdvObveznik && stavka.pdv_stopa > 0" style="font-size:10px;color:var(--tekst-sporedni);text-align:right;" x-text="'+PDV ' + stavka.pdv_stopa + '%'"></span>
|
||||
<span x-show="pdvObveznik && stavka.pdv_stopa > 0" style="font-size:10px;color:var(--tekst-sporedni);background:var(--pozadina);padding:1px 6px;border-radius:4px;" x-text="'PDV ' + stavka.pdv_stopa + '% uključen'"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding: 8px 10px">
|
||||
@@ -171,8 +193,22 @@
|
||||
</template>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style="border-top: 0.5px solid var(--ivica)">
|
||||
<td colspan="4" style="padding: 10px; text-align: right; font-size: 13px; color: var(--tekst-sporedni); font-weight: 500;">Ukupno:</td>
|
||||
<tr x-show="pdvObveznik" x-cloak style="border-top: 0.5px solid var(--ivica)">
|
||||
<td colspan="4" style="padding: 4px 10px; text-align: right; font-size: 12px; color: var(--tekst-sporedni);">Osnovica:</td>
|
||||
<td style="padding: 4px 10px; text-align: right; font-size: 12px; color: var(--tekst-sporedni);">
|
||||
<span x-text="fmtDin(osnovicaSvega()) + ' din'"></span>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr x-show="pdvObveznik" x-cloak>
|
||||
<td colspan="4" style="padding: 0 10px 4px; text-align: right; font-size: 12px; color: var(--tekst-sporedni);">PDV:</td>
|
||||
<td style="padding: 0 10px 4px; text-align: right; font-size: 12px; color: var(--tekst-sporedni);">
|
||||
<span x-text="fmtDin(pdvSvega()) + ' din'"></span>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr :style="!pdvObveznik ? 'border-top: 0.5px solid var(--ivica);' : ''">
|
||||
<td colspan="4" style="padding: 10px; text-align: right; font-size: 13px; color: var(--tekst-sporedni); font-weight: 500;" x-text="pdvObveznik ? 'Ukupno za naplatu:' : 'Ukupno:'"></td>
|
||||
<td style="padding: 10px; text-align: right; font-size: 15px; font-weight: 600; color: var(--tekst-glavni);">
|
||||
<span x-text="fmtDin(ukupnoSvega()) + ' din'"></span>
|
||||
</td>
|
||||
@@ -185,7 +221,7 @@
|
||||
<!-- mobilne kartice stavki -->
|
||||
<div class="stavke-kartice" style="display: none; flex-direction: column; gap: 10px">
|
||||
<!-- pretraga na mobilnom -->
|
||||
<input type="text" x-model="pretragaArtikal" placeholder="Pretraži artikle..." style="width:100%;">
|
||||
<input type="text" x-model="pretragaArtikal" @keydown.enter.prevent="dodajPoBarkodu()" placeholder="Pretraži artikle ili skeniraj barkod..." autocomplete="off" style="width:100%;">
|
||||
<template x-for="(stavka, i) in stavke" :key="i">
|
||||
<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;">
|
||||
@@ -197,14 +233,36 @@
|
||||
<input type="hidden" :name="'pdv_stopa[]'" :value="stavka.pdv_stopa">
|
||||
<!-- kategorija badge -->
|
||||
<div x-show="stavka.kategorija_naziv" style="font-size:11px;color:var(--tekst-sporedni);background:var(--pozadina);padding:2px 8px;border-radius:4px;align-self:flex-start;" x-text="stavka.kategorija_naziv"></div>
|
||||
<div>
|
||||
<div style="position: relative;">
|
||||
<label class="polje-labela">Artikal</label>
|
||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id" @change="popuniCenu(stavka)" :disabled="!isMobile" style="width: 100%">
|
||||
<option value="">— odaberi artikal —</option>
|
||||
<template x-for="a in filtriraniArtikli" :key="a.id">
|
||||
<option :value="a.id" x-text="a.naziv"></option>
|
||||
</template>
|
||||
</select>
|
||||
<input type="hidden" :name="'artikal_id[]'" :value="stavka.artikal_id" :disabled="!isMobile">
|
||||
<template x-if="stavka.artikal_id">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:6px;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" x-text="stavka._naziv"></span>
|
||||
<button type="button" @click="promeniArtikal(stavka)" :disabled="!isMobile" title="Promeni artikal"
|
||||
style="background:none;border:none;cursor:pointer;color:var(--tekst-sporedni);font-size:13px;flex-shrink:0;padding:0;">✎</button>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!stavka.artikal_id">
|
||||
<div @click.outside="stavka._prikaziListu = false">
|
||||
<input type="text" x-model="stavka._pretraga" @focus="stavka._prikaziListu = true" @input="stavka._prikaziListu = true"
|
||||
placeholder="Pretraži artikal..." autocomplete="off" :disabled="!isMobile" style="width: 100%">
|
||||
<div x-show="stavka._prikaziListu && filtriraniArtikliZaStavku(stavka).length" x-cloak
|
||||
@scroll="ucitajViseArtikala(stavka, $event)"
|
||||
style="position: absolute; z-index: 40; left: 0; right: 0; margin-top: 4px; background: var(--pozadina); border: 0.5px solid var(--ivica); border-radius: 10px; max-height: 240px; overflow: auto; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);">
|
||||
<template x-for="a in filtriraniArtikliZaStavku(stavka)" :key="a.id">
|
||||
<div @click="izaberiArtikal(stavka, a)" style="display:flex;justify-content:space-between;gap:10px;padding:8px 12px;cursor:pointer;border-bottom:0.5px solid var(--ivica);">
|
||||
<div style="min-width:0;">
|
||||
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" x-text="a.naziv"></div>
|
||||
<div style="font-size:11px;color:var(--tekst-sporedni);" x-text="a.kategorija_naziv"></div>
|
||||
</div>
|
||||
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);white-space:nowrap;" x-text="fmtDin(pdvObveznik ? a.cena_sa_pdv : a.cena) + ' din'"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="imaJosArtikala(stavka)" style="padding: 6px 12px; text-align: center; font-size: 11px; color: var(--tekst-sporedni)">Skrolujte za još rezultata...</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<div>
|
||||
@@ -215,8 +273,9 @@
|
||||
x-text="'Na stanju: ' + dostupnaKolicina(i)" style="font-size: 11px; margin-top: 3px;"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Cena/kom (din)</label>
|
||||
<label class="polje-labela">Cena/kom (din){{if index .Moduli "pdv"}} sa PDV{{end}}</label>
|
||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="!isMobile" style="width: 100%" />
|
||||
<div x-show="pdvObveznik && stavka.pdv_stopa > 0" style="font-size:10px;color:var(--tekst-sporedni);margin-top:3px;" x-text="'PDV ' + stavka.pdv_stopa + '% uključen'"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
@@ -231,8 +290,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="pdvObveznik" x-cloak style="text-align: right; font-size: 12px; color: var(--tekst-sporedni); padding: 0 4px;">
|
||||
Osnovica: <span x-text="fmtDin(osnovicaSvega()) + ' din'"></span> · PDV: <span x-text="fmtDin(pdvSvega()) + ' din'"></span>
|
||||
</div>
|
||||
<div style="text-align: right; font-size: 15px; font-weight: 600; color: var(--tekst-glavni); padding: 8px 4px;">
|
||||
Ukupno: <span x-text="fmtDin(ukupnoSvega()) + ' din'"></span>
|
||||
<span x-text="pdvObveznik ? 'Ukupno za naplatu:' : 'Ukupno:'"></span> <span x-text="fmtDin(ukupnoSvega()) + ' din'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,7 +302,7 @@
|
||||
<!-- dugmad forme -->
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px">
|
||||
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno" :disabled="imaPrekoracenja()" :style="imaPrekoracenja() ? 'opacity:0.4;cursor:not-allowed;' : ''">
|
||||
<button type="submit" class="btn-primarno" :disabled="imaPrekoracenja() || imaPraznihStavki()" :style="(imaPrekoracenja() || imaPraznihStavki()) ? 'opacity:0.4;cursor:not-allowed;' : ''">
|
||||
Sačuvaj prodaju
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user