ui: update ProductPerformanceProfitability page to enhance table views and tabs management
This commit is contained in:
+1145
-24
@@ -7,8 +7,11 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type ProductPerformanceFilters struct {
|
||||
@@ -25,12 +28,13 @@ type ProductPerformanceFilters struct {
|
||||
}
|
||||
|
||||
type ProductPerformanceRefreshRequest struct {
|
||||
Mode string
|
||||
Stage string
|
||||
ResumeAfter int
|
||||
SkipDelete bool
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
Mode string
|
||||
Stage string
|
||||
ResumeAfter int
|
||||
SkipDelete bool
|
||||
ProductPrefix string
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
}
|
||||
|
||||
type ProductPerformanceRefreshResult struct {
|
||||
@@ -219,7 +223,8 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
||||
func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerformanceRefreshRequest) (ProductPerformanceRefreshResult, error) {
|
||||
started := time.Now()
|
||||
stage := productPerformanceRefreshStage(req.Stage)
|
||||
log.Printf("[ProductPerformanceRefresh] start mode=%s stage=%s start=%s end=%s", req.Mode, stage, req.StartDate.Format("2006-01-02"), req.EndDate.Format("2006-01-02"))
|
||||
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")
|
||||
}
|
||||
@@ -282,12 +287,18 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
|
||||
if shouldRun("sales") {
|
||||
if err := runStage("sales", func(tx *sql.Tx) error {
|
||||
log.Printf("[ProductPerformanceRefresh] delete sales cache start")
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM mk_product_performance_sales_daily WHERE sales_date BETWEEN $1 AND $2`, req.StartDate, req.EndDate); err != nil {
|
||||
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)
|
||||
rows, err := refreshProductPerformanceSales(ctx, tx, req.StartDate, req.EndDate, productPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -303,7 +314,7 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
|
||||
|
||||
var stockRows int
|
||||
if shouldRun("stock") {
|
||||
rows, err := refreshProductPerformanceStockChunked(ctx, pg, req.StartDate, req.EndDate, started, req.SkipDelete || req.ResumeAfter > 0, req.ResumeAfter)
|
||||
rows, err := refreshProductPerformanceStockChunked(ctx, pg, req.StartDate, req.EndDate, started, req.SkipDelete || req.ResumeAfter > 0, req.ResumeAfter, productPrefix)
|
||||
if err != nil {
|
||||
return ProductPerformanceRefreshResult{}, err
|
||||
}
|
||||
@@ -315,7 +326,7 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
|
||||
if shouldRun("price") {
|
||||
if err := runStage("price", func(tx *sql.Tx) error {
|
||||
log.Printf("[ProductPerformanceRefresh] price refresh start")
|
||||
if err := refreshProductPerformancePrices(ctx, tx); err != nil {
|
||||
if err := refreshProductPerformancePrices(ctx, tx, productPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[ProductPerformanceRefresh] price refresh done elapsed=%s", time.Since(started).Round(time.Second))
|
||||
@@ -369,9 +380,19 @@ func productPerformanceRefreshStage(raw string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func refreshProductPerformanceSales(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time) (int, error) {
|
||||
log.Printf("[ProductPerformanceRefresh] sales mssql query start start=%s end=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceSalesSQL(), startDate, endDate)
|
||||
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
|
||||
}
|
||||
@@ -432,9 +453,9 @@ DO UPDATE SET
|
||||
return count, rows.Err()
|
||||
}
|
||||
|
||||
func refreshProductPerformanceStock(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time) (int, error) {
|
||||
log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockSQL(), startDate, endDate)
|
||||
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, productPerformanceStockSQL(), startDate, endDate, productPrefix)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -461,7 +482,7 @@ func refreshProductPerformanceStock(ctx context.Context, tx *sql.Tx, startDate,
|
||||
return count, rows.Err()
|
||||
}
|
||||
|
||||
func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, startDate, endDate, started time.Time, skipDelete bool, resumeAfter int) (int, error) {
|
||||
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 {
|
||||
@@ -472,7 +493,13 @@ func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, star
|
||||
}
|
||||
defer deleteTx.Rollback()
|
||||
log.Printf("[ProductPerformanceRefresh] delete stock cache start")
|
||||
if _, err := deleteTx.ExecContext(ctx, `DELETE FROM mk_product_performance_stock_daily WHERE stock_date BETWEEN $1 AND $2`, startDate, endDate); err != nil {
|
||||
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")
|
||||
@@ -484,8 +511,8 @@ func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, star
|
||||
}
|
||||
|
||||
log.Printf("[ProductPerformanceRefresh] stock refresh start")
|
||||
log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockSQL(), startDate, endDate)
|
||||
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, productPerformanceStockSQL(), startDate, endDate, productPrefix)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -582,9 +609,9 @@ DO UPDATE SET
|
||||
return err
|
||||
}
|
||||
|
||||
func refreshProductPerformancePrices(ctx context.Context, tx *sql.Tx) error {
|
||||
log.Printf("[ProductPerformanceRefresh] price mssql query start")
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformancePriceSQL())
|
||||
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
|
||||
}
|
||||
@@ -742,6 +769,965 @@ CROSS JOIN Sales90
|
||||
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
|
||||
),
|
||||
StockOnly AS (
|
||||
SELECT
|
||||
s.product_code,
|
||||
s.color_code,
|
||||
s.yaka_kodu,
|
||||
'STOK'::text AS market_key,
|
||||
MAX(s.item_description) AS item_description,
|
||||
MAX(s.kategori) AS kategori,
|
||||
MAX(s.seri) AS seri,
|
||||
MAX(s.yas_grubu) AS yas_grubu,
|
||||
MAX(s.askili_yan) AS askili_yan,
|
||||
MAX(s.urun_ilk_grubu) AS urun_ilk_grubu,
|
||||
MAX(s.urun_ana_grubu) AS urun_ana_grubu,
|
||||
MAX(s.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
|
||||
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
|
||||
@@ -1076,6 +2062,141 @@ func productPerformanceOrderBy(sortBy string, desc bool) string {
|
||||
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())
|
||||
|
||||
@@ -153,7 +153,18 @@ func productPerformanceSalesSQL() string {
|
||||
WHERE I.InvoiceDate >= @p1
|
||||
AND I.InvoiceDate < DATEADD(DAY, 1, @p2)
|
||||
AND I.ItemTypeCode = 1
|
||||
AND (I.ItemCode LIKE 'S%' OR I.ItemCode LIKE 'O%' OR I.ItemCode LIKE 'X%' OR I.ItemCode LIKE 'N%')
|
||||
AND (
|
||||
LEN(LTRIM(RTRIM(I.ItemCode))) = 13
|
||||
AND (
|
||||
I.ItemCode LIKE 'S%'
|
||||
OR I.ItemCode LIKE 'O%'
|
||||
OR I.ItemCode LIKE 'N%'
|
||||
OR I.ItemCode LIKE 'X%'
|
||||
OR I.ItemCode LIKE 'I%'
|
||||
OR I.ItemCode LIKE 'A%'
|
||||
)
|
||||
)
|
||||
AND (@p3 = '' OR I.ItemCode LIKE @p3 + '%')
|
||||
AND (
|
||||
(I.CompanyCode = 1 AND I.ATAtt01 IN (1,2) AND I.ProcessCode IN ('WS','R'))
|
||||
OR (I.CompanyCode = 4 AND I.ProcessCode = 'R')
|
||||
@@ -228,7 +239,18 @@ Raw AS (
|
||||
INNER JOIN ActiveWarehouses W
|
||||
ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
|
||||
WHERE S.ItemTypeCode = 1
|
||||
AND (S.ItemCode LIKE 'S%' OR S.ItemCode LIKE 'O%' OR S.ItemCode LIKE 'X%' OR S.ItemCode LIKE 'N%')
|
||||
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,
|
||||
@@ -334,12 +356,34 @@ func productPerformancePriceSQL() string {
|
||||
SELECT DISTINCT ItemCode
|
||||
FROM trPriceListLine WITH(NOLOCK)
|
||||
WHERE ItemTypeCode = 1
|
||||
AND (ItemCode LIKE 'S%' OR ItemCode LIKE 'O%' OR ItemCode LIKE 'X%' OR ItemCode LIKE 'N%')
|
||||
AND (
|
||||
LEN(LTRIM(RTRIM(ItemCode))) = 13
|
||||
AND (
|
||||
ItemCode LIKE 'S%'
|
||||
OR ItemCode LIKE 'O%'
|
||||
OR ItemCode LIKE 'N%'
|
||||
OR ItemCode LIKE 'X%'
|
||||
OR ItemCode LIKE 'I%'
|
||||
OR ItemCode LIKE 'A%'
|
||||
)
|
||||
)
|
||||
AND (@p1 = '' OR ItemCode LIKE @p1 + '%')
|
||||
UNION
|
||||
SELECT DISTINCT ItemCode
|
||||
FROM prItemBasePrice WITH(NOLOCK)
|
||||
WHERE ItemTypeCode = 1
|
||||
AND (ItemCode LIKE 'S%' OR ItemCode LIKE 'O%' OR ItemCode LIKE 'X%' OR ItemCode LIKE 'N%')
|
||||
AND (
|
||||
LEN(LTRIM(RTRIM(ItemCode))) = 13
|
||||
AND (
|
||||
ItemCode LIKE 'S%'
|
||||
OR ItemCode LIKE 'O%'
|
||||
OR ItemCode LIKE 'N%'
|
||||
OR ItemCode LIKE 'X%'
|
||||
OR ItemCode LIKE 'I%'
|
||||
OR ItemCode LIKE 'A%'
|
||||
)
|
||||
)
|
||||
AND (@p1 = '' OR ItemCode LIKE @p1 + '%')
|
||||
),
|
||||
LatestPrice AS (
|
||||
SELECT
|
||||
|
||||
Reference in New Issue
Block a user