Fiskalizacija: inicijalna implementacija (8 koraka)

1. migracija 081 — tabela fiskalni_racuni + podešavanja pfr_api_key/pfr_kasir
2. model FiskalniRacun (internal/model/fiskal.go)
3. FiskalRepository interfejs (repository.go)
4. SQLite repozitorijum (sqlite/fiskal.go — Kreiraj, DohvatiPoProdaji)
5. Paket internal/fiskal/ — HTTP klijent + mapiranje prodaja→invoiceRequest
6. Handler proširenje — FiskalRepo + fiskalKlijent() dinamički helper
7. Inicijalizacija kroz Novi() / reinicializuj — klijent se čita dinamički iz podešavanja
8. Prodaja integracija — fiskalizacija u SacuvajProdaju + QR prikaz u prodaja_detalji.html
This commit is contained in:
2026-06-25 22:51:59 +02:00
parent 563a7a7fe2
commit edcea0ddcf
9 changed files with 518 additions and 4 deletions
+117
View File
@@ -0,0 +1,117 @@
package fiskal
import (
"math"
"ntech/internal/model"
)
// OznakaPDV mapira NTech PDV stopu na ćiriličnu oznaku koju Fisk/Teron očekuje.
// ⚠️ 0% → "А" (neobveznici PDV-a), NE "Е" (Е = 10% posebna stopa).
func OznakaPDV(stopa float64) string {
switch {
case stopa >= 19.9 && stopa <= 20.1:
return "Ж" // opšta stopa 20%
case stopa >= 9.9 && stopa <= 10.1:
return "Ђ" // snižena stopa 10%
case stopa >= -0.01 && stopa <= 0.01:
return "А" // oslobođen / neobveznik PDV-a
default:
return "А" // fallback — ne rizikujemo pogrešnu oznaku
}
}
// BrutoCena konvertuje neto cenu u bruto (sa PDV-om) za zadatu stopu.
// Teron zahteva bruto iznose u unitPrice i totalAmount.
// Formula: bruto = neto * (1 + stopa/100), zaokruženo na 2 decimale.
func BrutoCena(neto, stopa float64) float64 {
return math.Round(neto*(1+stopa/100)*100) / 100
}
// TipPlacanja mapira NTech način plaćanja na Teron enumeraciju.
func TipPlacanja(nacin string) string {
switch nacin {
case "Gotovina":
return "Cash"
case "Kartica":
return "Card"
case "Virman", "Račun":
return "WireTransfer"
case "Ček":
return "Check"
case "Vaučer":
return "Voucher"
default:
return "Other"
}
}
// NapraviZahtev konvertuje prodajni nalog i stavke u Teron InvoiceRequest.
// VAŽNO: sve cene se konvertuju iz neto u bruto pre slanja (BrutoCena).
// pib/jmbg se koriste za buyerId identifikaciju kupca.
func NapraviZahtev(
nalog model.ProdajniNalog,
stavke []model.StavkaProdajeSaArtiklom,
pib, jmbg, kasir string,
) InvoiceRequest {
items := make([]InvoiceItem, 0, len(stavke))
ukupanIznos := 0.0
for _, s := range stavke {
stopa := 20.0 // podrazumevano 20% ako stopa nije eksplicitno postavljena
if s.PdvStopa > 0 {
stopa = s.PdvStopa
}
// CenaPoKomadu je neto cena — konvertujemo u bruto
netoCena := s.CenaBezPdv
if netoCena == 0 {
netoCena = s.CenaPoKomadu // fallback: ako CenaBezPdv nije popunjena
}
brutoCena := BrutoCena(netoCena, stopa)
brutoTotal := math.Round(brutoCena*float64(s.Kolicina)*100) / 100
items = append(items, InvoiceItem{
Name: s.ArtikalNaziv,
Labels: []string{OznakaPDV(stopa)},
TotalAmount: brutoTotal,
UnitPrice: brutoCena,
Quantity: float64(s.Kolicina),
})
ukupanIznos += brutoTotal
}
// Ukupno za plaćanje — iz naloga (bruto)
ukupnoPlacanje := nalog.Ukupno
if ukupnoPlacanje == 0 {
ukupnoPlacanje = ukupanIznos
}
nacinPlacanja := nalog.NacinPlacanja
if nacinPlacanja == "" {
nacinPlacanja = "Gotovina"
}
zahtev := InvoiceRequest{
InvoiceRequest: InvoiceRequestBody{
InvoiceType: "Normal",
TransactionType: "Sale",
Payment: []PaymentItem{
{
Amount: ukupnoPlacanje,
PaymentType: TipPlacanja(nacinPlacanja),
},
},
Items: items,
Cashier: kasir,
},
}
// buyerId: "10:PIB" za pravno lice, "01:JMBG" za fizičko
if pib != "" {
zahtev.InvoiceRequest.BuyerID = "10:" + pib
} else if jmbg != "" {
zahtev.InvoiceRequest.BuyerID = "01:" + jmbg
}
return zahtev
}