Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-06 16:54:36 +03:00
parent 3993c363cb
commit 5d4216d42e
3 changed files with 69 additions and 38 deletions
+36 -19
View File
@@ -3804,10 +3804,10 @@ SELECT
avg_stock_180d, avg_stock_180d,
avg_stock_365d, avg_stock_365d,
avg_stock_total, avg_stock_total,
CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END AS stock_turnover_90d, CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END AS stock_turnover_90d,
CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) ELSE 0 END AS stock_turnover_180d, CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END AS stock_turnover_180d,
CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END AS stock_turnover_365d, CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END AS stock_turnover_365d,
CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) ELSE 0 END AS stock_turnover_total, CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, (current_date - DATE '2022-01-01') + 1) ELSE 0 END AS stock_turnover_total,
has_cost, has_cost,
sales_index_90d, sales_index_90d,
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
@@ -3845,7 +3845,7 @@ SELECT
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
ROUND( ROUND(
LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30) LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30)
+ LEAST(20, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END) / 1.5 * 20) + 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(20, sales_usd_90d / 10000 * 20)
+ LEAST(15, market_count_90d / 8.0 * 15) + LEAST(15, market_count_90d / 8.0 * 15)
+ LEAST(15, customer_count_90d / 25.0 * 15), + LEAST(15, customer_count_90d / 25.0 * 15),
@@ -4517,10 +4517,10 @@ SELECT (
'stock_days_180d', stock_days_180d, 'stock_days_180d', stock_days_180d,
'stock_days_365d', stock_days_365d, 'stock_days_365d', stock_days_365d,
'stock_days_total', stock_days_total, 'stock_days_total', stock_days_total,
'stock_turnover_90d', CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END, 'stock_turnover_90d', CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END,
'stock_turnover_180d', CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) ELSE 0 END, 'stock_turnover_180d', CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END,
'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END, 'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END,
'stock_turnover_total', CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) ELSE 0 END 'stock_turnover_total', CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, ((SELECT kpi_date FROM LatestKPIDate) - DATE '2022-01-01') + 1) ELSE 0 END
) )
) AS row_json ) AS row_json
FROM ( FROM (
@@ -4643,10 +4643,10 @@ func queryProductPerformanceSQLLeafRows(ctx context.Context, pg *sql.DB, mode st
query := productPerformanceSQLSourceCTE(mode) + fmt.Sprintf(` query := productPerformanceSQLSourceCTE(mode) + fmt.Sprintf(`
SELECT to_jsonb(t) || jsonb_build_object( SELECT to_jsonb(t) || jsonb_build_object(
'row_key', 'leaf|' || product_code || '|' || color_code || '|' || yaka_kodu || '|' || market_key, 'row_key', 'leaf|' || product_code || '|' || color_code || '|' || yaka_kodu || '|' || market_key,
'stock_turnover_90d', CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) ELSE 0 END, 'stock_turnover_90d', CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END,
'stock_turnover_180d', CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) ELSE 0 END, 'stock_turnover_180d', CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END,
'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END, 'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END,
'stock_turnover_total', CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) ELSE 0 END 'stock_turnover_total', CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, ((SELECT kpi_date FROM LatestKPIDate) - DATE '2022-01-01') + 1) ELSE 0 END
) AS row_json ) AS row_json
FROM Source t FROM Source t
%s %s
@@ -6000,11 +6000,7 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
} else if _, ok := out["stock_days_"+suffix]; !ok { } else if _, ok := out["stock_days_"+suffix]; !ok {
out["stock_days_"+suffix] = 0 out["stock_days_"+suffix] = 0
} }
if turnoverBase > 0 { out["stock_turnover_"+suffix] = productPerformanceAnnualizedStockTurnover(qty, turnoverBase, days)
out["stock_turnover_"+suffix] = qty / turnoverBase
} else {
out["stock_turnover_"+suffix] = 0
}
if qty > 0 { if qty > 0 {
out["avg_price_usd_"+suffix] = sales / qty out["avg_price_usd_"+suffix] = sales / qty
} }
@@ -6111,7 +6107,7 @@ func productPerformancePeriodDays(row map[string]any, suffix string) float64 {
case "180d": case "180d":
return 180 return 180
case "365d": case "365d":
return 365 return 360
case "total": case "total":
start := parseProductPerformanceDate(stringFromMap(row, "period_start")) start := parseProductPerformanceDate(stringFromMap(row, "period_start"))
if start.IsZero() { if start.IsZero() {
@@ -6130,6 +6126,19 @@ func productPerformancePeriodDays(row map[string]any, suffix string) float64 {
} }
} }
const productPerformanceStockTurnoverYearDays = 360.0
func productPerformanceAnnualizedStockTurnover(salesQty, avgStock, periodDays float64) float64 {
if salesQty <= 0 || avgStock <= 0 {
return 0
}
raw := salesQty / avgStock
if periodDays <= 0 {
return raw
}
return raw * productPerformanceStockTurnoverYearDays / periodDays
}
func parseProductPerformanceDate(value string) time.Time { func parseProductPerformanceDate(value string) time.Time {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {
@@ -6206,8 +6215,12 @@ func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float
salesQty := floatFromMap(row, "sales_qty_"+suffix) salesQty := floatFromMap(row, "sales_qty_"+suffix)
stockTurnover := floatFromMap(row, "stock_turnover_"+suffix) stockTurnover := floatFromMap(row, "stock_turnover_"+suffix)
if stockTurnover == 0 { if stockTurnover == 0 {
if stockQty := floatFromMap(row, "stock_qty"); stockQty > 0 { avgStock := floatFromMap(row, "avg_stock_"+suffix)
stockTurnover = salesQty / stockQty if avgStock <= 0 {
avgStock = floatFromMap(row, "stock_qty")
}
if avgStock > 0 {
stockTurnover = productPerformanceAnnualizedStockTurnover(salesQty, avgStock, productPerformancePeriodDays(row, suffix))
} }
} }
return productPerformanceProductScore( return productPerformanceProductScore(
@@ -6267,13 +6280,17 @@ func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin,
} }
revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTarget(suffix)) revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTarget(suffix))
score := 0.30*productPerformanceMarginComponentScore(margin) + score := 0.30*productPerformanceMarginComponentScore(margin) +
0.20*productPerformanceRatioScore(stockTurnover, 1.50) + 0.20*productPerformanceRatioScore(stockTurnover, productPerformanceStockTurnoverTarget(suffix)) +
0.20*revenueScore + 0.20*revenueScore +
0.15*productPerformanceRatioScore(marketCount, 8) + 0.15*productPerformanceRatioScore(marketCount, 8) +
0.15*productPerformanceRatioScore(customerCount, 25) 0.15*productPerformanceRatioScore(customerCount, 25)
return productPerformanceRoundScore(score) return productPerformanceRoundScore(score)
} }
func productPerformanceStockTurnoverTarget(suffix string) float64 {
return 4
}
func productPerformanceCustomerScore(suffix string, salesUSD, margin, productGroupCount, salesQty float64) float64 { func productPerformanceCustomerScore(suffix string, salesUSD, margin, productGroupCount, salesQty float64) float64 {
if salesUSD <= 0 { if salesUSD <= 0 {
return 1 return 1
+4 -4
View File
@@ -638,10 +638,10 @@ Scored AS (
CASE WHEN b.avg_daily_sales_180d = 0 THEN 0 ELSE b.avg_stock_180d / NULLIF(b.avg_daily_sales_180d, 0) END AS stock_days_180d, CASE WHEN b.avg_daily_sales_180d = 0 THEN 0 ELSE b.avg_stock_180d / NULLIF(b.avg_daily_sales_180d, 0) END AS stock_days_180d,
CASE WHEN b.avg_daily_sales_365d = 0 THEN 0 ELSE b.avg_stock_365d / NULLIF(b.avg_daily_sales_365d, 0) END AS stock_days_365d, CASE WHEN b.avg_daily_sales_365d = 0 THEN 0 ELSE b.avg_stock_365d / NULLIF(b.avg_daily_sales_365d, 0) END AS stock_days_365d,
CASE WHEN b.avg_daily_sales_total = 0 THEN 0 ELSE b.avg_stock_total / NULLIF(b.avg_daily_sales_total, 0) END AS stock_days_total, CASE WHEN b.avg_daily_sales_total = 0 THEN 0 ELSE b.avg_stock_total / NULLIF(b.avg_daily_sales_total, 0) END AS stock_days_total,
CASE WHEN b.avg_stock_90d = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(b.avg_stock_90d, 0) END AS stock_turnover_90d, CASE WHEN b.avg_stock_90d = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(b.avg_stock_90d, 0) * 4.0 END AS stock_turnover_90d,
CASE WHEN b.avg_stock_180d = 0 THEN 0 ELSE b.sales_qty_180d / NULLIF(b.avg_stock_180d, 0) END AS stock_turnover_180d, CASE WHEN b.avg_stock_180d = 0 THEN 0 ELSE b.sales_qty_180d / NULLIF(b.avg_stock_180d, 0) * 2.0 END AS stock_turnover_180d,
CASE WHEN b.avg_stock_365d = 0 THEN 0 ELSE b.sales_qty_365d / NULLIF(b.avg_stock_365d, 0) END AS stock_turnover_365d, CASE WHEN b.avg_stock_365d = 0 THEN 0 ELSE b.sales_qty_365d / NULLIF(b.avg_stock_365d, 0) END AS stock_turnover_365d,
CASE WHEN b.avg_stock_total = 0 THEN 0 ELSE b.sales_qty_total / NULLIF(b.avg_stock_total, 0) END AS stock_turnover_total, CASE WHEN b.avg_stock_total = 0 THEN 0 ELSE b.sales_qty_total / NULLIF(b.avg_stock_total, 0) * 360.0 / GREATEST(1, ($1::date - DATE '2022-01-01') + 1) END AS stock_turnover_total,
CASE WHEN COALESCE(m.market_avg_sales_qty_90d, 0) = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(m.market_avg_sales_qty_90d, 0) END AS sales_index_90d, CASE WHEN COALESCE(m.market_avg_sales_qty_90d, 0) = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(m.market_avg_sales_qty_90d, 0) END AS sales_index_90d,
CASE WHEN COALESCE(m.market_avg_sales_qty_180d, 0) = 0 THEN 0 ELSE b.sales_qty_180d / NULLIF(m.market_avg_sales_qty_180d, 0) END AS sales_index_180d, CASE WHEN COALESCE(m.market_avg_sales_qty_180d, 0) = 0 THEN 0 ELSE b.sales_qty_180d / NULLIF(m.market_avg_sales_qty_180d, 0) END AS sales_index_180d,
CASE WHEN COALESCE(m.market_avg_sales_qty_365d, 0) = 0 THEN 0 ELSE b.sales_qty_365d / NULLIF(m.market_avg_sales_qty_365d, 0) END AS sales_index_365d, CASE WHEN COALESCE(m.market_avg_sales_qty_365d, 0) = 0 THEN 0 ELSE b.sales_qty_365d / NULLIF(m.market_avg_sales_qty_365d, 0) END AS sales_index_365d,
@@ -687,7 +687,7 @@ SELECT
+ LEAST(25, GREATEST(COALESCE(gross_margin_90d, 0), 0) * 50) + LEAST(25, GREATEST(COALESCE(gross_margin_90d, 0), 0) * 50)
+ LEAST(20, COALESCE(customer_count_90d, 0) * 2) + LEAST(20, COALESCE(customer_count_90d, 0) * 2)
+ LEAST(10, COALESCE(market_count_90d, 0) * 3) + LEAST(10, COALESCE(market_count_90d, 0) * 3)
+ CASE WHEN COALESCE(stock_days_90d, 0) BETWEEN 10 AND 90 THEN 10 WHEN COALESCE(stock_days_90d, 0) > 180 THEN -10 ELSE 0 END + LEAST(20, COALESCE(stock_turnover_90d, 0) / 4.0 * 20)
, 4) , 4)
END AS performance_score, END AS performance_score,
CASE CASE
@@ -3553,7 +3553,7 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
if (avgDaily > 0 && turnoverBase > 0) out[`stock_days_${suffix}`] = turnoverBase / avgDaily if (avgDaily > 0 && turnoverBase > 0) out[`stock_days_${suffix}`] = turnoverBase / avgDaily
else if (qty <= 0 && stockQty > 0) out[`stock_days_${suffix}`] = 9999 else if (qty <= 0 && stockQty > 0) out[`stock_days_${suffix}`] = 9999
else if (!Object.prototype.hasOwnProperty.call(out, `stock_days_${suffix}`)) out[`stock_days_${suffix}`] = 0 else if (!Object.prototype.hasOwnProperty.call(out, `stock_days_${suffix}`)) out[`stock_days_${suffix}`] = 0
out[`stock_turnover_${suffix}`] = turnoverBase > 0 ? qty / turnoverBase : 0 out[`stock_turnover_${suffix}`] = annualizedStockTurnover(qty, turnoverBase, days)
if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty
const { costPrice, basePrice } = periodCostPair(out, suffix) const { costPrice, basePrice } = periodCostPair(out, suffix)
out[`base_price_usd_${suffix}`] = basePrice out[`base_price_usd_${suffix}`] = basePrice
@@ -3606,7 +3606,7 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
function productPerformancePeriodDays (row, suffix) { function productPerformancePeriodDays (row, suffix) {
if (suffix === '90d') return 90 if (suffix === '90d') return 90
if (suffix === '180d') return 180 if (suffix === '180d') return 180
if (suffix === '365d') return 365 if (suffix === '365d') return 360
if (suffix !== 'total') return 0 if (suffix !== 'total') return 0
const start = parseProductPerformanceDate(row?.period_start) || new Date(Date.UTC(2022, 0, 1)) const start = parseProductPerformanceDate(row?.period_start) || new Date(Date.UTC(2022, 0, 1))
const end = parseProductPerformanceDate(row?.period_end) || parseProductPerformanceDate(row?.kpi_date) const end = parseProductPerformanceDate(row?.period_end) || parseProductPerformanceDate(row?.kpi_date)
@@ -3648,6 +3648,7 @@ function productPeriodMetricSource (row, suffix) {
const salesIndex = Number(row?.[`sales_index_${suffix}`] || 0) const salesIndex = Number(row?.[`sales_index_${suffix}`] || 0)
const qty = Number(row?.[`sales_qty_${suffix}`] || 0) const qty = Number(row?.[`sales_qty_${suffix}`] || 0)
const stockQty = Number(row?.stock_qty || 0) const stockQty = Number(row?.stock_qty || 0)
const avgStock = Number(row?.[`avg_stock_${suffix}`] || 0) || stockQty
const turnoverKey = `stock_turnover_${suffix}` const turnoverKey = `stock_turnover_${suffix}`
const hasTurnover = Object.prototype.hasOwnProperty.call(row || {}, turnoverKey) const hasTurnover = Object.prototype.hasOwnProperty.call(row || {}, turnoverKey)
return { return {
@@ -3657,7 +3658,7 @@ function productPeriodMetricSource (row, suffix) {
gross_margin_cost_90d: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0), gross_margin_cost_90d: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
gross_margin_90d: Number(row?.[`gross_margin_${suffix}`] ?? 0), gross_margin_90d: Number(row?.[`gross_margin_${suffix}`] ?? 0),
sales_qty_90d: qty, sales_qty_90d: qty,
stock_turnover_90d: hasTurnover ? Number(row?.[turnoverKey] || 0) : (stockQty > 0 ? qty / stockQty : 0), stock_turnover_90d: hasTurnover ? Number(row?.[turnoverKey] || 0) : annualizedStockTurnover(qty, avgStock, productPerformancePeriodDays(row, suffix)),
market_count_90d: periodCount(row, 'market_count', suffix), market_count_90d: periodCount(row, 'market_count', suffix),
customer_count_90d: periodCount(row, 'customer_count', suffix), customer_count_90d: periodCount(row, 'customer_count', suffix),
stock_qty: stockQty stock_qty: stockQty
@@ -3704,7 +3705,7 @@ function productScore100 ({ suffix = '90d', salesUSD = 0, margin = 0, stockTurno
const revenue = ratioScore(salesUSD, productRevenueTarget(suffix)) const revenue = ratioScore(salesUSD, productRevenueTarget(suffix))
return clampScore( return clampScore(
0.30 * marginScore(margin) + 0.30 * marginScore(margin) +
0.20 * ratioScore(stockTurnover, 1.5) + 0.20 * ratioScore(stockTurnover, stockTurnoverTarget(suffix)) +
0.20 * revenue + 0.20 * revenue +
0.15 * ratioScore(marketCount, 8) + 0.15 * ratioScore(marketCount, 8) +
0.15 * ratioScore(customerCount, 25) 0.15 * ratioScore(customerCount, 25)
@@ -3759,6 +3760,21 @@ function customerQtyTarget (suffix) {
return 500 return 500
} }
const STOCK_TURNOVER_YEAR_DAYS = 360
function annualizedStockTurnover (salesQty, avgStock, periodDays) {
const qty = Number(salesQty || 0)
const stock = Number(avgStock || 0)
const days = Number(periodDays || 0)
if (!Number.isFinite(qty) || !Number.isFinite(stock) || qty <= 0 || stock <= 0) return 0
const raw = qty / stock
return days > 0 ? raw * STOCK_TURNOVER_YEAR_DAYS / days : raw
}
function stockTurnoverTarget () {
return 4
}
function periodCount (row, prefix, suffix) { function periodCount (row, prefix, suffix) {
const keyed = Number(row?.[`${prefix}_${suffix}`] || 0) const keyed = Number(row?.[`${prefix}_${suffix}`] || 0)
if (keyed > 0) return keyed if (keyed > 0) return keyed
@@ -4206,10 +4222,10 @@ function withProductMargins (row) {
const out = { const out = {
...next, ...next,
avg_price_usd_365d: Number(next?.sales_qty_365d || 0) > 0 ? Number(next?.sales_usd_365d || 0) / Number(next?.sales_qty_365d || 0) : 0, avg_price_usd_365d: Number(next?.sales_qty_365d || 0) > 0 ? Number(next?.sales_usd_365d || 0) / Number(next?.sales_qty_365d || 0) : 0,
stock_turnover_90d: existingOrStockTurnover(next?.stock_turnover_90d, next?.sales_qty_90d, next?.stock_qty), stock_turnover_90d: existingOrStockTurnover(next?.stock_turnover_90d, next?.sales_qty_90d, next?.avg_stock_90d || next?.stock_qty, 90),
stock_turnover_180d: existingOrStockTurnover(next?.stock_turnover_180d, next?.sales_qty_180d, next?.stock_qty), stock_turnover_180d: existingOrStockTurnover(next?.stock_turnover_180d, next?.sales_qty_180d, next?.avg_stock_180d || next?.stock_qty, 180),
stock_turnover_365d: existingOrStockTurnover(next?.stock_turnover_365d, next?.sales_qty_365d, next?.stock_qty), stock_turnover_365d: existingOrStockTurnover(next?.stock_turnover_365d, next?.sales_qty_365d, next?.avg_stock_365d || next?.stock_qty, 360),
stock_turnover_total: existingOrStockTurnover(next?.stock_turnover_total, next?.sales_qty_total, next?.stock_qty) stock_turnover_total: existingOrStockTurnover(next?.stock_turnover_total, next?.sales_qty_total, next?.avg_stock_total || next?.stock_qty, productPerformancePeriodDays(next, 'total'))
} }
for (const suffix of ['90d', '180d', '365d', 'total']) { for (const suffix of ['90d', '180d', '365d', 'total']) {
applyPeriodProfitFields(out, suffix) applyPeriodProfitFields(out, suffix)
@@ -4222,7 +4238,7 @@ function withGeneralMargins (row) {
applyPeriodProfitFields(out, 'total') applyPeriodProfitFields(out, 'total')
return { return {
...out, ...out,
stock_turnover_total: existingOrStockTurnover(out?.stock_turnover_total, out?.sales_qty_total, out?.stock_qty) stock_turnover_total: existingOrStockTurnover(out?.stock_turnover_total, out?.sales_qty_total, out?.avg_stock_total || out?.stock_qty, productPerformancePeriodDays(out, 'total'))
} }
} }
@@ -4289,15 +4305,13 @@ function applyPeriodProfitFields (row, suffix) {
row[`gross_margin_cost_${suffix}`] = marginFromSalesCost(salesUSD, qty, costPrice) row[`gross_margin_cost_${suffix}`] = marginFromSalesCost(salesUSD, qty, costPrice)
} }
function stockTurnover (salesQty, stockQty) { function stockTurnover (salesQty, stockQty, periodDays) {
const stock = Number(stockQty || 0) return annualizedStockTurnover(salesQty, stockQty, periodDays)
if (stock <= 0) return 0
return Number(salesQty || 0) / stock
} }
function existingOrStockTurnover (value, salesQty, stockQty) { function existingOrStockTurnover (value, salesQty, stockQty, periodDays) {
const current = Number(value) const current = Number(value)
return value !== undefined && value !== null && Number.isFinite(current) ? current : stockTurnover(salesQty, stockQty) return value !== undefined && value !== null && Number.isFinite(current) ? current : stockTurnover(salesQty, stockQty, periodDays)
} }
function filterKey (tableKey, name) { function filterKey (tableKey, name) {