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

345 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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