- Nova stranica fiskal_pazar: presek prometa od poslednjeg zaključenja, zaključenje dana, generisanje PDF izveštaja - fiskal.go: dodati pozivi Presek/ZakljuciDan/GenerisiIzvestaj ka /api/financial/summary i /api/financial/report/summary - PrazninaKnjigovodstva razdvojena na Prodaja/Servis varijante sa ID listama za direktne linkove sa dashboard-a
227 lines
8.0 KiB
Go
227 lines
8.0 KiB
Go
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.
|
|
// Koristi prodaja_id (za prodaju) ILI servis_id (za servis) — ne oba.
|
|
func (r *FiskalRepo) Kreiraj(ctx context.Context, fr *model.FiskalniRacun) (int64, error) {
|
|
var prodajaID, servisID any
|
|
if fr.ProdajaID > 0 {
|
|
prodajaID = fr.ProdajaID
|
|
}
|
|
if fr.ServisID > 0 {
|
|
servisID = fr.ServisID
|
|
}
|
|
rez, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO fiskalni_racuni (
|
|
prodaja_id, servis_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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, prodajaID, servisID, 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.
|
|
func (r *FiskalRepo) DohvatiPoProdaji(ctx context.Context, prodajaID int64) (*model.FiskalniRacun, error) {
|
|
fr := &model.FiskalniRacun{}
|
|
var storniran int
|
|
var sID sql.NullInt64
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, prodaja_id, servis_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 = ?
|
|
ORDER BY id DESC LIMIT 1
|
|
`, prodajaID).Scan(
|
|
&fr.ID, &fr.ProdajaID, &sID, &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
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ntech: FiskalRepo.DohvatiPoProdaji: %w", err)
|
|
}
|
|
if sID.Valid {
|
|
fr.ServisID = sID.Int64
|
|
}
|
|
fr.Storniran = storniran != 0
|
|
return fr, nil
|
|
}
|
|
|
|
// DohvatiPoServisu vraća fiskalni račun za dati servisni nalog.
|
|
// Vraća nil bez greške ako račun ne postoji.
|
|
func (r *FiskalRepo) DohvatiPoServisu(ctx context.Context, servisID int64) (*model.FiskalniRacun, error) {
|
|
fr := &model.FiskalniRacun{}
|
|
var storniran int
|
|
var sID, pID sql.NullInt64
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, prodaja_id, servis_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 servis_id = ?
|
|
ORDER BY id DESC LIMIT 1
|
|
`, servisID).Scan(
|
|
&fr.ID, &pID, &sID, &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
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ntech: FiskalRepo.DohvatiPoServisu: %w", err)
|
|
}
|
|
if pID.Valid {
|
|
fr.ProdajaID = pID.Int64
|
|
}
|
|
if sID.Valid {
|
|
fr.ServisID = sID.Int64
|
|
}
|
|
fr.Storniran = storniran != 0
|
|
return fr, nil
|
|
}
|
|
|
|
// DohvatiPoServisuITip vraća poslednji nestorniran fiskalni račun za dati servisni
|
|
// nalog sa tačno određenim tipom (npr. tip_racuna="Advance", tip_transakcije="Sale"
|
|
// za avansni račun) — za razliku od DohvatiPoServisu koji vraća samo najnoviji
|
|
// zapis bilo kog tipa. Vraća nil bez greške ako takav račun ne postoji.
|
|
func (r *FiskalRepo) DohvatiPoServisuITip(ctx context.Context, servisID int64, tipRacuna, tipTransakcije string) (*model.FiskalniRacun, error) {
|
|
fr := &model.FiskalniRacun{}
|
|
var storniran int
|
|
var sID, pID sql.NullInt64
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, prodaja_id, servis_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 servis_id = ? AND tip_racuna = ? AND tip_transakcije = ?
|
|
ORDER BY id DESC LIMIT 1
|
|
`, servisID, tipRacuna, tipTransakcije).Scan(
|
|
&fr.ID, &pID, &sID, &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
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ntech: FiskalRepo.DohvatiPoServisuITip: %w", err)
|
|
}
|
|
if pID.Valid {
|
|
fr.ProdajaID = pID.Int64
|
|
}
|
|
if sID.Valid {
|
|
fr.ServisID = sID.Int64
|
|
}
|
|
fr.Storniran = storniran != 0
|
|
return fr, nil
|
|
}
|
|
|
|
// SumaAvansaPoServisu vraća neto fiskalizovan avans za dati servisni nalog —
|
|
// zbir Advance/Sale iznosa umanjen za Advance/Refund iznose (nestornirani).
|
|
// Koristi se da se pri izmeni avansa fiskalizuje samo razlika (delta), ne ceo
|
|
// novi iznos ponovo.
|
|
func (r *FiskalRepo) SumaAvansaPoServisu(ctx context.Context, servisID int64) (float64, error) {
|
|
var suma float64
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT COALESCE(SUM(CASE WHEN tip_transakcije = 'Refund' THEN -ukupno_za_naplatu ELSE ukupno_za_naplatu END), 0)
|
|
FROM fiskalni_racuni
|
|
WHERE servis_id = ? AND tip_racuna = 'Advance' AND storniran = 0
|
|
`, servisID).Scan(&suma)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("ntech: FiskalRepo.SumaAvansaPoServisu: %w", err)
|
|
}
|
|
return suma, nil
|
|
}
|
|
|
|
// ServisiBezFiskalnog vraća skup ID-eva servisnih naloga koji su u statusu "Preuzeto"
|
|
// a nemaju fiskalni račun (za oznaku u listi naloga).
|
|
func (r *FiskalRepo) ServisiBezFiskalnog(ctx context.Context) (map[int64]bool, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT sn.id FROM servisni_nalozi sn
|
|
LEFT JOIN fiskalni_racuni fr ON fr.servis_id = sn.id AND fr.storniran = 0
|
|
WHERE sn.status = 'Preuzeto' AND fr.id IS NULL`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make(map[int64]bool)
|
|
for rows.Next() {
|
|
var id int64
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
out[id] = true
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// ProdajeBezFiskalnog vraća skup ID-eva prodajnih naloga koji nisu stornirani
|
|
// a nemaju fiskalni račun (za oznaku u listi naloga).
|
|
func (r *FiskalRepo) ProdajeBezFiskalnog(ctx context.Context) (map[int64]bool, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT pn.id FROM prodajni_nalozi pn
|
|
LEFT JOIN fiskalni_racuni fr ON fr.prodaja_id = pn.id AND fr.storniran = 0
|
|
WHERE pn.stornirano = 0 AND fr.id IS NULL`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make(map[int64]bool)
|
|
for rows.Next() {
|
|
var id int64
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
out[id] = true
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// OznačiKaoStorniran postavlja storniran=1 za fiskalni račun sa datim ID-jem.
|
|
func (r *FiskalRepo) OznačiKaoStorniran(ctx context.Context, id int64) error {
|
|
_, err := r.db.ExecContext(ctx, "UPDATE fiskalni_racuni SET storniran = 1 WHERE id = ?", id)
|
|
if err != nil {
|
|
return fmt.Errorf("ntech: FiskalRepo.OznačiKaoStorniran: %w", err)
|
|
}
|
|
return nil
|
|
}
|