Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-02 16:14:54 +03:00
parent 5f3e975b6d
commit b4e87cfd47
25 changed files with 4918 additions and 287 deletions

View File

@@ -10,6 +10,8 @@ import (
"strconv"
"strings"
"time"
"github.com/lib/pq"
)
type ProductPricingFilters struct {
@@ -34,7 +36,7 @@ type ProductPricingPage struct {
Limit int
}
func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters) (ProductPricingPage, error) {
func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (ProductPricingPage, error) {
result := ProductPricingPage{
Rows: []models.ProductPricing{},
TotalCount: 0,
@@ -114,34 +116,53 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
}
whereSQL := strings.Join(whereParts, " AND ")
countQuery := `
SELECT COUNT(DISTINCT LTRIM(RTRIM(ProductCode)))
FROM ProductFilterWithDescription('TR')
WHERE ` + whereSQL + `;
`
var totalCount int
if err := db.MssqlDB.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil {
return result, err
}
result.TotalCount = totalCount
if totalCount == 0 {
if includeTotal {
countQuery := `
SELECT COUNT(DISTINCT LTRIM(RTRIM(ProductCode)))
FROM ProductFilterWithDescription('TR')
WHERE ` + whereSQL + `;
`
var totalCount int
if err := db.MssqlDB.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil {
return result, err
}
result.TotalCount = totalCount
if totalCount == 0 {
result.TotalPages = 0
result.Page = 1
return result, nil
}
totalPages := int(math.Ceil(float64(totalCount) / float64(limit)))
if totalPages <= 0 {
totalPages = 1
}
if page > totalPages {
page = totalPages
offset = (page - 1) * limit
}
result.Page = page
result.Limit = limit
result.TotalPages = totalPages
} else {
// Skip COUNT(*) for performance; client will infer hasMore from page size.
result.TotalCount = 0
result.TotalPages = 0
result.Page = 1
return result, nil
result.Page = page
result.Limit = limit
}
totalPages := int(math.Ceil(float64(totalCount) / float64(limit)))
if totalPages <= 0 {
totalPages = 1
}
if page > totalPages {
page = totalPages
offset = (page - 1) * limit
}
result.Page = page
result.Limit = limit
result.TotalPages = totalPages
// Stage 1: fetch only paged products first (fast path).
sortBy = strings.TrimSpace(sortBy)
orderDir := "DESC"
if !descending {
orderDir = "ASC"
}
// Only allow a small safe list.
orderExpr := "CAST(ROUND(ISNULL(sb.InventoryQty1, 0), 2) AS DECIMAL(18, 2))"
if sortBy == "productCode" {
orderExpr = "rc.ProductCode"
orderDir = "ASC"
}
productQuery := `
IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes;
IF OBJECT_ID('tempdb..#stock_base') IS NOT NULL DROP TABLE #stock_base;
@@ -156,7 +177,8 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
MAX(f.UrunAltGrubu) AS UrunAltGrubu,
MAX(f.Icerik) AS Icerik,
MAX(f.Karisim) AS Karisim,
MAX(f.Marka) AS Marka
MAX(f.Marka) AS Marka,
MAX(f.BrandCode) AS BrandCode
INTO #req_codes
FROM (
SELECT
@@ -169,7 +191,8 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu,
COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik,
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka,
COALESCE(LTRIM(RTRIM(ProductAtt10)), '') AS BrandCode
FROM ProductFilterWithDescription('TR')
WHERE ` + whereSQL + `
) f
@@ -200,12 +223,13 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
rc.UrunAltGrubu,
rc.Icerik,
rc.Karisim,
rc.Marka
rc.Marka,
rc.BrandCode
FROM #req_codes rc
LEFT JOIN #stock_base sb
ON sb.ItemCode = rc.ProductCode
ORDER BY
CAST(ROUND(ISNULL(sb.InventoryQty1, 0), 2) AS DECIMAL(18, 2)) DESC,
` + orderExpr + ` ` + orderDir + `,
rc.ProductCode ASC
OFFSET ` + strconv.Itoa(offset) + ` ROWS
FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY;
@@ -252,6 +276,7 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
&item.Icerik,
&item.Karisim,
&item.Marka,
&item.BrandCode,
); err != nil {
return result, err
}
@@ -284,6 +309,39 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(ProductCode)
WHERE LEN(LTRIM(RTRIM(v.ProductCode))) > 0
),
latest_pricelist_line AS (
-- Base prices from Nebim V3 price lists (trPriceListLine).
-- Pick the latest record per (ItemCode, Currency) using ValidDate/ValidTime, then LastUpdatedDate.
SELECT
LTRIM(RTRIM(p.ItemCode)) AS ItemCode,
LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode,
CAST(p.Price AS DECIMAL(18, 2)) AS Price,
ROW_NUMBER() OVER (
PARTITION BY LTRIM(RTRIM(p.ItemCode)), LTRIM(RTRIM(p.DocCurrencyCode))
ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC
) AS rn
FROM dbo.trPriceListLine p WITH(NOLOCK)
INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode))
WHERE p.ItemTypeCode = 1
AND ISNULL(p.IsDisabled, 0) = 0
AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY')
AND (
(LTRIM(RTRIM(p.DocCurrencyCode)) = 'USD' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-USD')
OR (LTRIM(RTRIM(p.DocCurrencyCode)) = 'TRY' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-TRY')
)
AND p.Price IS NOT NULL
AND p.Price > 0
),
base_prices AS (
SELECT
ItemCode,
MAX(CASE WHEN DocCurrencyCode = 'USD' THEN Price END) AS BasePriceUsd,
MAX(CASE WHEN DocCurrencyCode = 'TRY' THEN Price END) AS BasePriceTry
FROM latest_pricelist_line
WHERE rn = 1
GROUP BY ItemCode
),
latest_base_price AS (
SELECT
LTRIM(RTRIM(b.ItemCode)) AS ItemCode,
@@ -365,6 +423,8 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
SELECT
rc.ProductCode,
COALESCE(lp.CostPrice, 0) AS CostPrice,
COALESCE(bp.BasePriceUsd, 0) AS BasePriceUsd,
COALESCE(bp.BasePriceTry, 0) AS BasePriceTry,
CAST(ROUND(
ISNULL(sb.InventoryQty1, 0)
- ISNULL(pb.PickingQty1, 0)
@@ -372,11 +432,14 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
- ISNULL(db.DispOrderQty1, 0)
, 2) AS DECIMAL(18, 2)) AS StockQty,
COALESCE(se.StockEntryDate, '') AS StockEntryDate,
'' AS LastCostingDate,
COALESCE(lp.LastPricingDate, '') AS LastPricingDate
FROM req_codes rc
LEFT JOIN latest_base_price lp
ON lp.ItemCode = rc.ProductCode
AND lp.rn = 1
LEFT JOIN base_prices bp
ON bp.ItemCode = rc.ProductCode
LEFT JOIN stock_entry_dates se
ON se.ItemCode = rc.ProductCode
LEFT JOIN stock_base sb
@@ -397,8 +460,11 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
type metrics struct {
CostPrice float64
BasePriceUsd float64
BasePriceTry float64
StockQty float64
StockEntryDate string
LastCostingDate string
LastPricingDate string
}
metricsByCode := make(map[string]metrics, len(out))
@@ -410,8 +476,11 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
if err := metricsRows.Scan(
&code,
&m.CostPrice,
&m.BasePriceUsd,
&m.BasePriceTry,
&m.StockQty,
&m.StockEntryDate,
&m.LastCostingDate,
&m.LastPricingDate,
); err != nil {
return result, err
@@ -425,12 +494,222 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
for i := range out {
if m, ok := metricsByCode[strings.TrimSpace(out[i].ProductCode)]; ok {
out[i].CostPrice = m.CostPrice
out[i].BasePriceUsd = m.BasePriceUsd
out[i].BasePriceTry = m.BasePriceTry
out[i].StockQty = m.StockQty
out[i].StockEntryDate = m.StockEntryDate
out[i].LastCostingDate = m.LastCostingDate
out[i].LastPricingDate = m.LastPricingDate
}
}
// Stage 3: fetch costing date from UretimDB (separate MSSQL catalog).
// Pricing DB may not contain spUrtOnMLMas; do not fail listing on costing query errors.
if uretimDB := db.GetUretimDB(); uretimDB != nil {
costingQuery := `
WITH req_codes AS (
SELECT DISTINCT LTRIM(RTRIM(v.ProductCode)) AS ProductCode
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(ProductCode)
WHERE LEN(LTRIM(RTRIM(v.ProductCode))) > 0
)
SELECT
LTRIM(RTRIM(m.UrunKodu)) AS UrunKodu,
CONVERT(VARCHAR(10), MAX(m.Tarihi), 23) AS LastCostingDate
FROM dbo.spUrtOnMLMas m WITH(NOLOCK)
INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(m.UrunKodu))
GROUP BY LTRIM(RTRIM(m.UrunKodu));
`
costRows, err := uretimDB.QueryContext(ctx, costingQuery, metricArgs...)
if err == nil {
costingByCode := make(map[string]string, len(out))
for costRows.Next() {
var code, d string
if err := costRows.Scan(&code, &d); err != nil {
_ = costRows.Close()
costRows = nil
break
}
costingByCode[strings.TrimSpace(code)] = strings.TrimSpace(d)
}
if costRows != nil {
_ = costRows.Close()
for i := range out {
if d, ok := costingByCode[strings.TrimSpace(out[i].ProductCode)]; ok && d != "" {
out[i].LastCostingDate = d
}
}
}
}
}
// Stage 4: fetch latest tier prices (USD1..6, EUR1..6, TRY1..6) from PostgreSQL sdprc/mmitem.
if pg := db.PgDB; pg != nil {
type tierRow struct {
Code string
Grp int
Crn string
Prc float64
}
tierSQL := `
WITH ranked AS (
SELECT
mmitem.code AS code,
sdprc.sdprcgrp_id AS grp,
sdprc.crn AS crn,
COALESCE(sdprc.prc, 0) AS prc,
ROW_NUMBER() OVER (
PARTITION BY mmitem.code, sdprc.crn, sdprc.sdprcgrp_id
ORDER BY sdprc.zlins_dttm DESC
) AS rn
FROM sdprc
JOIN mmitem ON mmitem.id = sdprc.mmitem_id
WHERE mmitem.code = ANY($1)
AND sdprc.sdprcgrp_id BETWEEN 1 AND 6
AND sdprc.crn IN ('USD', 'EUR', 'TRY')
AND sdprc.prc IS NOT NULL
AND sdprc.prc > 0
)
SELECT code, grp, crn, prc
FROM ranked
WHERE rn = 1;
`
rows, err := pg.QueryContext(ctx, tierSQL, pq.Array(codes))
if err == nil {
defer rows.Close()
type key struct {
Code string
}
tiers := make(map[string]map[string]map[int]float64, len(out))
for rows.Next() {
var r tierRow
if err := rows.Scan(&r.Code, &r.Grp, &r.Crn, &r.Prc); err != nil {
break
}
r.Code = strings.TrimSpace(r.Code)
r.Crn = strings.TrimSpace(strings.ToUpper(r.Crn))
if r.Code == "" || r.Grp < 1 || r.Grp > 6 || r.Crn == "" {
continue
}
if _, ok := tiers[r.Code]; !ok {
tiers[r.Code] = map[string]map[int]float64{}
}
if _, ok := tiers[r.Code][r.Crn]; !ok {
tiers[r.Code][r.Crn] = map[int]float64{}
}
tiers[r.Code][r.Crn][r.Grp] = r.Prc
}
for i := range out {
code := strings.TrimSpace(out[i].ProductCode)
m, ok := tiers[code]
if !ok {
continue
}
apply := func(crn string, grp int, v float64) {
switch crn {
case "USD":
switch grp {
case 1:
out[i].USD1 = v
case 2:
out[i].USD2 = v
case 3:
out[i].USD3 = v
case 4:
out[i].USD4 = v
case 5:
out[i].USD5 = v
case 6:
out[i].USD6 = v
}
case "EUR":
switch grp {
case 1:
out[i].EUR1 = v
case 2:
out[i].EUR2 = v
case 3:
out[i].EUR3 = v
case 4:
out[i].EUR4 = v
case 5:
out[i].EUR5 = v
case 6:
out[i].EUR6 = v
}
case "TRY":
switch grp {
case 1:
out[i].TRY1 = v
case 2:
out[i].TRY2 = v
case 3:
out[i].TRY3 = v
case 4:
out[i].TRY4 = v
case 5:
out[i].TRY5 = v
case 6:
out[i].TRY6 = v
}
}
}
for crn, byGrp := range m {
for grp, v := range byGrp {
apply(crn, grp, v)
}
}
}
}
}
// Stage 5: brand group (classification) from Postgres mk_brandgrpmatch.
// Show classification result in BrandGroupSec field (read-only in UI).
if pg := db.PgDB; pg != nil {
brandCodes := make([]string, 0, len(out))
seen := make(map[string]struct{}, len(out))
for _, it := range out {
code := strings.TrimSpace(it.BrandCode)
if code == "" {
continue
}
if _, ok := seen[code]; ok {
continue
}
seen[code] = struct{}{}
brandCodes = append(brandCodes, code)
}
if len(brandCodes) > 0 {
rows, err := pg.QueryContext(ctx, `
SELECT
m.brand_code,
COALESCE(g.title, '') AS grp_title
FROM mk_brandgrpmatch m
JOIN mk_brandgrp g ON g.id = m.grp_id
WHERE m.brand_code = ANY($1)
`, pq.Array(brandCodes))
if err == nil {
defer rows.Close()
grpByBrand := make(map[string]string, len(brandCodes))
for rows.Next() {
var code, title string
if err := rows.Scan(&code, &title); err != nil {
break
}
grpByBrand[strings.TrimSpace(code)] = strings.TrimSpace(title)
}
for i := range out {
if title, ok := grpByBrand[strings.TrimSpace(out[i].BrandCode)]; ok {
out[i].BrandGroupSec = title
} else {
out[i].BrandGroupSec = ""
}
}
}
}
}
result.Rows = out
return result, nil
}