Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-05 23:43:17 +03:00
parent 9e1ecb9b3f
commit 53cdd1a4a1
4 changed files with 421 additions and 142 deletions
+219 -52
View File
@@ -445,12 +445,12 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_price_dim (
`
UPDATE mk_product_performance_price_dim
SET
cost_price_usd = GREATEST(cost_price_usd, base_price_usd),
base_price_usd = LEAST(cost_price_usd, base_price_usd),
cost_price_usd = LEAST(cost_price_usd, base_price_usd),
base_price_usd = GREATEST(cost_price_usd, base_price_usd),
updated_at = now()
WHERE cost_price_usd > 0
AND base_price_usd > 0
AND cost_price_usd < base_price_usd`,
AND cost_price_usd > base_price_usd`,
`
DELETE FROM mk_product_performance_price_dim
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')`,
@@ -527,20 +527,20 @@ WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00D
`
UPDATE mk_product_performance_kpi_daily
SET
cost_price_usd = GREATEST(cost_price_usd, base_price_usd),
base_price_usd = LEAST(cost_price_usd, base_price_usd),
gross_profit_usd_90d = COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * GREATEST(cost_price_usd, base_price_usd)),
gross_profit_usd_180d = COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * GREATEST(cost_price_usd, base_price_usd)),
gross_margin_90d = CASE WHEN COALESCE(sales_usd_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * GREATEST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_90d,0) END,
gross_margin_180d = CASE WHEN COALESCE(sales_usd_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * GREATEST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_180d,0) END,
unit_profit_cost_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
unit_profit_cost_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
unit_profit_base_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - LEAST(cost_price_usd, base_price_usd) END,
unit_profit_base_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - LEAST(cost_price_usd, base_price_usd) END,
cost_price_usd = LEAST(cost_price_usd, base_price_usd),
base_price_usd = GREATEST(cost_price_usd, base_price_usd),
gross_profit_usd_90d = COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * LEAST(cost_price_usd, base_price_usd)),
gross_profit_usd_180d = COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * LEAST(cost_price_usd, base_price_usd)),
gross_margin_90d = CASE WHEN COALESCE(sales_usd_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * LEAST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_90d,0) END,
gross_margin_180d = CASE WHEN COALESCE(sales_usd_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * LEAST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_180d,0) END,
unit_profit_cost_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - LEAST(cost_price_usd, base_price_usd) END,
unit_profit_cost_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - LEAST(cost_price_usd, base_price_usd) END,
unit_profit_base_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
unit_profit_base_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
updated_at = now()
WHERE cost_price_usd > 0
AND base_price_usd > 0
AND cost_price_usd < base_price_usd`,
AND cost_price_usd > base_price_usd`,
`
UPDATE mk_product_performance_kpi_daily
SET askili_yan = '', updated_at = now()
@@ -4486,7 +4486,29 @@ FROM (
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS sales_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(performance_score * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS performance_score,
CASE
WHEN SUM(CASE
WHEN COALESCE(sales_usd_90d,0) > 0 THEN sales_usd_90d
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
WHEN COALESCE(stock_qty,0) > 0 AND COALESCE(cost_price_usd,0) > 0 THEN stock_qty * cost_price_usd
WHEN COALESCE(stock_qty,0) > 0 THEN stock_qty
ELSE 1
END) > 0
THEN SUM(performance_score * CASE
WHEN COALESCE(sales_usd_90d,0) > 0 THEN sales_usd_90d
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
WHEN COALESCE(stock_qty,0) > 0 AND COALESCE(cost_price_usd,0) > 0 THEN stock_qty * cost_price_usd
WHEN COALESCE(stock_qty,0) > 0 THEN stock_qty
ELSE 1
END) / NULLIF(SUM(CASE
WHEN COALESCE(sales_usd_90d,0) > 0 THEN sales_usd_90d
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
WHEN COALESCE(stock_qty,0) > 0 AND COALESCE(cost_price_usd,0) > 0 THEN stock_qty * cost_price_usd
WHEN COALESCE(stock_qty,0) > 0 THEN stock_qty
ELSE 1
END),0)
ELSE 0
END AS performance_score,
MODE() WITHIN GROUP (ORDER BY performance_bucket) AS performance_bucket,
COALESCE(MODE() WITHIN GROUP (ORDER BY NULLIF(recommendation,'')), '') AS recommendation,
COALESCE(SUM(CASE WHEN stock_variant_rank = 1 THEN idle_cost_usd ELSE 0 END),0) AS idle_cost_usd
@@ -5167,7 +5189,7 @@ type productPerformanceGroupedSnapshotNode struct {
Count int
Children map[string]*productPerformanceGroupedSnapshotNode
ChildOrder []string
StockSeen map[string]bool
StockMetricSeen map[string]map[string]bool
IdleSeen map[string]bool
Avg map[string]*productPerformanceGroupedSnapshotAvgState
BucketCounts map[string]int
@@ -5235,19 +5257,8 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
continue
}
switch {
case key == "stock_qty":
variantKey := productPerformanceMapVariantKey(row)
if variantKey == "" {
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
continue
}
if n.StockSeen == nil {
n.StockSeen = map[string]bool{}
}
if !n.StockSeen[variantKey] {
n.StockSeen[variantKey] = true
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
}
case isProductPerformanceDistinctVariantStockMetric(key):
n.addDistinctVariantMetric(row, key, value)
case key == "idle_cost_usd":
variantKey := productPerformanceMapVariantKey(row)
if variantKey == "" {
@@ -5289,6 +5300,27 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
n.addDistinctSpread(row)
}
func (n *productPerformanceGroupedSnapshotNode) addDistinctVariantMetric(row map[string]any, key string, value any) {
variantKey := productPerformanceMapVariantKey(row)
if variantKey == "" {
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
return
}
if n.StockMetricSeen == nil {
n.StockMetricSeen = map[string]map[string]bool{}
}
seen := n.StockMetricSeen[key]
if seen == nil {
seen = map[string]bool{}
n.StockMetricSeen[key] = seen
}
if seen[variantKey] {
return
}
seen[variantKey] = true
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
}
func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
if market != "" && market != "STOK" {
@@ -5355,11 +5387,11 @@ func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
row[key] = state.Sum / state.Count
}
}
applyProductPerformanceDistinctSpread(row, n.Market90Seen, n.MarketTotalSeen, n.Customer90Seen, n.CustomerTotalSeen)
deriveProductPerformanceGroupMetrics(row, n.Field)
if bucket := n.dominantBucket(); bucket != "" {
row["performance_bucket"] = bucket
}
applyProductPerformanceDistinctSpread(row, n.Market90Seen, n.MarketTotalSeen, n.Customer90Seen, n.CustomerTotalSeen)
clearProductPerformanceGroupDimensions(row, n.Field, n.Value)
row["__group"] = true
row["row_key"] = "group|" + n.Key
@@ -5489,8 +5521,8 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
if isProductPerformanceMarginField(key) {
continue
}
if key == "stock_qty" {
out[key] = distinctProductPerformanceVariantStockQty(rows)
if isProductPerformanceDistinctVariantStockMetric(key) {
out[key] = distinctProductPerformanceVariantNumber(rows, key)
continue
}
if key == "idle_cost_usd" {
@@ -5517,9 +5549,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
}
}
}
applyProductPerformanceDistinctSpread(out, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
deriveProductPerformanceGroupMetrics(out, groupField)
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
applyProductPerformanceDistinctSpread(out, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
return out
}
@@ -5613,7 +5645,11 @@ func isProductPerformanceMarginField(field string) bool {
return strings.HasPrefix(field, "gross_margin")
}
func distinctProductPerformanceVariantStockQty(rows []map[string]any) float64 {
func isProductPerformanceDistinctVariantStockMetric(field string) bool {
return field == "stock_qty" || strings.HasPrefix(field, "avg_stock_")
}
func distinctProductPerformanceVariantNumber(rows []map[string]any, field string) float64 {
seen := map[string]bool{}
total := 0.0
hasKey := false
@@ -5627,13 +5663,13 @@ func distinctProductPerformanceVariantStockQty(rows []map[string]any) float64 {
continue
}
seen[key] = true
total += floatFromMap(row, "stock_qty")
total += floatFromMap(row, field)
}
if hasKey {
return total
}
for _, row := range rows {
total += floatFromMap(row, "stock_qty")
total += floatFromMap(row, field)
}
return total
}
@@ -5674,36 +5710,57 @@ func productPerformanceMapVariantKey(row map[string]any) string {
}
func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) {
normalizeProductPerformanceCostFields(out)
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
sales := floatFromMap(out, "sales_usd_"+suffix)
qty := floatFromMap(out, "sales_qty_"+suffix)
stockQty := floatFromMap(out, "stock_qty")
if _, exists := out["stock_turnover_"+suffix]; !exists && stockQty > 0 {
out["stock_turnover_"+suffix] = qty / stockQty
turnoverBase := floatFromMap(out, "avg_stock_"+suffix)
if turnoverBase <= 0 {
turnoverBase = stockQty
}
if turnoverBase > 0 {
out["stock_turnover_"+suffix] = qty / turnoverBase
} else {
out["stock_turnover_"+suffix] = 0
}
if qty > 0 {
out["avg_price_usd_"+suffix] = sales / qty
}
basePrice, hasBasePrice := productPerformanceUnitCostForSuffix(out, "base_price_usd", suffix)
costPrice, hasCostPrice := productPerformanceUnitCostForSuffix(out, "cost_price_usd", suffix)
if hasBasePrice {
out["base_price_usd_"+suffix] = basePrice
}
if hasCostPrice {
out["cost_price_usd_"+suffix] = costPrice
}
if qty > 0 && hasBasePrice {
out["unit_profit_base_"+suffix] = (sales / qty) - basePrice
avgPrice := sales / qty
out["unit_profit_base_"+suffix] = avgPrice - basePrice
out["gross_profit_base_usd_"+suffix] = sales - (qty * basePrice)
} else {
out["unit_profit_base_"+suffix] = 0
out["gross_profit_base_usd_"+suffix] = 0
}
if qty > 0 && hasCostPrice {
out["unit_profit_cost_"+suffix] = (sales / qty) - costPrice
avgPrice := sales / qty
out["unit_profit_cost_"+suffix] = avgPrice - costPrice
out["gross_profit_cost_usd_"+suffix] = sales - (qty * costPrice)
out["gross_profit_usd_"+suffix] = out["gross_profit_cost_usd_"+suffix]
} else {
out["unit_profit_cost_"+suffix] = 0
out["gross_profit_cost_usd_"+suffix] = 0
out["gross_profit_usd_"+suffix] = 0
}
if sales > 0 {
if _, ok := out["gross_profit_base_usd_"+suffix]; ok {
out["gross_margin_base_"+suffix] = floatFromMap(out, "gross_profit_base_usd_"+suffix) / sales
}
if _, ok := out["gross_profit_cost_usd_"+suffix]; ok {
out["gross_margin_cost_"+suffix] = floatFromMap(out, "gross_profit_cost_usd_"+suffix) / sales
}
if _, ok := out["gross_profit_usd_"+suffix]; ok {
out["gross_margin_"+suffix] = floatFromMap(out, "gross_profit_usd_"+suffix) / sales
}
out["gross_margin_base_"+suffix] = floatFromMap(out, "gross_profit_base_usd_"+suffix) / sales
out["gross_margin_cost_"+suffix] = floatFromMap(out, "gross_profit_cost_usd_"+suffix) / sales
out["gross_margin_"+suffix] = floatFromMap(out, "gross_profit_usd_"+suffix) / sales
} else {
out["gross_margin_base_"+suffix] = 0
out["gross_margin_cost_"+suffix] = 0
out["gross_margin_"+suffix] = 0
}
}
orderUSD := floatFromMap(out, "order_usd")
@@ -5743,7 +5800,11 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
}
if _, ok := out["performance_score"]; !ok {
out["performance_score"] = productPerformanceGroupScore(out, groupField)
if score, exists := productPerformanceOptionalFloat(out, "performance_score_90d"); exists {
out["performance_score"] = score
} else {
out["performance_score"] = productPerformanceGroupScore(out, groupField)
}
}
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
score := productPerformanceOrderGroupScore(out)
@@ -5751,16 +5812,53 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
out["performance_score_180d"] = score
out["performance_score_365d"] = score
out["performance_score_total"] = score
out["performance_score"] = score
}
}
func normalizeProductPerformanceCostFields(row map[string]any) {
normalize := func(costField, baseField string) {
cost, hasCost := productPerformanceOptionalFloat(row, costField)
base, hasBase := productPerformanceOptionalFloat(row, baseField)
if !hasCost && !hasBase {
return
}
cost, base = normalizeProductPerformanceCostPair(cost, base)
if hasCost || cost > 0 {
row[costField] = cost
}
if hasBase || base > 0 {
row[baseField] = base
}
}
normalize("cost_price_usd", "base_price_usd")
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
normalize("cost_price_usd_"+suffix, "base_price_usd_"+suffix)
}
}
func productPerformanceOptionalFloat(row map[string]any, field string) (float64, bool) {
value, ok := row[field]
if !ok {
return 0, false
}
return floatFromAny(value), true
}
func productPerformanceUnitCostForSuffix(row map[string]any, baseField, suffix string) (float64, bool) {
hasSuffix := false
if value, ok := row[baseField+"_"+suffix]; ok {
return floatFromAny(value), true
hasSuffix = true
if n := floatFromAny(value); n > 0 {
return n, true
}
}
if value, ok := row[baseField]; ok {
return floatFromAny(value), true
}
if hasSuffix {
return 0, true
}
return 0, false
}
@@ -6099,6 +6197,7 @@ func shouldAverageProductPerformanceField(field string) bool {
strings.HasPrefix(field, "expected_margin") ||
strings.HasPrefix(field, "sales_index") ||
strings.HasPrefix(field, "performance_score") ||
strings.HasPrefix(field, "customer_score") ||
strings.HasPrefix(field, "stock_days") ||
strings.HasPrefix(field, "stock_turnover")
}
@@ -6136,6 +6235,9 @@ func productPerformanceUsesCostWeight(field string) bool {
}
func productPerformanceMetricWeight(row map[string]any, field string) float64 {
if productPerformanceUsesScoreWeight(field) {
return productPerformanceScoreWeight(row, productPerformanceScoreSuffix(field))
}
weightField := productPerformanceMetricWeightField(field)
if weightField == "__product_cost_weight" {
return productPerformanceCostWeight(row)
@@ -6143,6 +6245,44 @@ func productPerformanceMetricWeight(row map[string]any, field string) float64 {
return floatFromMap(row, weightField)
}
func productPerformanceUsesScoreWeight(field string) bool {
return strings.HasPrefix(field, "performance_score") ||
strings.HasPrefix(field, "customer_score")
}
func productPerformanceScoreSuffix(field string) string {
switch {
case strings.Contains(field, "_180d"):
return "180d"
case strings.Contains(field, "_365d"):
return "365d"
case strings.Contains(field, "_total"):
return "total"
default:
return "90d"
}
}
func productPerformanceScoreWeight(row map[string]any, suffix string) float64 {
if weight := floatFromMap(row, "sales_usd_"+suffix); weight > 0 {
return weight
}
if weight := floatFromMap(row, "sales_qty_"+suffix); weight > 0 {
return weight
}
stockQty := floatFromMap(row, "stock_qty")
if stockQty <= 0 {
return 1
}
if cost := floatFromMap(row, "cost_price_usd_"+suffix); cost > 0 {
return stockQty * cost
}
if cost := floatFromMap(row, "cost_price_usd"); cost > 0 {
return stockQty * cost
}
return stockQty
}
func productPerformanceCostWeight(row map[string]any) float64 {
if weight := floatFromMap(row, "sales_qty_total"); weight > 0 {
return weight
@@ -6156,6 +6296,9 @@ func productPerformanceCostWeight(row map[string]any) float64 {
}
func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qtyField string) float64 {
if productPerformanceUsesScoreWeight(valueField) {
return weightedAverageProductPerformanceScoreRows(rows, valueField, productPerformanceScoreSuffix(valueField))
}
var weighted, qty float64
for _, row := range rows {
value := floatFromMap(row, valueField)
@@ -6177,6 +6320,30 @@ func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qt
return 0
}
func weightedAverageProductPerformanceScoreRows(rows []map[string]any, valueField, suffix string) float64 {
var weighted, weight, sum, count float64
for _, row := range rows {
value, ok := productPerformanceOptionalFloat(row, valueField)
if !ok {
continue
}
rowWeight := productPerformanceScoreWeight(row, suffix)
if rowWeight > 0 {
weighted += value * rowWeight
weight += rowWeight
}
sum += value
count++
}
if weight > 0 {
return weighted / weight
}
if count > 0 {
return sum / count
}
return 0
}
func averageProductPerformanceDistinctVariantField(rows []map[string]any, valueField string) float64 {
seen := map[string]bool{}
sum := 0.0
@@ -6642,7 +6809,7 @@ func productPerformanceVariantKey(productCode, colorCode, yakaKodu string) strin
}
func normalizeProductPerformanceCostPair(costPriceUSD, basePriceUSD float64) (float64, float64) {
if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD < basePriceUSD {
if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD > basePriceUSD {
return basePriceUSD, costPriceUSD
}
return costPriceUSD, basePriceUSD
+2 -2
View File
@@ -549,11 +549,11 @@ PriceDim AS (
COALESCE(urun_ana_grubu, '') AS urun_ana_grubu,
COALESCE(urun_alt_grubu, '') AS urun_alt_grubu,
CASE
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd < base_price_usd THEN base_price_usd
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd > base_price_usd THEN base_price_usd
ELSE cost_price_usd
END AS cost_price_usd,
CASE
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd < base_price_usd THEN cost_price_usd
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd > base_price_usd THEN cost_price_usd
ELSE base_price_usd
END AS base_price_usd,
base_price_try
+10 -2
View File
@@ -233,6 +233,7 @@ func (v *productPerformanceExcelVariant) fillDimensions(kpiDate, productCode, co
v.UrunAltGrubu = productPerformanceExcelFirstNonEmpty(v.UrunAltGrubu, urunAltGrubu)
v.BasePriceUSD = productPerformanceExcelFirstNonZero(v.BasePriceUSD, basePrice)
v.CostPriceUSD = productPerformanceExcelFirstNonZero(v.CostPriceUSD, costPrice)
v.CostPriceUSD, v.BasePriceUSD = productPerformanceExcelNormalizeCostPair(v.CostPriceUSD, v.BasePriceUSD)
if stockQty > v.StockQty {
v.StockQty = stockQty
}
@@ -402,11 +403,11 @@ func productPerformanceExcelPeriodColumns(label, suffix string, values func(*pro
_, usd, _, _ := values(v)
return usd
}),
numberExcelColumn(label+" Ciro/Adet USD", "number", func(v *productPerformanceExcelVariant) any {
numberExcelColumn(label+" Ort. Satış Fiyatı USD", "number", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelAvgPrice(usd, qty)
}),
numberExcelColumn(label+" Satış/Stok Oranı", "number", func(v *productPerformanceExcelVariant) any {
numberExcelColumn(label+" Stok Devir Hızı", "number", func(v *productPerformanceExcelVariant) any {
qty, _, _, _ := values(v)
return productPerformanceExcelStockTurnover(qty, v.StockQty)
}),
@@ -866,6 +867,13 @@ func productPerformanceExcelFirstNonZero(values ...float64) float64 {
return 0
}
func productPerformanceExcelNormalizeCostPair(costPriceUSD, basePriceUSD float64) (float64, float64) {
if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD > basePriceUSD {
return basePriceUSD, costPriceUSD
}
return costPriceUSD, basePriceUSD
}
func productPerformanceExcelAvgPrice(salesUSD, qty float64) float64 {
if qty <= 0 {
return 0
+190 -86
View File
@@ -1784,7 +1784,7 @@ const detailSalesColumns = [
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
{ name: 'sales_qty', label: 'Adet', field: row => formatNumber(row.sales_qty, 2), align: 'right', sortable: true },
{ name: 'sales_usd', label: 'USD', field: row => formatMoney(row.sales_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_price_usd, 'USD'), align: 'right', sortable: true }
{ name: 'avg_price_usd', label: 'Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd, 'USD'), align: 'right', sortable: true }
]
const columns = [
@@ -1816,14 +1816,14 @@ const columns = [
{ name: 'stock_days_180d', label: '180G Stok Gün', field: row => formatNumber(row.stock_days_180d, 1), align: 'right', sortable: true },
{ name: 'stock_days_365d', label: '360G Stok Gün', field: row => formatNumber(row.stock_days_365d, 1), align: 'right', sortable: true },
{ name: 'stock_days_total', label: 'Genel Stok Gün', field: row => formatNumber(row.stock_days_total, 1), align: 'right', sortable: true },
{ name: 'stock_turnover_90d', label: '90G Satış/Stok', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
{ name: 'stock_turnover_180d', label: '180G Satış/Stok', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true },
{ name: 'stock_turnover_365d', label: '360G Satış/Stok', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true },
{ name: 'stock_turnover_total', label: 'Genel Satış/Stok', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: '90G Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right' },
{ name: 'avg_price_usd_180d', label: '180G Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right' },
{ name: 'avg_price_usd_365d', label: '360G Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right' },
{ name: 'avg_price_usd_total', label: 'Genel Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right' },
{ name: 'stock_turnover_90d', label: '90G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
{ name: 'stock_turnover_180d', label: '180G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true },
{ name: 'stock_turnover_365d', label: '360G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true },
{ name: 'stock_turnover_total', label: 'Genel Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: '90G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right' },
{ name: 'avg_price_usd_180d', label: '180G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right' },
{ name: 'avg_price_usd_365d', label: '360G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right' },
{ name: 'avg_price_usd_total', label: 'Genel Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right' },
{ name: 'base_price_usd', label: 'USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd', label: 'USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'unit_profit_base_90d', label: '90G P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_90d, 'USD'), align: 'right', sortable: true },
@@ -2045,7 +2045,7 @@ const generalColumns = [
{ name: 'sales_usd_total', label: 'Toplam Ciro USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'avg_daily_sales_total', label: 'Günlük Ort.', field: row => formatNumber(row.avg_daily_sales_total, 3), align: 'right', sortable: true },
{ name: 'stock_days_total', label: 'Genel Stok Gün', field: row => formatNumber(row.stock_days_total, 1), align: 'right', sortable: true },
{ name: 'avg_price_usd_total', label: 'Genel Ciro/Adet USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_total', label: 'Genel Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'base_price_usd', label: 'USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd', label: 'USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'unit_profit_base_total', label: 'P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_total, 'USD'), align: 'right', sortable: true },
@@ -2108,7 +2108,7 @@ const orderGroupColumns = [
{ name: 'product_count', label: 'Ürün', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
{ name: 'order_qty', label: 'Açık Sipariş', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
{ name: 'order_usd', label: 'Sipariş USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_order_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_order_price_usd', label: 'Ort. Sipariş Fiyatı USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'base_cost_usd', label: 'Taban Maliyet USD', field: row => formatMoney(row.base_cost_usd, 'USD'), align: 'right', sortable: true },
{ name: 'cost_amount_usd', label: 'Çıplak Maliyet USD', field: row => formatMoney(row.cost_amount_usd, 'USD'), align: 'right', sortable: true },
{ name: 'expected_profit_base_usd', label: 'Brüt K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
@@ -2142,7 +2142,7 @@ const orderProductCustomerColumns = [
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'order_qty', label: 'Açık Sipariş', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
{ name: 'order_usd', label: 'Sipariş USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_order_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_order_price_usd', label: 'Ort. Sipariş Fiyatı USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
{ name: 'base_price_usd', label: 'Taban USD', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
@@ -2176,7 +2176,7 @@ const orderMarketDetailColumns = [
{ name: 'due_date', label: 'Termin', field: 'due_date', align: 'left', sortable: true },
{ name: 'order_qty', label: 'Adet', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
{ name: 'order_usd', label: 'USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_order_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_order_price_usd', label: 'Ort. Sipariş Fiyatı USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
{ name: 'expected_profit_base_usd', label: 'Brüt K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
@@ -2205,7 +2205,7 @@ const idleColumns = [
{ name: 'idle_cost_usd', label: 'Stok Maliyeti USD', field: 'idle_cost_usd', align: 'right', sortable: true },
{ name: 'sales_qty_90d', label: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'stock_days_90d', label: 'Stok Gün', field: row => formatNumber(row.stock_days_90d, 1), align: 'right', sortable: true },
{ name: 'stock_turnover_90d', label: '90G Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
{ name: 'stock_turnover_90d', label: '90G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left' },
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
]
@@ -2236,7 +2236,7 @@ const countryColumns = [
{ name: 'product_count', label: 'Ürün', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: row => formatMoney(row.sales_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'customer_count_90d', label: 'Müşteri', field: row => formatNumber(row.customer_count_90d, 0), align: 'right', sortable: true },
{ name: 'invoice_count_90d', label: 'Fatura', field: row => formatNumber(row.invoice_count_90d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
@@ -2252,7 +2252,7 @@ const customerColumns = [
{ name: 'product_count', label: 'Ürün', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. Satış Fiyatı USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
{ name: 'customer_score_90d', label: '90G Müşteri Skor', field: row => formatNumber(row.customer_score_90d, 2), align: 'right', sortable: true },
{ name: 'base_price_usd_90d', label: '90G Taban', field: row => formatMoney(row.base_price_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_90d', label: '90G Çıplak', field: row => formatMoney(row.cost_price_usd_90d, 'USD'), align: 'right', sortable: true },
@@ -2287,14 +2287,14 @@ const salesBreakdownColumns = [
{ name: 'invoice_count_90d', label: '90G Fatura', field: row => formatNumber(row.invoice_count_90d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
{ name: 'stock_turnover_90d', label: '90G Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: '90G Ort. Satış Fiyatı USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
{ name: 'stock_turnover_90d', label: '90G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
{ name: 'customer_count_180d', label: '180G Müşteri', field: row => formatNumber(row.customer_count_180d, 0), align: 'right', sortable: true },
{ name: 'invoice_count_180d', label: '180G Fatura', field: row => formatNumber(row.invoice_count_180d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_180d', label: '180G Adet', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_180d', label: '180G USD', field: row => formatMoney(row.sales_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_180d', label: '180G Ort. USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'stock_turnover_180d', label: '180G Stok Devir', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true },
{ name: 'avg_price_usd_180d', label: '180G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'stock_turnover_180d', label: '180G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true },
{ name: 'base_price_usd_180d', label: '180G Taban', field: row => formatMoney(row.base_price_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_180d', label: '180G Çıplak', field: row => formatMoney(row.cost_price_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_180d', label: '180G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_180d, 'USD'), align: 'right', sortable: true },
@@ -2304,8 +2304,8 @@ const salesBreakdownColumns = [
{ name: 'customer_score_180d', label: '180G Müşteri Skor', field: row => formatNumber(row.customer_score_180d, 2), align: 'right', sortable: true },
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_365d', label: '360G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_365d', label: '360G Ort. USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'stock_turnover_365d', label: '360G Stok Devir', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true },
{ name: 'avg_price_usd_365d', label: '360G Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'stock_turnover_365d', label: '360G Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true },
{ name: 'base_price_usd_365d', label: '360G Taban', field: row => formatMoney(row.base_price_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_365d', label: '360G Çıplak', field: row => formatMoney(row.cost_price_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_365d', label: '360G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_365d, 'USD'), align: 'right', sortable: true },
@@ -2317,8 +2317,8 @@ const salesBreakdownColumns = [
{ name: 'invoice_count_total', label: 'Genel Fatura', field: row => formatNumber(row.invoice_count_total, 0), align: 'right', sortable: true },
{ name: 'sales_qty_total', label: 'Genel Adet', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true },
{ name: 'sales_usd_total', label: 'Genel USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_total', label: 'Genel Ort. USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'stock_turnover_total', label: 'Genel Stok Devir', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true },
{ name: 'avg_price_usd_total', label: 'Genel Ort. Satış Fiyatı USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'stock_turnover_total', label: 'Genel Stok Devir Hızı', field: row => formatNumber(row.stock_turnover_total, 2), align: 'right', sortable: true },
{ name: 'base_price_usd_total', label: 'Genel Taban', field: row => formatMoney(row.base_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_total', label: 'Genel Çıplak', field: row => formatMoney(row.cost_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_total', label: 'Genel Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_total, 'USD'), align: 'right', sortable: true },
@@ -2359,11 +2359,11 @@ const metricLabelOverrides = {
sales_usd_180d: '180G Toplam Ciro USD',
sales_usd_365d: '360G Toplam Ciro USD',
sales_usd_total: 'Genel Toplam Ciro USD',
avg_price_usd: 'Ciro/Adet USD',
avg_price_usd_90d: '90G Ciro/Adet USD',
avg_price_usd_180d: '180G Ciro/Adet USD',
avg_price_usd_365d: '360G Ciro/Adet USD',
avg_price_usd_total: 'Genel Ciro/Adet USD',
avg_price_usd: 'Ort. Satış Fiyatı USD',
avg_price_usd_90d: '90G Ort. Satış Fiyatı USD',
avg_price_usd_180d: '180G Ort. Satış Fiyatı USD',
avg_price_usd_365d: '360G Ort. Satış Fiyatı USD',
avg_price_usd_total: 'Genel Ort. Satış Fiyatı USD',
base_price_usd: 'Taban Maliyet USD',
cost_price_usd: 'Çıplak Maliyet USD',
base_price_usd_90d: '90G Taban Maliyet USD',
@@ -2412,10 +2412,10 @@ const metricLabelOverrides = {
customer_count_90d: '90G Tekil Müşteri',
market_count_total: 'Genel Tekil Piyasa',
customer_count_total: 'Genel Tekil Müşteri',
stock_turnover_90d: '90G Satış/Stok',
stock_turnover_180d: '180G Satış/Stok',
stock_turnover_365d: '360G Satış/Stok',
stock_turnover_total: 'Genel Satış/Stok',
stock_turnover_90d: '90G Stok Devir Hızı',
stock_turnover_180d: '180G Stok Devir Hızı',
stock_turnover_365d: '360G Stok Devir Hızı',
stock_turnover_total: 'Genel Stok Devir Hızı',
sales_index_90d: '90G Endeks',
sales_index_total: 'Genel Endeks',
stock_days_90d: '90G Stok Gün',
@@ -2645,7 +2645,7 @@ function performanceCardsForRow (row) {
{ key: 'sales180', label: '180G Satış', value: formatNumber(source.sales_qty_180d, 0) },
{ key: 'markets', label: '90G Piyasa', value: formatNumber(source.market_count_90d, 0) },
{ key: 'customers', label: '90G Müşteri', value: formatNumber(source.customer_count_90d, 0) },
{ key: 'avg', label: 'Ort. USD Satış', value: formatMoney(source.avg_price_usd_90d, 'USD') },
{ key: 'avg', label: 'Ort. Satış Fiyatı', value: formatMoney(source.avg_price_usd_90d, 'USD') },
{ key: 'base', label: 'Taban Maliyet', value: formatMoney(source.base_price_usd, 'USD') },
{ key: 'cost', label: 'Çıplak Maliyet', value: formatMoney(source.cost_price_usd, 'USD') },
{ key: 'profitBase', label: 'P.Başı Taban K/Z', value: formatMoney(source.unit_profit_base_90d, 'USD') },
@@ -3470,7 +3470,7 @@ function shouldSumField (field) {
}
function shouldAverageField (field) {
return /^(avg_|unit_|base_price|cost_price|gross_margin|expected_margin|sales_index|performance_score|stock_days|stock_turnover)/i.test(field) ||
return /^(avg_|unit_|base_price|cost_price|gross_margin|expected_margin|sales_index|performance_score|customer_score|stock_days|stock_turnover)/i.test(field) ||
/(_price_usd|_margin|_index|_days)$/i.test(field)
}
@@ -3484,21 +3484,35 @@ function weightFieldForMetric (field) {
}
function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
Object.assign(out, normalizeProductCostFields(out))
for (const suffix of ['90d', '180d', '365d', 'total']) {
const sales = Number(out[`sales_usd_${suffix}`] || 0)
const qty = Number(out[`sales_qty_${suffix}`] || 0)
const stockQty = Number(out.stock_qty || 0)
const avgStock = Number(out[`avg_stock_${suffix}`] || 0)
const turnoverBase = avgStock > 0 ? avgStock : stockQty
out[`stock_turnover_${suffix}`] = turnoverBase > 0 ? qty / turnoverBase : 0
if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty
if (sales > 0) {
if (out[`gross_profit_base_usd_${suffix}`] !== undefined) {
out[`gross_margin_base_${suffix}`] = Number(out[`gross_profit_base_usd_${suffix}`] || 0) / sales
}
if (out[`gross_profit_cost_usd_${suffix}`] !== undefined) {
out[`gross_margin_cost_${suffix}`] = Number(out[`gross_profit_cost_usd_${suffix}`] || 0) / sales
}
if (out[`gross_profit_usd_${suffix}`] !== undefined) {
out[`gross_margin_${suffix}`] = Number(out[`gross_profit_usd_${suffix}`] || 0) / sales
}
const { costPrice, basePrice } = periodCostPair(out, suffix)
out[`base_price_usd_${suffix}`] = basePrice
out[`cost_price_usd_${suffix}`] = costPrice
if (qty > 0) {
const avgPrice = sales / qty
out[`unit_profit_base_${suffix}`] = avgPrice - basePrice
out[`unit_profit_cost_${suffix}`] = avgPrice - costPrice
out[`gross_profit_base_usd_${suffix}`] = sales - qty * basePrice
out[`gross_profit_cost_usd_${suffix}`] = sales - qty * costPrice
out[`gross_profit_usd_${suffix}`] = out[`gross_profit_cost_usd_${suffix}`]
} else {
out[`unit_profit_base_${suffix}`] = 0
out[`unit_profit_cost_${suffix}`] = 0
out[`gross_profit_base_usd_${suffix}`] = 0
out[`gross_profit_cost_usd_${suffix}`] = 0
out[`gross_profit_usd_${suffix}`] = 0
}
out[`gross_margin_base_${suffix}`] = sales > 0 ? Number(out[`gross_profit_base_usd_${suffix}`] || 0) / sales : 0
out[`gross_margin_cost_${suffix}`] = sales > 0 ? Number(out[`gross_profit_cost_usd_${suffix}`] || 0) / sales : 0
out[`gross_margin_${suffix}`] = sales > 0 ? Number(out[`gross_profit_usd_${suffix}`] || 0) / sales : 0
}
const orderUsd = Number(out.order_usd || 0)
@@ -3524,7 +3538,11 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_180d')) out.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(out, '180d'))
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_365d')) out.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(out, '365d'))
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_total')) out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total'))
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score')) out.performance_score = groupPerformanceScore(out, groupField)
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score')) {
out.performance_score = Object.prototype.hasOwnProperty.call(out, 'performance_score_90d')
? Number(out.performance_score_90d || 0)
: groupPerformanceScore(out, groupField)
}
}
function groupPerformanceScore (row, groupField = '') {
@@ -3837,6 +3855,9 @@ function weightedAverage (sourceRows, amountField, qtyField) {
}
function weightedAverageOrAverage (sourceRows, valueField, qtyField) {
if (/^(performance_score|customer_score)/i.test(valueField)) {
return weightedAverageScore(sourceRows, valueField, scoreSuffixForMetric(valueField))
}
const weightedRows = sourceRows
.map(row => ({
value: Number(row[valueField] || 0),
@@ -3850,6 +3871,39 @@ function weightedAverageOrAverage (sourceRows, valueField, qtyField) {
return 0
}
function scoreSuffixForMetric (field) {
if (field.includes('_180d')) return '180d'
if (field.includes('_365d')) return '365d'
if (field.includes('_total')) return 'total'
return '90d'
}
function scoreWeight (row, suffix) {
const salesUsd = Number(row?.[`sales_usd_${suffix}`] || 0)
if (salesUsd > 0) return salesUsd
const salesQty = Number(row?.[`sales_qty_${suffix}`] || 0)
if (salesQty > 0) return salesQty
const stockQty = Number(row?.stock_qty || 0)
if (stockQty <= 0) return 1
const costPrice = Number(row?.[`cost_price_usd_${suffix}`] || row?.cost_price_usd || 0)
return costPrice > 0 ? stockQty * costPrice : stockQty
}
function weightedAverageScore (sourceRows, valueField, suffix) {
const rows = sourceRows
.filter(row => Object.prototype.hasOwnProperty.call(row || {}, valueField))
.map(row => ({
value: Number(row[valueField] || 0),
weight: scoreWeight(row, suffix)
}))
.filter(row => Number.isFinite(row.value) && Number.isFinite(row.weight))
const weight = rows.reduce((sum, row) => sum + Math.max(0, row.weight), 0)
if (weight > 0) {
return rows.reduce((sum, row) => sum + row.value * Math.max(0, row.weight), 0) / weight
}
return rows.length ? rows.reduce((sum, row) => sum + row.value, 0) / rows.length : 0
}
function weightedAverageProductCost (sourceRows, valueField) {
const salesRows = sourceRows
.map(row => ({
@@ -4068,53 +4122,93 @@ function formatGroupCell (row, colOrName) {
}
function withProductMargins (row) {
const salesQtyTotal = Number(row?.sales_qty_total || 0)
const salesUSDTotal = Number(row?.sales_usd_total || 0)
const basePrice = Number(row?.base_price_usd || 0)
const costPrice = Number(row?.cost_price_usd || 0)
const grossProfitBaseTotal = salesQtyTotal > 0 ? salesUSDTotal - (salesQtyTotal * basePrice) : 0
const grossProfitCostTotal = salesQtyTotal > 0 ? salesUSDTotal - (salesQtyTotal * costPrice) : 0
return {
...row,
avg_price_usd_365d: Number(row?.sales_qty_365d || 0) > 0 ? Number(row?.sales_usd_365d || 0) / Number(row?.sales_qty_365d || 0) : 0,
avg_price_usd_total: salesQtyTotal > 0 ? salesUSDTotal / salesQtyTotal : Number(row?.avg_price_usd_total || 0),
stock_turnover_90d: existingOrStockTurnover(row?.stock_turnover_90d, row?.sales_qty_90d, row?.stock_qty),
stock_turnover_180d: existingOrStockTurnover(row?.stock_turnover_180d, row?.sales_qty_180d, row?.stock_qty),
stock_turnover_365d: existingOrStockTurnover(row?.stock_turnover_365d, row?.sales_qty_365d, row?.stock_qty),
stock_turnover_total: existingOrStockTurnover(row?.stock_turnover_total, row?.sales_qty_total, row?.stock_qty),
gross_profit_base_usd_total: grossProfitBaseTotal,
gross_profit_cost_usd_total: grossProfitCostTotal,
gross_profit_usd_total: Number(row?.gross_profit_usd_total ?? grossProfitCostTotal),
gross_margin_base_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.base_price_usd),
gross_margin_cost_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.cost_price_usd),
gross_margin_base_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.base_price_usd),
gross_margin_cost_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.cost_price_usd),
gross_margin_base_365d: marginFromSalesCost(row?.sales_usd_365d, row?.sales_qty_365d, row?.base_price_usd),
gross_margin_cost_365d: marginFromSalesCost(row?.sales_usd_365d, row?.sales_qty_365d, row?.cost_price_usd),
gross_margin_base_total: marginFromSalesCost(salesUSDTotal, salesQtyTotal, basePrice),
gross_margin_cost_total: marginFromSalesCost(salesUSDTotal, salesQtyTotal, costPrice)
const next = normalizeProductCostFields(row)
const out = {
...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,
stock_turnover_90d: existingOrStockTurnover(next?.stock_turnover_90d, next?.sales_qty_90d, next?.stock_qty),
stock_turnover_180d: existingOrStockTurnover(next?.stock_turnover_180d, next?.sales_qty_180d, next?.stock_qty),
stock_turnover_365d: existingOrStockTurnover(next?.stock_turnover_365d, next?.sales_qty_365d, next?.stock_qty),
stock_turnover_total: existingOrStockTurnover(next?.stock_turnover_total, next?.sales_qty_total, next?.stock_qty)
}
for (const suffix of ['90d', '180d', '365d', 'total']) {
applyPeriodProfitFields(out, suffix)
}
return out
}
function withGeneralMargins (row) {
const salesQtyTotal = Number(row?.sales_qty_total || 0)
const salesUSDTotal = Number(row?.sales_usd_total || 0)
const basePrice = Number(row?.base_price_usd || 0)
const costPrice = Number(row?.cost_price_usd || 0)
const grossProfitBaseTotal = salesQtyTotal > 0 ? salesUSDTotal - (salesQtyTotal * basePrice) : 0
const grossProfitCostTotal = salesQtyTotal > 0 ? salesUSDTotal - (salesQtyTotal * costPrice) : 0
const out = normalizeProductCostFields(row)
applyPeriodProfitFields(out, 'total')
return {
...row,
avg_price_usd_total: salesQtyTotal > 0 ? salesUSDTotal / salesQtyTotal : Number(row?.avg_price_usd_total || 0),
stock_turnover_total: existingOrStockTurnover(row?.stock_turnover_total, row?.sales_qty_total, row?.stock_qty),
gross_profit_base_usd_total: grossProfitBaseTotal,
gross_profit_cost_usd_total: grossProfitCostTotal,
gross_profit_usd_total: Number(row?.gross_profit_usd_total ?? grossProfitCostTotal),
gross_margin_base_total: marginFromSalesCost(salesUSDTotal, salesQtyTotal, basePrice),
gross_margin_cost_total: marginFromSalesCost(salesUSDTotal, salesQtyTotal, costPrice)
...out,
stock_turnover_total: existingOrStockTurnover(out?.stock_turnover_total, out?.sales_qty_total, out?.stock_qty)
}
}
function normalizeProductCostFields (row) {
const next = { ...row }
normalizeProductCostPairFields(next, 'cost_price_usd', 'base_price_usd')
for (const suffix of ['90d', '180d', '365d', 'total']) {
normalizeProductCostPairFields(next, `cost_price_usd_${suffix}`, `base_price_usd_${suffix}`)
}
return next
}
function normalizeProductCostPairFields (row, costField, baseField) {
const hasCost = Object.prototype.hasOwnProperty.call(row || {}, costField)
const hasBase = Object.prototype.hasOwnProperty.call(row || {}, baseField)
if (!hasCost && !hasBase) return
let costPrice = Number(row?.[costField] || 0)
let basePrice = Number(row?.[baseField] || 0)
if (costPrice > 0 && basePrice > 0 && costPrice > basePrice) {
const tmp = costPrice
costPrice = basePrice
basePrice = tmp
}
if (hasCost || costPrice > 0) row[costField] = costPrice
if (hasBase || basePrice > 0) row[baseField] = basePrice
}
function periodCostPair (row, suffix) {
const costField = `cost_price_usd_${suffix}`
const baseField = `base_price_usd_${suffix}`
let costPrice = Number(row?.[costField] || 0)
let basePrice = Number(row?.[baseField] || 0)
if (costPrice <= 0) costPrice = Number(row?.cost_price_usd || 0)
if (basePrice <= 0) basePrice = Number(row?.base_price_usd || 0)
if (costPrice > 0 && basePrice > 0 && costPrice > basePrice) {
const tmp = costPrice
costPrice = basePrice
basePrice = tmp
}
return { costPrice, basePrice }
}
function applyPeriodProfitFields (row, suffix) {
const qty = Number(row?.[`sales_qty_${suffix}`] || 0)
const salesUSD = Number(row?.[`sales_usd_${suffix}`] || 0)
const { costPrice, basePrice } = periodCostPair(row, suffix)
row[`base_price_usd_${suffix}`] = basePrice
row[`cost_price_usd_${suffix}`] = costPrice
if (qty > 0) {
row[`avg_price_usd_${suffix}`] = salesUSD / qty
row[`unit_profit_base_${suffix}`] = salesUSD / qty - basePrice
row[`unit_profit_cost_${suffix}`] = salesUSD / qty - costPrice
row[`gross_profit_base_usd_${suffix}`] = salesUSD - qty * basePrice
row[`gross_profit_cost_usd_${suffix}`] = salesUSD - qty * costPrice
row[`gross_profit_usd_${suffix}`] = row[`gross_profit_cost_usd_${suffix}`]
} else {
row[`unit_profit_base_${suffix}`] = 0
row[`unit_profit_cost_${suffix}`] = 0
row[`gross_profit_base_usd_${suffix}`] = 0
row[`gross_profit_cost_usd_${suffix}`] = 0
row[`gross_profit_usd_${suffix}`] = 0
}
row[`gross_margin_base_${suffix}`] = marginFromSalesCost(salesUSD, qty, basePrice)
row[`gross_margin_cost_${suffix}`] = marginFromSalesCost(salesUSD, qty, costPrice)
}
function stockTurnover (salesQty, stockQty) {
const stock = Number(stockQty || 0)
if (stock <= 0) return 0
@@ -4461,11 +4555,21 @@ function formatProductCell (row, name) {
case 'gross_profit_usd_90d':
case 'gross_profit_usd_180d':
case 'gross_profit_usd_total':
case 'gross_profit_base_usd_90d':
case 'gross_profit_cost_usd_90d':
case 'gross_profit_base_usd_180d':
case 'gross_profit_cost_usd_180d':
case 'gross_profit_base_usd_365d':
case 'gross_profit_cost_usd_365d':
case 'gross_profit_base_usd_total':
case 'gross_profit_cost_usd_total':
return formatMoney(row[name], 'USD')
case 'gross_margin_base_90d':
case 'gross_margin_cost_90d':
case 'gross_margin_base_180d':
case 'gross_margin_cost_180d':
case 'gross_margin_base_365d':
case 'gross_margin_cost_365d':
case 'gross_margin_base_total':
case 'gross_margin_cost_total':
case 'gross_margin_90d':
@@ -5045,7 +5149,7 @@ async function loadBackendGroupedRows (options = {}) {
function normalizeGroupedDisplayRow (tabKey, row) {
const nextRow = { ...row }
normalizeGroupedProductHierarchyValues(nextRow)
return nextRow
return withPeriodScores(withGeneralMargins(withProductMargins(nextRow)))
}
function normalizeGroupedProductHierarchyValues (row) {