From 3993c363cbc4203d480fd545339ba6572cfd7043 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Mon, 6 Jul 2026 01:28:44 +0300 Subject: [PATCH] Fix product performance grouped JSON build --- svc/models/product_performance.go | 145 ++++++++------- svc/queries/product_performance.go | 172 ++++++++++++++++-- svc/queries/product_performance_sql.go | 37 +++- .../pages/ProductPerformanceProfitability.vue | 43 +++-- 4 files changed, 296 insertions(+), 101 deletions(-) diff --git a/svc/models/product_performance.go b/svc/models/product_performance.go index fb1511a..1389b05 100644 --- a/svc/models/product_performance.go +++ b/svc/models/product_performance.go @@ -1,68 +1,85 @@ package models type ProductPerformanceRow struct { - KpiDate string `json:"kpi_date"` - ProductCode string `json:"product_code"` - ColorCode string `json:"color_code"` - ColorDescription string `json:"color_description"` - YakaKodu string `json:"yaka_kodu"` - ItemDescription string `json:"item_description"` - Kategori string `json:"kategori"` - Seri string `json:"seri"` - YasGrubu string `json:"yas_grubu"` - AskiliYan string `json:"askili_yan"` - UrunIlkGrubu string `json:"urun_ilk_grubu"` - UrunAnaGrubu string `json:"urun_ana_grubu"` - UrunAltGrubu string `json:"urun_alt_grubu"` - MarketKey string `json:"market_key"` - StockQty float64 `json:"stock_qty"` - SalesQty30 float64 `json:"sales_qty_30d"` - SalesQty90 float64 `json:"sales_qty_90d"` - SalesQty180 float64 `json:"sales_qty_180d"` - SalesQty365 float64 `json:"sales_qty_365d"` - SalesQty730 float64 `json:"sales_qty_730d"` - SalesQtyTotal float64 `json:"sales_qty_total"` - SalesUSD30 float64 `json:"sales_usd_30d"` - SalesUSD90 float64 `json:"sales_usd_90d"` - SalesUSD180 float64 `json:"sales_usd_180d"` - SalesUSD365 float64 `json:"sales_usd_365d"` - SalesUSDTotal float64 `json:"sales_usd_total"` - AvgDailySales90 float64 `json:"avg_daily_sales_90d"` - AvgDailySales180 float64 `json:"avg_daily_sales_180d"` - AvgDailySales365 float64 `json:"avg_daily_sales_365d"` - AvgDailySalesTotal float64 `json:"avg_daily_sales_total"` - StockDays90 float64 `json:"stock_days_90d"` - StockDays180 float64 `json:"stock_days_180d"` - StockDays365 float64 `json:"stock_days_365d"` - StockDaysTotal float64 `json:"stock_days_total"` - StockTurnover90 float64 `json:"stock_turnover_90d"` - StockTurnover180 float64 `json:"stock_turnover_180d"` - StockTurnover365 float64 `json:"stock_turnover_365d"` - StockTurnoverTotal float64 `json:"stock_turnover_total"` - AvgPriceUSD90 float64 `json:"avg_price_usd_90d"` - AvgPriceUSD180 float64 `json:"avg_price_usd_180d"` - CostPriceUSD float64 `json:"cost_price_usd"` - BasePriceUSD float64 `json:"base_price_usd"` - BasePriceTRY float64 `json:"base_price_try"` - GrossProfitUSD90 float64 `json:"gross_profit_usd_90d"` - GrossProfitUSD180 float64 `json:"gross_profit_usd_180d"` - GrossMargin90 float64 `json:"gross_margin_90d"` - GrossMargin180 float64 `json:"gross_margin_180d"` - UnitProfitCost90 float64 `json:"unit_profit_cost_90d"` - UnitProfitCost180 float64 `json:"unit_profit_cost_180d"` - UnitProfitBase90 float64 `json:"unit_profit_base_90d"` - UnitProfitBase180 float64 `json:"unit_profit_base_180d"` - MarketCount90 int `json:"market_count_90d"` - CustomerCount90 int `json:"customer_count_90d"` - SalesIndex90 float64 `json:"sales_index_90d"` - PriceIndex90 float64 `json:"price_index_90d"` - MarginIndex90 float64 `json:"margin_index_90d"` - PerformanceScore float64 `json:"performance_score"` - PerformanceBucket string `json:"performance_bucket"` - Recommendation string `json:"recommendation"` - LastSaleDate string `json:"last_sale_date"` - LastRefNumber string `json:"last_ref_number"` - UpdatedAt string `json:"updated_at"` + KpiDate string `json:"kpi_date"` + ProductCode string `json:"product_code"` + ColorCode string `json:"color_code"` + ColorDescription string `json:"color_description"` + YakaKodu string `json:"yaka_kodu"` + ItemDescription string `json:"item_description"` + Kategori string `json:"kategori"` + Seri string `json:"seri"` + YasGrubu string `json:"yas_grubu"` + AskiliYan string `json:"askili_yan"` + UrunIlkGrubu string `json:"urun_ilk_grubu"` + UrunAnaGrubu string `json:"urun_ana_grubu"` + UrunAltGrubu string `json:"urun_alt_grubu"` + MarketKey string `json:"market_key"` + StockQty float64 `json:"stock_qty"` + SalesQty30 float64 `json:"sales_qty_30d"` + SalesQty90 float64 `json:"sales_qty_90d"` + SalesQty180 float64 `json:"sales_qty_180d"` + SalesQty365 float64 `json:"sales_qty_365d"` + SalesQty730 float64 `json:"sales_qty_730d"` + SalesQtyTotal float64 `json:"sales_qty_total"` + SalesUSD30 float64 `json:"sales_usd_30d"` + SalesUSD90 float64 `json:"sales_usd_90d"` + SalesUSD180 float64 `json:"sales_usd_180d"` + SalesUSD365 float64 `json:"sales_usd_365d"` + SalesUSDTotal float64 `json:"sales_usd_total"` + AvgDailySales90 float64 `json:"avg_daily_sales_90d"` + AvgDailySales180 float64 `json:"avg_daily_sales_180d"` + AvgDailySales365 float64 `json:"avg_daily_sales_365d"` + AvgDailySalesTotal float64 `json:"avg_daily_sales_total"` + AvgStock90 float64 `json:"avg_stock_90d"` + AvgStock180 float64 `json:"avg_stock_180d"` + AvgStock365 float64 `json:"avg_stock_365d"` + AvgStockTotal float64 `json:"avg_stock_total"` + StockDays90 float64 `json:"stock_days_90d"` + StockDays180 float64 `json:"stock_days_180d"` + StockDays365 float64 `json:"stock_days_365d"` + StockDaysTotal float64 `json:"stock_days_total"` + StockTurnover90 float64 `json:"stock_turnover_90d"` + StockTurnover180 float64 `json:"stock_turnover_180d"` + StockTurnover365 float64 `json:"stock_turnover_365d"` + StockTurnoverTotal float64 `json:"stock_turnover_total"` + AvgPriceUSD90 float64 `json:"avg_price_usd_90d"` + AvgPriceUSD180 float64 `json:"avg_price_usd_180d"` + CostPriceUSD float64 `json:"cost_price_usd"` + BasePriceUSD float64 `json:"base_price_usd"` + BasePriceTRY float64 `json:"base_price_try"` + GrossProfitUSD90 float64 `json:"gross_profit_usd_90d"` + GrossProfitUSD180 float64 `json:"gross_profit_usd_180d"` + GrossMargin90 float64 `json:"gross_margin_90d"` + GrossMargin180 float64 `json:"gross_margin_180d"` + UnitProfitCost90 float64 `json:"unit_profit_cost_90d"` + UnitProfitCost180 float64 `json:"unit_profit_cost_180d"` + UnitProfitBase90 float64 `json:"unit_profit_base_90d"` + UnitProfitBase180 float64 `json:"unit_profit_base_180d"` + MarketCount90 int `json:"market_count_90d"` + MarketCount180 int `json:"market_count_180d"` + MarketCount365 int `json:"market_count_365d"` + MarketCountTotal int `json:"market_count_total"` + CustomerCount90 int `json:"customer_count_90d"` + CustomerCount180 int `json:"customer_count_180d"` + CustomerCount365 int `json:"customer_count_365d"` + CustomerCountTotal int `json:"customer_count_total"` + SalesIndex90 float64 `json:"sales_index_90d"` + SalesIndex180 float64 `json:"sales_index_180d"` + SalesIndex365 float64 `json:"sales_index_365d"` + SalesIndexTotal float64 `json:"sales_index_total"` + PriceIndex90 float64 `json:"price_index_90d"` + MarginIndex90 float64 `json:"margin_index_90d"` + PerformanceScore90 float64 `json:"performance_score_90d"` + PerformanceScore180 float64 `json:"performance_score_180d"` + PerformanceScore365 float64 `json:"performance_score_365d"` + PerformanceScoreTotal float64 `json:"performance_score_total"` + PerformanceScore float64 `json:"performance_score"` + PerformanceBucket string `json:"performance_bucket"` + Recommendation string `json:"recommendation"` + LastSaleDate string `json:"last_sale_date"` + LastRefNumber string `json:"last_ref_number"` + UpdatedAt string `json:"updated_at"` } type ProductPerformanceSummary struct { @@ -378,6 +395,10 @@ type ProductPerformanceSalesBreakdownRow struct { GrossMarginBaseTotal float64 `json:"gross_margin_base_total"` GrossMarginCostTotal float64 `json:"gross_margin_cost_total"` StockQty float64 `json:"stock_qty"` + AvgStock90 float64 `json:"avg_stock_90d"` + AvgStock180 float64 `json:"avg_stock_180d"` + AvgStock365 float64 `json:"avg_stock_365d"` + AvgStockTotal float64 `json:"avg_stock_total"` StockTurnover90 float64 `json:"stock_turnover_90d"` StockTurnover180 float64 `json:"stock_turnover_180d"` StockTurnover365 float64 `json:"stock_turnover_365d"` diff --git a/svc/queries/product_performance.go b/svc/queries/product_performance.go index a415168..1fce559 100644 --- a/svc/queries/product_performance.go +++ b/svc/queries/product_performance.go @@ -481,6 +481,10 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily ( sales_usd_365d NUMERIC(18,4) NOT NULL DEFAULT 0, avg_daily_sales_90d NUMERIC(18,6) NOT NULL DEFAULT 0, avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0, + avg_stock_90d NUMERIC(18,6) NOT NULL DEFAULT 0, + avg_stock_180d NUMERIC(18,6) NOT NULL DEFAULT 0, + avg_stock_365d NUMERIC(18,6) NOT NULL DEFAULT 0, + avg_stock_total NUMERIC(18,6) NOT NULL DEFAULT 0, stock_days_90d NUMERIC(18,6) NOT NULL DEFAULT 0, stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0, avg_price_usd_90d NUMERIC(18,6) NOT NULL DEFAULT 0, @@ -497,8 +501,17 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily ( unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0, unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0, market_count_90d INTEGER NOT NULL DEFAULT 0, + market_count_180d INTEGER NOT NULL DEFAULT 0, + market_count_365d INTEGER NOT NULL DEFAULT 0, + market_count_total INTEGER NOT NULL DEFAULT 0, customer_count_90d INTEGER NOT NULL DEFAULT 0, + customer_count_180d INTEGER NOT NULL DEFAULT 0, + customer_count_365d INTEGER NOT NULL DEFAULT 0, + customer_count_total INTEGER NOT NULL DEFAULT 0, sales_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0, + sales_index_180d NUMERIC(18,6) NOT NULL DEFAULT 0, + sales_index_365d NUMERIC(18,6) NOT NULL DEFAULT 0, + sales_index_total NUMERIC(18,6) NOT NULL DEFAULT 0, price_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0, margin_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0, performance_score NUMERIC(18,6) NOT NULL DEFAULT 0, @@ -552,6 +565,10 @@ WHERE btrim(askili_yan) = '-'`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_365d NUMERIC(18,6) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_total NUMERIC(18,6) NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_90d NUMERIC(18,6) NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_180d NUMERIC(18,6) NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_365d NUMERIC(18,6) NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_total NUMERIC(18,6) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_365d NUMERIC(18,6) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_total NUMERIC(18,6) NOT NULL DEFAULT 0`, @@ -567,6 +584,15 @@ WHERE btrim(askili_yan) = '-'`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0`, `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_90d INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_180d INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_365d INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_total INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_180d INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_365d INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_total INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_180d NUMERIC(18,6) NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_365d NUMERIC(18,6) NOT NULL DEFAULT 0`, + `ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_total NUMERIC(18,6) NOT NULL DEFAULT 0`, ` CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot ( report_key TEXT NOT NULL, @@ -1770,11 +1796,15 @@ SELECT sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total, sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total, avg_daily_sales_90d, avg_daily_sales_180d, avg_daily_sales_365d, avg_daily_sales_total, + avg_stock_90d, avg_stock_180d, avg_stock_365d, avg_stock_total, stock_days_90d, stock_days_180d, stock_days_365d, stock_days_total, stock_turnover_90d, stock_turnover_180d, stock_turnover_365d, stock_turnover_total, avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try, gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d, - unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, market_count_90d, customer_count_90d, sales_index_90d, + unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, + market_count_90d, market_count_180d, market_count_365d, market_count_total, + customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total, + sales_index_90d, sales_index_180d, sales_index_365d, sales_index_total, price_index_90d, margin_index_90d, performance_score, performance_bucket, recommendation, COALESCE(to_char(last_sale_date,'YYYY-MM-DD'),''), last_ref_number, to_char(updated_at,'YYYY-MM-DD HH24:MI:SS') @@ -1796,11 +1826,15 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...) &r.SalesQty30, &r.SalesQty90, &r.SalesQty180, &r.SalesQty365, &r.SalesQty730, &r.SalesQtyTotal, &r.SalesUSD30, &r.SalesUSD90, &r.SalesUSD180, &r.SalesUSD365, &r.SalesUSDTotal, &r.AvgDailySales90, &r.AvgDailySales180, &r.AvgDailySales365, &r.AvgDailySalesTotal, + &r.AvgStock90, &r.AvgStock180, &r.AvgStock365, &r.AvgStockTotal, &r.StockDays90, &r.StockDays180, &r.StockDays365, &r.StockDaysTotal, &r.StockTurnover90, &r.StockTurnover180, &r.StockTurnover365, &r.StockTurnoverTotal, &r.AvgPriceUSD90, &r.AvgPriceUSD180, &r.CostPriceUSD, &r.BasePriceUSD, &r.BasePriceTRY, &r.GrossProfitUSD90, &r.GrossProfitUSD180, &r.GrossMargin90, &r.GrossMargin180, - &r.UnitProfitCost90, &r.UnitProfitCost180, &r.UnitProfitBase90, &r.UnitProfitBase180, &r.MarketCount90, &r.CustomerCount90, &r.SalesIndex90, + &r.UnitProfitCost90, &r.UnitProfitCost180, &r.UnitProfitBase90, &r.UnitProfitBase180, + &r.MarketCount90, &r.MarketCount180, &r.MarketCount365, &r.MarketCountTotal, + &r.CustomerCount90, &r.CustomerCount180, &r.CustomerCount365, &r.CustomerCountTotal, + &r.SalesIndex90, &r.SalesIndex180, &r.SalesIndex365, &r.SalesIndexTotal, &r.PriceIndex90, &r.MarginIndex90, &r.PerformanceScore, &r.PerformanceBucket, &r.Recommendation, &r.LastSaleDate, &r.LastRefNumber, &r.UpdatedAt, ); err != nil { @@ -1824,37 +1858,104 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...) func applyProductPerformanceProductRowScores(rows []models.ProductPerformanceRow) { avg := productPerformanceProductRowAverages(rows) for i := range rows { - rows[i].PerformanceScore = productPerformanceProductScore( + rows[i].SalesIndex90 = productPerformanceRelativeIndex(rows[i].SalesUSD90, avg.salesUSD90) + rows[i].SalesIndex180 = productPerformanceRelativeIndex(rows[i].SalesUSD180, avg.salesUSD180) + rows[i].SalesIndex365 = productPerformanceRelativeIndex(rows[i].SalesUSD365, avg.salesUSD365) + rows[i].SalesIndexTotal = productPerformanceRelativeIndex(rows[i].SalesUSDTotal, avg.salesUSDTotal) + + margin365 := productPerformanceMarginFromSales(rows[i].SalesUSD365, rows[i].SalesQty365, rows[i].CostPriceUSD) + marginTotal := productPerformanceMarginFromSales(rows[i].SalesUSDTotal, rows[i].SalesQtyTotal, rows[i].CostPriceUSD) + + rows[i].PerformanceScore90 = productPerformanceProductScore( "90d", rows[i].SalesUSD90, - productPerformanceRelativeIndex(rows[i].SalesUSD90, avg.salesUSD90), + rows[i].SalesIndex90, rows[i].GrossMargin90, rows[i].StockTurnover90, float64(rows[i].MarketCount90), float64(rows[i].CustomerCount90), ) + rows[i].PerformanceScore180 = productPerformanceProductScore( + "180d", + rows[i].SalesUSD180, + rows[i].SalesIndex180, + rows[i].GrossMargin180, + rows[i].StockTurnover180, + float64(rows[i].MarketCount180), + float64(rows[i].CustomerCount180), + ) + rows[i].PerformanceScore365 = productPerformanceProductScore( + "365d", + rows[i].SalesUSD365, + rows[i].SalesIndex365, + margin365, + rows[i].StockTurnover365, + float64(rows[i].MarketCount365), + float64(rows[i].CustomerCount365), + ) + rows[i].PerformanceScoreTotal = productPerformanceProductScore( + "total", + rows[i].SalesUSDTotal, + rows[i].SalesIndexTotal, + marginTotal, + rows[i].StockTurnoverTotal, + float64(rows[i].MarketCountTotal), + float64(rows[i].CustomerCountTotal), + ) + rows[i].PerformanceScore = rows[i].PerformanceScore90 } } type productPerformanceProductRowAverage struct { - salesUSD90 float64 + salesUSD90 float64 + salesUSD180 float64 + salesUSD365 float64 + salesUSDTotal float64 } func productPerformanceProductRowAverages(rows []models.ProductPerformanceRow) productPerformanceProductRowAverage { - var sum90, count90 float64 + var sum90, count90, sum180, count180, sum365, count365, sumTotal, countTotal float64 for _, row := range rows { if row.SalesUSD90 > 0 { sum90 += row.SalesUSD90 count90++ } + if row.SalesUSD180 > 0 { + sum180 += row.SalesUSD180 + count180++ + } + if row.SalesUSD365 > 0 { + sum365 += row.SalesUSD365 + count365++ + } + if row.SalesUSDTotal > 0 { + sumTotal += row.SalesUSDTotal + countTotal++ + } } out := productPerformanceProductRowAverage{} if count90 > 0 { out.salesUSD90 = sum90 / count90 } + if count180 > 0 { + out.salesUSD180 = sum180 / count180 + } + if count365 > 0 { + out.salesUSD365 = sum365 / count365 + } + if countTotal > 0 { + out.salesUSDTotal = sumTotal / countTotal + } return out } +func productPerformanceMarginFromSales(salesUSD, salesQty, unitCost float64) float64 { + if salesUSD <= 0 { + return 0 + } + return (salesUSD - (salesQty * unitCost)) / salesUSD +} + func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.ProductPerformanceSummary, error) { if err := EnsureProductPerformanceTables(pg); err != nil { return models.ProductPerformanceSummary{}, err @@ -3699,6 +3800,10 @@ SELECT gross_profit_cost_usd_total, gross_margin_base_total, gross_margin_cost_total, + avg_stock_90d, + avg_stock_180d, + avg_stock_365d, + 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_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,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, @@ -3807,6 +3912,7 @@ LIMIT $1 &r.GrossMarginCost365, &r.CustomerCount365, &r.InvoiceCount365, &r.CustomerCountTotal, &r.InvoiceCountTotal, &r.SalesQtyTotal, &r.SalesUSDTotal, &r.AvgPriceUSDTotal, &r.BasePriceUSDTotal, &r.CostPriceUSDTotal, &r.GrossProfitBaseTotal, &r.GrossProfitCostTotal, &r.GrossMarginBaseTotal, &r.GrossMarginCostTotal, + &r.AvgStock90, &r.AvgStock180, &r.AvgStock365, &r.AvgStockTotal, &r.StockTurnover90, &r.StockTurnover180, &r.StockTurnover365, &r.StockTurnoverTotal, &r.HasCost, &r.SalesIndex90, &r.CustomerScore90, &r.CustomerScore180, &r.CustomerScore365, &r.CustomerScoreTotal, &r.PerformanceScore, &r.PerformanceBucket, &r.Recommendation, &r.LastSaleDate, @@ -4639,10 +4745,10 @@ Source AS ( urun_alt_grubu, market_key, COALESCE(stock_qty,0) AS stock_qty, - (COALESCE((SELECT s.stock_qty FROM Stock90Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0 AS avg_stock_90d, - (COALESCE((SELECT s.stock_qty FROM Stock180Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0 AS avg_stock_180d, - (COALESCE((SELECT s.stock_qty FROM Stock365Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0 AS avg_stock_365d, - (COALESCE((SELECT s.stock_qty FROM StockTotalStart s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0 AS avg_stock_total, + COALESCE(NULLIF(avg_stock_90d,0), (COALESCE((SELECT s.stock_qty FROM Stock90Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_90d, + COALESCE(NULLIF(avg_stock_180d,0), (COALESCE((SELECT s.stock_qty FROM Stock180Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_180d, + COALESCE(NULLIF(avg_stock_365d,0), (COALESCE((SELECT s.stock_qty FROM Stock365Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_365d, + COALESCE(NULLIF(avg_stock_total,0), (COALESCE((SELECT s.stock_qty FROM StockTotalStart s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_total, COALESCE(sales_qty_90d,0) AS sales_qty_90d, COALESCE(sales_qty_180d,0) AS sales_qty_180d, COALESCE(sales_qty_365d,0) AS sales_qty_365d, @@ -4669,8 +4775,17 @@ Source AS ( COALESCE(unit_profit_base_90d,0) AS unit_profit_base_90d, COALESCE(unit_profit_base_180d,0) AS unit_profit_base_180d, COALESCE(market_count_90d,0) AS market_count_90d, + COALESCE(market_count_180d,0) AS market_count_180d, + COALESCE(market_count_365d,0) AS market_count_365d, + COALESCE(market_count_total,0) AS market_count_total, COALESCE(customer_count_90d,0) AS customer_count_90d, + COALESCE(customer_count_180d,0) AS customer_count_180d, + COALESCE(customer_count_365d,0) AS customer_count_365d, + COALESCE(customer_count_total,0) AS customer_count_total, COALESCE(sales_index_90d,0) AS sales_index_90d, + COALESCE(sales_index_180d,0) AS sales_index_180d, + COALESCE(sales_index_365d,0) AS sales_index_365d, + COALESCE(sales_index_total,0) AS sales_index_total, COALESCE(price_index_90d,0) AS price_index_90d, COALESCE(margin_index_90d,0) AS margin_index_90d, COALESCE(performance_score,0) AS performance_score, @@ -5069,7 +5184,9 @@ func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql. } } if value, ok := general["performance_score"]; ok { - row["performance_score_total"] = value + if _, exists := row["performance_score_total"]; !exists { + row["performance_score_total"] = value + } } } return rows @@ -5849,6 +5966,19 @@ func productPerformanceMapVariantKey(row map[string]any) string { func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) { normalizeProductPerformanceCostFields(out) + preservedPerformanceScores := map[string]float64{} + if score, ok := productPerformanceOptionalFloat(out, "performance_score"); ok { + preservedPerformanceScores["90d"] = score + } + preservedCustomerScores := map[string]float64{} + for _, suffix := range productPerformancePeriodSuffixes() { + if score, ok := productPerformanceOptionalFloat(out, "performance_score_"+suffix); ok { + preservedPerformanceScores[suffix] = score + } + if score, ok := productPerformanceOptionalFloat(out, "customer_score_"+suffix); ok { + preservedCustomerScores[suffix] = score + } + } for _, suffix := range productPerformancePeriodSuffixes() { sales := floatFromMap(out, "sales_usd_"+suffix) qty := floatFromMap(out, "sales_qty_"+suffix) @@ -5926,14 +6056,18 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 { out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty } - out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d")) - out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d")) - out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d")) - out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total")) - out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d") - out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d") - out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d") - out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total") + for _, suffix := range productPerformancePeriodSuffixes() { + if score, ok := preservedCustomerScores[suffix]; ok { + out["customer_score_"+suffix] = score + } else { + out["customer_score_"+suffix] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, suffix)) + } + if score, ok := preservedPerformanceScores[suffix]; ok { + out["performance_score_"+suffix] = score + } else { + out["performance_score_"+suffix] = productPerformanceSalesPeriodScore(out, suffix) + } + } out["performance_score"] = out["performance_score_90d"] if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 { score := productPerformanceOrderGroupScore(out) diff --git a/svc/queries/product_performance_sql.go b/svc/queries/product_performance_sql.go index 4c44512..e2e8591 100644 --- a/svc/queries/product_performance_sql.go +++ b/svc/queries/product_performance_sql.go @@ -486,6 +486,9 @@ SalesAgg AS ( SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS sales_usd_365d, SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01') AS sales_usd_total, COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_90d, + COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_180d, + COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_365d, + COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(sales_usd, 0) > 0) AS customer_count_total, MAX(sales_date) AS last_sale_date, MAX(last_ref_number) AS last_ref_number FROM mk_product_performance_sales_daily @@ -500,7 +503,7 @@ Scope AS ( item_description, kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total, sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total, - customer_count_90d, last_sale_date, last_ref_number + customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total, last_sale_date, last_ref_number FROM SalesAgg UNION ALL SELECT @@ -528,6 +531,9 @@ Scope AS ( 0 AS sales_usd_365d, 0 AS sales_usd_total, 0 AS customer_count_90d, + 0 AS customer_count_180d, + 0 AS customer_count_365d, + 0 AS customer_count_total, NULL::date AS last_sale_date, '' AS last_ref_number FROM LatestStock ls @@ -602,15 +608,20 @@ Spread AS ( product_code, color_code, yaka_kodu, - COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) AS market_count_90d + COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_90d, 0) > 0) AS market_count_90d, + COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_180d, 0) > 0) AS market_count_180d, + COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_365d, 0) > 0) AS market_count_365d, + COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_total, 0) > 0) AS market_count_total FROM SalesAgg - WHERE COALESCE(sales_usd_90d, 0) > 0 GROUP BY product_code, color_code, yaka_kodu ), Market AS ( SELECT market_key, AVG(NULLIF(sales_qty_90d, 0)) AS market_avg_sales_qty_90d, + AVG(NULLIF(sales_qty_180d, 0)) AS market_avg_sales_qty_180d, + AVG(NULLIF(sales_qty_365d, 0)) AS market_avg_sales_qty_365d, + AVG(NULLIF(sales_qty_total, 0)) AS market_avg_sales_qty_total, AVG(NULLIF(avg_price_usd_90d, 0)) AS market_avg_price_usd_90d, AVG(NULLIF(gross_margin_90d, 0)) AS market_avg_margin_90d FROM Base @@ -620,6 +631,9 @@ Scored AS ( SELECT b.*, COALESCE(sp.market_count_90d, 0) AS market_count_90d, + COALESCE(sp.market_count_180d, 0) AS market_count_180d, + COALESCE(sp.market_count_365d, 0) AS market_count_365d, + COALESCE(sp.market_count_total, 0) AS market_count_total, CASE WHEN b.avg_daily_sales_90d = 0 THEN 0 ELSE b.avg_stock_90d / NULLIF(b.avg_daily_sales_90d, 0) END AS stock_days_90d, 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, @@ -629,6 +643,9 @@ Scored AS ( 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 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_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_total, 0) = 0 THEN 0 ELSE b.sales_qty_total / NULLIF(m.market_avg_sales_qty_total, 0) END AS sales_index_total, CASE WHEN COALESCE(m.market_avg_price_usd_90d, 0) = 0 THEN 0 ELSE b.avg_price_usd_90d / NULLIF(m.market_avg_price_usd_90d, 0) END AS price_index_90d, CASE WHEN COALESCE(m.market_avg_margin_90d, 0) = 0 THEN 0 ELSE b.gross_margin_90d / NULLIF(m.market_avg_margin_90d, 0) END AS margin_index_90d FROM Base b @@ -640,11 +657,14 @@ INSERT INTO mk_product_performance_kpi_daily ( kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty, sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total, sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total, avg_daily_sales_90d, avg_daily_sales_180d, avg_daily_sales_365d, avg_daily_sales_total, + avg_stock_90d, avg_stock_180d, avg_stock_365d, avg_stock_total, stock_days_90d, stock_days_180d, stock_days_365d, stock_days_total, stock_turnover_90d, stock_turnover_180d, stock_turnover_365d, stock_turnover_total, avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try, gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d, - unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, market_count_90d, customer_count_90d, - sales_index_90d, price_index_90d, margin_index_90d, performance_score, + unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, + market_count_90d, market_count_180d, market_count_365d, market_count_total, + customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total, + sales_index_90d, sales_index_180d, sales_index_365d, sales_index_total, price_index_90d, margin_index_90d, performance_score, performance_bucket, recommendation, last_sale_date, last_ref_number, updated_at ) SELECT @@ -653,11 +673,14 @@ SELECT kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty, COALESCE(sales_qty_30d, 0), COALESCE(sales_qty_90d, 0), COALESCE(sales_qty_180d, 0), COALESCE(sales_qty_365d, 0), COALESCE(sales_qty_730d, 0), COALESCE(sales_qty_total, 0), COALESCE(sales_usd_30d, 0), COALESCE(sales_usd_90d, 0), COALESCE(sales_usd_180d, 0), COALESCE(sales_usd_365d, 0), COALESCE(sales_usd_total, 0), COALESCE(avg_daily_sales_90d, 0), COALESCE(avg_daily_sales_180d, 0), COALESCE(avg_daily_sales_365d, 0), COALESCE(avg_daily_sales_total, 0), + COALESCE(avg_stock_90d, 0), COALESCE(avg_stock_180d, 0), COALESCE(avg_stock_365d, 0), COALESCE(avg_stock_total, 0), COALESCE(stock_days_90d, 0), COALESCE(stock_days_180d, 0), COALESCE(stock_days_365d, 0), COALESCE(stock_days_total, 0), COALESCE(stock_turnover_90d, 0), COALESCE(stock_turnover_180d, 0), COALESCE(stock_turnover_365d, 0), COALESCE(stock_turnover_total, 0), COALESCE(avg_price_usd_90d, 0), COALESCE(avg_price_usd_180d, 0), COALESCE(cost_price_usd, 0), COALESCE(base_price_usd, 0), COALESCE(base_price_try, 0), COALESCE(gross_profit_usd_90d, 0), COALESCE(gross_profit_usd_180d, 0), COALESCE(gross_margin_90d, 0), COALESCE(gross_margin_180d, 0), - COALESCE(unit_profit_cost_90d, 0), COALESCE(unit_profit_cost_180d, 0), COALESCE(unit_profit_base_90d, 0), COALESCE(unit_profit_base_180d, 0), COALESCE(market_count_90d, 0), COALESCE(customer_count_90d, 0), - COALESCE(sales_index_90d, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0), + COALESCE(unit_profit_cost_90d, 0), COALESCE(unit_profit_cost_180d, 0), COALESCE(unit_profit_base_90d, 0), COALESCE(unit_profit_base_180d, 0), + COALESCE(market_count_90d, 0), COALESCE(market_count_180d, 0), COALESCE(market_count_365d, 0), COALESCE(market_count_total, 0), + COALESCE(customer_count_90d, 0), COALESCE(customer_count_180d, 0), COALESCE(customer_count_365d, 0), COALESCE(customer_count_total, 0), + COALESCE(sales_index_90d, 0), COALESCE(sales_index_180d, 0), COALESCE(sales_index_365d, 0), COALESCE(sales_index_total, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0), CASE WHEN COALESCE(sales_usd_90d, 0) <= 0 THEN 1 ELSE ROUND( LEAST(35, COALESCE(sales_index_90d, 0) * 18) diff --git a/ui/src/pages/ProductPerformanceProfitability.vue b/ui/src/pages/ProductPerformanceProfitability.vue index 2adb396..e445c16 100644 --- a/ui/src/pages/ProductPerformanceProfitability.vue +++ b/ui/src/pages/ProductPerformanceProfitability.vue @@ -3479,8 +3479,8 @@ function aggregateGroupFields (sourceRows, groupField = '') { for (const field of fields) { if (field === 'row_key' || field === 'key') continue if (/^gross_margin(_base|_cost)?(_|$)/i.test(field)) continue - if (field === 'stock_qty') { - out[field] = distinctVariantStockQty(sourceRows) + if (field === 'stock_qty' || /^avg_stock_/i.test(field)) { + out[field] = distinctVariantNumber(sourceRows, field) continue } if (field === 'idle_cost_usd') { @@ -3528,6 +3528,19 @@ function weightFieldForMetric (field) { function applyDerivedGroupMetrics (out, sourceRows, groupField = '') { Object.assign(out, normalizeProductCostFields(out)) + const preservedPerformanceScores = {} + if (Object.prototype.hasOwnProperty.call(out, 'performance_score')) { + preservedPerformanceScores['90d'] = Number(out.performance_score || 0) + } + const preservedCustomerScores = {} + for (const suffix of ['90d', '180d', '365d', 'total']) { + if (Object.prototype.hasOwnProperty.call(out, `performance_score_${suffix}`)) { + preservedPerformanceScores[suffix] = Number(out[`performance_score_${suffix}`] || 0) + } + if (Object.prototype.hasOwnProperty.call(out, `customer_score_${suffix}`)) { + preservedCustomerScores[suffix] = Number(out[`customer_score_${suffix}`] || 0) + } + } for (const suffix of ['90d', '180d', '365d', 'total']) { const sales = Number(out[`sales_usd_${suffix}`] || 0) const qty = Number(out[`sales_qty_${suffix}`] || 0) @@ -3579,14 +3592,14 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') { if (sourceRows.some(row => row.performance_bucket)) { out.performance_bucket = dominantValue(sourceRows, 'performance_bucket') } - out.customer_score_90d = customerSalesPeriodScore(periodMetricSource(out, '90d')) - out.customer_score_180d = customerSalesPeriodScore(periodMetricSource(out, '180d')) - out.customer_score_365d = customerSalesPeriodScore(periodMetricSource(out, '365d')) - out.customer_score_total = customerSalesPeriodScore(periodMetricSource(out, 'total')) - out.performance_score_90d = productSalesPeriodScore(productPeriodMetricSource(out, '90d')) - out.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(out, '180d')) - out.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(out, '365d')) - out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total')) + for (const suffix of ['90d', '180d', '365d', 'total']) { + out[`customer_score_${suffix}`] = Object.prototype.hasOwnProperty.call(preservedCustomerScores, suffix) + ? preservedCustomerScores[suffix] + : customerSalesPeriodScore(periodMetricSource(out, suffix)) + out[`performance_score_${suffix}`] = Object.prototype.hasOwnProperty.call(preservedPerformanceScores, suffix) + ? preservedPerformanceScores[suffix] + : productSalesPeriodScore(productPeriodMetricSource(out, suffix)) + } out.performance_score = Number(out.performance_score_90d || 0) } @@ -3779,19 +3792,23 @@ function productVariantStockKey (row) { } function distinctVariantStockQty (sourceRows) { + return distinctVariantNumber(sourceRows, 'stock_qty') +} + +function distinctVariantNumber (sourceRows, field) { const seen = new Set() let total = 0 let keyed = false for (const row of sourceRows) { const key = productVariantStockKey(row) - const stockQty = Number(row?.stock_qty || 0) + const value = Number(row?.[field] || 0) if (!key) continue keyed = true if (seen.has(key)) continue seen.add(key) - total += stockQty + total += value } - return keyed ? total : sumRows(sourceRows, 'stock_qty') + return keyed ? total : sumRows(sourceRows, field) } function distinctVariantStockCost (sourceRows) {