Merge remote-tracking branch 'origin/master'
This commit is contained in:
86
svc/queries/last10_avg_purchase_price_cache.go
Normal file
86
svc/queries/last10_avg_purchase_price_cache.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Last10AvgPurchasePriceRow struct {
|
||||
ItemCode string
|
||||
CurrencyCode string
|
||||
SampleCount int
|
||||
AvgDocPrice float64
|
||||
MinInvoiceDate sql.NullString
|
||||
MaxInvoiceDate sql.NullString
|
||||
}
|
||||
|
||||
// LookupLast10AvgPurchasePriceByItemCodes reads from dbo.cache_last10_avg_purchase_price (Nebim/V3 MSSQL).
|
||||
// It is designed to be used in hot paths (save) where live invoice scans are too slow.
|
||||
func LookupLast10AvgPurchasePriceByItemCodes(ctx context.Context, mssqlDB *sql.DB, itemCodes []string) ([]Last10AvgPurchasePriceRow, error) {
|
||||
if mssqlDB == nil {
|
||||
return nil, fmt.Errorf("mssql db is nil")
|
||||
}
|
||||
codes := make([]string, 0, len(itemCodes))
|
||||
seen := map[string]struct{}{}
|
||||
for _, c := range itemCodes {
|
||||
c = strings.TrimSpace(c)
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[c]; ok {
|
||||
continue
|
||||
}
|
||||
seen[c] = struct{}{}
|
||||
codes = append(codes, c)
|
||||
}
|
||||
if len(codes) == 0 {
|
||||
return []Last10AvgPurchasePriceRow{}, nil
|
||||
}
|
||||
|
||||
valParts := make([]string, 0, len(codes))
|
||||
args := make([]any, 0, len(codes))
|
||||
for i, code := range codes {
|
||||
valParts = append(valParts, fmt.Sprintf("(@p%d)", i+1))
|
||||
args = append(args, code)
|
||||
}
|
||||
|
||||
sqlText := fmt.Sprintf(`
|
||||
WITH C AS (
|
||||
SELECT LTRIM(RTRIM(V.code)) AS ItemCode
|
||||
FROM (VALUES %s) AS V(code)
|
||||
)
|
||||
SELECT
|
||||
T.ItemCode,
|
||||
T.Doc_CurrencyCode,
|
||||
T.sample_count,
|
||||
T.avg_doc_price,
|
||||
CONVERT(varchar(10), T.min_invoice_date, 23) AS min_invoice_date,
|
||||
CONVERT(varchar(10), T.max_invoice_date, 23) AS max_invoice_date
|
||||
FROM dbo.cache_last10_avg_purchase_price T WITH (NOLOCK)
|
||||
INNER JOIN C
|
||||
ON C.ItemCode = T.ItemCode
|
||||
`, strings.Join(valParts, ","))
|
||||
|
||||
rows, err := mssqlDB.QueryContext(ctx, sqlText, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]Last10AvgPurchasePriceRow, 0, len(codes))
|
||||
for rows.Next() {
|
||||
var r Last10AvgPurchasePriceRow
|
||||
if err := rows.Scan(&r.ItemCode, &r.CurrencyCode, &r.SampleCount, &r.AvgDocPrice, &r.MinInvoiceDate, &r.MaxInvoiceDate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.ItemCode = strings.TrimSpace(r.ItemCode)
|
||||
r.CurrencyCode = strings.TrimSpace(strings.ToUpper(r.CurrencyCode))
|
||||
out = append(out, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
33
svc/queries/product_first_group_by_product_code.go
Normal file
33
svc/queries/product_first_group_by_product_code.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetProductFirstGroupCodeDescByUrunKodu resolves (ProductAtt42, ProductAtt42Desc) for a given product code.
|
||||
func GetProductFirstGroupCodeDescByUrunKodu(ctx context.Context, mssqlDB *sql.DB, urunKodu string) (code string, title string, err error) {
|
||||
urunKodu = strings.TrimSpace(urunKodu)
|
||||
if mssqlDB == nil || urunKodu == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
sqlText := `
|
||||
SELECT TOP 1
|
||||
LTRIM(RTRIM(ISNULL(ProductAtt42, ''))) AS group_code,
|
||||
LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))) AS group_title
|
||||
FROM ProductFilterWithDescription('TR')
|
||||
WHERE IsBlocked = 0
|
||||
AND LTRIM(RTRIM(ProductCode)) = @p1
|
||||
ORDER BY ProductCode;
|
||||
`
|
||||
row := mssqlDB.QueryRowContext(ctx, sqlText, urunKodu)
|
||||
if err := row.Scan(&code, &title); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", "", nil
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
return strings.TrimSpace(code), strings.TrimSpace(title), nil
|
||||
}
|
||||
43
svc/queries/product_first_group_code_desc.go
Normal file
43
svc/queries/product_first_group_code_desc.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ListProductFirstGroupCodeDescOptions returns distinct (ProductAtt42, ProductAtt42Desc) values from Nebim V3.
|
||||
// ProductAtt42: first group code
|
||||
// ProductAtt42Desc: first group description
|
||||
func ListProductFirstGroupCodeDescOptions(ctx context.Context, mssqlDB *sql.DB, search string, limit int) (*sql.Rows, error) {
|
||||
search = strings.TrimSpace(search)
|
||||
if mssqlDB == nil {
|
||||
return nil, sql.ErrConnDone
|
||||
}
|
||||
if limit <= 0 || limit > 5000 {
|
||||
limit = 5000
|
||||
}
|
||||
|
||||
sqlText := `
|
||||
SELECT TOP (@p2)
|
||||
LTRIM(RTRIM(ISNULL(ProductAtt42, ''))) AS group_code,
|
||||
LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))) AS group_title
|
||||
FROM ProductFilterWithDescription('TR')
|
||||
WHERE IsBlocked = 0
|
||||
AND LTRIM(RTRIM(ISNULL(ProductAtt42, ''))) <> ''
|
||||
AND LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))) <> ''
|
||||
AND (
|
||||
@p1 = ''
|
||||
OR LTRIM(RTRIM(ISNULL(ProductAtt42, ''))) LIKE '%' + @p1 + '%'
|
||||
OR LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))) LIKE '%' + @p1 + '%'
|
||||
)
|
||||
GROUP BY
|
||||
LTRIM(RTRIM(ISNULL(ProductAtt42, ''))),
|
||||
LTRIM(RTRIM(ISNULL(ProductAtt42Desc, '')))
|
||||
ORDER BY
|
||||
LTRIM(RTRIM(ISNULL(ProductAtt42Desc, ''))),
|
||||
LTRIM(RTRIM(ISNULL(ProductAtt42, '')));
|
||||
`
|
||||
|
||||
return mssqlDB.QueryContext(ctx, sqlText, search, limit)
|
||||
}
|
||||
@@ -912,10 +912,10 @@ func GetProductionHasCostDetailRowsByOnMLNo(
|
||||
nOnMLNo int,
|
||||
) (*sql.Rows, error) {
|
||||
sqlText := `
|
||||
SELECT
|
||||
-- Prefer the group label stored on the OnML detail row (D.sAciklama3),
|
||||
-- because some hammadde type master rows may have empty/legacy group labels.
|
||||
ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ')) AS sAciklama3,
|
||||
SELECT
|
||||
-- Prefer the group label stored on the OnML detail row (D.sAciklama3),
|
||||
-- because some hammadde type master rows may have empty/legacy group labels.
|
||||
ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ')) AS sAciklama3,
|
||||
SUM(ISNULL(D.lTutar, 0)) OVER (
|
||||
PARTITION BY ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ'))
|
||||
) AS GroupTotalTutar,
|
||||
@@ -924,14 +924,14 @@ func GetProductionHasCostDetailRowsByOnMLNo(
|
||||
) AS GroupTotalUSDTutar,
|
||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLNo, 0))) AS nOnMLNo,
|
||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLDetNo, 0))) AS nOnMLDetNo,
|
||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
|
||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nUrtMTBolumID, 0))) AS nUrtMTBolumID,
|
||||
-- Normalize code to variantless (tbStok.sModel) when D.sKodu is a variant-coded stock record.
|
||||
ISNULL(NULLIF(LTRIM(RTRIM(SX.sModel)), ''), ISNULL(D.sKodu, '')) AS sKodu,
|
||||
ISNULL(NULLIF(LTRIM(RTRIM(SX.sAciklama)), ''), ISNULL(D.sAciklama, '')) AS sAciklama,
|
||||
ISNULL(D.sRenk, '') AS sRenk,
|
||||
ISNULL(D.sBeden, '') AS sBeden,
|
||||
ISNULL(D.sAciklama2, '') AS sAciklama2,
|
||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
|
||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nUrtMTBolumID, 0))) AS nUrtMTBolumID,
|
||||
-- Normalize code to variantless (tbStok.sModel) when D.sKodu is a variant-coded stock record.
|
||||
ISNULL(NULLIF(SX.sModel, ''), ISNULL(D.sKodu, '')) AS sKodu,
|
||||
ISNULL(NULLIF(SX.sAciklama, ''), ISNULL(D.sAciklama, '')) AS sAciklama,
|
||||
ISNULL(D.sRenk, '') AS sRenk,
|
||||
ISNULL(D.sBeden, '') AS sBeden,
|
||||
ISNULL(D.sAciklama2, '') AS sAciklama2,
|
||||
ISNULL(D.lMiktar, 0) AS lMiktar,
|
||||
ISNULL(D.lFiyat, 0) AS lFiyat,
|
||||
ISNULL(D.lTutar, 0) AS lTutar,
|
||||
@@ -949,25 +949,21 @@ func GetProductionHasCostDetailRowsByOnMLNo(
|
||||
ISNULL(D.sBirim, '') AS sBirim,
|
||||
ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
|
||||
ISNULL(B.sAdi, '') AS sParcaAdi
|
||||
FROM dbo.spUrtOnMLMasDet D
|
||||
LEFT JOIN dbo.spUrtOnMLHammaddeTuru T
|
||||
ON T.nHammaddeTuruNo = D.nHammaddeTuruNo
|
||||
LEFT JOIN dbo.spUrtMTBolum B
|
||||
ON B.nUrtMTBolumID = D.nUrtMTBolumID
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1
|
||||
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) AS sModel,
|
||||
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sAciklama, '')))) AS sAciklama
|
||||
FROM dbo.tbStok S WITH (NOLOCK)
|
||||
WHERE LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) = LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(D.sKodu, ''))))
|
||||
) SX
|
||||
WHERE D.nOnMLNo = @p1
|
||||
ORDER BY
|
||||
GroupTotalTutar DESC,
|
||||
sAciklama3 ASC,
|
||||
ISNULL(D.lTutar, 0) DESC,
|
||||
D.nOnMLDetNo ASC
|
||||
`
|
||||
FROM dbo.spUrtOnMLMasDet D
|
||||
LEFT JOIN dbo.spUrtOnMLHammaddeTuru T
|
||||
ON T.nHammaddeTuruNo = D.nHammaddeTuruNo
|
||||
LEFT JOIN dbo.spUrtMTBolum B
|
||||
ON B.nUrtMTBolumID = D.nUrtMTBolumID
|
||||
LEFT JOIN dbo.tbStok SX WITH (NOLOCK)
|
||||
ON (SX.IsBlocked = 0 OR SX.IsBlocked IS NULL)
|
||||
AND ISNULL(SX.sKodu,'') = ISNULL(D.sKodu,'')
|
||||
WHERE D.nOnMLNo = @p1
|
||||
ORDER BY
|
||||
GroupTotalTutar DESC,
|
||||
sAciklama3 ASC,
|
||||
ISNULL(D.lTutar, 0) DESC,
|
||||
D.nOnMLDetNo ASC
|
||||
`
|
||||
|
||||
return uretimDB.QueryContext(ctx, sqlText, nOnMLNo)
|
||||
}
|
||||
@@ -2033,6 +2029,111 @@ ORDER BY
|
||||
return mssqlDB.QueryRowContext(ctx, sqlText, sKodu, costDate, colorCode, itemDim1Code), nil
|
||||
}
|
||||
|
||||
// Bulk version of GetProductionHasCostLatestPurchasePriceForItem.
|
||||
// Uses OPENJSON to avoid 1-query-per-item fan-out.
|
||||
// For each requested rowKey, picks the latest purchase invoice before costDate,
|
||||
// preferring exact ColorCode/ItemDim1Code when provided.
|
||||
func GetProductionHasCostLatestPurchasePricesForItems(
|
||||
ctx context.Context,
|
||||
mssqlDB *sql.DB,
|
||||
itemsJSON string,
|
||||
costDate string,
|
||||
) (*sql.Rows, error) {
|
||||
itemsJSON = strings.TrimSpace(itemsJSON)
|
||||
costDate = strings.TrimSpace(costDate)
|
||||
|
||||
sqlText := `
|
||||
DECLARE @targetDate date = TRY_CONVERT(date, NULLIF(@p2, ''), 23);
|
||||
|
||||
WITH REQ AS (
|
||||
SELECT
|
||||
RowKey,
|
||||
LTRIM(RTRIM(ISNULL(ItemCode, ''))) AS ItemCode,
|
||||
LTRIM(RTRIM(ISNULL(ColorCode, ''))) AS ColorCode,
|
||||
LTRIM(RTRIM(ISNULL(ItemDim1Code, ''))) AS ItemDim1Code
|
||||
FROM OPENJSON(@p1) WITH (
|
||||
RowKey NVARCHAR(128) '$.rowKey',
|
||||
ItemCode NVARCHAR(128) '$.sKodu',
|
||||
ColorCode NVARCHAR(64) '$.colorCode',
|
||||
ItemDim1Code NVARCHAR(64) '$.itemDim1Code'
|
||||
)
|
||||
WHERE LTRIM(RTRIM(ISNULL(ItemCode, ''))) <> ''
|
||||
), BASE AS (
|
||||
SELECT
|
||||
R.RowKey,
|
||||
A.InvoiceDate,
|
||||
A.InvoiceNumber,
|
||||
A.ItemTypeCode,
|
||||
A.ItemCode,
|
||||
A.ColorCode,
|
||||
A.ItemDim1Code,
|
||||
A.Qty1,
|
||||
A.Doc_Price,
|
||||
A.Doc_CurrencyCode,
|
||||
CASE
|
||||
WHEN R.ColorCode <> '' AND LTRIM(RTRIM(ISNULL(A.ColorCode, ''))) = R.ColorCode THEN 0
|
||||
WHEN R.ColorCode = '' THEN 0
|
||||
ELSE 1
|
||||
END AS colorRank,
|
||||
CASE
|
||||
WHEN R.ItemDim1Code <> '' AND LTRIM(RTRIM(ISNULL(A.ItemDim1Code, ''))) = R.ItemDim1Code THEN 0
|
||||
WHEN R.ItemDim1Code = '' THEN 0
|
||||
ELSE 1
|
||||
END AS dimRank
|
||||
FROM REQ R
|
||||
INNER JOIN AllInvoicesWithAttributes A
|
||||
ON LTRIM(RTRIM(A.ItemCode)) = R.ItemCode
|
||||
WHERE A.ProcessCode IN ('BP')
|
||||
AND A.ATAtt01 IN (1, 2)
|
||||
AND A.CompanyCode IN (1, 2, 5)
|
||||
AND A.IsCompleted = 1
|
||||
AND YEAR(A.InvoiceDate) >= 2022
|
||||
AND (@targetDate IS NULL OR CONVERT(date, A.InvoiceDate) < @targetDate)
|
||||
), RANKED AS (
|
||||
SELECT
|
||||
B.*,
|
||||
ROW_NUMBER() OVER (PARTITION BY B.RowKey ORDER BY B.colorRank, B.dimRank, B.InvoiceDate DESC, B.InvoiceNumber DESC) AS rn
|
||||
FROM BASE B
|
||||
)
|
||||
SELECT
|
||||
R.RowKey,
|
||||
'MAN' AS priceType,
|
||||
CONVERT(VARCHAR(16), R.InvoiceDate, 120) AS Tarih,
|
||||
ISNULL(R.InvoiceNumber, '') AS FaturaKodu,
|
||||
LTRIM(RTRIM(ISNULL(R.ItemCode, ''))) AS MasrafKodu,
|
||||
ISNULL(ID.ItemDescription, '') AS MasrafDetay,
|
||||
ISNULL(R.ColorCode, '') AS ColorCode,
|
||||
ISNULL(COL.ColorDescription, '') AS ColorDescription,
|
||||
ISNULL(R.ItemDim1Code, '') AS ItemDim1Code,
|
||||
ISNULL(DIM1.ItemDim1Description, '') AS ItemDim1Description,
|
||||
ISNULL(R.Doc_Price, 0) AS EvrakFiyat,
|
||||
ISNULL(R.Doc_CurrencyCode, '') AS EvrakDoviz
|
||||
FROM RANKED R
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 ItemDescription
|
||||
FROM cdItemDesc
|
||||
WHERE ItemTypeCode = R.ItemTypeCode
|
||||
AND ItemCode = R.ItemCode
|
||||
AND LangCode = 'TR'
|
||||
) ID
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 ItemDim1Description
|
||||
FROM cdItemDim1Desc
|
||||
WHERE ItemDim1Code = R.ItemDim1Code
|
||||
AND LangCode = 'TR'
|
||||
) DIM1
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 ColorDescription
|
||||
FROM cdColorDesc
|
||||
WHERE ColorCode = R.ColorCode
|
||||
AND LangCode = 'TR'
|
||||
) COL
|
||||
WHERE R.rn = 1;
|
||||
`
|
||||
|
||||
return mssqlDB.QueryContext(ctx, sqlText, itemsJSON, costDate)
|
||||
}
|
||||
|
||||
func GetProductionHasCostPurchaseHistoryByExpenseCode(
|
||||
ctx context.Context,
|
||||
mssqlDB *sql.DB,
|
||||
|
||||
228
svc/queries/production_product_costing_last10_warnings_pg.go
Normal file
228
svc/queries/production_product_costing_last10_warnings_pg.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProductionCostingLast10WarningRow struct {
|
||||
NOnMLNo int `json:"n_onml_no"`
|
||||
UrunKodu string `json:"urun_kodu"`
|
||||
MaliyetTarihi string `json:"maliyet_tarihi"` // YYYY-MM-DD
|
||||
ItemCode string `json:"item_code"`
|
||||
CurrencyCode string `json:"currency_code"`
|
||||
InputPrice float64 `json:"input_price"`
|
||||
AvgDocPrice float64 `json:"avg_doc_price"`
|
||||
InputUSD float64 `json:"input_usd"`
|
||||
AvgUSD float64 `json:"avg_usd"`
|
||||
DiffRatio float64 `json:"diff_ratio"` // e.g. 0.12 means 12%
|
||||
SampleCount int `json:"sample_count"`
|
||||
MinInvoice string `json:"min_invoice_date,omitempty"`
|
||||
MaxInvoice string `json:"max_invoice_date,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
func EnsureProductionCostingLast10WarningTables(pg *sql.DB) error {
|
||||
if pg == nil {
|
||||
return fmt.Errorf("pg db is nil")
|
||||
}
|
||||
stmts := []string{
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS mk_costing_last10_warning (
|
||||
n_onml_no INT NOT NULL,
|
||||
item_code TEXT NOT NULL,
|
||||
currency_code TEXT NOT NULL,
|
||||
urun_kodu TEXT NOT NULL DEFAULT '',
|
||||
maliyet_tarihi DATE,
|
||||
input_price DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
avg_doc_price DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
input_usd DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
avg_usd DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
diff_ratio DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
sample_count INT NOT NULL DEFAULT 0,
|
||||
min_invoice_date DATE,
|
||||
max_invoice_date DATE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (n_onml_no, item_code, currency_code)
|
||||
)
|
||||
`,
|
||||
`CREATE INDEX IF NOT EXISTS ix_costing_last10_warning_onml ON mk_costing_last10_warning (n_onml_no)`,
|
||||
`CREATE INDEX IF NOT EXISTS ix_costing_last10_warning_item ON mk_costing_last10_warning (item_code, currency_code)`,
|
||||
}
|
||||
for _, s := range stmts {
|
||||
if _, err := pg.Exec(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReplaceProductionCostingLast10Warnings(
|
||||
ctx context.Context,
|
||||
pg *sql.DB,
|
||||
nOnMLNo int,
|
||||
urunKodu string,
|
||||
maliyetTarihi string, // YYYY-MM-DD
|
||||
createdBy string,
|
||||
items []Last10AvgPurchasePriceRow,
|
||||
inputByKey map[string]float64, // key = ITEM|CUR (input in doc currency)
|
||||
inputUSDByKey map[string]float64, // key = ITEM|CUR (input converted to USD basis)
|
||||
avgUSDByKey map[string]float64, // key = ITEM|CUR (avg converted to USD basis)
|
||||
) error {
|
||||
if pg == nil {
|
||||
return fmt.Errorf("pg db is nil")
|
||||
}
|
||||
if nOnMLNo <= 0 {
|
||||
return fmt.Errorf("invalid n_onml_no")
|
||||
}
|
||||
urunKodu = strings.TrimSpace(urunKodu)
|
||||
createdBy = strings.TrimSpace(createdBy)
|
||||
maliyetTarihi = strings.TrimSpace(maliyetTarihi)
|
||||
|
||||
// best-effort date parse for insert; allow NULL date if invalid
|
||||
var mtDate any = nil
|
||||
if maliyetTarihi != "" {
|
||||
if t, err := time.Parse("2006-01-02", maliyetTarihi); err == nil {
|
||||
mtDate = t
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := pg.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM mk_costing_last10_warning WHERE n_onml_no = $1`, nOnMLNo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert only rows that exist in both cache and inputByKey and violate threshold
|
||||
for _, ar := range items {
|
||||
code := strings.TrimSpace(ar.ItemCode)
|
||||
cur := strings.ToUpper(strings.TrimSpace(ar.CurrencyCode))
|
||||
if code == "" || cur == "" {
|
||||
continue
|
||||
}
|
||||
key := code + "|" + cur
|
||||
in := inputByKey[key]
|
||||
inUSD := inputUSDByKey[key]
|
||||
avgUSD := avgUSDByKey[key]
|
||||
if in <= 0 || ar.SampleCount <= 0 || ar.AvgDocPrice <= 0 {
|
||||
continue
|
||||
}
|
||||
// diff ratio is evaluated on USD basis (requested behavior)
|
||||
if inUSD <= 0 || avgUSD <= 0 {
|
||||
continue
|
||||
}
|
||||
diff := (inUSD - avgUSD) / avgUSD
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff <= 0.10 {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO mk_costing_last10_warning (
|
||||
n_onml_no, item_code, currency_code,
|
||||
urun_kodu, maliyet_tarihi,
|
||||
input_price, avg_doc_price, input_usd, avg_usd, diff_ratio,
|
||||
sample_count, min_invoice_date, max_invoice_date,
|
||||
created_by
|
||||
) VALUES (
|
||||
$1,$2,$3,
|
||||
$4,$5,
|
||||
$6,$7,$8,$9,$10,
|
||||
$11,$12,$13,
|
||||
$14
|
||||
)
|
||||
ON CONFLICT (n_onml_no, item_code, currency_code) DO UPDATE SET
|
||||
urun_kodu = EXCLUDED.urun_kodu,
|
||||
maliyet_tarihi = EXCLUDED.maliyet_tarihi,
|
||||
input_price = EXCLUDED.input_price,
|
||||
avg_doc_price = EXCLUDED.avg_doc_price,
|
||||
input_usd = EXCLUDED.input_usd,
|
||||
avg_usd = EXCLUDED.avg_usd,
|
||||
diff_ratio = EXCLUDED.diff_ratio,
|
||||
sample_count = EXCLUDED.sample_count,
|
||||
min_invoice_date = EXCLUDED.min_invoice_date,
|
||||
max_invoice_date = EXCLUDED.max_invoice_date,
|
||||
created_at = now(),
|
||||
created_by = EXCLUDED.created_by
|
||||
`, nOnMLNo, code, cur, urunKodu, mtDate, in, ar.AvgDocPrice, inUSD, avgUSD, diff, ar.SampleCount, ar.MinInvoiceDate, ar.MaxInvoiceDate, createdBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ListProductionCostingLast10WarningsByOnMLNo(ctx context.Context, pg *sql.DB, nOnMLNo int) ([]ProductionCostingLast10WarningRow, error) {
|
||||
if pg == nil {
|
||||
return nil, fmt.Errorf("pg db is nil")
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT
|
||||
n_onml_no,
|
||||
COALESCE(urun_kodu,'') AS urun_kodu,
|
||||
COALESCE(TO_CHAR(maliyet_tarihi, 'YYYY-MM-DD'),'') AS maliyet_tarihi,
|
||||
item_code,
|
||||
currency_code,
|
||||
input_price,
|
||||
avg_doc_price,
|
||||
input_usd,
|
||||
avg_usd,
|
||||
diff_ratio,
|
||||
sample_count,
|
||||
COALESCE(TO_CHAR(min_invoice_date, 'YYYY-MM-DD'),'') AS min_invoice_date,
|
||||
COALESCE(TO_CHAR(max_invoice_date, 'YYYY-MM-DD'),'') AS max_invoice_date,
|
||||
COALESCE(TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI:SS'),'') AS created_at,
|
||||
COALESCE(created_by,'') AS created_by
|
||||
FROM mk_costing_last10_warning
|
||||
WHERE n_onml_no = $1
|
||||
ORDER BY diff_ratio DESC, item_code ASC, currency_code ASC
|
||||
`, nOnMLNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]ProductionCostingLast10WarningRow, 0, 32)
|
||||
for rows.Next() {
|
||||
var r ProductionCostingLast10WarningRow
|
||||
if err := rows.Scan(
|
||||
&r.NOnMLNo,
|
||||
&r.UrunKodu,
|
||||
&r.MaliyetTarihi,
|
||||
&r.ItemCode,
|
||||
&r.CurrencyCode,
|
||||
&r.InputPrice,
|
||||
&r.AvgDocPrice,
|
||||
&r.InputUSD,
|
||||
&r.AvgUSD,
|
||||
&r.DiffRatio,
|
||||
&r.SampleCount,
|
||||
&r.MinInvoice,
|
||||
&r.MaxInvoice,
|
||||
&r.CreatedAt,
|
||||
&r.CreatedBy,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
106
svc/queries/tbstok_exists_bulk.go
Normal file
106
svc/queries/tbstok_exists_bulk.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LookupTbStokExistsByCodes checks if tbStok contains records matching the given codes.
|
||||
// Match rules are aligned with costing usage:
|
||||
// - exact sKodu (ignoring spaces)
|
||||
// - exact sModel
|
||||
//
|
||||
// Returns a map[code]exists.
|
||||
func LookupTbStokExistsByCodes(ctx context.Context, mssqlDB *sql.DB, codes []string) (map[string]bool, error) {
|
||||
if mssqlDB == nil {
|
||||
return nil, fmt.Errorf("mssql db is nil")
|
||||
}
|
||||
norm := make([]string, 0, len(codes))
|
||||
seen := map[string]struct{}{}
|
||||
for _, c := range codes {
|
||||
c = strings.TrimSpace(c)
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[c]; ok {
|
||||
continue
|
||||
}
|
||||
seen[c] = struct{}{}
|
||||
norm = append(norm, c)
|
||||
}
|
||||
out := map[string]bool{}
|
||||
for _, c := range norm {
|
||||
out[c] = false
|
||||
}
|
||||
if len(norm) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
valParts := make([]string, 0, len(norm))
|
||||
args := make([]any, 0, len(norm))
|
||||
for i, code := range norm {
|
||||
valParts = append(valParts, fmt.Sprintf("(@p%d)", i+1))
|
||||
args = append(args, code)
|
||||
}
|
||||
|
||||
// NOTE: This endpoint is a UX validation helper and must be fast.
|
||||
// Keep predicates sargable: no function wrapping on tbStok columns.
|
||||
//
|
||||
// For "ignore spaces" behavior, we send both the original code and its no-space variant from input,
|
||||
// and match via exact equality against tbStok.sKodu.
|
||||
//
|
||||
// IMPORTANT: We intentionally do not filter by IsBlocked here because IsBlocked is nullable and
|
||||
// filtering requires OR logic, which can prevent index seeks and cause timeouts. This is a best-effort
|
||||
// existence check for UX highlighting.
|
||||
sqlText := fmt.Sprintf(`
|
||||
WITH C AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(V.code)) AS code,
|
||||
REPLACE(LTRIM(RTRIM(V.code)), ' ', '') AS code_nospace
|
||||
FROM (VALUES %s) AS V(code)
|
||||
),
|
||||
HIT AS (
|
||||
SELECT DISTINCT C.code
|
||||
FROM C
|
||||
JOIN dbo.tbStok S WITH (NOLOCK)
|
||||
ON S.sKodu = C.code
|
||||
OR S.sKodu = C.code_nospace
|
||||
UNION
|
||||
SELECT DISTINCT C.code
|
||||
FROM C
|
||||
JOIN dbo.tbStok S WITH (NOLOCK)
|
||||
ON S.sModel = C.code
|
||||
)
|
||||
SELECT
|
||||
C.code,
|
||||
CASE WHEN H.code IS NULL THEN 0 ELSE 1 END AS existsFlag
|
||||
FROM C
|
||||
LEFT JOIN HIT H
|
||||
ON H.code = C.code
|
||||
`, strings.Join(valParts, ","))
|
||||
|
||||
rows, err := mssqlDB.QueryContext(ctx, sqlText, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var code string
|
||||
var existsFlag int
|
||||
if err := rows.Scan(&code, &existsFlag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
out[code] = existsFlag == 1
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user