Files
GoNtech/Magacin.md
T
Dasko 5f8fbf5b46 Kartica emulator (BE) + fiskalna verifikacija + ime/prezime korisnika
- internal/be: emulator kartice (TCP listener za L-PFR mock)
- /v/: javna stranica za verifikaciju fiskalnog računa (QR kod)
- /servis/{id}/fiskalni-racun: štampa fiskalnog računa
- Podešavanja fiskalizacije: BE status/reset, proširen UI
- Korisnik: polja ime i prezime (model, DB, handler, admin profil)
- Fisk server: ažuriran receipt.py i server.py
- Koncept.md, Magacin.md: dokumentacija
2026-06-26 23:17:52 +02:00

11 KiB
Raw Blame History

Magacin — Plan izmena: Cena sa PDV-om

Dodajemo polje Cena sa PDV-om i PDV dropdown u forme za Novi artikal i Izmeni artikal.


Trenutno stanje

Kolona u DB Šta čuva Default
prodajna_cena Prodajna cena (neto, bez PDV-a) 0
pdv_stopa PDV stopa u % 20.0
cena_sa_pdv ne postoji

Model metode (internal/model/artikal.go, linije 4147):

// PAŽNJA: ove metode tretiraju ProdajnaCenu kao BRUTO — ne odgovaraju
// korisnikovom modelu gde je prodajna_cena NETO.
func (a Artikal) CenaBezPdv() float64 { return a.ProdajnaCena / (1 + a.PdvStopa/100) }
func (a Artikal) PdvIznos() float64   { return a.ProdajnaCena - a.CenaBezPdv() }

Ove metode treba ispraviti posle ove izmene (videti §6).


Šta dodajemo

Novo Opis
Kolona cena_sa_pdv Prodajna cena sa PDV-om (bruto). Čuva se u bazi radi brzog prikaza.
Dropdown pdv_stopa Izbor PDV stope u formi. Default: Opšta stopa 20%.
JS dvosmerna kalkulacija Prodajna ↔ sa PDV (automatsko popunjavanje u zavisnosti od toga šta je uneto)

Formula:

cena_sa_pdv = prodajna_cena * (1 + pdv_stopa / 100)
prodajna_cena = cena_sa_pdv / (1 + pdv_stopa / 100)

Izmene po fajlovima

1. migrations/087_artikal_cena_sa_pdv.sql — NOVI FAJL

ALTER TABLE artikli ADD COLUMN cena_sa_pdv REAL NOT NULL DEFAULT 0;

UPDATE artikli
SET cena_sa_pdv = ROUND(prodajna_cena * (1 + pdv_stopa / 100), 2);

Drugi red popunjava sve postojeće artikle — svaka prodajna cena se konvertuje u bruto.


2. internal/model/artikal.go

Linija 26 — posle ProdajnaCena float64 dodati novo polje:

// DODATI:
CenaSaPdv    float64     // prodajna cena sa PDV-om (bruto); = ProdajnaCena * (1 + PdvStopa/100)

Linije 4047 — ispraviti metode:

// STARO:
func (a Artikal) CenaBezPdv() float64 {
    return a.ProdajnaCena / (1 + a.PdvStopa/100)
}
func (a Artikal) PdvIznos() float64 {
    return a.ProdajnaCena - a.CenaBezPdv()
}

// NOVO:
func (a Artikal) CenaBezPdv() float64 {
    return a.ProdajnaCena   // prodajna_cena je već neto
}
func (a Artikal) PdvIznos() float64 {
    return a.CenaSaPdv - a.ProdajnaCena
}

3. internal/db/sqlite/artikal.go

3a. SELECT u Lista (linija ~33) — dodati cena_sa_pdv na kraj SELECT-a

-- STARO (kraj SELECT-a):
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.marza, a.napomena,

-- NOVO:
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.cena_sa_pdv, a.marza, a.napomena,

3b. Scan u Lista (linija ~96) — dodati &a.CenaSaPdv iza &a.PdvStopa:

// STARO:
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,

// NOVO:
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.CenaSaPdv, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,

3c. SELECT u DohvatiID (linija ~139141) — isti princip:

-- STARO:
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena, datum_unosa, arhiviran

-- NOVO:
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, cena_sa_pdv, marza, napomena, datum_unosa, arhiviran

3d. Scan u DohvatiID (linija ~143):

// STARO:
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,

// NOVO:
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.CenaSaPdv, &marza, &a.Napomena, &a.DatumUnosa, &arhiviran,

3e. INSERT u Kreiraj (linije ~179182) — dodati cena_sa_pdv:

// STARO:
INSERT INTO artikli
    (..., nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
...
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena,

// NOVO:
INSERT INTO artikli
    (..., nabavna_cena, prodajna_cena, pdv_stopa, cena_sa_pdv, marza, napomena)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
...
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.CenaSaPdv, a.Marza, a.Napomena,

3f. UPDATE u Izmeni (linije ~210214) — dodati cena_sa_pdv = ?:

// STARO:
UPDATE artikli SET
    ..., nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, marza = ?, napomena = ?
WHERE id = ?
...
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena, a.ID,

// NOVO:
UPDATE artikli SET
    ..., nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, cena_sa_pdv = ?, marza = ?, napomena = ?
WHERE id = ?
...
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.CenaSaPdv, a.Marza, a.Napomena, a.ID,

4. internal/handler/magacin_forma.go

4a. parseFormuArtikla — čitanje pdv_stopa iz forme (dodati iza linije 389, pre prodajna_cena):

// DODATI — čitanje PDV stope iz forme:
pdvStopa := 20.0
if p := r.FormValue("pdv_stopa"); p != "" {
    if v, err := strconv.ParseFloat(p, 64); err == nil && v >= 0 {
        pdvStopa = v
    }
}
artikal.PdvStopa = pdvStopa

4b. parseFormuArtikla — kalkulacija CenaSaPdv (dodati na kraju, pre return artikal, ""):

// DODATI — izračunaj cena_sa_pdv na osnovu unetog polja:
if c := r.FormValue("cena_sa_pdv"); c != "" {
    // korisnik je uneo bruto cenu → izračunaj neto
    if v, err := strconv.ParseFloat(c, 64); err == nil && v >= 0 {
        artikal.CenaSaPdv = v
        // ako prodajna_cena nije uneta ručno, izračunaj je
        if r.FormValue("prodajna_cena") == "" {
            artikal.ProdajnaCena = v / (1 + artikal.PdvStopa/100)
        }
    }
} else {
    // korisnik je uneo neto cenu → izračunaj bruto
    artikal.CenaSaPdv = artikal.ProdajnaCena * (1 + artikal.PdvStopa/100)
}

4c. Nivelacioni trag — linija ~278

Ovaj deo poredi artikal.ProdajnaCena - staraCena. Ništa ne menjamo — i dalje poredimo neto cene. Ostaje kako jeste.


5. web/templates/stranice/magacin_forma.html

5a. Blok cena (linije 106122) — trenutno izgleda ovako:

<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
    <div>
        <label class="polje-labela">Nabavna cena (din)</label>
        <input type="number" name="nabavna_cena" ...>
    </div>
    <div>
        <label class="polje-labela">Prodajna cena (din)</label>
        <input type="number" name="prodajna_cena" ...>
    </div>
</div>

<div>
    <label class="polje-labela">Marža (%)</label>
    <input type="number" name="marza" ...>
</div>

5b. ZAMENA — novi blok cena sa PDV dropdown-om i trećim poljem:

<!-- PDV stopa -->
<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>

<!-- cene: nabavna / prodajna / sa PDV -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
    <div>
        <label class="polje-labela">Nabavna cena (din)</label>
        <input type="number" id="nabavna-cena" name="nabavna_cena"
               value="{{.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="{{.Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;">
    </div>
    <div>
        <label class="polje-labela">Cena sa PDV-om (din)</label>
        <input type="number" id="cena-sa-pdv" name="cena_sa_pdv"
               value="{{.Artikal.CenaSaPdv}}" min="0" step="0.01" style="width:100%;">
    </div>
</div>

<!-- marža ostaje ispod -->
<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>

5c. JavaScript blok (linije ~187207) — dodati logiku za cena_sa_pdv i PDV stopa

// NOVI JS — zamenjuje stari blok kalkulacije cena:
(function() {
    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();
    }

    prodajna.addEventListener('input', izProdajne);
    saPdv.addEventListener('input', izSaPdv);
    nabavna.addEventListener('input', izMarzeKalc);
    marza.addEventListener('input', izMarzeKalc);
    pdvSel.addEventListener('change', izPdvStope);
})();

6. Napomene — šta NE menjamo u ovoj fazi

Fajl Zašto ne diramo
internal/handler/prodaja.go linija 74 POS i dalje koristi a.ProdajnaCena kao neto cenu; korisnik vidi neto pri prodaji. Ako treba prikazati bruto → posebna izmena.
internal/fiskal/mapiranje.go linija 66 Koristi s.CenaBezPdv iz model.StavkaProdaje (ne iz model.Artikal) — to je poseban struct, nije zahvaćen.
internal/db/sqlite/izvestaj.go Koristi ProdajnaCena za vrednost zaliha — neto ostaje logična osnova za to.
magacin.html (lista) Prikazuje ProdajnaCena — može ostati neto, ili se doda kolona CenaSaPdv. Nije hitan.

Redosled implementacije

  1. Migracija 087_artikal_cena_sa_pdv.sql — nova kolona + popunjavanje postojećih
  2. Model internal/model/artikal.go — novo polje + ispravka metoda
  3. SQLite repo internal/db/sqlite/artikal.go — SELECT, Scan, INSERT, UPDATE (4 mesta)
  4. Handler parser internal/handler/magacin_forma.go — čitanje pdv_stopa + kalkulacija cena_sa_pdv
  5. Šablon web/templates/stranice/magacin_forma.html — PDV dropdown + treće polje + novi JS