diff --git a/internal/db/repository.go b/internal/db/repository.go index 063c76e..c55d5fb 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -367,3 +367,10 @@ type PodsetnikRepository interface { Obrisi(ctx context.Context, id int64) error BrojAktivnih(ctx context.Context, filter PodsetnikFilter) (int, error) } + +// FiskalRepository definiše operacije nad fiskalnim računima (Teron L-PFR). +// Svaki prodajni nalog ima najviše jedan fiskalni račun (UNIQUE prodaja_id). +type FiskalRepository interface { + Kreiraj(ctx context.Context, fr *model.FiskalniRacun) (int64, error) + DohvatiPoProdaji(ctx context.Context, prodajaID int64) (*model.FiskalniRacun, error) +} diff --git a/internal/db/sqlite/fiskal.go b/internal/db/sqlite/fiskal.go new file mode 100644 index 0000000..258dc38 --- /dev/null +++ b/internal/db/sqlite/fiskal.go @@ -0,0 +1,71 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + + "ntech/internal/model" +) + +// FiskalRepo je SQLite implementacija FiskalRepository interfejsa +type FiskalRepo struct { + db *sql.DB +} + +// NoviFiskalRepo kreira novi FiskalRepo +func NoviFiskalRepo(db *sql.DB) *FiskalRepo { + return &FiskalRepo{db: db} +} + +// Kreiraj dodaje novi fiskalni račun u bazu. Vraća ID kreiranog reda. +// ProdajaID je UNIQUE — drugi poziv sa istim ID-jem pada na constraint. +func (r *FiskalRepo) Kreiraj(ctx context.Context, fr *model.FiskalniRacun) (int64, error) { + rez, err := r.db.ExecContext(ctx, ` + INSERT INTO fiskalni_racuni ( + prodaja_id, tip_racuna, tip_transakcije, pfr_broj, pfr_vreme, + brojac, ekstenzija_brojaca, url_verifikacija, qr_kod, + poreske_stavke, ukupno_za_naplatu, ukupan_porez, + sirovi_odgovor, potpisao, zatrazio, poruka + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, fr.ProdajaID, fr.TipRacuna, fr.TipTransakcije, fr.PfrBroj, fr.PfrVreme, + fr.Brojac, fr.EkstenzijaBrojaca, fr.UrlVerifikacija, fr.QRKod, + fr.PoreskeStavke, fr.UkupnoZaNaplatu, fr.UkupanPorez, + fr.SiroviOdgovor, fr.Potpisao, fr.Zatrazio, fr.Poruka, + ) + if err != nil { + return 0, fmt.Errorf("ntech: FiskalRepo.Kreiraj: %w", err) + } + id, err := rez.LastInsertId() + if err != nil { + return 0, fmt.Errorf("ntech: FiskalRepo.Kreiraj.LastInsertId: %w", err) + } + return id, nil +} + +// DohvatiPoProdaji vraća fiskalni račun za dati prodajni nalog. +// Vraća nil bez greške ako račun ne postoji (nije fiskalizovano). +func (r *FiskalRepo) DohvatiPoProdaji(ctx context.Context, prodajaID int64) (*model.FiskalniRacun, error) { + fr := &model.FiskalniRacun{} + var storniran int + err := r.db.QueryRowContext(ctx, ` + SELECT id, prodaja_id, tip_racuna, tip_transakcije, pfr_broj, pfr_vreme, + brojac, ekstenzija_brojaca, url_verifikacija, qr_kod, + poreske_stavke, ukupno_za_naplatu, ukupan_porez, + sirovi_odgovor, potpisao, zatrazio, poruka, storniran, vreme_kreiranja + FROM fiskalni_racuni WHERE prodaja_id = ? + `, prodajaID).Scan( + &fr.ID, &fr.ProdajaID, &fr.TipRacuna, &fr.TipTransakcije, &fr.PfrBroj, &fr.PfrVreme, + &fr.Brojac, &fr.EkstenzijaBrojaca, &fr.UrlVerifikacija, &fr.QRKod, + &fr.PoreskeStavke, &fr.UkupnoZaNaplatu, &fr.UkupanPorez, + &fr.SiroviOdgovor, &fr.Potpisao, &fr.Zatrazio, &fr.Poruka, &storniran, &fr.VremeKreiranja, + ) + if err == sql.ErrNoRows { + return nil, nil // nije fiskalizovano — nije greška + } + if err != nil { + return nil, fmt.Errorf("ntech: FiskalRepo.DohvatiPoProdaji: %w", err) + } + fr.Storniran = storniran != 0 + return fr, nil +} diff --git a/internal/fiskal/fiskal.go b/internal/fiskal/fiskal.go new file mode 100644 index 0000000..f52ba9e --- /dev/null +++ b/internal/fiskal/fiskal.go @@ -0,0 +1,155 @@ +package fiskal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// Klijent je HTTP klijent za Teron L-PFR mock server (ili pravi Teron uređaj). +// Komunicira JSON-om preko HTTP POST/GET na baseURL/api/.... +type Klijent struct { + BaseURL string + APIKey string + HTTPKlijent *http.Client +} + +// NoviKlijent kreira novi fiskalni klijent. baseURL je adresa PFR servera +// (npr. "http://127.0.0.1:4566"), apiKey je Bearer token. +func NoviKlijent(baseURL, apiKey string) *Klijent { + return &Klijent{ + BaseURL: strings.TrimRight(baseURL, "/"), + APIKey: apiKey, + HTTPKlijent: &http.Client{Timeout: 10 * time.Second}, + } +} + +// InvoiceRequest je omotač za Teron invoiceRequest JSON objekat. +type InvoiceRequest struct { + InvoiceRequest InvoiceRequestBody `json:"invoiceRequest"` +} + +// InvoiceRequestBody su podaci koje Teron očekuje unutar invoiceRequest polja. +type InvoiceRequestBody struct { + InvoiceType string `json:"invoiceType"` + TransactionType string `json:"transactionType"` + Payment []PaymentItem `json:"payment"` + Items []InvoiceItem `json:"items"` + Cashier string `json:"cashier"` + BuyerID string `json:"buyerId,omitempty"` + ReferentDocumentNumber string `json:"referentDocumentNumber,omitempty"` +} + +// PaymentItem opisuje jedno plaćanje (može više — keš + kartica). +type PaymentItem struct { + Amount float64 `json:"amount"` + PaymentType string `json:"paymentType"` +} + +// InvoiceItem je jedna stavka fiskalnog računa. +type InvoiceItem struct { + Name string `json:"name"` + Labels []string `json:"labels"` + TotalAmount float64 `json:"totalAmount"` + UnitPrice float64 `json:"unitPrice"` + Quantity float64 `json:"quantity"` +} + +// InvoiceResponse je odgovor PFR servera na zahtev za fiskalizaciju. +// Polja prate stvarni JSON koji Fisk server vraća (vidi NtechFisk.md §3.2). +type InvoiceResponse struct { + RequestedBy string `json:"requestedBy"` + SignedBy string `json:"signedBy"` + SdcDateTime string `json:"sdcDateTime"` + InvoiceCounter string `json:"invoiceCounter"` + InvoiceCounterExtension string `json:"invoiceCounterExtension"` + InvoiceNumber string `json:"invoiceNumber"` + VerificationURL string `json:"verificationUrl"` + VerificationQRCode string `json:"verificationQRCode"` + Journal string `json:"journal"` + TaxItems []TaxItem `json:"taxItems"` + TotalAmount float64 `json:"totalAmount"` + TotalTax float64 `json:"totalTax"` + Messages string `json:"messages"` +} + +// TaxItem je stavka poreskog obračuna u fiskalnom odgovoru. +type TaxItem struct { + Label string `json:"label"` + CategoryName string `json:"categoryName"` + CategoryType int `json:"categoryType"` + Rate float64 `json:"rate"` + Amount float64 `json:"amount"` +} + +// Status vraća trenutni status PFR servera (GET /api/status). +func (k *Klijent) Status(ctx context.Context) (map[string]any, error) { + url := k.BaseURL + "/api/status" + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("ntech: fiskal.Status: %w", err) + } + k.dodajAuth(req) + + resp, err := k.HTTPKlijent.Do(req) + if err != nil { + return nil, fmt.Errorf("ntech: fiskal.Status: server nedostupan: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + telo, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("ntech: fiskal.Status: HTTP %d: %s", resp.StatusCode, string(telo)) + } + + var result map[string]any + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("ntech: fiskal.Status: JSON parse: %w", err) + } + return result, nil +} + +// IzdajRacun šalje zahtev za fiskalizaciju PFR serveru (POST /api/invoices). +func (k *Klijent) IzdajRacun(ctx context.Context, zahtev InvoiceRequest) (*InvoiceResponse, error) { + telo, err := json.Marshal(zahtev) + if err != nil { + return nil, fmt.Errorf("ntech: fiskal.IzdajRacun: marshal: %w", err) + } + + url := k.BaseURL + "/api/invoices" + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(telo)) + if err != nil { + return nil, fmt.Errorf("ntech: fiskal.IzdajRacun: %w", err) + } + req.Header.Set("Content-Type", "application/json") + k.dodajAuth(req) + + resp, err := k.HTTPKlijent.Do(req) + if err != nil { + return nil, fmt.Errorf("ntech: fiskal.IzdajRacun: server nedostupan: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + teloOdg, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("ntech: fiskal.IzdajRacun: HTTP %d: %s", resp.StatusCode, string(teloOdg)) + } + + var odgovor InvoiceResponse + if err := json.NewDecoder(resp.Body).Decode(&odgovor); err != nil { + return nil, fmt.Errorf("ntech: fiskal.IzdajRacun: JSON parse: %w", err) + } + return &odgovor, nil +} + +// dodajAuth postavlja Authorization header ako je API ključ podešen. +func (k *Klijent) dodajAuth(req *http.Request) { + if k.APIKey != "" { + req.Header.Set("Authorization", "Bearer "+k.APIKey) + } +} diff --git a/internal/fiskal/mapiranje.go b/internal/fiskal/mapiranje.go new file mode 100644 index 0000000..7f8cf0c --- /dev/null +++ b/internal/fiskal/mapiranje.go @@ -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 +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 78acbdc..f2f7ed2 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -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) +} diff --git a/internal/handler/prodaja.go b/internal/handler/prodaja.go index 9607ec8..9fbe0fe 100644 --- a/internal/handler/prodaja.go +++ b/internal/handler/prodaja.go @@ -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", } diff --git a/internal/model/fiskal.go b/internal/model/fiskal.go new file mode 100644 index 0000000..41ab07c --- /dev/null +++ b/internal/model/fiskal.go @@ -0,0 +1,25 @@ +package model + +// FiskalniRacun je veza između prodajnog naloga i odgovora fiskalne kase (Teron L-PFR). +// Polja prate tačno ono što Fisk server vraća u JSON odgovoru (vidi NtechFisk.md §3.2). +type FiskalniRacun struct { + ID int64 + ProdajaID int64 + TipRacuna string // "Normal", "Advance", "Copy", "Training" + TipTransakcije string // "Sale", "Refund" + PfrBroj string // invoiceNumber — jedinstven broj fiskalnog računa + PfrVreme string // sdcDateTime — PFR vreme izdavanja + Brojac string // invoiceCounter (npr. "1/1ПП") + EkstenzijaBrojaca string // invoiceCounterExtension (npr. "ПП") + UrlVerifikacija string // verificationUrl — link ka PURS proveri + QRKod string // verificationQRCode — base64 PNG, direktno u + PoreskeStavke string // JSON niz taxItems[] iz odgovora + UkupnoZaNaplatu float64 // totalAmount + UkupanPorez float64 // totalTax + SiroviOdgovor string // ceo JSON odgovor — sadrži journal, taxItems i ostalo za audit + Potpisao string // signedBy — identifikator PFR-a koji je potpisao + Zatrazio string // requestedBy — identifikator ESIR-a + Poruka string // messages (npr. "Success") + Storniran bool // true ako je račun naknadno storniran + VremeKreiranja string +} diff --git a/migrations/081_fiskalni_racuni.sql b/migrations/081_fiskalni_racuni.sql new file mode 100644 index 0000000..222a44f --- /dev/null +++ b/migrations/081_fiskalni_racuni.sql @@ -0,0 +1,33 @@ +-- Fiskalni računi: veza prodaje sa PFR odgovorom (Teron L-PFR). +-- Svaki prodajni nalog može imati najviše jedan fiskalni račun (UNIQUE prodaja_id). +-- QR kod se čuva kao base64 PNG (dolazi gotov iz Fisk servera). +-- sirovi_odgovor čuva ceo JSON odgovor za audit (sadrži journal, taxItems, itd.). +CREATE TABLE IF NOT EXISTS fiskalni_racuni ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + prodaja_id INTEGER NOT NULL UNIQUE, + tip_racuna TEXT NOT NULL DEFAULT 'Normal', + tip_transakcije TEXT NOT NULL DEFAULT 'Sale', + pfr_broj TEXT NOT NULL, -- invoiceNumber iz PFR odgovora + pfr_vreme TEXT NOT NULL, -- sdcDateTime + brojac TEXT, -- invoiceCounter (npr. "1/1ПП") + ekstenzija_brojaca TEXT, -- invoiceCounterExtension (npr. "ПП") + url_verifikacija TEXT, -- verificationUrl + qr_kod TEXT, -- verificationQRCode (base64 PNG) + poreske_stavke TEXT, -- JSON niz taxItems[] + ukupno_za_naplatu REAL, -- totalAmount + ukupan_porez REAL, -- totalTax + sirovi_odgovor TEXT, -- ceo JSON odgovor (audit trail) + potpisao TEXT, -- signedBy + zatrazio TEXT, -- requestedBy + poruka TEXT, -- messages + storniran INTEGER NOT NULL DEFAULT 0, -- 1 ako je fiskalni račun storniran + vreme_kreiranja TEXT NOT NULL DEFAULT (datetime('now','localtime')), + FOREIGN KEY (prodaja_id) REFERENCES prodajni_nalozi(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_fiskalni_racuni_prodaja ON fiskalni_racuni(prodaja_id); +CREATE INDEX IF NOT EXISTS idx_fiskalni_racuni_pfr_broj ON fiskalni_racuni(pfr_broj); + +-- Podešavanja za fiskalizaciju koja nedostaju u 080 +INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('pfr_api_key', ''); +INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('pfr_kasir', 'NTech'); diff --git a/web/templates/stranice/prodaja_detalji.html b/web/templates/stranice/prodaja_detalji.html index 630b75a..4c8bcdb 100644 --- a/web/templates/stranice/prodaja_detalji.html +++ b/web/templates/stranice/prodaja_detalji.html @@ -246,6 +246,29 @@ {{end}} + {{if .FiskalniRacun}} +
+

Fiskalni račun

+
+
+

PFR broj: {{.FiskalniRacun.PfrBroj}}

+

Vreme: {{.FiskalniRacun.PfrVreme}}

+

Brojač: {{.FiskalniRacun.Brojac}}

+ {{if .FiskalniRacun.UrlVerifikacija}} +

+ Provera na PURS-u → +

+ {{end}} +
+ {{if .FiskalniRacun.QRKod}} +
+ QR kod za proveru fiskalnog računa +
+ {{end}} +
+
+ {{end}} + {{if index $.Dozvole "prodaja.obrisi"}}