Fix product performance grouped JSON build
This commit is contained in:
@@ -1901,6 +1901,7 @@ func applyProductPerformanceProductRowScores(rows []models.ProductPerformanceRow
|
||||
rows[i].StockTurnoverTotal,
|
||||
float64(rows[i].MarketCountTotal),
|
||||
float64(rows[i].CustomerCountTotal),
|
||||
productPerformanceProductRowTotalPeriodDays(rows[i]),
|
||||
)
|
||||
rows[i].PerformanceScore = rows[i].PerformanceScore90
|
||||
}
|
||||
@@ -1949,6 +1950,10 @@ func productPerformanceProductRowAverages(rows []models.ProductPerformanceRow) p
|
||||
return out
|
||||
}
|
||||
|
||||
func productPerformanceProductRowTotalPeriodDays(row models.ProductPerformanceRow) float64 {
|
||||
return productPerformancePeriodDays(map[string]any{"kpi_date": row.KpiDate}, "total")
|
||||
}
|
||||
|
||||
func productPerformanceMarginFromSales(salesUSD, salesQty, unitCost float64) float64 {
|
||||
if salesUSD <= 0 {
|
||||
return 0
|
||||
@@ -2286,7 +2291,7 @@ SELECT
|
||||
WHEN sales_qty_total > 0 AND stock_qty <= 0 AND sales_index_total >= 1 THEN 'STOKSUZ_TALEP'
|
||||
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'STOK_RISKI'
|
||||
WHEN gross_margin_total < 0 THEN 'FIYAT_BASKISI'
|
||||
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'YILDIZ_URUN'
|
||||
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'YILDIZ_URUN'
|
||||
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'FIYAT_FIRSATI'
|
||||
ELSE 'TAKIP'
|
||||
END AS performance_bucket,
|
||||
@@ -2294,7 +2299,7 @@ SELECT
|
||||
WHEN sales_qty_total > 0 AND stock_qty <= 0 THEN 'Talep var, stok yok. Uretim/satin alma onceligi ver.'
|
||||
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'Satis yok, stok maliyeti tasiyor. Piyasa/fiyat aksiyonu gerekli.'
|
||||
WHEN gross_margin_total < 0 THEN 'Ciplak maliyet altinda satis var. Fiyat veya maliyet kontrol edilmeli.'
|
||||
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
|
||||
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
|
||||
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'Karli ama yavas. Dogru piyasada satis firsati var.'
|
||||
ELSE 'Izleme ve piyasa bazli aksiyon.'
|
||||
END AS recommendation,
|
||||
@@ -3581,6 +3586,19 @@ StockTotalStart AS (
|
||||
WHERE stock_date <= DATE '2022-01-01'
|
||||
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
|
||||
),
|
||||
FirstSaleByProduct AS (
|
||||
SELECT
|
||||
product_code,
|
||||
color_code,
|
||||
yaka_kodu,
|
||||
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days') AS first_sale_date_90d,
|
||||
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days') AS first_sale_date_180d,
|
||||
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days') AS first_sale_date_365d,
|
||||
MIN(sales_date) FILTER (WHERE sales_date >= DATE '2022-01-01') AS first_sale_date_total
|
||||
FROM mk_product_performance_sales_daily
|
||||
WHERE sales_date >= DATE '2022-01-01'
|
||||
GROUP BY product_code, color_code, yaka_kodu
|
||||
),
|
||||
Agg AS (
|
||||
SELECT
|
||||
$2::text AS breakdown,
|
||||
@@ -3683,18 +3701,55 @@ Agg AS (
|
||||
COALESCE(pd.base_price_usd,0) AS base_price_usd,
|
||||
COALESCE(pd.cost_price_usd,0) AS cost_price_usd,
|
||||
COALESCE(st.stock_qty,0) AS stock_qty,
|
||||
(COALESCE(s90.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_90d,
|
||||
(COALESCE(s180.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_180d,
|
||||
(COALESCE(s365.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_365d,
|
||||
(COALESCE(stotal.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_total
|
||||
(COALESCE(s90.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_90d,
|
||||
(COALESCE(s180.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_180d,
|
||||
(COALESCE(s365.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_365d,
|
||||
(COALESCE(stotal.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_total
|
||||
FROM mk_product_performance_sales_daily s
|
||||
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code = s.product_code
|
||||
LEFT JOIN LatestKPI k ON k.product_code = s.product_code AND k.color_code = s.color_code AND k.yaka_kodu = s.yaka_kodu
|
||||
LEFT JOIN FirstSaleByProduct fs ON fs.product_code = s.product_code AND fs.color_code = s.color_code AND fs.yaka_kodu = s.yaka_kodu
|
||||
LEFT JOIN StockAgg st ON st.product_code = s.product_code AND st.color_code = s.color_code AND st.yaka_kodu = s.yaka_kodu
|
||||
LEFT JOIN Stock90Start s90 ON s90.product_code = s.product_code AND s90.color_code = s.color_code AND s90.yaka_kodu = s.yaka_kodu
|
||||
LEFT JOIN Stock180Start s180 ON s180.product_code = s.product_code AND s180.color_code = s.color_code AND s180.yaka_kodu = s.yaka_kodu
|
||||
LEFT JOIN Stock365Start s365 ON s365.product_code = s.product_code AND s365.color_code = s.color_code AND s365.yaka_kodu = s.yaka_kodu
|
||||
LEFT JOIN StockTotalStart stotal ON stotal.product_code = s.product_code AND stotal.color_code = s.color_code AND stotal.yaka_kodu = s.yaka_kodu
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.stock_qty
|
||||
FROM mk_product_performance_stock_daily x
|
||||
WHERE x.product_code = s.product_code
|
||||
AND x.color_code = s.color_code
|
||||
AND x.yaka_kodu = s.yaka_kodu
|
||||
AND x.stock_date <= COALESCE(fs.first_sale_date_90d, current_date - INTERVAL '89 days')
|
||||
ORDER BY x.stock_date DESC
|
||||
LIMIT 1
|
||||
) s90 ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.stock_qty
|
||||
FROM mk_product_performance_stock_daily x
|
||||
WHERE x.product_code = s.product_code
|
||||
AND x.color_code = s.color_code
|
||||
AND x.yaka_kodu = s.yaka_kodu
|
||||
AND x.stock_date <= COALESCE(fs.first_sale_date_180d, current_date - INTERVAL '179 days')
|
||||
ORDER BY x.stock_date DESC
|
||||
LIMIT 1
|
||||
) s180 ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.stock_qty
|
||||
FROM mk_product_performance_stock_daily x
|
||||
WHERE x.product_code = s.product_code
|
||||
AND x.color_code = s.color_code
|
||||
AND x.yaka_kodu = s.yaka_kodu
|
||||
AND x.stock_date <= COALESCE(fs.first_sale_date_365d, current_date - INTERVAL '359 days')
|
||||
ORDER BY x.stock_date DESC
|
||||
LIMIT 1
|
||||
) s365 ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.stock_qty
|
||||
FROM mk_product_performance_stock_daily x
|
||||
WHERE x.product_code = s.product_code
|
||||
AND x.color_code = s.color_code
|
||||
AND x.yaka_kodu = s.yaka_kodu
|
||||
AND x.stock_date <= COALESCE(fs.first_sale_date_total, DATE '2022-01-01')
|
||||
ORDER BY x.stock_date DESC
|
||||
LIMIT 1
|
||||
) stotal ON TRUE
|
||||
) s
|
||||
WHERE sales_date >= DATE '2022-01-01'
|
||||
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
||||
@@ -3813,7 +3868,7 @@ SELECT
|
||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
|
||||
ROUND((
|
||||
LEAST(35, sales_usd_90d / 25000 * 35)
|
||||
+ LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30)
|
||||
+ LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.45 * 30)
|
||||
+ LEAST(20, product_group_count_90d / 8.0 * 20)
|
||||
+ LEAST(15, sales_qty_90d / 500 * 15)
|
||||
)::numeric, 4)
|
||||
@@ -3821,7 +3876,7 @@ SELECT
|
||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_180d <= 0 THEN 1 ELSE
|
||||
ROUND((
|
||||
LEAST(35, sales_usd_180d / 50000 * 35)
|
||||
+ LEAST(30, GREATEST(gross_margin_cost_180d,0) / 0.40 * 30)
|
||||
+ LEAST(30, GREATEST(gross_margin_cost_180d,0) / 0.45 * 30)
|
||||
+ LEAST(20, product_group_count_180d / 8.0 * 20)
|
||||
+ LEAST(15, sales_qty_180d / 1000 * 15)
|
||||
)::numeric, 4)
|
||||
@@ -3829,7 +3884,7 @@ SELECT
|
||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_365d <= 0 THEN 1 ELSE
|
||||
ROUND((
|
||||
LEAST(35, sales_usd_365d / 100000 * 35)
|
||||
+ LEAST(30, GREATEST(gross_margin_cost_365d,0) / 0.40 * 30)
|
||||
+ LEAST(30, GREATEST(gross_margin_cost_365d,0) / 0.45 * 30)
|
||||
+ LEAST(20, product_group_count_365d / 8.0 * 20)
|
||||
+ LEAST(15, sales_qty_365d / 2000 * 15)
|
||||
)::numeric, 4)
|
||||
@@ -3837,18 +3892,18 @@ SELECT
|
||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_total <= 0 THEN 1 ELSE
|
||||
ROUND((
|
||||
LEAST(35, sales_usd_total / 300000 * 35)
|
||||
+ LEAST(30, GREATEST(gross_margin_cost_total,0) / 0.40 * 30)
|
||||
+ LEAST(30, GREATEST(gross_margin_cost_total,0) / 0.45 * 30)
|
||||
+ LEAST(20, product_group_count_total / 8.0 * 20)
|
||||
+ LEAST(15, sales_qty_total / 6000 * 15)
|
||||
)::numeric, 4)
|
||||
END AS customer_score_total,
|
||||
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
|
||||
ROUND(
|
||||
LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30)
|
||||
LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.45 * 30)
|
||||
+ LEAST(20, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END) / 4.0 * 20)
|
||||
+ LEAST(20, sales_usd_90d / 10000 * 20)
|
||||
+ LEAST(15, market_count_90d / 8.0 * 15)
|
||||
+ LEAST(15, customer_count_90d / 25.0 * 15),
|
||||
+ LEAST(20, sales_usd_90d / 25000 * 20)
|
||||
+ LEAST(5, market_count_90d / 3.0 * 5)
|
||||
+ LEAST(25, customer_count_90d / 8.0 * 25),
|
||||
4
|
||||
)
|
||||
END AS performance_score,
|
||||
@@ -6249,6 +6304,7 @@ func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float
|
||||
stockTurnover,
|
||||
productPerformancePeriodCount(row, "market_count", suffix),
|
||||
productPerformancePeriodCount(row, "customer_count", suffix),
|
||||
productPerformancePeriodDays(row, suffix),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6292,16 +6348,17 @@ func productPerformanceCustomerSalesPeriodScore(row map[string]any) float64 {
|
||||
return productPerformanceCustomerScore(suffix, salesUSD, margin, productCount, salesQty)
|
||||
}
|
||||
|
||||
func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64) float64 {
|
||||
func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64, periodDays ...float64) float64 {
|
||||
if salesUSD <= 0 {
|
||||
return 1
|
||||
}
|
||||
revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTarget(suffix))
|
||||
days := productPerformanceScorePeriodDays(suffix, periodDays...)
|
||||
revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTargetForDays(suffix, days))
|
||||
score := 0.30*productPerformanceMarginComponentScore(margin) +
|
||||
0.20*productPerformanceRatioScore(stockTurnover, productPerformanceStockTurnoverTarget(suffix)) +
|
||||
0.20*revenueScore +
|
||||
0.05*productPerformanceRatioScore(marketCount, productPerformanceMarketSpreadTarget(suffix)) +
|
||||
0.25*productPerformanceRatioScore(customerCount, productPerformanceCustomerSpreadTarget(suffix))
|
||||
0.25*productPerformanceRatioScore(customerCount, productPerformanceCustomerSpreadTargetForDays(suffix, days))
|
||||
return productPerformanceRoundScore(score)
|
||||
}
|
||||
|
||||
@@ -6323,6 +6380,10 @@ func productPerformanceMarketSpreadTarget(suffix string) float64 {
|
||||
}
|
||||
|
||||
func productPerformanceCustomerSpreadTarget(suffix string) float64 {
|
||||
return productPerformanceCustomerSpreadTargetForDays(suffix, productPerformanceScorePeriodDays(suffix))
|
||||
}
|
||||
|
||||
func productPerformanceCustomerSpreadTargetForDays(suffix string, periodDays float64) float64 {
|
||||
switch suffix {
|
||||
case "90d":
|
||||
return 8
|
||||
@@ -6331,7 +6392,7 @@ func productPerformanceCustomerSpreadTarget(suffix string) float64 {
|
||||
case "365d":
|
||||
return 50
|
||||
default:
|
||||
return 50
|
||||
return 20 * productPerformanceTotalPeriodMultiplier(periodDays)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6358,7 +6419,7 @@ func productPerformanceRelativeIndex(value, average float64) float64 {
|
||||
}
|
||||
|
||||
func productPerformanceMarginComponentScore(margin float64) float64 {
|
||||
return productPerformanceRatioScore(maxFloat(0, margin), 0.40)
|
||||
return productPerformanceRatioScore(maxFloat(0, margin), 0.45)
|
||||
}
|
||||
|
||||
func productPerformanceRatioScore(value, target float64) float64 {
|
||||
@@ -6379,18 +6440,49 @@ func productPerformanceRoundScore(score float64) float64 {
|
||||
}
|
||||
|
||||
func productPerformanceProductRevenueTarget(suffix string) float64 {
|
||||
return productPerformanceProductRevenueTargetForDays(suffix, productPerformanceScorePeriodDays(suffix))
|
||||
}
|
||||
|
||||
func productPerformanceProductRevenueTargetForDays(suffix string, periodDays float64) float64 {
|
||||
switch suffix {
|
||||
case "180d":
|
||||
return 20000
|
||||
return 50000
|
||||
case "365d":
|
||||
return 40000
|
||||
return 100000
|
||||
case "total":
|
||||
return 120000
|
||||
return 50000 * productPerformanceTotalPeriodMultiplier(periodDays)
|
||||
default:
|
||||
return 10000
|
||||
return 25000
|
||||
}
|
||||
}
|
||||
|
||||
func productPerformanceScorePeriodDays(suffix string, periodDays ...float64) float64 {
|
||||
if len(periodDays) > 0 && periodDays[0] > 0 {
|
||||
return periodDays[0]
|
||||
}
|
||||
switch suffix {
|
||||
case "90d":
|
||||
return 90
|
||||
case "180d":
|
||||
return 180
|
||||
case "365d":
|
||||
return 360
|
||||
case "total":
|
||||
return productPerformancePeriodDays(map[string]any{
|
||||
"kpi_date": time.Now().Format("2006-01-02"),
|
||||
}, "total")
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func productPerformanceTotalPeriodMultiplier(periodDays float64) float64 {
|
||||
if periodDays <= 0 {
|
||||
periodDays = 180
|
||||
}
|
||||
return maxFloat(1, periodDays/180.0)
|
||||
}
|
||||
|
||||
func productPerformanceCustomerRevenueTarget(suffix string) float64 {
|
||||
switch suffix {
|
||||
case "180d":
|
||||
|
||||
Reference in New Issue
Block a user