2519 lines
97 KiB
Go
2519 lines
97 KiB
Go
package queries
|
||
|
||
import (
|
||
"bssapp-backend/db"
|
||
"bssapp-backend/models"
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"log"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/lib/pq"
|
||
)
|
||
|
||
type ProductPerformanceFilters struct {
|
||
Search string
|
||
ProductCode string
|
||
MarketKey string
|
||
Kategori string
|
||
Seri string
|
||
Bucket string
|
||
SortBy string
|
||
Descending bool
|
||
Limit int
|
||
Page int
|
||
}
|
||
|
||
type ProductPerformanceRefreshRequest struct {
|
||
Mode string
|
||
Stage string
|
||
ResumeAfter int
|
||
SkipDelete bool
|
||
ProductPrefix string
|
||
StartDate time.Time
|
||
EndDate time.Time
|
||
}
|
||
|
||
type ProductPerformanceRefreshResult struct {
|
||
Mode string `json:"mode"`
|
||
Stage string `json:"stage"`
|
||
StartDate string `json:"start_date"`
|
||
EndDate string `json:"end_date"`
|
||
SalesRows int `json:"sales_rows"`
|
||
StockRows int `json:"stock_rows"`
|
||
KpiRows int `json:"kpi_rows"`
|
||
DurationMS int64 `json:"duration_ms"`
|
||
}
|
||
|
||
func productPerformanceStockQuery() string {
|
||
return `
|
||
;WITH ActiveWarehouses AS (
|
||
SELECT WarehouseCode
|
||
FROM (VALUES
|
||
('1-0-12'),('1.01.2014'),('1.02.2005'),('1.02.2004'),('1-0-43'),
|
||
('4.02.2001'),('1-0-55'),('1.01.2003'),('1-0-21'),('1-0-2'),
|
||
('1.01.2004'),('1-0-49'),('1-0-37'),('1-0-29'),('1-0-28'),
|
||
('1-0-10'),('100'),('1.02.2006'),('1-0-42'),('1.01.2002'),
|
||
('1-0-52')
|
||
) W(WarehouseCode)
|
||
),
|
||
Raw AS (
|
||
SELECT
|
||
ProductCode = LTRIM(RTRIM(S.ItemCode)),
|
||
ColorCode = LTRIM(RTRIM(ISNULL(S.ColorCode, ''))),
|
||
YakaKodu = LTRIM(RTRIM(ISNULL(S.ItemDim2Code, ''))),
|
||
MovementDate = CAST(S.DocumentDate AS date),
|
||
ProcessCode = CASE
|
||
WHEN LTRIM(RTRIM(ISNULL(S.ProcessCode, ''))) <> '' THEN LTRIM(RTRIM(S.ProcessCode))
|
||
ELSE LTRIM(RTRIM(ISNULL(S.InnerProcessCode, '')))
|
||
END,
|
||
InQty = SUM(S.In_Qty1),
|
||
OutQty = SUM(S.Out_Qty1),
|
||
NetQty = SUM(S.InventoryQty1)
|
||
FROM StockWithCost S WITH(NOLOCK)
|
||
INNER JOIN ActiveWarehouses W
|
||
ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
|
||
WHERE S.ItemTypeCode = 1
|
||
AND (
|
||
LEN(LTRIM(RTRIM(S.ItemCode))) = 13
|
||
AND (
|
||
S.ItemCode LIKE 'S%'
|
||
OR S.ItemCode LIKE 'O%'
|
||
OR S.ItemCode LIKE 'N%'
|
||
OR S.ItemCode LIKE 'X%'
|
||
OR S.ItemCode LIKE 'I%'
|
||
OR S.ItemCode LIKE 'A%'
|
||
)
|
||
)
|
||
AND (@p3 = '' OR S.ItemCode LIKE @p3 + '%')
|
||
AND S.DocumentDate < DATEADD(DAY, 1, @p2)
|
||
GROUP BY
|
||
S.ItemCode,
|
||
S.ColorCode,
|
||
S.ItemDim2Code,
|
||
CAST(S.DocumentDate AS date),
|
||
CASE
|
||
WHEN LTRIM(RTRIM(ISNULL(S.ProcessCode, ''))) <> '' THEN LTRIM(RTRIM(S.ProcessCode))
|
||
ELSE LTRIM(RTRIM(ISNULL(S.InnerProcessCode, '')))
|
||
END
|
||
),
|
||
Opening AS (
|
||
SELECT
|
||
StockDate = @p1,
|
||
ProductCode,
|
||
ColorCode,
|
||
YakaKodu,
|
||
NetQty = SUM(NetQty),
|
||
InQty = CAST(0 AS decimal(18,4)),
|
||
OutQty = CAST(0 AS decimal(18,4)),
|
||
KpiInQty = CAST(0 AS decimal(18,4)),
|
||
KpiOutQty = CAST(0 AS decimal(18,4)),
|
||
SalesMovementQty = CAST(0 AS decimal(18,4)),
|
||
ProductionInQty = CAST(0 AS decimal(18,4)),
|
||
PurchaseInQty = CAST(0 AS decimal(18,4)),
|
||
ConsumptionOutQty = CAST(0 AS decimal(18,4)),
|
||
CountDiffQty = CAST(0 AS decimal(18,4))
|
||
FROM Raw
|
||
WHERE MovementDate < @p1
|
||
GROUP BY ProductCode, ColorCode, YakaKodu
|
||
),
|
||
Daily AS (
|
||
SELECT
|
||
StockDate = MovementDate,
|
||
ProductCode,
|
||
ColorCode,
|
||
YakaKodu,
|
||
NetQty = SUM(NetQty),
|
||
InQty = SUM(InQty),
|
||
OutQty = SUM(OutQty),
|
||
KpiInQty = SUM(CASE WHEN ProcessCode IN ('OP','BP','CI') THEN InQty ELSE 0 END),
|
||
KpiOutQty = SUM(CASE WHEN ProcessCode IN ('R','WS','OC','CO') THEN OutQty ELSE 0 END),
|
||
SalesMovementQty = SUM(CASE WHEN ProcessCode IN ('R','WS') THEN OutQty ELSE 0 END),
|
||
ProductionInQty = SUM(CASE WHEN ProcessCode = 'OP' THEN InQty ELSE 0 END),
|
||
PurchaseInQty = SUM(CASE WHEN ProcessCode = 'BP' THEN InQty ELSE 0 END),
|
||
ConsumptionOutQty = SUM(CASE WHEN ProcessCode = 'OC' THEN OutQty ELSE 0 END),
|
||
CountDiffQty = SUM(CASE WHEN ProcessCode IN ('CO','CI') THEN NetQty ELSE 0 END)
|
||
FROM Raw
|
||
WHERE MovementDate BETWEEN @p1 AND @p2
|
||
GROUP BY MovementDate, ProductCode, ColorCode, YakaKodu
|
||
),
|
||
Series AS (
|
||
SELECT * FROM Opening
|
||
UNION ALL
|
||
SELECT * FROM Daily
|
||
),
|
||
Collapsed AS (
|
||
SELECT
|
||
StockDate,
|
||
ProductCode,
|
||
ColorCode,
|
||
YakaKodu,
|
||
NetQty = SUM(NetQty),
|
||
InQty = SUM(InQty),
|
||
OutQty = SUM(OutQty),
|
||
KpiInQty = SUM(KpiInQty),
|
||
KpiOutQty = SUM(KpiOutQty),
|
||
SalesMovementQty = SUM(SalesMovementQty),
|
||
ProductionInQty = SUM(ProductionInQty),
|
||
PurchaseInQty = SUM(PurchaseInQty),
|
||
ConsumptionOutQty = SUM(ConsumptionOutQty),
|
||
CountDiffQty = SUM(CountDiffQty)
|
||
FROM Series
|
||
GROUP BY StockDate, ProductCode, ColorCode, YakaKodu
|
||
),
|
||
Running AS (
|
||
SELECT
|
||
StockDate,
|
||
ProductCode,
|
||
ColorCode,
|
||
YakaKodu,
|
||
StockQty = SUM(NetQty) OVER (
|
||
PARTITION BY ProductCode, ColorCode, YakaKodu
|
||
ORDER BY StockDate
|
||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||
),
|
||
InQty,
|
||
OutQty,
|
||
KpiInQty,
|
||
KpiOutQty,
|
||
SalesMovementQty,
|
||
ProductionInQty,
|
||
PurchaseInQty,
|
||
ConsumptionOutQty,
|
||
CountDiffQty
|
||
FROM Collapsed
|
||
)
|
||
SELECT
|
||
StockDate,
|
||
ProductCode,
|
||
ColorCode,
|
||
YakaKodu,
|
||
StockQty,
|
||
InQty,
|
||
OutQty,
|
||
KpiInQty,
|
||
KpiOutQty,
|
||
SalesMovementQty,
|
||
ProductionInQty,
|
||
PurchaseInQty,
|
||
ConsumptionOutQty,
|
||
CountDiffQty
|
||
FROM Running
|
||
WHERE StockDate BETWEEN @p1 AND @p2
|
||
ORDER BY StockDate, ProductCode, ColorCode, YakaKodu
|
||
`
|
||
}
|
||
|
||
func EnsureProductPerformanceTables(pg *sql.DB) error {
|
||
stmts := []string{
|
||
`
|
||
CREATE TABLE IF NOT EXISTS mk_product_performance_sales_daily (
|
||
sales_date DATE NOT NULL,
|
||
product_code TEXT NOT NULL,
|
||
color_code TEXT NOT NULL DEFAULT '',
|
||
yaka_kodu TEXT NOT NULL DEFAULT '',
|
||
item_description TEXT NOT NULL DEFAULT '',
|
||
kategori TEXT NOT NULL DEFAULT '',
|
||
seri TEXT NOT NULL DEFAULT '',
|
||
yas_grubu TEXT NOT NULL DEFAULT '',
|
||
askili_yan TEXT NOT NULL DEFAULT '',
|
||
urun_ilk_grubu TEXT NOT NULL DEFAULT '',
|
||
urun_ana_grubu TEXT NOT NULL DEFAULT '',
|
||
urun_alt_grubu TEXT NOT NULL DEFAULT '',
|
||
market_key TEXT NOT NULL DEFAULT '',
|
||
channel_code TEXT NOT NULL DEFAULT '',
|
||
customer_country TEXT NOT NULL DEFAULT '',
|
||
customer_segment TEXT NOT NULL DEFAULT '',
|
||
customer_code TEXT NOT NULL DEFAULT '',
|
||
customer_name TEXT NOT NULL DEFAULT '',
|
||
sales_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_tl NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_usd NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
avg_price_usd NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
invoice_line_count INTEGER NOT NULL DEFAULT 0,
|
||
invoice_count INTEGER NOT NULL DEFAULT 0,
|
||
customer_count INTEGER NOT NULL DEFAULT 0,
|
||
last_ref_number TEXT NOT NULL DEFAULT '',
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
CONSTRAINT pk_mk_product_performance_sales_daily PRIMARY KEY
|
||
(sales_date, product_code, color_code, yaka_kodu, market_key, customer_country, customer_segment, customer_code)
|
||
)`,
|
||
`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS customer_code TEXT NOT NULL DEFAULT ''`,
|
||
`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS customer_name TEXT NOT NULL DEFAULT ''`,
|
||
`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS urun_ilk_grubu TEXT NOT NULL DEFAULT ''`,
|
||
`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS urun_ana_grubu TEXT NOT NULL DEFAULT ''`,
|
||
`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS urun_alt_grubu TEXT NOT NULL DEFAULT ''`,
|
||
`
|
||
DO $$
|
||
BEGIN
|
||
IF EXISTS (
|
||
SELECT 1
|
||
FROM pg_constraint
|
||
WHERE conname = 'pk_mk_product_performance_sales_daily'
|
||
AND conrelid = 'mk_product_performance_sales_daily'::regclass
|
||
) THEN
|
||
ALTER TABLE mk_product_performance_sales_daily DROP CONSTRAINT pk_mk_product_performance_sales_daily;
|
||
END IF;
|
||
ALTER TABLE mk_product_performance_sales_daily
|
||
ADD CONSTRAINT pk_mk_product_performance_sales_daily PRIMARY KEY
|
||
(sales_date, product_code, color_code, yaka_kodu, market_key, customer_country, customer_segment, customer_code);
|
||
END $$`,
|
||
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_sales_product_date ON mk_product_performance_sales_daily (product_code, sales_date DESC)`,
|
||
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_sales_market ON mk_product_performance_sales_daily (market_key, sales_date DESC)`,
|
||
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_sales_customer ON mk_product_performance_sales_daily (customer_code, sales_date DESC)`,
|
||
`
|
||
CREATE TABLE IF NOT EXISTS mk_product_performance_stock_daily (
|
||
stock_date DATE NOT NULL,
|
||
product_code TEXT NOT NULL,
|
||
color_code TEXT NOT NULL DEFAULT '',
|
||
yaka_kodu TEXT NOT NULL DEFAULT '',
|
||
stock_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
in_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
out_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
kpi_in_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
kpi_out_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_movement_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
production_in_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
purchase_in_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
consumption_out_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
count_diff_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
CONSTRAINT pk_mk_product_performance_stock_daily PRIMARY KEY
|
||
(stock_date, product_code, color_code, yaka_kodu)
|
||
)`,
|
||
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_stock_product_date ON mk_product_performance_stock_daily (product_code, stock_date DESC)`,
|
||
`
|
||
CREATE TABLE IF NOT EXISTS mk_product_performance_price_dim (
|
||
product_code TEXT PRIMARY KEY,
|
||
cost_price_usd NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
base_price_usd NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
base_price_try NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
last_pricing_date DATE,
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
)`,
|
||
`
|
||
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
||
kpi_date DATE NOT NULL,
|
||
product_code TEXT NOT NULL,
|
||
color_code TEXT NOT NULL DEFAULT '',
|
||
yaka_kodu TEXT NOT NULL DEFAULT '',
|
||
item_description TEXT NOT NULL DEFAULT '',
|
||
kategori TEXT NOT NULL DEFAULT '',
|
||
seri TEXT NOT NULL DEFAULT '',
|
||
yas_grubu TEXT NOT NULL DEFAULT '',
|
||
askili_yan TEXT NOT NULL DEFAULT '',
|
||
urun_ilk_grubu TEXT NOT NULL DEFAULT '',
|
||
urun_ana_grubu TEXT NOT NULL DEFAULT '',
|
||
urun_alt_grubu TEXT NOT NULL DEFAULT '',
|
||
market_key TEXT NOT NULL DEFAULT '',
|
||
stock_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_qty_30d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_qty_90d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_qty_180d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_qty_365d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_qty_730d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_usd_30d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_usd_90d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
sales_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
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,
|
||
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,
|
||
avg_price_usd_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
cost_price_usd NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
base_price_usd NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
base_price_try NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
gross_profit_usd_90d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
gross_profit_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||
gross_margin_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
gross_margin_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
unit_profit_cost_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
unit_profit_cost_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||
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,
|
||
customer_count_90d INTEGER NOT NULL DEFAULT 0,
|
||
sales_index_90d 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,
|
||
performance_bucket TEXT NOT NULL DEFAULT '',
|
||
recommendation TEXT NOT NULL DEFAULT '',
|
||
last_sale_date DATE,
|
||
last_ref_number TEXT NOT NULL DEFAULT '',
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
CONSTRAINT pk_mk_product_performance_kpi_daily PRIMARY KEY
|
||
(kpi_date, product_code, color_code, yaka_kodu, market_key)
|
||
)`,
|
||
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_kpi_bucket ON mk_product_performance_kpi_daily (kpi_date DESC, performance_bucket)`,
|
||
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_kpi_market ON mk_product_performance_kpi_daily (kpi_date DESC, market_key, sales_index_90d DESC)`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS urun_ilk_grubu TEXT NOT NULL DEFAULT ''`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS urun_ana_grubu TEXT NOT NULL DEFAULT ''`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS urun_alt_grubu TEXT NOT NULL DEFAULT ''`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_qty_180d NUMERIC(18,4) NOT NULL DEFAULT 0`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0`,
|
||
`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 stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_price_usd_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS gross_profit_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS gross_margin_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_cost_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_cost_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||
`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`,
|
||
}
|
||
for _, stmt := range stmts {
|
||
if _, err := pg.Exec(stmt); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerformanceRefreshRequest) (ProductPerformanceRefreshResult, error) {
|
||
started := time.Now()
|
||
stage := productPerformanceRefreshStage(req.Stage)
|
||
productPrefix := productPerformanceProductPrefix(req.ProductPrefix)
|
||
log.Printf("[ProductPerformanceRefresh] start mode=%s stage=%s prefix=%s start=%s end=%s", req.Mode, stage, productPrefix, req.StartDate.Format("2006-01-02"), req.EndDate.Format("2006-01-02"))
|
||
if pg == nil {
|
||
return ProductPerformanceRefreshResult{}, fmt.Errorf("postgres db nil")
|
||
}
|
||
if db.MssqlDB == nil {
|
||
return ProductPerformanceRefreshResult{}, fmt.Errorf("mssql db nil")
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] ensure tables start")
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return ProductPerformanceRefreshResult{}, err
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] ensure tables done")
|
||
mode := strings.ToLower(strings.TrimSpace(req.Mode))
|
||
if mode == "" {
|
||
mode = "delta"
|
||
}
|
||
if req.EndDate.IsZero() {
|
||
req.EndDate = time.Now()
|
||
}
|
||
if req.StartDate.IsZero() {
|
||
if mode == "full" {
|
||
req.StartDate = time.Date(2022, 1, 1, 0, 0, 0, 0, req.EndDate.Location())
|
||
} else {
|
||
req.StartDate = req.EndDate.AddDate(0, 0, -45)
|
||
}
|
||
}
|
||
req.StartDate = dateOnly(req.StartDate)
|
||
req.EndDate = dateOnly(req.EndDate)
|
||
shouldRun := func(name string) bool {
|
||
if stage == "all" {
|
||
return true
|
||
}
|
||
order := map[string]int{
|
||
"sales": 1,
|
||
"stock": 2,
|
||
"price": 3,
|
||
"kpi": 4,
|
||
}
|
||
return order[name] >= order[stage]
|
||
}
|
||
|
||
runStage := func(stage string, fn func(*sql.Tx) error) error {
|
||
log.Printf("[ProductPerformanceRefresh] %s tx begin", stage)
|
||
tx, err := pg.BeginTx(ctx, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer tx.Rollback()
|
||
if err := fn(tx); err != nil {
|
||
return err
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] %s tx commit start", stage)
|
||
if err := tx.Commit(); err != nil {
|
||
return err
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] %s tx commit done elapsed=%s", stage, time.Since(started).Round(time.Second))
|
||
return nil
|
||
}
|
||
|
||
var salesRows int
|
||
if shouldRun("sales") {
|
||
if err := runStage("sales", func(tx *sql.Tx) error {
|
||
log.Printf("[ProductPerformanceRefresh] delete sales cache start")
|
||
deleteSQL := `DELETE FROM mk_product_performance_sales_daily WHERE sales_date BETWEEN $1 AND $2`
|
||
deleteArgs := []any{req.StartDate, req.EndDate}
|
||
if productPrefix != "" {
|
||
deleteSQL += ` AND product_code ILIKE $3 || '%'`
|
||
deleteArgs = append(deleteArgs, productPrefix)
|
||
}
|
||
if _, err := tx.ExecContext(ctx, deleteSQL, deleteArgs...); err != nil {
|
||
return err
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] delete sales cache done")
|
||
log.Printf("[ProductPerformanceRefresh] sales refresh start")
|
||
rows, err := refreshProductPerformanceSales(ctx, tx, req.StartDate, req.EndDate, productPrefix)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
salesRows = rows
|
||
log.Printf("[ProductPerformanceRefresh] sales refresh done rows=%d elapsed=%s", salesRows, time.Since(started).Round(time.Second))
|
||
return nil
|
||
}); err != nil {
|
||
return ProductPerformanceRefreshResult{}, err
|
||
}
|
||
} else {
|
||
log.Printf("[ProductPerformanceRefresh] sales skipped stage=%s", stage)
|
||
}
|
||
|
||
var stockRows int
|
||
if shouldRun("stock") {
|
||
rows, err := refreshProductPerformanceStockChunked(ctx, pg, req.StartDate, req.EndDate, started, req.SkipDelete || req.ResumeAfter > 0, req.ResumeAfter, productPrefix)
|
||
if err != nil {
|
||
return ProductPerformanceRefreshResult{}, err
|
||
}
|
||
stockRows = rows
|
||
} else {
|
||
log.Printf("[ProductPerformanceRefresh] stock skipped stage=%s", stage)
|
||
}
|
||
|
||
if shouldRun("price") {
|
||
if err := runStage("price", func(tx *sql.Tx) error {
|
||
log.Printf("[ProductPerformanceRefresh] price refresh start")
|
||
if err := refreshProductPerformancePrices(ctx, tx, productPrefix); err != nil {
|
||
return err
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] price refresh done elapsed=%s", time.Since(started).Round(time.Second))
|
||
return nil
|
||
}); err != nil {
|
||
return ProductPerformanceRefreshResult{}, err
|
||
}
|
||
} else {
|
||
log.Printf("[ProductPerformanceRefresh] price skipped stage=%s", stage)
|
||
}
|
||
|
||
var kpiRows int
|
||
if shouldRun("kpi") {
|
||
if err := runStage("kpi", func(tx *sql.Tx) error {
|
||
log.Printf("[ProductPerformanceRefresh] kpi rebuild start")
|
||
rows, err := RebuildProductPerformanceKPI(ctx, tx, req.EndDate)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
kpiRows = rows
|
||
log.Printf("[ProductPerformanceRefresh] kpi rebuild done rows=%d elapsed=%s", kpiRows, time.Since(started).Round(time.Second))
|
||
return nil
|
||
}); err != nil {
|
||
return ProductPerformanceRefreshResult{}, err
|
||
}
|
||
} else {
|
||
log.Printf("[ProductPerformanceRefresh] kpi skipped stage=%s", stage)
|
||
}
|
||
|
||
log.Printf("[ProductPerformanceRefresh] refresh done total_elapsed=%s", time.Since(started).Round(time.Second))
|
||
return ProductPerformanceRefreshResult{
|
||
Mode: mode,
|
||
Stage: stage,
|
||
StartDate: req.StartDate.Format("2006-01-02"),
|
||
EndDate: req.EndDate.Format("2006-01-02"),
|
||
SalesRows: salesRows,
|
||
StockRows: stockRows,
|
||
KpiRows: kpiRows,
|
||
DurationMS: time.Since(started).Milliseconds(),
|
||
}, nil
|
||
}
|
||
|
||
func productPerformanceRefreshStage(raw string) string {
|
||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||
case "", "all", "full":
|
||
return "all"
|
||
case "sales", "stock", "price", "kpi":
|
||
return strings.ToLower(strings.TrimSpace(raw))
|
||
default:
|
||
return "all"
|
||
}
|
||
}
|
||
|
||
func productPerformanceProductPrefix(raw string) string {
|
||
prefix := strings.ToUpper(strings.TrimSpace(raw))
|
||
switch prefix {
|
||
case "", "S", "O", "N", "X", "I", "A":
|
||
return prefix
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func refreshProductPerformanceSales(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time, productPrefix string) (int, error) {
|
||
log.Printf("[ProductPerformanceRefresh] sales mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
|
||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceSalesSQL(), startDate, endDate, productPrefix)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer rows.Close()
|
||
log.Printf("[ProductPerformanceRefresh] sales mssql query returned, postgres insert start")
|
||
count := 0
|
||
for rows.Next() {
|
||
var r productPerformanceSalesDaily
|
||
if err := rows.Scan(
|
||
&r.SalesDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
|
||
&r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey, &r.ChannelCode,
|
||
&r.CustomerCountry, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName, &r.SalesQty, &r.SalesTL, &r.SalesUSD,
|
||
&r.AvgPriceUSD, &r.InvoiceLineCount, &r.InvoiceCount, &r.CustomerCount, &r.LastRefNumber,
|
||
); err != nil {
|
||
return count, err
|
||
}
|
||
if _, err := tx.ExecContext(ctx, `
|
||
INSERT INTO mk_product_performance_sales_daily (
|
||
sales_date, product_code, color_code, yaka_kodu, item_description,
|
||
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, channel_code,
|
||
customer_country, customer_segment, customer_code, customer_name, sales_qty, sales_tl, sales_usd,
|
||
avg_price_usd, invoice_line_count, invoice_count, customer_count, last_ref_number, updated_at
|
||
) VALUES (
|
||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,now()
|
||
)
|
||
ON CONFLICT (sales_date, product_code, color_code, yaka_kodu, market_key, customer_country, customer_segment, customer_code)
|
||
DO UPDATE SET
|
||
item_description=EXCLUDED.item_description,
|
||
kategori=EXCLUDED.kategori,
|
||
seri=EXCLUDED.seri,
|
||
yas_grubu=EXCLUDED.yas_grubu,
|
||
askili_yan=EXCLUDED.askili_yan,
|
||
urun_ilk_grubu=EXCLUDED.urun_ilk_grubu,
|
||
urun_ana_grubu=EXCLUDED.urun_ana_grubu,
|
||
urun_alt_grubu=EXCLUDED.urun_alt_grubu,
|
||
channel_code=EXCLUDED.channel_code,
|
||
customer_name=EXCLUDED.customer_name,
|
||
sales_qty=EXCLUDED.sales_qty,
|
||
sales_tl=EXCLUDED.sales_tl,
|
||
sales_usd=EXCLUDED.sales_usd,
|
||
avg_price_usd=EXCLUDED.avg_price_usd,
|
||
invoice_line_count=EXCLUDED.invoice_line_count,
|
||
invoice_count=EXCLUDED.invoice_count,
|
||
customer_count=EXCLUDED.customer_count,
|
||
last_ref_number=EXCLUDED.last_ref_number,
|
||
updated_at=now()
|
||
`, r.SalesDate, r.ProductCode, r.ColorCode, r.YakaKodu, r.ItemDescription,
|
||
r.Kategori, r.Seri, r.YasGrubu, r.AskiliYan, r.UrunIlkGrubu, r.UrunAnaGrubu, r.UrunAltGrubu, r.MarketKey, r.ChannelCode,
|
||
r.CustomerCountry, r.CustomerSegment, r.CustomerCode, r.CustomerName, r.SalesQty, r.SalesTL, r.SalesUSD,
|
||
r.AvgPriceUSD, r.InvoiceLineCount, r.InvoiceCount, r.CustomerCount, r.LastRefNumber); err != nil {
|
||
return count, err
|
||
}
|
||
count++
|
||
if count%5000 == 0 {
|
||
log.Printf("[ProductPerformanceRefresh] sales inserted rows=%d", count)
|
||
}
|
||
}
|
||
return count, rows.Err()
|
||
}
|
||
|
||
func refreshProductPerformanceStock(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time, productPrefix string) (int, error) {
|
||
log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
|
||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockQuery(), startDate, endDate, productPrefix)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer rows.Close()
|
||
log.Printf("[ProductPerformanceRefresh] stock mssql query returned, postgres insert start")
|
||
count := 0
|
||
for rows.Next() {
|
||
var r productPerformanceStockDaily
|
||
if err := rows.Scan(
|
||
&r.StockDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.StockQty,
|
||
&r.InQty, &r.OutQty, &r.KpiInQty, &r.KpiOutQty, &r.SalesMovementQty,
|
||
&r.ProductionInQty, &r.PurchaseInQty, &r.ConsumptionOutQty, &r.CountDiffQty,
|
||
); err != nil {
|
||
return count, err
|
||
}
|
||
if err := insertProductPerformanceStock(ctx, tx, r); err != nil {
|
||
return count, err
|
||
}
|
||
count++
|
||
if count%5000 == 0 {
|
||
log.Printf("[ProductPerformanceRefresh] stock inserted rows=%d", count)
|
||
}
|
||
}
|
||
return count, rows.Err()
|
||
}
|
||
|
||
func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, startDate, endDate, started time.Time, skipDelete bool, resumeAfter int, productPrefix string) (int, error) {
|
||
if skipDelete {
|
||
log.Printf("[ProductPerformanceRefresh] stock delete skipped resume_after=%d", resumeAfter)
|
||
} else {
|
||
log.Printf("[ProductPerformanceRefresh] stock delete tx begin")
|
||
deleteTx, err := pg.BeginTx(ctx, nil)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer deleteTx.Rollback()
|
||
log.Printf("[ProductPerformanceRefresh] delete stock cache start")
|
||
deleteSQL := `DELETE FROM mk_product_performance_stock_daily WHERE stock_date BETWEEN $1 AND $2`
|
||
deleteArgs := []any{startDate, endDate}
|
||
if productPrefix != "" {
|
||
deleteSQL += ` AND product_code ILIKE $3 || '%'`
|
||
deleteArgs = append(deleteArgs, productPrefix)
|
||
}
|
||
if _, err := deleteTx.ExecContext(ctx, deleteSQL, deleteArgs...); err != nil {
|
||
return 0, err
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] delete stock cache done")
|
||
log.Printf("[ProductPerformanceRefresh] stock delete tx commit start")
|
||
if err := deleteTx.Commit(); err != nil {
|
||
return 0, err
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] stock delete tx commit done elapsed=%s", time.Since(started).Round(time.Second))
|
||
}
|
||
|
||
log.Printf("[ProductPerformanceRefresh] stock refresh start")
|
||
log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
|
||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockQuery(), startDate, endDate, productPrefix)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer rows.Close()
|
||
log.Printf("[ProductPerformanceRefresh] stock mssql query returned, postgres chunk upsert start")
|
||
|
||
const chunkSize = 5000
|
||
count := 0
|
||
seen := 0
|
||
chunkRows := 0
|
||
tx, err := pg.BeginTx(ctx, nil)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer tx.Rollback()
|
||
|
||
for rows.Next() {
|
||
var r productPerformanceStockDaily
|
||
if err := rows.Scan(
|
||
&r.StockDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.StockQty,
|
||
&r.InQty, &r.OutQty, &r.KpiInQty, &r.KpiOutQty, &r.SalesMovementQty,
|
||
&r.ProductionInQty, &r.PurchaseInQty, &r.ConsumptionOutQty, &r.CountDiffQty,
|
||
); err != nil {
|
||
return count, err
|
||
}
|
||
seen++
|
||
if resumeAfter > 0 && seen <= resumeAfter {
|
||
if seen%50000 == 0 || seen == resumeAfter {
|
||
log.Printf("[ProductPerformanceRefresh] stock resume skipped rows=%d", seen)
|
||
}
|
||
continue
|
||
}
|
||
if err := insertProductPerformanceStock(ctx, tx, r); err != nil {
|
||
return count, err
|
||
}
|
||
count++
|
||
chunkRows++
|
||
if chunkRows >= chunkSize {
|
||
log.Printf("[ProductPerformanceRefresh] stock chunk commit start upserted_rows=%d scanned_rows=%d", count, seen)
|
||
if err := tx.Commit(); err != nil {
|
||
return count, err
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] stock chunk commit done upserted_rows=%d scanned_rows=%d elapsed=%s", count, seen, time.Since(started).Round(time.Second))
|
||
tx, err = pg.BeginTx(ctx, nil)
|
||
if err != nil {
|
||
return count, err
|
||
}
|
||
chunkRows = 0
|
||
}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return count, err
|
||
}
|
||
if chunkRows > 0 {
|
||
log.Printf("[ProductPerformanceRefresh] stock final chunk commit start upserted_rows=%d scanned_rows=%d", count, seen)
|
||
if err := tx.Commit(); err != nil {
|
||
return count, err
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] stock final chunk commit done upserted_rows=%d scanned_rows=%d elapsed=%s", count, seen, time.Since(started).Round(time.Second))
|
||
} else {
|
||
_ = tx.Rollback()
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] stock refresh done upserted_rows=%d scanned_rows=%d elapsed=%s", count, seen, time.Since(started).Round(time.Second))
|
||
return count, nil
|
||
}
|
||
|
||
type productPerformanceStockExec interface {
|
||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||
}
|
||
|
||
func prepareProductPerformanceStockCopy(ctx context.Context, tx *sql.Tx) (*sql.Stmt, error) {
|
||
return tx.PrepareContext(ctx, pq.CopyIn(
|
||
"mk_product_performance_stock_daily",
|
||
"stock_date",
|
||
"product_code",
|
||
"color_code",
|
||
"yaka_kodu",
|
||
"stock_qty",
|
||
"in_qty",
|
||
"out_qty",
|
||
"kpi_in_qty",
|
||
"kpi_out_qty",
|
||
"sales_movement_qty",
|
||
"production_in_qty",
|
||
"purchase_in_qty",
|
||
"consumption_out_qty",
|
||
"count_diff_qty",
|
||
))
|
||
}
|
||
|
||
func insertProductPerformanceStock(ctx context.Context, exec productPerformanceStockExec, r productPerformanceStockDaily) error {
|
||
_, err := exec.ExecContext(ctx, `
|
||
INSERT INTO mk_product_performance_stock_daily (
|
||
stock_date, product_code, color_code, yaka_kodu, stock_qty, in_qty, out_qty,
|
||
kpi_in_qty, kpi_out_qty, sales_movement_qty, production_in_qty, purchase_in_qty,
|
||
consumption_out_qty, count_diff_qty, updated_at
|
||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,now())
|
||
ON CONFLICT (stock_date, product_code, color_code, yaka_kodu)
|
||
DO UPDATE SET
|
||
stock_qty=EXCLUDED.stock_qty,
|
||
in_qty=EXCLUDED.in_qty,
|
||
out_qty=EXCLUDED.out_qty,
|
||
kpi_in_qty=EXCLUDED.kpi_in_qty,
|
||
kpi_out_qty=EXCLUDED.kpi_out_qty,
|
||
sales_movement_qty=EXCLUDED.sales_movement_qty,
|
||
production_in_qty=EXCLUDED.production_in_qty,
|
||
purchase_in_qty=EXCLUDED.purchase_in_qty,
|
||
consumption_out_qty=EXCLUDED.consumption_out_qty,
|
||
count_diff_qty=EXCLUDED.count_diff_qty,
|
||
updated_at=now()
|
||
`, r.StockDate, r.ProductCode, r.ColorCode, r.YakaKodu, r.StockQty,
|
||
r.InQty, r.OutQty, r.KpiInQty, r.KpiOutQty, r.SalesMovementQty,
|
||
r.ProductionInQty, r.PurchaseInQty, r.ConsumptionOutQty, r.CountDiffQty)
|
||
return err
|
||
}
|
||
|
||
func refreshProductPerformancePrices(ctx context.Context, tx *sql.Tx, productPrefix string) error {
|
||
log.Printf("[ProductPerformanceRefresh] price mssql query start prefix=%s", productPrefix)
|
||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformancePriceSQL(), productPrefix)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer rows.Close()
|
||
log.Printf("[ProductPerformanceRefresh] price mssql query returned, postgres upsert start")
|
||
count := 0
|
||
for rows.Next() {
|
||
var code string
|
||
var cost, usd, tryPrice float64
|
||
var lastPricing sql.NullTime
|
||
if err := rows.Scan(&code, &cost, &usd, &tryPrice, &lastPricing); err != nil {
|
||
return err
|
||
}
|
||
var lp any
|
||
if lastPricing.Valid {
|
||
lp = lastPricing.Time
|
||
}
|
||
if _, err := tx.ExecContext(ctx, `
|
||
INSERT INTO mk_product_performance_price_dim (product_code, cost_price_usd, base_price_usd, base_price_try, last_pricing_date, updated_at)
|
||
VALUES ($1,$2,$3,$4,$5,now())
|
||
ON CONFLICT (product_code) DO UPDATE SET
|
||
cost_price_usd=EXCLUDED.cost_price_usd,
|
||
base_price_usd=EXCLUDED.base_price_usd,
|
||
base_price_try=EXCLUDED.base_price_try,
|
||
last_pricing_date=EXCLUDED.last_pricing_date,
|
||
updated_at=now()
|
||
`, strings.TrimSpace(code), cost, usd, tryPrice, lp); err != nil {
|
||
return err
|
||
}
|
||
count++
|
||
if count%5000 == 0 {
|
||
log.Printf("[ProductPerformanceRefresh] price upserted rows=%d", count)
|
||
}
|
||
}
|
||
log.Printf("[ProductPerformanceRefresh] price upsert done rows=%d", count)
|
||
return rows.Err()
|
||
}
|
||
|
||
func RebuildProductPerformanceKPI(ctx context.Context, exec interface {
|
||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||
}, kpiDate time.Time) (int, error) {
|
||
kpiDate = dateOnly(kpiDate)
|
||
if _, err := exec.ExecContext(ctx, `DELETE FROM mk_product_performance_kpi_daily WHERE kpi_date=$1`, kpiDate); err != nil {
|
||
return 0, err
|
||
}
|
||
res, err := exec.ExecContext(ctx, productPerformanceKPISQL(), kpiDate)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
n, _ := res.RowsAffected()
|
||
return int(n), nil
|
||
}
|
||
|
||
func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanceFilters) ([]models.ProductPerformanceRow, int, error) {
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return nil, 0, err
|
||
}
|
||
limit := f.Limit
|
||
if limit <= 0 {
|
||
limit = 100
|
||
} else if limit > 50000 {
|
||
limit = 50000
|
||
}
|
||
page := f.Page
|
||
if page <= 0 {
|
||
page = 1
|
||
}
|
||
where, args := productPerformanceWhere(f)
|
||
countQuery := `SELECT COUNT(*) FROM mk_product_performance_kpi_daily WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)` + where
|
||
var total int
|
||
if err := pg.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
|
||
return nil, 0, err
|
||
}
|
||
args = append(args, limit, (page-1)*limit)
|
||
orderBy := productPerformanceOrderBy(f.SortBy, f.Descending)
|
||
rows, err := pg.QueryContext(ctx, `
|
||
SELECT
|
||
to_char(kpi_date,'YYYY-MM-DD'), product_code, color_code, yaka_kodu, item_description,
|
||
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_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, avg_daily_sales_90d, avg_daily_sales_180d,
|
||
stock_days_90d, stock_days_180d, 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, 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')
|
||
FROM mk_product_performance_kpi_daily
|
||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)`+where+`
|
||
ORDER BY `+orderBy+`
|
||
LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
defer rows.Close()
|
||
out := make([]models.ProductPerformanceRow, 0, limit)
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceRow
|
||
if err := rows.Scan(
|
||
&r.KpiDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
|
||
&r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey, &r.StockQty,
|
||
&r.SalesQty30, &r.SalesQty90, &r.SalesQty180, &r.SalesQty365, &r.SalesQty730,
|
||
&r.SalesUSD30, &r.SalesUSD90, &r.SalesUSD180, &r.SalesUSD365, &r.AvgDailySales90, &r.AvgDailySales180,
|
||
&r.StockDays90, &r.StockDays180, &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.PriceIndex90, &r.MarginIndex90, &r.PerformanceScore, &r.PerformanceBucket,
|
||
&r.Recommendation, &r.LastSaleDate, &r.LastRefNumber, &r.UpdatedAt,
|
||
); err != nil {
|
||
return nil, 0, err
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, total, rows.Err()
|
||
}
|
||
|
||
func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.ProductPerformanceSummary, error) {
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return models.ProductPerformanceSummary{}, err
|
||
}
|
||
var s models.ProductPerformanceSummary
|
||
err := pg.QueryRowContext(ctx, `
|
||
WITH Latest AS (
|
||
SELECT MAX(kpi_date) AS kpi_date
|
||
FROM mk_product_performance_kpi_daily
|
||
),
|
||
KPI AS (
|
||
SELECT *
|
||
FROM mk_product_performance_kpi_daily
|
||
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
||
),
|
||
VariantStock AS (
|
||
SELECT
|
||
product_code,
|
||
color_code,
|
||
yaka_kodu,
|
||
MAX(stock_qty) AS stock_qty,
|
||
MAX(cost_price_usd) AS cost_price_usd,
|
||
BOOL_OR(performance_bucket IN ('STOK_RISKI','TAKIP')) AS has_risk_bucket,
|
||
SUM(COALESCE(sales_qty_90d,0)) AS sales_qty_90d
|
||
FROM KPI
|
||
GROUP BY product_code, color_code, yaka_kodu
|
||
),
|
||
StockTotals AS (
|
||
SELECT
|
||
COALESCE(SUM(stock_qty),0) AS total_stock,
|
||
COALESCE(SUM(stock_qty * cost_price_usd),0) AS stock_cost_usd,
|
||
COALESCE(SUM(CASE WHEN has_risk_bucket AND COALESCE(sales_qty_90d,0)=0 THEN stock_qty * cost_price_usd ELSE 0 END),0) AS risk_cost_usd
|
||
FROM VariantStock
|
||
),
|
||
Sales90 AS (
|
||
SELECT
|
||
COUNT(DISTINCT market_key) AS market_count_90d,
|
||
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)
|
||
)
|
||
SELECT
|
||
COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
|
||
COUNT(*),
|
||
COUNT(*) FILTER (WHERE performance_bucket='YILDIZ_URUN'),
|
||
COUNT(*) FILTER (WHERE performance_bucket='STOK_RISKI'),
|
||
COUNT(*) FILTER (WHERE performance_bucket='STOKSUZ_TALEP'),
|
||
COALESCE(MAX(StockTotals.total_stock),0),
|
||
COALESCE(MAX(StockTotals.stock_cost_usd),0),
|
||
COALESCE(MAX(StockTotals.risk_cost_usd),0),
|
||
COALESCE(SUM(sales_qty_90d),0),
|
||
COALESCE(SUM(sales_usd_90d),0),
|
||
COALESCE(SUM(gross_profit_usd_90d),0),
|
||
COALESCE(MAX(Sales90.market_count_90d),0)::integer,
|
||
COALESCE(MAX(Sales90.customer_count_90d),0)::integer,
|
||
COALESCE(to_char(MAX(updated_at),'YYYY-MM-DD HH24:MI:SS'),'')
|
||
FROM KPI
|
||
CROSS JOIN Sales90
|
||
CROSS JOIN StockTotals
|
||
`).Scan(&s.KpiDate, &s.TotalRows, &s.StarCount, &s.StockRisk, &s.NoStockDemand, &s.TotalStock, &s.StockCostUSD, &s.RiskCostUSD, &s.SalesQty90, &s.SalesUSD90, &s.GrossProfit90, &s.MarketCount90, &s.CustomerCount90, &s.UpdatedAt)
|
||
return s, err
|
||
}
|
||
|
||
func ListProductPerformanceGeneral(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceGeneralRow, error) {
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return nil, err
|
||
}
|
||
if limit <= 0 || limit > 1000 {
|
||
limit = 500
|
||
}
|
||
rows, err := pg.QueryContext(ctx, `
|
||
WITH Bounds AS (
|
||
SELECT
|
||
DATE '2022-01-01' AS period_start,
|
||
COALESCE(
|
||
(SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily),
|
||
(SELECT MAX(sales_date) FROM mk_product_performance_sales_daily),
|
||
current_date
|
||
) AS period_end
|
||
),
|
||
LatestStockDate AS (
|
||
SELECT MAX(stock_date) AS stock_date
|
||
FROM mk_product_performance_stock_daily
|
||
),
|
||
SalesAgg AS (
|
||
SELECT
|
||
product_code,
|
||
color_code,
|
||
yaka_kodu,
|
||
market_key,
|
||
MAX(item_description) AS item_description,
|
||
MAX(kategori) AS kategori,
|
||
MAX(seri) AS seri,
|
||
MAX(yas_grubu) AS yas_grubu,
|
||
MAX(askili_yan) AS askili_yan,
|
||
MAX(urun_ilk_grubu) AS urun_ilk_grubu,
|
||
MAX(urun_ana_grubu) AS urun_ana_grubu,
|
||
MAX(urun_alt_grubu) AS urun_alt_grubu,
|
||
MIN(sales_date) AS first_sale_date,
|
||
MAX(sales_date) AS last_sale_date,
|
||
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(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
|
||
GROUP BY product_code, color_code, yaka_kodu, market_key
|
||
),
|
||
StockAgg AS (
|
||
SELECT
|
||
s.product_code,
|
||
s.color_code,
|
||
s.yaka_kodu,
|
||
COALESCE(SUM(s.stock_qty),0) AS stock_qty
|
||
FROM mk_product_performance_stock_daily s
|
||
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
|
||
GROUP BY s.product_code, s.color_code, s.yaka_kodu
|
||
),
|
||
Dim AS (
|
||
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
|
||
product_code,
|
||
color_code,
|
||
yaka_kodu,
|
||
item_description,
|
||
kategori,
|
||
seri,
|
||
yas_grubu,
|
||
askili_yan,
|
||
urun_ilk_grubu,
|
||
urun_ana_grubu,
|
||
urun_alt_grubu
|
||
FROM (
|
||
SELECT
|
||
product_code,
|
||
color_code,
|
||
yaka_kodu,
|
||
item_description,
|
||
kategori,
|
||
seri,
|
||
yas_grubu,
|
||
askili_yan,
|
||
urun_ilk_grubu,
|
||
urun_ana_grubu,
|
||
urun_alt_grubu,
|
||
1 AS src_rank
|
||
FROM mk_product_performance_kpi_daily
|
||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
||
UNION ALL
|
||
SELECT
|
||
product_code,
|
||
color_code,
|
||
yaka_kodu,
|
||
MAX(item_description) AS item_description,
|
||
MAX(kategori) AS kategori,
|
||
MAX(seri) AS seri,
|
||
MAX(yas_grubu) AS yas_grubu,
|
||
MAX(askili_yan) AS askili_yan,
|
||
MAX(urun_ilk_grubu) AS urun_ilk_grubu,
|
||
MAX(urun_ana_grubu) AS urun_ana_grubu,
|
||
MAX(urun_alt_grubu) AS urun_alt_grubu,
|
||
2 AS src_rank
|
||
FROM mk_product_performance_sales_daily
|
||
GROUP BY product_code, color_code, yaka_kodu
|
||
) x
|
||
ORDER BY product_code, color_code, yaka_kodu, src_rank
|
||
),
|
||
StockOnly AS (
|
||
SELECT
|
||
s.product_code,
|
||
s.color_code,
|
||
s.yaka_kodu,
|
||
'STOK'::text AS market_key,
|
||
COALESCE(MAX(d.item_description),'') AS item_description,
|
||
COALESCE(MAX(d.kategori),'') AS kategori,
|
||
COALESCE(MAX(d.seri),'') AS seri,
|
||
COALESCE(MAX(d.yas_grubu),'') AS yas_grubu,
|
||
COALESCE(MAX(d.askili_yan),'') AS askili_yan,
|
||
COALESCE(MAX(d.urun_ilk_grubu),'') AS urun_ilk_grubu,
|
||
COALESCE(MAX(d.urun_ana_grubu),'') AS urun_ana_grubu,
|
||
COALESCE(MAX(d.urun_alt_grubu),'') AS urun_alt_grubu,
|
||
NULL::date AS first_sale_date,
|
||
NULL::date AS last_sale_date,
|
||
''::text AS last_ref_number,
|
||
0::numeric AS sales_qty_total,
|
||
0::numeric AS sales_usd_total,
|
||
0::integer AS customer_count_total,
|
||
0::integer AS invoice_count_total
|
||
FROM mk_product_performance_stock_daily s
|
||
LEFT JOIN Dim d
|
||
ON d.product_code = s.product_code
|
||
AND d.color_code = s.color_code
|
||
AND d.yaka_kodu = s.yaka_kodu
|
||
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
|
||
AND NOT EXISTS (
|
||
SELECT 1
|
||
FROM SalesAgg a
|
||
WHERE a.product_code = s.product_code
|
||
AND a.color_code = s.color_code
|
||
AND a.yaka_kodu = s.yaka_kodu
|
||
)
|
||
GROUP BY s.product_code, s.color_code, s.yaka_kodu
|
||
),
|
||
BaseRows AS (
|
||
SELECT * FROM SalesAgg
|
||
UNION ALL
|
||
SELECT * FROM StockOnly
|
||
),
|
||
Spread AS (
|
||
SELECT
|
||
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
|
||
FROM mk_product_performance_sales_daily, Bounds
|
||
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
||
GROUP BY product_code, color_code, yaka_kodu
|
||
),
|
||
Scored AS (
|
||
SELECT
|
||
Bounds.period_start,
|
||
Bounds.period_end,
|
||
b.product_code,
|
||
b.color_code,
|
||
b.yaka_kodu,
|
||
b.item_description,
|
||
b.kategori,
|
||
b.seri,
|
||
b.yas_grubu,
|
||
b.askili_yan,
|
||
b.urun_ilk_grubu,
|
||
b.urun_ana_grubu,
|
||
b.urun_alt_grubu,
|
||
b.market_key,
|
||
COALESCE(st.stock_qty, 0) AS stock_qty,
|
||
b.sales_qty_total,
|
||
b.sales_usd_total,
|
||
b.sales_qty_total / GREATEST(1, (Bounds.period_end - Bounds.period_start + 1)) AS avg_daily_sales_total,
|
||
CASE
|
||
WHEN b.sales_qty_total <= 0 THEN 9999
|
||
ELSE COALESCE(st.stock_qty, 0) / NULLIF(b.sales_qty_total / GREATEST(1, (Bounds.period_end - Bounds.period_start + 1)), 0)
|
||
END AS stock_days_total,
|
||
CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE b.sales_usd_total / NULLIF(b.sales_qty_total,0) END AS avg_price_usd_total,
|
||
COALESCE(pd.cost_price_usd,0) AS cost_price_usd,
|
||
COALESCE(pd.base_price_usd,0) AS base_price_usd,
|
||
b.sales_usd_total - (b.sales_qty_total * COALESCE(pd.cost_price_usd,0)) AS gross_profit_usd_total,
|
||
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,
|
||
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
|
||
ELSE b.sales_qty_total / NULLIF(AVG(b.sales_qty_total) OVER (PARTITION BY b.market_key, b.kategori, b.urun_ana_grubu),0)
|
||
END AS sales_index_total,
|
||
b.first_sale_date,
|
||
b.last_sale_date,
|
||
b.last_ref_number
|
||
FROM BaseRows b
|
||
CROSS JOIN Bounds
|
||
LEFT JOIN StockAgg st
|
||
ON st.product_code = b.product_code
|
||
AND st.color_code = b.color_code
|
||
AND st.yaka_kodu = b.yaka_kodu
|
||
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code = b.product_code
|
||
LEFT JOIN Spread sp
|
||
ON sp.product_code = b.product_code
|
||
AND sp.color_code = b.color_code
|
||
AND sp.yaka_kodu = b.yaka_kodu
|
||
)
|
||
SELECT
|
||
to_char(period_start,'YYYY-MM-DD'),
|
||
to_char(period_end,'YYYY-MM-DD'),
|
||
product_code, color_code, yaka_kodu, item_description,
|
||
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key,
|
||
stock_qty, sales_qty_total, sales_usd_total, avg_daily_sales_total, stock_days_total, avg_price_usd_total,
|
||
cost_price_usd, base_price_usd, gross_profit_usd_total, gross_margin_total, unit_profit_cost_total, unit_profit_base_total,
|
||
market_count_total, customer_count_total, invoice_count_total, sales_index_total,
|
||
ROUND((
|
||
LEAST(35, GREATEST(0, sales_index_total) * 18)
|
||
+ LEAST(25, GREATEST(0, gross_margin_total) * 50)
|
||
+ LEAST(20, customer_count_total * 1.5)
|
||
+ LEAST(10, market_count_total * 2.5)
|
||
+ CASE
|
||
WHEN sales_qty_total > 0 AND stock_days_total BETWEEN 20 AND 180 THEN 10
|
||
WHEN sales_qty_total > 0 AND stock_days_total > 365 THEN -10
|
||
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN -15
|
||
ELSE 0
|
||
END
|
||
)::numeric, 4) AS performance_score,
|
||
CASE
|
||
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 gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'FIYAT_FIRSATI'
|
||
ELSE 'TAKIP'
|
||
END AS performance_bucket,
|
||
CASE
|
||
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 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,
|
||
COALESCE(to_char(first_sale_date,'YYYY-MM-DD'),''),
|
||
COALESCE(to_char(last_sale_date,'YYYY-MM-DD'),''),
|
||
COALESCE(last_ref_number,'')
|
||
FROM Scored
|
||
ORDER BY performance_score DESC, sales_usd_total DESC, product_code ASC, color_code ASC, yaka_kodu ASC
|
||
LIMIT $1
|
||
`, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := make([]models.ProductPerformanceGeneralRow, 0, limit)
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceGeneralRow
|
||
if err := rows.Scan(
|
||
&r.PeriodStart, &r.PeriodEnd, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
|
||
&r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey,
|
||
&r.StockQty, &r.SalesQtyTotal, &r.SalesUSDTotal, &r.AvgDailySalesTotal, &r.StockDaysTotal, &r.AvgPriceUSDTotal,
|
||
&r.CostPriceUSD, &r.BasePriceUSD, &r.GrossProfitUSDTotal, &r.GrossMarginTotal, &r.UnitProfitCostTotal, &r.UnitProfitBaseTotal,
|
||
&r.MarketCountTotal, &r.CustomerCountTotal, &r.InvoiceCountTotal, &r.SalesIndexTotal, &r.PerformanceScore,
|
||
&r.PerformanceBucket, &r.Recommendation, &r.FirstSaleDate, &r.LastSaleDate, &r.LastRefNumber,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderAnalysisRow, error) {
|
||
if db.MssqlDB == nil {
|
||
return nil, fmt.Errorf("mssql db nil")
|
||
}
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return nil, err
|
||
}
|
||
if limit <= 0 || limit > 1000 {
|
||
limit = 500
|
||
}
|
||
|
||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||
WITH OpenOrderLines AS (
|
||
SELECT
|
||
OrderDate = CAST(h.OrderDate AS date),
|
||
DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date),
|
||
h.OrderHeaderID,
|
||
h.OrderNumber,
|
||
h.CurrAccCode,
|
||
ProductCode = LTRIM(RTRIM(l.ItemCode)),
|
||
ColorCode = LTRIM(RTRIM(ISNULL(l.ColorCode, ''))),
|
||
YakaKodu = LTRIM(RTRIM(ISNULL(l.ItemDim2Code, ''))),
|
||
ItemDescription = dbo.HG_Temizlik(ISNULL((
|
||
SELECT ItemDescription
|
||
FROM cdItemDesc WITH(NOLOCK)
|
||
WHERE cdItemDesc.ItemTypeCode = l.ItemTypeCode
|
||
AND cdItemDesc.ItemCode = l.ItemCode
|
||
AND cdItemDesc.LangCode = 'TR'
|
||
), SPACE(0))),
|
||
Kategori = dbo.HG_Temizlik(ISNULL((
|
||
SELECT AttributeDescription
|
||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||
AND cdItemAttributeDesc.AttributeTypeCode = 42
|
||
AND cdItemAttributeDesc.AttributeCode = (
|
||
SELECT TOP 1 AttributeCode
|
||
FROM prItemAttribute WITH(NOLOCK)
|
||
WHERE AttributeTypeCode = 42
|
||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||
AND prItemAttribute.ItemCode = l.ItemCode
|
||
)
|
||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||
), SPACE(0))),
|
||
Seri = dbo.HG_Temizlik(ISNULL((
|
||
SELECT AttributeDescription
|
||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||
AND cdItemAttributeDesc.AttributeTypeCode = 2
|
||
AND cdItemAttributeDesc.AttributeCode = (
|
||
SELECT TOP 1 AttributeCode
|
||
FROM prItemAttribute WITH(NOLOCK)
|
||
WHERE AttributeTypeCode = 2
|
||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||
AND prItemAttribute.ItemCode = l.ItemCode
|
||
)
|
||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||
), SPACE(0))),
|
||
YasGrubu = dbo.HG_Temizlik(ISNULL((
|
||
SELECT AttributeDescription
|
||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||
AND cdItemAttributeDesc.AttributeTypeCode = 44
|
||
AND cdItemAttributeDesc.AttributeCode = (
|
||
SELECT TOP 1 AttributeCode
|
||
FROM prItemAttribute WITH(NOLOCK)
|
||
WHERE AttributeTypeCode = 44
|
||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||
AND prItemAttribute.ItemCode = l.ItemCode
|
||
)
|
||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||
), SPACE(0))),
|
||
AskiliYan = dbo.HG_Temizlik(ISNULL((
|
||
SELECT TOP 1 ProductAtt45
|
||
FROM ProductAttributesFilter WITH(NOLOCK)
|
||
WHERE ProductAttributesFilter.ItemCode = l.ItemCode
|
||
), SPACE(0))),
|
||
UrunAnaGrubu = dbo.HG_Temizlik(ISNULL((
|
||
SELECT AttributeDescription
|
||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||
AND cdItemAttributeDesc.AttributeTypeCode = 1
|
||
AND cdItemAttributeDesc.AttributeCode = (
|
||
SELECT TOP 1 AttributeCode
|
||
FROM prItemAttribute WITH(NOLOCK)
|
||
WHERE AttributeTypeCode = 1
|
||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||
AND prItemAttribute.ItemCode = l.ItemCode
|
||
)
|
||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||
), SPACE(0))),
|
||
UrunAltGrubu = dbo.HG_Temizlik(ISNULL((
|
||
SELECT AttributeDescription
|
||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||
AND cdItemAttributeDesc.AttributeTypeCode = 2
|
||
AND cdItemAttributeDesc.AttributeCode = (
|
||
SELECT TOP 1 AttributeCode
|
||
FROM prItemAttribute WITH(NOLOCK)
|
||
WHERE AttributeTypeCode = 2
|
||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||
AND prItemAttribute.ItemCode = l.ItemCode
|
||
)
|
||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||
), SPACE(0))),
|
||
MarketKey = dbo.HG_Temizlik(ISNULL((
|
||
SELECT AttributeDescription
|
||
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
|
||
WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
|
||
AND cdCurrAccAttributeDesc.AttributeTypeCode = 1
|
||
AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01
|
||
AND cdCurrAccAttributeDesc.LangCode = 'TR'
|
||
), SPACE(0))),
|
||
Qty = ISNULL(l.Qty1, 0),
|
||
AmountUSD = CASE
|
||
WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
|
||
WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
|
||
WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
|
||
ELSE 0
|
||
END
|
||
FROM dbo.trOrderHeader h WITH(NOLOCK)
|
||
INNER JOIN dbo.trOrderLine l WITH(NOLOCK)
|
||
ON l.OrderHeaderID = h.OrderHeaderID
|
||
LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK)
|
||
ON c.OrderLineID = l.OrderLineID
|
||
AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
|
||
LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK)
|
||
ON caf.CurrAccTypeCode = h.CurrAccTypeCode
|
||
AND caf.CurrAccCode = h.CurrAccCode
|
||
OUTER APPLY (
|
||
SELECT TOP 1 Rate
|
||
FROM dbo.AllExchangeRates WITH(NOLOCK)
|
||
WHERE CurrencyCode = 'USD'
|
||
AND RelationCurrencyCode = 'TRY'
|
||
AND ExchangeTypeCode = 6
|
||
AND Rate > 0
|
||
AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
|
||
ORDER BY Date DESC
|
||
) usd
|
||
OUTER APPLY (
|
||
SELECT TOP 1 Rate
|
||
FROM dbo.AllExchangeRates WITH(NOLOCK)
|
||
WHERE CurrencyCode = h.DocCurrencyCode
|
||
AND RelationCurrencyCode = 'TRY'
|
||
AND ExchangeTypeCode = 6
|
||
AND Rate > 0
|
||
AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
|
||
ORDER BY Date DESC
|
||
) cur
|
||
WHERE ISNULL(h.IsCancelOrder, 0) = 0
|
||
AND ISNULL(h.IsClosed, 0) = 0
|
||
AND h.OrderTypeCode = 1
|
||
AND h.ProcessCode = 'WS'
|
||
AND ISNULL(l.IsClosed, 0) = 0
|
||
AND l.ItemTypeCode = 1
|
||
AND ISNULL(l.Qty1, 0) > 0
|
||
AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
|
||
AND (
|
||
l.ItemCode LIKE 'S%'
|
||
OR l.ItemCode LIKE 'O%'
|
||
OR l.ItemCode LIKE 'N%'
|
||
OR l.ItemCode LIKE 'X%'
|
||
OR l.ItemCode LIKE 'I%'
|
||
OR l.ItemCode LIKE 'A%'
|
||
)
|
||
),
|
||
Spread AS (
|
||
SELECT
|
||
ProductCode,
|
||
ColorCode,
|
||
YakaKodu,
|
||
MarketCount = COUNT(DISTINCT NULLIF(MarketKey, '')),
|
||
CustomerCount = COUNT(DISTINCT NULLIF(CurrAccCode, ''))
|
||
FROM OpenOrderLines
|
||
GROUP BY ProductCode, ColorCode, YakaKodu
|
||
)
|
||
SELECT TOP (@p1)
|
||
o.ProductCode,
|
||
o.ColorCode,
|
||
o.YakaKodu,
|
||
ItemDescription = MAX(o.ItemDescription),
|
||
Kategori = MAX(o.Kategori),
|
||
Seri = MAX(o.Seri),
|
||
YasGrubu = MAX(o.YasGrubu),
|
||
AskiliYan = MAX(o.AskiliYan),
|
||
UrunAnaGrubu = MAX(o.UrunAnaGrubu),
|
||
UrunAltGrubu = MAX(o.UrunAltGrubu),
|
||
o.MarketKey,
|
||
OrderQty = SUM(o.Qty),
|
||
OrderUSD = SUM(o.AmountUSD),
|
||
AvgOrderPriceUSD = CASE WHEN SUM(o.Qty) = 0 THEN 0 ELSE SUM(o.AmountUSD) / NULLIF(SUM(o.Qty), 0) END,
|
||
MarketCount = MAX(s.MarketCount),
|
||
CustomerCount = COUNT(DISTINCT NULLIF(o.CurrAccCode, '')),
|
||
OrderCount = COUNT(DISTINCT o.OrderHeaderID),
|
||
LineCount = COUNT(*),
|
||
FirstOrderDate = CONVERT(varchar, MIN(o.OrderDate), 23),
|
||
LastOrderDate = CONVERT(varchar, MAX(o.OrderDate), 23),
|
||
EarliestDueDate = CONVERT(varchar, MIN(o.DueDate), 23),
|
||
OverdueQty = SUM(CASE WHEN o.DueDate < CAST(GETDATE() AS date) THEN o.Qty ELSE 0 END)
|
||
FROM OpenOrderLines o
|
||
LEFT JOIN Spread s
|
||
ON s.ProductCode = o.ProductCode
|
||
AND s.ColorCode = o.ColorCode
|
||
AND s.YakaKodu = o.YakaKodu
|
||
GROUP BY o.ProductCode, o.ColorCode, o.YakaKodu, o.MarketKey
|
||
ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.YakaKodu
|
||
`, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
out := make([]models.ProductPerformanceOrderAnalysisRow, 0, limit)
|
||
productCodes := make([]string, 0, limit)
|
||
stockKeys := make([]string, 0, limit)
|
||
seenProducts := map[string]bool{}
|
||
seenStockKeys := map[string]bool{}
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceOrderAnalysisRow
|
||
if err := rows.Scan(
|
||
&r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription, &r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan,
|
||
&r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey, &r.OrderQty, &r.OrderUSD, &r.AvgOrderPriceUSD,
|
||
&r.MarketCount, &r.CustomerCount, &r.OrderCount, &r.LineCount, &r.FirstOrderDate, &r.LastOrderDate, &r.EarliestDueDate, &r.OverdueQty,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
r.UrunIlkGrubu = r.YasGrubu
|
||
out = append(out, r)
|
||
if !seenProducts[r.ProductCode] {
|
||
seenProducts[r.ProductCode] = true
|
||
productCodes = append(productCodes, r.ProductCode)
|
||
}
|
||
key := productPerformanceVariantKey(r.ProductCode, r.ColorCode, r.YakaKodu)
|
||
if !seenStockKeys[key] {
|
||
seenStockKeys[key] = true
|
||
stockKeys = append(stockKeys, key)
|
||
}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for i := range out {
|
||
row := &out[i]
|
||
price := priceByProduct[row.ProductCode]
|
||
row.CostPriceUSD = price.cost
|
||
row.BasePriceUSD = price.base
|
||
row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||
row.NetStockAfterOrder = row.StockQty - row.OrderQty
|
||
row.UnitProfitCostUSD = row.AvgOrderPriceUSD - row.CostPriceUSD
|
||
row.UnitProfitBaseUSD = row.AvgOrderPriceUSD - row.BasePriceUSD
|
||
row.ExpectedProfitCostUSD = row.UnitProfitCostUSD * row.OrderQty
|
||
row.ExpectedProfitBaseUSD = row.UnitProfitBaseUSD * row.OrderQty
|
||
if row.OrderUSD != 0 {
|
||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||
}
|
||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderRecommendation(*row)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func ListProductPerformanceOrderGroups(ctx context.Context, pg *sql.DB, breakdown string, limit int) ([]models.ProductPerformanceOrderGroupRow, error) {
|
||
if db.MssqlDB == nil {
|
||
return nil, fmt.Errorf("mssql db nil")
|
||
}
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return nil, err
|
||
}
|
||
mode := strings.ToLower(strings.TrimSpace(breakdown))
|
||
if mode != "customer" {
|
||
mode = "market"
|
||
}
|
||
if limit <= 0 || limit > 1000 {
|
||
limit = 500
|
||
}
|
||
|
||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||
WITH OpenOrderLines AS (
|
||
SELECT
|
||
h.OrderHeaderID,
|
||
h.CurrAccCode,
|
||
CustomerName = ISNULL(cad.CurrAccDescription, ''),
|
||
ProductCode = LTRIM(RTRIM(l.ItemCode)),
|
||
ColorCode = LTRIM(RTRIM(ISNULL(l.ColorCode, ''))),
|
||
YakaKodu = LTRIM(RTRIM(ISNULL(l.ItemDim2Code, ''))),
|
||
MarketKey = dbo.HG_Temizlik(ISNULL((
|
||
SELECT AttributeDescription
|
||
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
|
||
WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
|
||
AND cdCurrAccAttributeDesc.AttributeTypeCode = 1
|
||
AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01
|
||
AND cdCurrAccAttributeDesc.LangCode = 'TR'
|
||
), SPACE(0))),
|
||
Qty = ISNULL(l.Qty1, 0),
|
||
AmountUSD = CASE
|
||
WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
|
||
WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
|
||
WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
|
||
ELSE 0
|
||
END,
|
||
DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date)
|
||
FROM dbo.trOrderHeader h WITH(NOLOCK)
|
||
INNER JOIN dbo.trOrderLine l WITH(NOLOCK)
|
||
ON l.OrderHeaderID = h.OrderHeaderID
|
||
LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK)
|
||
ON c.OrderLineID = l.OrderLineID
|
||
AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
|
||
LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK)
|
||
ON caf.CurrAccTypeCode = h.CurrAccTypeCode
|
||
AND caf.CurrAccCode = h.CurrAccCode
|
||
LEFT JOIN dbo.cdCurrAccDesc cad WITH(NOLOCK)
|
||
ON cad.CurrAccTypeCode = h.CurrAccTypeCode
|
||
AND cad.CurrAccCode = h.CurrAccCode
|
||
AND cad.LangCode = 'TR'
|
||
OUTER APPLY (
|
||
SELECT TOP 1 Rate
|
||
FROM dbo.AllExchangeRates WITH(NOLOCK)
|
||
WHERE CurrencyCode = 'USD'
|
||
AND RelationCurrencyCode = 'TRY'
|
||
AND ExchangeTypeCode = 6
|
||
AND Rate > 0
|
||
AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
|
||
ORDER BY Date DESC
|
||
) usd
|
||
OUTER APPLY (
|
||
SELECT TOP 1 Rate
|
||
FROM dbo.AllExchangeRates WITH(NOLOCK)
|
||
WHERE CurrencyCode = h.DocCurrencyCode
|
||
AND RelationCurrencyCode = 'TRY'
|
||
AND ExchangeTypeCode = 6
|
||
AND Rate > 0
|
||
AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
|
||
ORDER BY Date DESC
|
||
) cur
|
||
WHERE ISNULL(h.IsCancelOrder, 0) = 0
|
||
AND ISNULL(h.IsClosed, 0) = 0
|
||
AND h.OrderTypeCode = 1
|
||
AND h.ProcessCode = 'WS'
|
||
AND ISNULL(l.IsClosed, 0) = 0
|
||
AND l.ItemTypeCode = 1
|
||
AND ISNULL(l.Qty1, 0) > 0
|
||
AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
|
||
AND (
|
||
l.ItemCode LIKE 'S%'
|
||
OR l.ItemCode LIKE 'O%'
|
||
OR l.ItemCode LIKE 'N%'
|
||
OR l.ItemCode LIKE 'X%'
|
||
OR l.ItemCode LIKE 'I%'
|
||
OR l.ItemCode LIKE 'A%'
|
||
)
|
||
)
|
||
SELECT TOP (@p1)
|
||
ProductCode,
|
||
ColorCode,
|
||
YakaKodu,
|
||
MarketKey,
|
||
CurrAccCode,
|
||
CustomerName,
|
||
OrderQty = SUM(Qty),
|
||
OrderUSD = SUM(AmountUSD),
|
||
OrderCount = COUNT(DISTINCT OrderHeaderID),
|
||
LineCount = COUNT(*),
|
||
OverdueQty = SUM(CASE WHEN DueDate < CAST(GETDATE() AS date) THEN Qty ELSE 0 END)
|
||
FROM OpenOrderLines
|
||
GROUP BY ProductCode, ColorCode, YakaKodu, MarketKey, CurrAccCode, CustomerName
|
||
ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
|
||
`, limit*4)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
type detail struct {
|
||
productCode string
|
||
colorCode string
|
||
yakaKodu string
|
||
marketKey string
|
||
customerCode string
|
||
customerName string
|
||
orderQty float64
|
||
orderUSD float64
|
||
orderCount int
|
||
lineCount int
|
||
overdueQty float64
|
||
}
|
||
details := make([]detail, 0, limit*2)
|
||
productCodes := make([]string, 0, limit)
|
||
stockKeys := make([]string, 0, limit)
|
||
seenProducts := map[string]bool{}
|
||
seenStockKeys := map[string]bool{}
|
||
for rows.Next() {
|
||
var d detail
|
||
if err := rows.Scan(&d.productCode, &d.colorCode, &d.yakaKodu, &d.marketKey, &d.customerCode, &d.customerName, &d.orderQty, &d.orderUSD, &d.orderCount, &d.lineCount, &d.overdueQty); err != nil {
|
||
return nil, err
|
||
}
|
||
details = append(details, d)
|
||
if !seenProducts[d.productCode] {
|
||
seenProducts[d.productCode] = true
|
||
productCodes = append(productCodes, d.productCode)
|
||
}
|
||
key := productPerformanceVariantKey(d.productCode, d.colorCode, d.yakaKodu)
|
||
if !seenStockKeys[key] {
|
||
seenStockKeys[key] = true
|
||
stockKeys = append(stockKeys, key)
|
||
}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
grouped := map[string]*models.ProductPerformanceOrderGroupRow{}
|
||
productSets := map[string]map[string]bool{}
|
||
marketSets := map[string]map[string]bool{}
|
||
customerSets := map[string]map[string]bool{}
|
||
stockVariantSets := map[string]map[string]bool{}
|
||
for _, d := range details {
|
||
groupKey := strings.TrimSpace(d.marketKey)
|
||
customerCode := ""
|
||
customerName := ""
|
||
if mode == "customer" {
|
||
groupKey = strings.TrimSpace(d.customerCode)
|
||
customerCode = strings.TrimSpace(d.customerCode)
|
||
customerName = strings.TrimSpace(d.customerName)
|
||
}
|
||
if groupKey == "" {
|
||
groupKey = "-"
|
||
}
|
||
row := grouped[groupKey]
|
||
if row == nil {
|
||
row = &models.ProductPerformanceOrderGroupRow{
|
||
Breakdown: mode,
|
||
GroupKey: groupKey,
|
||
MarketKey: strings.TrimSpace(d.marketKey),
|
||
CustomerCode: customerCode,
|
||
CustomerName: customerName,
|
||
}
|
||
grouped[groupKey] = row
|
||
productSets[groupKey] = map[string]bool{}
|
||
marketSets[groupKey] = map[string]bool{}
|
||
customerSets[groupKey] = map[string]bool{}
|
||
stockVariantSets[groupKey] = map[string]bool{}
|
||
}
|
||
price := priceByProduct[d.productCode]
|
||
variantKey := productPerformanceVariantKey(d.productCode, d.colorCode, d.yakaKodu)
|
||
row.OrderQty += d.orderQty
|
||
row.OrderUSD += d.orderUSD
|
||
row.BaseCostUSD += d.orderQty * price.base
|
||
row.CostAmountUSD += d.orderQty * price.cost
|
||
if !stockVariantSets[groupKey][variantKey] {
|
||
row.StockQty += stockByKey[variantKey]
|
||
stockVariantSets[groupKey][variantKey] = true
|
||
}
|
||
row.OrderCount += d.orderCount
|
||
row.LineCount += d.lineCount
|
||
row.OverdueQty += d.overdueQty
|
||
productSets[groupKey][d.productCode] = true
|
||
if strings.TrimSpace(d.marketKey) != "" {
|
||
marketSets[groupKey][strings.TrimSpace(d.marketKey)] = true
|
||
}
|
||
if strings.TrimSpace(d.customerCode) != "" {
|
||
customerSets[groupKey][strings.TrimSpace(d.customerCode)] = true
|
||
}
|
||
}
|
||
|
||
out := make([]models.ProductPerformanceOrderGroupRow, 0, len(grouped))
|
||
for key, row := range grouped {
|
||
row.ProductCount = len(productSets[key])
|
||
row.MarketCount = len(marketSets[key])
|
||
row.CustomerCount = len(customerSets[key])
|
||
row.NetStockAfterOrder = row.StockQty - row.OrderQty
|
||
if row.OrderQty != 0 {
|
||
row.AvgOrderPriceUSD = row.OrderUSD / row.OrderQty
|
||
}
|
||
row.ExpectedProfitBaseUSD = row.OrderUSD - row.BaseCostUSD
|
||
row.ExpectedProfitCostUSD = row.OrderUSD - row.CostAmountUSD
|
||
if row.OrderUSD != 0 {
|
||
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||
}
|
||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderGroupRecommendation(*row)
|
||
out = append(out, *row)
|
||
}
|
||
sort.Slice(out, func(i, j int) bool {
|
||
if out[i].ExpectedProfitCostUSD < 0 && out[j].ExpectedProfitCostUSD >= 0 {
|
||
return true
|
||
}
|
||
if out[i].ExpectedProfitCostUSD >= 0 && out[j].ExpectedProfitCostUSD < 0 {
|
||
return false
|
||
}
|
||
return out[i].OrderUSD > out[j].OrderUSD
|
||
})
|
||
if len(out) > limit {
|
||
out = out[:limit]
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func ListProductPerformanceOrderProductCustomers(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderProductCustomerRow, error) {
|
||
if db.MssqlDB == nil {
|
||
return nil, fmt.Errorf("mssql db nil")
|
||
}
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return nil, err
|
||
}
|
||
if limit <= 0 || limit > 1000 {
|
||
limit = 500
|
||
}
|
||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||
WITH OpenOrderLines AS (
|
||
SELECT
|
||
h.OrderHeaderID,
|
||
h.CurrAccCode,
|
||
CustomerName = ISNULL(cad.CurrAccDescription, ''),
|
||
ProductCode = LTRIM(RTRIM(l.ItemCode)),
|
||
ColorCode = LTRIM(RTRIM(ISNULL(l.ColorCode, ''))),
|
||
YakaKodu = LTRIM(RTRIM(ISNULL(l.ItemDim2Code, ''))),
|
||
ItemDescription = dbo.HG_Temizlik(ISNULL((SELECT ItemDescription FROM cdItemDesc WITH(NOLOCK) WHERE cdItemDesc.ItemTypeCode = l.ItemTypeCode AND cdItemDesc.ItemCode = l.ItemCode AND cdItemDesc.LangCode = 'TR'), SPACE(0))),
|
||
MarketKey = dbo.HG_Temizlik(ISNULL((
|
||
SELECT AttributeDescription FROM cdCurrAccAttributeDesc WITH(NOLOCK)
|
||
WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
|
||
AND cdCurrAccAttributeDesc.AttributeTypeCode = 1
|
||
AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01
|
||
AND cdCurrAccAttributeDesc.LangCode = 'TR'
|
||
), SPACE(0))),
|
||
Qty = ISNULL(l.Qty1, 0),
|
||
AmountUSD = CASE
|
||
WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
|
||
WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
|
||
WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
|
||
ELSE 0
|
||
END,
|
||
DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date)
|
||
FROM dbo.trOrderHeader h WITH(NOLOCK)
|
||
INNER JOIN dbo.trOrderLine l WITH(NOLOCK) ON l.OrderHeaderID = h.OrderHeaderID
|
||
LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK) ON c.OrderLineID = l.OrderLineID AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
|
||
LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK) ON caf.CurrAccTypeCode = h.CurrAccTypeCode AND caf.CurrAccCode = h.CurrAccCode
|
||
LEFT JOIN dbo.cdCurrAccDesc cad WITH(NOLOCK) ON cad.CurrAccTypeCode = h.CurrAccTypeCode AND cad.CurrAccCode = h.CurrAccCode AND cad.LangCode = 'TR'
|
||
OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = 'USD' AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) usd
|
||
OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = h.DocCurrencyCode AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) cur
|
||
WHERE ISNULL(h.IsCancelOrder, 0) = 0
|
||
AND ISNULL(h.IsClosed, 0) = 0
|
||
AND h.OrderTypeCode = 1
|
||
AND h.ProcessCode = 'WS'
|
||
AND ISNULL(l.IsClosed, 0) = 0
|
||
AND l.ItemTypeCode = 1
|
||
AND ISNULL(l.Qty1, 0) > 0
|
||
AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
|
||
AND (l.ItemCode LIKE 'S%' OR l.ItemCode LIKE 'O%' OR l.ItemCode LIKE 'N%' OR l.ItemCode LIKE 'X%' OR l.ItemCode LIKE 'I%' OR l.ItemCode LIKE 'A%')
|
||
)
|
||
SELECT TOP (@p1)
|
||
ProductCode, ColorCode, YakaKodu, MAX(ItemDescription), MarketKey, CurrAccCode, CustomerName,
|
||
SUM(Qty), SUM(AmountUSD), CASE WHEN SUM(Qty)=0 THEN 0 ELSE SUM(AmountUSD)/NULLIF(SUM(Qty),0) END,
|
||
COUNT(DISTINCT OrderHeaderID), COUNT(*), SUM(CASE WHEN DueDate < CAST(GETDATE() AS date) THEN Qty ELSE 0 END)
|
||
FROM OpenOrderLines
|
||
GROUP BY ProductCode, ColorCode, YakaKodu, MarketKey, CurrAccCode, CustomerName
|
||
ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
|
||
`, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := make([]models.ProductPerformanceOrderProductCustomerRow, 0, limit)
|
||
productCodes := make([]string, 0, limit)
|
||
stockKeys := make([]string, 0, limit)
|
||
seenProducts := map[string]bool{}
|
||
seenStockKeys := map[string]bool{}
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceOrderProductCustomerRow
|
||
if err := rows.Scan(&r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription, &r.MarketKey, &r.CustomerCode, &r.CustomerName, &r.OrderQty, &r.OrderUSD, &r.AvgOrderPriceUSD, &r.OrderCount, &r.LineCount, &r.OverdueQty); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, r)
|
||
if !seenProducts[r.ProductCode] {
|
||
seenProducts[r.ProductCode] = true
|
||
productCodes = append(productCodes, r.ProductCode)
|
||
}
|
||
key := productPerformanceVariantKey(r.ProductCode, r.ColorCode, r.YakaKodu)
|
||
if !seenStockKeys[key] {
|
||
seenStockKeys[key] = true
|
||
stockKeys = append(stockKeys, key)
|
||
}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for i := range out {
|
||
row := &out[i]
|
||
price := priceByProduct[row.ProductCode]
|
||
row.CostPriceUSD = price.cost
|
||
row.BasePriceUSD = price.base
|
||
row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||
row.NetStockAfterOrder = row.StockQty - row.OrderQty
|
||
row.ExpectedProfitBaseUSD = row.OrderUSD - row.OrderQty*row.BasePriceUSD
|
||
row.ExpectedProfitCostUSD = row.OrderUSD - row.OrderQty*row.CostPriceUSD
|
||
if row.OrderUSD != 0 {
|
||
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||
}
|
||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderProductCustomerRecommendation(*row)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func ListProductPerformanceOrderMarketDetails(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderMarketDetailRow, error) {
|
||
if db.MssqlDB == nil {
|
||
return nil, fmt.Errorf("mssql db nil")
|
||
}
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return nil, err
|
||
}
|
||
if limit <= 0 || limit > 1500 {
|
||
limit = 800
|
||
}
|
||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||
WITH OpenOrderLines AS (
|
||
SELECT
|
||
MarketKey = dbo.HG_Temizlik(ISNULL((SELECT AttributeDescription FROM cdCurrAccAttributeDesc WITH(NOLOCK) WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3 AND cdCurrAccAttributeDesc.AttributeTypeCode = 1 AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01 AND cdCurrAccAttributeDesc.LangCode = 'TR'), SPACE(0))),
|
||
h.CurrAccCode,
|
||
CustomerName = ISNULL(cad.CurrAccDescription, ''),
|
||
h.OrderNumber,
|
||
OrderDate = CAST(h.OrderDate AS date),
|
||
DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date),
|
||
ProductCode = LTRIM(RTRIM(l.ItemCode)),
|
||
ColorCode = LTRIM(RTRIM(ISNULL(l.ColorCode, ''))),
|
||
YakaKodu = LTRIM(RTRIM(ISNULL(l.ItemDim2Code, ''))),
|
||
ItemDescription = dbo.HG_Temizlik(ISNULL((SELECT ItemDescription FROM cdItemDesc WITH(NOLOCK) WHERE cdItemDesc.ItemTypeCode = l.ItemTypeCode AND cdItemDesc.ItemCode = l.ItemCode AND cdItemDesc.LangCode = 'TR'), SPACE(0))),
|
||
Qty = ISNULL(l.Qty1, 0),
|
||
AmountUSD = CASE
|
||
WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
|
||
WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
|
||
WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
|
||
ELSE 0
|
||
END
|
||
FROM dbo.trOrderHeader h WITH(NOLOCK)
|
||
INNER JOIN dbo.trOrderLine l WITH(NOLOCK) ON l.OrderHeaderID = h.OrderHeaderID
|
||
LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK) ON c.OrderLineID = l.OrderLineID AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
|
||
LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK) ON caf.CurrAccTypeCode = h.CurrAccTypeCode AND caf.CurrAccCode = h.CurrAccCode
|
||
LEFT JOIN dbo.cdCurrAccDesc cad WITH(NOLOCK) ON cad.CurrAccTypeCode = h.CurrAccTypeCode AND cad.CurrAccCode = h.CurrAccCode AND cad.LangCode = 'TR'
|
||
OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = 'USD' AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) usd
|
||
OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = h.DocCurrencyCode AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) cur
|
||
WHERE ISNULL(h.IsCancelOrder, 0) = 0
|
||
AND ISNULL(h.IsClosed, 0) = 0
|
||
AND h.OrderTypeCode = 1
|
||
AND h.ProcessCode = 'WS'
|
||
AND ISNULL(l.IsClosed, 0) = 0
|
||
AND l.ItemTypeCode = 1
|
||
AND ISNULL(l.Qty1, 0) > 0
|
||
AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
|
||
AND (l.ItemCode LIKE 'S%' OR l.ItemCode LIKE 'O%' OR l.ItemCode LIKE 'N%' OR l.ItemCode LIKE 'X%' OR l.ItemCode LIKE 'I%' OR l.ItemCode LIKE 'A%')
|
||
)
|
||
SELECT TOP (@p1)
|
||
MarketKey, CurrAccCode, CustomerName, OrderNumber, CONVERT(varchar, OrderDate, 23), CONVERT(varchar, DueDate, 23),
|
||
ProductCode, ColorCode, YakaKodu, MAX(ItemDescription),
|
||
SUM(Qty), SUM(AmountUSD), CASE WHEN SUM(Qty)=0 THEN 0 ELSE SUM(AmountUSD)/NULLIF(SUM(Qty),0) END,
|
||
CASE WHEN MIN(DueDate) < CAST(GETDATE() AS date) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END
|
||
FROM OpenOrderLines
|
||
GROUP BY MarketKey, CurrAccCode, CustomerName, OrderNumber, OrderDate, DueDate, ProductCode, ColorCode, YakaKodu
|
||
ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DESC
|
||
`, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := make([]models.ProductPerformanceOrderMarketDetailRow, 0, limit)
|
||
productCodes := make([]string, 0, limit)
|
||
stockKeys := make([]string, 0, limit)
|
||
seenProducts := map[string]bool{}
|
||
seenStockKeys := map[string]bool{}
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceOrderMarketDetailRow
|
||
if err := rows.Scan(&r.MarketKey, &r.CustomerCode, &r.CustomerName, &r.OrderNumber, &r.OrderDate, &r.DueDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription, &r.OrderQty, &r.OrderUSD, &r.AvgOrderPriceUSD, &r.IsOverdue); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, r)
|
||
if !seenProducts[r.ProductCode] {
|
||
seenProducts[r.ProductCode] = true
|
||
productCodes = append(productCodes, r.ProductCode)
|
||
}
|
||
key := productPerformanceVariantKey(r.ProductCode, r.ColorCode, r.YakaKodu)
|
||
if !seenStockKeys[key] {
|
||
seenStockKeys[key] = true
|
||
stockKeys = append(stockKeys, key)
|
||
}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for i := range out {
|
||
row := &out[i]
|
||
price := priceByProduct[row.ProductCode]
|
||
row.CostPriceUSD = price.cost
|
||
row.BasePriceUSD = price.base
|
||
row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||
row.NetStockAfterOrder = row.StockQty - row.OrderQty
|
||
row.ExpectedProfitBaseUSD = row.OrderUSD - row.OrderQty*row.BasePriceUSD
|
||
row.ExpectedProfitCostUSD = row.OrderUSD - row.OrderQty*row.CostPriceUSD
|
||
if row.OrderUSD != 0 {
|
||
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||
}
|
||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderMarketDetailRecommendation(*row)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func ListProductPerformanceMarkets(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceMarketRow, error) {
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return nil, err
|
||
}
|
||
if limit <= 0 || limit > 500 {
|
||
limit = 100
|
||
}
|
||
rows, err := pg.QueryContext(ctx, `
|
||
SELECT
|
||
market_key,
|
||
MAX(kategori) AS kategori,
|
||
MAX(seri) AS seri,
|
||
MAX(yas_grubu) AS yas_grubu,
|
||
MAX(askili_yan) AS askili_yan,
|
||
MAX(urun_ilk_grubu) AS urun_ilk_grubu,
|
||
MAX(urun_ana_grubu) AS urun_ana_grubu,
|
||
MAX(urun_alt_grubu) AS urun_alt_grubu,
|
||
COUNT(DISTINCT product_code) AS product_count,
|
||
COUNT(*) FILTER (WHERE performance_bucket='YILDIZ_URUN') AS star_count,
|
||
COUNT(*) FILTER (WHERE performance_bucket='STOK_RISKI') AS stock_risk_count,
|
||
COALESCE(SUM(stock_qty),0) AS stock_qty,
|
||
COALESCE(SUM(stock_qty * cost_price_usd),0) AS stock_cost_value_usd,
|
||
COALESCE(SUM(CASE WHEN performance_bucket IN ('STOK_RISKI','TAKIP') AND COALESCE(sales_qty_90d,0)=0 THEN stock_qty * cost_price_usd ELSE 0 END),0) AS risk_stock_cost_value_usd,
|
||
COALESCE(SUM(sales_qty_90d),0) AS sales_qty_90d,
|
||
COALESCE(SUM(sales_usd_90d),0) AS sales_usd_90d,
|
||
COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d,
|
||
COALESCE(AVG(NULLIF(gross_margin_90d,0)),0) AS avg_gross_margin_90d,
|
||
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)
|
||
GROUP BY market_key
|
||
ORDER BY risk_stock_cost_value_usd DESC, stock_cost_value_usd DESC, sales_usd_90d DESC
|
||
LIMIT $1
|
||
`, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := make([]models.ProductPerformanceMarketRow, 0, limit)
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceMarketRow
|
||
if err := rows.Scan(
|
||
&r.MarketKey, &r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu,
|
||
&r.ProductCount, &r.StarCount, &r.StockRiskCount, &r.StockQty,
|
||
&r.StockCostValueUSD, &r.RiskStockCostValueUSD, &r.SalesQty90,
|
||
&r.SalesUSD90, &r.GrossProfitUSD90, &r.AvgGrossMargin90, &r.AvgStockDays90,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func ListProductPerformanceCountries(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceCountryRow, error) {
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return nil, err
|
||
}
|
||
if limit <= 0 || limit > 500 {
|
||
limit = 100
|
||
}
|
||
rows, err := pg.QueryContext(ctx, `
|
||
SELECT
|
||
customer_country,
|
||
customer_segment,
|
||
market_key,
|
||
MAX(kategori) AS kategori,
|
||
MAX(seri) AS seri,
|
||
COUNT(DISTINCT product_code) AS product_count,
|
||
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,
|
||
CASE
|
||
WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)=0 THEN 0
|
||
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,
|
||
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'
|
||
GROUP BY customer_country, customer_segment, market_key
|
||
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
||
LIMIT $1
|
||
`, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := make([]models.ProductPerformanceCountryRow, 0, limit)
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceCountryRow
|
||
if err := rows.Scan(
|
||
&r.Country, &r.CustomerSegment, &r.MarketKey, &r.Kategori, &r.Seri,
|
||
&r.ProductCount, &r.SalesQty90, &r.SalesUSD90, &r.AvgPriceUSD90,
|
||
&r.CustomerCount90, &r.InvoiceCount90, &r.SalesQty365, &r.SalesUSD365,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func ListProductPerformanceCustomers(ctx context.Context, pg *sql.DB, breakdown string, limit int) ([]models.ProductPerformanceCustomerRow, error) {
|
||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||
return nil, err
|
||
}
|
||
if limit <= 0 || limit > 500 {
|
||
limit = 100
|
||
}
|
||
mode := strings.ToLower(strings.TrimSpace(breakdown))
|
||
if mode == "" {
|
||
mode = "market_customer"
|
||
}
|
||
selectMarket := "''"
|
||
selectCountry := "''"
|
||
selectSegment := "''"
|
||
groupCols := []string{"customer_code", "customer_name"}
|
||
switch mode {
|
||
case "country_customer":
|
||
selectCountry = "customer_country"
|
||
selectSegment = "customer_segment"
|
||
groupCols = append(groupCols, "customer_country", "customer_segment")
|
||
case "market_country_customer":
|
||
selectMarket = "market_key"
|
||
selectCountry = "customer_country"
|
||
selectSegment = "customer_segment"
|
||
groupCols = append(groupCols, "market_key", "customer_country", "customer_segment")
|
||
default:
|
||
mode = "market_customer"
|
||
selectMarket = "market_key"
|
||
groupCols = append(groupCols, "market_key")
|
||
}
|
||
|
||
query := fmt.Sprintf(`
|
||
SELECT
|
||
$2::text AS breakdown,
|
||
%s AS market_key,
|
||
%s AS country,
|
||
%s AS customer_segment,
|
||
customer_code,
|
||
customer_name,
|
||
COUNT(DISTINCT product_code) AS product_count,
|
||
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,
|
||
CASE
|
||
WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)=0 THEN 0
|
||
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(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,
|
||
COALESCE(to_char(MAX(sales_date),'YYYY-MM-DD'),'') AS last_sale_date
|
||
FROM mk_product_performance_sales_daily
|
||
WHERE sales_date >= current_date - INTERVAL '364 days'
|
||
AND COALESCE(customer_code,'') <> ''
|
||
GROUP BY %s
|
||
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
|
||
LIMIT $1
|
||
`, selectMarket, selectCountry, selectSegment, strings.Join(groupCols, ", "))
|
||
|
||
rows, err := pg.QueryContext(ctx, query, limit, mode)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := make([]models.ProductPerformanceCustomerRow, 0, limit)
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceCustomerRow
|
||
if err := rows.Scan(
|
||
&r.Breakdown, &r.MarketKey, &r.Country, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName,
|
||
&r.ProductCount, &r.SalesQty90, &r.SalesUSD90, &r.AvgPriceUSD90, &r.InvoiceCount90,
|
||
&r.SalesQty365, &r.SalesUSD365, &r.LastSaleDate,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func ListProductPerformanceSalesDetails(ctx context.Context, pg *sql.DB, productCode, colorCode, yakaKodu string, limit int) ([]models.ProductPerformanceSalesDetailRow, error) {
|
||
if limit <= 0 || limit > 500 {
|
||
limit = 100
|
||
}
|
||
rows, err := pg.QueryContext(ctx, `
|
||
SELECT
|
||
to_char(sales_date,'YYYY-MM-DD') AS sales_date,
|
||
last_ref_number,
|
||
market_key,
|
||
customer_country,
|
||
customer_segment,
|
||
customer_code,
|
||
customer_name,
|
||
sales_qty,
|
||
sales_usd,
|
||
avg_price_usd
|
||
FROM mk_product_performance_sales_daily
|
||
WHERE product_code=$1
|
||
AND color_code=$2
|
||
AND yaka_kodu=$3
|
||
ORDER BY sales_date DESC, sales_usd DESC
|
||
LIMIT $4
|
||
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu), limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := make([]models.ProductPerformanceSalesDetailRow, 0, limit)
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceSalesDetailRow
|
||
if err := rows.Scan(&r.SalesDate, &r.RefNumber, &r.MarketKey, &r.Country, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName, &r.SalesQty, &r.SalesUSD, &r.AvgPriceUSD); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func ListProductPerformanceStockSizes(ctx context.Context, productCode, colorCode, yakaKodu string) ([]models.ProductPerformanceStockSizeRow, error) {
|
||
if db.MssqlDB == nil {
|
||
return nil, fmt.Errorf("mssql db nil")
|
||
}
|
||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||
;WITH ActiveWarehouses AS (
|
||
SELECT WarehouseCode
|
||
FROM (VALUES
|
||
('1-0-12'),('1.01.2014'),('1.02.2005'),('1.02.2004'),('1-0-43'),
|
||
('4.02.2001'),('1-0-55'),('1.01.2003'),('1-0-21'),('1-0-2'),
|
||
('1.01.2004'),('1-0-49'),('1-0-37'),('1-0-29'),('1-0-28'),
|
||
('1-0-10'),('100'),('1.02.2006'),('1-0-42'),('1.01.2002'),
|
||
('1-0-52')
|
||
) W(WarehouseCode)
|
||
)
|
||
SELECT
|
||
SizeCode = LTRIM(RTRIM(ISNULL(S.ItemDim1Code, ''))),
|
||
StockQty = SUM(S.InventoryQty1)
|
||
FROM StockWithCost S WITH(NOLOCK)
|
||
INNER JOIN ActiveWarehouses W
|
||
ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
|
||
WHERE S.ItemTypeCode = 1
|
||
AND LTRIM(RTRIM(S.ItemCode)) = @p1
|
||
AND LTRIM(RTRIM(ISNULL(S.ColorCode, ''))) = @p2
|
||
AND LTRIM(RTRIM(ISNULL(S.ItemDim2Code, ''))) = @p3
|
||
GROUP BY LTRIM(RTRIM(ISNULL(S.ItemDim1Code, '')))
|
||
HAVING SUM(S.InventoryQty1) <> 0
|
||
ORDER BY LTRIM(RTRIM(ISNULL(S.ItemDim1Code, '')))
|
||
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := make([]models.ProductPerformanceStockSizeRow, 0, 32)
|
||
for rows.Next() {
|
||
var r models.ProductPerformanceStockSizeRow
|
||
if err := rows.Scan(&r.SizeCode, &r.StockQty); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func productPerformanceWhere(f ProductPerformanceFilters) (string, []any) {
|
||
parts := make([]string, 0, 6)
|
||
args := make([]any, 0, 6)
|
||
add := func(cond string, value any) {
|
||
args = append(args, value)
|
||
parts = append(parts, fmt.Sprintf(cond, len(args)))
|
||
}
|
||
if q := strings.TrimSpace(f.Search); q != "" {
|
||
args = append(args, q)
|
||
idx := len(args)
|
||
parts = append(parts, fmt.Sprintf(" AND (product_code ILIKE '%%' || $%d || '%%' OR item_description ILIKE '%%' || $%d || '%%')", idx, idx))
|
||
}
|
||
if v := strings.TrimSpace(f.ProductCode); v != "" {
|
||
add(" AND product_code = $%d", v)
|
||
}
|
||
if v := strings.TrimSpace(f.MarketKey); v != "" {
|
||
add(" AND market_key = $%d", v)
|
||
}
|
||
if v := strings.TrimSpace(f.Kategori); v != "" {
|
||
add(" AND kategori = $%d", v)
|
||
}
|
||
if v := strings.TrimSpace(f.Seri); v != "" {
|
||
args = append(args, v)
|
||
idx := len(args)
|
||
parts = append(parts, fmt.Sprintf(" AND (seri = $%d OR urun_ana_grubu = $%d)", idx, idx))
|
||
}
|
||
if v := strings.TrimSpace(f.Bucket); v != "" {
|
||
add(" AND performance_bucket = $%d", v)
|
||
}
|
||
return strings.Join(parts, ""), args
|
||
}
|
||
|
||
func productPerformanceOrderBy(sortBy string, desc bool) string {
|
||
allowed := map[string]string{
|
||
"product_code": "product_code",
|
||
"color_code": "color_code",
|
||
"yaka_kodu": "yaka_kodu",
|
||
"item_description": "item_description",
|
||
"kategori": "kategori",
|
||
"urun_ilk_grubu": "urun_ilk_grubu",
|
||
"askili_yan": "askili_yan",
|
||
"urun_ana_grubu": "urun_ana_grubu",
|
||
"urun_alt_grubu": "urun_alt_grubu",
|
||
"market_key": "market_key",
|
||
"stock_qty": "stock_qty",
|
||
"sales_qty_90d": "sales_qty_90d",
|
||
"sales_qty_180d": "sales_qty_180d",
|
||
"sales_usd_90d": "sales_usd_90d",
|
||
"sales_usd_180d": "sales_usd_180d",
|
||
"stock_days_90d": "stock_days_90d",
|
||
"stock_days_180d": "stock_days_180d",
|
||
"avg_price_usd_90d": "avg_price_usd_90d",
|
||
"avg_price_usd_180d": "avg_price_usd_180d",
|
||
"base_price_usd": "base_price_usd",
|
||
"cost_price_usd": "cost_price_usd",
|
||
"unit_profit_base_90d": "unit_profit_base_90d",
|
||
"unit_profit_cost_90d": "unit_profit_cost_90d",
|
||
"unit_profit_base_180d": "unit_profit_base_180d",
|
||
"unit_profit_cost_180d": "unit_profit_cost_180d",
|
||
"gross_margin_base_90d": "(CASE WHEN COALESCE(sales_usd_90d,0) <= 0 THEN 0 ELSE (sales_usd_90d - (sales_qty_90d * COALESCE(base_price_usd,0))) / NULLIF(sales_usd_90d,0) END)",
|
||
"gross_margin_cost_90d": "gross_margin_90d",
|
||
"gross_margin_base_180d": "(CASE WHEN COALESCE(sales_usd_180d,0) <= 0 THEN 0 ELSE (sales_usd_180d - (sales_qty_180d * COALESCE(base_price_usd,0))) / NULLIF(sales_usd_180d,0) END)",
|
||
"gross_margin_cost_180d": "gross_margin_180d",
|
||
"gross_margin_90d": "gross_margin_90d",
|
||
"gross_margin_180d": "gross_margin_180d",
|
||
"market_count_90d": "market_count_90d",
|
||
"customer_count_90d": "customer_count_90d",
|
||
"sales_index_90d": "sales_index_90d",
|
||
"performance_score": "performance_score",
|
||
"performance_bucket": "performance_bucket",
|
||
"last_sale_date": "last_sale_date",
|
||
}
|
||
col := allowed[strings.TrimSpace(sortBy)]
|
||
if col == "" {
|
||
col = "performance_score"
|
||
desc = true
|
||
}
|
||
dir := "ASC"
|
||
if desc {
|
||
dir = "DESC"
|
||
}
|
||
return col + " " + dir + ", product_code ASC, color_code ASC, yaka_kodu ASC"
|
||
}
|
||
|
||
type productPerformancePriceInfo struct {
|
||
cost float64
|
||
base float64
|
||
}
|
||
|
||
func productPerformanceVariantKey(productCode, colorCode, yakaKodu string) string {
|
||
return strings.TrimSpace(productCode) + "|" + strings.TrimSpace(colorCode) + "|" + strings.TrimSpace(yakaKodu)
|
||
}
|
||
|
||
func productPerformancePriceLookup(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]productPerformancePriceInfo, error) {
|
||
out := make(map[string]productPerformancePriceInfo, len(productCodes))
|
||
if len(productCodes) == 0 {
|
||
return out, nil
|
||
}
|
||
rows, err := pg.QueryContext(ctx, `
|
||
SELECT product_code, COALESCE(cost_price_usd,0), COALESCE(base_price_usd,0)
|
||
FROM mk_product_performance_price_dim
|
||
WHERE product_code = ANY($1)
|
||
`, pq.Array(productCodes))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
for rows.Next() {
|
||
var productCode string
|
||
var p productPerformancePriceInfo
|
||
if err := rows.Scan(&productCode, &p.cost, &p.base); err != nil {
|
||
return nil, err
|
||
}
|
||
out[productCode] = p
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func productPerformanceStockLookup(ctx context.Context, pg *sql.DB, variantKeys []string) (map[string]float64, error) {
|
||
out := make(map[string]float64, len(variantKeys))
|
||
if len(variantKeys) == 0 {
|
||
return out, nil
|
||
}
|
||
rows, err := pg.QueryContext(ctx, `
|
||
WITH Latest AS (
|
||
SELECT MAX(stock_date) AS stock_date
|
||
FROM mk_product_performance_stock_daily
|
||
)
|
||
SELECT
|
||
product_code || '|' || color_code || '|' || yaka_kodu AS variant_key,
|
||
COALESCE(SUM(stock_qty),0) AS stock_qty
|
||
FROM mk_product_performance_stock_daily
|
||
WHERE stock_date = (SELECT stock_date FROM Latest)
|
||
AND product_code || '|' || color_code || '|' || yaka_kodu = ANY($1)
|
||
GROUP BY product_code, color_code, yaka_kodu
|
||
`, pq.Array(variantKeys))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
for rows.Next() {
|
||
var key string
|
||
var stockQty float64
|
||
if err := rows.Scan(&key, &stockQty); err != nil {
|
||
return nil, err
|
||
}
|
||
out[key] = stockQty
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func productPerformanceOrderRecommendation(row models.ProductPerformanceOrderAnalysisRow) (string, string) {
|
||
switch {
|
||
case row.OrderQty > 0 && row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||
return "STOKSUZ_TALEP", "Açık sipariş stoktan büyük. Karlı talep var; üretim/satın alma önceliği ver."
|
||
case row.ExpectedProfitCostUSD < 0:
|
||
return "FIYAT_BASKISI", "Açık sipariş çıplak maliyete göre zarar yazıyor. Fiyat/maliyet kontrol edilmeli."
|
||
case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder >= 0:
|
||
return "YILDIZ_URUN", "Açık sipariş karlı ve stok karşılıyor. Teslimat korunmalı."
|
||
case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder < 0:
|
||
return "FIYAT_FIRSATI", "Karlı talep var ama stok yetersiz. Üretim planına alınmalı."
|
||
case row.OverdueQty > 0:
|
||
return "TAKIP", "Termin gecikmesi olan açık sipariş var. Operasyon takibi gerekli."
|
||
default:
|
||
return "TAKIP", "Sipariş, stok ve fiyat düzenli izlenmeli."
|
||
}
|
||
}
|
||
|
||
func productPerformanceOrderGroupRecommendation(row models.ProductPerformanceOrderGroupRow) (string, string) {
|
||
switch {
|
||
case row.ExpectedProfitCostUSD < 0:
|
||
return "FIYAT_BASKISI", "Çıplak maliyete göre zarar yazan açık sipariş var. Fiyat/maliyet acil kontrol edilmeli."
|
||
case row.ExpectedProfitBaseUSD < 0:
|
||
return "FIYAT_BASKISI", "Taban maliyete göre brüt zarar var. Satış fiyatı veya iskonto kontrol edilmeli."
|
||
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||
return "STOKSUZ_TALEP", "Karlı açık talep var ama stok yetersiz. Üretim/satın alma önceliği ver."
|
||
case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
|
||
return "YILDIZ_URUN", "Piyasa/müşteri açık siparişleri karlı. Teslimat ve stok korunmalı."
|
||
case row.OverdueQty > 0:
|
||
return "TAKIP", "Geciken açık sipariş var. Operasyon takibi gerekli."
|
||
default:
|
||
return "TAKIP", "Sipariş karlılığı ve stok yeterliliği izlenmeli."
|
||
}
|
||
}
|
||
|
||
func productPerformanceOrderProductCustomerRecommendation(row models.ProductPerformanceOrderProductCustomerRow) (string, string) {
|
||
switch {
|
||
case row.ExpectedProfitCostUSD < 0:
|
||
return "FIYAT_BASKISI", "Bu müşteri/ürün açık siparişi çıplak maliyete göre zarar yazıyor."
|
||
case row.ExpectedProfitBaseUSD < 0:
|
||
return "FIYAT_BASKISI", "Bu müşteri/ürün açık siparişi taban maliyete göre brüt zarar yazıyor."
|
||
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||
return "STOKSUZ_TALEP", "Müşteride karlı talep var ama stok yetersiz."
|
||
case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
|
||
return "YILDIZ_URUN", "Müşteri bazında karlı açık talep var."
|
||
case row.OverdueQty > 0:
|
||
return "TAKIP", "Bu müşteri/ürün kırılımında geciken açık sipariş var."
|
||
default:
|
||
return "TAKIP", "Müşteri talebi, fiyat ve stok birlikte izlenmeli."
|
||
}
|
||
}
|
||
|
||
func productPerformanceOrderMarketDetailRecommendation(row models.ProductPerformanceOrderMarketDetailRow) (string, string) {
|
||
switch {
|
||
case row.ExpectedProfitCostUSD < 0:
|
||
return "FIYAT_BASKISI", "Sipariş satırı çıplak maliyete göre zarar yazıyor."
|
||
case row.ExpectedProfitBaseUSD < 0:
|
||
return "FIYAT_BASKISI", "Sipariş satırı taban maliyete göre brüt zarar yazıyor."
|
||
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||
return "STOKSUZ_TALEP", "Karlı sipariş var ama mevcut stok siparişi karşılamıyor."
|
||
case row.IsOverdue:
|
||
return "TAKIP", "Termin geçmiş; teslimat aksiyonu gerekli."
|
||
case row.ExpectedMarginCost >= 0.25:
|
||
return "YILDIZ_URUN", "Sipariş karlı; teslimat ve stok korunmalı."
|
||
default:
|
||
return "TAKIP", "Sipariş fiyatı, stok ve termin izlenmeli."
|
||
}
|
||
}
|
||
|
||
func dateOnly(t time.Time) time.Time {
|
||
y, m, d := t.Date()
|
||
return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
|
||
}
|
||
|
||
type productPerformanceSalesDaily struct {
|
||
SalesDate time.Time
|
||
ProductCode string
|
||
ColorCode string
|
||
YakaKodu string
|
||
ItemDescription string
|
||
Kategori string
|
||
Seri string
|
||
YasGrubu string
|
||
AskiliYan string
|
||
UrunIlkGrubu string
|
||
UrunAnaGrubu string
|
||
UrunAltGrubu string
|
||
MarketKey string
|
||
ChannelCode string
|
||
CustomerCountry string
|
||
CustomerSegment string
|
||
CustomerCode string
|
||
CustomerName string
|
||
SalesQty float64
|
||
SalesTL float64
|
||
SalesUSD float64
|
||
AvgPriceUSD float64
|
||
InvoiceLineCount int
|
||
InvoiceCount int
|
||
CustomerCount int
|
||
LastRefNumber string
|
||
}
|
||
|
||
type productPerformanceStockDaily struct {
|
||
StockDate time.Time
|
||
ProductCode string
|
||
ColorCode string
|
||
YakaKodu string
|
||
StockQty float64
|
||
InQty float64
|
||
OutQty float64
|
||
KpiInQty float64
|
||
KpiOutQty float64
|
||
SalesMovementQty float64
|
||
ProductionInQty float64
|
||
PurchaseInQty float64
|
||
ConsumptionOutQty float64
|
||
CountDiffQty float64
|
||
}
|