Merge remote-tracking branch 'origin/master'
This commit is contained in:
12
svc/main.go
12
svc/main.go
@@ -841,7 +841,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/production-product-costing/onml/save", "POST",
|
"/api/pricing/production-product-costing/onml/save", "POST",
|
||||||
"order", "view",
|
"order", "view",
|
||||||
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLSaveHandler)),
|
wrapV3(routes.PostProductionProductCostingOnMLSaveHandlerWithMailer(ml)),
|
||||||
)
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/production-product-costing/onml/pdf", "GET",
|
"/api/pricing/production-product-costing/onml/pdf", "GET",
|
||||||
@@ -883,6 +883,16 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"order", "view",
|
"order", "view",
|
||||||
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingDefaultQuantitiesRefreshHandler)),
|
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingDefaultQuantitiesRefreshHandler)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/production-product-costing/tbstok/exists-bulk", "POST",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingTbStokExistsBulkHandler)),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/production-product-costing/last10-warnings", "GET",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.GetProductionProductCostingLast10WarningsHandler)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/production-product-costing/options/urun-ana-grup", "GET",
|
"/api/pricing/production-product-costing/options/urun-ana-grup", "GET",
|
||||||
"order", "view",
|
"order", "view",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
type FirstGroupOption struct {
|
type FirstGroupOption struct {
|
||||||
ID string `json:"id"`
|
Code string `json:"code"`
|
||||||
Label string `json:"label"`
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirstGroupMailOption struct {
|
type FirstGroupMailOption struct {
|
||||||
@@ -11,7 +11,9 @@ type FirstGroupMailOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirstGroupMailMappingRow struct {
|
type FirstGroupMailMappingRow struct {
|
||||||
UrunIlkGrubu string `json:"urun_ilk_grubu"`
|
UrunIlkGrubu string `json:"urun_ilk_grubu"` // group code (kept for backward compatibility)
|
||||||
|
GroupCode string `json:"group_code"`
|
||||||
|
GroupTitle string `json:"group_title"`
|
||||||
MailIDs []string `json:"mail_ids"`
|
MailIDs []string `json:"mail_ids"`
|
||||||
Mails []FirstGroupMailOption `json:"mails"`
|
Mails []FirstGroupMailOption `json:"mails"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,16 @@ type ProductionProductCostingOnMLSaveRequest struct {
|
|||||||
|
|
||||||
type ProductionProductCostingOnMLSaveResponse struct {
|
type ProductionProductCostingOnMLSaveResponse struct {
|
||||||
NOnMLNo int `json:"n_onml_no"`
|
NOnMLNo int `json:"n_onml_no"`
|
||||||
|
Warnings []string `json:"warnings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductionProductCostingTbStokExistsBulkRequest struct {
|
||||||
|
Codes []string `json:"codes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductionProductCostingTbStokExistsBulkResponse struct {
|
||||||
|
Missing []string `json:"missing"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductionProductCostingOnMLDeleteRequest struct {
|
type ProductionProductCostingOnMLDeleteRequest struct {
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@@ -927,8 +927,8 @@ func GetProductionHasCostDetailRowsByOnMLNo(
|
|||||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
|
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
|
||||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nUrtMTBolumID, 0))) AS nUrtMTBolumID,
|
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.
|
-- 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(SX.sModel, ''), ISNULL(D.sKodu, '')) AS sKodu,
|
||||||
ISNULL(NULLIF(LTRIM(RTRIM(SX.sAciklama)), ''), ISNULL(D.sAciklama, '')) AS sAciklama,
|
ISNULL(NULLIF(SX.sAciklama, ''), ISNULL(D.sAciklama, '')) AS sAciklama,
|
||||||
ISNULL(D.sRenk, '') AS sRenk,
|
ISNULL(D.sRenk, '') AS sRenk,
|
||||||
ISNULL(D.sBeden, '') AS sBeden,
|
ISNULL(D.sBeden, '') AS sBeden,
|
||||||
ISNULL(D.sAciklama2, '') AS sAciklama2,
|
ISNULL(D.sAciklama2, '') AS sAciklama2,
|
||||||
@@ -954,13 +954,9 @@ func GetProductionHasCostDetailRowsByOnMLNo(
|
|||||||
ON T.nHammaddeTuruNo = D.nHammaddeTuruNo
|
ON T.nHammaddeTuruNo = D.nHammaddeTuruNo
|
||||||
LEFT JOIN dbo.spUrtMTBolum B
|
LEFT JOIN dbo.spUrtMTBolum B
|
||||||
ON B.nUrtMTBolumID = D.nUrtMTBolumID
|
ON B.nUrtMTBolumID = D.nUrtMTBolumID
|
||||||
OUTER APPLY (
|
LEFT JOIN dbo.tbStok SX WITH (NOLOCK)
|
||||||
SELECT TOP 1
|
ON (SX.IsBlocked = 0 OR SX.IsBlocked IS NULL)
|
||||||
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) AS sModel,
|
AND ISNULL(SX.sKodu,'') = ISNULL(D.sKodu,'')
|
||||||
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
|
WHERE D.nOnMLNo = @p1
|
||||||
ORDER BY
|
ORDER BY
|
||||||
GroupTotalTutar DESC,
|
GroupTotalTutar DESC,
|
||||||
@@ -2033,6 +2029,111 @@ ORDER BY
|
|||||||
return mssqlDB.QueryRowContext(ctx, sqlText, sKodu, costDate, colorCode, itemDim1Code), nil
|
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(
|
func GetProductionHasCostPurchaseHistoryByExpenseCode(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
mssqlDB *sql.DB,
|
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
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -23,6 +22,42 @@ type FirstGroupMailLookupResponse struct {
|
|||||||
Mails []models.MailOption `json:"mails"`
|
Mails []models.MailOption `json:"mails"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureFirstGroupMailMappingTables(pg *sql.DB) error {
|
||||||
|
// Idempotent bootstrap: create tables if they don't exist.
|
||||||
|
// We keep schema minimal: (group_code, mail_id) + created_at and FK to mk_mail.
|
||||||
|
stmts := []string{
|
||||||
|
`
|
||||||
|
CREATE TABLE IF NOT EXISTS mk_costing_first_group_mail (
|
||||||
|
urun_ilk_grubu TEXT NOT NULL,
|
||||||
|
mail_id UUID NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (urun_ilk_grubu, mail_id),
|
||||||
|
CONSTRAINT fk_costing_first_group_mail_mail
|
||||||
|
FOREIGN KEY (mail_id) REFERENCES mk_mail(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS ix_costing_first_group_mail_group ON mk_costing_first_group_mail (urun_ilk_grubu)`,
|
||||||
|
`
|
||||||
|
CREATE TABLE IF NOT EXISTS mk_pricing_first_group_mail (
|
||||||
|
urun_ilk_grubu TEXT NOT NULL,
|
||||||
|
mail_id UUID NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (urun_ilk_grubu, mail_id),
|
||||||
|
CONSTRAINT fk_pricing_first_group_mail_mail
|
||||||
|
FOREIGN KEY (mail_id) REFERENCES mk_mail(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS ix_pricing_first_group_mail_group ON mk_pricing_first_group_mail (urun_ilk_grubu)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range stmts {
|
||||||
|
if _, err := pg.Exec(s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetCostingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc {
|
func GetCostingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
@@ -32,6 +67,10 @@ func GetCostingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc
|
|||||||
http.Error(w, "mssql connection not available", http.StatusServiceUnavailable)
|
http.Error(w, "mssql connection not available", http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||||
|
http.Error(w, "mapping table bootstrap error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
traceID := utils.TraceIDFromRequest(r)
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
@@ -39,21 +78,23 @@ func GetCostingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc
|
|||||||
firstGroups := make([]models.FirstGroupOption, 0, 256)
|
firstGroups := make([]models.FirstGroupOption, 0, 256)
|
||||||
mails := make([]models.MailOption, 0, 256)
|
mails := make([]models.MailOption, 0, 256)
|
||||||
|
|
||||||
fgRows, err := queries.ListProductFirstGroupOptions(ctx, mssql, "", 5000)
|
fgRows, err := queries.ListProductFirstGroupCodeDescOptions(ctx, mssql, "", 5000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "first group lookup error", http.StatusInternalServerError)
|
http.Error(w, "first group lookup error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer fgRows.Close()
|
defer fgRows.Close()
|
||||||
for fgRows.Next() {
|
for fgRows.Next() {
|
||||||
var g string
|
var code string
|
||||||
if err := fgRows.Scan(&g); err != nil {
|
var title string
|
||||||
|
if err := fgRows.Scan(&code, &title); err != nil {
|
||||||
http.Error(w, "first group scan error", http.StatusInternalServerError)
|
http.Error(w, "first group scan error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g = strings.TrimSpace(g)
|
code = strings.TrimSpace(code)
|
||||||
if g != "" {
|
title = strings.TrimSpace(title)
|
||||||
firstGroups = append(firstGroups, models.FirstGroupOption{ID: g, Label: g})
|
if code != "" {
|
||||||
|
firstGroups = append(firstGroups, models.FirstGroupOption{Code: code, Title: title})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := fgRows.Err(); err != nil {
|
if err := fgRows.Err(); err != nil {
|
||||||
@@ -98,34 +139,44 @@ func GetCostingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
http.Error(w, "mssql connection not available", http.StatusServiceUnavailable)
|
http.Error(w, "mssql connection not available", http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||||
|
http.Error(w, "mapping table bootstrap error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
traceID := utils.TraceIDFromRequest(r)
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
|
||||||
// Fetch all first groups from V3 (source of truth for the list)
|
// Fetch all first groups from V3 (source of truth for the list)
|
||||||
allGroups := make([]string, 0, 512)
|
allCodes := make([]string, 0, 512)
|
||||||
fgRows, err := queries.ListProductFirstGroupOptions(ctx, mssql, "", 5000)
|
titleByCode := make(map[string]string, 512)
|
||||||
|
fgRows, err := queries.ListProductFirstGroupCodeDescOptions(ctx, mssql, "", 5000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "first group lookup error", http.StatusInternalServerError)
|
http.Error(w, "first group lookup error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer fgRows.Close()
|
defer fgRows.Close()
|
||||||
for fgRows.Next() {
|
for fgRows.Next() {
|
||||||
var g string
|
var code string
|
||||||
if err := fgRows.Scan(&g); err != nil {
|
var title string
|
||||||
|
if err := fgRows.Scan(&code, &title); err != nil {
|
||||||
http.Error(w, "first group scan error", http.StatusInternalServerError)
|
http.Error(w, "first group scan error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g = strings.TrimSpace(g)
|
code = strings.TrimSpace(code)
|
||||||
if g != "" {
|
title = strings.TrimSpace(title)
|
||||||
allGroups = append(allGroups, g)
|
if code != "" {
|
||||||
|
allCodes = append(allCodes, code)
|
||||||
|
if _, ok := titleByCode[code]; !ok {
|
||||||
|
titleByCode[code] = title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := fgRows.Err(); err != nil {
|
if err := fgRows.Err(); err != nil {
|
||||||
http.Error(w, "first group rows error", http.StatusInternalServerError)
|
http.Error(w, "first group rows error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sort.Strings(allGroups)
|
allCodes = normalizeIDList(allCodes)
|
||||||
|
|
||||||
// Fetch mappings from Postgres
|
// Fetch mappings from Postgres
|
||||||
rows, err := pg.Query(queries.GetCostingFirstGroupMailMappingRows)
|
rows, err := pg.Query(queries.GetCostingFirstGroupMailMappingRows)
|
||||||
@@ -136,9 +187,11 @@ func GetCostingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
byGroup := map[string]*models.FirstGroupMailMappingRow{}
|
byGroup := map[string]*models.FirstGroupMailMappingRow{}
|
||||||
for _, g := range allGroups {
|
for _, code := range allCodes {
|
||||||
byGroup[g] = &models.FirstGroupMailMappingRow{
|
byGroup[code] = &models.FirstGroupMailMappingRow{
|
||||||
UrunIlkGrubu: g,
|
UrunIlkGrubu: code,
|
||||||
|
GroupCode: code,
|
||||||
|
GroupTitle: titleByCode[code],
|
||||||
MailIDs: make([]string, 0, 8),
|
MailIDs: make([]string, 0, 8),
|
||||||
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
||||||
}
|
}
|
||||||
@@ -153,19 +206,21 @@ func GetCostingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
http.Error(w, "mapping scan error", http.StatusInternalServerError)
|
http.Error(w, "mapping scan error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g := strings.TrimSpace(group.String)
|
code := strings.TrimSpace(group.String)
|
||||||
if g == "" {
|
if code == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
row, ok := byGroup[g]
|
row, ok := byGroup[code]
|
||||||
if !ok {
|
if !ok {
|
||||||
row = &models.FirstGroupMailMappingRow{
|
row = &models.FirstGroupMailMappingRow{
|
||||||
UrunIlkGrubu: g,
|
UrunIlkGrubu: code,
|
||||||
|
GroupCode: code,
|
||||||
|
GroupTitle: titleByCode[code],
|
||||||
MailIDs: make([]string, 0, 8),
|
MailIDs: make([]string, 0, 8),
|
||||||
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
||||||
}
|
}
|
||||||
byGroup[g] = row
|
byGroup[code] = row
|
||||||
allGroups = append(allGroups, g)
|
allCodes = append(allCodes, code)
|
||||||
}
|
}
|
||||||
if mailID.Valid && strings.TrimSpace(mailID.String) != "" {
|
if mailID.Valid && strings.TrimSpace(mailID.String) != "" {
|
||||||
id := strings.TrimSpace(mailID.String)
|
id := strings.TrimSpace(mailID.String)
|
||||||
@@ -182,11 +237,15 @@ func GetCostingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(allGroups)
|
allCodes = normalizeIDList(allCodes)
|
||||||
out := make([]models.FirstGroupMailMappingRow, 0, len(allGroups))
|
out := make([]models.FirstGroupMailMappingRow, 0, len(allCodes))
|
||||||
for _, g := range allGroups {
|
for _, code := range allCodes {
|
||||||
if r := byGroup[g]; r != nil {
|
if r := byGroup[code]; r != nil {
|
||||||
r.MailIDs = normalizeIDList(r.MailIDs)
|
r.MailIDs = normalizeIDList(r.MailIDs)
|
||||||
|
// Fill title if missing
|
||||||
|
if strings.TrimSpace(r.GroupTitle) == "" {
|
||||||
|
r.GroupTitle = titleByCode[code]
|
||||||
|
}
|
||||||
out = append(out, *r)
|
out = append(out, *r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,33 +262,43 @@ func GetPricingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
http.Error(w, "mssql connection not available", http.StatusServiceUnavailable)
|
http.Error(w, "mssql connection not available", http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||||
|
http.Error(w, "mapping table bootstrap error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
traceID := utils.TraceIDFromRequest(r)
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
|
||||||
allGroups := make([]string, 0, 512)
|
allCodes := make([]string, 0, 512)
|
||||||
fgRows, err := queries.ListProductFirstGroupOptions(ctx, mssql, "", 5000)
|
titleByCode := make(map[string]string, 512)
|
||||||
|
fgRows, err := queries.ListProductFirstGroupCodeDescOptions(ctx, mssql, "", 5000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "first group lookup error", http.StatusInternalServerError)
|
http.Error(w, "first group lookup error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer fgRows.Close()
|
defer fgRows.Close()
|
||||||
for fgRows.Next() {
|
for fgRows.Next() {
|
||||||
var g string
|
var code string
|
||||||
if err := fgRows.Scan(&g); err != nil {
|
var title string
|
||||||
|
if err := fgRows.Scan(&code, &title); err != nil {
|
||||||
http.Error(w, "first group scan error", http.StatusInternalServerError)
|
http.Error(w, "first group scan error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g = strings.TrimSpace(g)
|
code = strings.TrimSpace(code)
|
||||||
if g != "" {
|
title = strings.TrimSpace(title)
|
||||||
allGroups = append(allGroups, g)
|
if code != "" {
|
||||||
|
allCodes = append(allCodes, code)
|
||||||
|
if _, ok := titleByCode[code]; !ok {
|
||||||
|
titleByCode[code] = title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := fgRows.Err(); err != nil {
|
if err := fgRows.Err(); err != nil {
|
||||||
http.Error(w, "first group rows error", http.StatusInternalServerError)
|
http.Error(w, "first group rows error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sort.Strings(allGroups)
|
allCodes = normalizeIDList(allCodes)
|
||||||
|
|
||||||
rows, err := pg.Query(queries.GetPricingFirstGroupMailMappingRows)
|
rows, err := pg.Query(queries.GetPricingFirstGroupMailMappingRows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -239,9 +308,11 @@ func GetPricingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
byGroup := map[string]*models.FirstGroupMailMappingRow{}
|
byGroup := map[string]*models.FirstGroupMailMappingRow{}
|
||||||
for _, g := range allGroups {
|
for _, code := range allCodes {
|
||||||
byGroup[g] = &models.FirstGroupMailMappingRow{
|
byGroup[code] = &models.FirstGroupMailMappingRow{
|
||||||
UrunIlkGrubu: g,
|
UrunIlkGrubu: code,
|
||||||
|
GroupCode: code,
|
||||||
|
GroupTitle: titleByCode[code],
|
||||||
MailIDs: make([]string, 0, 8),
|
MailIDs: make([]string, 0, 8),
|
||||||
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
||||||
}
|
}
|
||||||
@@ -256,19 +327,21 @@ func GetPricingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
http.Error(w, "mapping scan error", http.StatusInternalServerError)
|
http.Error(w, "mapping scan error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g := strings.TrimSpace(group.String)
|
code := strings.TrimSpace(group.String)
|
||||||
if g == "" {
|
if code == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
row, ok := byGroup[g]
|
row, ok := byGroup[code]
|
||||||
if !ok {
|
if !ok {
|
||||||
row = &models.FirstGroupMailMappingRow{
|
row = &models.FirstGroupMailMappingRow{
|
||||||
UrunIlkGrubu: g,
|
UrunIlkGrubu: code,
|
||||||
|
GroupCode: code,
|
||||||
|
GroupTitle: titleByCode[code],
|
||||||
MailIDs: make([]string, 0, 8),
|
MailIDs: make([]string, 0, 8),
|
||||||
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
||||||
}
|
}
|
||||||
byGroup[g] = row
|
byGroup[code] = row
|
||||||
allGroups = append(allGroups, g)
|
allCodes = append(allCodes, code)
|
||||||
}
|
}
|
||||||
if mailID.Valid && strings.TrimSpace(mailID.String) != "" {
|
if mailID.Valid && strings.TrimSpace(mailID.String) != "" {
|
||||||
id := strings.TrimSpace(mailID.String)
|
id := strings.TrimSpace(mailID.String)
|
||||||
@@ -285,11 +358,14 @@ func GetPricingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(allGroups)
|
allCodes = normalizeIDList(allCodes)
|
||||||
out := make([]models.FirstGroupMailMappingRow, 0, len(allGroups))
|
out := make([]models.FirstGroupMailMappingRow, 0, len(allCodes))
|
||||||
for _, g := range allGroups {
|
for _, code := range allCodes {
|
||||||
if r := byGroup[g]; r != nil {
|
if r := byGroup[code]; r != nil {
|
||||||
r.MailIDs = normalizeIDList(r.MailIDs)
|
r.MailIDs = normalizeIDList(r.MailIDs)
|
||||||
|
if strings.TrimSpace(r.GroupTitle) == "" {
|
||||||
|
r.GroupTitle = titleByCode[code]
|
||||||
|
}
|
||||||
out = append(out, *r)
|
out = append(out, *r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,31 +36,18 @@ func ensureMkMail(tx *sql.Tx, email string) error {
|
|||||||
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
newID := utils.NewUUID()
|
newID := utils.NewUUID()
|
||||||
_, err = tx.Exec(`
|
// Keep this insert intentionally minimal because mk_mail schema may vary between environments.
|
||||||
INSERT INTO mk_mail (
|
// Only rely on columns we already SELECT elsewhere (id/email/display_name/is_active).
|
||||||
id,
|
_, err = tx.Exec(`INSERT INTO mk_mail (id, email, display_name, is_active) VALUES ($1, $2, '', true)`, newID, mail)
|
||||||
email,
|
|
||||||
display_name,
|
|
||||||
"type",
|
|
||||||
is_primary,
|
|
||||||
external_id,
|
|
||||||
is_active,
|
|
||||||
created_at
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, '', 'user', true, true, true, NOW())
|
|
||||||
`, newID, mail)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exists: normalize + activate. Avoid touching created_at.
|
// Exists: normalize + activate.
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
UPDATE mk_mail
|
UPDATE mk_mail
|
||||||
SET
|
SET
|
||||||
email = $2,
|
email = $2,
|
||||||
display_name = COALESCE(display_name, ''),
|
display_name = COALESCE(display_name, ''),
|
||||||
"type" = 'user',
|
|
||||||
is_primary = true,
|
|
||||||
external_id = true,
|
|
||||||
is_active = true
|
is_active = true
|
||||||
WHERE id::text = $1
|
WHERE id::text = $1
|
||||||
`, id, mail)
|
`, id, mail)
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"bssapp-backend/auth"
|
"bssapp-backend/auth"
|
||||||
"bssapp-backend/db"
|
"bssapp-backend/db"
|
||||||
|
"bssapp-backend/internal/mailer"
|
||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
"bssapp-backend/queries"
|
"bssapp-backend/queries"
|
||||||
"bssapp-backend/utils"
|
"bssapp-backend/utils"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -1222,8 +1224,82 @@ func PostProductionProductCostingDefaultQuantitiesRefreshHandler(w http.Response
|
|||||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "top_n": topN})
|
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "top_n": topN})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /api/pricing/production-product-costing/tbstok/exists-bulk
|
||||||
|
// Validates whether given codes exist in URETIM dbo.tbStok (or match sModel rules).
|
||||||
|
// Used by UI to highlight invalid codes before save.
|
||||||
|
func PostProductionProductCostingTbStokExistsBulkHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
uretimDB := db.GetUretimDB()
|
||||||
|
if uretimDB == nil {
|
||||||
|
// Non-blocking UX helper: if URETIM isn't reachable in this environment, return empty result.
|
||||||
|
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingTbStokExistsBulkResponse{
|
||||||
|
Missing: []string{},
|
||||||
|
Error: "URETIM baglantisi aktif degil",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.tbstok.exists-bulk")
|
||||||
|
|
||||||
|
var req models.ProductionProductCostingTbStokExistsBulkRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// short timeout: this is a UX helper, must not hang (but should still complete on moderate load)
|
||||||
|
checkCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// log a small sample to diagnose timeouts without flooding logs
|
||||||
|
sample := make([]string, 0, 8)
|
||||||
|
for _, c := range req.Codes {
|
||||||
|
c = strings.TrimSpace(c)
|
||||||
|
if c == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sample = append(sample, c)
|
||||||
|
if len(sample) >= 8 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Info("lookup start", "codes", len(req.Codes), "sample", strings.Join(sample, ","))
|
||||||
|
existsBy, err := queries.LookupTbStokExistsByCodes(checkCtx, uretimDB, req.Codes)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("lookup failed", "err", err, "codes", len(req.Codes))
|
||||||
|
// Non-blocking UX helper: return empty list + error so UI can continue without hard failure.
|
||||||
|
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingTbStokExistsBulkResponse{
|
||||||
|
Missing: []string{},
|
||||||
|
Error: "tbStok sorgu hatasi",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
missing := make([]string, 0, 16)
|
||||||
|
for code, ok := range existsBy {
|
||||||
|
if !ok && strings.TrimSpace(code) != "" {
|
||||||
|
missing = append(missing, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingTbStokExistsBulkResponse{Missing: missing})
|
||||||
|
}
|
||||||
|
|
||||||
// POST /api/pricing/production-product-costing/onml/save
|
// POST /api/pricing/production-product-costing/onml/save
|
||||||
|
func PostProductionProductCostingOnMLSaveHandlerWithMailer(ml *mailer.GraphMailer) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
postProductionProductCostingOnMLSaveHandler(w, r, ml)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward-compatible entrypoint (no mailer).
|
||||||
func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.Request) {
|
func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
postProductionProductCostingOnMLSaveHandler(w, r, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.Request, ml *mailer.GraphMailer) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
uretimDB := db.GetUretimDB()
|
uretimDB := db.GetUretimDB()
|
||||||
@@ -1413,6 +1489,14 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
logger.Warn("tx rollback failed", "trace_id", traceID, "n_onml_no", nOnMLNo, "err", err)
|
logger.Warn("tx rollback failed", "trace_id", traceID, "n_onml_no", nOnMLNo, "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
warnings := make([]string, 0, 4)
|
||||||
|
// Determine whether this is a new costing record or an update (before header upsert).
|
||||||
|
isUpdate := false
|
||||||
|
{
|
||||||
|
var flag int
|
||||||
|
_ = tx.QueryRowContext(ctx, `SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.spUrtOnMLMas WITH (NOLOCK) WHERE nOnMLNo=@p1) THEN 1 ELSE 0 END`, nOnMLNo).Scan(&flag)
|
||||||
|
isUpdate = flag == 1
|
||||||
|
}
|
||||||
|
|
||||||
// Determine mamul turu inside same tx (to keep create atomic)
|
// Determine mamul turu inside same tx (to keep create atomic)
|
||||||
mamulLabel := ""
|
mamulLabel := ""
|
||||||
@@ -1512,6 +1596,96 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
sKodu string
|
sKodu string
|
||||||
}
|
}
|
||||||
recipeQtyByKey := map[recipeKey]float64{}
|
recipeQtyByKey := map[recipeKey]float64{}
|
||||||
|
|
||||||
|
// Bulk resolve stock type id from tbStok (huge performance win vs per-row queries).
|
||||||
|
// IMPORTANT: Do NOT run tbStok lookups on the transaction connection.
|
||||||
|
// We have seen network timeouts against the tbStok server poison the tx connection ("driver: bad connection"),
|
||||||
|
// which then makes rollback/commit impossible and returns 500. Use a separate DB handle + short timeouts.
|
||||||
|
lookupDB := mssqlDB
|
||||||
|
if lookupDB == nil {
|
||||||
|
lookupDB = uretimDB
|
||||||
|
}
|
||||||
|
uniqueCodes := make([]string, 0, len(req.Detail.Upserts))
|
||||||
|
seenCode := map[string]struct{}{}
|
||||||
|
for _, row := range req.Detail.Upserts {
|
||||||
|
if row.NOnMLDetNo <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
code := strings.TrimSpace(row.SKodu)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seenCode[code]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenCode[code] = struct{}{}
|
||||||
|
uniqueCodes = append(uniqueCodes, code)
|
||||||
|
}
|
||||||
|
stockTypeByCode := map[string]int{}
|
||||||
|
bulkStockTypeLookupFailed := false
|
||||||
|
if len(uniqueCodes) > 0 {
|
||||||
|
lookupCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Build a VALUES list with parameters: (VALUES (@p1), (@p2), ...)
|
||||||
|
valParts := make([]string, 0, len(uniqueCodes))
|
||||||
|
args := make([]any, 0, len(uniqueCodes))
|
||||||
|
for i, code := range uniqueCodes {
|
||||||
|
// Parameters are 1-based in our SQL style (@p1, @p2, ...)
|
||||||
|
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 code
|
||||||
|
FROM (VALUES %s) AS V(code)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
C.code,
|
||||||
|
ISNULL((
|
||||||
|
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
|
||||||
|
FROM dbo.tbStok S WITH (NOLOCK)
|
||||||
|
WHERE ISNULL(S.IsBlocked, 0) = 0
|
||||||
|
AND (
|
||||||
|
REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(C.code, ' ', '')
|
||||||
|
OR LTRIM(RTRIM(ISNULL(S.sModel,''))) = C.code
|
||||||
|
OR C.code LIKE LTRIM(RTRIM(ISNULL(S.sModel,''))) + '%%'
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(C.code, ' ', '') THEN 0
|
||||||
|
WHEN LTRIM(RTRIM(ISNULL(S.sModel,''))) = C.code THEN 1
|
||||||
|
ELSE 2
|
||||||
|
END,
|
||||||
|
S.dteKayitTarihi DESC,
|
||||||
|
S.nStokID DESC
|
||||||
|
), 0) AS nStokTipiID
|
||||||
|
FROM C
|
||||||
|
`, strings.Join(valParts, ","))
|
||||||
|
|
||||||
|
rows, err := lookupDB.QueryContext(lookupCtx, sqlText, args...)
|
||||||
|
if err != nil {
|
||||||
|
// Do not fail the whole save for bulk lookup. We'll fallback to per-row queries below.
|
||||||
|
logger.Error("bulk stok tipi lookup error (fallback to per-row)", "err", err)
|
||||||
|
bulkStockTypeLookupFailed = true
|
||||||
|
} else {
|
||||||
|
for rows.Next() {
|
||||||
|
var code string
|
||||||
|
var nStokTipiID int
|
||||||
|
if err := rows.Scan(&code, &nStokTipiID); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
logger.Error("bulk stok tipi scan error (fallback to per-row)", "err", err)
|
||||||
|
bulkStockTypeLookupFailed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
if code != "" {
|
||||||
|
stockTypeByCode[code] = nStokTipiID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, row := range req.Detail.Upserts {
|
for _, row := range req.Detail.Upserts {
|
||||||
if row.NOnMLDetNo <= 0 {
|
if row.NOnMLDetNo <= 0 {
|
||||||
skippedUpserts += 1
|
skippedUpserts += 1
|
||||||
@@ -1640,23 +1814,25 @@ WHERE nHammaddeTuruNo = @p1
|
|||||||
lTutar := unitTRY * qty
|
lTutar := unitTRY * qty
|
||||||
lDovizTutari := unitUSD * qty
|
lDovizTutari := unitUSD * qty
|
||||||
|
|
||||||
// Debug log for price resolution
|
// Keep logs lean: per-row price debug was too noisy and slow in large payloads.
|
||||||
logger.Info("price debug",
|
|
||||||
"s_kodu", strings.TrimSpace(row.SKodu),
|
|
||||||
"qty", qty,
|
|
||||||
"fiyat_girilen", row.FiyatGirilen,
|
|
||||||
"fiyat_doviz", strings.TrimSpace(row.FiyatDoviz),
|
|
||||||
"unitTRY", unitTRY,
|
|
||||||
"lTutar", lTutar,
|
|
||||||
"lDovizTutari", lDovizTutari,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Resolve stock type id from tbStok by sKodu (exact), then fallback to model-based match.
|
// Resolve stock type id from tbStok by sKodu (exact), then fallback to model-based match.
|
||||||
// Note: In this DB, stock type is stored as tbStok.nStokTipi but spUrtOnMLMasDet expects nStokTipiID (int).
|
// Note: In this DB, stock type is stored as tbStok.nStokTipi but spUrtOnMLMasDet expects nStokTipiID (int).
|
||||||
rawSKodu := strings.TrimSpace(row.SKodu)
|
rawSKodu := strings.TrimSpace(row.SKodu)
|
||||||
logger.Info("resolving stock type", "s_kodu", rawSKodu)
|
nStokTipiID, ok := stockTypeByCode[rawSKodu]
|
||||||
var nStokTipiID int
|
if !ok || nStokTipiID <= 0 {
|
||||||
err := tx.QueryRowContext(ctx, `
|
// If bulk lookup already failed (usually due to network/driver timeouts), do NOT attempt per-row lookups.
|
||||||
|
// Per-row fallback would multiply latency and still likely fail, without adding value.
|
||||||
|
if bulkStockTypeLookupFailed {
|
||||||
|
nStokTipiID = 1
|
||||||
|
if rawSKodu != "" {
|
||||||
|
stockTypeByCode[rawSKodu] = 1
|
||||||
|
}
|
||||||
|
} else if rawSKodu != "" {
|
||||||
|
// Fallback to per-row query. Cache results back into the map.
|
||||||
|
var tmp int
|
||||||
|
perRowCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||||
|
err := lookupDB.QueryRowContext(perRowCtx, `
|
||||||
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
|
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
|
||||||
FROM dbo.tbStok S WITH (NOLOCK)
|
FROM dbo.tbStok S WITH (NOLOCK)
|
||||||
WHERE ISNULL(S.IsBlocked, 0) = 0
|
WHERE ISNULL(S.IsBlocked, 0) = 0
|
||||||
@@ -1673,28 +1849,28 @@ ORDER BY
|
|||||||
END,
|
END,
|
||||||
S.dteKayitTarihi DESC,
|
S.dteKayitTarihi DESC,
|
||||||
S.nStokID DESC
|
S.nStokID DESC
|
||||||
`, rawSKodu).Scan(&nStokTipiID)
|
`, rawSKodu).Scan(&tmp)
|
||||||
if err != nil {
|
cancel()
|
||||||
if err == sql.ErrNoRows {
|
if err == nil {
|
||||||
// FALLBACK: If stock item not found in tbStok at all, default to 1.
|
nStokTipiID = tmp
|
||||||
logger.Warn("stok tipi not found in tbStok, falling back to 1",
|
stockTypeByCode[rawSKodu] = nStokTipiID
|
||||||
"trace_id", traceID,
|
} else if err == sql.ErrNoRows {
|
||||||
"n_onml_no", nOnMLNo,
|
// keep 0 -> will fallback to 1 below
|
||||||
"n_onml_det_no", row.NOnMLDetNo,
|
nStokTipiID = 0
|
||||||
"s_kodu", rawSKodu,
|
stockTypeByCode[rawSKodu] = 0
|
||||||
)
|
|
||||||
nStokTipiID = 1
|
|
||||||
} else {
|
} else {
|
||||||
logger.Error("stok tipi lookup error", "err", err)
|
// Do not block save for stock type lookup failures.
|
||||||
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
|
// Most common cause: tbStok DB is temporarily unreachable (timeouts / bad connection).
|
||||||
return
|
logger.Error("stok tipi lookup error (per-row)", "err", err, "s_kodu", rawSKodu)
|
||||||
|
nStokTipiID = 1
|
||||||
|
stockTypeByCode[rawSKodu] = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.Info("stock type resolved", "s_kodu", rawSKodu, "n_stok_tipi_id", nStokTipiID)
|
|
||||||
if nStokTipiID <= 0 {
|
if nStokTipiID <= 0 {
|
||||||
// FALLBACK: If stock type is missing or 0 in tbStok, default to 1 (usually 'Raw Material' or 'General').
|
// FALLBACK: If stock type is missing or 0 in tbStok, default to 1 (usually 'Raw Material' or 'General').
|
||||||
// This prevents blocking the save process for items not fully configured in tbStok.
|
// This prevents blocking the save process for items not fully configured in tbStok.
|
||||||
logger.Warn("stok tipi <= 0, falling back to 1",
|
logger.Warn("stok tipi <= 0 (bulk), falling back to 1",
|
||||||
"trace_id", traceID,
|
"trace_id", traceID,
|
||||||
"n_onml_no", nOnMLNo,
|
"n_onml_no", nOnMLNo,
|
||||||
"n_onml_det_no", row.NOnMLDetNo,
|
"n_onml_det_no", row.NOnMLDetNo,
|
||||||
@@ -1886,8 +2062,9 @@ WHERE nUrtReceteID = @p1
|
|||||||
AND nUrtMBolumID = @p2
|
AND nUrtMBolumID = @p2
|
||||||
AND LTRIM(RTRIM(ISNULL(nHStokID_G,''))) = @p3
|
AND LTRIM(RTRIM(ISNULL(nHStokID_G,''))) = @p3
|
||||||
`, req.Header.NUrtReceteID, k.nUrtMBolumID, k.sKodu, q, user); err != nil {
|
`, req.Header.NUrtReceteID, k.nUrtMBolumID, k.sKodu, q, user); err != nil {
|
||||||
logger.Warn("recipe qty update failed", "trace_id", traceID, "n_urt_recete_id", req.Header.NUrtReceteID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu, "err", err)
|
logger.Error("recipe qty update failed", "trace_id", traceID, "n_urt_recete_id", req.Header.NUrtReceteID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu, "err", err)
|
||||||
continue
|
http.Error(w, "Recete miktar guncellemesi basarisiz", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
updated++
|
updated++
|
||||||
}
|
}
|
||||||
@@ -1899,7 +2076,9 @@ WHERE nUrtReceteID = @p1
|
|||||||
SELECT ISNULL(MAX(CONVERT(int, nUrtRecMBolumID)), 0) AS MaxID
|
SELECT ISNULL(MAX(CONVERT(int, nUrtRecMBolumID)), 0) AS MaxID
|
||||||
FROM dbo.spUrtRecMBolum WITH (UPDLOCK, HOLDLOCK)
|
FROM dbo.spUrtRecMBolum WITH (UPDLOCK, HOLDLOCK)
|
||||||
`).Scan(&baseID); err != nil {
|
`).Scan(&baseID); err != nil {
|
||||||
logger.Warn("recipe base id lookup failed (skipping inserts)", "trace_id", traceID, "err", err)
|
logger.Error("recipe base id lookup failed", "trace_id", traceID, "err", err)
|
||||||
|
http.Error(w, "Recete insert hazirligi basarisiz", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
inserted := 0
|
inserted := 0
|
||||||
nextID := baseID
|
nextID := baseID
|
||||||
@@ -1913,8 +2092,9 @@ FROM dbo.spUrtRecMBolum WITH (UPDLOCK, HOLDLOCK)
|
|||||||
if err := tx.QueryRowContext(ctx, `
|
if err := tx.QueryRowContext(ctx, `
|
||||||
SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.spUrtMBolum WITH (NOLOCK) WHERE nUrtMBolumID = @p1) THEN 1 ELSE 0 END
|
SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.spUrtMBolum WITH (NOLOCK) WHERE nUrtMBolumID = @p1) THEN 1 ELSE 0 END
|
||||||
`, k.nUrtMBolumID).Scan(&bolumExists); err != nil || bolumExists != 1 {
|
`, k.nUrtMBolumID).Scan(&bolumExists); err != nil || bolumExists != 1 {
|
||||||
logger.Warn("recipe insert skipped (missing spUrtMBolum FK)", "trace_id", traceID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu)
|
logger.Error("recipe insert blocked (missing spUrtMBolum FK)", "trace_id", traceID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu, "err", err)
|
||||||
continue
|
http.Error(w, "Recete insert engellendi (bolum FK yok)", http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nextID++
|
nextID++
|
||||||
@@ -1960,8 +2140,9 @@ VALUES (
|
|||||||
@p7,GETDATE()
|
@p7,GETDATE()
|
||||||
)
|
)
|
||||||
`, nextID, req.Header.NUrtReceteID, nUrtUBolumID, k.nUrtMBolumID, k.sKodu, q, user); err != nil {
|
`, nextID, req.Header.NUrtReceteID, nUrtUBolumID, k.nUrtMBolumID, k.sKodu, q, user); err != nil {
|
||||||
logger.Warn("recipe insert failed", "trace_id", traceID, "n_urt_recete_id", req.Header.NUrtReceteID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu, "err", err)
|
logger.Error("recipe insert failed", "trace_id", traceID, "n_urt_recete_id", req.Header.NUrtReceteID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu, "err", err)
|
||||||
continue
|
http.Error(w, "Recete insert basarisiz", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
inserted++
|
inserted++
|
||||||
}
|
}
|
||||||
@@ -1978,18 +2159,227 @@ VALUES (
|
|||||||
committed = true
|
committed = true
|
||||||
logger.Info("tx commit ok", "trace_id", traceID, "n_onml_no", nOnMLNo)
|
logger.Info("tx commit ok", "trace_id", traceID, "n_onml_no", nOnMLNo)
|
||||||
|
|
||||||
// V3: update base price table so pricing screens reflect latest costing.
|
// Post-commit async tasks (save latency reduction):
|
||||||
// Not transactional with URETIM DB; if this fails, URETIM save has already succeeded.
|
// - V3 base price upsert (MSSQL)
|
||||||
if mssqlDB != nil {
|
// - Costing mail send (Graph + Postgres mappings)
|
||||||
logger.Info("post-commit step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "v3_base_price_upsert")
|
// - Last10 avg deviation warnings (MSSQL cache -> Postgres panel)
|
||||||
if err := queries.UpsertV3ItemBasePriceUSD(ctx, mssqlDB, req.Header.UrunKodu, req.Header.MaliyetTarihi, totalUSD, user); err != nil {
|
//
|
||||||
logger.Error("v3 base price upsert error", "err", err)
|
// These must NOT block the HTTP response. They are retried with backoff and only logged on failure.
|
||||||
http.Error(w, "URETIM kaydedildi ama V3 maliyet guncellenemedi", http.StatusInternalServerError)
|
{
|
||||||
|
reqCopy := req
|
||||||
|
if len(req.Detail.Upserts) > 0 {
|
||||||
|
up := make([]models.ProductionProductCostingOnMLSaveDetailUpsertRow, len(req.Detail.Upserts))
|
||||||
|
copy(up, req.Detail.Upserts)
|
||||||
|
reqCopy.Detail.Upserts = up
|
||||||
|
}
|
||||||
|
if len(req.Detail.Deletes) > 0 {
|
||||||
|
del := make([]models.ProductionProductCostingOnMLSaveDetailDeleteRow, len(req.Detail.Deletes))
|
||||||
|
copy(del, req.Detail.Deletes)
|
||||||
|
reqCopy.Detail.Deletes = del
|
||||||
|
}
|
||||||
|
|
||||||
|
actorUser := user
|
||||||
|
nOnMLNoLocal := nOnMLNo
|
||||||
|
isUpdateLocal := isUpdate
|
||||||
|
urunKoduLocal := strings.TrimSpace(req.Header.UrunKodu)
|
||||||
|
maliyetTarihiLocal := strings.TrimSpace(req.Header.MaliyetTarihi)
|
||||||
|
totalUSDLocal := totalUSD
|
||||||
|
totalTRYLocal := totalTRY
|
||||||
|
totalEURLocal := totalEUR
|
||||||
|
usdRateLocal := usdRate
|
||||||
|
eurRateLocal := eurRate
|
||||||
|
gbpRateLocal := gbpRate
|
||||||
|
mssqlLocal := mssqlDB
|
||||||
|
uretimLocal := uretimDB
|
||||||
|
pgLocal := db.PgDB
|
||||||
|
mlLocal := ml
|
||||||
|
traceIDLocal := traceID
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
bg := context.Background()
|
||||||
|
bg = utils.ContextWithTraceID(bg, traceIDLocal)
|
||||||
|
bgLogger := utils.SlogFromContext(bg).With("handler", "production-product-costing.onml.save.post-commit", "n_onml_no", nOnMLNoLocal)
|
||||||
|
|
||||||
|
// 1) V3 base price upsert: retry 3 times with backoff.
|
||||||
|
if mssqlLocal != nil && urunKoduLocal != "" && maliyetTarihiLocal != "" {
|
||||||
|
backoff := []time.Duration{300 * time.Millisecond, 1200 * time.Millisecond, 3500 * time.Millisecond}
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < len(backoff)+1; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
time.Sleep(backoff[attempt-1])
|
||||||
|
}
|
||||||
|
stepCtx, cancel := context.WithTimeout(bg, 10*time.Second)
|
||||||
|
err := queries.UpsertV3ItemBasePriceUSD(stepCtx, mssqlLocal, urunKoduLocal, maliyetTarihiLocal, totalUSDLocal, actorUser)
|
||||||
|
cancel()
|
||||||
|
if err == nil {
|
||||||
|
bgLogger.Info("post-commit ok", "step", "v3_base_price_upsert")
|
||||||
|
lastErr = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
bgLogger.Warn("post-commit retry", "step", "v3_base_price_upsert", "attempt", attempt+1, "err", err)
|
||||||
|
}
|
||||||
|
if lastErr != nil {
|
||||||
|
bgLogger.Error("post-commit failed", "step", "v3_base_price_upsert", "err", lastErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bgLogger.Info("post-commit skipped", "step", "v3_base_price_upsert")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Costing mail: retry 2 times with backoff.
|
||||||
|
if mlLocal != nil && pgLocal != nil && mssqlLocal != nil {
|
||||||
|
backoff := []time.Duration{800 * time.Millisecond, 2500 * time.Millisecond}
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < len(backoff)+1; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
time.Sleep(backoff[attempt-1])
|
||||||
|
}
|
||||||
|
stepCtx, cancel := context.WithTimeout(bg, 25*time.Second)
|
||||||
|
err := sendCostingSummaryMail(stepCtx, pgLocal, mssqlLocal, uretimLocal, mlLocal, reqCopy, nOnMLNoLocal, isUpdateLocal, usdRateLocal, eurRateLocal, gbpRateLocal, totalUSDLocal, totalTRYLocal, totalEURLocal, actorUser)
|
||||||
|
cancel()
|
||||||
|
if err == nil {
|
||||||
|
bgLogger.Info("post-commit ok", "step", "costing_mail_send")
|
||||||
|
lastErr = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
bgLogger.Warn("post-commit retry", "step", "costing_mail_send", "attempt", attempt+1, "err", err)
|
||||||
|
}
|
||||||
|
if lastErr != nil {
|
||||||
|
bgLogger.Error("post-commit failed", "step", "costing_mail_send", "err", lastErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bgLogger.Info("post-commit skipped", "step", "costing_mail_send")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Last10 avg deviation warnings: dedupe by (code,currency), read from MSSQL cache, write to Postgres table.
|
||||||
|
// Keep generous timeouts: this is async and should succeed on slow networks.
|
||||||
|
if pgLocal != nil && mssqlLocal != nil && len(reqCopy.Detail.Upserts) > 0 {
|
||||||
|
_, cancelBoot := context.WithTimeout(bg, 5*time.Second)
|
||||||
|
if err := queries.EnsureProductionCostingLast10WarningTables(pgLocal); err != nil {
|
||||||
|
cancelBoot()
|
||||||
|
bgLogger.Error("post-commit failed", "step", "last10_warning_bootstrap", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
cancelBoot()
|
||||||
|
|
||||||
|
// dedupe input by code+currency (USD basis comparison)
|
||||||
|
inputByKey := map[string]float64{}
|
||||||
|
inputUSDByKey := map[string]float64{}
|
||||||
|
codes := make([]string, 0, len(reqCopy.Detail.Upserts))
|
||||||
|
seenCode := map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, row := range reqCopy.Detail.Upserts {
|
||||||
|
code := strings.TrimSpace(row.SKodu)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz))
|
||||||
|
if cur == "" {
|
||||||
|
cur = "USD"
|
||||||
|
}
|
||||||
|
in := row.FiyatGirilen
|
||||||
|
if in <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := code + "|" + cur
|
||||||
|
if _, ok := inputByKey[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inputByKey[key] = in
|
||||||
|
|
||||||
|
// input USD basis
|
||||||
|
inUSD := 0.0
|
||||||
|
switch cur {
|
||||||
|
case "USD":
|
||||||
|
inUSD = in
|
||||||
|
case "TRY", "TL":
|
||||||
|
if usdRateLocal > 0 {
|
||||||
|
inUSD = in / usdRateLocal
|
||||||
|
}
|
||||||
|
case "EUR":
|
||||||
|
if usdRateLocal > 0 && eurRateLocal > 0 {
|
||||||
|
inUSD = (in * eurRateLocal) / usdRateLocal
|
||||||
|
}
|
||||||
|
case "GBP":
|
||||||
|
if usdRateLocal > 0 && gbpRateLocal > 0 {
|
||||||
|
inUSD = (in * gbpRateLocal) / usdRateLocal
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
inUSD = in
|
||||||
|
}
|
||||||
|
inputUSDByKey[key] = inUSD
|
||||||
|
|
||||||
|
if _, ok := seenCode[code]; !ok {
|
||||||
|
seenCode[code] = struct{}{}
|
||||||
|
codes = append(codes, code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingOnMLSaveResponse{NOnMLNo: nOnMLNo})
|
lookupCtx, cancelLookup := context.WithTimeout(bg, 6*time.Second)
|
||||||
|
avgRows, err := queries.LookupLast10AvgPurchasePriceByItemCodes(lookupCtx, mssqlLocal, codes)
|
||||||
|
cancelLookup()
|
||||||
|
if err != nil {
|
||||||
|
bgLogger.Warn("post-commit failed", "step", "last10_cache_lookup", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
avgUSDByKey := map[string]float64{}
|
||||||
|
for _, ar := range avgRows {
|
||||||
|
code := strings.TrimSpace(ar.ItemCode)
|
||||||
|
cur := strings.ToUpper(strings.TrimSpace(ar.CurrencyCode))
|
||||||
|
if code == "" || cur == "" || ar.SampleCount <= 0 || ar.AvgDocPrice <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := code + "|" + cur
|
||||||
|
avgUSD := 0.0
|
||||||
|
switch cur {
|
||||||
|
case "USD":
|
||||||
|
avgUSD = ar.AvgDocPrice
|
||||||
|
case "TRY", "TL":
|
||||||
|
if usdRateLocal > 0 {
|
||||||
|
avgUSD = ar.AvgDocPrice / usdRateLocal
|
||||||
|
}
|
||||||
|
case "EUR":
|
||||||
|
if usdRateLocal > 0 && eurRateLocal > 0 {
|
||||||
|
avgUSD = (ar.AvgDocPrice * eurRateLocal) / usdRateLocal
|
||||||
|
}
|
||||||
|
case "GBP":
|
||||||
|
if usdRateLocal > 0 && gbpRateLocal > 0 {
|
||||||
|
avgUSD = (ar.AvgDocPrice * gbpRateLocal) / usdRateLocal
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
avgUSD = ar.AvgDocPrice
|
||||||
|
}
|
||||||
|
avgUSDByKey[key] = avgUSD
|
||||||
|
}
|
||||||
|
|
||||||
|
writeCtx, cancelWrite := context.WithTimeout(bg, 12*time.Second)
|
||||||
|
err = queries.ReplaceProductionCostingLast10Warnings(
|
||||||
|
writeCtx,
|
||||||
|
pgLocal,
|
||||||
|
nOnMLNoLocal,
|
||||||
|
urunKoduLocal,
|
||||||
|
maliyetTarihiLocal,
|
||||||
|
actorUser,
|
||||||
|
avgRows,
|
||||||
|
inputByKey,
|
||||||
|
inputUSDByKey,
|
||||||
|
avgUSDByKey,
|
||||||
|
)
|
||||||
|
cancelWrite()
|
||||||
|
if err != nil {
|
||||||
|
bgLogger.Error("post-commit failed", "step", "last10_warning_write", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bgLogger.Info("post-commit ok", "step", "last10_warning_write", "keys", len(inputByKey), "codes", len(codes), "cache_rows", len(avgRows))
|
||||||
|
} else {
|
||||||
|
bgLogger.Info("post-commit skipped", "step", "last10_warning_write")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingOnMLSaveResponse{NOnMLNo: nOnMLNo, Warnings: warnings})
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/pricing/production-product-costing/onml/delete
|
// POST /api/pricing/production-product-costing/onml/delete
|
||||||
@@ -2206,7 +2596,6 @@ func PostProductionHasCostDetailBulkPricesHandler(w http.ResponseWriter, r *http
|
|||||||
|
|
||||||
costDate := strings.TrimSpace(req.MaliyetTarihi)
|
costDate := strings.TrimSpace(req.MaliyetTarihi)
|
||||||
itemsCount := len(req.Items)
|
itemsCount := len(req.Items)
|
||||||
responseChan := make(chan *models.ProductionHasCostDetailBulkPriceRow, itemsCount)
|
|
||||||
|
|
||||||
logger.Info("request start",
|
logger.Info("request start",
|
||||||
"n_onml_no", strings.TrimSpace(req.NOnMLNo),
|
"n_onml_no", strings.TrimSpace(req.NOnMLNo),
|
||||||
@@ -2216,38 +2605,112 @@ func PostProductionHasCostDetailBulkPricesHandler(w http.ResponseWriter, r *http
|
|||||||
)
|
)
|
||||||
log.Printf("[ProductionHasCostDetailBulkPrices] start n_onml_no=%s urun_kodu=%s maliyet_tarihi=%s item_count=%d", strings.TrimSpace(req.NOnMLNo), strings.TrimSpace(req.UrunKodu), costDate, itemsCount)
|
log.Printf("[ProductionHasCostDetailBulkPrices] start n_onml_no=%s urun_kodu=%s maliyet_tarihi=%s item_count=%d", strings.TrimSpace(req.NOnMLNo), strings.TrimSpace(req.UrunKodu), costDate, itemsCount)
|
||||||
|
|
||||||
for _, item := range req.Items {
|
// Bulk query (single roundtrip): send request items as JSON and resolve latest purchase price before cost date.
|
||||||
go func(item models.ProductionHasCostDetailPriceLookupItem) {
|
type bulkReqItem struct {
|
||||||
sKodu := normalizeLookupValue(item.SKodu)
|
RowKey string `json:"rowKey"`
|
||||||
|
SKodu string `json:"sKodu"`
|
||||||
|
ColorCode string `json:"colorCode"`
|
||||||
|
ItemDim1Code string `json:"itemDim1Code"`
|
||||||
|
}
|
||||||
|
reqItems := make([]bulkReqItem, 0, itemsCount)
|
||||||
|
metaByRowKey := map[string]models.ProductionHasCostDetailPriceLookupItem{}
|
||||||
|
for _, it := range req.Items {
|
||||||
|
sKodu := normalizeLookupValue(it.SKodu)
|
||||||
if sKodu == "" {
|
if sKodu == "" {
|
||||||
responseChan <- nil
|
continue
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
colorCode := firstNonEmptyString(
|
colorCode := firstNonEmptyString(
|
||||||
normalizeLookupValue(item.ColorCode),
|
normalizeLookupValue(it.ColorCode),
|
||||||
normalizeLookupValue(item.SRenk),
|
normalizeLookupValue(it.SRenk),
|
||||||
)
|
)
|
||||||
itemDim1Code := firstNonEmptyString(
|
itemDim1Code := firstNonEmptyString(normalizeLookupValue(it.ItemDim1Code))
|
||||||
normalizeLookupValue(item.ItemDim1Code),
|
rowKey := strings.TrimSpace(it.RowKey)
|
||||||
)
|
if rowKey == "" {
|
||||||
|
// keep a stable key even if UI didn't pass it (should not happen).
|
||||||
row, err := queries.GetProductionHasCostLatestPurchasePriceForItem(
|
rowKey = strings.TrimSpace(it.NOnMLDetNo + "|" + sKodu)
|
||||||
ctx,
|
}
|
||||||
mssqlDB,
|
reqItems = append(reqItems, bulkReqItem{RowKey: rowKey, SKodu: sKodu, ColorCode: colorCode, ItemDim1Code: itemDim1Code})
|
||||||
sKodu,
|
metaByRowKey[rowKey] = it
|
||||||
colorCode,
|
|
||||||
itemDim1Code,
|
|
||||||
costDate,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("item lookup error", "s_kodu", sKodu, "color_code", colorCode, "item_dim1_code", itemDim1Code, "err", err)
|
|
||||||
responseChan <- nil
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemsJSONBytes, err := json.Marshal(reqItems)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("bulk request invalid", "reason", "items json marshal failed", "err", err)
|
||||||
|
http.Error(w, "Toplu fiyat verisi hazirlanamadi", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := queries.GetProductionHasCostLatestPurchasePricesForItems(ctx, mssqlDB, string(itemsJSONBytes), costDate)
|
||||||
|
response := make([]models.ProductionHasCostDetailBulkPriceRow, 0, len(reqItems))
|
||||||
|
if err != nil {
|
||||||
|
// Fallback: some MSSQL instances are on low compatibility level and don't support OPENJSON.
|
||||||
|
// In that case, fall back to the legacy per-item lookup but with bounded concurrency.
|
||||||
|
logger.Warn("bulk lookup error (fallback to per-item)", "err", err)
|
||||||
|
type job struct {
|
||||||
|
rowKey string
|
||||||
|
sKodu string
|
||||||
|
colorCode string
|
||||||
|
itemDim1Code string
|
||||||
|
}
|
||||||
|
jobs := make(chan job, len(reqItems))
|
||||||
|
results := make(chan *models.ProductionHasCostDetailBulkPriceRow, len(reqItems))
|
||||||
|
|
||||||
|
worker := func() {
|
||||||
|
for j := range jobs {
|
||||||
|
row, qerr := queries.GetProductionHasCostLatestPurchasePriceForItem(ctx, mssqlDB, j.sKodu, j.colorCode, j.itemDim1Code, costDate)
|
||||||
|
if qerr != nil {
|
||||||
|
results <- nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var res models.ProductionHasCostDetailBulkPriceRow
|
||||||
|
if serr := row.Scan(
|
||||||
|
&res.PriceType,
|
||||||
|
&res.Tarih,
|
||||||
|
&res.FaturaKodu,
|
||||||
|
&res.MasrafKodu,
|
||||||
|
&res.MasrafDetay,
|
||||||
|
&res.ColorCode,
|
||||||
|
&res.ColorDescription,
|
||||||
|
&res.ItemDim1Code,
|
||||||
|
&res.ItemDim1Description,
|
||||||
|
&res.FiyatGirilen,
|
||||||
|
&res.FiyatDoviz,
|
||||||
|
); serr != nil {
|
||||||
|
results <- nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
meta := metaByRowKey[j.rowKey]
|
||||||
|
res.RowKey = strings.TrimSpace(meta.RowKey)
|
||||||
|
if res.RowKey == "" {
|
||||||
|
res.RowKey = j.rowKey
|
||||||
|
}
|
||||||
|
res.NOnMLDetNo = strings.TrimSpace(meta.NOnMLDetNo)
|
||||||
|
res.NHammaddeTuruNo = strings.TrimSpace(meta.NHammaddeTuruNo)
|
||||||
|
res.SKodu = normalizeLookupValue(meta.SKodu)
|
||||||
|
results <- &res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workerCount := 10
|
||||||
|
for i := 0; i < workerCount; i++ {
|
||||||
|
go worker()
|
||||||
|
}
|
||||||
|
for _, it := range reqItems {
|
||||||
|
jobs <- job{rowKey: it.RowKey, sKodu: it.SKodu, colorCode: it.ColorCode, itemDim1Code: it.ItemDim1Code}
|
||||||
|
}
|
||||||
|
close(jobs)
|
||||||
|
for i := 0; i < len(reqItems); i++ {
|
||||||
|
r := <-results
|
||||||
|
if r != nil {
|
||||||
|
response = append(response, *r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var rowKey string
|
||||||
var result models.ProductionHasCostDetailBulkPriceRow
|
var result models.ProductionHasCostDetailBulkPriceRow
|
||||||
if err := row.Scan(
|
if err := rows.Scan(
|
||||||
|
&rowKey,
|
||||||
&result.PriceType,
|
&result.PriceType,
|
||||||
&result.Tarih,
|
&result.Tarih,
|
||||||
&result.FaturaKodu,
|
&result.FaturaKodu,
|
||||||
@@ -2260,32 +2723,21 @@ func PostProductionHasCostDetailBulkPricesHandler(w http.ResponseWriter, r *http
|
|||||||
&result.FiyatGirilen,
|
&result.FiyatGirilen,
|
||||||
&result.FiyatDoviz,
|
&result.FiyatDoviz,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Warn("item scan error", "s_kodu", sKodu, "color_code", colorCode, "item_dim1_code", itemDim1Code, "err", err)
|
logger.Warn("bulk scan error", "err", err)
|
||||||
responseChan <- nil
|
continue
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
meta := metaByRowKey[rowKey]
|
||||||
result.RowKey = strings.TrimSpace(item.RowKey)
|
result.RowKey = strings.TrimSpace(meta.RowKey)
|
||||||
result.NOnMLDetNo = strings.TrimSpace(item.NOnMLDetNo)
|
if result.RowKey == "" {
|
||||||
result.NHammaddeTuruNo = strings.TrimSpace(item.NHammaddeTuruNo)
|
result.RowKey = rowKey
|
||||||
result.SKodu = sKodu
|
|
||||||
|
|
||||||
if strings.TrimSpace(result.ColorCode) == "" {
|
|
||||||
result.ColorCode = colorCode
|
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(result.ItemDim1Code) == "" {
|
result.NOnMLDetNo = strings.TrimSpace(meta.NOnMLDetNo)
|
||||||
result.ItemDim1Code = itemDim1Code
|
result.NHammaddeTuruNo = strings.TrimSpace(meta.NHammaddeTuruNo)
|
||||||
|
result.SKodu = normalizeLookupValue(meta.SKodu)
|
||||||
|
response = append(response, result)
|
||||||
}
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
responseChan <- &result
|
logger.Warn("bulk rows error", "err", err)
|
||||||
}(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
response := make([]models.ProductionHasCostDetailBulkPriceRow, 0, itemsCount)
|
|
||||||
for i := 0; i < itemsCount; i++ {
|
|
||||||
res := <-responseChan
|
|
||||||
if res != nil {
|
|
||||||
response = append(response, *res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
47
svc/routes/production_product_costing_last10_warnings.go
Normal file
47
svc/routes/production_product_costing_last10_warnings.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/db"
|
||||||
|
"bssapp-backend/queries"
|
||||||
|
"bssapp-backend/utils"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /api/pricing/production-product-costing/last10-warnings?n_onml_no=100001
|
||||||
|
func GetProductionProductCostingLast10WarningsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
pg := db.PgDB
|
||||||
|
if pg == nil {
|
||||||
|
http.Error(w, "Postgres baglantisi aktif degil", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := queries.EnsureProductionCostingLast10WarningTables(pg); err != nil {
|
||||||
|
http.Error(w, "warning table bootstrap error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nStr := strings.TrimSpace(r.URL.Query().Get("n_onml_no"))
|
||||||
|
n, _ := strconv.Atoi(nStr)
|
||||||
|
if n <= 0 {
|
||||||
|
http.Error(w, "n_onml_no zorunlu", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
|
||||||
|
list, err := queries.ListProductionCostingLast10WarningsByOnMLNo(ctx, pg, n)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"n_onml_no": n,
|
||||||
|
"count": len(list),
|
||||||
|
"items": list,
|
||||||
|
})
|
||||||
|
}
|
||||||
719
svc/routes/production_product_costing_mail.go
Normal file
719
svc/routes/production_product_costing_mail.go
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bssapp-backend/internal/mailer"
|
||||||
|
"bssapp-backend/models"
|
||||||
|
"bssapp-backend/queries"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mailBucket struct {
|
||||||
|
InputByCur map[string]float64
|
||||||
|
USD float64
|
||||||
|
TRY float64
|
||||||
|
HasCMT bool
|
||||||
|
// Fabric-only helpers (for UI parity)
|
||||||
|
MeterQty float64
|
||||||
|
MeterUom string
|
||||||
|
UnitIn float64
|
||||||
|
UnitCur string
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDateTR2(t time.Time) string {
|
||||||
|
if t.IsZero() {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
// dd.MM.yyyy
|
||||||
|
return t.Format("02.01.2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDateTimeTR2(t time.Time) string {
|
||||||
|
if t.IsZero() {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
// dd.MM.yyyy HH:mm
|
||||||
|
return t.Format("02.01.2006 15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAnyDateTimeTR2(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
// Common MSSQL string renderings (best-effort).
|
||||||
|
layouts := []string{
|
||||||
|
"2006-01-02 15:04:05.9999999",
|
||||||
|
"2006-01-02 15:04:05.999999",
|
||||||
|
"2006-01-02 15:04:05.999",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
time.RFC3339Nano,
|
||||||
|
time.RFC3339,
|
||||||
|
"2006-01-02T15:04:05",
|
||||||
|
"2006-01-02",
|
||||||
|
}
|
||||||
|
for _, layout := range layouts {
|
||||||
|
if t, err := time.Parse(layout, s); err == nil {
|
||||||
|
// Date-only vs datetime
|
||||||
|
if layout == "2006-01-02" {
|
||||||
|
return formatDateTR2(t)
|
||||||
|
}
|
||||||
|
return formatDateTimeTR2(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func addInputAmount(b *mailBucket, cur string, amount float64) {
|
||||||
|
if math.IsNaN(amount) || math.IsInf(amount, 0) || amount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if b.InputByCur == nil {
|
||||||
|
b.InputByCur = map[string]float64{}
|
||||||
|
}
|
||||||
|
b.InputByCur[cur] = b.InputByCur[cur] + amount
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMoney2(v float64) string {
|
||||||
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
// Keep 2 decimals with dot (mail clients).
|
||||||
|
return fmt.Sprintf("%.2f", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatQty2(v float64) string {
|
||||||
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePartFromMtBolumTitle(title string) string {
|
||||||
|
v := strings.ToUpper(strings.TrimSpace(title))
|
||||||
|
if v == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Keep the part name dynamic (UI shows spUrtMTBolum.sAdi). Still normalize a few aliases.
|
||||||
|
switch {
|
||||||
|
case strings.Contains(v, "AKSESUAR") || strings.Contains(v, "AKS"):
|
||||||
|
return "AKSESUAR"
|
||||||
|
default:
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeInputByCurrency(b *mailBucket) (amountLabel string, curLabel string) {
|
||||||
|
if b == nil || len(b.InputByCur) == 0 {
|
||||||
|
return "-", "-"
|
||||||
|
}
|
||||||
|
curs := make([]string, 0, len(b.InputByCur))
|
||||||
|
for c := range b.InputByCur {
|
||||||
|
c = strings.ToUpper(strings.TrimSpace(c))
|
||||||
|
if c == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
curs = append(curs, c)
|
||||||
|
}
|
||||||
|
sort.Strings(curs)
|
||||||
|
if len(curs) == 0 {
|
||||||
|
return "-", "-"
|
||||||
|
}
|
||||||
|
if len(curs) == 1 {
|
||||||
|
c := curs[0]
|
||||||
|
return formatMoney2(b.InputByCur[c]), c
|
||||||
|
}
|
||||||
|
sum := 0.0
|
||||||
|
for _, c := range curs {
|
||||||
|
sum += b.InputByCur[c]
|
||||||
|
}
|
||||||
|
return formatMoney2(sum), "MIX"
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMeterLabel(b *mailBucket) string {
|
||||||
|
if b == nil || !(b.MeterQty > 0) {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
u := strings.TrimSpace(b.MeterUom)
|
||||||
|
if u == "" {
|
||||||
|
u = "MT"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", formatQty2(b.MeterQty), u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCostingRecipients(pg *sql.DB, firstGroupCode string) ([]string, error) {
|
||||||
|
rows, err := pg.Query(`
|
||||||
|
SELECT DISTINCT TRIM(m.email) AS email
|
||||||
|
FROM mk_costing_first_group_mail f
|
||||||
|
JOIN mk_mail m
|
||||||
|
ON m.id = f.mail_id
|
||||||
|
WHERE m.is_active = true
|
||||||
|
AND COALESCE(TRIM(m.email), '') <> ''
|
||||||
|
AND UPPER(TRIM(f.urun_ilk_grubu)) = UPPER(TRIM($1))
|
||||||
|
ORDER BY email
|
||||||
|
`, strings.TrimSpace(firstGroupCode))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]string, 0, 16)
|
||||||
|
for rows.Next() {
|
||||||
|
var email string
|
||||||
|
if err := rows.Scan(&email); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
email = strings.TrimSpace(email)
|
||||||
|
if email != "" {
|
||||||
|
out = append(out, email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendCostingSummaryMail(
|
||||||
|
ctx context.Context,
|
||||||
|
pg *sql.DB,
|
||||||
|
mssql *sql.DB,
|
||||||
|
uretim *sql.DB,
|
||||||
|
ml *mailer.GraphMailer,
|
||||||
|
req models.ProductionProductCostingOnMLSaveRequest,
|
||||||
|
nOnMLNo int,
|
||||||
|
isUpdate bool,
|
||||||
|
usdRate float64,
|
||||||
|
eurRate float64,
|
||||||
|
gbpRate float64,
|
||||||
|
totalUSD float64,
|
||||||
|
totalTRY float64,
|
||||||
|
totalEUR float64,
|
||||||
|
actor string,
|
||||||
|
) error {
|
||||||
|
if ml == nil {
|
||||||
|
return fmt.Errorf("mailer not initialized")
|
||||||
|
}
|
||||||
|
if pg == nil || mssql == nil {
|
||||||
|
return fmt.Errorf("db not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure mapping tables exist (first save can happen before mapping screens are visited).
|
||||||
|
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||||
|
return fmt.Errorf("mapping table bootstrap error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstGroupCode, _, err := queries.GetProductFirstGroupCodeDescByUrunKodu(ctx, mssql, req.Header.UrunKodu)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("first group resolve error: %w", err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(firstGroupCode) == "" {
|
||||||
|
return fmt.Errorf("first group code not found for product")
|
||||||
|
}
|
||||||
|
|
||||||
|
recipients, err := loadCostingRecipients(pg, firstGroupCode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("recipient query error: %w", err)
|
||||||
|
}
|
||||||
|
if len(recipients) == 0 {
|
||||||
|
// Don't hard fail; mapping might be intentionally empty.
|
||||||
|
return fmt.Errorf("no costing mail mapping for first group: %s", firstGroupCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the same header payload used by UI (best-effort) so the mail can show every header label.
|
||||||
|
var uiHeader models.ProductionHasCostDetailHeader
|
||||||
|
uiHeaderLoaded := false
|
||||||
|
if uretim != nil && nOnMLNo > 0 {
|
||||||
|
row, err := queries.GetProductionHasCostDetailHeaderByOnMLNo(ctx, uretim, nOnMLNo)
|
||||||
|
if err == nil && row != nil {
|
||||||
|
// Keep scan fields aligned with GetProductionHasCostDetailHeaderHandler
|
||||||
|
if err := row.Scan(
|
||||||
|
&uiHeader.UretimiYapanFirma,
|
||||||
|
&uiHeader.SonIsEmriVeren,
|
||||||
|
&uiHeader.FirmaKodu,
|
||||||
|
&uiHeader.NFirmaID,
|
||||||
|
&uiHeader.NOnMLNo,
|
||||||
|
&uiHeader.UrunKodu,
|
||||||
|
&uiHeader.UrunAdi,
|
||||||
|
&uiHeader.UretimSekliID,
|
||||||
|
&uiHeader.UretimSekli,
|
||||||
|
&uiHeader.MaliyetTarihi,
|
||||||
|
&uiHeader.DteKayitTarihi,
|
||||||
|
&uiHeader.SKullaniciAdi,
|
||||||
|
&uiHeader.LTutarTL,
|
||||||
|
&uiHeader.LTutarUSD,
|
||||||
|
&uiHeader.LTutarEURO,
|
||||||
|
&uiHeader.LTutarGBP,
|
||||||
|
&uiHeader.SDovizCinsi,
|
||||||
|
&uiHeader.LTutarDoviz,
|
||||||
|
&uiHeader.DteGuncellemeTarihi,
|
||||||
|
&uiHeader.SGuncellemeKullaniciAdi,
|
||||||
|
&uiHeader.NUrtReceteID,
|
||||||
|
); err == nil {
|
||||||
|
uiHeaderLoaded = true
|
||||||
|
if mssql != nil {
|
||||||
|
ilk, ana, alt, _ := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssql, uiHeader.UrunKodu)
|
||||||
|
uiHeader.UrunIlkGrubu = ilk
|
||||||
|
uiHeader.UrunAnaGrubu = ana
|
||||||
|
uiHeader.UrunAltGrubu = alt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich header (fallback) if UI header wasn't loadable.
|
||||||
|
ilkGrup, anaGrup, altGrup := "", "", ""
|
||||||
|
if uiHeaderLoaded {
|
||||||
|
ilkGrup, anaGrup, altGrup = strings.TrimSpace(uiHeader.UrunIlkGrubu), strings.TrimSpace(uiHeader.UrunAnaGrubu), strings.TrimSpace(uiHeader.UrunAltGrubu)
|
||||||
|
} else if mssql != nil {
|
||||||
|
ilkGrup, anaGrup, altGrup, _ = queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssql, req.Header.UrunKodu)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve MT bolum titles so we can bucket rows into CEKET/PANTOLON/YELEK/AKSESUAR.
|
||||||
|
mtTitleByID := map[int]string{}
|
||||||
|
if uretim != nil {
|
||||||
|
ids := make([]int, 0, len(req.Detail.Upserts))
|
||||||
|
seen := map[int]struct{}{}
|
||||||
|
for _, row := range req.Detail.Upserts {
|
||||||
|
if row.NUrtMTBolumID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[row.NUrtMTBolumID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[row.NUrtMTBolumID] = struct{}{}
|
||||||
|
ids = append(ids, row.NUrtMTBolumID)
|
||||||
|
}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
vals := make([]string, 0, len(ids))
|
||||||
|
args := make([]any, 0, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
vals = append(vals, fmt.Sprintf("(@p%d)", i+1))
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
q := fmt.Sprintf(`
|
||||||
|
WITH X AS (SELECT CONVERT(int, V.id) AS id FROM (VALUES %s) AS V(id))
|
||||||
|
SELECT X.id, LTRIM(RTRIM(ISNULL(M.sAdi,''))) AS title
|
||||||
|
FROM X
|
||||||
|
LEFT JOIN dbo.spUrtMTBolum M WITH (NOLOCK)
|
||||||
|
ON M.nUrtMTBolumID = X.id
|
||||||
|
`, strings.Join(vals, ","))
|
||||||
|
rows, err := uretim.QueryContext(ctx, q, args...)
|
||||||
|
if err == nil {
|
||||||
|
for rows.Next() {
|
||||||
|
var id int
|
||||||
|
var title string
|
||||||
|
if err := rows.Scan(&id, &title); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mtTitleByID[id] = strings.TrimSpace(title)
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic part list derived from detail rows.
|
||||||
|
preferred := []string{"CEKET", "PANTOLON", "YELEK", "AKSESUAR", "YAKA"}
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
dynamic := make([]string, 0, 16)
|
||||||
|
|
||||||
|
for _, row := range req.Detail.Upserts {
|
||||||
|
group := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
|
||||||
|
included := strings.Contains(group, "CM2") || strings.Contains(group, "CM1") || row.MaliyeteDahil == 1
|
||||||
|
if !included {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
part := ""
|
||||||
|
if t := mtTitleByID[row.NUrtMTBolumID]; t != "" {
|
||||||
|
part = normalizePartFromMtBolumTitle(t)
|
||||||
|
}
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[part]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[part] = struct{}{}
|
||||||
|
dynamic = append(dynamic, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := make([]string, 0, len(dynamic)+len(preferred))
|
||||||
|
for _, p := range preferred {
|
||||||
|
if _, ok := seen[p]; ok {
|
||||||
|
parts = append(parts, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, p := range dynamic {
|
||||||
|
isPreferred := false
|
||||||
|
for _, pref := range preferred {
|
||||||
|
if pref == p {
|
||||||
|
isPreferred = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isPreferred {
|
||||||
|
parts = append(parts, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
parts = []string{"TANIMSIZ"}
|
||||||
|
}
|
||||||
|
labor := map[string]*mailBucket{}
|
||||||
|
material := map[string]*mailBucket{}
|
||||||
|
fabric := map[string]*mailBucket{}
|
||||||
|
for _, p := range parts {
|
||||||
|
labor[p] = &mailBucket{}
|
||||||
|
material[p] = &mailBucket{}
|
||||||
|
fabric[p] = &mailBucket{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range req.Detail.Upserts {
|
||||||
|
group := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
|
||||||
|
cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz))
|
||||||
|
qty := row.LMiktar
|
||||||
|
if qty < 0 {
|
||||||
|
qty = 0
|
||||||
|
}
|
||||||
|
in := row.FiyatGirilen
|
||||||
|
|
||||||
|
// Included rule: CM2 always; others only when maliyete_dahil = 1.
|
||||||
|
included := strings.Contains(group, "CM2") || strings.Contains(group, "CM1") || row.MaliyeteDahil == 1
|
||||||
|
if !included {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part bucket:
|
||||||
|
part := ""
|
||||||
|
if t := mtTitleByID[row.NUrtMTBolumID]; t != "" {
|
||||||
|
part = normalizePartFromMtBolumTitle(t)
|
||||||
|
}
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert input to TRY unit
|
||||||
|
unitTRY := in
|
||||||
|
switch cur {
|
||||||
|
case "USD":
|
||||||
|
unitTRY = in * usdRate
|
||||||
|
case "EUR":
|
||||||
|
unitTRY = in * eurRate
|
||||||
|
case "GBP":
|
||||||
|
unitTRY = in * gbpRate
|
||||||
|
case "TRY", "TL", "":
|
||||||
|
unitTRY = in
|
||||||
|
default:
|
||||||
|
unitTRY = in
|
||||||
|
}
|
||||||
|
unitUSD := 0.0
|
||||||
|
if usdRate > 0 {
|
||||||
|
unitUSD = unitTRY / usdRate
|
||||||
|
}
|
||||||
|
amountTRY := unitTRY * qty
|
||||||
|
amountUSD := unitUSD * qty
|
||||||
|
|
||||||
|
// input totals for display: inputPrice * qty in input currency
|
||||||
|
inputAmount := in * qty
|
||||||
|
|
||||||
|
if strings.Contains(group, "CM2") || strings.Contains(group, "CM1") {
|
||||||
|
b := labor[part]
|
||||||
|
b.USD += amountUSD
|
||||||
|
b.TRY += amountTRY
|
||||||
|
addInputAmount(b, cur, inputAmount)
|
||||||
|
// UI rule: tick only when cm_price_type_id == 2 (malzemeli). Nil/empty defaults to 1 (unticked).
|
||||||
|
if row.CMPriceTypeID != nil && *row.CMPriceTypeID == 2 {
|
||||||
|
b.HasCMT = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if group == "DT" || strings.Contains(group, " DT") || group == "TP" || strings.Contains(group, " TP") {
|
||||||
|
b := material[part]
|
||||||
|
b.USD += amountUSD
|
||||||
|
b.TRY += amountTRY
|
||||||
|
addInputAmount(b, cur, inputAmount)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if group == "FABRIC" || strings.Contains(group, "FABRIC") {
|
||||||
|
b := fabric[part]
|
||||||
|
b.USD += amountUSD
|
||||||
|
b.TRY += amountTRY
|
||||||
|
addInputAmount(b, cur, inputAmount)
|
||||||
|
// UI parity: fabric summary shows metraj and a representative unit input price (first non-zero).
|
||||||
|
if qty > 0 {
|
||||||
|
b.MeterQty += qty
|
||||||
|
if strings.TrimSpace(b.MeterUom) == "" {
|
||||||
|
if u := strings.TrimSpace(row.SBirim); u != "" {
|
||||||
|
b.MeterUom = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b.UnitIn <= 0 && in > 0 {
|
||||||
|
b.UnitIn = in
|
||||||
|
b.UnitCur = cur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maliyetTarihi := strings.TrimSpace(req.Header.MaliyetTarihi)
|
||||||
|
if uiHeaderLoaded {
|
||||||
|
// Prefer the UI header date (can differ from "today").
|
||||||
|
if v := strings.TrimSpace(uiHeader.MaliyetTarihi); v != "" {
|
||||||
|
maliyetTarihi = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Display format: dd.MM.yyyy (mail). Keep the original YYYY-MM-DD for subject readability if present.
|
||||||
|
maliyetTarihiLabel := maliyetTarihi
|
||||||
|
if parsed, err := time.Parse("2006-01-02", maliyetTarihi); err == nil {
|
||||||
|
maliyetTarihiLabel = formatDateTR2(parsed)
|
||||||
|
} else if maliyetTarihi == "" {
|
||||||
|
maliyetTarihi = time.Now().Format("2006-01-02")
|
||||||
|
maliyetTarihiLabel = formatDateTR2(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
titleLabel := "MALIYETI GIRIS YAPILAN URUN"
|
||||||
|
if isUpdate {
|
||||||
|
titleLabel = "MALIYETI GUNCELLENEN URUN"
|
||||||
|
}
|
||||||
|
subject := fmt.Sprintf("%s | %s | %s | OnML:%d", strings.TrimSpace(req.Header.UrunKodu), titleLabel, maliyetTarihi, nOnMLNo)
|
||||||
|
if strings.TrimSpace(actor) != "" {
|
||||||
|
subject = fmt.Sprintf("%s tarafindan %s", strings.TrimSpace(actor), subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build HTML with 4 tables.
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(`<div style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#1f2a37;">`)
|
||||||
|
b.WriteString(fmt.Sprintf(`<h3 style="margin:8px 0 6px;font-size:13px;">%s</h3>`, htmlEsc(titleLabel)))
|
||||||
|
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:720px;">`)
|
||||||
|
b.WriteString(`<tr style="background:#f3f4f6;font-weight:bold;"><td style="width:220px;">Alan</td><td>Deger</td></tr>`)
|
||||||
|
|
||||||
|
// Prefer resolved UI header (more complete), fallback to request header.
|
||||||
|
urunKodu := strings.TrimSpace(req.Header.UrunKodu)
|
||||||
|
urunAdi := strings.TrimSpace(req.Header.UrunAdi)
|
||||||
|
if uiHeaderLoaded {
|
||||||
|
if v := strings.TrimSpace(uiHeader.UrunKodu); v != "" {
|
||||||
|
urunKodu = v
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(uiHeader.UrunAdi); v != "" {
|
||||||
|
urunAdi = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>UrunKodu</td><td>%s</td></tr>`, htmlEsc(urunKodu)))
|
||||||
|
if urunAdi != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>UrunAdi</td><td>%s</td></tr>`, htmlEsc(urunAdi)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(ilkGrup) != "" || strings.TrimSpace(anaGrup) != "" || strings.TrimSpace(altGrup) != "" {
|
||||||
|
if strings.TrimSpace(ilkGrup) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Urun Ilk Grubu</td><td>%s</td></tr>`, htmlEsc(ilkGrup)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(anaGrup) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Urun Ana Grubu</td><td>%s</td></tr>`, htmlEsc(anaGrup)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(altGrup) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Urun Alt Grubu</td><td>%s</td></tr>`, htmlEsc(altGrup)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Maliyet Tarihi</td><td>%s</td></tr>`, htmlEsc(maliyetTarihiLabel)))
|
||||||
|
|
||||||
|
// Mirror UI header labels when available:
|
||||||
|
if uiHeaderLoaded {
|
||||||
|
if strings.TrimSpace(uiHeader.UretimSekli) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Uretim Sekli</td><td>%s</td></tr>`, htmlEsc(uiHeader.UretimSekli)))
|
||||||
|
} else if strings.TrimSpace(uiHeader.UretimSekliID) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Uretim Sekli ID</td><td>%s</td></tr>`, htmlEsc(uiHeader.UretimSekliID)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(uiHeader.UretimiYapanFirma) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Uretimi Yapan Firma</td><td>%s</td></tr>`, htmlEsc(uiHeader.UretimiYapanFirma)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(uiHeader.SonIsEmriVeren) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>2.Firma</td><td>%s</td></tr>`, htmlEsc(uiHeader.SonIsEmriVeren)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(uiHeader.NOnMLNo) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>nOnMLNo</td><td>%s</td></tr>`, htmlEsc(uiHeader.NOnMLNo)))
|
||||||
|
} else {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>nOnMLNo</td><td>%d</td></tr>`, nOnMLNo))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(uiHeader.SKullaniciAdi) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>sKullaniciAdi</td><td>%s</td></tr>`, htmlEsc(uiHeader.SKullaniciAdi)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(uiHeader.DteKayitTarihi) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Kayit Tarihi</td><td>%s</td></tr>`, htmlEsc(formatAnyDateTimeTR2(uiHeader.DteKayitTarihi))))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(uiHeader.DteGuncellemeTarihi) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Son Guncelleme Tarihi</td><td>%s</td></tr>`, htmlEsc(formatAnyDateTimeTR2(uiHeader.DteGuncellemeTarihi))))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(uiHeader.SGuncellemeKullaniciAdi) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>sGuncellemeKullaniciAdi</td><td>%s</td></tr>`, htmlEsc(uiHeader.SGuncellemeKullaniciAdi)))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(uiHeader.NUrtReceteID) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>nUrtReceteID</td><td>%s</td></tr>`, htmlEsc(uiHeader.NUrtReceteID)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>nOnMLNo</td><td>%d</td></tr>`, nOnMLNo))
|
||||||
|
if req.Header.NUrtReceteID > 0 {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>nUrtReceteID</td><td>%d</td></tr>`, req.Header.NUrtReceteID))
|
||||||
|
}
|
||||||
|
if req.Header.UretimSekliID > 0 {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Uretim Sekli ID</td><td>%d</td></tr>`, req.Header.UretimSekliID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free text description (from request).
|
||||||
|
if strings.TrimSpace(req.Header.SAciklama) != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td>Aciklama</td><td>%s</td></tr>`, htmlEsc(req.Header.SAciklama)))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(`</table>`)
|
||||||
|
|
||||||
|
// 1) Header totals
|
||||||
|
b.WriteString(`<h3 style="margin:12px 0 6px;font-size:13px;">Maliyetlere Islenen Toplam Tutar</h3>`)
|
||||||
|
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:520px;">`)
|
||||||
|
gbpTotal := 0.0
|
||||||
|
if gbpRate > 0 {
|
||||||
|
gbpTotal = totalTRY / gbpRate
|
||||||
|
}
|
||||||
|
// UI format (2 rows, key/value pairs)
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td style="font-weight:bold;color:#374151;">USD</td><td style="text-align:right;">%s</td><td style="font-weight:bold;color:#374151;">TRY</td><td style="text-align:right;">%s</td></tr>`,
|
||||||
|
formatMoney2(totalUSD), formatMoney2(totalTRY)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<tr><td style="font-weight:bold;color:#374151;">EUR</td><td style="text-align:right;">%s</td><td style="font-weight:bold;color:#374151;">GBP</td><td style="text-align:right;">%s</td></tr>`,
|
||||||
|
formatMoney2(totalEUR), formatMoney2(gbpTotal)))
|
||||||
|
b.WriteString(`</table>`)
|
||||||
|
|
||||||
|
renderLaborTable := func(title string, m map[string]*mailBucket) {
|
||||||
|
b.WriteString(fmt.Sprintf(`<h3 style="margin:14px 0 6px;font-size:13px;">%s</h3>`, htmlEsc(title)))
|
||||||
|
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:720px;">`)
|
||||||
|
b.WriteString(`<tr style="background:#f3f4f6;font-weight:bold;">`)
|
||||||
|
b.WriteString(`<td>Parca</td><td style="text-align:right;">Giris</td><td>Pr.Br.</td><td style="text-align:right;">USD Tutar</td><td style="text-align:right;">TRY Tutar</td><td style="text-align:center;">CMT/Malzemeli</td>`)
|
||||||
|
b.WriteString(`</tr>`)
|
||||||
|
totalUSD := 0.0
|
||||||
|
totalTRY := 0.0
|
||||||
|
for _, p := range parts {
|
||||||
|
row := m[p]
|
||||||
|
inAmt, inCur := summarizeInputByCurrency(row)
|
||||||
|
tick := ""
|
||||||
|
if row != nil && row.HasCMT {
|
||||||
|
tick = "✓"
|
||||||
|
}
|
||||||
|
if row != nil {
|
||||||
|
totalUSD += row.USD
|
||||||
|
totalTRY += row.TRY
|
||||||
|
}
|
||||||
|
b.WriteString(`<tr>`)
|
||||||
|
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(p)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, htmlEsc(inAmt)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(inCur)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.USD)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.TRY)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:center;">%s</td>`, tick))
|
||||||
|
b.WriteString(`</tr>`)
|
||||||
|
}
|
||||||
|
b.WriteString(`<tr style="border-top:2px solid #e5e7eb;font-weight:bold;background:#fbfbfd;">`)
|
||||||
|
b.WriteString(`<td>TOPLAM</td><td></td><td></td>`)
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalUSD)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalTRY)))
|
||||||
|
b.WriteString(`<td></td>`)
|
||||||
|
b.WriteString(`</tr>`)
|
||||||
|
b.WriteString(`</table>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMaterialTable := func(title string, m map[string]*mailBucket) {
|
||||||
|
b.WriteString(fmt.Sprintf(`<h3 style="margin:14px 0 6px;font-size:13px;">%s</h3>`, htmlEsc(title)))
|
||||||
|
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:520px;">`)
|
||||||
|
b.WriteString(`<tr style="background:#f3f4f6;font-weight:bold;"><td>Parca</td><td style="text-align:right;">USD Tutar</td><td style="text-align:right;">TRY Tutar</td></tr>`)
|
||||||
|
totalUSD := 0.0
|
||||||
|
totalTRY := 0.0
|
||||||
|
for _, p := range parts {
|
||||||
|
row := m[p]
|
||||||
|
if row != nil {
|
||||||
|
totalUSD += row.USD
|
||||||
|
totalTRY += row.TRY
|
||||||
|
}
|
||||||
|
b.WriteString(`<tr>`)
|
||||||
|
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(p)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.USD)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.TRY)))
|
||||||
|
b.WriteString(`</tr>`)
|
||||||
|
}
|
||||||
|
b.WriteString(`<tr style="border-top:2px solid #e5e7eb;font-weight:bold;background:#fbfbfd;">`)
|
||||||
|
b.WriteString(`<td>TOPLAM</td>`)
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalUSD)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalTRY)))
|
||||||
|
b.WriteString(`</tr>`)
|
||||||
|
b.WriteString(`</table>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFabricTable := func(title string, m map[string]*mailBucket) {
|
||||||
|
b.WriteString(fmt.Sprintf(`<h3 style="margin:14px 0 6px;font-size:13px;">%s</h3>`, htmlEsc(title)))
|
||||||
|
b.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;min-width:720px;">`)
|
||||||
|
b.WriteString(`<tr style="background:#f3f4f6;font-weight:bold;">`)
|
||||||
|
b.WriteString(`<td>Parca</td><td style="text-align:right;">Metraj</td><td style="text-align:right;">MT Giris Fiyat</td><td>Pr.Br.</td><td style="text-align:right;">USD Tutar</td><td style="text-align:right;">TRY Tutar</td>`)
|
||||||
|
b.WriteString(`</tr>`)
|
||||||
|
totalUSD := 0.0
|
||||||
|
totalTRY := 0.0
|
||||||
|
totalMeter := 0.0
|
||||||
|
for _, p := range parts {
|
||||||
|
row := m[p]
|
||||||
|
inAmt, inCur := summarizeInputByCurrency(row)
|
||||||
|
_ = inAmt
|
||||||
|
unitLabel := "-"
|
||||||
|
curLabel := inCur
|
||||||
|
if row != nil && row.UnitIn > 0 {
|
||||||
|
unitLabel = formatMoney2(row.UnitIn)
|
||||||
|
if strings.TrimSpace(row.UnitCur) != "" {
|
||||||
|
curLabel = strings.ToUpper(strings.TrimSpace(row.UnitCur))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(curLabel) == "" {
|
||||||
|
curLabel = "-"
|
||||||
|
}
|
||||||
|
if row != nil {
|
||||||
|
totalUSD += row.USD
|
||||||
|
totalTRY += row.TRY
|
||||||
|
totalMeter += row.MeterQty
|
||||||
|
}
|
||||||
|
b.WriteString(`<tr>`)
|
||||||
|
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(p)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, htmlEsc(formatMeterLabel(row))))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, htmlEsc(unitLabel)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td>%s</td>`, htmlEsc(curLabel)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.USD)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(row.TRY)))
|
||||||
|
b.WriteString(`</tr>`)
|
||||||
|
}
|
||||||
|
totalMeterLabel := "-"
|
||||||
|
if totalMeter > 0 {
|
||||||
|
totalMeterLabel = fmt.Sprintf("%s MT", formatQty2(totalMeter))
|
||||||
|
}
|
||||||
|
b.WriteString(`<tr style="border-top:2px solid #e5e7eb;font-weight:bold;background:#fbfbfd;">`)
|
||||||
|
b.WriteString(`<td>TOPLAM</td>`)
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, htmlEsc(totalMeterLabel)))
|
||||||
|
b.WriteString(`<td></td><td></td>`)
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalUSD)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="text-align:right;">%s</td>`, formatMoney2(totalTRY)))
|
||||||
|
b.WriteString(`</tr>`)
|
||||||
|
b.WriteString(`</table>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLaborTable("Iscilik Fiyatlari (CM2)", labor)
|
||||||
|
renderMaterialTable("Malzeme Fiyatlari (DT/TP, maliyete dahil)", material)
|
||||||
|
renderFabricTable("Kumas Fiyatlari (FABRIC, maliyete dahil)", fabric)
|
||||||
|
|
||||||
|
b.WriteString(`<p style="margin-top:10px;color:#6b7280;"><i>Bu mail BaggiSS App uzerinden otomatik gonderilmistir.</i></p>`)
|
||||||
|
b.WriteString(`</div>`)
|
||||||
|
|
||||||
|
msg := mailer.Message{
|
||||||
|
To: recipients,
|
||||||
|
Subject: subject,
|
||||||
|
BodyHTML: b.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graph send
|
||||||
|
if err := ml.Send(context.Background(), msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -335,7 +335,6 @@ type pdfGroupTotalRow struct {
|
|||||||
func (c *costingPDF) drawHeaderSummaryTables() {
|
func (c *costingPDF) drawHeaderSummaryTables() {
|
||||||
pdf := c.pdf
|
pdf := c.pdf
|
||||||
|
|
||||||
partRows := c.computePartSummary()
|
|
||||||
groupRows, grandTRY, grandUSD, grandEUR := c.computeGroupTotals()
|
groupRows, grandTRY, grandUSD, grandEUR := c.computeGroupTotals()
|
||||||
|
|
||||||
// Table styling (use same brand palette as statements PDF)
|
// Table styling (use same brand palette as statements PDF)
|
||||||
@@ -345,28 +344,6 @@ func (c *costingPDF) drawHeaderSummaryTables() {
|
|||||||
pdf.CellFormat(0, 5.5, "Ozet", "", 1, "L", false, 0, "")
|
pdf.CellFormat(0, 5.5, "Ozet", "", 1, "L", false, 0, "")
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
|
|
||||||
// Part-based summary table
|
|
||||||
pdf.SetFont("dejavu", "B", 8.2)
|
|
||||||
pdf.CellFormat(0, 4.8, "Parca Bazli Maliyet Ozellikleri", "", 1, "L", false, 0, "")
|
|
||||||
partCols := []string{"Parca", "TRY", "USD", "EUR"}
|
|
||||||
partW := []float64{70, 22, 22, 22}
|
|
||||||
// Add TOTAL row
|
|
||||||
totalTry, totalUsd, totalEur := 0.0, 0.0, 0.0
|
|
||||||
for _, r := range partRows {
|
|
||||||
totalTry += r.try
|
|
||||||
totalUsd += r.usd
|
|
||||||
totalEur += r.eur
|
|
||||||
}
|
|
||||||
partRowsWithTotal := append(partRows, pdfPartSummaryRow{name: "TOPLAM", try: totalTry, usd: totalUsd, eur: totalEur})
|
|
||||||
c.drawMiniTable(partCols, partW, func(i int) []string {
|
|
||||||
if i >= len(partRowsWithTotal) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
r := partRowsWithTotal[i]
|
|
||||||
return []string{r.name, pdfMoney(r.try), pdfMoney(r.usd), pdfMoney(r.eur)}
|
|
||||||
}, len(partRowsWithTotal), true, true)
|
|
||||||
pdf.Ln(2)
|
|
||||||
|
|
||||||
// Group totals table
|
// Group totals table
|
||||||
pdf.SetFont("dejavu", "B", 8.2)
|
pdf.SetFont("dejavu", "B", 8.2)
|
||||||
pdf.CellFormat(0, 4.8, "Grup Toplamlari", "", 1, "L", false, 0, "")
|
pdf.CellFormat(0, 4.8, "Grup Toplamlari", "", 1, "L", false, 0, "")
|
||||||
@@ -515,6 +492,7 @@ func (c *costingPDF) drawMiniTable(cols []string, widths []float64, rowFn func(i
|
|||||||
pdf.SetXY(x0, y+rh)
|
pdf.SetXY(x0, y+rh)
|
||||||
}
|
}
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
|
pdf.SetFont("dejavu", "", 7.4)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatDateTRDot(s string) string {
|
func formatDateTRDot(s string) string {
|
||||||
@@ -537,6 +515,9 @@ func formatDateTRDot(s string) string {
|
|||||||
|
|
||||||
func (c *costingPDF) drawGroup(g models.ProductionHasCostDetailGroup, firstGroup bool) {
|
func (c *costingPDF) drawGroup(g models.ProductionHasCostDetailGroup, firstGroup bool) {
|
||||||
pdf := c.pdf
|
pdf := c.pdf
|
||||||
|
// Reset any font/color left over from header summary tables.
|
||||||
|
pdf.SetFont("dejavu", "", 7.2)
|
||||||
|
pdf.SetTextColor(0, 0, 0)
|
||||||
|
|
||||||
// Group bar
|
// Group bar
|
||||||
c.drawGroupBar(g, false)
|
c.drawGroupBar(g, false)
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
|
||||||
* DO NOT EDIT.
|
|
||||||
*
|
|
||||||
* You are probably looking on adding startup/initialization code.
|
|
||||||
* Use "quasar new boot <name>" and add it there.
|
|
||||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
|
||||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
|
||||||
*
|
|
||||||
* Boot files are your "main.js"
|
|
||||||
**/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { Quasar } from 'quasar'
|
|
||||||
import { markRaw } from 'vue'
|
|
||||||
import RootComponent from 'app/src/App.vue'
|
|
||||||
|
|
||||||
import createStore from 'app/src/stores/index'
|
|
||||||
import createRouter from 'app/src/router/index'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default async function (createAppFn, quasarUserOptions) {
|
|
||||||
|
|
||||||
|
|
||||||
// Create the app instance.
|
|
||||||
// Here we inject into it the Quasar UI, the router & possibly the store.
|
|
||||||
const app = createAppFn(RootComponent)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.use(Quasar, quasarUserOptions)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const store = typeof createStore === 'function'
|
|
||||||
? await createStore({})
|
|
||||||
: createStore
|
|
||||||
|
|
||||||
|
|
||||||
app.use(store)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const router = markRaw(
|
|
||||||
typeof createRouter === 'function'
|
|
||||||
? await createRouter({store})
|
|
||||||
: createRouter
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
// make router instance available in store
|
|
||||||
|
|
||||||
store.use(({ store }) => { store.router = router })
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Expose the app, the router and the store.
|
|
||||||
// Note that we are not mounting the app here, since bootstrapping will be
|
|
||||||
// different depending on whether we are in a browser or on the server.
|
|
||||||
return {
|
|
||||||
app,
|
|
||||||
store,
|
|
||||||
router
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
|
||||||
* DO NOT EDIT.
|
|
||||||
*
|
|
||||||
* You are probably looking on adding startup/initialization code.
|
|
||||||
* Use "quasar new boot <name>" and add it there.
|
|
||||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
|
||||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
|
||||||
*
|
|
||||||
* Boot files are your "main.js"
|
|
||||||
**/
|
|
||||||
|
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import '@quasar/extras/roboto-font/roboto-font.css'
|
|
||||||
|
|
||||||
import '@quasar/extras/material-icons/material-icons.css'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// We load Quasar stylesheet file
|
|
||||||
import 'quasar/dist/quasar.sass'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import 'src/css/app.css'
|
|
||||||
|
|
||||||
|
|
||||||
import createQuasarApp from './app.js'
|
|
||||||
import quasarUserOptions from './quasar-user-options.js'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const publicPath = `/`
|
|
||||||
|
|
||||||
|
|
||||||
async function start ({
|
|
||||||
app,
|
|
||||||
router
|
|
||||||
, store
|
|
||||||
}, bootFiles) {
|
|
||||||
|
|
||||||
let hasRedirected = false
|
|
||||||
const getRedirectUrl = url => {
|
|
||||||
try { return router.resolve(url).href }
|
|
||||||
catch (err) {}
|
|
||||||
|
|
||||||
return Object(url) === url
|
|
||||||
? null
|
|
||||||
: url
|
|
||||||
}
|
|
||||||
const redirect = url => {
|
|
||||||
hasRedirected = true
|
|
||||||
|
|
||||||
if (typeof url === 'string' && /^https?:\/\//.test(url)) {
|
|
||||||
window.location.href = url
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const href = getRedirectUrl(url)
|
|
||||||
|
|
||||||
// continue if we didn't fail to resolve the url
|
|
||||||
if (href !== null) {
|
|
||||||
window.location.href = href
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlPath = window.location.href.replace(window.location.origin, '')
|
|
||||||
|
|
||||||
for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) {
|
|
||||||
try {
|
|
||||||
await bootFiles[i]({
|
|
||||||
app,
|
|
||||||
router,
|
|
||||||
store,
|
|
||||||
ssrContext: null,
|
|
||||||
redirect,
|
|
||||||
urlPath,
|
|
||||||
publicPath
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
if (err && err.url) {
|
|
||||||
redirect(err.url)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('[Quasar] boot error:', err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasRedirected === true) return
|
|
||||||
|
|
||||||
|
|
||||||
app.use(router)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.mount('#q-app')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
createQuasarApp(createApp, quasarUserOptions)
|
|
||||||
|
|
||||||
.then(app => {
|
|
||||||
// eventually remove this when Cordova/Capacitor/Electron support becomes old
|
|
||||||
const [ method, mapFn ] = Promise.allSettled !== void 0
|
|
||||||
? [
|
|
||||||
'allSettled',
|
|
||||||
bootFiles => bootFiles.map(result => {
|
|
||||||
if (result.status === 'rejected') {
|
|
||||||
console.error('[Quasar] boot error:', result.reason)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return result.value.default
|
|
||||||
})
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
'all',
|
|
||||||
bootFiles => bootFiles.map(entry => entry.default)
|
|
||||||
]
|
|
||||||
|
|
||||||
return Promise[ method ]([
|
|
||||||
|
|
||||||
import(/* webpackMode: "eager" */ 'boot/dayjs'),
|
|
||||||
|
|
||||||
import(/* webpackMode: "eager" */ 'boot/locale'),
|
|
||||||
|
|
||||||
import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard')
|
|
||||||
|
|
||||||
]).then(bootFiles => {
|
|
||||||
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
|
|
||||||
start(app, boot)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
|
||||||
* DO NOT EDIT.
|
|
||||||
*
|
|
||||||
* You are probably looking on adding startup/initialization code.
|
|
||||||
* Use "quasar new boot <name>" and add it there.
|
|
||||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
|
||||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
|
||||||
*
|
|
||||||
* Boot files are your "main.js"
|
|
||||||
**/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import App from 'app/src/App.vue'
|
|
||||||
let appPrefetch = typeof App.preFetch === 'function'
|
|
||||||
? App.preFetch
|
|
||||||
: (
|
|
||||||
// Class components return the component options (and the preFetch hook) inside __c property
|
|
||||||
App.__c !== void 0 && typeof App.__c.preFetch === 'function'
|
|
||||||
? App.__c.preFetch
|
|
||||||
: false
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
function getMatchedComponents (to, router) {
|
|
||||||
const route = to
|
|
||||||
? (to.matched ? to : router.resolve(to).route)
|
|
||||||
: router.currentRoute.value
|
|
||||||
|
|
||||||
if (!route) { return [] }
|
|
||||||
|
|
||||||
const matched = route.matched.filter(m => m.components !== void 0)
|
|
||||||
|
|
||||||
if (matched.length === 0) { return [] }
|
|
||||||
|
|
||||||
return Array.prototype.concat.apply([], matched.map(m => {
|
|
||||||
return Object.keys(m.components).map(key => {
|
|
||||||
const comp = m.components[key]
|
|
||||||
return {
|
|
||||||
path: m.path,
|
|
||||||
c: comp
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addPreFetchHooks ({ router, store, publicPath }) {
|
|
||||||
// Add router hook for handling preFetch.
|
|
||||||
// Doing it after initial route is resolved so that we don't double-fetch
|
|
||||||
// the data that we already have. Using router.beforeResolve() so that all
|
|
||||||
// async components are resolved.
|
|
||||||
router.beforeResolve((to, from, next) => {
|
|
||||||
const
|
|
||||||
urlPath = window.location.href.replace(window.location.origin, ''),
|
|
||||||
matched = getMatchedComponents(to, router),
|
|
||||||
prevMatched = getMatchedComponents(from, router)
|
|
||||||
|
|
||||||
let diffed = false
|
|
||||||
const preFetchList = matched
|
|
||||||
.filter((m, i) => {
|
|
||||||
return diffed || (diffed = (
|
|
||||||
!prevMatched[i] ||
|
|
||||||
prevMatched[i].c !== m.c ||
|
|
||||||
m.path.indexOf('/:') > -1 // does it has params?
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.filter(m => m.c !== void 0 && (
|
|
||||||
typeof m.c.preFetch === 'function'
|
|
||||||
// Class components return the component options (and the preFetch hook) inside __c property
|
|
||||||
|| (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function')
|
|
||||||
))
|
|
||||||
.map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch)
|
|
||||||
|
|
||||||
|
|
||||||
if (appPrefetch !== false) {
|
|
||||||
preFetchList.unshift(appPrefetch)
|
|
||||||
appPrefetch = false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (preFetchList.length === 0) {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasRedirected = false
|
|
||||||
const redirect = url => {
|
|
||||||
hasRedirected = true
|
|
||||||
next(url)
|
|
||||||
}
|
|
||||||
const proceed = () => {
|
|
||||||
|
|
||||||
if (hasRedirected === false) { next() }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
preFetchList.reduce(
|
|
||||||
(promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({
|
|
||||||
store,
|
|
||||||
currentRoute: to,
|
|
||||||
previousRoute: from,
|
|
||||||
redirect,
|
|
||||||
urlPath,
|
|
||||||
publicPath
|
|
||||||
})),
|
|
||||||
Promise.resolve()
|
|
||||||
)
|
|
||||||
.then(proceed)
|
|
||||||
.catch(e => {
|
|
||||||
console.error(e)
|
|
||||||
proceed()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
|
||||||
* DO NOT EDIT.
|
|
||||||
*
|
|
||||||
* You are probably looking on adding startup/initialization code.
|
|
||||||
* Use "quasar new boot <name>" and add it there.
|
|
||||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
|
||||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
|
||||||
*
|
|
||||||
* Boot files are your "main.js"
|
|
||||||
**/
|
|
||||||
|
|
||||||
import lang from 'quasar/lang/tr.js'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import {Loading,Dialog,Notify} from 'quasar'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} }
|
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
dense
|
dense
|
||||||
row-key="urun_ilk_grubu"
|
row-key="group_code"
|
||||||
:loading="store.loading"
|
:loading="store.loading"
|
||||||
:rows="store.rows"
|
:rows="store.rows"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
<template #body-cell-mail_selector="props">
|
<template #body-cell-mail_selector="props">
|
||||||
<q-td :props="props">
|
<q-td :props="props">
|
||||||
<q-select
|
<q-select
|
||||||
:model-value="editableByGroup[props.row.urun_ilk_grubu] || []"
|
:model-value="editableByGroup[props.row.group_code] || []"
|
||||||
:options="mailOptionsByGroup[props.row.urun_ilk_grubu] || allMailOptions"
|
:options="mailOptionsByGroup[props.row.group_code] || allMailOptions"
|
||||||
option-value="id"
|
option-value="id"
|
||||||
option-label="label"
|
option-label="label"
|
||||||
emit-value
|
emit-value
|
||||||
@@ -39,8 +39,8 @@
|
|||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
label="Mail ara ve sec"
|
label="Mail ara ve sec"
|
||||||
@filter="(val, update) => filterMailOptions(props.row.urun_ilk_grubu, val, update)"
|
@filter="(val, update) => filterMailOptions(props.row.group_code, val, update)"
|
||||||
@update:model-value="(val) => updateRowSelection(props.row.urun_ilk_grubu, val)"
|
@update:model-value="(val) => updateRowSelection(props.row.group_code, val)"
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
@@ -71,7 +71,8 @@ const originalByGroup = ref({})
|
|||||||
const mailOptionsByGroup = ref({})
|
const mailOptionsByGroup = ref({})
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'urun_ilk_grubu', label: 'Urun Ilk Grubu', field: 'urun_ilk_grubu', align: 'left' },
|
{ name: 'group_code', label: 'Urun Ilk Grup Kodu', field: 'group_code', align: 'left' },
|
||||||
|
{ name: 'group_title', label: 'Urun Ilk Grup Aciklama', field: 'group_title', align: 'left' },
|
||||||
{ name: 'mail_selector', label: 'Maliyet Mail Eslestirme', field: 'mail_selector', align: 'left' }
|
{ name: 'mail_selector', label: 'Maliyet Mail Eslestirme', field: 'mail_selector', align: 'left' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ const allMailOptions = computed(() =>
|
|||||||
|
|
||||||
const changedGroups = computed(() => {
|
const changedGroups = computed(() => {
|
||||||
return (store.rows || [])
|
return (store.rows || [])
|
||||||
.map((r) => String(r.urun_ilk_grubu || '').trim())
|
.map((r) => String(r.group_code || r.urun_ilk_grubu || '').trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter((g) => {
|
.filter((g) => {
|
||||||
const current = normalizeList(editableByGroup.value[g] || [])
|
const current = normalizeList(editableByGroup.value[g] || [])
|
||||||
@@ -115,7 +116,7 @@ function initEditableState () {
|
|||||||
const original = {}
|
const original = {}
|
||||||
|
|
||||||
;(store.rows || []).forEach((row) => {
|
;(store.rows || []).forEach((row) => {
|
||||||
const g = String(row.urun_ilk_grubu || '').trim()
|
const g = String(row.group_code || row.urun_ilk_grubu || '').trim()
|
||||||
const selected = normalizeList(row.mail_ids || [])
|
const selected = normalizeList(row.mail_ids || [])
|
||||||
editable[g] = [...selected]
|
editable[g] = [...selected]
|
||||||
original[g] = [...selected]
|
original[g] = [...selected]
|
||||||
@@ -169,4 +170,3 @@ async function saveChanges () {
|
|||||||
|
|
||||||
onMounted(() => { init() })
|
onMounted(() => { init() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
dense
|
dense
|
||||||
row-key="urun_ilk_grubu"
|
row-key="group_code"
|
||||||
:loading="store.loading"
|
:loading="store.loading"
|
||||||
:rows="store.rows"
|
:rows="store.rows"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
<template #body-cell-mail_selector="props">
|
<template #body-cell-mail_selector="props">
|
||||||
<q-td :props="props">
|
<q-td :props="props">
|
||||||
<q-select
|
<q-select
|
||||||
:model-value="editableByGroup[props.row.urun_ilk_grubu] || []"
|
:model-value="editableByGroup[props.row.group_code] || []"
|
||||||
:options="mailOptionsByGroup[props.row.urun_ilk_grubu] || allMailOptions"
|
:options="mailOptionsByGroup[props.row.group_code] || allMailOptions"
|
||||||
option-value="id"
|
option-value="id"
|
||||||
option-label="label"
|
option-label="label"
|
||||||
emit-value
|
emit-value
|
||||||
@@ -39,8 +39,8 @@
|
|||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
label="Mail ara ve sec"
|
label="Mail ara ve sec"
|
||||||
@filter="(val, update) => filterMailOptions(props.row.urun_ilk_grubu, val, update)"
|
@filter="(val, update) => filterMailOptions(props.row.group_code, val, update)"
|
||||||
@update:model-value="(val) => updateRowSelection(props.row.urun_ilk_grubu, val)"
|
@update:model-value="(val) => updateRowSelection(props.row.group_code, val)"
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
@@ -71,7 +71,8 @@ const originalByGroup = ref({})
|
|||||||
const mailOptionsByGroup = ref({})
|
const mailOptionsByGroup = ref({})
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'urun_ilk_grubu', label: 'Urun Ilk Grubu', field: 'urun_ilk_grubu', align: 'left' },
|
{ name: 'group_code', label: 'Urun Ilk Grup Kodu', field: 'group_code', align: 'left' },
|
||||||
|
{ name: 'group_title', label: 'Urun Ilk Grup Aciklama', field: 'group_title', align: 'left' },
|
||||||
{ name: 'mail_selector', label: 'Fiyatlandirma Mail Eslestirme', field: 'mail_selector', align: 'left' }
|
{ name: 'mail_selector', label: 'Fiyatlandirma Mail Eslestirme', field: 'mail_selector', align: 'left' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ const allMailOptions = computed(() =>
|
|||||||
|
|
||||||
const changedGroups = computed(() => {
|
const changedGroups = computed(() => {
|
||||||
return (store.rows || [])
|
return (store.rows || [])
|
||||||
.map((r) => String(r.urun_ilk_grubu || '').trim())
|
.map((r) => String(r.group_code || r.urun_ilk_grubu || '').trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter((g) => {
|
.filter((g) => {
|
||||||
const current = normalizeList(editableByGroup.value[g] || [])
|
const current = normalizeList(editableByGroup.value[g] || [])
|
||||||
@@ -115,7 +116,7 @@ function initEditableState () {
|
|||||||
const original = {}
|
const original = {}
|
||||||
|
|
||||||
;(store.rows || []).forEach((row) => {
|
;(store.rows || []).forEach((row) => {
|
||||||
const g = String(row.urun_ilk_grubu || '').trim()
|
const g = String(row.group_code || row.urun_ilk_grubu || '').trim()
|
||||||
const selected = normalizeList(row.mail_ids || [])
|
const selected = normalizeList(row.mail_ids || [])
|
||||||
editable[g] = [...selected]
|
editable[g] = [...selected]
|
||||||
original[g] = [...selected]
|
original[g] = [...selected]
|
||||||
@@ -169,4 +170,3 @@ async function saveChanges () {
|
|||||||
|
|
||||||
onMounted(() => { init() })
|
onMounted(() => { init() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,16 @@
|
|||||||
<div class="pcd-toolbar-left">
|
<div class="pcd-toolbar-left">
|
||||||
<div class="pcd-toolbar-title">Maliyet Detay Sayfasi</div>
|
<div class="pcd-toolbar-title">Maliyet Detay Sayfasi</div>
|
||||||
<div v-if="detailHeader && !detailLoading" class="pcd-toolbar-summary">
|
<div v-if="detailHeader && !detailLoading" class="pcd-toolbar-summary">
|
||||||
|
<!-- tbStok kontrolu (exists-bulk) manuel giris kapandigi icin devre disi -->
|
||||||
|
<div class="pcd-toolbar-summary-row">
|
||||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||||
<span class="pcd-toolbar-pill-label">USD</span>
|
<span class="pcd-toolbar-pill-label">USD</span>
|
||||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.usdTotal) }}</span>
|
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.usdTotal) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||||
|
<span class="pcd-toolbar-pill-label">TRY</span>
|
||||||
|
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.tryTotal) }}</span>
|
||||||
|
</div>
|
||||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||||
<span class="pcd-toolbar-pill-label">EUR</span>
|
<span class="pcd-toolbar-pill-label">EUR</span>
|
||||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.eurTotal) }}</span>
|
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.eurTotal) }}</span>
|
||||||
@@ -18,6 +24,8 @@
|
|||||||
<span class="pcd-toolbar-pill-label">GBP</span>
|
<span class="pcd-toolbar-pill-label">GBP</span>
|
||||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.gbpTotal) }}</span>
|
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.gbpTotal) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pcd-toolbar-summary-row">
|
||||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||||||
<span class="pcd-toolbar-pill-label">USD Kur</span>
|
<span class="pcd-toolbar-pill-label">USD Kur</span>
|
||||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.usdRate) }}</span>
|
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.usdRate) }}</span>
|
||||||
@@ -32,8 +40,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pcd-toolbar-actions">
|
<div class="pcd-toolbar-actions">
|
||||||
|
<div
|
||||||
|
v-if="last10WarningCount > 0"
|
||||||
|
class="pcd-toolbar-pill pcd-toolbar-pill-warn"
|
||||||
|
style="cursor:pointer;"
|
||||||
|
title="Son 10 ort. fiyata gore %10+ sapma var"
|
||||||
|
@click="last10WarningDialogOpen = true"
|
||||||
|
>
|
||||||
|
<span class="pcd-toolbar-pill-label">Fiyat Uyari</span>
|
||||||
|
<span class="pcd-toolbar-pill-value">{{ last10WarningCount }}</span>
|
||||||
|
</div>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
color="grey-7"
|
||||||
|
class="pcd-toolbar-btn"
|
||||||
|
:label="summaryOpen ? 'OZET GIZLE' : 'OZET GOSTER'"
|
||||||
|
:icon="summaryOpen ? 'visibility_off' : 'visibility'"
|
||||||
|
@click="summaryOpen = !summaryOpen"
|
||||||
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
@@ -114,6 +142,178 @@
|
|||||||
Hata: {{ detailError }}
|
Hata: {{ detailError }}
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
v-if="detailHeader && !detailLoading"
|
||||||
|
v-model="summaryOpen"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
class="pcd-summary-expansion q-mx-md q-mb-md"
|
||||||
|
icon="summarize"
|
||||||
|
label="Ozet"
|
||||||
|
>
|
||||||
|
<div class="row q-col-gutter-md q-pa-sm">
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="pcd-summary-title">Maliyetlere Islenen Toplam Tutar</div>
|
||||||
|
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="pcd-summary-k">USD</td>
|
||||||
|
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.usd) }}</td>
|
||||||
|
<td class="pcd-summary-k">TRY</td>
|
||||||
|
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.try) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="pcd-summary-k">EUR</td>
|
||||||
|
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.eur) }}</td>
|
||||||
|
<td class="pcd-summary-k">GBP</td>
|
||||||
|
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.gbp) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</q-markup-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="pcd-summary-title">Iscilik Fiyatlari (CM2)</div>
|
||||||
|
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">Parca</th>
|
||||||
|
<th class="text-right">Giris</th>
|
||||||
|
<th class="text-left">Pr.Br.</th>
|
||||||
|
<th class="text-right">USD Tutar</th>
|
||||||
|
<th class="text-right">TRY Tutar</th>
|
||||||
|
<th class="text-center">CMT/Malzemeli</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="r in mailSummary.laborRows" :key="'labor-'+r.part">
|
||||||
|
<td class="text-left">{{ r.part }}</td>
|
||||||
|
<td class="text-right">{{ r.inputAmountLabel }}</td>
|
||||||
|
<td class="text-left">{{ r.inputCurrencyLabel }}</td>
|
||||||
|
<td class="text-right">{{ formatMoney(r.usd) }}</td>
|
||||||
|
<td class="text-right">{{ formatMoney(r.try) }}</td>
|
||||||
|
<td class="text-center">{{ r.hasCmtOrMalzemeli ? '✓' : '' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="pcd-summary-total-row">
|
||||||
|
<td class="text-left text-weight-bold">TOPLAM</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.laborRows.reduce((a, x) => a + (x.usd || 0), 0)) }}</td>
|
||||||
|
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.laborRows.reduce((a, x) => a + (x.try || 0), 0)) }}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</q-markup-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="pcd-summary-title">Malzeme Fiyatlari (DT/TP, maliyete dahil)</div>
|
||||||
|
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">Parca</th>
|
||||||
|
<th class="text-right">USD Tutar</th>
|
||||||
|
<th class="text-right">TRY Tutar</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="r in mailSummary.materialRows" :key="'mat-'+r.part">
|
||||||
|
<td class="text-left">{{ r.part }}</td>
|
||||||
|
<td class="text-right">{{ formatMoney(r.usd) }}</td>
|
||||||
|
<td class="text-right">{{ formatMoney(r.try) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="pcd-summary-total-row">
|
||||||
|
<td class="text-left text-weight-bold">TOPLAM</td>
|
||||||
|
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.materialRows.reduce((a, x) => a + (x.usd || 0), 0)) }}</td>
|
||||||
|
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.materialRows.reduce((a, x) => a + (x.try || 0), 0)) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</q-markup-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="pcd-summary-title">Kumas Fiyatlari (FABRIC, maliyete dahil)</div>
|
||||||
|
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">Parca</th>
|
||||||
|
<th class="text-right">Metraj</th>
|
||||||
|
<th class="text-right">MT Giris Fiyat</th>
|
||||||
|
<th class="text-left">Pr.Br.</th>
|
||||||
|
<th class="text-right">USD Tutar</th>
|
||||||
|
<th class="text-right">TRY Tutar</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="r in mailSummary.fabricRows" :key="'fab-'+r.part">
|
||||||
|
<td class="text-left">{{ r.part }}</td>
|
||||||
|
<td class="text-right">{{ r.meterLabel }}</td>
|
||||||
|
<td class="text-right">{{ r.inputUnitLabel }}</td>
|
||||||
|
<td class="text-left">{{ r.inputCurrencyLabel }}</td>
|
||||||
|
<td class="text-right">{{ formatMoney(r.usd) }}</td>
|
||||||
|
<td class="text-right">{{ formatMoney(r.try) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="pcd-summary-total-row">
|
||||||
|
<td class="text-left text-weight-bold">TOPLAM</td>
|
||||||
|
<td class="text-right text-weight-bold">
|
||||||
|
{{
|
||||||
|
(() => {
|
||||||
|
const total = mailSummary.fabricRows.reduce((a, x) => a + (Number(x.meterQty || 0) || 0), 0)
|
||||||
|
return total > 0 ? `${formatBarQuantity(total)} MT` : '-'
|
||||||
|
})()
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.fabricRows.reduce((a, x) => a + (x.usd || 0), 0)) }}</td>
|
||||||
|
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.fabricRows.reduce((a, x) => a + (x.try || 0), 0)) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</q-markup-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-dialog v-model="last10WarningDialogOpen" persistent>
|
||||||
|
<q-card style="min-width: 860px; max-width: 92vw;">
|
||||||
|
<q-card-section class="row items-center">
|
||||||
|
<div class="text-h6">Son 10 Ort. Fiyat Sapmalari</div>
|
||||||
|
<q-space />
|
||||||
|
<q-btn icon="close" flat round dense v-close-popup />
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
<q-card-section style="max-height: 70vh; overflow: auto;">
|
||||||
|
<q-markup-table dense flat bordered>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">Kod</th>
|
||||||
|
<th class="text-left">Doviz</th>
|
||||||
|
<th class="text-right">Giris</th>
|
||||||
|
<th class="text-right">Ort10</th>
|
||||||
|
<th class="text-right">Sapma %</th>
|
||||||
|
<th class="text-right">Sample</th>
|
||||||
|
<th class="text-left">Tarih Araligi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="w in last10Warnings" :key="w.item_code + '|' + w.currency_code">
|
||||||
|
<td class="text-left">{{ w.item_code }}</td>
|
||||||
|
<td class="text-left">{{ w.currency_code }}</td>
|
||||||
|
<td class="text-right">{{ formatMoney(w.input_price) }}</td>
|
||||||
|
<td class="text-right">{{ formatMoney(w.avg_doc_price) }}</td>
|
||||||
|
<td class="text-right">{{ formatPercent(w.diff_ratio) }}</td>
|
||||||
|
<td class="text-right">{{ w.sample_count }}</td>
|
||||||
|
<td class="text-left">{{ (w.min_invoice_date || '-') + ' / ' + (w.max_invoice_date || '-') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="last10Warnings.length === 0">
|
||||||
|
<td colspan="7" class="text-center text-grey-7">Kayit yok</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</q-markup-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<div v-if="detailHeader && !detailLoading && !headerInfoCollapsed" class="filter-bar pcd-detail-header-bar q-mx-md q-mb-md">
|
<div v-if="detailHeader && !detailLoading && !headerInfoCollapsed" class="filter-bar pcd-detail-header-bar q-mx-md q-mb-md">
|
||||||
<div class="row q-col-gutter-sm">
|
<div class="row q-col-gutter-sm">
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-3">
|
||||||
@@ -189,7 +389,7 @@
|
|||||||
<q-input dense filled readonly label="nUrtReceteID" :model-value="detailHeader.nUrtReceteID || '-'" />
|
<q-input dense filled readonly label="nUrtReceteID" :model-value="detailHeader.nUrtReceteID || '-'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="partSummary && partSummary.length > 0" class="col-12">
|
<div v-if="false && partSummary && partSummary.length > 0" class="col-12">
|
||||||
<div class="pcd-part-summary-card">
|
<div class="pcd-part-summary-card">
|
||||||
<div class="pcd-part-summary-title">Parça Bazlı Maliyet Özellikleri</div>
|
<div class="pcd-part-summary-title">Parça Bazlı Maliyet Özellikleri</div>
|
||||||
<q-markup-table dense flat bordered separator="cell" class="pcd-part-summary-table">
|
<q-markup-table dense flat bordered separator="cell" class="pcd-part-summary-table">
|
||||||
@@ -386,7 +586,7 @@
|
|||||||
<template #body-cell-sKodu="props">
|
<template #body-cell-sKodu="props">
|
||||||
<q-td
|
<q-td
|
||||||
:props="props"
|
:props="props"
|
||||||
:class="resolveAutoOrICodeHighlightClass(props.row)"
|
:class="[resolveAutoOrICodeHighlightClass(props.row), resolveMissingStockCodeClass(props.row)]"
|
||||||
>
|
>
|
||||||
<span>{{ props.value }}</span>
|
<span>{{ props.value }}</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
@@ -395,7 +595,7 @@
|
|||||||
<template #body-cell-sAciklama="props">
|
<template #body-cell-sAciklama="props">
|
||||||
<q-td
|
<q-td
|
||||||
:props="props"
|
:props="props"
|
||||||
:class="resolveAutoOrICodeHighlightClass(props.row)"
|
:class="[resolveAutoOrICodeHighlightClass(props.row), resolveMissingStockCodeClass(props.row)]"
|
||||||
>
|
>
|
||||||
<span>{{ props.value }}</span>
|
<span>{{ props.value }}</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
@@ -880,6 +1080,7 @@ const rowEditorHammaddeLoading = ref(false)
|
|||||||
const rowEditorItemOptions = ref([])
|
const rowEditorItemOptions = ref([])
|
||||||
const rowEditorItemAllOptions = ref([])
|
const rowEditorItemAllOptions = ref([])
|
||||||
const rowEditorItemLoading = ref(false)
|
const rowEditorItemLoading = ref(false)
|
||||||
|
const rowEditorLastValidItemValue = ref('')
|
||||||
const rowEditorColorOptions = ref([])
|
const rowEditorColorOptions = ref([])
|
||||||
const rowEditorColorAllOptions = ref([])
|
const rowEditorColorAllOptions = ref([])
|
||||||
const rowEditorColorLoading = ref(false)
|
const rowEditorColorLoading = ref(false)
|
||||||
@@ -894,7 +1095,89 @@ const lineHistoryTargetSummary = ref('')
|
|||||||
const lineHistorySearchMode = ref('exact')
|
const lineHistorySearchMode = ref('exact')
|
||||||
const lineHistoryLastPurchaseMatchStage = ref('')
|
const lineHistoryLastPurchaseMatchStage = ref('')
|
||||||
const lineHistoryLastRecipeMatchStage = ref('')
|
const lineHistoryLastRecipeMatchStage = ref('')
|
||||||
|
|
||||||
|
// tbStok validation (missing stock cards)
|
||||||
|
// Manuel giris kapandigi icin tbStok exists-bulk kontrolunu devre disi biraktik.
|
||||||
|
// Eski UI/CSS hook'lari kalsin diye state'i tutuyoruz ama request atmayacagiz.
|
||||||
|
const missingTbStokCodesMap = ref({})
|
||||||
|
const tbStokValidationLastError = ref('')
|
||||||
|
const tbStokMissingDialogShown = ref(false)
|
||||||
|
let tbStokValidationTimer = null
|
||||||
|
|
||||||
|
function normalizeStockCodeKey (code) {
|
||||||
|
return String(code || '').trim().toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMissingTbStokDialog () {
|
||||||
|
const missing = Object.keys(missingTbStokCodesMap.value || {})
|
||||||
|
if (missing.length === 0) return
|
||||||
|
$q.dialog({
|
||||||
|
title: 'tbStok Eksik Kodlar',
|
||||||
|
message: `Asagidaki kodlar tbStok'ta yok. Stok kartlarini duzeltmeden maliyet kaydetmeyin:\n\n${missing.slice(0, 80).join(', ')}${missing.length > 80 ? `\n(+${missing.length - 80} daha)` : ''}`,
|
||||||
|
ok: { label: 'Tamam' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTbStokMissingCode (code) {
|
||||||
|
code = normalizeStockCodeKey(code)
|
||||||
|
if (!code) return false
|
||||||
|
return Boolean(missingTbStokCodesMap.value?.[code])
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMissingStockCodeClass (row) {
|
||||||
|
const code = normalizeStockCodeKey(row?.sKodu)
|
||||||
|
if (!code) return ''
|
||||||
|
if (isTbStokMissingCode(code)) return 'pcd-missing-stock-code'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUniqueCodesFromRows () {
|
||||||
|
const out = []
|
||||||
|
const seen = new Set()
|
||||||
|
for (const r of flatDetailRows.value || []) {
|
||||||
|
const code = normalizeStockCodeKey(r?.sKodu)
|
||||||
|
if (!code) continue
|
||||||
|
if (seen.has(code)) continue
|
||||||
|
seen.add(code)
|
||||||
|
out.push(code)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTbStokMissingCodes () {
|
||||||
|
tbStokValidationLastError.value = ''
|
||||||
|
missingTbStokCodesMap.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPercent (ratio) {
|
||||||
|
const n = Number(ratio)
|
||||||
|
if (!Number.isFinite(n)) return '-'
|
||||||
|
return `${(n * 100).toFixed(0)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLast10Warnings () {
|
||||||
|
if (!onMLNo.value) {
|
||||||
|
last10Warnings.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resp = await get('/pricing/production-product-costing/last10-warnings', { n_onml_no: onMLNo.value })
|
||||||
|
const items = Array.isArray(resp?.items) ? resp.items : []
|
||||||
|
last10Warnings.value = items
|
||||||
|
} catch (err) {
|
||||||
|
// non-blocking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleTbStokValidation () {
|
||||||
|
// no-op (tbStok kontrolu devre disi)
|
||||||
|
if (tbStokValidationTimer) clearTimeout(tbStokValidationTimer)
|
||||||
|
tbStokValidationTimer = null
|
||||||
|
}
|
||||||
const purchaseAvgUSDCachedByCode = ref({})
|
const purchaseAvgUSDCachedByCode = ref({})
|
||||||
|
const last10Warnings = ref([])
|
||||||
|
const last10WarningDialogOpen = ref(false)
|
||||||
|
const last10WarningCount = computed(() => (Array.isArray(last10Warnings.value) ? last10Warnings.value.length : 0))
|
||||||
const headerInfoCollapsed = ref(false)
|
const headerInfoCollapsed = ref(false)
|
||||||
const subHeaderTop = ref(140)
|
const subHeaderTop = ref(140)
|
||||||
const stickyStackRef = ref(null)
|
const stickyStackRef = ref(null)
|
||||||
@@ -1114,6 +1397,167 @@ const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) =>
|
|||||||
gbpTotal: 0
|
gbpTotal: 0
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const summaryOpen = ref(false)
|
||||||
|
|
||||||
|
function makeEmptySummaryRow (part) {
|
||||||
|
return {
|
||||||
|
part,
|
||||||
|
inputAmountLabel: '-',
|
||||||
|
inputCurrencyLabel: '-',
|
||||||
|
inputUnitLabel: '-',
|
||||||
|
meterLabel: '-',
|
||||||
|
meterQty: 0,
|
||||||
|
meterUom: '',
|
||||||
|
usd: 0,
|
||||||
|
try: 0,
|
||||||
|
hasCmtOrMalzemeli: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function accumulateInputAmount (bucket, row) {
|
||||||
|
const qty = resolveNumericRowQuantity(row)
|
||||||
|
const inputPrice = resolveNumericRowInputPrice(row)
|
||||||
|
const cur = resolveInputCurrency(row) || 'USD'
|
||||||
|
const amount = (Number.isFinite(inputPrice) ? inputPrice : 0) * (Number.isFinite(qty) ? qty : 0)
|
||||||
|
if (!Number.isFinite(amount) || amount === 0) return
|
||||||
|
if (!bucket.inputByCur) bucket.inputByCur = {}
|
||||||
|
bucket.inputByCur[cur] = (bucket.inputByCur[cur] || 0) + amount
|
||||||
|
}
|
||||||
|
|
||||||
|
function accumulateUnitPrice (bucket, row) {
|
||||||
|
// For fabric summary: show a representative unit input price (first non-zero).
|
||||||
|
const inputPrice = resolveNumericRowInputPrice(row)
|
||||||
|
const cur = resolveInputCurrency(row) || 'USD'
|
||||||
|
if (!Number.isFinite(inputPrice) || inputPrice <= 0) return
|
||||||
|
if (bucket.unitPrice === undefined || bucket.unitPrice === null) {
|
||||||
|
bucket.unitPrice = inputPrice
|
||||||
|
bucket.unitCur = cur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeInputLabel (bucket) {
|
||||||
|
if (Number(bucket.meterQty || 0) > 0) {
|
||||||
|
const uom = String(bucket.meterUom || '').trim()
|
||||||
|
bucket.meterLabel = `${formatBarQuantity(bucket.meterQty)}${uom ? ' ' + uom : ''}`
|
||||||
|
} else {
|
||||||
|
bucket.meterLabel = bucket.meterLabel || '-'
|
||||||
|
}
|
||||||
|
const byCur = bucket.inputByCur || {}
|
||||||
|
const currencies = Object.keys(byCur).filter(Boolean)
|
||||||
|
if (currencies.length === 0) {
|
||||||
|
bucket.inputAmountLabel = '-'
|
||||||
|
bucket.inputCurrencyLabel = '-'
|
||||||
|
bucket.inputUnitLabel = bucket.inputUnitLabel || '-'
|
||||||
|
return bucket
|
||||||
|
}
|
||||||
|
if (currencies.length === 1) {
|
||||||
|
const cur = currencies[0]
|
||||||
|
bucket.inputCurrencyLabel = cur
|
||||||
|
bucket.inputAmountLabel = formatMoney(byCur[cur] || 0)
|
||||||
|
if (bucket.unitPrice !== undefined && bucket.unitCur) {
|
||||||
|
bucket.inputUnitLabel = formatMoney(bucket.unitPrice)
|
||||||
|
bucket.inputCurrencyLabel = String(bucket.unitCur || cur).toUpperCase()
|
||||||
|
}
|
||||||
|
return bucket
|
||||||
|
}
|
||||||
|
// Multiple currencies mixed: keep labels short.
|
||||||
|
bucket.inputCurrencyLabel = 'MIX'
|
||||||
|
bucket.inputAmountLabel = formatMoney(currencies.reduce((acc, c) => acc + (byCur[c] || 0), 0))
|
||||||
|
return bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailSummary = computed(() => {
|
||||||
|
// Parts must be dynamic. Use sParcaAdi coming from backend (spUrtMTBolum.sAdi),
|
||||||
|
// and keep a stable order: common parts first, then the rest by first appearance.
|
||||||
|
const preferredParts = ['CEKET', 'PANTOLON', 'YELEK', 'AKSESUAR', 'YAKA']
|
||||||
|
const seenParts = new Set()
|
||||||
|
const dynamicParts = []
|
||||||
|
flatDetailRows.value.forEach((row) => {
|
||||||
|
const partRaw = normalizeGroupName(row?.sParcaAdi)
|
||||||
|
const part = String(partRaw || '').trim().toUpperCase()
|
||||||
|
if (!part) return
|
||||||
|
if (seenParts.has(part)) return
|
||||||
|
seenParts.add(part)
|
||||||
|
dynamicParts.push(part)
|
||||||
|
})
|
||||||
|
const parts = [
|
||||||
|
...preferredParts.filter(p => seenParts.has(p)),
|
||||||
|
...dynamicParts.filter(p => !preferredParts.includes(p))
|
||||||
|
]
|
||||||
|
const base = {
|
||||||
|
headerTotals: {
|
||||||
|
usd: toolbarSummary.value.usdTotal || 0,
|
||||||
|
try: toolbarSummary.value.tryTotal || 0,
|
||||||
|
eur: toolbarSummary.value.eurTotal || 0,
|
||||||
|
gbp: toolbarSummary.value.gbpTotal || 0
|
||||||
|
},
|
||||||
|
laborByPart: {},
|
||||||
|
materialByPart: {},
|
||||||
|
fabricByPart: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.forEach((p) => {
|
||||||
|
base.laborByPart[p] = makeEmptySummaryRow(p)
|
||||||
|
base.materialByPart[p] = makeEmptySummaryRow(p)
|
||||||
|
base.fabricByPart[p] = makeEmptySummaryRow(p)
|
||||||
|
})
|
||||||
|
|
||||||
|
flatDetailRows.value.forEach((row) => {
|
||||||
|
const partRaw = normalizeGroupName(row?.sParcaAdi)
|
||||||
|
const part = String(partRaw || '').trim().toUpperCase()
|
||||||
|
if (!part || !parts.includes(part)) return
|
||||||
|
|
||||||
|
const group = String(normalizeGroupName(row?.sAciklama3)).trim().toUpperCase()
|
||||||
|
const included = Boolean(row?.maliyeteDahil) || isCMGroupName(group)
|
||||||
|
if (!included) return
|
||||||
|
|
||||||
|
const usdAmount = resolveRowUSDTutar(row)
|
||||||
|
const tryAmount = resolveRowTRYTutar(row)
|
||||||
|
|
||||||
|
if (isCMGroupName(group)) {
|
||||||
|
const b = base.laborByPart[part]
|
||||||
|
b.usd += usdAmount
|
||||||
|
b.try += tryAmount
|
||||||
|
accumulateInputAmount(b, row)
|
||||||
|
// Tick should reflect actual checkbox state (only when user selected "malzemeli"/type=2).
|
||||||
|
if (resolveCMPriceTypeChecked(row)) b.hasCmtOrMalzemeli = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group === 'DT' || group.includes(' DT') || group === 'TP' || group.includes(' TP')) {
|
||||||
|
const b = base.materialByPart[part]
|
||||||
|
b.usd += usdAmount
|
||||||
|
b.try += tryAmount
|
||||||
|
accumulateInputAmount(b, row)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group === 'FABRIC' || group.includes('FABRIC')) {
|
||||||
|
const b = base.fabricByPart[part]
|
||||||
|
b.usd += usdAmount
|
||||||
|
b.try += tryAmount
|
||||||
|
accumulateInputAmount(b, row)
|
||||||
|
accumulateUnitPrice(b, row)
|
||||||
|
const qty = resolveNumericRowQuantity(row)
|
||||||
|
if (Number.isFinite(qty) && qty > 0) {
|
||||||
|
b.meterQty += qty
|
||||||
|
if (!b.meterUom) b.meterUom = String(row?.sBirim || '').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const laborRows = parts.map((p) => finalizeInputLabel(base.laborByPart[p]))
|
||||||
|
const materialRows = parts.map((p) => finalizeInputLabel(base.materialByPart[p]))
|
||||||
|
const fabricRows = parts.map((p) => finalizeInputLabel(base.fabricByPart[p]))
|
||||||
|
|
||||||
|
return {
|
||||||
|
headerTotals: base.headerTotals,
|
||||||
|
laborRows,
|
||||||
|
materialRows,
|
||||||
|
fabricRows
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const partSummary = computed(() => {
|
const partSummary = computed(() => {
|
||||||
const summary = {}
|
const summary = {}
|
||||||
flatDetailRows.value.forEach(row => {
|
flatDetailRows.value.forEach(row => {
|
||||||
@@ -1130,23 +1574,25 @@ const partSummary = computed(() => {
|
|||||||
return Object.entries(summary).map(([name, totals]) => ({ name, ...totals }))
|
return Object.entries(summary).map(([name, totals]) => ({ name, ...totals }))
|
||||||
})
|
})
|
||||||
const lineHistoryColumns = [
|
const lineHistoryColumns = [
|
||||||
{ name: 'sourceLabel', label: 'Kaynak', field: 'sourceLabel', align: 'left', sortable: false, style: 'width:6%', headerStyle: 'width:6%' },
|
// Not: fixed percentage widths were overflowing at 100% zoom.
|
||||||
{ name: 'dateLabel', label: 'Tarih', field: 'dateLabel', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
// Let the table layout decide widths so the "Sec" column stays visible.
|
||||||
{ name: 'invoiceCode', label: 'Fatura/OnML', field: 'invoiceCode', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
{ name: 'sourceLabel', label: 'Kaynak', field: 'sourceLabel', align: 'left', sortable: false },
|
||||||
{ name: 'companyCode', label: 'Firma Kodu', field: 'companyCode', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
{ name: 'dateLabel', label: 'Tarih', field: 'dateLabel', align: 'left', sortable: true },
|
||||||
{ name: 'companyDescription', label: 'Firma Aciklama', field: 'companyDescription', align: 'left', sortable: true, style: 'width:12%', headerStyle: 'width:12%' },
|
{ name: 'invoiceCode', label: 'Fatura/OnML', field: 'invoiceCode', align: 'left', sortable: true },
|
||||||
{ name: 'itemCode', label: 'Masraf/sKodu', field: 'itemCode', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
{ name: 'companyCode', label: 'Firma Kodu', field: 'companyCode', align: 'left', sortable: true },
|
||||||
{ name: 'itemDescription', label: 'Masraf Detay', field: 'itemDescription', align: 'left', sortable: true, style: 'width:11%', headerStyle: 'width:11%' },
|
{ name: 'companyDescription', label: 'Firma Aciklama', field: 'companyDescription', align: 'left', sortable: true },
|
||||||
{ name: 'colorCode', label: 'Renk', field: 'colorCode', align: 'left', sortable: true, style: 'width:5%', headerStyle: 'width:5%' },
|
{ name: 'itemCode', label: 'Masraf/sKodu', field: 'itemCode', align: 'left', sortable: true },
|
||||||
{ name: 'colorDescription', label: 'Renk Aciklama', field: 'colorDescription', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
{ name: 'itemDescription', label: 'Masraf Detay', field: 'itemDescription', align: 'left', sortable: true },
|
||||||
{ name: 'itemDim1Code', label: 'Dim1', field: 'itemDim1Code', align: 'left', sortable: true, style: 'width:5%', headerStyle: 'width:5%' },
|
{ name: 'colorCode', label: 'Renk', field: 'colorCode', align: 'left', sortable: true },
|
||||||
{ name: 'itemDim1Description', label: 'Dim1 Aciklama', field: 'itemDim1Description', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
{ name: 'colorDescription', label: 'Renk Aciklama', field: 'colorDescription', align: 'left', sortable: true },
|
||||||
{ name: 'quantity', label: 'Miktar', field: 'quantity', align: 'right', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
{ name: 'itemDim1Code', label: 'Dim1', field: 'itemDim1Code', align: 'left', sortable: true },
|
||||||
{ name: 'unit', label: 'Birim', field: 'unit', align: 'left', sortable: true, style: 'width:4%', headerStyle: 'width:4%' },
|
{ name: 'itemDim1Description', label: 'Dim1 Aciklama', field: 'itemDim1Description', align: 'left', sortable: true },
|
||||||
{ name: 'price', label: 'Fiyat', field: 'price', align: 'right', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
{ name: 'quantity', label: 'Miktar', field: 'quantity', align: 'right', sortable: true },
|
||||||
{ name: 'amount', label: 'Tutar', field: 'amount', align: 'right', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
{ name: 'unit', label: 'Birim', field: 'unit', align: 'left', sortable: true },
|
||||||
{ name: 'currency', label: 'Pr. Br.', field: 'currency', align: 'left', sortable: true, style: 'width:5%', headerStyle: 'width:5%' },
|
{ name: 'price', label: 'Fiyat', field: 'price', align: 'right', sortable: true },
|
||||||
{ name: 'select', label: '', field: 'select', align: 'right', sortable: false, style: 'width:6%', headerStyle: 'width:6%' }
|
{ name: 'amount', label: 'Tutar', field: 'amount', align: 'right', sortable: true },
|
||||||
|
{ name: 'currency', label: 'Pr. Br.', field: 'currency', align: 'left', sortable: true },
|
||||||
|
{ name: 'select', label: '', field: 'select', align: 'right', sortable: false }
|
||||||
]
|
]
|
||||||
|
|
||||||
function resolveLineHistoryRowClass (row) {
|
function resolveLineHistoryRowClass (row) {
|
||||||
@@ -1166,7 +1612,7 @@ const detailColumns = [
|
|||||||
{ name: 'sRenk', label: 'Renk', field: 'sRenk', align: 'left', sortable: true },
|
{ name: 'sRenk', label: 'Renk', field: 'sRenk', align: 'left', sortable: true },
|
||||||
{ name: 'lMiktar', label: 'Miktar', field: 'lMiktar', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width: 80px', headerStyle: 'width: 80px' },
|
{ name: 'lMiktar', label: 'Miktar', field: 'lMiktar', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width: 80px', headerStyle: 'width: 80px' },
|
||||||
{ name: 'inputPrice', label: 'Fiyat Giriş', field: 'inputPrice', align: 'right', sortable: false, style: 'width: 80px', headerStyle: 'width: 80px' },
|
{ name: 'inputPrice', label: 'Fiyat Giriş', field: 'inputPrice', align: 'right', sortable: false, style: 'width: 80px', headerStyle: 'width: 80px' },
|
||||||
{ name: 'inputPricePrBr', label: 'Fiyat Giriş Pr.Br.', field: 'inputPricePrBr', align: 'left', sortable: false, style: 'width: 80px', headerStyle: 'width: 80px' },
|
{ name: 'inputPricePrBr', label: 'Fiyat Giriş Pr.Br.', field: 'inputPricePrBr', align: 'left', sortable: false, style: 'width: 92px', headerStyle: 'width: 92px' },
|
||||||
{ name: 'maliyeteDahil', label: 'Maliyete Dahil', field: 'maliyeteDahil', align: 'center', sortable: false },
|
{ name: 'maliyeteDahil', label: 'Maliyete Dahil', field: 'maliyeteDahil', align: 'center', sortable: false },
|
||||||
{ name: 'cmPriceType', label: 'CMT', field: 'cm_price_type_id', align: 'center', sortable: false, style: 'width: 72px', headerStyle: 'width: 72px' },
|
{ name: 'cmPriceType', label: 'CMT', field: 'cm_price_type_id', align: 'center', sortable: false, style: 'width: 72px', headerStyle: 'width: 72px' },
|
||||||
{ name: 'lFiyat', label: 'lFiyat', field: 'lFiyat', align: 'right', sortable: true, format: val => formatMoney(val) },
|
{ name: 'lFiyat', label: 'lFiyat', field: 'lFiyat', align: 'right', sortable: true, format: val => formatMoney(val) },
|
||||||
@@ -1290,14 +1736,22 @@ function formatQuantity (value) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatQuantity2 (value) {
|
||||||
|
return parseMoneyInput(value).toLocaleString('tr-TR', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function formatBarMoney (value) {
|
function formatBarMoney (value) {
|
||||||
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100
|
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100
|
||||||
return formatMoney(roundedValue)
|
return formatMoney(roundedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBarQuantity (value) {
|
function formatBarQuantity (value) {
|
||||||
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 10000) / 10000
|
// Bar/summary quantities: show 2 decimals (requested for MT totals).
|
||||||
return formatQuantity(roundedValue)
|
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100
|
||||||
|
return formatQuantity2(roundedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertPriceToUSD (price, currency) {
|
function convertPriceToUSD (price, currency) {
|
||||||
@@ -1630,8 +2084,15 @@ async function loadRowEditorColorOptions () {
|
|||||||
function primeRowEditorOptionsFromForm () {
|
function primeRowEditorOptionsFromForm () {
|
||||||
upsertEditorOption(rowEditorHammaddeAllOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
upsertEditorOption(rowEditorHammaddeAllOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
||||||
upsertEditorOption(rowEditorHammaddeOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
upsertEditorOption(rowEditorHammaddeOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
||||||
upsertEditorOption(rowEditorItemAllOptions, buildRowEditorItemOption(rowEditorForm.value))
|
// Do not inject invalid free-typed values (e.g. "GAMBOÇ") into the item select options.
|
||||||
upsertEditorOption(rowEditorItemOptions, buildRowEditorItemOption(rowEditorForm.value))
|
// Item options must represent real tbStok model codes (usually like "M.X", "K.X", "I.X").
|
||||||
|
const itemCandidate = buildRowEditorItemOption(rowEditorForm.value)
|
||||||
|
const itemKey = String(itemCandidate?.value || '').trim()
|
||||||
|
const looksLikeModel = /^[A-Za-z0-9]\./.test(itemKey)
|
||||||
|
if (looksLikeModel) {
|
||||||
|
upsertEditorOption(rowEditorItemAllOptions, itemCandidate)
|
||||||
|
upsertEditorOption(rowEditorItemOptions, itemCandidate)
|
||||||
|
}
|
||||||
upsertEditorOption(rowEditorColorAllOptions, buildRowEditorColorOption(rowEditorForm.value))
|
upsertEditorOption(rowEditorColorAllOptions, buildRowEditorColorOption(rowEditorForm.value))
|
||||||
upsertEditorOption(rowEditorColorOptions, buildRowEditorColorOption(rowEditorForm.value))
|
upsertEditorOption(rowEditorColorOptions, buildRowEditorColorOption(rowEditorForm.value))
|
||||||
}
|
}
|
||||||
@@ -2402,7 +2863,8 @@ async function fetchDetail (options = {}) {
|
|||||||
])
|
])
|
||||||
detailHeader.value = headerData && typeof headerData === 'object' ? headerData : null
|
detailHeader.value = headerData && typeof headerData === 'object' ? headerData : null
|
||||||
productionTypes.value = Array.isArray(typesData) ? typesData : []
|
productionTypes.value = Array.isArray(typesData) ? typesData : []
|
||||||
costDate.value = normalizeDateInput(detailHeader.value?.dteKayitTarihi)
|
// Prefer true costing date (spUrtOnMLMas.Tarihi) over record create/update timestamps.
|
||||||
|
costDate.value = normalizeDateInput(detailHeader.value?.maliyetTarihi || detailHeader.value?.dteKayitTarihi)
|
||||||
detailGroups.value = normalizeDetailGroups(groupsData)
|
detailGroups.value = normalizeDetailGroups(groupsData)
|
||||||
initialHeaderSnapshot.value = currentHeaderSnapshot.value
|
initialHeaderSnapshot.value = currentHeaderSnapshot.value
|
||||||
// Optional: hydrate local draft after base data load.
|
// Optional: hydrate local draft after base data load.
|
||||||
@@ -2437,6 +2899,10 @@ async function fetchDetail (options = {}) {
|
|||||||
urun_kodu: detailHeader.value?.UrunKodu || productCode.value,
|
urun_kodu: detailHeader.value?.UrunKodu || productCode.value,
|
||||||
n_urt_recete_id: detailHeader.value?.nUrtReceteID || ''
|
n_urt_recete_id: detailHeader.value?.nUrtReceteID || ''
|
||||||
})
|
})
|
||||||
|
// tbStok exists-bulk kontrolu devre disi (manual giris kapali).
|
||||||
|
// Load last10 avg deviation warnings panel (non-blocking).
|
||||||
|
await refreshLast10Warnings()
|
||||||
|
// (eski tbStok missing dialog/notify kaldirildi)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
detailError.value = await extractApiErrorDetail(err)
|
detailError.value = await extractApiErrorDetail(err)
|
||||||
slog.error('production-product-costing.detail', 'fetch-detail:error', {
|
slog.error('production-product-costing.detail', 'fetch-detail:error', {
|
||||||
@@ -2908,6 +3374,8 @@ function resolveDetailRowClass (row) {
|
|||||||
const key = String(row?.__rowKey || '').trim()
|
const key = String(row?.__rowKey || '').trim()
|
||||||
if (key && requiredAttentionRowKeys.value?.[key]) return 'pcd-detail-row-required'
|
if (key && requiredAttentionRowKeys.value?.[key]) return 'pcd-detail-row-required'
|
||||||
if (row?.requiredPlaceholder) return 'pcd-detail-row-required'
|
if (row?.requiredPlaceholder) return 'pcd-detail-row-required'
|
||||||
|
const code = String(row?.sKodu || '').trim()
|
||||||
|
if (code && isTbStokMissingCode(code)) return 'pcd-detail-row-missing-stock'
|
||||||
return row?.draftChanged ? 'pcd-detail-row-secondary' : ''
|
return row?.draftChanged ? 'pcd-detail-row-secondary' : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3011,8 +3479,27 @@ async function applyFabricCopySelection () {
|
|||||||
|
|
||||||
async function onRowEditorItemChange (value) {
|
async function onRowEditorItemChange (value) {
|
||||||
const selected = rowEditorItemOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
const selected = rowEditorItemOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||||||
|
// Prevent free-typed values from being accepted as "code". User must pick from list.
|
||||||
|
if (!selected) {
|
||||||
|
const typed = String(value || '').trim()
|
||||||
|
// If user cleared the field, allow clearing (other validations will catch blank-on-save if needed).
|
||||||
|
if (!typed) {
|
||||||
|
rowEditorForm.value.sKodu = ''
|
||||||
|
rowEditorLastValidItemValue.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Revert to last valid selection.
|
||||||
|
rowEditorForm.value.sKodu = String(rowEditorLastValidItemValue.value || '').trim()
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Kod secilmeden serbest metin girilemez. Listeden secim yapin.',
|
||||||
|
position: 'top-right'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
rowEditorForm.value.sKodu = String(value || '').trim()
|
rowEditorForm.value.sKodu = String(value || '').trim()
|
||||||
if (!selected) return
|
rowEditorLastValidItemValue.value = String(value || '').trim()
|
||||||
const previousColorCode = String(rowEditorForm.value.ColorCode || '').trim()
|
const previousColorCode = String(rowEditorForm.value.ColorCode || '').trim()
|
||||||
rowEditorForm.value.nStokID = String(selected.nStokID || '').trim()
|
rowEditorForm.value.nStokID = String(selected.nStokID || '').trim()
|
||||||
rowEditorForm.value.sModel = String(selected.sModel || '').trim()
|
rowEditorForm.value.sModel = String(selected.sModel || '').trim()
|
||||||
@@ -3129,6 +3616,7 @@ function applyEditorRowToGroups (nextRow) {
|
|||||||
detailGroups.value = sortDetailGroups(nextGroups)
|
detailGroups.value = sortDetailGroups(nextGroups)
|
||||||
syncAllGroupsOpen()
|
syncAllGroupsOpen()
|
||||||
schedulePersistLocalDraft()
|
schedulePersistLocalDraft()
|
||||||
|
// tbStok validation devre disi
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncAllGroupsOpen () {
|
function syncAllGroupsOpen () {
|
||||||
@@ -3704,6 +4192,7 @@ function openNewRowDialog () {
|
|||||||
maliyeteDahil: true,
|
maliyeteDahil: true,
|
||||||
sBirim: 'AD'
|
sBirim: 'AD'
|
||||||
})
|
})
|
||||||
|
rowEditorLastValidItemValue.value = ''
|
||||||
primeRowEditorOptionsFromForm()
|
primeRowEditorOptionsFromForm()
|
||||||
rowEditorDialogOpen.value = true
|
rowEditorDialogOpen.value = true
|
||||||
void bootstrapRowEditorOptions()
|
void bootstrapRowEditorOptions()
|
||||||
@@ -3716,6 +4205,7 @@ function openRowEditorForEdit (row) {
|
|||||||
...row,
|
...row,
|
||||||
sAciklama3: row?.sAciklama3 || ''
|
sAciklama3: row?.sAciklama3 || ''
|
||||||
})
|
})
|
||||||
|
rowEditorLastValidItemValue.value = String(rowEditorForm.value?.sKodu || '').trim()
|
||||||
primeRowEditorOptionsFromForm()
|
primeRowEditorOptionsFromForm()
|
||||||
rowEditorDialogOpen.value = true
|
rowEditorDialogOpen.value = true
|
||||||
void bootstrapRowEditorOptions()
|
void bootstrapRowEditorOptions()
|
||||||
@@ -4113,6 +4603,7 @@ async function saveChanges () {
|
|||||||
saveLoading.value = true
|
saveLoading.value = true
|
||||||
try {
|
try {
|
||||||
requiredAttentionRowKeys.value = {}
|
requiredAttentionRowKeys.value = {}
|
||||||
|
// tbStok exists-bulk kontrolu devre disi (manual giris kapali): save bloke edilmez.
|
||||||
if (isNoCostDetail.value) {
|
if (isNoCostDetail.value) {
|
||||||
const missing = computeMissingRequiredSlots()
|
const missing = computeMissingRequiredSlots()
|
||||||
slog.info('production-product-costing.detail', 'required:missing:computed', {
|
slog.info('production-product-costing.detail', 'required:missing:computed', {
|
||||||
@@ -4319,6 +4810,10 @@ async function saveChanges () {
|
|||||||
|
|
||||||
// For existing costing, just refresh the detail.
|
// For existing costing, just refresh the detail.
|
||||||
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
||||||
|
// last10 warnings are computed async on backend; re-check shortly after save.
|
||||||
|
window.setTimeout(() => {
|
||||||
|
try { refreshLast10Warnings() } catch {}
|
||||||
|
}, 1200)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Surface backend message (http.Error text) when available.
|
// Surface backend message (http.Error text) when available.
|
||||||
const msg = String(
|
const msg = String(
|
||||||
@@ -4487,13 +4982,21 @@ watch(
|
|||||||
|
|
||||||
.pcd-toolbar-summary {
|
.pcd-toolbar-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pcd-toolbar-summary-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.pcd-toolbar-pill {
|
.pcd-toolbar-pill {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -4516,6 +5019,64 @@ watch(
|
|||||||
color: #2b3c54;
|
color: #2b3c54;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pcd-toolbar-pill-warn {
|
||||||
|
background: #ef6c00;
|
||||||
|
border-color: #ef6c00;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-summary-expansion :deep(.q-expansion-item__container) {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-missing-stock-code {
|
||||||
|
background: #c62828 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-missing-stock-code :deep(*) {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-detail-row-missing-stock td {
|
||||||
|
background: #c62828 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-detail-row-missing-stock td :deep(*) {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-summary-title {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #2b3c54;
|
||||||
|
margin: 2px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-summary-table td,
|
||||||
|
.pcd-summary-table th {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-summary-total-row td {
|
||||||
|
border-top: 2px solid rgba(0, 0, 0, 0.15);
|
||||||
|
background: #fbfbfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-summary-k {
|
||||||
|
width: 60px;
|
||||||
|
color: #6b7680;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-summary-v {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.pcd-toolbar-pill-label {
|
.pcd-toolbar-pill-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ import api from 'src/services/api'
|
|||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
|
|
||||||
const { canUpdate } = usePermission()
|
const { canUpdate } = usePermission()
|
||||||
const canUpdateUser = canUpdate('user')
|
// This screen manages system-wide permission sets; gate by system:update.
|
||||||
|
const canUpdateUser = canUpdate('system')
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -394,13 +395,22 @@ async function save () {
|
|||||||
|
|
||||||
const payload = []
|
const payload = []
|
||||||
|
|
||||||
|
// UI action keys -> backend action codes
|
||||||
|
const toBackendAction = {
|
||||||
|
write: 'insert',
|
||||||
|
read: 'view',
|
||||||
|
delete: 'delete',
|
||||||
|
update: 'update',
|
||||||
|
export: 'export'
|
||||||
|
}
|
||||||
|
|
||||||
rows.value.forEach(r => {
|
rows.value.forEach(r => {
|
||||||
|
|
||||||
actions.forEach(a => {
|
actions.forEach(a => {
|
||||||
|
|
||||||
payload.push({
|
payload.push({
|
||||||
module: r.module,
|
module: r.module,
|
||||||
action: a.key,
|
action: toBackendAction[a.key] || a.key,
|
||||||
allowed: r[a.key]
|
allowed: r[a.key]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user