diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index d602bd5..99421aa 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -315,6 +315,7 @@ func main() { r.With(modul("pdv")).Get("/pdv/kpr/nova", h.NoviPdvKpr) r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kpr/nova", h.SacuvajPdvKpr) r.With(modul("pdv"), doz("pdv.obrisi")).Post("/pdv/kpr/obrisi/{id}", h.ObrisiPdvKpr) + r.With(modul("pdv"), doz("pdv.dodaj")).Post("/pdv/kpr/backfill-nabavke", h.KprBackfillNabavke) r.With(modul("pdv")).Get("/pdv/obracun", h.PdvObracunStranica) r.Get("/magacin", h.Magacin) r.Get("/usluge", h.Usluge) diff --git a/internal/db/repository.go b/internal/db/repository.go index b688d23..502c264 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -78,6 +78,8 @@ type PdvKprRepository interface { Obrisi(ctx context.Context, id int64) error // ObrisiPoIzvoru briše zapise vezane za dati izvor (npr. pri brisanju nabavke) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error + // PostojiZaIzvor vraća true ako postoji bar jedan zapis za dati izvor i izvorID + PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error) } // NivelacijaRepository definiše operacije nad evidencijom promene prodajnih cena diff --git a/internal/db/sqlite/nabavka.go b/internal/db/sqlite/nabavka.go index bb33ce7..dae96cf 100644 --- a/internal/db/sqlite/nabavka.go +++ b/internal/db/sqlite/nabavka.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "time" "ntech/internal/model" ) @@ -24,6 +25,7 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err SELECT n.id, n.dobavljac_id, n.napomena, n.ukupno, n.metod_raspodele, n.stornirano, n.razlog_storniranja, n.datum, + n.broj_racuna, n.datum_racuna, n.pdv_iznos, COALESCE(d.naziv, '') AS dobavljac_naziv FROM nabavke n LEFT JOIN dobavljaci d ON n.dobavljac_id = d.id @@ -37,11 +39,12 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err for redovi.Next() { var n model.NabavkaSaDetaljem var dobavljacID sql.NullInt64 - var napomena, metod, razlogStorniranja sql.NullString + var napomena, metod, razlogStorniranja, brojRacuna, datumRacuna sql.NullString err := redovi.Scan( &n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod, &n.Stornirano, &razlogStorniranja, &n.Datum, + &brojRacuna, &datumRacuna, &n.PdvIznos, &n.DobavljacNaziv, ) if err != nil { @@ -54,6 +57,12 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err n.Napomena = napomena.String n.MetodRaspodele = metod.String n.RazlogStorniranja = razlogStorniranja.String + n.BrojRacuna = brojRacuna.String + if datumRacuna.Valid && datumRacuna.String != "" { + if t, e := time.Parse("2006-01-02", datumRacuna.String[:10]); e == nil { + n.DatumRacuna = &t + } + } rezultat = append(rezultat, n) } @@ -65,14 +74,16 @@ func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, err func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error) { var n model.Nabavka var dobavljacID sql.NullInt64 - var napomena, metod, razlogStorniranja sql.NullString + var napomena, metod, razlogStorniranja, brojRacuna, datumRacuna sql.NullString err := r.db.QueryRowContext(ctx, ` SELECT id, dobavljac_id, napomena, ukupno, metod_raspodele, - stornirano, razlog_storniranja, datum + stornirano, razlog_storniranja, datum, + broj_racuna, datum_racuna, pdv_iznos FROM nabavke WHERE id = ?`, id).Scan( &n.ID, &dobavljacID, &napomena, &n.Ukupno, &metod, &n.Stornirano, &razlogStorniranja, &n.Datum, + &brojRacuna, &datumRacuna, &n.PdvIznos, ) if err != nil { return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiID: %w", err) @@ -84,6 +95,12 @@ func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka, n.Napomena = napomena.String n.MetodRaspodele = metod.String n.RazlogStorniranja = razlogStorniranja.String + n.BrojRacuna = brojRacuna.String + if datumRacuna.Valid && datumRacuna.String != "" { + if t, e := time.Parse("2006-01-02", datumRacuna.String[:10]); e == nil { + n.DatumRacuna = &t + } + } return &n, nil } @@ -163,9 +180,10 @@ func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []mo // upisujemo zaglavlje nabavke rezultat, err := tx.ExecContext(ctx, ` - INSERT INTO nabavke (dobavljac_id, napomena, ukupno, metod_raspodele) - VALUES (?, ?, ?, ?)`, + INSERT INTO nabavke (dobavljac_id, napomena, ukupno, metod_raspodele, broj_racuna, datum_racuna, pdv_iznos) + VALUES (?, ?, ?, ?, ?, ?, ?)`, nullInt64(n.DobavljacID), nullString(n.Napomena), ukupno, nullString(n.MetodRaspodele), + nullString(n.BrojRacuna), nullDateString(n.DatumRacuna), n.PdvIznos, ) if err != nil { return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: insert nabavka: %w", err) diff --git a/internal/db/sqlite/pdv_evidencija.go b/internal/db/sqlite/pdv_evidencija.go index f6a7ace..7e29538 100644 --- a/internal/db/sqlite/pdv_evidencija.go +++ b/internal/db/sqlite/pdv_evidencija.go @@ -292,3 +292,15 @@ func (r *PdvKprRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID i } return nil } + +// PostojiZaIzvor vraća true ako postoji bar jedan KPR zapis za dati izvor i izvorID +func (r *PdvKprRepo) PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error) { + var n int + err := r.db.QueryRowContext(ctx, + "SELECT COUNT(*) FROM pdv_kpr WHERE izvor = ? AND izvor_id = ?", izvor, izvorID, + ).Scan(&n) + if err != nil { + return false, fmt.Errorf("ntech: PdvKprRepo.PostojiZaIzvor: %w", err) + } + return n > 0, nil +} diff --git a/internal/db/sqlite/prodaja.go b/internal/db/sqlite/prodaja.go index 56ff91a..7745a3a 100644 --- a/internal/db/sqlite/prodaja.go +++ b/internal/db/sqlite/prodaja.go @@ -51,8 +51,8 @@ func (r *ProdajaRepo) Lista(ctx context.Context, pretraga string) ([]model.Proda args := []any{} if pretraga != "" { - upit += " AND pn.broj_naloga LIKE ?" - args = append(args, "%"+pretraga+"%") + upit += " AND (pn.broj_naloga LIKE ? OR kp.naziv LIKE ? OR pn.napomena LIKE ?)" + args = append(args, "%"+pretraga+"%", "%"+pretraga+"%", "%"+pretraga+"%") } upit += " ORDER BY pn.datum DESC" @@ -122,10 +122,13 @@ func (r *ProdajaRepo) DohvatiID(ctx context.Context, id int64) (*model.ProdajniN // DohvatiStavke vraća stavke prodaje sa nazivima artikala i PDV podacima za dati nalog func (r *ProdajaRepo) DohvatiStavke(ctx context.Context, nalogID int64) ([]model.StavkaProdajeSaArtiklom, error) { redovi, err := r.db.QueryContext(ctx, ` - SELECT sp.id, sp.nalog_id, sp.artikal_id, sp.kolicina, sp.cena_po_komadu, sp.ukupno, - sp.pdv_stopa, sp.pdv_iznos, sp.cena_bez_pdv, a.naziv, a.jedinica_mere + SELECT sp.id, sp.nalog_id, sp.artikal_id, sp.kolicina, sp.cena_po_komadu, + sp.popust_procenat, sp.ukupno, + sp.pdv_stopa, sp.pdv_iznos, sp.cena_bez_pdv, + a.naziv, a.jedinica_mere, COALESCE(k.naziv, '') AS kategorija_naziv FROM stavke_prodaje sp JOIN artikli a ON a.id = sp.artikal_id + LEFT JOIN kategorije k ON k.id = a.kategorija_id WHERE sp.nalog_id = ? ORDER BY sp.id`, nalogID) if err != nil { @@ -138,9 +141,9 @@ func (r *ProdajaRepo) DohvatiStavke(ctx context.Context, nalogID int64) ([]model var s model.StavkaProdajeSaArtiklom err := redovi.Scan( &s.ID, &s.NalogID, &s.ArtikalID, &s.Kolicina, - &s.CenaPoKomadu, &s.Ukupno, + &s.CenaPoKomadu, &s.PopustProcenat, &s.Ukupno, &s.PdvStopa, &s.PdvIznos, &s.CenaBezPdv, - &s.ArtikalNaziv, &s.JedinicaMere, + &s.ArtikalNaziv, &s.JedinicaMere, &s.KategorijaNaziv, ) if err != nil { return nil, fmt.Errorf("ntech: ProdajaRepo.DohvatiStavke: scan: %w", err) @@ -215,12 +218,17 @@ func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavk pdvIznos = cenaBezPdv * s.PdvStopa / 100 } - ukupnoStavke := float64(s.Kolicina) * s.CenaPoKomadu + // popust umanjuje cenu po komadu pre računanja ukupnog i PDV-a + cenaPoslePopusta := s.CenaPoKomadu + if s.PopustProcenat > 0 { + cenaPoslePopusta = cenaPoslePopusta * (1 - s.PopustProcenat/100) + } + ukupnoStavke := float64(s.Kolicina) * cenaPoslePopusta _, err = tx.ExecContext(ctx, ` INSERT INTO stavke_prodaje - (nalog_id, artikal_id, kolicina, cena_po_komadu, ukupno, pdv_stopa, pdv_iznos, cena_bez_pdv) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - nalogID, s.ArtikalID, s.Kolicina, s.CenaPoKomadu, ukupnoStavke, + (nalog_id, artikal_id, kolicina, cena_po_komadu, popust_procenat, ukupno, pdv_stopa, pdv_iznos, cena_bez_pdv) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + nalogID, s.ArtikalID, s.Kolicina, s.CenaPoKomadu, s.PopustProcenat, ukupnoStavke, s.PdvStopa, pdvIznos, cenaBezPdv, ) if err != nil { diff --git a/internal/db/sqlite/utils.go b/internal/db/sqlite/utils.go index 5ff9a3e..4f058ab 100644 --- a/internal/db/sqlite/utils.go +++ b/internal/db/sqlite/utils.go @@ -46,3 +46,12 @@ func nullTime(v *time.Time) sql.NullTime { } return sql.NullTime{Time: *v, Valid: true} } + +// nullDateString upisuje *time.Time kao "YYYY-MM-DD" string ili NULL — +// koristiti za DATE kolone gde sql.NullTime nije kompatibilan s driver-om +func nullDateString(v *time.Time) sql.NullString { + if v == nil { + return sql.NullString{} + } + return sql.NullString{String: v.Format("2006-01-02"), Valid: true} +} diff --git a/internal/handler/nabavka.go b/internal/handler/nabavka.go index 1706ea3..90f7ed0 100644 --- a/internal/handler/nabavka.go +++ b/internal/handler/nabavka.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" "strings" + "time" "ntech/internal/db" "ntech/internal/db/sqlite" @@ -32,6 +33,7 @@ type PodaciFormeNabavke struct { Kategorije []model.Kategorija // za dropdown u modalu novog artikla Marza string // podrazumevana marža (%) za kalkulaciju PdvObveznik bool // da li firma obračunava PDV (utiče na prodajnu cenu u kalkulaciji) + Danas string // format "2006-01-02" — default za datum računa Greska string } @@ -44,6 +46,7 @@ type PodaciDetaljiNabavke struct { UkupanTrosak float64 DobavljacNaziv string Sacuvano bool + Greska string } // artikalUJSON pretvara listu artikala u template.JS vrednost bezbednu za umetanje u
- -
-
- Nova prodaja +
+ Nova prodaja
-
+
+
-
- Stavke -
+ +
+ +
+
- - - - - + + + + + +
- Artikal - - Količina - - Cena/kom (din) - - Ukupno - ArtikalKol.Cena/komPopust%Ukupno