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:
@@ -11,6 +11,7 @@ import (
|
||||
"ntech/internal/config"
|
||||
"ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/fiskal"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
)
|
||||
@@ -44,6 +45,7 @@ type Handler struct {
|
||||
PdvKirRepo db.PdvKirRepository
|
||||
PdvKprRepo db.PdvKprRepository
|
||||
NivelacijaRepo db.NivelacijaRepository
|
||||
FiskalRepo db.FiskalRepository
|
||||
Verzija string
|
||||
JelDemo bool
|
||||
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
|
||||
@@ -112,6 +114,7 @@ func Novi(baza *sql.DB, totpKljuc []byte) *Handler {
|
||||
PdvKirRepo: sqlite.NoviPdvKirRepo(baza),
|
||||
PdvKprRepo: sqlite.NoviPdvKprRepo(baza),
|
||||
NivelacijaRepo: sqlite.NoviNivelacijaRepo(baza),
|
||||
FiskalRepo: sqlite.NoviFiskalRepo(baza),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +148,7 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
|
||||
h.PdvKirRepo = sqlite.NoviPdvKirRepo(novaDB)
|
||||
h.PdvKprRepo = sqlite.NoviPdvKprRepo(novaDB)
|
||||
h.NivelacijaRepo = sqlite.NoviNivelacijaRepo(novaDB)
|
||||
h.FiskalRepo = sqlite.NoviFiskalRepo(novaDB)
|
||||
}
|
||||
|
||||
// modulUkljucen vraća da li je zakonski modul (npr. „pdv") uključen za firmu prema profilu.
|
||||
@@ -232,3 +236,18 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
||||
|
||||
return ps
|
||||
}
|
||||
|
||||
// fiskalKlijent vraća inicijalizovan fiskalni HTTP klijent na osnovu trenutnih
|
||||
// podešavanja. Vraća nil ako pfr_url nije podešen. URL se čita dinamički pri
|
||||
// svakom pozivu — nema keširanja, pa promena podešavanja stupa na snagu odmah.
|
||||
func (h *Handler) fiskalKlijent() *fiskal.Klijent {
|
||||
url, _ := sqlite.DohvatiPodesavanje(context.Background(), h.DB, "pfr_url")
|
||||
key, _ := sqlite.DohvatiPodesavanje(context.Background(), h.DB, "pfr_api_key")
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
if key == "" {
|
||||
key = "test"
|
||||
}
|
||||
return fiskal.NoviKlijent(url, key)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ntech/internal/config"
|
||||
appdb "ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/fiskal"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
@@ -39,10 +41,11 @@ type PodaciFormeProdaje struct {
|
||||
// PodaciDetaljiProdaje su podaci za pregled jedne prodaje sa stavkama
|
||||
type PodaciDetaljiProdaje struct {
|
||||
model.PodaciStranice
|
||||
Nalog model.ProdajniNalog
|
||||
Stavke []model.StavkaProdajeSaArtiklom
|
||||
KlijentNaziv string
|
||||
Sacuvano bool
|
||||
Nalog model.ProdajniNalog
|
||||
Stavke []model.StavkaProdajeSaArtiklom
|
||||
KlijentNaziv string
|
||||
FiskalniRacun *model.FiskalniRacun // nil ako nije fiskalizovano
|
||||
Sacuvano bool
|
||||
}
|
||||
|
||||
// PodaciStampeProdaje su podaci za stranicu za štampanje priznanice
|
||||
@@ -213,6 +216,63 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fiskalizacija — ako je modul uključen (best-effort: prodaja ostaje validna i bez fiskalizacije)
|
||||
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
klijent := h.fiskalKlijent()
|
||||
if klijent != nil {
|
||||
// učitaj nalog sa ID-jem (potreban za buyerId)
|
||||
spremljen, e := h.ProdajaRepo.DohvatiID(r.Context(), id)
|
||||
if e == nil {
|
||||
spremljeneStavke, e2 := h.ProdajaRepo.DohvatiStavke(r.Context(), id)
|
||||
if e2 == nil {
|
||||
kasir, _ := sqlite.DohvatiPodesavanje(r.Context(), h.DB, "pfr_kasir")
|
||||
if kasir == "" {
|
||||
kasir = "NTech"
|
||||
}
|
||||
|
||||
var pib, jmbg string
|
||||
if spremljen.KlijentID != nil {
|
||||
if kk, e3 := h.KlijentiRepo.DohvatiID(r.Context(), *spremljen.KlijentID); e3 == nil {
|
||||
pib, jmbg = kk.PIB, kk.JMBG
|
||||
}
|
||||
}
|
||||
|
||||
zahtev := fiskal.NapraviZahtev(*spremljen, spremljeneStavke, pib, jmbg, kasir)
|
||||
|
||||
odgovor, errFisk := klijent.IzdajRacun(r.Context(), zahtev)
|
||||
if errFisk != nil {
|
||||
slog.Error("fiskalizacija nije uspela", "prodaja_id", id, "error", errFisk)
|
||||
} else {
|
||||
poreskeJSON, _ := json.Marshal(odgovor.TaxItems)
|
||||
siroviJSON, _ := json.Marshal(odgovor)
|
||||
|
||||
fr := &model.FiskalniRacun{
|
||||
ProdajaID: id,
|
||||
TipRacuna: "Normal",
|
||||
TipTransakcije: "Sale",
|
||||
PfrBroj: odgovor.InvoiceNumber,
|
||||
PfrVreme: odgovor.SdcDateTime,
|
||||
Brojac: odgovor.InvoiceCounter,
|
||||
EkstenzijaBrojaca: odgovor.InvoiceCounterExtension,
|
||||
UrlVerifikacija: odgovor.VerificationURL,
|
||||
QRKod: odgovor.VerificationQRCode,
|
||||
PoreskeStavke: string(poreskeJSON),
|
||||
UkupnoZaNaplatu: odgovor.TotalAmount,
|
||||
UkupanPorez: odgovor.TotalTax,
|
||||
SiroviOdgovor: string(siroviJSON),
|
||||
Potpisao: odgovor.SignedBy,
|
||||
Zatrazio: odgovor.RequestedBy,
|
||||
Poruka: odgovor.Messages,
|
||||
}
|
||||
if _, errFisk := h.FiskalRepo.Kreiraj(r.Context(), fr); errFisk != nil {
|
||||
slog.Error("greška pri čuvanju fiskalnog računa", "prodaja_id", id, "error", errFisk)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -254,6 +314,9 @@ func (h *Handler) DetaljiProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// učitaj fiskalni račun ako postoji
|
||||
fr, _ := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id)
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "prodaja"
|
||||
ps.NaslovStranice = "Detalji prodaje"
|
||||
@@ -262,6 +325,7 @@ func (h *Handler) DetaljiProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
Nalog: *nalog,
|
||||
Stavke: stavke,
|
||||
KlijentNaziv: klijentNaziv,
|
||||
FiskalniRacun: fr,
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user