|
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"math"
|
|
|
|
|
"os"
|
|
|
|
|
"sort"
|
|
|
|
|
"strconv"
|
|
|
|
@@ -372,10 +373,10 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_sales_daily (
|
|
|
|
|
UPDATE mk_product_performance_sales_daily
|
|
|
|
|
SET urun_ilk_grubu = '', updated_at = now()
|
|
|
|
|
WHERE btrim(urun_ilk_grubu) = '-'
|
|
|
|
|
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
|
|
|
|
OR upper(translate(btrim(urun_ilk_grubu), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
|
|
|
|
`
|
|
|
|
|
DELETE FROM mk_product_performance_sales_daily
|
|
|
|
|
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
|
|
|
|
|
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')`,
|
|
|
|
|
`
|
|
|
|
|
UPDATE mk_product_performance_sales_daily
|
|
|
|
|
SET askili_yan = '', updated_at = now()
|
|
|
|
@@ -444,15 +445,15 @@ 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(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
|
|
|
|
|
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')`,
|
|
|
|
|
`
|
|
|
|
|
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
|
|
|
|
kpi_date DATE NOT NULL,
|
|
|
|
@@ -480,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,
|
|
|
|
@@ -496,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,
|
|
|
|
@@ -519,27 +533,27 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
|
|
|
|
UPDATE mk_product_performance_kpi_daily
|
|
|
|
|
SET urun_ilk_grubu = '', updated_at = now()
|
|
|
|
|
WHERE btrim(urun_ilk_grubu) = '-'
|
|
|
|
|
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
|
|
|
|
OR upper(translate(btrim(urun_ilk_grubu), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
|
|
|
|
|
`
|
|
|
|
|
DELETE FROM mk_product_performance_kpi_daily
|
|
|
|
|
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
|
|
|
|
|
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')`,
|
|
|
|
|
`
|
|
|
|
|
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()
|
|
|
|
@@ -551,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`,
|
|
|
|
@@ -566,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,
|
|
|
|
@@ -602,6 +629,7 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot (
|
|
|
|
|
)`,
|
|
|
|
|
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_key ON mk_product_performance_grouped_snapshot (report_key, row_order)`,
|
|
|
|
|
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_level ON mk_product_performance_grouped_snapshot (report_key, group_level, row_order)`,
|
|
|
|
|
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_field ON mk_product_performance_grouped_snapshot (report_key, group_field, group_value)`,
|
|
|
|
|
`
|
|
|
|
|
CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot_meta (
|
|
|
|
|
report_key TEXT PRIMARY KEY,
|
|
|
|
@@ -883,6 +911,11 @@ func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (
|
|
|
|
|
total := 0
|
|
|
|
|
|
|
|
|
|
save := func(reportKey string, rows any, started time.Time) error {
|
|
|
|
|
rowCount := productPerformanceSnapshotPayloadLen(rows)
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot save start key=%s rows=%d", reportKey, rowCount)
|
|
|
|
|
defer func() {
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot save done key=%s rows=%d elapsed=%s", reportKey, rowCount, time.Since(started).Round(time.Second))
|
|
|
|
|
}()
|
|
|
|
|
switch v := rows.(type) {
|
|
|
|
|
case []models.ProductPerformanceSummary:
|
|
|
|
|
total += len(v)
|
|
|
|
@@ -923,24 +956,28 @@ func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
started := time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("summary"))
|
|
|
|
|
if summary, err := GetProductPerformanceSummary(ctx, pg); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
} else if err := save(productPerformanceSnapshotKey("summary"), []models.ProductPerformanceSummary{summary}, started); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("products"))
|
|
|
|
|
if rows, _, err := ListProductPerformance(ctx, pg, ProductPerformanceFilters{Limit: 50000, Page: 1, SortBy: "performance_score", Descending: true}); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
} else if err := save(productPerformanceSnapshotKey("products"), rows, started); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("general"))
|
|
|
|
|
if rows, err := ListProductPerformanceGeneral(ctx, pg, 50000); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
} else if err := save(productPerformanceSnapshotKey("general"), rows, started); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("orders"))
|
|
|
|
|
if rows, err := ListProductPerformanceOrderAnalysis(ctx, pg, 50000); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
} else if err := save(productPerformanceSnapshotKey("orders"), rows, started); err != nil {
|
|
|
|
@@ -948,6 +985,7 @@ func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (
|
|
|
|
|
}
|
|
|
|
|
for _, mode := range []string{"market", "customer"} {
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("order-groups", mode))
|
|
|
|
|
rows, err := ListProductPerformanceOrderGroups(ctx, pg, mode, 50000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return total, err
|
|
|
|
@@ -957,24 +995,28 @@ func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("order-product-customers"))
|
|
|
|
|
if rows, err := ListProductPerformanceOrderProductCustomers(ctx, pg, 50000); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
} else if err := save(productPerformanceSnapshotKey("order-product-customers"), rows, started); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("order-market-details"))
|
|
|
|
|
if rows, err := ListProductPerformanceOrderMarketDetails(ctx, pg, 50000); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
} else if err := save(productPerformanceSnapshotKey("order-market-details"), rows, started); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("markets"))
|
|
|
|
|
if rows, err := ListProductPerformanceMarkets(ctx, pg, 50000); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
} else if err := save(productPerformanceSnapshotKey("markets"), rows, started); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("countries"))
|
|
|
|
|
if rows, err := ListProductPerformanceCountries(ctx, pg, 50000); err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
} else if err := save(productPerformanceSnapshotKey("countries"), rows, started); err != nil {
|
|
|
|
@@ -982,6 +1024,7 @@ func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (
|
|
|
|
|
}
|
|
|
|
|
for _, mode := range []string{"market_customer", "country_customer", "market_country_customer"} {
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("customers", mode))
|
|
|
|
|
rows, err := ListProductPerformanceCustomers(ctx, pg, mode, 50000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return total, err
|
|
|
|
@@ -992,6 +1035,7 @@ func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (
|
|
|
|
|
}
|
|
|
|
|
for _, mode := range []string{"color_yaka_market_customer", "product_country_segment_market_customer", "market_customer_product", "country_segment_market_customer_product"} {
|
|
|
|
|
started = time.Now()
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("sales-breakdown", mode))
|
|
|
|
|
rows, err := ListProductPerformanceSalesBreakdown(ctx, pg, mode, 50000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return total, err
|
|
|
|
@@ -1000,14 +1044,45 @@ func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] grouped snapshots rebuild start")
|
|
|
|
|
groupedRows, err := RebuildProductPerformanceGroupedSnapshots(snapshotReadCtx, pg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] grouped snapshots rebuild done rows=%d", groupedRows)
|
|
|
|
|
total += groupedRows
|
|
|
|
|
return total, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceSnapshotPayloadLen(rows any) int {
|
|
|
|
|
switch v := rows.(type) {
|
|
|
|
|
case []models.ProductPerformanceSummary:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceGeneralRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceOrderAnalysisRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceOrderGroupRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceOrderProductCustomerRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceOrderMarketDetailRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceMarketRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceCountryRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceCustomerRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
case []models.ProductPerformanceSalesBreakdownRow:
|
|
|
|
|
return len(v)
|
|
|
|
|
default:
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type productPerformanceGroupedSnapshotDefinition struct {
|
|
|
|
|
Mode string
|
|
|
|
|
Levels []string
|
|
|
|
@@ -1022,25 +1097,32 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] grouped snapshot definitions ready count=%d", len(defs))
|
|
|
|
|
if err := deleteStaleProductPerformanceGroupedSnapshots(ctx, pg, productPerformanceSnapshotKey("grouped", "product_detail")+":%"); err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
total := 0
|
|
|
|
|
for _, def := range defs {
|
|
|
|
|
for i, def := range defs {
|
|
|
|
|
started := time.Now()
|
|
|
|
|
levels := sanitizeProductPerformanceGroupLevels(def.Levels)
|
|
|
|
|
if len(levels) == 0 {
|
|
|
|
|
levels = defaultProductPerformanceGroupLevels(def.Mode)
|
|
|
|
|
}
|
|
|
|
|
reportKey := productPerformanceGroupedSnapshotReportKey(def.Mode, levels, def.MainGroup)
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] grouped snapshot start %d/%d key=%s mode=%s main_group=%s levels=%s", i+1, len(defs), reportKey, def.Mode, def.MainGroup, productPerformanceGroupedLevelsKey(levels))
|
|
|
|
|
sourceRows, err := productPerformanceGroupedRawSnapshotSourceRows(ctx, pg, def.Mode, 50000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] grouped snapshot source loaded key=%s source_rows=%d elapsed=%s", reportKey, len(sourceRows), time.Since(started).Round(time.Second))
|
|
|
|
|
req := ProductPerformanceGroupedRequest{
|
|
|
|
|
Mode: def.Mode,
|
|
|
|
|
MainGroup: def.MainGroup,
|
|
|
|
|
}
|
|
|
|
|
sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
|
|
|
|
|
rows := make([]map[string]any, 0, len(sourceRows))
|
|
|
|
|
appendProductPerformanceGroupedRows(&rows, sourceRows, levels, 0, 0, []string{"tab:" + def.Mode}, nil, len(levels)-1)
|
|
|
|
|
reportKey := productPerformanceGroupedSnapshotReportKey(def.Mode, levels, def.MainGroup)
|
|
|
|
|
groupStarted := time.Now()
|
|
|
|
|
rows := buildProductPerformanceGroupedSnapshotRows(sourceRows, levels, def.Mode)
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] grouped snapshot tree built key=%s rows=%d elapsed=%s total_elapsed=%s", reportKey, len(rows), time.Since(groupStarted).Round(time.Second), time.Since(started).Round(time.Second))
|
|
|
|
|
if err := saveProductPerformanceGroupedSnapshotRows(ctx, pg, reportKey, productPerformanceGroupedSnapshotDefinition{
|
|
|
|
|
Mode: def.Mode,
|
|
|
|
|
Levels: levels,
|
|
|
|
@@ -1049,45 +1131,38 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
|
|
|
|
|
return total, err
|
|
|
|
|
}
|
|
|
|
|
total += len(rows)
|
|
|
|
|
log.Printf("[ProductPerformanceRefresh] grouped snapshot done key=%s rows=%d cumulative_rows=%d elapsed=%s", reportKey, len(rows), total, time.Since(started).Round(time.Second))
|
|
|
|
|
}
|
|
|
|
|
return total, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func deleteStaleProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB, reportKeyLike string) error {
|
|
|
|
|
reportKeyLike = strings.TrimSpace(reportKeyLike)
|
|
|
|
|
if reportKeyLike == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot WHERE report_key LIKE $1`, reportKeyLike); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot_meta WHERE report_key LIKE $1`, reportKeyLike); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.DB) ([]productPerformanceGroupedSnapshotDefinition, error) {
|
|
|
|
|
_ = ctx
|
|
|
|
|
_ = pg
|
|
|
|
|
defs := []productPerformanceGroupedSnapshotDefinition{
|
|
|
|
|
{Mode: "products", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}},
|
|
|
|
|
{Mode: "products", Levels: []string{"urun_ana_grubu"}},
|
|
|
|
|
{Mode: "idle", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
|
|
|
|
{Mode: "sales_color_yaka_market_customer", Levels: []string{"color_yaka", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "country", "market_key", "customer_segment", "customer_code", "customer_name"}},
|
|
|
|
|
{Mode: "sales_color_yaka_market_customer", Levels: []string{"urun_ana_grubu", "color_yaka", "market_key", "customer_code", "customer_name", "urun_alt_grubu", "product_code"}},
|
|
|
|
|
{Mode: "sales_product_country_segment_market_customer", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "country", "customer_segment", "market_key", "customer_code", "customer_name"}},
|
|
|
|
|
{Mode: "sales_country_segment_market_customer_product", Levels: []string{"country", "customer_segment", "market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
|
|
|
|
{Mode: "order_product_customers", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key", "customer_code", "customer_name"}},
|
|
|
|
|
{Mode: "order_market_details", Levels: []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
|
|
|
|
|
}
|
|
|
|
|
productRows, ok, err := loadProductPerformanceSnapshotMapRows(ctx, pg, productPerformanceSnapshotKey("products"), 50000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if ok {
|
|
|
|
|
mainGroups := make([]string, 0)
|
|
|
|
|
seen := map[string]bool{}
|
|
|
|
|
for _, row := range productRows {
|
|
|
|
|
mainGroup := productPerformanceGroupedFilterValue(row, "urun_ana_grubu")
|
|
|
|
|
if mainGroup == "" || seen[mainGroup] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[mainGroup] = true
|
|
|
|
|
mainGroups = append(mainGroups, mainGroup)
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(mainGroups)
|
|
|
|
|
for _, mainGroup := range mainGroups {
|
|
|
|
|
defs = append(defs, productPerformanceGroupedSnapshotDefinition{
|
|
|
|
|
Mode: "product_detail",
|
|
|
|
|
Levels: []string{"urun_alt_grubu", "product_code", "color_yaka", "market_key"},
|
|
|
|
|
MainGroup: mainGroup,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return defs, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1702,6 +1777,7 @@ func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanc
|
|
|
|
|
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceRow](ctx, pg, productPerformanceSnapshotKey("products"), limit); err != nil {
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
} else if ok {
|
|
|
|
|
applyProductPerformanceProductRowScores(rows)
|
|
|
|
|
return rows, len(rows), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1720,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')
|
|
|
|
@@ -1746,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 {
|
|
|
|
@@ -1767,9 +1851,116 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
|
|
|
|
for i := range out {
|
|
|
|
|
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
|
|
|
|
}
|
|
|
|
|
applyProductPerformanceProductRowScores(out)
|
|
|
|
|
return out, total, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyProductPerformanceProductRowScores(rows []models.ProductPerformanceRow) {
|
|
|
|
|
avg := productPerformanceProductRowAverages(rows)
|
|
|
|
|
for i := range rows {
|
|
|
|
|
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,
|
|
|
|
|
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),
|
|
|
|
|
productPerformanceProductRowTotalPeriodDays(rows[i]),
|
|
|
|
|
)
|
|
|
|
|
rows[i].PerformanceScore = rows[i].PerformanceScore90
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type productPerformanceProductRowAverage struct {
|
|
|
|
|
salesUSD90 float64
|
|
|
|
|
salesUSD180 float64
|
|
|
|
|
salesUSD365 float64
|
|
|
|
|
salesUSDTotal float64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceProductRowAverages(rows []models.ProductPerformanceRow) productPerformanceProductRowAverage {
|
|
|
|
|
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 productPerformanceProductRowTotalPeriodDays(row models.ProductPerformanceRow) float64 {
|
|
|
|
|
return productPerformancePeriodDays(map[string]any{"kpi_date": row.KpiDate}, "total")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceMarginFromSales(salesUSD, salesQty, unitCost float64) float64 {
|
|
|
|
|
if salesUSD <= 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
@@ -1789,7 +1980,7 @@ KPI AS (
|
|
|
|
|
SELECT *
|
|
|
|
|
FROM mk_product_performance_kpi_daily
|
|
|
|
|
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
|
|
|
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
),
|
|
|
|
|
VariantStock AS (
|
|
|
|
|
SELECT
|
|
|
|
@@ -1816,7 +2007,7 @@ Sales90 AS (
|
|
|
|
|
COUNT(DISTINCT NULLIF(customer_code, '-')) AS customer_count_90d
|
|
|
|
|
FROM mk_product_performance_sales_daily
|
|
|
|
|
WHERE sales_date BETWEEN (SELECT kpi_date FROM Latest) - INTERVAL '89 days' AND (SELECT kpi_date FROM Latest)
|
|
|
|
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
|
|
|
|
@@ -1852,6 +2043,7 @@ func ListProductPerformanceGeneral(ctx context.Context, pg *sql.DB, limit int) (
|
|
|
|
|
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceGeneralRow](ctx, pg, productPerformanceSnapshotKey("general"), limit); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
} else if ok {
|
|
|
|
|
applyProductPerformanceGeneralRowScores(rows)
|
|
|
|
|
return rows, nil
|
|
|
|
|
}
|
|
|
|
|
rows, err := pg.QueryContext(ctx, `
|
|
|
|
@@ -1887,11 +2079,11 @@ SalesAgg AS (
|
|
|
|
|
MAX(last_ref_number) AS last_ref_number,
|
|
|
|
|
COALESCE(SUM(sales_qty),0) AS sales_qty_total,
|
|
|
|
|
COALESCE(SUM(sales_usd),0) AS sales_usd_total,
|
|
|
|
|
COALESCE(COUNT(DISTINCT NULLIF(customer_code, '-')),0)::integer AS customer_count_total,
|
|
|
|
|
COALESCE(COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE COALESCE(sales_usd,0) > 0),0)::integer AS customer_count_total,
|
|
|
|
|
COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total
|
|
|
|
|
FROM mk_product_performance_sales_daily, Bounds
|
|
|
|
|
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
|
|
|
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
GROUP BY product_code, color_code, yaka_kodu, market_key
|
|
|
|
|
),
|
|
|
|
|
StockAgg AS (
|
|
|
|
@@ -1943,7 +2135,7 @@ Dim AS (
|
|
|
|
|
1 AS src_rank
|
|
|
|
|
FROM mk_product_performance_kpi_daily
|
|
|
|
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
|
|
|
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
UNION ALL
|
|
|
|
|
SELECT
|
|
|
|
|
product_code,
|
|
|
|
@@ -1959,7 +2151,7 @@ Dim AS (
|
|
|
|
|
MAX(urun_alt_grubu) AS urun_alt_grubu,
|
|
|
|
|
2 AS src_rank
|
|
|
|
|
FROM mk_product_performance_sales_daily
|
|
|
|
|
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
GROUP BY product_code, color_code, yaka_kodu
|
|
|
|
|
) x
|
|
|
|
|
ORDER BY product_code, color_code, yaka_kodu, src_rank
|
|
|
|
@@ -1991,7 +2183,7 @@ StockOnly AS (
|
|
|
|
|
AND d.color_code = s.color_code
|
|
|
|
|
AND d.yaka_kodu = s.yaka_kodu
|
|
|
|
|
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
|
|
|
|
|
AND upper(btrim(COALESCE(d.urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(d.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND NOT EXISTS (
|
|
|
|
|
SELECT 1
|
|
|
|
|
FROM SalesAgg a
|
|
|
|
@@ -2011,11 +2203,11 @@ Spread AS (
|
|
|
|
|
product_code,
|
|
|
|
|
color_code,
|
|
|
|
|
yaka_kodu,
|
|
|
|
|
COUNT(DISTINCT NULLIF(market_key, 'STOK'))::integer AS market_count_total,
|
|
|
|
|
COUNT(DISTINCT NULLIF(customer_code, '-'))::integer AS customer_count_total_all
|
|
|
|
|
COUNT(DISTINCT NULLIF(market_key, 'STOK')) FILTER (WHERE COALESCE(sales_usd,0) > 0)::integer AS market_count_total,
|
|
|
|
|
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE COALESCE(sales_usd,0) > 0)::integer AS customer_count_total_all
|
|
|
|
|
FROM mk_product_performance_sales_daily, Bounds
|
|
|
|
|
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
|
|
|
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
GROUP BY product_code, color_code, yaka_kodu
|
|
|
|
|
),
|
|
|
|
|
Scored AS (
|
|
|
|
@@ -2049,8 +2241,8 @@ Scored AS (
|
|
|
|
|
CASE WHEN b.sales_usd_total <= 0 THEN 0 ELSE (b.sales_usd_total - (b.sales_qty_total * COALESCE(pd.cost_price_usd,0))) / NULLIF(b.sales_usd_total,0) END AS gross_margin_total,
|
|
|
|
|
CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE (b.sales_usd_total / NULLIF(b.sales_qty_total,0)) - COALESCE(pd.cost_price_usd,0) END AS unit_profit_cost_total,
|
|
|
|
|
CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE (b.sales_usd_total / NULLIF(b.sales_qty_total,0)) - COALESCE(pd.base_price_usd,0) END AS unit_profit_base_total,
|
|
|
|
|
COALESCE(sp.market_count_total,0) AS market_count_total,
|
|
|
|
|
COALESCE(sp.customer_count_total_all, b.customer_count_total) AS customer_count_total,
|
|
|
|
|
CASE WHEN b.market_key <> 'STOK' AND b.sales_usd_total > 0 THEN 1 ELSE 0 END AS market_count_total,
|
|
|
|
|
b.customer_count_total AS customer_count_total,
|
|
|
|
|
b.invoice_count_total,
|
|
|
|
|
CASE
|
|
|
|
|
WHEN AVG(b.sales_qty_total) OVER (PARTITION BY b.market_key, b.kategori, b.urun_ana_grubu) <= 0 THEN 0
|
|
|
|
@@ -2099,7 +2291,7 @@ SELECT
|
|
|
|
|
WHEN sales_qty_total > 0 AND stock_qty <= 0 AND sales_index_total >= 1 THEN 'STOKSUZ_TALEP'
|
|
|
|
|
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'STOK_RISKI'
|
|
|
|
|
WHEN gross_margin_total < 0 THEN 'FIYAT_BASKISI'
|
|
|
|
|
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'YILDIZ_URUN'
|
|
|
|
|
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'YILDIZ_URUN'
|
|
|
|
|
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'FIYAT_FIRSATI'
|
|
|
|
|
ELSE 'TAKIP'
|
|
|
|
|
END AS performance_bucket,
|
|
|
|
@@ -2107,7 +2299,7 @@ SELECT
|
|
|
|
|
WHEN sales_qty_total > 0 AND stock_qty <= 0 THEN 'Talep var, stok yok. Uretim/satin alma onceligi ver.'
|
|
|
|
|
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'Satis yok, stok maliyeti tasiyor. Piyasa/fiyat aksiyonu gerekli.'
|
|
|
|
|
WHEN gross_margin_total < 0 THEN 'Ciplak maliyet altinda satis var. Fiyat veya maliyet kontrol edilmeli.'
|
|
|
|
|
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
|
|
|
|
|
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
|
|
|
|
|
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'Karli ama yavas. Dogru piyasada satis firsati var.'
|
|
|
|
|
ELSE 'Izleme ve piyasa bazli aksiyon.'
|
|
|
|
|
END AS recommendation,
|
|
|
|
@@ -2147,9 +2339,28 @@ LIMIT $1
|
|
|
|
|
for i := range out {
|
|
|
|
|
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
|
|
|
|
|
}
|
|
|
|
|
applyProductPerformanceGeneralRowScores(out)
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyProductPerformanceGeneralRowScores(rows []models.ProductPerformanceGeneralRow) {
|
|
|
|
|
for i := range rows {
|
|
|
|
|
turnover := 0.0
|
|
|
|
|
if rows[i].StockQty > 0 {
|
|
|
|
|
turnover = rows[i].SalesQtyTotal / rows[i].StockQty
|
|
|
|
|
}
|
|
|
|
|
rows[i].PerformanceScore = productPerformanceProductScore(
|
|
|
|
|
"total",
|
|
|
|
|
rows[i].SalesUSDTotal,
|
|
|
|
|
rows[i].SalesIndexTotal,
|
|
|
|
|
rows[i].GrossMarginTotal,
|
|
|
|
|
turnover,
|
|
|
|
|
float64(rows[i].MarketCountTotal),
|
|
|
|
|
float64(rows[i].CustomerCountTotal),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderAnalysisRow, error) {
|
|
|
|
|
if db.MssqlDB == nil {
|
|
|
|
|
return nil, fmt.Errorf("mssql db nil")
|
|
|
|
@@ -3009,7 +3220,7 @@ SELECT
|
|
|
|
|
COALESCE(AVG(NULLIF(stock_days_90d,0)),0) AS avg_stock_days_90d
|
|
|
|
|
FROM mk_product_performance_kpi_daily
|
|
|
|
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
|
|
|
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
GROUP BY market_key
|
|
|
|
|
ORDER BY risk_stock_cost_value_usd DESC, stock_cost_value_usd DESC, sales_usd_90d DESC
|
|
|
|
|
LIMIT $1
|
|
|
|
@@ -3064,13 +3275,13 @@ SELECT
|
|
|
|
|
ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
|
|
|
|
|
/ NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
|
|
|
|
|
END AS avg_price_usd_90d,
|
|
|
|
|
COALESCE(SUM(customer_count) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)::integer AS customer_count_90d,
|
|
|
|
|
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_90d,
|
|
|
|
|
COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)::integer AS invoice_count_90d,
|
|
|
|
|
COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_qty_365d,
|
|
|
|
|
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_usd_365d
|
|
|
|
|
FROM mk_product_performance_sales_daily
|
|
|
|
|
WHERE sales_date >= current_date - INTERVAL '364 days'
|
|
|
|
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
GROUP BY customer_country, customer_segment, market_key
|
|
|
|
|
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
|
|
|
|
LIMIT $1
|
|
|
|
@@ -3155,7 +3366,7 @@ SELECT
|
|
|
|
|
FROM mk_product_performance_sales_daily
|
|
|
|
|
WHERE sales_date >= current_date - INTERVAL '364 days'
|
|
|
|
|
AND COALESCE(customer_code,'') <> ''
|
|
|
|
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
GROUP BY %s
|
|
|
|
|
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
|
|
|
|
LIMIT $1
|
|
|
|
@@ -3198,7 +3409,7 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
|
|
|
|
|
"item_description": "MAX(s.item_description)",
|
|
|
|
|
"kategori": "COALESCE(MAX(s.kategori),'')",
|
|
|
|
|
"askili_yan": "COALESCE(MAX(CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END),'')",
|
|
|
|
|
"urun_ilk_grubu": "COALESCE(MAX(CASE WHEN btrim(COALESCE(s.urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE s.urun_ilk_grubu END),'')",
|
|
|
|
|
"urun_ilk_grubu": "COALESCE(MAX(" + productPerformanceCleanFirstGroupSQL("s.urun_ilk_grubu") + "),'')",
|
|
|
|
|
"urun_ana_grubu": "COALESCE(MAX(s.urun_ana_grubu),'')",
|
|
|
|
|
"urun_alt_grubu": "MAX(s.urun_alt_grubu)",
|
|
|
|
|
"market_key": "s.market_key",
|
|
|
|
@@ -3225,7 +3436,7 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
|
|
|
|
|
selected["market_key"] = true
|
|
|
|
|
selected["customer_code"] = true
|
|
|
|
|
selected["customer_name"] = true
|
|
|
|
|
groupCols = []string{"CASE WHEN btrim(COALESCE(s.urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE s.urun_ilk_grubu END", "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
|
|
|
|
|
groupCols = []string{productPerformanceCleanFirstGroupSQL("s.urun_ilk_grubu"), "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
|
|
|
|
|
case "product_country_segment_market_customer":
|
|
|
|
|
selected["product_code"] = true
|
|
|
|
|
selected["item_description"] = true
|
|
|
|
@@ -3288,12 +3499,13 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
|
|
|
|
|
selected["market_key"] = true
|
|
|
|
|
selected["customer_code"] = true
|
|
|
|
|
selected["customer_name"] = true
|
|
|
|
|
groupCols = []string{"CASE WHEN btrim(COALESCE(s.urun_ilk_grubu,'')) = '-' THEN '' WHEN upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE s.urun_ilk_grubu END", "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
|
|
|
|
|
groupCols = []string{productPerformanceCleanFirstGroupSQL("s.urun_ilk_grubu"), "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceSalesBreakdownRow](ctx, pg, productPerformanceSnapshotKey("sales-breakdown", mode), limit); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
} else if ok {
|
|
|
|
|
applyProductPerformanceSalesBreakdownScores(rows)
|
|
|
|
|
return rows, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -3314,14 +3526,14 @@ WITH LatestKPI AS (
|
|
|
|
|
CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan,
|
|
|
|
|
CASE
|
|
|
|
|
WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN ''
|
|
|
|
|
WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN ''
|
|
|
|
|
WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN ''
|
|
|
|
|
ELSE COALESCE(urun_ilk_grubu,'')
|
|
|
|
|
END AS urun_ilk_grubu,
|
|
|
|
|
COALESCE(urun_ana_grubu,'') AS urun_ana_grubu,
|
|
|
|
|
urun_alt_grubu
|
|
|
|
|
FROM mk_product_performance_kpi_daily
|
|
|
|
|
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
|
|
|
|
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
|
|
|
|
|
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
|
|
|
|
|
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
|
|
|
|
|
),
|
|
|
|
|
StockAgg AS (
|
|
|
|
@@ -3374,6 +3586,19 @@ StockTotalStart AS (
|
|
|
|
|
WHERE stock_date <= DATE '2022-01-01'
|
|
|
|
|
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
|
|
|
|
|
),
|
|
|
|
|
FirstSaleByProduct AS (
|
|
|
|
|
SELECT
|
|
|
|
|
product_code,
|
|
|
|
|
color_code,
|
|
|
|
|
yaka_kodu,
|
|
|
|
|
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days') AS first_sale_date_90d,
|
|
|
|
|
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days') AS first_sale_date_180d,
|
|
|
|
|
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days') AS first_sale_date_365d,
|
|
|
|
|
MIN(sales_date) FILTER (WHERE sales_date >= DATE '2022-01-01') AS first_sale_date_total
|
|
|
|
|
FROM mk_product_performance_sales_daily
|
|
|
|
|
WHERE sales_date >= DATE '2022-01-01'
|
|
|
|
|
GROUP BY product_code, color_code, yaka_kodu
|
|
|
|
|
),
|
|
|
|
|
Agg AS (
|
|
|
|
|
SELECT
|
|
|
|
|
$2::text AS breakdown,
|
|
|
|
@@ -3392,12 +3617,20 @@ Agg AS (
|
|
|
|
|
%s,
|
|
|
|
|
%s,
|
|
|
|
|
COUNT(DISTINCT product_code) AS product_count,
|
|
|
|
|
COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days')::integer AS product_group_count_90d,
|
|
|
|
|
COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days')::integer AS product_group_count_180d,
|
|
|
|
|
COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days')::integer AS product_group_count_365d,
|
|
|
|
|
COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= DATE '2022-01-01')::integer AS product_group_count_total,
|
|
|
|
|
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_90d,
|
|
|
|
|
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_180d,
|
|
|
|
|
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_365d,
|
|
|
|
|
COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_total,
|
|
|
|
|
COALESCE(MAX(stock_qty),0) AS stock_qty,
|
|
|
|
|
COALESCE(MAX(avg_stock_90d),0) AS avg_stock_90d,
|
|
|
|
|
COALESCE(MAX(avg_stock_180d),0) AS avg_stock_180d,
|
|
|
|
|
COALESCE(MAX(avg_stock_365d),0) AS avg_stock_365d,
|
|
|
|
|
COALESCE(MAX(avg_stock_total),0) AS avg_stock_total,
|
|
|
|
|
COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(customer_code,'') <> '')::integer AS customer_count_90d,
|
|
|
|
|
COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_90d,
|
|
|
|
|
COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)::integer AS invoice_count_90d,
|
|
|
|
|
COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_qty_90d,
|
|
|
|
|
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_usd_90d,
|
|
|
|
@@ -3413,7 +3646,7 @@ Agg AS (
|
|
|
|
|
ELSE COALESCE(SUM(sales_qty * cost_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
|
|
|
|
|
/ NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
|
|
|
|
|
END AS cost_price_usd_90d,
|
|
|
|
|
COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days' AND COALESCE(customer_code,'') <> '')::integer AS customer_count_180d,
|
|
|
|
|
COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_180d,
|
|
|
|
|
COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)::integer AS invoice_count_180d,
|
|
|
|
|
COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0) AS sales_qty_180d,
|
|
|
|
|
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0) AS sales_usd_180d,
|
|
|
|
@@ -3431,7 +3664,7 @@ Agg AS (
|
|
|
|
|
END AS cost_price_usd_180d,
|
|
|
|
|
COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0) AS sales_qty_365d,
|
|
|
|
|
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0) AS sales_usd_365d,
|
|
|
|
|
COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days' AND COALESCE(customer_code,'') <> '')::integer AS customer_count_365d,
|
|
|
|
|
COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_365d,
|
|
|
|
|
COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)::integer AS invoice_count_365d,
|
|
|
|
|
CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)=0 THEN 0
|
|
|
|
|
ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
|
|
|
|
@@ -3445,7 +3678,7 @@ Agg AS (
|
|
|
|
|
ELSE COALESCE(SUM(sales_qty * cost_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
|
|
|
|
|
/ NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
|
|
|
|
|
END AS cost_price_usd_365d,
|
|
|
|
|
COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(customer_code,'') <> '')::integer AS customer_count_total,
|
|
|
|
|
COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_total,
|
|
|
|
|
COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)::integer AS invoice_count_total,
|
|
|
|
|
COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01'),0) AS sales_qty_total,
|
|
|
|
|
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01'),0) AS sales_usd_total,
|
|
|
|
@@ -3468,21 +3701,58 @@ Agg AS (
|
|
|
|
|
COALESCE(pd.base_price_usd,0) AS base_price_usd,
|
|
|
|
|
COALESCE(pd.cost_price_usd,0) AS cost_price_usd,
|
|
|
|
|
COALESCE(st.stock_qty,0) AS stock_qty,
|
|
|
|
|
(COALESCE(s90.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_90d,
|
|
|
|
|
(COALESCE(s180.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_180d,
|
|
|
|
|
(COALESCE(s365.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_365d,
|
|
|
|
|
(COALESCE(stotal.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_total
|
|
|
|
|
(COALESCE(s90.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_90d,
|
|
|
|
|
(COALESCE(s180.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_180d,
|
|
|
|
|
(COALESCE(s365.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_365d,
|
|
|
|
|
(COALESCE(stotal.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_total
|
|
|
|
|
FROM mk_product_performance_sales_daily s
|
|
|
|
|
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code = s.product_code
|
|
|
|
|
LEFT JOIN LatestKPI k ON k.product_code = s.product_code AND k.color_code = s.color_code AND k.yaka_kodu = s.yaka_kodu
|
|
|
|
|
LEFT JOIN FirstSaleByProduct fs ON fs.product_code = s.product_code AND fs.color_code = s.color_code AND fs.yaka_kodu = s.yaka_kodu
|
|
|
|
|
LEFT JOIN StockAgg st ON st.product_code = s.product_code AND st.color_code = s.color_code AND st.yaka_kodu = s.yaka_kodu
|
|
|
|
|
LEFT JOIN Stock90Start s90 ON s90.product_code = s.product_code AND s90.color_code = s.color_code AND s90.yaka_kodu = s.yaka_kodu
|
|
|
|
|
LEFT JOIN Stock180Start s180 ON s180.product_code = s.product_code AND s180.color_code = s.color_code AND s180.yaka_kodu = s.yaka_kodu
|
|
|
|
|
LEFT JOIN Stock365Start s365 ON s365.product_code = s.product_code AND s365.color_code = s.color_code AND s365.yaka_kodu = s.yaka_kodu
|
|
|
|
|
LEFT JOIN StockTotalStart stotal ON stotal.product_code = s.product_code AND stotal.color_code = s.color_code AND stotal.yaka_kodu = s.yaka_kodu
|
|