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
This commit is contained in:
2026-06-26 23:17:52 +02:00
parent 6293c7d946
commit 5f8fbf5b46
22 changed files with 1769 additions and 116 deletions
+344
View File
@@ -0,0 +1,344 @@
# 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):
```go
// 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
```sql
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:
```go
// DODATI:
CenaSaPdv float64 // prodajna cena sa PDV-om (bruto); = ProdajnaCena * (1 + PdvStopa/100)
```
**Linije 4047** — ispraviti metode:
```go
// 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
```sql
-- 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`:
```go
// 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:
```sql
-- 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):
```go
// 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`:
```go
// 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 = ?`:
```go
// 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`):
```go
// 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, ""`):
```go
// 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:
```html
<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:
```html
<!-- 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
```javascript
// 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