Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-20 16:18:48 +03:00
parent 8975e7127b
commit d094adf0b4
2 changed files with 113 additions and 111 deletions

View File

@@ -1228,6 +1228,15 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
http.Error(w, "Gecersiz JSON", http.StatusBadRequest) http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return return
} }
// Guardrail: do not allow blank stock codes for rows that specify a hammadde type.
// UI should already block this, but we enforce it server-side too.
for _, row := range req.Detail.Upserts {
if row.NHammaddeTuruNo > 0 && strings.TrimSpace(row.SKodu) == "" {
logger.Warn("validation failed: blank s_kodu", "n_hammadde_turu_no", row.NHammaddeTuruNo, "n_onml_det_no", row.NOnMLDetNo)
http.Error(w, "Kod bos olamaz (hammadde turu secili satir var)", http.StatusBadRequest)
return
}
}
req.DetailSource = strings.ToLower(strings.TrimSpace(req.DetailSource)) req.DetailSource = strings.ToLower(strings.TrimSpace(req.DetailSource))
req.Header.UrunKodu = strings.TrimSpace(req.Header.UrunKodu) req.Header.UrunKodu = strings.TrimSpace(req.Header.UrunKodu)
@@ -1696,19 +1705,23 @@ WHEN NOT MATCHED THEN
// ============================================================ // ============================================================
// Recipe sync (URETIM): ensure recipe contains all OnML hammadde rows // Recipe sync (URETIM): ensure recipe contains all OnML hammadde rows
// so future no-cost loads don't keep showing them as missing. // so future no-cost loads don't keep showing them as missing.
// Table observed in queries: dbo.spUrtRecMBolumMik (nUrtMBolumID stores nHammaddeTuruNo). // 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 { if req.Header.NUrtReceteID > 0 {
receteID := req.Header.NUrtReceteID receteID := req.Header.NUrtReceteID
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "recipe_sync", "n_urt_recete_id", receteID) 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) // Determine next available recipe detail id (nUrtRecMBolumID) globally.
// NOTE: nUrtRecMBolumID is smallint and not identity in this schema.
nextRecDetID := 0 nextRecDetID := 0
_ = tx.QueryRowContext(ctx, ` _ = tx.QueryRowContext(ctx, `
SELECT ISNULL(MAX(RMik.nUrtRecMBolumID), 0) + 1 SELECT ISNULL(MAX(R.nUrtRecMBolumID), 0) + 1
FROM dbo.spUrtRecMBolumMik RMik WITH (UPDLOCK, HOLDLOCK) FROM dbo.spUrtRecMBolum R WITH (UPDLOCK, HOLDLOCK)
WHERE RMik.nUrtReceteID = @p1 `).Scan(&nextRecDetID)
`, receteID).Scan(&nextRecDetID)
if nextRecDetID <= 0 { if nextRecDetID <= 0 {
nextRecDetID = 1 nextRecDetID = 1
} }
@@ -1718,6 +1731,11 @@ WHERE RMik.nUrtReceteID = @p1
if hNo <= 0 { if hNo <= 0 {
continue 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. // 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. // We check the group label (sAciklama3) from the row itself.
@@ -1727,132 +1745,93 @@ WHERE RMik.nUrtReceteID = @p1
continue continue
} }
// In this version of URETIM DB, the table name is dbo.spUrtRecMBolumMik // FK safety: nUrtMBolumID must exist in dbo.spUrtMBolum.
// but it uses _G suffixes for quantity and amount columns. var bolumExists int
// Also nHStokID_G stores the stock code string rather than just an ID.
rawSKodu := strings.TrimSpace(row.SKodu)
// Ensure a section entry (spUrtRecMBolum) exists for this hNo (Hammadde Turu)
// in the current recipe, otherwise detail rows (Mik) cannot be linked properly.
var sectionExists int
_ = tx.QueryRowContext(ctx, `
SELECT COUNT(1) FROM dbo.spUrtRecMBolum WITH (NOLOCK)
WHERE nUrtReceteID = @p1 AND nUrtMBolumID = @p2
`, receteID, hNo).Scan(&sectionExists)
if sectionExists <= 0 {
logger.Info("creating missing recipe section", "n_urt_recete_id", receteID, "n_urt_m_bolum_id", hNo)
_, _ = tx.ExecContext(ctx, `
INSERT INTO dbo.spUrtRecMBolum (nUrtReceteID, nUrtUBolumID, nUrtMBolumID, nUrtMTBolumID, sKullaniciAdi, dteIslemTarihi)
VALUES (@p1, 13, @p2, @p3, @p4, GETDATE())
`, receteID, hNo, row.NUrtMTBolumID, user)
}
// Update quantity and prices if row already exists for (recete, hammadde, stok_code)
// Using nHStokID_G (string code) for matching as per user screenshot.
var exists int
if err := tx.QueryRowContext(ctx, ` if err := tx.QueryRowContext(ctx, `
SELECT COUNT(1) SELECT COUNT(1) FROM dbo.spUrtMBolum WITH (NOLOCK)
FROM dbo.spUrtRecMBolumMik RMik WITH (NOLOCK) WHERE nUrtMBolumID = @p1
WHERE RMik.nUrtReceteID = @p1 `, hNo).Scan(&bolumExists); err != nil || bolumExists <= 0 {
AND RMik.nUrtMBolumID = @p2 logger.Warn("recipe sync skip: missing spUrtMBolum", "n_urt_m_bolum_id", hNo, "s_kodu", strings.TrimSpace(row.SKodu))
AND LTRIM(RTRIM(RMik.nHStokID_G)) = @p3
`, receteID, hNo, rawSKodu).Scan(&exists); err == nil && exists > 0 {
// Compute TRY unit price for recipe mirror columns.
cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz))
in := row.FiyatGirilen
unitTRYRec := in
switch cur {
case "USD":
unitTRYRec = in * usdRate
case "EUR":
unitTRYRec = in * eurRate
case "GBP":
unitTRYRec = in * gbpRate
case "TRY", "TL", "":
unitTRYRec = in
default:
unitTRYRec = in
}
_, _ = tx.ExecContext(ctx, `
UPDATE dbo.spUrtRecMBolumMik
SET lHMiktar_G = @p4,
lHMaliyet_G = @p5,
sKullaniciAdiDeg = @p6,
dteIslemTarihiDeg = GETDATE(),
bIslem = @p7
WHERE nUrtReceteID = @p1
AND nUrtMBolumID = @p2
AND LTRIM(RTRIM(nHStokID_G)) = @p3
`, receteID, hNo, rawSKodu, row.LMiktar, unitTRYRec, user, row.MaliyeteDahil)
continue continue
} }
// Insert missing: using _G columns and storing code in nHStokID_G. // Upsert target key: (receteID, hNo, sKodu).
// Compute TRY unit price for recipe mirror columns. rawSKodu := strings.TrimSpace(row.SKodu)
cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz)) if rawSKodu == "" {
in := row.FiyatGirilen continue
unitTRYRec := in }
switch cur {
case "USD": // Update qty if exists.
unitTRYRec = in * usdRate var exists int
case "EUR": if err := tx.QueryRowContext(ctx, `
unitTRYRec = in * eurRate SELECT COUNT(1)
case "GBP": FROM dbo.spUrtRecMBolum R WITH (NOLOCK)
unitTRYRec = in * gbpRate WHERE R.nUrtReceteID = @p1
case "TRY", "TL", "": AND R.nUrtMBolumID = @p2
unitTRYRec = in AND LTRIM(RTRIM(R.nHStokID_G)) = @p3
default: `, receteID, hNo, rawSKodu).Scan(&exists); err == nil && exists > 0 {
unitTRYRec = in _, _ = 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, ` _, insertErr := tx.ExecContext(ctx, `
INSERT INTO dbo.spUrtRecMBolumMik ( INSERT INTO dbo.spUrtRecMBolum (
nUrtRecMBolumID,
nUrtReceteID, nUrtReceteID,
nUrtUBolumID, nUrtUBolumID,
nUrtRecMBolumID, nUrtMBolumID,
nStokID, nUrtMTBolumID,
nStokTipiID,
nHStokID_G, nHStokID_G,
lHMiktar_G, lHMiktar_G,
lHFire_G, lHFire_G,
lHCarpan,
nMaliyetTipiID, nMaliyetTipiID,
lHMaliyet_G, lHMaliyet_G,
lMiktar_G, nMTalimat_G,
sIslemKodu,
nUrtMBolumID,
nUrtMTBolumID,
lHCarpan,
bIslem, bIslem,
nSure, nSure,
sAciklama, sIslemKodu,
sKullaniciAdi, lHMiktar_GHedef,
dteIslemTarihi,
nMBolumSarfTipiNo nMBolumSarfTipiNo
) )
VALUES ( VALUES (
@p1, @p1, -- nUrtRecMBolumID (smallint)
13, @p2, -- nUrtReceteID
@p2, @p3, -- nUrtUBolumID
0, @p4, -- nUrtMBolumID
@p3, -- nHStokID_G (Code) 0, -- nUrtMTBolumID (tinyint)
@p4, -- lHMiktar_G 1, -- nStokTipiID
0, @p5, -- nHStokID_G (sKodu)
6, @p6, -- lHMiktar_G
@p5, -- lHMaliyet_G 0, -- lHFire_G
1, -- lMiktar_G 1, -- lHCarpan
'', 6, -- nMaliyetTipiID
@p6, 0, -- lHMaliyet_G
@p7, 2, -- nMTalimat_G
1, 0, -- bIslem
@p8, -- bIslem 0, -- nSure
0, '', -- sIslemKodu (NOT NULL)
NULL, 0, -- lHMiktar_GHedef
@p9, 1 -- nMBolumSarfTipiNo
GETDATE(),
1
) )
`, receteID, nextRecDetID, rawSKodu, row.LMiktar, unitTRYRec, hNo, row.NUrtMTBolumID, row.MaliyeteDahil, user) `, nextRecDetID, receteID, 13, hNo, rawSKodu, row.LMiktar)
if insertErr == nil { if insertErr == nil {
nextRecDetID += 1 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)
} }
} }
} }

View File

@@ -3627,6 +3627,29 @@ async function saveChanges () {
return return
} }
// Validate: "Kod" (sKodu) is mandatory for any row that has a hammadde type.
// We block saving if there are rows with empty/whitespace code, to avoid sending blank rows to backend.
const blankCodeRows = (Array.isArray(flatDetailRows.value) ? flatDetailRows.value : [])
.filter(r => {
const hNo = parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
if (!(hNo > 0)) return false
return String(r?.sKodu || '').trim() === ''
})
if (blankCodeRows.length > 0) {
const next = {}
blankCodeRows.forEach(r => {
const key = String(r?.__rowKey || '').trim()
if (key) next[key] = true
})
requiredAttentionRowKeys.value = next
$q.notify({
type: 'warning',
message: `Kod bos birakilan satirlar var. Kaydetmeden once Kod alanini doldurun. (Adet: ${blankCodeRows.length})`,
position: 'top-right'
})
return
}
const header = detailHeader.value const header = detailHeader.value
const upserts = flatDetailRows.value.map(r => ({ const upserts = flatDetailRows.value.map(r => ({
n_onml_det_no: parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0, n_onml_det_no: parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0,