Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -767,7 +767,15 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := queries.GetProductionHasCostDetailHammaddeTypeOptions(ctx, uretimDB, search, limit)
|
||||
group := strings.TrimSpace(r.URL.Query().Get("group"))
|
||||
rawOnlyActive := strings.TrimSpace(r.URL.Query().Get("only_active"))
|
||||
var onlyActive *bool = nil
|
||||
if rawOnlyActive != "" {
|
||||
v := rawOnlyActive == "1" || strings.EqualFold(rawOnlyActive, "true")
|
||||
onlyActive = &v
|
||||
}
|
||||
|
||||
rows, err := queries.GetProductionHasCostDetailHammaddeTypeOptions(ctx, uretimDB, search, limit, group, onlyActive)
|
||||
if err != nil {
|
||||
logger.Error("hammadde query error", "err", err)
|
||||
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde query error: %v", err)
|
||||
@@ -839,8 +847,14 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
|
||||
continue
|
||||
}
|
||||
item.Kind = "item"
|
||||
item.Value = item.SKodu
|
||||
item.Label = strings.TrimSpace(item.SKodu + " - " + item.SAciklama)
|
||||
// Use variantless code (tbStok.sModel) as the value for costing/recipe.
|
||||
// Variant-coded tbStok.sKodu can include color/variant suffixes; costing uses base model codes.
|
||||
code := strings.TrimSpace(item.SModel)
|
||||
if code == "" {
|
||||
code = strings.TrimSpace(item.SKodu)
|
||||
}
|
||||
item.Value = code
|
||||
item.Label = strings.TrimSpace(code + " - " + item.SAciklama)
|
||||
list = append(list, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -1237,6 +1251,12 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
}
|
||||
// Header validation: uretim sekli must be selected.
|
||||
if req.Header.UretimSekliID <= 0 {
|
||||
logger.Warn("validation failed: uretim_sekli_id <= 0", "uretim_sekli_id", req.Header.UretimSekliID)
|
||||
http.Error(w, "Uretim sekli secilmeden kayit yapilamaz", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
req.DetailSource = strings.ToLower(strings.TrimSpace(req.DetailSource))
|
||||
req.Header.UrunKodu = strings.TrimSpace(req.Header.UrunKodu)
|
||||
@@ -1457,8 +1477,10 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
|
||||
// Deletes
|
||||
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_deletes", "count", len(req.Detail.Deletes))
|
||||
skippedDeletes := 0
|
||||
for _, d := range req.Detail.Deletes {
|
||||
if d.NOnMLDetNo <= 0 {
|
||||
skippedDeletes += 1
|
||||
continue
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo=@p1 AND nOnMLDetNo=@p2`, nOnMLNo, d.NOnMLDetNo); err != nil {
|
||||
@@ -1467,13 +1489,31 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
}
|
||||
if skippedDeletes > 0 {
|
||||
logger.Warn("detail deletes skipped (det_no<=0)", "trace_id", traceID, "n_onml_no", nOnMLNo, "skipped", skippedDeletes)
|
||||
}
|
||||
|
||||
// Upserts
|
||||
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_upserts", "count", len(req.Detail.Upserts))
|
||||
skippedUpserts := 0
|
||||
skippedUpsertsSample := 0
|
||||
for _, row := range req.Detail.Upserts {
|
||||
if row.NOnMLDetNo <= 0 {
|
||||
skippedUpserts += 1
|
||||
if skippedUpsertsSample < 5 {
|
||||
skippedUpsertsSample += 1
|
||||
logger.Warn("detail upsert skipped (det_no<=0)",
|
||||
"trace_id", traceID,
|
||||
"n_onml_no", nOnMLNo,
|
||||
"n_onml_det_no", row.NOnMLDetNo,
|
||||
"n_hammadde_turu_no", row.NHammaddeTuruNo,
|
||||
"s_kodu", strings.TrimSpace(row.SKodu),
|
||||
"s_aciklama3", strings.TrimSpace(row.SAciklama3),
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
|
||||
// FALLBACK: If nHammaddeTuruNo is missing but sKodu is present, default to 1 (General/Labor)
|
||||
// to avoid blocking the user, especially for labor items.
|
||||
@@ -1701,141 +1741,13 @@ WHEN NOT MATCHED THEN
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Recipe sync (URETIM): ensure recipe contains all OnML hammadde rows
|
||||
// so future no-cost loads don't keep showing them as missing.
|
||||
// IMPORTANT: In current URETIM DB, the detail table is dbo.spUrtRecMBolum (NOT NULL cols, FK to dbo.spUrtMBolum).
|
||||
// We must:
|
||||
// 1) Skip hammadde types that do not exist in dbo.spUrtMBolum (FK safety),
|
||||
// 2) Upsert by (nUrtReceteID, nUrtMBolumID, nHStokID_G=sKodu),
|
||||
// 3) When inserting, generate nUrtRecMBolumID (smallint, not identity) and fill required columns incl. sIslemKodu=''.
|
||||
// ============================================================
|
||||
if req.Header.NUrtReceteID > 0 {
|
||||
receteID := req.Header.NUrtReceteID
|
||||
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "recipe_sync", "n_urt_recete_id", receteID)
|
||||
|
||||
// Determine next available recipe detail id (nUrtRecMBolumID) globally.
|
||||
// NOTE: nUrtRecMBolumID is smallint and not identity in this schema.
|
||||
nextRecDetID := 0
|
||||
_ = tx.QueryRowContext(ctx, `
|
||||
SELECT ISNULL(MAX(R.nUrtRecMBolumID), 0) + 1
|
||||
FROM dbo.spUrtRecMBolum R WITH (UPDLOCK, HOLDLOCK)
|
||||
`).Scan(&nextRecDetID)
|
||||
if nextRecDetID <= 0 {
|
||||
nextRecDetID = 1
|
||||
}
|
||||
|
||||
for _, row := range req.Detail.Upserts {
|
||||
hNo := row.NHammaddeTuruNo
|
||||
if hNo <= 0 {
|
||||
continue
|
||||
}
|
||||
// Legacy mapping: merge deprecated hammadde types into canonical ones.
|
||||
// We migrated 1104 -> 1105 historically; keep runtime mapping to avoid FK issues.
|
||||
if hNo == 1104 {
|
||||
hNo = 1105
|
||||
}
|
||||
|
||||
// 1. FILTER: CM1/CM2 (Labor/Service) rows must NOT be written back into recipe tables.
|
||||
// We check the group label (sAciklama3) from the row itself.
|
||||
g := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
|
||||
if g == "CM1" || g == "CM2" {
|
||||
logger.Info("recipe sync skip: labor item", "s_kodu", row.SKodu, "group", g)
|
||||
continue
|
||||
}
|
||||
|
||||
// FK safety: nUrtMBolumID must exist in dbo.spUrtMBolum.
|
||||
var bolumExists int
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
SELECT COUNT(1) FROM dbo.spUrtMBolum WITH (NOLOCK)
|
||||
WHERE nUrtMBolumID = @p1
|
||||
`, hNo).Scan(&bolumExists); err != nil || bolumExists <= 0 {
|
||||
logger.Warn("recipe sync skip: missing spUrtMBolum", "n_urt_m_bolum_id", hNo, "s_kodu", strings.TrimSpace(row.SKodu))
|
||||
continue
|
||||
}
|
||||
|
||||
// Upsert target key: (receteID, hNo, sKodu).
|
||||
rawSKodu := strings.TrimSpace(row.SKodu)
|
||||
if rawSKodu == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update qty if exists.
|
||||
var exists int
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
SELECT COUNT(1)
|
||||
FROM dbo.spUrtRecMBolum R WITH (NOLOCK)
|
||||
WHERE R.nUrtReceteID = @p1
|
||||
AND R.nUrtMBolumID = @p2
|
||||
AND LTRIM(RTRIM(R.nHStokID_G)) = @p3
|
||||
`, receteID, hNo, rawSKodu).Scan(&exists); err == nil && exists > 0 {
|
||||
_, _ = tx.ExecContext(ctx, `
|
||||
UPDATE dbo.spUrtRecMBolum
|
||||
SET lHMiktar_G = @p4
|
||||
WHERE nUrtReceteID = @p1
|
||||
AND nUrtMBolumID = @p2
|
||||
AND LTRIM(RTRIM(nHStokID_G)) = @p3
|
||||
`, receteID, hNo, rawSKodu, row.LMiktar)
|
||||
continue
|
||||
}
|
||||
|
||||
// Insert missing into dbo.spUrtRecMBolum.
|
||||
// nUrtRecMBolumID is not identity; keep incrementing, but guard against smallint overflow.
|
||||
if nextRecDetID > 32767 {
|
||||
logger.Warn("recipe sync skip: nUrtRecMBolumID overflow risk", "next_id", nextRecDetID, "n_urt_recete_id", receteID)
|
||||
continue
|
||||
}
|
||||
_, insertErr := tx.ExecContext(ctx, `
|
||||
INSERT INTO dbo.spUrtRecMBolum (
|
||||
nUrtRecMBolumID,
|
||||
nUrtReceteID,
|
||||
nUrtUBolumID,
|
||||
nUrtMBolumID,
|
||||
nUrtMTBolumID,
|
||||
nStokTipiID,
|
||||
nHStokID_G,
|
||||
lHMiktar_G,
|
||||
lHFire_G,
|
||||
lHCarpan,
|
||||
nMaliyetTipiID,
|
||||
lHMaliyet_G,
|
||||
nMTalimat_G,
|
||||
bIslem,
|
||||
nSure,
|
||||
sIslemKodu,
|
||||
lHMiktar_GHedef,
|
||||
nMBolumSarfTipiNo
|
||||
)
|
||||
VALUES (
|
||||
@p1, -- nUrtRecMBolumID (smallint)
|
||||
@p2, -- nUrtReceteID
|
||||
@p3, -- nUrtUBolumID
|
||||
@p4, -- nUrtMBolumID
|
||||
0, -- nUrtMTBolumID (tinyint)
|
||||
1, -- nStokTipiID
|
||||
@p5, -- nHStokID_G (sKodu)
|
||||
@p6, -- lHMiktar_G
|
||||
0, -- lHFire_G
|
||||
1, -- lHCarpan
|
||||
6, -- nMaliyetTipiID
|
||||
0, -- lHMaliyet_G
|
||||
2, -- nMTalimat_G
|
||||
0, -- bIslem
|
||||
0, -- nSure
|
||||
'', -- sIslemKodu (NOT NULL)
|
||||
0, -- lHMiktar_GHedef
|
||||
1 -- nMBolumSarfTipiNo
|
||||
)
|
||||
`, nextRecDetID, receteID, 13, hNo, rawSKodu, row.LMiktar)
|
||||
if insertErr == nil {
|
||||
nextRecDetID += 1
|
||||
} else {
|
||||
logger.Warn("recipe sync insert error", "err", insertErr, "n_urt_recete_id", receteID, "n_urt_m_bolum_id", hNo, "s_kodu", rawSKodu)
|
||||
}
|
||||
}
|
||||
if skippedUpserts > 0 {
|
||||
logger.Warn("detail upserts skipped summary (det_no<=0)", "trace_id", traceID, "n_onml_no", nOnMLNo, "skipped", skippedUpserts)
|
||||
}
|
||||
|
||||
// NOTE: Recipe tables are intentionally NOT synced from OnML saves.
|
||||
// This costing screen is the source of truth only for dbo.spUrtOnMLMas / dbo.spUrtOnMLMasDet.
|
||||
|
||||
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "commit")
|
||||
if err := tx.Commit(); err != nil {
|
||||
logger.Error("tx commit error", "err", err)
|
||||
@@ -1942,67 +1854,49 @@ DELETE FROM dbo.spUrtOnMLMas WHERE nOnMLNo = @p1
|
||||
return
|
||||
}
|
||||
|
||||
// V3: Delete the base price row we created for this costing date (PriceDate = maliyetTarihi).
|
||||
// V3: Delete base price rows we created for this costing date (PriceDate = maliyetTarihi).
|
||||
// We intentionally do NOT delete older base prices for the same item.
|
||||
// NOTE: UpsertV3ItemBasePriceUSD intentionally uses a non-TR CountryCode to avoid touching the original TR base price.
|
||||
// Therefore delete must NOT filter by CountryCode='TR'; instead, delete by (ItemCode, PriceDate, Currency=USD, BasePriceCode=1)
|
||||
// and only if it was created/updated by this app (Created/LastUpdated starts with BSSAPP).
|
||||
deletedBasePrice := false
|
||||
deletedBasePriceCount := 0
|
||||
if mssqlDB != nil && urunKodu != "" {
|
||||
priceDate := maliyetTarihi.Format("2006-01-02")
|
||||
// Primary rule: delete only the row for this exact date and USD currency.
|
||||
// Safety: require that either CreatedUserName/LastUpdatedUserName matches current user, or one of them starts with BSSAPP.
|
||||
var createdBy sql.NullString
|
||||
var lastBy sql.NullString
|
||||
_ = mssqlDB.QueryRowContext(ctx, `
|
||||
SELECT TOP 1
|
||||
ISNULL(CreatedUserName,'') AS CreatedUserName,
|
||||
ISNULL(LastUpdatedUserName,'') AS LastUpdatedUserName
|
||||
FROM dbo.prItemBasePrice WITH (NOLOCK)
|
||||
WHERE ItemTypeCode = 1
|
||||
AND LTRIM(RTRIM(ItemCode)) = @p1
|
||||
AND ISNULL(CountryCode,'') = 'TR'
|
||||
AND ISNULL(SeasonCode,'') = ''
|
||||
AND ISNULL(BasePriceCode,0) = 1
|
||||
AND CONVERT(date, PriceDate) = CONVERT(date, @p2, 23)
|
||||
AND LTRIM(RTRIM(ISNULL(CurrencyCode,''))) = 'USD'
|
||||
`, urunKodu, priceDate).Scan(&createdBy, &lastBy)
|
||||
|
||||
created := strings.ToUpper(strings.TrimSpace(createdBy.String))
|
||||
last := strings.ToUpper(strings.TrimSpace(lastBy.String))
|
||||
u := strings.ToUpper(strings.TrimSpace(user))
|
||||
|
||||
allowed := false
|
||||
if u != "" && (strings.ToUpper(strings.TrimSpace(createdBy.String)) == u || strings.ToUpper(strings.TrimSpace(lastBy.String)) == u) {
|
||||
allowed = true
|
||||
}
|
||||
if strings.HasPrefix(created, "BSSAPP") || strings.HasPrefix(last, "BSSAPP") {
|
||||
allowed = true
|
||||
}
|
||||
|
||||
if allowed {
|
||||
if _, err := mssqlDB.ExecContext(ctx, `
|
||||
// Delete only rows owned by this app (Created/LastUpdated starts with BSSAPP).
|
||||
res, err := mssqlDB.ExecContext(ctx, `
|
||||
DELETE FROM dbo.prItemBasePrice
|
||||
WHERE ItemTypeCode = 1
|
||||
AND LTRIM(RTRIM(ItemCode)) = @p1
|
||||
AND ISNULL(CountryCode,'') = 'TR'
|
||||
AND ISNULL(SeasonCode,'') = ''
|
||||
AND ISNULL(BasePriceCode,0) = 1
|
||||
AND CONVERT(date, PriceDate) = CONVERT(date, @p2, 23)
|
||||
AND LTRIM(RTRIM(ISNULL(CurrencyCode,''))) = 'USD'
|
||||
`, urunKodu, priceDate); err == nil {
|
||||
deletedBasePrice = true
|
||||
} else {
|
||||
logger.Warn("v3 base price delete failed", "err", err, "urun_kodu", urunKodu, "price_date", priceDate)
|
||||
}
|
||||
AND (
|
||||
UPPER(LTRIM(RTRIM(ISNULL(CreatedUserName,'')))) LIKE 'BSSAPP%'
|
||||
OR UPPER(LTRIM(RTRIM(ISNULL(LastUpdatedUserName,'')))) LIKE 'BSSAPP%'
|
||||
)
|
||||
`, urunKodu, priceDate)
|
||||
if err != nil {
|
||||
logger.Warn("v3 base price delete failed", "err", err, "urun_kodu", urunKodu, "price_date", priceDate)
|
||||
} else {
|
||||
logger.Info("v3 base price delete skipped (not owned)", "urun_kodu", urunKodu, "price_date", priceDate, "created_by", createdBy.String, "last_by", lastBy.String, "user", user)
|
||||
if rows, rerr := res.RowsAffected(); rerr == nil {
|
||||
deletedBasePriceCount = int(rows)
|
||||
deletedBasePrice = deletedBasePriceCount > 0
|
||||
} else {
|
||||
// Unknown affected rows, still mark as attempted.
|
||||
deletedBasePrice = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("delete done", "n_onml_no", req.NOnMLNo, "urun_kodu", urunKodu, "deleted_base_price", deletedBasePrice, "user", user)
|
||||
logger.Info("delete done", "n_onml_no", req.NOnMLNo, "urun_kodu", urunKodu, "deleted_base_price", deletedBasePrice, "deleted_base_price_count", deletedBasePriceCount, "user", user)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"n_onml_no": req.NOnMLNo,
|
||||
"urun_kodu": urunKodu,
|
||||
"deleted_baseprice": deletedBasePrice,
|
||||
"ok": true,
|
||||
"n_onml_no": req.NOnMLNo,
|
||||
"urun_kodu": urunKodu,
|
||||
"deleted_baseprice": deletedBasePrice,
|
||||
"deleted_baseprice_count": deletedBasePriceCount,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3029,12 +2923,51 @@ func GetProductionProductCostingParcaMappingsHandler(w http.ResponseWriter, r *h
|
||||
continue
|
||||
}
|
||||
row.BAktif = bAktif.Valid && bAktif.Bool
|
||||
// Normalize legacy/duplicate hammadde type numbers so UI doesn't miss required CM2 slots.
|
||||
// Some environments have both inactive and active equivalents (e.g. 463->500, 464->3900, 466->12700).
|
||||
normalizeHNo := func(v int) int {
|
||||
switch v {
|
||||
case 108, 463:
|
||||
return 500 // CKT CM2
|
||||
case 109, 464:
|
||||
return 3900 // PNT CM2
|
||||
case 110, 465:
|
||||
return 8300 // YLK CM2
|
||||
case 466:
|
||||
return 12700 // YKA CM2
|
||||
case 467:
|
||||
return 12100 // AKS CM2
|
||||
case 468:
|
||||
return 13500 // GML CM2
|
||||
case 488:
|
||||
return 15300 // KBN CM2
|
||||
case 493:
|
||||
return 15301 // MNT CM2
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
seenH := make(map[int]struct{}, 16)
|
||||
row.NHammaddeTurleri = make([]string, 0, 8)
|
||||
if hammaddeCsv.Valid {
|
||||
for _, part := range strings.Split(hammaddeCsv.String, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "" {
|
||||
row.NHammaddeTurleri = append(row.NHammaddeTurleri, part)
|
||||
n, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
// keep as-is
|
||||
row.NHammaddeTurleri = append(row.NHammaddeTurleri, part)
|
||||
continue
|
||||
}
|
||||
n = normalizeHNo(n)
|
||||
if n <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenH[n]; ok {
|
||||
continue
|
||||
}
|
||||
seenH[n] = struct{}{}
|
||||
row.NHammaddeTurleri = append(row.NHammaddeTurleri, strconv.Itoa(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user