Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -496,6 +496,13 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Keep mk_mail in sync for downstream mail mapping screens.
|
||||
if err := ensureMkMail(tx, payload.Email); err != nil {
|
||||
log.Printf("USER CREATE ensureMkMail ERROR user_id=%d email=%q err=%v", newID, payload.Email, err)
|
||||
http.Error(w, "Mail kaydi guncellenemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// ROLES
|
||||
for _, role := range payload.Roles {
|
||||
role = strings.TrimSpace(role)
|
||||
|
||||
68
svc/routes/mk_mail_helper.go
Normal file
68
svc/routes/mk_mail_helper.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"bssapp-backend/utils"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var simpleEmailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
|
||||
|
||||
// ensureMkMail makes sure the given email exists in mk_mail.
|
||||
// If it already exists (case-insensitive match), it is re-activated and normalized.
|
||||
// This is intentionally tolerant (no ON CONFLICT) to avoid relying on a specific unique constraint.
|
||||
func ensureMkMail(tx *sql.Tx, email string) error {
|
||||
mail := strings.ToLower(strings.TrimSpace(email))
|
||||
if mail == "" {
|
||||
return nil
|
||||
}
|
||||
if !simpleEmailRe.MatchString(mail) {
|
||||
// user email field can be free-form; don't hard fail user save because of mk_mail bookkeeping
|
||||
return nil
|
||||
}
|
||||
|
||||
var id string
|
||||
err := tx.QueryRow(`
|
||||
SELECT m.id::text
|
||||
FROM mk_mail m
|
||||
WHERE LOWER(TRIM(m.email)) = LOWER(TRIM($1))
|
||||
LIMIT 1
|
||||
`, mail).Scan(&id)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
newID := utils.NewUUID()
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO mk_mail (
|
||||
id,
|
||||
email,
|
||||
display_name,
|
||||
"type",
|
||||
is_primary,
|
||||
external_id,
|
||||
is_active,
|
||||
created_at
|
||||
)
|
||||
VALUES ($1, $2, '', 'user', true, true, true, NOW())
|
||||
`, newID, mail)
|
||||
return err
|
||||
}
|
||||
|
||||
// Exists: normalize + activate. Avoid touching created_at.
|
||||
_, err = tx.Exec(`
|
||||
UPDATE mk_mail
|
||||
SET
|
||||
email = $2,
|
||||
display_name = COALESCE(display_name, ''),
|
||||
"type" = 'user',
|
||||
is_primary = true,
|
||||
external_id = true,
|
||||
is_active = true
|
||||
WHERE id::text = $1
|
||||
`, id, mail)
|
||||
return err
|
||||
}
|
||||
@@ -327,6 +327,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
||||
groupName string
|
||||
groupTotal float64
|
||||
groupTotalUSD float64
|
||||
nOnMLNoStr string
|
||||
nOnMLDetNoStr string
|
||||
hNoStr string
|
||||
fiyatGirilen sql.NullFloat64
|
||||
fiyatDoviz sql.NullString
|
||||
maliyeteDahil sql.NullBool
|
||||
@@ -338,9 +341,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
||||
&groupName,
|
||||
&groupTotal,
|
||||
&groupTotalUSD,
|
||||
&item.NOnMLNo,
|
||||
&item.NOnMLDetNo,
|
||||
&item.NHammaddeTuruNo,
|
||||
&nOnMLNoStr,
|
||||
&nOnMLDetNoStr,
|
||||
&hNoStr,
|
||||
&item.SKodu,
|
||||
&item.SAciklama,
|
||||
&item.SRenk,
|
||||
@@ -371,6 +374,10 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
scannedRows += 1
|
||||
|
||||
item.NOnMLNo, _ = strconv.Atoi(nOnMLNoStr)
|
||||
item.NOnMLDetNo, _ = strconv.Atoi(nOnMLDetNoStr)
|
||||
item.NHammaddeTuruNo, _ = strconv.Atoi(hNoStr)
|
||||
|
||||
if fiyatGirilen.Valid {
|
||||
item.FiyatGirilen = new(float64)
|
||||
*item.FiyatGirilen = fiyatGirilen.Float64
|
||||
@@ -426,8 +433,8 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
||||
rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo)
|
||||
if err != nil {
|
||||
logger.Error("query error", "err", err)
|
||||
log.Printf("⌠[ProductionHasCostDetailGroups] query error: %v", err)
|
||||
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
|
||||
log.Printf("❌ [ProductionHasCostDetailGroups] query error: %v", err)
|
||||
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
@@ -442,6 +449,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
||||
groupName string
|
||||
groupTotal float64
|
||||
groupTotalUSD float64
|
||||
nOnMLNoStr string
|
||||
nOnMLDetNoStr string
|
||||
hNoStr string
|
||||
fiyatGirilen sql.NullFloat64
|
||||
fiyatDoviz sql.NullString
|
||||
maliyeteDahil sql.NullBool
|
||||
@@ -453,9 +463,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
||||
&groupName,
|
||||
&groupTotal,
|
||||
&groupTotalUSD,
|
||||
&item.NOnMLNo,
|
||||
&item.NOnMLDetNo,
|
||||
&item.NHammaddeTuruNo,
|
||||
&nOnMLNoStr,
|
||||
&nOnMLDetNoStr,
|
||||
&hNoStr,
|
||||
&item.SKodu,
|
||||
&item.SAciklama,
|
||||
&item.SRenk,
|
||||
@@ -479,10 +489,16 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
||||
&item.SHammaddeTuruAdi,
|
||||
&item.SParcaAdi,
|
||||
); err != nil {
|
||||
log.Printf("âš ï¸ [ProductionHasCostDetailGroups] scan error: %v", err)
|
||||
scanErrors++
|
||||
logger.Warn("scan error", "scan_index", scannedRows+scanErrors, "err", err)
|
||||
log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err)
|
||||
continue
|
||||
}
|
||||
scannedRows += 1
|
||||
scannedRows++
|
||||
|
||||
item.NOnMLNo, _ = strconv.Atoi(nOnMLNoStr)
|
||||
item.NOnMLDetNo, _ = strconv.Atoi(nOnMLDetNoStr)
|
||||
item.NHammaddeTuruNo, _ = strconv.Atoi(hNoStr)
|
||||
|
||||
if fiyatGirilen.Valid {
|
||||
item.FiyatGirilen = new(float64)
|
||||
@@ -1093,21 +1109,74 @@ func PostProductionProductCostingDefaultQuantitiesLookupHandler(w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
m, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos)
|
||||
rows, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos)
|
||||
if err != nil {
|
||||
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(m))
|
||||
for no, qty := range m {
|
||||
out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(rows))
|
||||
for _, r0 := range rows {
|
||||
out = append(out, models.ProductionProductCostingDefaultQtyLookupItem{
|
||||
NHammaddeTuruNo: no,
|
||||
LDefaultMiktar: qty,
|
||||
NHammaddeTuruNo: r0.No,
|
||||
SAciklama: r0.Aciklama,
|
||||
LDefaultMiktar: r0.Qty,
|
||||
})
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
// POST /api/pricing/production-product-costing/has-cost-detail/last-detail
|
||||
func PostProductionProductCostingHasCostDetailLastDetailHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
uretimDB := db.GetUretimDB()
|
||||
if uretimDB == nil {
|
||||
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
traceID := utils.TraceIDFromRequest(r)
|
||||
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||
|
||||
var req models.ProductionProductCostingLastOnMLDetLookupRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rows, err := queries.LookupLastOnMLMasDetByHammaddeNos(ctx, uretimDB, req.NHammaddeTuruNos, req.BeforeDate, req.ExcludeOnMLNo, req.NFirmaID, req.OnlyICode, 1)
|
||||
if err != nil {
|
||||
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
|
||||
// POST /api/pricing/production-product-costing/options/hammadde-by-nos
|
||||
func PostProductionProductCostingOptionsHammaddeByNosHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
uretimDB := db.GetUretimDB()
|
||||
if uretimDB == nil {
|
||||
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
traceID := utils.TraceIDFromRequest(r)
|
||||
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||
|
||||
var req models.ProductionProductCostingHammaddeByNosRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rows, err := queries.GetProductionHammaddeByNos(ctx, uretimDB, req.NHammaddeTuruNos)
|
||||
if err != nil {
|
||||
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
|
||||
// POST /api/pricing/production-product-costing/default-quantities/refresh
|
||||
func PostProductionProductCostingDefaultQuantitiesRefreshHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
@@ -1166,6 +1235,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
req.Header.FirmaKodu = strings.TrimSpace(req.Header.FirmaKodu)
|
||||
|
||||
if req.Header.UrunKodu == "" || req.Header.MaliyetTarihi == "" {
|
||||
logger.Warn("validation failed: missing required header fields",
|
||||
"trace_id", traceID,
|
||||
"urun_kodu", req.Header.UrunKodu,
|
||||
"maliyet_tarihi", req.Header.MaliyetTarihi,
|
||||
)
|
||||
http.Error(w, "urun_kodu ve maliyet_tarihi zorunlu", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -1210,6 +1284,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
firmaID = id
|
||||
}
|
||||
if firmaID <= 0 {
|
||||
logger.Warn("validation failed: missing firma",
|
||||
"trace_id", traceID,
|
||||
"n_firma_id", req.Header.NFirmaID,
|
||||
"firma_kodu", req.Header.FirmaKodu,
|
||||
)
|
||||
http.Error(w, "Firma secilmeden kaydedilemez (n_firma_id / firma_kodu)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -1264,6 +1343,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
// Parse Tarihi
|
||||
tarihi, err := time.Parse("2006-01-02", req.Header.MaliyetTarihi)
|
||||
if err != nil {
|
||||
logger.Warn("validation failed: maliyet_tarihi parse error",
|
||||
"trace_id", traceID,
|
||||
"maliyet_tarihi", req.Header.MaliyetTarihi,
|
||||
"err", err,
|
||||
)
|
||||
http.Error(w, "maliyet_tarihi YYYY-MM-DD formatinda olmali", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -1274,7 +1358,23 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
http.Error(w, "Islem baslatilamadi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
committed := false
|
||||
logger.Info("tx begin",
|
||||
"trace_id", traceID,
|
||||
"n_onml_no", nOnMLNo,
|
||||
"urun_kodu", req.Header.UrunKodu,
|
||||
"maliyet_tarihi", req.Header.MaliyetTarihi,
|
||||
)
|
||||
defer func() {
|
||||
if committed {
|
||||
return
|
||||
}
|
||||
if err := tx.Rollback(); err == nil {
|
||||
logger.Info("tx rollback", "trace_id", traceID, "n_onml_no", nOnMLNo)
|
||||
} else {
|
||||
logger.Warn("tx rollback failed", "trace_id", traceID, "n_onml_no", nOnMLNo, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Determine mamul turu inside same tx (to keep create atomic)
|
||||
mamulLabel := ""
|
||||
@@ -1299,6 +1399,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
if mt <= 0 {
|
||||
logger.Warn("validation failed: mamul turu not found", "trace_id", traceID, "n_onml_no", nOnMLNo, "mamul_label", mamulLabel)
|
||||
http.Error(w, "Mamul turu bulunamadi: "+mamulLabel, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -1320,6 +1421,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
sAciklama = sql.NullString{String: req.Header.SAciklama, Valid: true}
|
||||
}
|
||||
|
||||
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "upsert_header")
|
||||
if err := queries.UpsertOnMLHeader(tx, ctx, queries.OnMLHeaderUpsertArgs{
|
||||
NOnMLNo: nOnMLNo,
|
||||
UrunKodu: req.Header.UrunKodu,
|
||||
@@ -1343,6 +1445,7 @@ 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))
|
||||
for _, d := range req.Detail.Deletes {
|
||||
if d.NOnMLDetNo <= 0 {
|
||||
continue
|
||||
@@ -1355,13 +1458,33 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Upserts
|
||||
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_upserts", "count", len(req.Detail.Upserts))
|
||||
for _, row := range req.Detail.Upserts {
|
||||
if row.NOnMLDetNo <= 0 {
|
||||
continue
|
||||
}
|
||||
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
|
||||
http.Error(w, "Detay satirinda n_hammadde_turu_no ve s_kodu zorunlu", http.StatusBadRequest)
|
||||
return
|
||||
// FALLBACK: If nHammaddeTuruNo is missing but sKodu is present, default to 1 (General/Labor)
|
||||
// to avoid blocking the user, especially for labor items.
|
||||
if strings.TrimSpace(row.SKodu) != "" && row.NHammaddeTuruNo <= 0 {
|
||||
logger.Warn("n_hammadde_turu_no <= 0, falling back to 1",
|
||||
"trace_id", traceID,
|
||||
"n_onml_no", nOnMLNo,
|
||||
"n_onml_det_no", row.NOnMLDetNo,
|
||||
"s_kodu", strings.TrimSpace(row.SKodu),
|
||||
)
|
||||
row.NHammaddeTuruNo = 1
|
||||
} else {
|
||||
logger.Warn("validation failed: missing required detail fields (s_kodu empty)",
|
||||
"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),
|
||||
)
|
||||
http.Error(w, "Detay satirinda s_kodu zorunlu", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
qty := row.LMiktar
|
||||
if qty < 0 {
|
||||
@@ -1387,9 +1510,21 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
lTutar := unitTRY * qty
|
||||
lDovizTutari := unitUSD * qty
|
||||
|
||||
// Debug log for price resolution
|
||||
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.
|
||||
// Note: In this DB, stock type is stored as tbStok.nStokTipi but spUrtOnMLMasDet expects nStokTipiID (int).
|
||||
rawSKodu := strings.TrimSpace(row.SKodu)
|
||||
logger.Info("resolving stock type", "s_kodu", rawSKodu)
|
||||
var nStokTipiID int
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
|
||||
@@ -1411,16 +1546,32 @@ ORDER BY
|
||||
`, rawSKodu).Scan(&nStokTipiID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
|
||||
// FALLBACK: If stock item not found in tbStok at all, default to 1.
|
||||
logger.Warn("stok tipi not found in tbStok, falling back to 1",
|
||||
"trace_id", traceID,
|
||||
"n_onml_no", nOnMLNo,
|
||||
"n_onml_det_no", row.NOnMLDetNo,
|
||||
"s_kodu", rawSKodu,
|
||||
)
|
||||
nStokTipiID = 1
|
||||
} else {
|
||||
logger.Error("stok tipi lookup error", "err", err)
|
||||
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
logger.Error("stok tipi lookup error", "err", err)
|
||||
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
logger.Info("stock type resolved", "s_kodu", rawSKodu, "n_stok_tipi_id", nStokTipiID)
|
||||
if nStokTipiID <= 0 {
|
||||
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
|
||||
return
|
||||
// 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.
|
||||
logger.Warn("stok tipi <= 0, falling back to 1",
|
||||
"trace_id", traceID,
|
||||
"n_onml_no", nOnMLNo,
|
||||
"n_onml_det_no", row.NOnMLDetNo,
|
||||
"s_kodu", rawSKodu,
|
||||
"n_stok_tipi_id_orig", nStokTipiID,
|
||||
)
|
||||
nStokTipiID = 1
|
||||
}
|
||||
|
||||
// Dummy/system-mapped required fields:
|
||||
@@ -1469,7 +1620,8 @@ WHEN MATCHED THEN
|
||||
Maliyete_dahil = @p29,
|
||||
cm_price_type_id = @p30,
|
||||
sKullaniciAdiDeg = @p31,
|
||||
dteIslemTarihiDeg = GETDATE()
|
||||
dteIslemTarihiDeg = GETDATE(),
|
||||
sAciklama3 = NULLIF(@p32, '')
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (
|
||||
nOnMLNo,nOnMLDetNo,nHammaddeTuruNo,sKodu,sAciklama,sRenk,lMiktar,lFiyat,lTutar,
|
||||
@@ -1480,7 +1632,7 @@ WHEN NOT MATCHED THEN
|
||||
VALUES (
|
||||
@p1,@p2,@p3,@p4,@p5,@p6,@p9,@p10,@p11,
|
||||
@p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19,
|
||||
@p20,NULLIF(@p8,''),@p21,@p22,@p31,GETDATE(),NULLIF(@p7,''),NULL,@p23,@p24,
|
||||
@p20,NULLIF(@p8,''),@p21,@p22,@p31,GETDATE(),NULLIF(@p7,''),NULLIF(@p32, ''),@p23,@p24,
|
||||
@p25,@p26,NULLIF(@p27,0),NULLIF(@p28,''),@p29,@p30
|
||||
);
|
||||
`
|
||||
@@ -1519,8 +1671,21 @@ WHEN NOT MATCHED THEN
|
||||
row.MaliyeteDahil,
|
||||
row.CMPriceTypeID,
|
||||
user,
|
||||
strings.TrimSpace(row.SAciklama3), // p32: sAciklama3 (Grup Adi)
|
||||
); err != nil {
|
||||
logger.Error("detail merge error", "err", err)
|
||||
logger.Error("detail merge error",
|
||||
"trace_id", traceID,
|
||||
"n_onml_no", nOnMLNo,
|
||||
"n_onml_det_no", row.NOnMLDetNo,
|
||||
"n_hammadde_turu_no", row.NHammaddeTuruNo,
|
||||
"n_urt_mt_bolum_id", row.NUrtMTBolumID,
|
||||
"s_kodu", strings.TrimSpace(row.SKodu),
|
||||
"fiyat_girilen", row.FiyatGirilen,
|
||||
"fiyat_doviz", strings.TrimSpace(row.FiyatDoviz),
|
||||
"l_miktar", qty,
|
||||
"n_stok_tipi_id", nStokTipiID,
|
||||
"err", err,
|
||||
)
|
||||
http.Error(w, "Detay kaydedilemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -1533,6 +1698,7 @@ WHEN NOT MATCHED THEN
|
||||
// ============================================================
|
||||
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)
|
||||
nextRecDetID := 0
|
||||
@@ -1551,94 +1717,72 @@ WHERE RMik.nUrtReceteID = @p1
|
||||
continue
|
||||
}
|
||||
|
||||
// CM1/CM2 rows are cost-only and must NOT be written back into recipe tables.
|
||||
// Source of truth: spUrtOnMLHammaddeTuru.sAciklama3 (e.g. 'CM2').
|
||||
var grp sql.NullString
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
SELECT TOP 1 LTRIM(RTRIM(ISNULL(H.sAciklama3, '')))
|
||||
FROM dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
|
||||
WHERE H.nHammaddeTuruNo = @p1
|
||||
`, hNo).Scan(&grp); err == nil {
|
||||
g := strings.ToUpper(strings.TrimSpace(grp.String))
|
||||
if g == "CM1" || g == "CM2" {
|
||||
continue
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// Resolve nHStokID from tbStok using sKodu (exact), then sModel fallback.
|
||||
// IMPORTANT: nHStokID must be resolved; otherwise we'd update/insert too broadly.
|
||||
// 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)
|
||||
var nHStokID int
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT TOP 1 ISNULL(S.nStokID, 0)
|
||||
FROM dbo.tbStok S WITH (NOLOCK)
|
||||
WHERE ISNULL(S.IsBlocked, 0) = 0
|
||||
AND (
|
||||
REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '')
|
||||
OR LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1
|
||||
OR @p1 LIKE LTRIM(RTRIM(ISNULL(S.sModel,''))) + '%'
|
||||
)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '') THEN 0
|
||||
WHEN LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1 THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
S.dteKayitTarihi DESC,
|
||||
S.nStokID DESC
|
||||
`, rawSKodu).Scan(&nHStokID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Recete sync icin stok bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
logger.Error("recipe stok lookup error", "err", err)
|
||||
http.Error(w, "Recete sync stok bulunamadi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if nHStokID == 0 {
|
||||
http.Error(w, "Recete sync icin stok bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// If we cannot resolve stock, we still try to insert by hammadde no only.
|
||||
// Some recipes may omit nHStokID; but we prefer filling it when possible.
|
||||
|
||||
// Update quantity if row already exists for (recete, hammadde, stok)
|
||||
// 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, `
|
||||
SELECT COUNT(1)
|
||||
FROM dbo.spUrtRecMBolumMik RMik WITH (NOLOCK)
|
||||
WHERE RMik.nUrtReceteID = @p1
|
||||
AND RMik.nUrtMBolumID = @p2
|
||||
AND RMik.nHStokID = @p3
|
||||
`, receteID, hNo, nHStokID).Scan(&exists); err == nil && exists > 0 {
|
||||
AND LTRIM(RTRIM(RMik.nHStokID_G)) = @p3
|
||||
`, receteID, hNo, rawSKodu).Scan(&exists); err == nil && exists > 0 {
|
||||
_, _ = tx.ExecContext(ctx, `
|
||||
UPDATE dbo.spUrtRecMBolumMik
|
||||
SET lHMiktar = @p4,
|
||||
sKullaniciAdiDeg = @p5,
|
||||
dteIslemTarihiDeg = GETDATE()
|
||||
SET lHMiktar_G = @p4,
|
||||
lHMaliyet_G = @p5,
|
||||
sKullaniciAdiDeg = @p6,
|
||||
dteIslemTarihiDeg = GETDATE(),
|
||||
bIslem = @p7
|
||||
WHERE nUrtReceteID = @p1
|
||||
AND nUrtMBolumID = @p2
|
||||
AND nHStokID = @p3
|
||||
`, receteID, hNo, nHStokID, row.LMiktar, user)
|
||||
AND LTRIM(RTRIM(nHStokID_G)) = @p3
|
||||
`, receteID, hNo, rawSKodu, row.LMiktar, unitTRY, user, row.MaliyeteDahil)
|
||||
continue
|
||||
}
|
||||
|
||||
// Insert missing: best-effort with minimal columns.
|
||||
// NOTE: This assumes nUrtRecMBolumID can be a sequential int and that other columns are nullable/have defaults.
|
||||
// Insert missing: using _G columns and storing code in nHStokID_G.
|
||||
_, insertErr := tx.ExecContext(ctx, `
|
||||
INSERT INTO dbo.spUrtRecMBolumMik (
|
||||
nUrtReceteID,
|
||||
nUrtUBolumID,
|
||||
nUrtRecMBolumID,
|
||||
nStokID,
|
||||
nHStokID,
|
||||
lHMiktar,
|
||||
lHFire,
|
||||
nHStokID_G,
|
||||
lHMiktar_G,
|
||||
lHFire_G,
|
||||
nMaliyetTipiID,
|
||||
lHMaliyet,
|
||||
lMiktar,
|
||||
lHMaliyet_G,
|
||||
lMiktar_G,
|
||||
sIslemKodu,
|
||||
nUrtMBolumID,
|
||||
nUrtMTBolumID,
|
||||
@@ -1646,56 +1790,52 @@ INSERT INTO dbo.spUrtRecMBolumMik (
|
||||
bIslem,
|
||||
nSure,
|
||||
sAciklama,
|
||||
dteDovizTarihi,
|
||||
sDovizCinsi,
|
||||
lDovizOran,
|
||||
lDovizFiyat,
|
||||
sKullaniciAdi,
|
||||
dteIslemTarihi,
|
||||
nMBolumSarfTipiNo
|
||||
)
|
||||
VALUES (
|
||||
@p1,
|
||||
0,
|
||||
13,
|
||||
@p2,
|
||||
0,
|
||||
@p3,
|
||||
@p4,
|
||||
@p3, -- nHStokID_G (Code)
|
||||
@p4, -- lHMiktar_G
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
@p5, -- lHMaliyet_G
|
||||
1, -- lMiktar_G
|
||||
'',
|
||||
@p5,
|
||||
@p6,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
@p7,
|
||||
1,
|
||||
@p8, -- bIslem
|
||||
0,
|
||||
NULL,
|
||||
@p9,
|
||||
GETDATE(),
|
||||
1
|
||||
)
|
||||
`, receteID, nextRecDetID, nHStokID, row.LMiktar, hNo, row.NUrtMTBolumID, user)
|
||||
`, receteID, nextRecDetID, rawSKodu, row.LMiktar, unitTRY, hNo, row.NUrtMTBolumID, row.MaliyeteDahil, user)
|
||||
if insertErr == nil {
|
||||
nextRecDetID += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
http.Error(w, "Kaydetme tamamlanamadi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
committed = true
|
||||
logger.Info("tx commit ok", "trace_id", traceID, "n_onml_no", nOnMLNo)
|
||||
|
||||
// V3: update base price table so pricing screens reflect latest costing.
|
||||
// Not transactional with URETIM DB; if this fails, URETIM save has already succeeded.
|
||||
if mssqlDB != nil {
|
||||
logger.Info("post-commit step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "v3_base_price_upsert")
|
||||
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)
|
||||
http.Error(w, "URETIM kaydedildi ama V3 maliyet guncellenemedi", http.StatusInternalServerError)
|
||||
@@ -1706,6 +1846,131 @@ VALUES (
|
||||
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingOnMLSaveResponse{NOnMLNo: nOnMLNo})
|
||||
}
|
||||
|
||||
// POST /api/pricing/production-product-costing/onml/delete
|
||||
// Deletes costing records created in URETIM (OnML header + details) and, if created by this app, V3 base price row.
|
||||
// IMPORTANT: Recipe tables are NOT touched.
|
||||
func PostProductionProductCostingOnMLDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
uretimDB := db.GetUretimDB()
|
||||
if uretimDB == nil {
|
||||
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
mssqlDB := db.GetDB()
|
||||
|
||||
claims, _ := auth.GetClaimsFromContext(r.Context())
|
||||
user := ""
|
||||
if claims != nil {
|
||||
user = strings.TrimSpace(claims.Username)
|
||||
}
|
||||
|
||||
traceID := utils.TraceIDFromRequest(r)
|
||||
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.onml.delete")
|
||||
|
||||
var req models.ProductionProductCostingOnMLDeleteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.NOnMLNo <= 0 {
|
||||
http.Error(w, "n_onml_no zorunlu", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Load header info for safe deletes and redirect behavior.
|
||||
var urunKodu string
|
||||
var maliyetTarihi time.Time
|
||||
err := uretimDB.QueryRowContext(ctx, `
|
||||
SELECT TOP 1
|
||||
LTRIM(RTRIM(ISNULL(UrunKodu,''))) AS UrunKodu,
|
||||
COALESCE(Tarihi, dteKayitTarihi, GETDATE()) AS Tarihi
|
||||
FROM dbo.spUrtOnMLMas WITH (NOLOCK)
|
||||
WHERE nOnMLNo = @p1
|
||||
`, req.NOnMLNo).Scan(&urunKodu, &maliyetTarihi)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Kayit bulunamadi", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
logger.Error("header lookup error", "err", err)
|
||||
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
urunKodu = strings.TrimSpace(urunKodu)
|
||||
|
||||
tx, err := uretimDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
// Delete details first, then header.
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo = @p1
|
||||
`, req.NOnMLNo); err != nil {
|
||||
logger.Error("delete detail error", "err", err)
|
||||
http.Error(w, "Detay silinemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM dbo.spUrtOnMLMas WHERE nOnMLNo = @p1
|
||||
`, req.NOnMLNo); err != nil {
|
||||
logger.Error("delete header error", "err", err)
|
||||
http.Error(w, "Header silinemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
logger.Error("tx commit error", "err", err)
|
||||
http.Error(w, "Silme tamamlanamadi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// V3: Delete base price ONLY if row was created by this app (CreatedUserName starts with BSSAPP).
|
||||
deletedBasePrice := false
|
||||
if mssqlDB != nil && urunKodu != "" {
|
||||
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
|
||||
`, urunKodu).Scan(&createdBy, &lastBy)
|
||||
|
||||
created := strings.ToUpper(strings.TrimSpace(createdBy.String))
|
||||
last := strings.ToUpper(strings.TrimSpace(lastBy.String))
|
||||
if strings.HasPrefix(created, "BSSAPP") && strings.HasPrefix(last, "BSSAPP") {
|
||||
if _, 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
|
||||
`, urunKodu); err == nil {
|
||||
deletedBasePrice = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("delete done", "n_onml_no", req.NOnMLNo, "urun_kodu", urunKodu, "deleted_base_price", deletedBasePrice, "user", user)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"n_onml_no": req.NOnMLNo,
|
||||
"urun_kodu": urunKodu,
|
||||
"deleted_baseprice": deletedBasePrice,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/pricing/production-product-costing/has-cost-detail-exchange-rates
|
||||
func GetProductionHasCostDetailExchangeRatesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
@@ -251,6 +251,13 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
||||
return
|
||||
}
|
||||
|
||||
// Keep mk_mail in sync for downstream mail mapping screens.
|
||||
if err := ensureMkMail(tx, payload.Email); err != nil {
|
||||
log.Printf("ERROR [UserDetail] ensureMkMail failed user_id=%d email=%q err=%v", userID, payload.Email, err)
|
||||
http.Error(w, "Mail kaydi guncellenemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID); err != nil {
|
||||
log.Printf("❌ [UserDetail] delete roles failed user_id=%d err=%v", userID, err)
|
||||
http.Error(w, "Roller temizlenemedi", http.StatusInternalServerError)
|
||||
|
||||
Reference in New Issue
Block a user