feat(kalkulacija): Faza C — marža po kategoriji/artiklu + zavisni troškovi (backend)
Celina 1 (kompletna) — marža po kategoriji/artiklu: - migracija 046: nullable marza na artikli i kategorije - model Marza *float64 (Artikal, Kategorija) + KategorijaMarza u ArtikalSaKategorijom - repo: čitanje/pisanje marže; nove DohvatiID/Izmeni za kategoriju - dozvola kategorija.izmeni; handler IzmeniKategoriju + ruta - UI: polje marže u formi artikla i kategorije; modal izmene kategorije - nabavka: fallback predlog marže artikal → kategorija → globalna (izaberiArtikal) Celina 2 (backend) — zavisni troškovi nabavke: - migracija 047: tabela nabavka_troskovi + kolona metod_raspodele na nabavke - model NabavkaTrosak, MetodRaspodele; čista funkcija RasporediTroskove + test - repo: Kreiraj upisuje troškove i metod; DohvatiTroskove - handler: parsiranje troškova/metoda; kalkulativna nabavna cena na serveru UI forme troškova i prikaz u detaljima nabavke slede.
This commit is contained in:
@@ -14,6 +14,7 @@ type Artikal struct {
|
||||
NabavnaCena float64
|
||||
ProdajnaCena float64
|
||||
PdvStopa float64
|
||||
Marza *float64 // podrazumevana marža (%) za kalkulaciju; NULL = nije postavljeno
|
||||
Napomena string
|
||||
DatumUnosa time.Time
|
||||
}
|
||||
@@ -33,11 +34,13 @@ type Kategorija struct {
|
||||
ID int64
|
||||
Naziv string
|
||||
Opis string
|
||||
Marza *float64 // podrazumevana marža (%) za artikle ove kategorije; NULL = nije postavljeno
|
||||
}
|
||||
|
||||
// ArtikalSaKategorijom je artikal sa nazivom kategorije — za prikaz u tabeli
|
||||
type ArtikalSaKategorijom struct {
|
||||
Artikal
|
||||
KategorijaNaziv string
|
||||
KategorijaMarza *float64 // marža kategorije; za fallback predloga marže pri nabavci
|
||||
KriticnaZaliha bool
|
||||
}
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Nabavka predstavlja zaglavlje jedne nabavke
|
||||
type Nabavka struct {
|
||||
ID int64
|
||||
DobavljacID *int64
|
||||
Napomena string
|
||||
Ukupno float64
|
||||
Datum time.Time
|
||||
ID int64
|
||||
DobavljacID *int64
|
||||
Napomena string
|
||||
Ukupno float64
|
||||
MetodRaspodele string // "vrednost" ili "kolicina"; prazno = nema zavisnih troškova
|
||||
Datum time.Time
|
||||
}
|
||||
|
||||
// NabavkaTrosak je jedna stavka zavisnih troškova nabavke (npr. prevoz, carina)
|
||||
type NabavkaTrosak struct {
|
||||
ID int64
|
||||
NabavkaID int64
|
||||
Naziv string
|
||||
Iznos float64
|
||||
}
|
||||
|
||||
// StavkaNabavke predstavlja jednu liniju (artikal) unutar nabavke
|
||||
@@ -21,6 +33,44 @@ type StavkaNabavke struct {
|
||||
Ukupno float64
|
||||
}
|
||||
|
||||
// RasporediTroskove raspodeljuje ukupan zavisni trošak na stavke nabavke i vraća
|
||||
// kalkulativnu nabavnu cenu po komadu za svaku stavku (isti redosled kao ulazne stavke).
|
||||
// metod "kolicina" deli trošak po broju komada; svaka druga vrednost (uklj. "vrednost")
|
||||
// deli po nabavnoj vrednosti stavke (količina × cena).
|
||||
// Ako nema troška ili je osnovica nula, vraća nepromenjenu cenu po komadu.
|
||||
func RasporediTroskove(stavke []StavkaNabavke, ukupanTrosak float64, metod string) []float64 {
|
||||
kalk := make([]float64, len(stavke))
|
||||
|
||||
// osnovica raspodele po izabranom metodu
|
||||
var osnovica float64
|
||||
for _, s := range stavke {
|
||||
if metod == "kolicina" {
|
||||
osnovica += float64(s.Kolicina)
|
||||
} else {
|
||||
osnovica += float64(s.Kolicina) * s.CenaPoKomadu
|
||||
}
|
||||
}
|
||||
|
||||
for i, s := range stavke {
|
||||
kalk[i] = s.CenaPoKomadu
|
||||
// bez troška, bez osnovice ili bez količine — nema šta da se raspodeli
|
||||
if ukupanTrosak <= 0 || osnovica <= 0 || s.Kolicina == 0 {
|
||||
continue
|
||||
}
|
||||
var udeo float64
|
||||
if metod == "kolicina" {
|
||||
udeo = float64(s.Kolicina) / osnovica
|
||||
} else {
|
||||
udeo = (float64(s.Kolicina) * s.CenaPoKomadu) / osnovica
|
||||
}
|
||||
trosakPoKomadu := (ukupanTrosak * udeo) / float64(s.Kolicina)
|
||||
// zaokruženo na 2 decimale — kalkulativna nabavna je cena koja se čuva
|
||||
kalk[i] = math.Round((s.CenaPoKomadu+trosakPoKomadu)*100) / 100
|
||||
}
|
||||
|
||||
return kalk
|
||||
}
|
||||
|
||||
// NabavkaSaDetaljem je nabavka sa nazivom dobavljača — za prikaz u listi
|
||||
type NabavkaSaDetaljem struct {
|
||||
Nabavka
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
// proverava raspodelu zavisnih troškova na stavke nabavke po oba metoda
|
||||
func TestRasporediTroskove(t *testing.T) {
|
||||
// dve stavke: A = 10×100 (vrednost 1000), B = 10×200 (vrednost 2000)
|
||||
stavke := []StavkaNabavke{
|
||||
{Kolicina: 10, CenaPoKomadu: 100},
|
||||
{Kolicina: 10, CenaPoKomadu: 200},
|
||||
}
|
||||
|
||||
slucajevi := []struct {
|
||||
naziv string
|
||||
stavke []StavkaNabavke
|
||||
trosak float64
|
||||
metod string
|
||||
ocekuje []float64
|
||||
}{
|
||||
{
|
||||
naziv: "bez troška — cena nepromenjena",
|
||||
stavke: stavke,
|
||||
trosak: 0,
|
||||
metod: "vrednost",
|
||||
ocekuje: []float64{100, 200},
|
||||
},
|
||||
{
|
||||
// trošak 300 po vrednosti: A nosi 1/3 (100 → 10/kom), B nosi 2/3 (200 → 20/kom)
|
||||
naziv: "po vrednosti",
|
||||
stavke: stavke,
|
||||
trosak: 300,
|
||||
metod: "vrednost",
|
||||
ocekuje: []float64{110, 220},
|
||||
},
|
||||
{
|
||||
// trošak 300 po količini: svaka stavka 1/2 (150 → 15/kom)
|
||||
naziv: "po količini",
|
||||
stavke: stavke,
|
||||
trosak: 300,
|
||||
metod: "kolicina",
|
||||
ocekuje: []float64{115, 215},
|
||||
},
|
||||
{
|
||||
// osnovica po vrednosti je 0 (sve cene 0) — nema raspodele
|
||||
naziv: "nulta osnovica vrednosti",
|
||||
stavke: []StavkaNabavke{{Kolicina: 5, CenaPoKomadu: 0}},
|
||||
trosak: 100,
|
||||
metod: "vrednost",
|
||||
ocekuje: []float64{0},
|
||||
},
|
||||
{
|
||||
// stavka bez količine se preskače (bez deljenja nulom)
|
||||
naziv: "količina nula",
|
||||
stavke: []StavkaNabavke{{Kolicina: 0, CenaPoKomadu: 50}},
|
||||
trosak: 100,
|
||||
metod: "kolicina",
|
||||
ocekuje: []float64{50},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range slucajevi {
|
||||
t.Run(s.naziv, func(t *testing.T) {
|
||||
dobijeno := RasporediTroskove(s.stavke, s.trosak, s.metod)
|
||||
if len(dobijeno) != len(s.ocekuje) {
|
||||
t.Fatalf("dužina = %d, očekivano %d", len(dobijeno), len(s.ocekuje))
|
||||
}
|
||||
for i := range s.ocekuje {
|
||||
if dobijeno[i] != s.ocekuje[i] {
|
||||
t.Errorf("stavka %d: dobijeno %.2f, očekivano %.2f", i, dobijeno[i], s.ocekuje[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user