From d094adf0b4f290c034d7f7b446cbd19b7b81de90 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Wed, 20 May 2026 16:18:48 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/routes/production_product_costing.go | 201 ++++++++---------- .../ProductionProductCostingHasCostDetail.vue | 23 ++ 2 files changed, 113 insertions(+), 111 deletions(-) diff --git a/svc/routes/production_product_costing.go b/svc/routes/production_product_costing.go index c850533..5083d27 100644 --- a/svc/routes/production_product_costing.go +++ b/svc/routes/production_product_costing.go @@ -1228,6 +1228,15 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http. http.Error(w, "Gecersiz JSON", http.StatusBadRequest) 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.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 // 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 { 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) + // 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(RMik.nUrtRecMBolumID), 0) + 1 -FROM dbo.spUrtRecMBolumMik RMik WITH (UPDLOCK, HOLDLOCK) -WHERE RMik.nUrtReceteID = @p1 -`, receteID).Scan(&nextRecDetID) +SELECT ISNULL(MAX(R.nUrtRecMBolumID), 0) + 1 +FROM dbo.spUrtRecMBolum R WITH (UPDLOCK, HOLDLOCK) +`).Scan(&nextRecDetID) if nextRecDetID <= 0 { nextRecDetID = 1 } @@ -1718,6 +1731,11 @@ WHERE RMik.nUrtReceteID = @p1 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. @@ -1727,132 +1745,93 @@ WHERE RMik.nUrtReceteID = @p1 continue } - // In this version of URETIM DB, the table name is dbo.spUrtRecMBolumMik - // but it uses _G suffixes for quantity and amount columns. - // 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(§ionExists) - - 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 + // FK safety: nUrtMBolumID must exist in dbo.spUrtMBolum. + var bolumExists int if err := tx.QueryRowContext(ctx, ` -SELECT COUNT(1) -FROM dbo.spUrtRecMBolumMik RMik WITH (NOLOCK) -WHERE RMik.nUrtReceteID = @p1 - AND RMik.nUrtMBolumID = @p2 - 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) +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 } - // Insert missing: using _G columns and storing code in nHStokID_G. - // 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 + // 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.spUrtRecMBolumMik ( +INSERT INTO dbo.spUrtRecMBolum ( + nUrtRecMBolumID, nUrtReceteID, nUrtUBolumID, - nUrtRecMBolumID, - nStokID, + nUrtMBolumID, + nUrtMTBolumID, + nStokTipiID, nHStokID_G, lHMiktar_G, lHFire_G, + lHCarpan, nMaliyetTipiID, lHMaliyet_G, - lMiktar_G, - sIslemKodu, - nUrtMBolumID, - nUrtMTBolumID, - lHCarpan, + nMTalimat_G, bIslem, nSure, - sAciklama, - sKullaniciAdi, - dteIslemTarihi, + sIslemKodu, + lHMiktar_GHedef, nMBolumSarfTipiNo ) VALUES ( - @p1, - 13, - @p2, - 0, - @p3, -- nHStokID_G (Code) - @p4, -- lHMiktar_G - 0, - 6, - @p5, -- lHMaliyet_G - 1, -- lMiktar_G - '', - @p6, - @p7, - 1, - @p8, -- bIslem - 0, - NULL, - @p9, - GETDATE(), - 1 + @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 ) - `, receteID, nextRecDetID, rawSKodu, row.LMiktar, unitTRYRec, hNo, row.NUrtMTBolumID, row.MaliyeteDahil, user) +`, 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) } } } diff --git a/ui/src/pages/ProductionProductCostingHasCostDetail.vue b/ui/src/pages/ProductionProductCostingHasCostDetail.vue index 092367c..99704ac 100644 --- a/ui/src/pages/ProductionProductCostingHasCostDetail.vue +++ b/ui/src/pages/ProductionProductCostingHasCostDetail.vue @@ -3627,6 +3627,29 @@ async function saveChanges () { 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 upserts = flatDetailRows.value.map(r => ({ n_onml_det_no: parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0,