Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-14 02:19:59 +03:00
parent 43f965a3cf
commit 7d1304b75a
8 changed files with 726 additions and 109 deletions

View File

@@ -21,7 +21,8 @@ UI_DIR=/opt/bssapp/ui/dist
# ===============================
# DATABASES
# ===============================
POSTGRES_CONN=host=46.224.33.150 port=5432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable
# Local dev: connect via SSH tunnel to the server (Windows -> 15432 -> server 127.0.0.1:5432).
POSTGRES_CONN=host=127.0.0.1 port=15432 user=postgres password=tayitkan dbname=baggib2b sslmode=disable
MSSQL_CONN=sqlserver://sa:Gil_0150@10.0.0.9:1433?database=BAGGI_V3&encrypt=disable
URETIM_MSSQL_CONN=sqlserver://sa:Gil_0150@10.0.0.9:1433?database=URETIM&encrypt=disable

View File

@@ -16,6 +16,7 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
@@ -906,6 +907,12 @@ func main() {
log.Println("⚠️ .env / mail.env bulunamadı")
}
// Local dev convenience: on Windows we generally want to override .env with .env.local
// (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432).
if runtime.GOOS == "windows" {
_ = godotenv.Overload(".env.local")
}
jwtSecret := os.Getenv("JWT_SECRET")
if len(jwtSecret) < 10 {
log.Fatal("❌ JWT_SECRET tanımlı değil veya çok kısa (min 10 karakter)")

View File

@@ -98,6 +98,7 @@ type ProductionHasCostDetailHeader struct {
NOnMLNo string `json:"nOnMLNo"`
UrunKodu string `json:"UrunKodu"`
UrunAdi string `json:"UrunAdi"`
UrunIlkGrubu string `json:"UrunIlkGrubu"`
UrunAnaGrubu string `json:"UrunAnaGrubu"`
UrunAltGrubu string `json:"UrunAltGrubu"`
UretimSekliID string `json:"UretimSekliID"`
@@ -131,6 +132,7 @@ type ProductionHasCostDetailEditorOption struct {
NHammaddeTuruNo string `json:"nHammaddeTuruNo"`
SHammaddeTuruAdi string `json:"sHammaddeTuruAdi"`
SAciklama3 string `json:"sAciklama3"`
MTUrtMTBolumID int `json:"mtUrtMTBolumID"`
SKodu string `json:"sKodu"`
SAciklama string `json:"sAciklama"`
SModel string `json:"sModel"`

View File

@@ -37,6 +37,34 @@ ORDER BY ProductCode;
return urunAnaGrubu, urunAltGrubu, nil
}
func GetProductIlkAnaAltGrupByUrunKodu(ctx context.Context, mssqlDB *sql.DB, urunKodu string) (urunIlkGrubu string, urunAnaGrubu string, urunAltGrubu string, err error) {
urunKodu = strings.TrimSpace(urunKodu)
if mssqlDB == nil || urunKodu == "" {
return "", "", "", nil
}
// Nebim V3: ProductFilterWithDescription exposes ProductAtt42Desc (ilk grup), ProductAtt01Desc (ana), ProductAtt02Desc (alt).
sqlText := `
SELECT TOP 1
ISNULL(ProductAtt42Desc, '') AS UrunIlkGrubu,
ISNULL(ProductAtt01Desc, '') AS UrunAnaGrubu,
ISNULL(ProductAtt02Desc, '') AS UrunAltGrubu
FROM ProductFilterWithDescription('TR')
WHERE IsBlocked = 0
AND LTRIM(RTRIM(ProductCode)) = @p1
ORDER BY ProductCode;
`
row := mssqlDB.QueryRowContext(ctx, sqlText, urunKodu)
if err := row.Scan(&urunIlkGrubu, &urunAnaGrubu, &urunAltGrubu); err != nil {
if err == sql.ErrNoRows {
return "", "", "", nil
}
return "", "", "", err
}
return urunIlkGrubu, urunAnaGrubu, urunAltGrubu, nil
}
func GetProductionProductCostingAnaGrupOptions(ctx context.Context, mssqlDB *sql.DB, search string, limit int) (*sql.Rows, error) {
search = strings.TrimSpace(search)
if limit <= 0 {
@@ -131,6 +159,7 @@ SELECT TOP (@p2)
ISNULL(B.sAdi, '') AS sAdi
FROM dbo.spUrtMTBolum B WITH (NOLOCK)
WHERE ISNULL(B.bAktif, 0) = 1
AND ISNULL(B.nUrtTipiID, 0) = 1
AND (@p1 = '' OR ISNULL(B.sAdi, '') LIKE @p3 OR CONVERT(VARCHAR(32), ISNULL(B.nUrtMTBolumID, 0)) LIKE @p3)
ORDER BY B.nUrtMTBolumID
`
@@ -160,6 +189,7 @@ SELECT
FROM dbo.mk_MaliyetParcaEslestirme M WITH (NOLOCK)
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
ON B.nUrtMTBolumID = M.nUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1
OUTER APPLY (
SELECT
STUFF((
@@ -268,6 +298,97 @@ DECLARE @id INT;
return mappingID, nil
}
type ProductionProductCostingInvalidHammaddeForPart struct {
NHammaddeTuruNo int
Aciklama string
MTBolumID int
}
// FilterHammaddeTurleriForPart enforces spUrtOnMLHammaddeTuru.MTnUrtMTBolumID rules:
// - If MTnUrtMTBolumID > 0, the hammadde type is allowed only for that part (nUrtMTBolumID).
// - If MTnUrtMTBolumID is 0/NULL, it is considered global/unknown and allowed.
func FilterHammaddeTurleriForPart(
ctx context.Context,
uretimDB *sql.DB,
nUrtMTBolumID int,
nHammaddeTurleri []int,
) (allowed []int, invalid []ProductionProductCostingInvalidHammaddeForPart, err error) {
seen := make(map[int]bool, len(nHammaddeTurleri))
unique := make([]int, 0, len(nHammaddeTurleri))
for _, n := range nHammaddeTurleri {
if n <= 0 || seen[n] {
continue
}
seen[n] = true
unique = append(unique, n)
}
if len(unique) == 0 {
return []int{}, []ProductionProductCostingInvalidHammaddeForPart{}, nil
}
// Build IN list safely.
ph := make([]string, 0, len(unique))
args := make([]any, 0, len(unique))
for i, n := range unique {
ph = append(ph, fmt.Sprintf("@p%d", i+1))
args = append(args, n)
}
sqlText := fmt.Sprintf(`
SELECT
ISNULL(H.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
ISNULL(H.sAciklama, '') AS sAciklama,
ISNULL(H.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID
FROM dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
WHERE H.nHammaddeTuruNo IN (%s)
`, strings.Join(ph, ","))
rows, qerr := uretimDB.QueryContext(ctx, sqlText, args...)
if qerr != nil {
return nil, nil, qerr
}
defer rows.Close()
type meta struct {
aciklama string
mtBolumID int
}
metaByNo := map[int]meta{}
for rows.Next() {
var no int
var aciklama string
var mtBolum int
if scanErr := rows.Scan(&no, &aciklama, &mtBolum); scanErr != nil {
return nil, nil, scanErr
}
metaByNo[no] = meta{aciklama: strings.TrimSpace(aciklama), mtBolumID: mtBolum}
}
if rowsErr := rows.Err(); rowsErr != nil {
return nil, nil, rowsErr
}
allowed = make([]int, 0, len(unique))
invalid = make([]ProductionProductCostingInvalidHammaddeForPart, 0)
for _, n := range unique {
m, ok := metaByNo[n]
if !ok {
// Unknown in lookup; allow (we can't validate).
allowed = append(allowed, n)
continue
}
if m.mtBolumID > 0 && nUrtMTBolumID > 0 && m.mtBolumID != nUrtMTBolumID {
invalid = append(invalid, ProductionProductCostingInvalidHammaddeForPart{
NHammaddeTuruNo: n,
Aciklama: m.aciklama,
MTBolumID: m.mtBolumID,
})
continue
}
allowed = append(allowed, n)
}
return allowed, invalid, nil
}
func SetProductionProductCostingParcaMappingActive(ctx context.Context, uretimDB *sql.DB, id int, bAktif bool, user string) error {
user = strings.TrimSpace(user)
activeVal := 0
@@ -699,7 +820,8 @@ WITH RecipeMatch AS (
),
HammaddeTekil AS (
SELECT
ISNULL(NULLIF(LTRIM(RTRIM(HT.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(HT.sAciklama)), ''), N'TANIMSIZ')) AS sAciklama3,
-- Group label: DT/TP/CM2/FABRIC... Prefer sAciklama3, then sAciklama2. Never fall back to sAciklama (name).
COALESCE(NULLIF(LTRIM(RTRIM(HT.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(HT.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
ISNULL(HT.nHammaddeTuruNo, 0) AS nHammaddeTuruNoSort,
RTRIM(CONVERT(VARCHAR(32), ISNULL(HT.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
-- Match URETIM's sp_pUrtOnMaliyetRecetedenKop behavior: use model code + color code instead of variant stock code.
@@ -709,6 +831,8 @@ HammaddeTekil AS (
ISNULL(HT.sAciklama, '') AS sHammaddeTuruAdi,
ISNULL(S.sBirimCinsi1, '') AS sBirim,
ISNULL(RMik.lHMiktar, 0) AS lMiktar,
ISNULL(HT.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID,
ISNULL(B.sAdi, '') AS sParcaAdi,
ROW_NUMBER() OVER (
PARTITION BY HT.nHammaddeTuruNo
ORDER BY ISNULL(S.sModel, ISNULL(S.sKodu, ''))
@@ -725,13 +849,20 @@ HammaddeTekil AS (
SELECT TOP 1
H.nHammaddeTuruNo,
H.sAciklama,
H.sAciklama3
H.sAciklama2,
H.sAciklama3,
H.MTnUrtMTBolumID
FROM dbo.spUrtOnMLHammaddeTuru H
WHERE H.nUrtMBolumID = RMik.nUrtMBolumID
-- In our data, RMik.nUrtMBolumID carries the required hammadde type number (e.g. 900/901/3300),
-- not a "bolum id". So match by hammadde type number.
WHERE H.nHammaddeTuruNo = RMik.nUrtMBolumID
ORDER BY
CASE WHEN H.nUrtMTBolumID = RMik.nUrtMTBolumID THEN 0 ELSE 1 END,
CASE WHEN H.MTnUrtMTBolumID = RMik.nUrtMTBolumID THEN 0 ELSE 1 END,
H.nHammaddeTuruNo
) HT
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
ON B.nUrtMTBolumID = HT.MTnUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE HT.nHammaddeTuruNo IS NOT NULL
)
SELECT
@@ -762,7 +893,7 @@ SELECT
0.0 AS gbpTutar,
HT.sBirim,
HT.sHammaddeTuruAdi,
HT.sHammaddeTuruAdi AS sParcaAdi
HT.sParcaAdi AS sParcaAdi
FROM HammaddeTekil HT
WHERE HT.rn = 1
ORDER BY
@@ -800,15 +931,22 @@ func GetProductionHasCostDetailHammaddeTypeOptions(
SELECT TOP (@p2)
RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ') AS sAciklama3
FROM dbo.spUrtOnMLHammaddeTuru T
WHERE
ISNULL(T.bAktif, 0) = 1
AND (
COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
ISNULL(B.sAdi, '') AS sParcaAdi
FROM dbo.spUrtOnMLHammaddeTuru T WITH (NOLOCK)
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE
ISNULL(T.bAktif, 0) = 1
AND (
@p1 = ''
OR RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) LIKE @p3
OR ISNULL(T.sAciklama, '') LIKE @p3
OR ISNULL(T.sAciklama2, '') LIKE @p3
OR ISNULL(T.sAciklama3, '') LIKE @p3
OR ISNULL(B.sAdi, '') LIKE @p3
)
ORDER BY
CASE

View File

@@ -596,10 +596,11 @@ func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Requ
logger.Info("request done", "n_urt_recete_id", item.NUrtReceteID, "urun_kodu", item.UrunKodu)
if mssqlDB != nil {
ana, alt, err := queries.GetProductAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu)
ilk, ana, alt, err := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu)
if err != nil {
logger.Warn("product group query error", "err", err)
} else {
item.UrunIlkGrubu = ilk
item.UrunAnaGrubu = ana
item.UrunAltGrubu = alt
}
@@ -659,10 +660,11 @@ func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Requ
logger.Info("request done", "n_onml_no", item.NOnMLNo, "urun_kodu", item.UrunKodu, "n_urt_recete_id", item.NUrtReceteID)
if mssqlDB != nil {
ana, alt, err := queries.GetProductAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu)
ilk, ana, alt, err := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu)
if err != nil {
logger.Warn("product group query error", "err", err)
} else {
item.UrunIlkGrubu = ilk
item.UrunAnaGrubu = ana
item.UrunAltGrubu = alt
}
@@ -761,7 +763,7 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
list := make([]models.ProductionHasCostDetailEditorOption, 0, limit)
for rows.Next() {
var item models.ProductionHasCostDetailEditorOption
if err := rows.Scan(&item.NHammaddeTuruNo, &item.SHammaddeTuruAdi, &item.SAciklama3); err != nil {
if err := rows.Scan(&item.NHammaddeTuruNo, &item.SHammaddeTuruAdi, &item.SAciklama3, &item.MTUrtMTBolumID, &item.SParcaAdi); err != nil {
logger.Warn("hammadde scan error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde scan error: %v", err)
continue
@@ -769,7 +771,6 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
item.Kind = "hammadde"
item.Value = item.NHammaddeTuruNo
item.Label = strings.TrimSpace(item.NHammaddeTuruNo + " - " + item.SHammaddeTuruAdi)
item.SParcaAdi = item.SAciklama3
list = append(list, item)
}
if err := rows.Err(); err != nil {
@@ -1958,7 +1959,18 @@ func PostProductionProductCostingParcaMappingUpsertHandler(w http.ResponseWriter
return
}
id, err := queries.UpsertProductionProductCostingParcaMapping(ctx, uretimDB, req.UrunIlkGrubu, req.UrunAnaGrubu, req.UrunAltGrubu, req.NUrtMTBolumID, req.NHammaddeTurleri, req.BAktif, user)
allowed, invalid, ferr := queries.FilterHammaddeTurleriForPart(ctx, uretimDB, req.NUrtMTBolumID, req.NHammaddeTurleri)
if ferr != nil {
logger.Error("hammadde validation error", "err", ferr)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
// Soft-enforce: drop invalid types instead of hard failing. This prevents a "multi-part save" from failing
// when the UI reuses the same hammadde list across multiple MT bolum selections.
// We still return the ignored list so UI/logs can surface it.
ignored := invalid
id, err := queries.UpsertProductionProductCostingParcaMapping(ctx, uretimDB, req.UrunIlkGrubu, req.UrunAnaGrubu, req.UrunAltGrubu, req.NUrtMTBolumID, allowed, req.BAktif, user)
if err != nil {
logger.Error("exec error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
@@ -1966,7 +1978,10 @@ func PostProductionProductCostingParcaMappingUpsertHandler(w http.ResponseWriter
}
logger.Info("request done", "id", id, "user", user, "bAktif", req.BAktif)
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "id": id})
if len(ignored) > 0 {
logger.Warn("hammadde types ignored due to MT bolum mismatch", "nUrtMTBolumID", req.NUrtMTBolumID, "ignored_count", len(ignored))
}
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "id": id, "ignored": ignored})
}
type productionProductCostingSetActiveRequest struct {