Fix product performance grouped JSON build
This commit is contained in:
@@ -4592,10 +4592,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) * 4.0 ELSE 0 END,
|
'stock_turnover_90d', stock_turnover_90d,
|
||||||
'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_180d', stock_turnover_180d,
|
||||||
'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END,
|
'stock_turnover_365d', stock_turnover_365d,
|
||||||
'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
|
'stock_turnover_total', stock_turnover_total
|
||||||
)
|
)
|
||||||
) AS row_json
|
) AS row_json
|
||||||
FROM (
|
FROM (
|
||||||
@@ -4660,10 +4660,10 @@ FROM (
|
|||||||
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_cost_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_cost_180d,
|
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_cost_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_cost_180d,
|
||||||
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_base_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_base_90d,
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_base_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_base_90d,
|
||||||
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_base_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_base_180d,
|
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_base_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_base_180d,
|
||||||
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d,
|
COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d,
|
||||||
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_180d,0) > 0)::integer AS market_count_180d,
|
COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_180d,0) > 0)::integer AS market_count_180d,
|
||||||
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_365d,0) > 0)::integer AS market_count_365d,
|
COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_365d,0) > 0)::integer AS market_count_365d,
|
||||||
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_total,0) > 0)::integer AS market_count_total,
|
COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_total,0) > 0)::integer AS market_count_total,
|
||||||
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_90d,0) > 0 THEN customer_count_90d ELSE 0 END),0)::integer AS customer_count_90d,
|
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_90d,0) > 0 THEN customer_count_90d ELSE 0 END),0)::integer AS customer_count_90d,
|
||||||
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_180d,0) > 0 THEN customer_count_180d ELSE 0 END),0)::integer AS customer_count_180d,
|
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_180d,0) > 0 THEN customer_count_180d ELSE 0 END),0)::integer AS customer_count_180d,
|
||||||
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_365d,0) > 0 THEN customer_count_365d ELSE 0 END),0)::integer AS customer_count_365d,
|
COALESCE(SUM(CASE WHEN COALESCE(sales_usd_365d,0) > 0 THEN customer_count_365d ELSE 0 END),0)::integer AS customer_count_365d,
|
||||||
@@ -4674,6 +4674,10 @@ FROM (
|
|||||||
CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(sales_index_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS sales_index_total,
|
CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(sales_index_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS sales_index_total,
|
||||||
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d,
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d,
|
||||||
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d,
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d,
|
||||||
|
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(stock_turnover_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS stock_turnover_90d,
|
||||||
|
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(stock_turnover_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS stock_turnover_180d,
|
||||||
|
CASE WHEN SUM(sales_qty_365d) > 0 THEN SUM(stock_turnover_365d * sales_qty_365d) / NULLIF(SUM(sales_qty_365d),0) ELSE 0 END AS stock_turnover_365d,
|
||||||
|
CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(stock_turnover_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS stock_turnover_total,
|
||||||
CASE
|
CASE
|
||||||
WHEN SUM(CASE
|
WHEN SUM(CASE
|
||||||
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
|
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
|
||||||
@@ -4694,6 +4698,11 @@ FROM (
|
|||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
Source.*,
|
Source.*,
|
||||||
|
btrim(regexp_replace(COALESCE(Source.market_key,''), '^.*[|]', '')) AS display_market_key,
|
||||||
|
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) * 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_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 stock_turnover_total,
|
||||||
ROW_NUMBER() OVER (
|
ROW_NUMBER() OVER (
|
||||||
PARTITION BY COALESCE(%s, ''), product_code, color_code, yaka_kodu
|
PARTITION BY COALESCE(%s, ''), product_code, color_code, yaka_kodu
|
||||||
ORDER BY performance_score DESC NULLS LAST
|
ORDER BY performance_score DESC NULLS LAST
|
||||||
@@ -4886,7 +4895,7 @@ func productPerformanceSQLGroupExpr(field string) (string, bool) {
|
|||||||
case "color_yaka":
|
case "color_yaka":
|
||||||
return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true
|
return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true
|
||||||
case "market_key":
|
case "market_key":
|
||||||
return "btrim(regexp_replace(COALESCE(market_key,''), '^.*\\|', ''))", true
|
return "btrim(regexp_replace(COALESCE(market_key,''), '^.*[|]', ''))", true
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -5300,6 +5309,26 @@ Spread AS (
|
|||||||
s.color_code,
|
s.color_code,
|
||||||
s.yaka_kodu,
|
s.yaka_kodu,
|
||||||
s.market_key,
|
s.market_key,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND display_market_key NOT IN ('', 'STOK', '-')
|
||||||
|
), '[]'::jsonb) AS market_keys_90d,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '179 days' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND display_market_key NOT IN ('', 'STOK', '-')
|
||||||
|
), '[]'::jsonb) AS market_keys_180d,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '359 days' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND display_market_key NOT IN ('', 'STOK', '-')
|
||||||
|
), '[]'::jsonb) AS market_keys_365d,
|
||||||
|
COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
|
||||||
|
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
|
||||||
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
|
AND display_market_key NOT IN ('', 'STOK', '-')
|
||||||
|
), '[]'::jsonb) AS market_keys_total,
|
||||||
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
|
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
|
||||||
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
|
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
|
||||||
AND COALESCE(s.sales_usd,0) > 0
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
@@ -5320,7 +5349,12 @@ Spread AS (
|
|||||||
AND COALESCE(s.sales_usd,0) > 0
|
AND COALESCE(s.sales_usd,0) > 0
|
||||||
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
|
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
|
||||||
), '[]'::jsonb) AS customer_keys_total
|
), '[]'::jsonb) AS customer_keys_total
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
s.*,
|
||||||
|
btrim(regexp_replace(COALESCE(s.market_key,''), '^.*[|]', '')) AS display_market_key
|
||||||
FROM mk_product_performance_sales_daily s
|
FROM mk_product_performance_sales_daily s
|
||||||
|
) s
|
||||||
CROSS JOIN Latest
|
CROSS JOIN Latest
|
||||||
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
|
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
|
||||||
AND upper(translate(btrim(COALESCE(s.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')
|
AND upper(translate(btrim(COALESCE(s.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')
|
||||||
@@ -5331,6 +5365,10 @@ SELECT jsonb_build_object(
|
|||||||
'color_code', color_code,
|
'color_code', color_code,
|
||||||
'yaka_kodu', yaka_kodu,
|
'yaka_kodu', yaka_kodu,
|
||||||
'market_key', market_key,
|
'market_key', market_key,
|
||||||
|
'__market_keys_90d', market_keys_90d,
|
||||||
|
'__market_keys_180d', market_keys_180d,
|
||||||
|
'__market_keys_365d', market_keys_365d,
|
||||||
|
'__market_keys_total', market_keys_total,
|
||||||
'__customer_keys_90d', customer_keys_90d,
|
'__customer_keys_90d', customer_keys_90d,
|
||||||
'__customer_keys_180d', customer_keys_180d,
|
'__customer_keys_180d', customer_keys_180d,
|
||||||
'__customer_keys_365d', customer_keys_365d,
|
'__customer_keys_365d', customer_keys_365d,
|
||||||
@@ -5355,12 +5393,14 @@ FROM Spread`
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, suffix := range productPerformancePeriodSuffixes() {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
field := "__customer_keys_" + suffix
|
for _, prefix := range []string{"__market_keys_", "__customer_keys_"} {
|
||||||
|
field := prefix + suffix
|
||||||
if value, ok := spread[field]; ok {
|
if value, ok := spread[field]; ok {
|
||||||
row[field] = value
|
row[field] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5569,6 +5609,13 @@ func applyProductPerformanceGroupedChildScoreAverages(rows []map[string]any, mod
|
|||||||
}
|
}
|
||||||
row[field] = weightedAverageProductPerformanceScoreRows(childRows, field, suffix)
|
row[field] = weightedAverageProductPerformanceScoreRows(childRows, field, suffix)
|
||||||
}
|
}
|
||||||
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
field := "stock_turnover_" + suffix
|
||||||
|
if !productPerformanceRowsHaveField(childRows, field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[field] = weightedAverageProductPerformanceRows(childRows, field, "sales_qty_"+suffix)
|
||||||
|
}
|
||||||
if score, ok := productPerformanceOptionalFloat(row, "performance_score_90d"); ok {
|
if score, ok := productPerformanceOptionalFloat(row, "performance_score_90d"); ok {
|
||||||
row["performance_score"] = score
|
row["performance_score"] = score
|
||||||
}
|
}
|
||||||
@@ -5710,6 +5757,7 @@ func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, suffix := range productPerformancePeriodSuffixes() {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
n.MarketSeen = productPerformanceAddSeenStrings(n.MarketSeen, suffix, productPerformanceStringSliceFromMap(row, "__market_keys_"+suffix))
|
||||||
n.CustomerSeen = productPerformanceAddSeenStrings(n.CustomerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
|
n.CustomerSeen = productPerformanceAddSeenStrings(n.CustomerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5940,6 +5988,7 @@ func addProductPerformanceDistinctSpread(row map[string]any, marketSeen, custome
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, suffix := range productPerformancePeriodSuffixes() {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
|
marketSeen = productPerformanceAddSeenStrings(marketSeen, suffix, productPerformanceStringSliceFromMap(row, "__market_keys_"+suffix))
|
||||||
customerSeen = productPerformanceAddSeenStrings(customerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
|
customerSeen = productPerformanceAddSeenStrings(customerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6135,6 +6184,7 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
|
|||||||
}
|
}
|
||||||
preservedCustomerScores := map[string]float64{}
|
preservedCustomerScores := map[string]float64{}
|
||||||
preservedProductScores := map[string]float64{}
|
preservedProductScores := map[string]float64{}
|
||||||
|
preservedStockTurnovers := map[string]float64{}
|
||||||
for _, suffix := range productPerformancePeriodSuffixes() {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
if score, ok := productPerformanceOptionalFloat(out, "customer_score_"+suffix); ok {
|
if score, ok := productPerformanceOptionalFloat(out, "customer_score_"+suffix); ok {
|
||||||
preservedCustomerScores[suffix] = score
|
preservedCustomerScores[suffix] = score
|
||||||
@@ -6142,6 +6192,11 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
|
|||||||
if score, ok := productPerformanceOptionalFloat(out, "performance_score_"+suffix); ok {
|
if score, ok := productPerformanceOptionalFloat(out, "performance_score_"+suffix); ok {
|
||||||
preservedProductScores[suffix] = score
|
preservedProductScores[suffix] = score
|
||||||
}
|
}
|
||||||
|
if isGroup {
|
||||||
|
if turnover, ok := productPerformanceOptionalFloat(out, "stock_turnover_"+suffix); ok {
|
||||||
|
preservedStockTurnovers[suffix] = turnover
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if score, ok := productPerformanceOptionalFloat(out, "performance_score"); ok {
|
if score, ok := productPerformanceOptionalFloat(out, "performance_score"); ok {
|
||||||
if _, exists := preservedProductScores["90d"]; !exists {
|
if _, exists := preservedProductScores["90d"]; !exists {
|
||||||
@@ -6170,6 +6225,9 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
|
|||||||
out["stock_days_"+suffix] = 0
|
out["stock_days_"+suffix] = 0
|
||||||
}
|
}
|
||||||
out["stock_turnover_"+suffix] = productPerformanceAnnualizedStockTurnover(qty, turnoverBase, days)
|
out["stock_turnover_"+suffix] = productPerformanceAnnualizedStockTurnover(qty, turnoverBase, days)
|
||||||
|
if turnover, ok := preservedStockTurnovers[suffix]; ok {
|
||||||
|
out["stock_turnover_"+suffix] = turnover
|
||||||
|
}
|
||||||
if qty > 0 {
|
if qty > 0 {
|
||||||
out["avg_price_usd_"+suffix] = sales / qty
|
out["avg_price_usd_"+suffix] = sales / qty
|
||||||
}
|
}
|
||||||
@@ -6775,6 +6833,9 @@ func productPerformanceMetricWeightField(field string) string {
|
|||||||
if productPerformanceUsesCostWeight(field) {
|
if productPerformanceUsesCostWeight(field) {
|
||||||
return "__product_cost_weight"
|
return "__product_cost_weight"
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(field, "stock_turnover") {
|
||||||
|
return "sales_qty_" + productPerformanceScoreSuffix(field)
|
||||||
|
}
|
||||||
if strings.Contains(field, "_180d") {
|
if strings.Contains(field, "_180d") {
|
||||||
return "sales_qty_180d"
|
return "sales_qty_180d"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3525,6 +3525,7 @@ function shouldAverageField (field) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function weightFieldForMetric (field) {
|
function weightFieldForMetric (field) {
|
||||||
|
if (field.startsWith('stock_turnover')) return `sales_qty_${countPeriodSuffix(field)}`
|
||||||
if (field.includes('_180d')) return 'sales_qty_180d'
|
if (field.includes('_180d')) return 'sales_qty_180d'
|
||||||
if (field.includes('_365d')) return 'sales_qty_365d'
|
if (field.includes('_365d')) return 'sales_qty_365d'
|
||||||
if (field.includes('_total')) return 'sales_qty_total'
|
if (field.includes('_total')) return 'sales_qty_total'
|
||||||
@@ -3538,6 +3539,7 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
|||||||
const isGroup = Boolean(String(groupField || '').trim())
|
const isGroup = Boolean(String(groupField || '').trim())
|
||||||
const preservedCustomerScores = {}
|
const preservedCustomerScores = {}
|
||||||
const preservedProductScores = {}
|
const preservedProductScores = {}
|
||||||
|
const preservedStockTurnovers = {}
|
||||||
for (const suffix of ['90d', '180d', '365d', 'total']) {
|
for (const suffix of ['90d', '180d', '365d', 'total']) {
|
||||||
if (Object.prototype.hasOwnProperty.call(out, `customer_score_${suffix}`)) {
|
if (Object.prototype.hasOwnProperty.call(out, `customer_score_${suffix}`)) {
|
||||||
preservedCustomerScores[suffix] = Number(out[`customer_score_${suffix}`] || 0)
|
preservedCustomerScores[suffix] = Number(out[`customer_score_${suffix}`] || 0)
|
||||||
@@ -3545,6 +3547,9 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
|||||||
if (Object.prototype.hasOwnProperty.call(out, `performance_score_${suffix}`)) {
|
if (Object.prototype.hasOwnProperty.call(out, `performance_score_${suffix}`)) {
|
||||||
preservedProductScores[suffix] = Number(out[`performance_score_${suffix}`] || 0)
|
preservedProductScores[suffix] = Number(out[`performance_score_${suffix}`] || 0)
|
||||||
}
|
}
|
||||||
|
if (isGroup && Object.prototype.hasOwnProperty.call(out, `stock_turnover_${suffix}`)) {
|
||||||
|
preservedStockTurnovers[suffix] = Number(out[`stock_turnover_${suffix}`] || 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
Object.prototype.hasOwnProperty.call(out, 'performance_score') &&
|
Object.prototype.hasOwnProperty.call(out, 'performance_score') &&
|
||||||
@@ -3565,6 +3570,9 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
|||||||
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}`] = annualizedStockTurnover(qty, turnoverBase, days)
|
out[`stock_turnover_${suffix}`] = annualizedStockTurnover(qty, turnoverBase, days)
|
||||||
|
if (Object.prototype.hasOwnProperty.call(preservedStockTurnovers, suffix)) {
|
||||||
|
out[`stock_turnover_${suffix}`] = preservedStockTurnovers[suffix]
|
||||||
|
}
|
||||||
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
|
||||||
@@ -3917,6 +3925,12 @@ function distinctRevenueMarketCount (sourceRows, suffix, fallbackField) {
|
|||||||
const values = new Set()
|
const values = new Set()
|
||||||
for (const row of sourceRows) {
|
for (const row of sourceRows) {
|
||||||
if (Number(row?.[salesField] || 0) <= 0) continue
|
if (Number(row?.[salesField] || 0) <= 0) continue
|
||||||
|
const marketKeys = stringListFromRow(row, `__market_keys_${suffix}`)
|
||||||
|
for (const key of marketKeys) {
|
||||||
|
const value = displayMarketName(key)
|
||||||
|
if (value && value !== 'STOK' && value !== '-') values.add(value)
|
||||||
|
}
|
||||||
|
if (marketKeys.length > 0) continue
|
||||||
const value = displayMarketName(row?.market_key)
|
const value = displayMarketName(row?.market_key)
|
||||||
if (value && value !== 'STOK' && value !== '-') values.add(value)
|
if (value && value !== 'STOK' && value !== '-') values.add(value)
|
||||||
}
|
}
|
||||||
@@ -3931,6 +3945,11 @@ function distinctRevenueCustomerCount (sourceRows, suffix, fallbackField) {
|
|||||||
const values = new Set()
|
const values = new Set()
|
||||||
for (const row of sourceRows) {
|
for (const row of sourceRows) {
|
||||||
if (Number(row?.[salesField] || 0) <= 0) continue
|
if (Number(row?.[salesField] || 0) <= 0) continue
|
||||||
|
const customerKeys = stringListFromRow(row, `__customer_keys_${suffix}`)
|
||||||
|
for (const code of customerKeys) {
|
||||||
|
if (code && code !== '-') values.add(code)
|
||||||
|
}
|
||||||
|
if (customerKeys.length > 0) continue
|
||||||
const code = String(row?.customer_code || '').trim()
|
const code = String(row?.customer_code || '').trim()
|
||||||
if (code && code !== '-') values.add(code)
|
if (code && code !== '-') values.add(code)
|
||||||
}
|
}
|
||||||
@@ -3940,6 +3959,22 @@ function distinctRevenueCustomerCount (sourceRows, suffix, fallbackField) {
|
|||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringListFromRow (row, field) {
|
||||||
|
const value = row?.[field]
|
||||||
|
if (Array.isArray(value)) return value.map(item => String(item || '').trim()).filter(Boolean)
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return []
|
||||||
|
if (text.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
return Array.isArray(parsed) ? parsed.map(item => String(item || '').trim()).filter(Boolean) : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [text]
|
||||||
|
}
|
||||||
|
|
||||||
function distinctSummary (sourceRows, field, fallbackField) {
|
function distinctSummary (sourceRows, field, fallbackField) {
|
||||||
const values = new Set()
|
const values = new Set()
|
||||||
for (const row of sourceRows) {
|
for (const row of sourceRows) {
|
||||||
@@ -4351,9 +4386,10 @@ function groupedOrExistingStockTurnover (row, suffix, periodDays) {
|
|||||||
const avgStock = Number(row?.[`avg_stock_${suffix}`] || 0)
|
const avgStock = Number(row?.[`avg_stock_${suffix}`] || 0)
|
||||||
const stockQty = Number(row?.stock_qty || 0)
|
const stockQty = Number(row?.stock_qty || 0)
|
||||||
if (row?.__group) {
|
if (row?.__group) {
|
||||||
if (avgStock > 0) return stockTurnover(salesQty, avgStock, periodDays)
|
|
||||||
const current = Number(row?.[`stock_turnover_${suffix}`])
|
const current = Number(row?.[`stock_turnover_${suffix}`])
|
||||||
return Number.isFinite(current) ? current : 0
|
if (Number.isFinite(current)) return current
|
||||||
|
if (avgStock > 0) return stockTurnover(salesQty, avgStock, periodDays)
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
const stockBase = avgStock > 0 ? avgStock : stockQty
|
const stockBase = avgStock > 0 ? avgStock : stockQty
|
||||||
return existingOrStockTurnover(row?.[`stock_turnover_${suffix}`], salesQty, stockBase, periodDays)
|
return existingOrStockTurnover(row?.[`stock_turnover_${suffix}`], salesQty, stockBase, periodDays)
|
||||||
|
|||||||
Reference in New Issue
Block a user