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:
+344
@@ -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 41–47):
|
||||
```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 40–47** — 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 ~139–141) — 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 ~179–182) — 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 ~210–214) — 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 106–122) — 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 ~187–207) — 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
|
||||
Reference in New Issue
Block a user