Dodavanje modula prodaje — lista, forma, detalji, provera stanja, vraćanje pri brisanju

This commit is contained in:
2026-06-02 17:53:50 +02:00
parent d3b4bb329d
commit def84e1a69
10 changed files with 1249 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
package db
// ErrNedovoljnoKolicine se vraća kada prodaja traži više nego što ima na stanju
type ErrNedovoljnoKolicine struct {
ArtikalNaziv string
}
func (e *ErrNedovoljnoKolicine) Error() string {
return "Nedovoljno količine na stanju za artikal: " + e.ArtikalNaziv
}
+10
View File
@@ -64,3 +64,13 @@ type ServisRepository interface {
Obrisi(ctx context.Context, id int64) error
SledeciBroj(ctx context.Context) (string, error)
}
// ProdajaRepository definiše operacije nad prodajnim nalozima
type ProdajaRepository interface {
Lista(ctx context.Context, pretraga string) ([]model.ProdajniNalogSaDetaljem, error)
DohvatiID(ctx context.Context, id int64) (*model.ProdajniNalog, error)
DohvatiStavke(ctx context.Context, nalogID int64) ([]model.StavkaProdajeSaArtiklom, error)
Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavke []model.StavkaProdaje) (int64, error)
Obrisi(ctx context.Context, id int64) error
SledeciBroj(ctx context.Context) (string, error)
}
+262
View File
@@ -0,0 +1,262 @@
package sqlite
import (
"context"
"database/sql"
"fmt"
"time"
"ntech/internal/db"
"ntech/internal/model"
)
// ProdajaRepo je SQLite implementacija ProdajaRepository interfejsa
type ProdajaRepo struct {
db *sql.DB
}
// NoviProdajaRepo kreira novi ProdajaRepo
func NoviProdajaRepo(db *sql.DB) *ProdajaRepo {
return &ProdajaRepo{db: db}
}
// SledeciBroj generiše sledeći broj naloga u formatu PR-GGGG-NNNN
func (r *ProdajaRepo) SledeciBroj(ctx context.Context) (string, error) {
godina := time.Now().Year()
uzorak := fmt.Sprintf("PR-%d-%%", godina)
var sledeci int
err := r.db.QueryRowContext(ctx, `
SELECT COALESCE(MAX(CAST(SUBSTR(broj_naloga, 9) AS INTEGER)), 0) + 1
FROM prodajni_nalozi
WHERE broj_naloga LIKE ?`, uzorak).Scan(&sledeci)
if err != nil {
return "", fmt.Errorf("ntech: ProdajaRepo.SledeciBroj: %w", err)
}
return fmt.Sprintf("PR-%d-%04d", godina, sledeci), nil
}
// Lista vraća listu prodajnih naloga sa imenom klijenta, opcionom pretragom
func (r *ProdajaRepo) Lista(ctx context.Context, pretraga string) ([]model.ProdajniNalogSaDetaljem, error) {
upit := `
SELECT
pn.id, pn.klijent_id, pn.broj_naloga, pn.napomena, pn.ukupno, pn.datum,
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '') AS klijent_naziv
FROM prodajni_nalozi pn
LEFT JOIN klijenti k ON k.id = pn.klijent_id
WHERE 1=1`
args := []any{}
if pretraga != "" {
upit += " AND pn.broj_naloga LIKE ?"
args = append(args, "%"+pretraga+"%")
}
upit += " ORDER BY pn.datum DESC"
redovi, err := r.db.QueryContext(ctx, upit, args...)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.Lista: %w", err)
}
defer redovi.Close()
var rezultat []model.ProdajniNalogSaDetaljem
for redovi.Next() {
var n model.ProdajniNalogSaDetaljem
var klijentID sql.NullInt64
var napomena sql.NullString
err := redovi.Scan(
&n.ID, &klijentID, &n.BrojNaloga, &napomena, &n.Ukupno, &n.Datum,
&n.KlijentNaziv,
)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.Lista: scan: %w", err)
}
if klijentID.Valid {
v := klijentID.Int64
n.KlijentID = &v
}
n.Napomena = napomena.String
rezultat = append(rezultat, n)
}
return rezultat, nil
}
// DohvatiID vraća jedan prodajni nalog po ID-u
func (r *ProdajaRepo) DohvatiID(ctx context.Context, id int64) (*model.ProdajniNalog, error) {
red := r.db.QueryRowContext(ctx, `
SELECT id, klijent_id, broj_naloga, napomena, ukupno, datum
FROM prodajni_nalozi WHERE id = ?`, id)
var n model.ProdajniNalog
var klijentID sql.NullInt64
var napomena sql.NullString
err := red.Scan(&n.ID, &klijentID, &n.BrojNaloga, &napomena, &n.Ukupno, &n.Datum)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.DohvatiID: %w", err)
}
if klijentID.Valid {
v := klijentID.Int64
n.KlijentID = &v
}
n.Napomena = napomena.String
return &n, nil
}
// DohvatiStavke vraća stavke prodaje sa nazivima artikala 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,
a.naziv
FROM stavke_prodaje sp
JOIN artikli a ON a.id = sp.artikal_id
WHERE sp.nalog_id = ?
ORDER BY sp.id`, nalogID)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.DohvatiStavke: %w", err)
}
defer redovi.Close()
var stavke []model.StavkaProdajeSaArtiklom
for redovi.Next() {
var s model.StavkaProdajeSaArtiklom
err := redovi.Scan(
&s.ID, &s.NalogID, &s.ArtikalID, &s.Kolicina,
&s.CenaPoKomadu, &s.Ukupno, &s.ArtikalNaziv,
)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.DohvatiStavke: scan: %w", err)
}
stavke = append(stavke, s)
}
return stavke, nil
}
// Kreiraj upisuje novi prodajni nalog u bazu u okviru jedne transakcije.
// Za svaku stavku proverava stanje u magacinu i smanjuje ga.
// Ako bilo koji artikal nema dovoljno stanja, vraća ErrNedovoljnoKolicine.
func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavke []model.StavkaProdaje) (int64, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: begin tx: %w", err)
}
defer tx.Rollback()
// provera i smanjenje stanja za svaku stavku
for _, s := range stavke {
var naziv string
var kolicinaNaStanju int
err := tx.QueryRowContext(ctx,
"SELECT naziv, kolicina FROM artikli WHERE id = ?", s.ArtikalID,
).Scan(&naziv, &kolicinaNaStanju)
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: dohvati artikal: %w", err)
}
if kolicinaNaStanju < s.Kolicina {
return 0, &db.ErrNedovoljnoKolicine{ArtikalNaziv: naziv}
}
_, err = tx.ExecContext(ctx,
"UPDATE artikli SET kolicina = kolicina - ? WHERE id = ?",
s.Kolicina, s.ArtikalID,
)
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: update stanje: %w", err)
}
}
// insert zaglavlja naloga
rezultat, err := tx.ExecContext(ctx, `
INSERT INTO prodajni_nalozi (klijent_id, broj_naloga, napomena, ukupno, datum)
VALUES (?, ?, ?, ?, ?)`,
nullInt64(n.KlijentID), n.BrojNaloga, nullString(n.Napomena), n.Ukupno, n.Datum,
)
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: insert nalog: %w", err)
}
nalogID, err := rezultat.LastInsertId()
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: last insert id: %w", err)
}
// insert stavki
for _, s := range stavke {
ukupnoStavke := float64(s.Kolicina) * s.CenaPoKomadu
_, err := tx.ExecContext(ctx, `
INSERT INTO stavke_prodaje (nalog_id, artikal_id, kolicina, cena_po_komadu, ukupno)
VALUES (?, ?, ?, ?, ?)`,
nalogID, s.ArtikalID, s.Kolicina, s.CenaPoKomadu, ukupnoStavke,
)
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: insert stavka: %w", err)
}
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: commit: %w", err)
}
return nalogID, nil
}
// Obrisi briše prodajni nalog i vraća količine artikala na stanje (u transakciji)
func (r *ProdajaRepo) Obrisi(ctx context.Context, id int64) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: begin tx: %w", err)
}
defer tx.Rollback()
// vraćanje stanja u magacin
redovi, err := tx.QueryContext(ctx,
"SELECT artikal_id, kolicina FROM stavke_prodaje WHERE nalog_id = ?", id)
if err != nil {
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: dohvati stavke: %w", err)
}
type povrat struct{ artikalID int64; kolicina int }
var stavke []povrat
for redovi.Next() {
var p povrat
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
redovi.Close()
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: scan stavke: %w", err)
}
stavke = append(stavke, p)
}
redovi.Close()
for _, p := range stavke {
_, err := tx.ExecContext(ctx,
"UPDATE artikli SET kolicina = kolicina + ? WHERE id = ?",
p.kolicina, p.artikalID,
)
if err != nil {
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: vrati stanje: %w", err)
}
}
// CASCADE briše i stavke_prodaje
_, err = tx.ExecContext(ctx, "DELETE FROM prodajni_nalozi WHERE id = ?", id)
if err != nil {
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: delete: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: commit: %w", err)
}
return nil
}