Files
bssapp/svc/routes/production_product_costing.go
2026-05-20 16:19:23 +03:00

3189 lines
102 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"bssapp-backend/models"
"bssapp-backend/queries"
"bssapp-backend/utils"
"context"
"database/sql"
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"time"
)
func logProductionHasCostDetailEditorOptionItemDiagnostics(ctx context.Context, uretimDB *sql.DB, search string, limit int) {
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.detail-editor-options.diagnostics",
"search", search,
"limit", limit,
)
expectedColumns := []string{
"nStokID",
"sKodu",
"sAciklama",
"sModel",
"sBirimCinsi1",
"IsBlocked",
}
probes := []struct {
name string
sqlText string
}{
{
name: "projection:nStokID",
sqlText: `SELECT TOP 1 RTRIM(CONVERT(VARCHAR(32), ISNULL(S.nStokID, 0))) FROM dbo.tbStok S`,
},
{
name: "projection:sKodu",
sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) FROM dbo.tbStok S`,
},
{
name: "projection:sAciklama",
sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sAciklama, '')))) FROM dbo.tbStok S`,
},
{
name: "projection:sModel",
sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) FROM dbo.tbStok S`,
},
{
name: "projection:sBirimCinsi1",
sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(64), ISNULL(S.sBirimCinsi1, '')))) FROM dbo.tbStok S`,
},
{
name: "projection:IsBlocked",
sqlText: `SELECT TOP 1 RTRIM(CONVERT(VARCHAR(32), ISNULL(S.IsBlocked, 0))) FROM dbo.tbStok S`,
},
{
name: "filter:model_like_count",
sqlText: `SELECT RTRIM(CONVERT(VARCHAR(32), COUNT(1))) FROM dbo.tbStok S WHERE LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE '_.%'`,
},
{
name: "filter:isblocked_count",
sqlText: `SELECT RTRIM(CONVERT(VARCHAR(32), COUNT(1))) FROM dbo.tbStok S WHERE ISNULL(S.IsBlocked, 0) = 0`,
},
{
name: "filter:final_count",
sqlText: `SELECT RTRIM(CONVERT(VARCHAR(32), COUNT(1))) FROM dbo.tbStok S WHERE ISNULL(S.IsBlocked, 0) = 0 AND LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE '_.%'`,
},
}
log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics start search=%q limit=%d", search, limit)
for _, columnName := range expectedColumns {
var exists int
err := uretimDB.QueryRowContext(
ctx,
`SELECT COUNT(1)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'dbo'
AND TABLE_NAME = 'tbStok'
AND COLUMN_NAME = @p1`,
columnName,
).Scan(&exists)
if err != nil {
logger.Error("query error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item diagnostics column check error column=%s err=%v", columnName, err)
continue
}
log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics column=%s exists=%t", columnName, exists > 0)
}
for _, probe := range probes {
var sample sql.NullString
err := uretimDB.QueryRowContext(ctx, probe.sqlText).Scan(&sample)
if err != nil {
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item diagnostics probe=%s err=%v", probe.name, err)
continue
}
log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics probe=%s sample=%q", probe.name, strings.TrimSpace(sample.String))
}
log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics end search=%q limit=%d", search, limit)
}
// GET /api/pricing/production-product-costing/no-cost-products
func GetProductionNoCostProductsHandler(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
}
search := strings.TrimSpace(r.URL.Query().Get("search"))
fromDate := strings.TrimSpace(r.URL.Query().Get("from_date"))
rows, err := queries.GetProductionNoCostProducts(r.Context(), uretimDB, fromDate, search)
if err != nil {
log.Printf("❌ [ProductionNoCost] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.ProductionNoCostProductRow, 0, 200)
for rows.Next() {
var item models.ProductionNoCostProductRow
if err := rows.Scan(
&item.UretimSekli,
&item.UrtSiparisNo,
&item.IslemTarihi,
&item.FirmaKodu,
&item.FirmaAdi,
&item.SonIsEmriVeren,
&item.MMiktarG,
&item.MModelKodu,
&item.Kodu,
&item.ModelAdi,
&item.SKullaniciAdi,
&item.SKullaniciGunc,
); err != nil {
log.Printf("⚠️ [ProductionNoCost] scan error: %v", err)
continue
}
list = append(list, item)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ [ProductionNoCost] rows error: %v", err)
http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(list)
}
// GET /api/pricing/production-product-costing/has-cost-products
func GetProductionHasCostProductsHandler(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
}
search := strings.TrimSpace(r.URL.Query().Get("search"))
offset := parsePositiveIntOrDefault(r.URL.Query().Get("offset"), 0)
limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 300)
if limit > 1000 {
limit = 1000
}
rows, err := queries.GetProductionHasCostProducts(r.Context(), uretimDB, search, offset, limit)
if err != nil {
log.Printf("❌ [ProductionHasCost] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.ProductionHasCostProductRow, 0, 200)
for rows.Next() {
var item models.ProductionHasCostProductRow
if err := rows.Scan(
&item.UretimSekli,
&item.NOnMLNo,
&item.UrunKodu,
&item.UrunAdi,
&item.Tarihi,
&item.DteKayitTarihi,
&item.SKullaniciAdi,
&item.LTutarTL,
&item.LTutarUSD,
&item.LTutarEURO,
&item.DteGuncellemeTarihi,
&item.SGuncellemeKullaniciAdi,
&item.NUrtReceteID,
&item.SAciklama,
&item.SonSiparisTarihi,
&item.MaliyetDurumu,
); err != nil {
log.Printf("⚠️ [ProductionHasCost] scan error: %v", err)
continue
}
list = append(list, item)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ [ProductionHasCost] rows error: %v", err)
http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(list)
}
// GET /api/pricing/production-product-costing/has-cost-history
func GetProductionHasCostHistoryHandler(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
}
productCode := strings.TrimSpace(r.URL.Query().Get("urun_kodu"))
if productCode == "" {
http.Error(w, "urun_kodu zorunlu", http.StatusBadRequest)
return
}
rows, err := queries.GetProductionHasCostHistoryByProductCode(r.Context(), uretimDB, productCode)
if err != nil {
log.Printf("❌ [ProductionHasCostHistory] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.ProductionHasCostHistoryRow, 0, 100)
for rows.Next() {
var item models.ProductionHasCostHistoryRow
if err := rows.Scan(
&item.NOnMLNo,
&item.UrunKodu,
&item.UrunAdi,
&item.Tarihi,
&item.SKullaniciAdi,
&item.LTutarUSD,
&item.LTutarTL,
&item.LTutarEURO,
&item.SDovizCinsi,
&item.LTutarDoviz,
&item.DteGuncellemeTarihi,
&item.SGuncellemeKullaniciAdi,
&item.NUrtReceteID,
&item.SAciklama,
); err != nil {
log.Printf("⚠️ [ProductionHasCostHistory] scan error: %v", err)
continue
}
list = append(list, item)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ [ProductionHasCostHistory] rows error: %v", err)
http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(list)
}
// GET /api/pricing/production-product-costing/has-cost-detail-groups
func GetProductionHasCostDetailGroupsHandler(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
}
detailSource := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("detail_source")))
recipeCode := strings.TrimSpace(r.URL.Query().Get("recete_kodu"))
productCode := strings.TrimSpace(r.URL.Query().Get("urun_kodu"))
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.detail-groups",
"detail_source", detailSource,
"urun_kodu", productCode,
"recete_kodu", recipeCode,
)
if detailSource == "no-cost" || recipeCode != "" {
if recipeCode == "" {
logger.Warn("request invalid", "reason", "missing recete_kodu")
http.Error(w, "recete_kodu zorunlu", http.StatusBadRequest)
return
}
logger.Info("request start")
rows, err := queries.GetProductionNoCostDetailRowsByRecipeCode(ctx, uretimDB, recipeCode, productCode)
if err != nil {
logger.Error("query error", "err", err)
log.Printf("❌ [ProductionNoCostDetailGroups] query error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
groups := make([]models.ProductionHasCostDetailGroup, 0, 16)
groupIndexByName := map[string]int{}
scannedRows := 0
scanErrors := 0
for rows.Next() {
var (
groupName string
groupTotal float64
groupTotalUSD float64
nOnMLNoStr string
nOnMLDetNoStr string
hNoStr string
fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool
cmPriceTypeID sql.NullInt64
item models.ProductionHasCostDetailGroupItem
)
if err := rows.Scan(
&groupName,
&groupTotal,
&groupTotalUSD,
&nOnMLNoStr,
&nOnMLDetNoStr,
&hNoStr,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.SBeden,
&item.SAciklama2,
&item.LMiktar,
&item.LFiyat,
&item.LTutar,
&item.SFiyatTipi,
&item.SDovizCinsi,
&item.LDovizKuru,
&item.LDovizFiyati,
&fiyatGirilen,
&fiyatDoviz,
&maliyeteDahil,
&cmPriceTypeID,
&item.USDTutar,
&item.EURTutar,
&item.GBPTutar,
&item.SBirim,
&item.SHammaddeTuruAdi,
&item.SParcaAdi,
); err != nil {
scanErrors += 1
logger.Warn("scan error", "scan_index", scannedRows+scanErrors+1, "err", err)
log.Printf("⚠️ [ProductionNoCostDetailGroups] scan error: %v", err)
continue
}
scannedRows += 1
// Model fields are strings; keep as-is to avoid type mismatches.
item.NOnMLNo = strings.TrimSpace(nOnMLNoStr)
item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr)
item.NHammaddeTuruNo = strings.TrimSpace(hNoStr)
if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64)
*item.FiyatGirilen = fiyatGirilen.Float64
}
if fiyatDoviz.Valid {
item.FiyatDoviz = strings.TrimSpace(fiyatDoviz.String)
}
item.MaliyeteDahil = !maliyeteDahil.Valid || maliyeteDahil.Bool
if cmPriceTypeID.Valid {
value := int(cmPriceTypeID.Int64)
item.CMPriceTypeID = &value
}
idx, ok := groupIndexByName[groupName]
if !ok {
groups = append(groups, models.ProductionHasCostDetailGroup{
SAciklama3: groupName,
TotalTutar: groupTotal,
TotalUSDTutar: groupTotalUSD,
Items: make([]models.ProductionHasCostDetailGroupItem, 0, 8),
})
idx = len(groups) - 1
groupIndexByName[groupName] = idx
}
groups[idx].Items = append(groups[idx].Items, item)
}
if err := rows.Err(); err != nil {
logger.Error("rows error", "err", err)
log.Printf("⚠️ [ProductionNoCostDetailGroups] rows error: %v", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "group_count", len(groups), "row_count", scannedRows, "scan_errors", scanErrors)
log.Printf("[ProductionNoCostDetailGroups] done recete_kodu=%s groups=%d", recipeCode, len(groups))
_ = json.NewEncoder(w).Encode(groups)
return
}
rawOnMLNo := strings.TrimSpace(r.URL.Query().Get("n_onml_no"))
nOnMLNo, err := strconv.Atoi(rawOnMLNo)
if err != nil || nOnMLNo <= 0 {
logger.Warn("request invalid", "reason", "invalid n_onml_no", "raw_n_onml_no", rawOnMLNo)
http.Error(w, "n_onml_no zorunlu ve pozitif sayi olmali", http.StatusBadRequest)
return
}
logger = logger.With("n_onml_no", nOnMLNo)
logger.Info("request start")
log.Printf("[ProductionHasCostDetailGroups] start n_onml_no=%d", nOnMLNo)
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)
return
}
defer rows.Close()
groups := make([]models.ProductionHasCostDetailGroup, 0, 16)
groupIndexByName := map[string]int{}
scannedRows := 0
scanErrors := 0
for rows.Next() {
var (
groupName string
groupTotal float64
groupTotalUSD float64
nOnMLNoStr string
nOnMLDetNoStr string
hNoStr string
fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool
cmPriceTypeID sql.NullInt64
item models.ProductionHasCostDetailGroupItem
)
if err := rows.Scan(
&groupName,
&groupTotal,
&groupTotalUSD,
&nOnMLNoStr,
&nOnMLDetNoStr,
&hNoStr,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.SBeden,
&item.SAciklama2,
&item.LMiktar,
&item.LFiyat,
&item.LTutar,
&item.SFiyatTipi,
&item.SDovizCinsi,
&item.LDovizKuru,
&item.LDovizFiyati,
&fiyatGirilen,
&fiyatDoviz,
&maliyeteDahil,
&cmPriceTypeID,
&item.USDTutar,
&item.EURTutar,
&item.GBPTutar,
&item.SBirim,
&item.SHammaddeTuruAdi,
&item.SParcaAdi,
); err != nil {
scanErrors++
logger.Warn("scan error", "scan_index", scannedRows+scanErrors, "err", err)
log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err)
continue
}
scannedRows++
// Model fields are strings; keep as-is to avoid type mismatches.
item.NOnMLNo = strings.TrimSpace(nOnMLNoStr)
item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr)
item.NHammaddeTuruNo = strings.TrimSpace(hNoStr)
if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64)
*item.FiyatGirilen = fiyatGirilen.Float64
}
if fiyatDoviz.Valid {
item.FiyatDoviz = strings.TrimSpace(fiyatDoviz.String)
}
item.MaliyeteDahil = maliyeteDahil.Valid && maliyeteDahil.Bool
if cmPriceTypeID.Valid {
value := int(cmPriceTypeID.Int64)
item.CMPriceTypeID = &value
}
idx, ok := groupIndexByName[groupName]
if !ok {
groups = append(groups, models.ProductionHasCostDetailGroup{
SAciklama3: groupName,
TotalTutar: groupTotal,
TotalUSDTutar: groupTotalUSD,
Items: make([]models.ProductionHasCostDetailGroupItem, 0, 24),
})
idx = len(groups) - 1
groupIndexByName[groupName] = idx
}
groups[idx].Items = append(groups[idx].Items, item)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ [ProductionHasCostDetailGroups] rows error: %v", err)
http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError)
return
}
logger.Info("request done", "group_count", len(groups), "row_count", scannedRows, "scan_errors", scanErrors)
log.Printf("[ProductionHasCostDetailGroups] done n_onml_no=%d groups=%d rows=%d scan_errors=%d", nOnMLNo, len(groups), scannedRows, scanErrors)
_ = json.NewEncoder(w).Encode(groups)
}
// GET /api/pricing/production-product-costing/has-cost-detail-header
func GetProductionHasCostDetailHeaderHandler(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()
detailSource := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("detail_source")))
recipeCode := strings.TrimSpace(r.URL.Query().Get("recete_kodu"))
productCode := strings.TrimSpace(r.URL.Query().Get("urun_kodu"))
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.detail-header",
"detail_source", detailSource,
"urun_kodu", productCode,
"recete_kodu", recipeCode,
)
if detailSource == "no-cost" || recipeCode != "" {
if recipeCode == "" {
logger.Warn("request invalid", "reason", "missing recete_kodu")
http.Error(w, "recete_kodu zorunlu", http.StatusBadRequest)
return
}
logger.Info("request start")
row, err := queries.GetProductionNoCostDetailHeaderByRecipeCode(ctx, uretimDB, recipeCode, productCode)
if err != nil {
logger.Error("query prepare error", "err", err)
log.Printf("❌ [ProductionNoCostDetailHeader] query prepare error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
var item models.ProductionHasCostDetailHeader
if err := row.Scan(
&item.UretimiYapanFirma,
&item.SonIsEmriVeren,
&item.FirmaKodu,
&item.NFirmaID,
&item.NOnMLNo,
&item.UrunKodu,
&item.UrunAdi,
&item.UretimSekliID,
&item.UretimSekli,
&item.DteKayitTarihi,
&item.SKullaniciAdi,
&item.LTutarTL,
&item.LTutarUSD,
&item.LTutarEURO,
&item.LTutarGBP,
&item.SDovizCinsi,
&item.LTutarDoviz,
&item.DteGuncellemeTarihi,
&item.SGuncellemeKullaniciAdi,
&item.NUrtReceteID,
); err != nil {
logger.Warn("scan or not found", "err", err)
if err == sql.ErrNoRows {
logger.Warn("row not found")
http.Error(w, "Kayit bulunamadi", http.StatusNotFound)
return
}
log.Printf("❌ [ProductionNoCostDetailHeader] scan error: %v", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "n_urt_recete_id", item.NUrtReceteID, "urun_kodu", item.UrunKodu)
if mssqlDB != nil {
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
}
}
_ = json.NewEncoder(w).Encode(item)
return
}
rawOnMLNo := strings.TrimSpace(r.URL.Query().Get("n_onml_no"))
nOnMLNo, err := strconv.Atoi(rawOnMLNo)
if err != nil || nOnMLNo <= 0 {
logger.Warn("request invalid", "reason", "invalid n_onml_no", "raw_n_onml_no", rawOnMLNo)
http.Error(w, "n_onml_no zorunlu ve pozitif sayi olmali", http.StatusBadRequest)
return
}
logger = logger.With("n_onml_no", nOnMLNo)
logger.Info("request start")
row, err := queries.GetProductionHasCostDetailHeaderByOnMLNo(ctx, uretimDB, nOnMLNo)
if err != nil {
logger.Error("query prepare error", "err", err)
log.Printf("❌ [ProductionHasCostDetailHeader] query prepare error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
var item models.ProductionHasCostDetailHeader
if err := row.Scan(
&item.UretimiYapanFirma,
&item.SonIsEmriVeren,
&item.FirmaKodu,
&item.NFirmaID,
&item.NOnMLNo,
&item.UrunKodu,
&item.UrunAdi,
&item.UretimSekliID,
&item.UretimSekli,
&item.DteKayitTarihi,
&item.SKullaniciAdi,
&item.LTutarTL,
&item.LTutarUSD,
&item.LTutarEURO,
&item.LTutarGBP,
&item.SDovizCinsi,
&item.LTutarDoviz,
&item.DteGuncellemeTarihi,
&item.SGuncellemeKullaniciAdi,
&item.NUrtReceteID,
); err != nil {
logger.Warn("scan or not found", "err", err)
if err == sql.ErrNoRows {
http.Error(w, "Kayit bulunamadi", http.StatusNotFound)
return
}
log.Printf("❌ [ProductionHasCostDetailHeader] scan error: %v", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "n_onml_no", item.NOnMLNo, "urun_kodu", item.UrunKodu, "n_urt_recete_id", item.NUrtReceteID)
if mssqlDB != nil {
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
}
}
_ = json.NewEncoder(w).Encode(item)
}
// GET /api/pricing/production-product-costing/production-types
func GetProductionTypesHandler(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)
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.production-types")
logger.Info("request start")
rows, err := queries.GetProductionTypes(ctx, uretimDB)
if err != nil {
logger.Error("query error", "err", err)
log.Printf("❌ [ProductionTypes] query error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
var types []models.ProductionType
for rows.Next() {
var t models.ProductionType
if err := rows.Scan(&t.ID, &t.Aciklama); err != nil {
logger.Warn("scan error", "err", err)
log.Printf("⚠️ [ProductionTypes] scan error: %v", err)
continue
}
types = append(types, t)
}
if err := rows.Err(); err != nil {
logger.Error("rows error", "err", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "row_count", len(types))
_ = json.NewEncoder(w).Encode(types)
}
// GET /api/pricing/production-product-costing/detail-editor-options
func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
kind := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("kind")))
search := strings.TrimSpace(r.URL.Query().Get("search"))
limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 100)
if limit > 200 {
limit = 200
}
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.detail-editor-options",
"kind", kind,
"search", search,
"limit", limit,
)
logger.Info("request start")
switch kind {
case "hammadde":
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
rows, err := queries.GetProductionHasCostDetailHammaddeTypeOptions(ctx, uretimDB, search, limit)
if err != nil {
logger.Error("hammadde query error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde query error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
if columns, columnErr := rows.Columns(); columnErr != nil {
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item columns read error search=%q limit=%d err=%v", search, limit, columnErr)
} else {
log.Printf("[ProductionHasCostDetailEditorOptions] item columns=%v", columns)
}
list := make([]models.ProductionHasCostDetailEditorOption, 0, limit)
for rows.Next() {
var item models.ProductionHasCostDetailEditorOption
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
}
item.Kind = "hammadde"
item.Value = item.NHammaddeTuruNo
item.Label = strings.TrimSpace(item.NHammaddeTuruNo + " - " + item.SHammaddeTuruAdi)
list = append(list, item)
}
if err := rows.Err(); err != nil {
logger.Error("hammadde rows error", "err", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "kind", "hammadde", "row_count", len(list))
_ = json.NewEncoder(w).Encode(list)
return
case "item":
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
log.Printf("[ProductionHasCostDetailEditorOptions] item start search=%q limit=%d", search, limit)
rows, err := queries.GetProductionHasCostDetailItemOptions(ctx, uretimDB, search, limit)
if err != nil {
logger.Error("item query error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item query error search=%q limit=%d err=%v", search, limit, err)
logProductionHasCostDetailEditorOptionItemDiagnostics(ctx, uretimDB, search, limit)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
if columns, columnErr := rows.Columns(); columnErr != nil {
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item columns read error search=%q limit=%d err=%v", search, limit, columnErr)
} else {
log.Printf("[ProductionHasCostDetailEditorOptions] item columns=%v", columns)
}
list := make([]models.ProductionHasCostDetailEditorOption, 0, limit)
scanErrors := 0
for rows.Next() {
var item models.ProductionHasCostDetailEditorOption
if err := rows.Scan(&item.NStokID, &item.SKodu, &item.SAciklama, &item.SModel, &item.SBirim); err != nil {
scanErrors += 1
logger.Warn("item scan error", "scan_index", len(list)+scanErrors, "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item scan error scan_index=%d err=%v", len(list)+scanErrors, err)
continue
}
item.Kind = "item"
item.Value = item.SKodu
item.Label = strings.TrimSpace(item.SKodu + " - " + item.SAciklama)
list = append(list, item)
}
if err := rows.Err(); err != nil {
logger.Error("item rows error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item rows error search=%q limit=%d err=%v", search, limit, err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "kind", "item", "row_count", len(list), "scan_errors", scanErrors)
log.Printf("[ProductionHasCostDetailEditorOptions] item done search=%q limit=%d row_count=%d scan_errors=%d", search, limit, len(list), scanErrors)
_ = json.NewEncoder(w).Encode(list)
return
case "color":
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
modelCode := strings.TrimSpace(r.URL.Query().Get("model_code"))
if modelCode == "" {
_ = json.NewEncoder(w).Encode([]models.ProductionHasCostDetailEditorOption{})
return
}
rows, err := queries.GetProductionHasCostDetailColorOptions(ctx, uretimDB, modelCode, search, limit)
if err != nil {
logger.Error("color query error", "model_code", modelCode, "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] color query error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.ProductionHasCostDetailEditorOption, 0, limit)
for rows.Next() {
var item models.ProductionHasCostDetailEditorOption
if err := rows.Scan(&item.ColorCode, &item.ColorDescription); err != nil {
logger.Warn("color scan error", "model_code", modelCode, "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] color scan error: %v", err)
continue
}
item.Kind = "color"
item.Value = item.ColorCode
item.Label = strings.TrimSpace(item.ColorCode + " - " + item.ColorDescription)
list = append(list, item)
}
if err := rows.Err(); err != nil {
logger.Error("color rows error", "model_code", modelCode, "err", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "kind", "color", "model_code", modelCode, "row_count", len(list))
_ = json.NewEncoder(w).Encode(list)
return
}
logger.Warn("request invalid", "reason", "invalid kind")
http.Error(w, "kind hammadde, item veya color olmali", http.StatusBadRequest)
}
// GET /api/pricing/production-product-costing/default-quantities
func GetProductionProductCostingDefaultQuantitiesHandler(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)
search := strings.TrimSpace(r.URL.Query().Get("search"))
limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 500)
// Always list only active hammadde turleri.
rows, err := queries.ListProductionProductCostingDefaultQtyRows(ctx, uretimDB, search, limit)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
out := make([]models.ProductionProductCostingDefaultQtyRow, 0, 256)
for rows.Next() {
var item models.ProductionProductCostingDefaultQtyRow
if err := rows.Scan(&item.NHammaddeTuruNo, &item.SAciklama, &item.LDefaultMiktar, &item.DteCalcTarihi, &item.BAktif); err != nil {
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
out = append(out, item)
}
if err := rows.Err(); err != nil {
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(out)
}
// POST /api/pricing/production-product-costing/default-quantities/upsert
func PostProductionProductCostingDefaultQuantitiesUpsertHandler(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.ProductionProductCostingDefaultQtyUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
if req.NHammaddeTuruNo <= 0 {
http.Error(w, "nHammaddeTuruNo zorunlu", http.StatusBadRequest)
return
}
if req.LDefaultMiktar <= 0 {
http.Error(w, "lDefaultMiktar pozitif olmali", http.StatusBadRequest)
return
}
if err := queries.UpsertProductionProductCostingDefaultQtyRow(ctx, uretimDB, req.NHammaddeTuruNo, req.LDefaultMiktar, req.BAktif); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}
// POST /api/pricing/production-product-costing/default-quantities/update-bulk
func PostProductionProductCostingDefaultQuantitiesBulkUpdateHandler(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.ProductionProductCostingDefaultQtyBulkUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
if len(req.Items) == 0 {
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "updated": 0})
return
}
if len(req.Items) > 5000 {
http.Error(w, "cok fazla satir", http.StatusBadRequest)
return
}
tx, err := uretimDB.BeginTx(ctx, nil)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer func() { _ = tx.Rollback() }()
updated := 0
for _, item := range req.Items {
if item.NHammaddeTuruNo <= 0 {
http.Error(w, "nHammaddeTuruNo zorunlu", http.StatusBadRequest)
return
}
if item.LDefaultMiktar <= 0 {
http.Error(w, "lDefaultMiktar pozitif olmali", http.StatusBadRequest)
return
}
activeVal := -1
if item.BAktif != nil {
if *item.BAktif {
activeVal = 1
} else {
activeVal = 0
}
}
sqlText := `
UPDATE dbo.mk_MaliyetParcaEslestirme_vmiktarlar
SET
lDefaultMiktar = @p2,
dteCalcTarihi = GETDATE(),
bAktif = CASE WHEN @p3 < 0 THEN ISNULL(bAktif, 1) ELSE @p3 END
WHERE nHammaddeTuruNo = @p1;
SELECT @@ROWCOUNT;
`
var affected int
if err := tx.QueryRowContext(ctx, sqlText, item.NHammaddeTuruNo, item.LDefaultMiktar, activeVal).Scan(&affected); err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
if affected == 0 {
http.Error(w, "satir bulunamadi: "+strconv.Itoa(item.NHammaddeTuruNo), http.StatusBadRequest)
return
}
updated++
}
if err := tx.Commit(); err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "updated": updated})
}
// POST /api/pricing/production-product-costing/default-quantities/calc-avg
func PostProductionProductCostingDefaultQuantitiesCalcAvgHandler(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.ProductionProductCostingDefaultQtyCalcRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
if req.NHammaddeTuruNo <= 0 {
http.Error(w, "nHammaddeTuruNo zorunlu", http.StatusBadRequest)
return
}
if req.TopN <= 0 {
req.TopN = 10
}
avg, cnt, err := queries.CalcProductionProductCostingDefaultQtyFromLastOnML(ctx, uretimDB, req.NHammaddeTuruNo, req.TopN)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingDefaultQtyCalcResponse{
NHammaddeTuruNo: req.NHammaddeTuruNo,
LDefaultMiktar: avg,
NSampleCount: cnt,
})
}
// POST /api/pricing/production-product-costing/default-quantities/lookup
func PostProductionProductCostingDefaultQuantitiesLookupHandler(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.ProductionProductCostingDefaultQtyLookupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
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(rows))
for _, r0 := range rows {
out = append(out, models.ProductionProductCostingDefaultQtyLookupItem{
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")
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)
topN := parsePositiveIntOrDefault(r.URL.Query().Get("top_n"), 10)
if err := queries.RefreshProductionProductCostingDefaultQty(ctx, uretimDB, topN); err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "top_n": topN})
}
// POST /api/pricing/production-product-costing/onml/save
func PostProductionProductCostingOnMLSaveHandler(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.save")
var req models.ProductionProductCostingOnMLSaveRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logger.Warn("invalid json", "err", err)
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)
req.Header.UrunAdi = strings.TrimSpace(req.Header.UrunAdi)
req.Header.MaliyetTarihi = strings.TrimSpace(req.Header.MaliyetTarihi)
req.Header.SAciklama = strings.TrimSpace(req.Header.SAciklama)
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
}
// Resolve exchange rates (for price conversions).
usdRate := 0.0
eurRate := 0.0
gbpRate := 0.0
if mssqlDB != nil {
row, err := queries.GetProductionHasCostDetailExchangeRatesByDate(ctx, mssqlDB, req.Header.MaliyetTarihi)
if err != nil {
logger.Error("exchange rate query error", "err", err)
http.Error(w, "Kur bilgisi alinamadi", http.StatusInternalServerError)
return
}
var rateDate string
if err := row.Scan(&rateDate, &usdRate, &eurRate, &gbpRate); err != nil {
logger.Error("exchange rate scan error", "err", err)
http.Error(w, "Kur bilgisi alinamadi", http.StatusInternalServerError)
return
}
}
if usdRate <= 0 {
usdRate = 1
}
if eurRate <= 0 {
eurRate = 1
}
if gbpRate <= 0 {
gbpRate = 1
}
// Resolve firma id
firmaID := req.Header.NFirmaID
if firmaID <= 0 && req.Header.FirmaKodu != "" {
id, err := queries.LookupFirmaIDByKodu(ctx, uretimDB, req.Header.FirmaKodu)
if err != nil {
logger.Error("firma lookup error", "err", err)
http.Error(w, "Firma bulunamadi", http.StatusBadRequest)
return
}
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
}
// Resolve or generate nOnMLNo
nOnMLNo := req.Header.NOnMLNo
if nOnMLNo <= 0 {
next, err := queries.GetNextOnMLNoFrom100k(ctx, uretimDB)
if err != nil {
logger.Error("next onmlno error", "err", err)
http.Error(w, "Yeni nOnMLNo uretilemedi", http.StatusInternalServerError)
return
}
nOnMLNo = next
}
// Resolve / create nMamulTuruNo from product groups (UrunAna + UrunAlt)
nMamulTuruNo := 1
// Compute totals from incoming rows (TRY/USD/EUR)
totalTRY := 0.0
totalUSD := 0.0
totalEUR := 0.0
for _, r := range req.Detail.Upserts {
qty := r.LMiktar
if qty < 0 {
qty = 0
}
// Convert input price to TRY (unit)
cur := strings.ToUpper(strings.TrimSpace(r.FiyatDoviz))
in := r.FiyatGirilen
unitTRY := in
switch cur {
case "USD":
unitTRY = in * usdRate
case "EUR":
unitTRY = in * eurRate
case "GBP":
unitTRY = in * gbpRate
case "TRY", "TL", "":
unitTRY = in
default:
unitTRY = in
}
unitUSD := unitTRY / usdRate
unitEUR := unitTRY / eurRate
totalTRY += unitTRY * qty
totalUSD += unitUSD * qty
totalEUR += unitEUR * qty
}
// 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
}
tx, err := uretimDB.BeginTx(ctx, nil)
if err != nil {
logger.Error("tx begin error", "err", err)
http.Error(w, "Islem baslatilamadi", http.StatusInternalServerError)
return
}
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 := ""
if mssqlDB != nil {
_, ana, alt, err := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssqlDB, req.Header.UrunKodu)
if err == nil {
ana = strings.TrimSpace(ana)
alt = strings.TrimSpace(alt)
if alt == "" || alt == "-" {
mamulLabel = ana
} else {
// matches inserted convention: "ANA-ALT" (no spaces)
mamulLabel = strings.TrimSpace(ana + "-" + alt)
}
}
}
if strings.TrimSpace(mamulLabel) != "" && mamulLabel != "-" {
mt, err := queries.GetOnMLMamulTuruNoByAciklama(ctx, tx, mamulLabel)
if err != nil {
logger.Error("mamul turu resolve error", "err", err)
http.Error(w, "Mamul turu olusturulamadi", http.StatusInternalServerError)
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
}
nMamulTuruNo = mt
} else {
nMamulTuruNo = 1
}
var receteID sql.NullInt64
if req.Header.NUrtReceteID > 0 {
receteID = sql.NullInt64{Int64: int64(req.Header.NUrtReceteID), Valid: true}
}
var uretimSekliID sql.NullInt64
if req.Header.UretimSekliID > 0 {
uretimSekliID = sql.NullInt64{Int64: int64(req.Header.UretimSekliID), Valid: true}
}
var sAciklama sql.NullString
if req.Header.SAciklama != "" {
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,
UrunAdi: req.Header.UrunAdi,
Tarihi: tarihi,
NMamulTuruNo: nMamulTuruNo,
NUrtReceteID: receteID,
UretimSekliID: uretimSekliID,
SAciklama: sAciklama,
NFirmaID: firmaID,
SUser: user,
LTutarTL: totalTRY,
LTutarUSD: totalUSD,
LTutarEURO: totalEUR,
SDovizCinsi: "USD",
LTutarDoviz: totalUSD,
}); err != nil {
logger.Error("header upsert error", "err", err)
http.Error(w, "Header kaydedilemedi", http.StatusInternalServerError)
return
}
// 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
}
if _, err := tx.ExecContext(ctx, `DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo=@p1 AND nOnMLDetNo=@p2`, nOnMLNo, d.NOnMLDetNo); err != nil {
logger.Error("detail delete error", "err", err)
http.Error(w, "Detay silinemedi", http.StatusInternalServerError)
return
}
}
// 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) == "" {
// 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 {
qty = 0
}
cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz))
in := row.FiyatGirilen
unitTRY := in
switch cur {
case "USD":
unitTRY = in * usdRate
case "EUR":
unitTRY = in * eurRate
case "GBP":
unitTRY = in * gbpRate
case "TRY", "TL", "":
unitTRY = in
default:
unitTRY = in
}
unitUSD := unitTRY / usdRate
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
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(&nStokTipiID)
if err != nil {
if err == sql.ErrNoRows {
// 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.Info("stock type resolved", "s_kodu", rawSKodu, "n_stok_tipi_id", nStokTipiID)
if nStokTipiID <= 0 {
// 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:
const bOtoFiyat = 0
const lFireOrani = 0
const nTutarGirisTipiNo = 1
const sFiyatTipi = "ML"
const nKurTipiNo = 6
const lEkMasraf = 0
const lMiktar2 = 0
const lEkFiyat = 0
// MERGE by (nOnMLNo, nOnMLDetNo)
mergeSQL := `
MERGE dbo.spUrtOnMLMasDet AS T
USING (SELECT @p1 AS nOnMLNo, @p2 AS nOnMLDetNo) AS S
ON (T.nOnMLNo = S.nOnMLNo AND T.nOnMLDetNo = S.nOnMLDetNo)
WHEN MATCHED THEN
UPDATE SET
nHammaddeTuruNo = @p3,
sKodu = @p4,
sAciklama = @p5,
sRenk = @p6,
sBeden = NULLIF(@p7,''),
sAciklama2 = NULLIF(@p8,''),
lMiktar = @p9,
lFiyat = @p10,
lTutar = @p11,
bOtoFiyat = @p12,
lFireOrani = @p13,
nTutarGirisTipiNo = @p14,
sFiyatTipi = @p15,
sDovizCinsi = @p16,
nKurTipiNo = @p17,
lDovizKuru = @p18,
lDovizFiyati = @p19,
sBirim = @p20,
lDovizTutari = @p21,
lEkMasraf = @p22,
lMiktar2 = @p23,
nStokTipiID = @p24,
lEkFiyat = @p25,
nUrtMTBolumID = @p26,
fiyat_girilen = NULLIF(@p27, 0),
fiyat_doviz = NULLIF(@p28,''),
Maliyete_dahil = @p29,
cm_price_type_id = @p30,
sKullaniciAdiDeg = @p31,
dteIslemTarihiDeg = GETDATE(),
sAciklama3 = NULLIF(@p32, '')
WHEN NOT MATCHED THEN
INSERT (
nOnMLNo,nOnMLDetNo,nHammaddeTuruNo,sKodu,sAciklama,sRenk,lMiktar,lFiyat,lTutar,
bOtoFiyat,lFireOrani,nTutarGirisTipiNo,sFiyatTipi,sDovizCinsi,nKurTipiNo,lDovizKuru,lDovizFiyati,
sBirim,sAciklama2,lDovizTutari,lEkMasraf,sKullaniciAdi,dteIslemTarihi,sBeden,sAciklama3,lMiktar2,nStokTipiID,
lEkFiyat,nUrtMTBolumID,fiyat_girilen,fiyat_doviz,Maliyete_dahil,cm_price_type_id
)
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,''),NULLIF(@p32, ''),@p23,@p24,
@p25,@p26,NULLIF(@p27,0),NULLIF(@p28,''),@p29,@p30
);
`
if _, err := tx.ExecContext(
ctx,
mergeSQL,
nOnMLNo,
row.NOnMLDetNo,
row.NHammaddeTuruNo,
strings.TrimSpace(row.SKodu),
strings.TrimSpace(row.SAciklama),
strings.TrimSpace(row.SRenk),
strings.TrimSpace(row.SBeden),
strings.TrimSpace(row.SAciklama2),
qty,
unitTRY,
lTutar,
bOtoFiyat,
lFireOrani,
nTutarGirisTipiNo,
sFiyatTipi,
"USD",
nKurTipiNo,
usdRate,
unitUSD,
strings.TrimSpace(row.SBirim),
lDovizTutari,
lEkMasraf,
lMiktar2,
nStokTipiID,
lEkFiyat,
row.NUrtMTBolumID,
row.FiyatGirilen,
strings.TrimSpace(row.FiyatDoviz),
row.MaliyeteDahil,
row.CMPriceTypeID,
user,
strings.TrimSpace(row.SAciklama3), // p32: sAciklama3 (Grup Adi)
); err != nil {
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
}
}
// ============================================================
// 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)
}
}
}
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)
return
}
}
_ = 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 the base price row we created for this costing date (PriceDate = maliyetTarihi).
// We intentionally do NOT delete older base prices for the same item.
deletedBasePrice := false
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 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)
}
} 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)
}
}
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")
mssqlDB := db.GetDB()
if mssqlDB == nil {
http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.exchange-rates")
rawCostDate := strings.TrimSpace(r.URL.Query().Get("maliyet_tarihi"))
costDate := ""
if rawCostDate != "" {
parsedDate, err := time.Parse("2006-01-02", rawCostDate)
if err != nil {
logger.Warn("request invalid", "reason", "invalid maliyet_tarihi", "maliyet_tarihi", rawCostDate)
http.Error(w, "maliyet_tarihi YYYY-MM-DD formatinda olmali", http.StatusBadRequest)
return
}
costDate = parsedDate.Format("2006-01-02")
}
logger.Info("request start", "maliyet_tarihi", costDate)
log.Printf("[ProductionHasCostDetailExchangeRates] start maliyet_tarihi=%s", costDate)
row, err := queries.GetProductionHasCostDetailExchangeRatesByDate(ctx, mssqlDB, costDate)
if err != nil {
logger.Error("query prepare error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailExchangeRates] query prepare error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
item := models.ProductionHasCostDetailExchangeRates{
TRYRate: 1,
}
if err := row.Scan(
&item.RateDate,
&item.USDRate,
&item.EURRate,
&item.GBPRate,
); err != nil {
if err == sql.ErrNoRows {
item.RateDate = costDate
logger.Info("request done", "rate_date", item.RateDate, "fallback", true)
_ = json.NewEncoder(w).Encode(item)
return
}
log.Printf("⚠️ [ProductionHasCostDetailExchangeRates] scan error: %v", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "rate_date", item.RateDate, "usd_rate", item.USDRate, "eur_rate", item.EURRate, "gbp_rate", item.GBPRate)
log.Printf("[ProductionHasCostDetailExchangeRates] done maliyet_tarihi=%s rate_date=%s usd=%.4f eur=%.4f", costDate, item.RateDate, item.USDRate, item.EURRate)
_ = json.NewEncoder(w).Encode(item)
}
// POST /api/pricing/production-product-costing/has-cost-detail-bulk-prices
func PostProductionHasCostDetailBulkPricesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
mssqlDB := db.GetDB()
if mssqlDB == nil {
http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.bulk-prices")
var req models.ProductionHasCostDetailBulkPriceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logger.Warn("request invalid", "reason", "invalid request body", "err", err)
http.Error(w, "Gecersiz istek govdesi", http.StatusBadRequest)
return
}
costDate := strings.TrimSpace(req.MaliyetTarihi)
itemsCount := len(req.Items)
responseChan := make(chan *models.ProductionHasCostDetailBulkPriceRow, itemsCount)
logger.Info("request start",
"n_onml_no", strings.TrimSpace(req.NOnMLNo),
"urun_kodu", strings.TrimSpace(req.UrunKodu),
"maliyet_tarihi", costDate,
"item_count", itemsCount,
)
log.Printf("[ProductionHasCostDetailBulkPrices] start n_onml_no=%s urun_kodu=%s maliyet_tarihi=%s item_count=%d", strings.TrimSpace(req.NOnMLNo), strings.TrimSpace(req.UrunKodu), costDate, itemsCount)
for _, item := range req.Items {
go func(item models.ProductionHasCostDetailPriceLookupItem) {
sKodu := normalizeLookupValue(item.SKodu)
if sKodu == "" {
responseChan <- nil
return
}
colorCode := firstNonEmptyString(
normalizeLookupValue(item.ColorCode),
normalizeLookupValue(item.SRenk),
)
itemDim1Code := firstNonEmptyString(
normalizeLookupValue(item.ItemDim1Code),
)
row, err := queries.GetProductionHasCostLatestPurchasePriceForItem(
ctx,
mssqlDB,
sKodu,
colorCode,
itemDim1Code,
costDate,
)
if err != nil {
logger.Warn("item lookup error", "s_kodu", sKodu, "color_code", colorCode, "item_dim1_code", itemDim1Code, "err", err)
responseChan <- nil
return
}
var result models.ProductionHasCostDetailBulkPriceRow
if err := row.Scan(
&result.PriceType,
&result.Tarih,
&result.FaturaKodu,
&result.MasrafKodu,
&result.MasrafDetay,
&result.ColorCode,
&result.ColorDescription,
&result.ItemDim1Code,
&result.ItemDim1Description,
&result.FiyatGirilen,
&result.FiyatDoviz,
); err != nil {
logger.Warn("item scan error", "s_kodu", sKodu, "color_code", colorCode, "item_dim1_code", itemDim1Code, "err", err)
responseChan <- nil
return
}
result.RowKey = strings.TrimSpace(item.RowKey)
result.NOnMLDetNo = strings.TrimSpace(item.NOnMLDetNo)
result.NHammaddeTuruNo = strings.TrimSpace(item.NHammaddeTuruNo)
result.SKodu = sKodu
if strings.TrimSpace(result.ColorCode) == "" {
result.ColorCode = colorCode
}
if strings.TrimSpace(result.ItemDim1Code) == "" {
result.ItemDim1Code = itemDim1Code
}
responseChan <- &result
}(item)
}
response := make([]models.ProductionHasCostDetailBulkPriceRow, 0, itemsCount)
for i := 0; i < itemsCount; i++ {
res := <-responseChan
if res != nil {
response = append(response, *res)
}
}
logger.Info("request done", "item_count", itemsCount, "matched_count", len(response))
log.Printf("[ProductionHasCostDetailBulkPrices] done item_count=%d matched=%d", itemsCount, len(response))
_ = json.NewEncoder(w).Encode(map[string]any{
"items": response,
})
}
// GET /api/pricing/production-product-costing/has-cost-detail-line-history
func GetProductionHasCostDetailLineHistoryHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
mssqlDB := db.GetDB()
if uretimDB == nil && mssqlDB == nil {
http.Error(w, "Veritabani baglantilari aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.line-history")
rawOnMLNo := strings.TrimSpace(r.URL.Query().Get("n_onml_no"))
currentOnMLNo, _ := strconv.Atoi(rawOnMLNo)
nHammaddeTuruNo := strings.TrimSpace(r.URL.Query().Get("n_hammadde_turu_no"))
sKodu := normalizeLookupValue(r.URL.Query().Get("s_kodu"))
colorCode := firstNonEmptyString(
normalizeLookupValue(r.URL.Query().Get("color_code")),
normalizeLookupValue(r.URL.Query().Get("s_renk")),
)
costDate := strings.TrimSpace(r.URL.Query().Get("maliyet_tarihi"))
if sKodu == "" {
logger.Warn("request invalid", "reason", "missing s_kodu")
http.Error(w, "s_kodu zorunlu", http.StatusBadRequest)
return
}
logger.Info("request start", "n_onml_no", currentOnMLNo, "n_hammadde_turu_no", nHammaddeTuruNo, "s_kodu", sKodu, "color_code", colorCode, "maliyet_tarihi", costDate)
log.Printf("[ProductionHasCostDetailLineHistory] start n_onml_no=%d n_hammadde_turu_no=%s s_kodu=%s color=%s maliyet_tarihi=%s", currentOnMLNo, nHammaddeTuruNo, sKodu, colorCode, costDate)
const LINE_HISTORY_ROW_LIMIT = 500
response := models.ProductionHasCostDetailLineHistoryResponse{
PurchaseRows: make([]models.ProductionHasCostDetailPurchaseHistoryRow, 0, LINE_HISTORY_ROW_LIMIT),
RecipeRows: make([]models.ProductionHasCostDetailRecipeHistoryRow, 0, LINE_HISTORY_ROW_LIMIT),
}
allowLegacyAutoFallback := true
if mssqlDB != nil {
rows, err := queries.GetProductionHasCostPurchaseHistoryByExpenseCode(
ctx,
mssqlDB,
sKodu,
costDate,
LINE_HISTORY_ROW_LIMIT,
)
if err != nil {
logger.Warn("purchase query error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase query error: %v", err)
} else {
defer rows.Close()
for rows.Next() {
var item models.ProductionHasCostDetailPurchaseHistoryRow
if err := rows.Scan(
&item.SourceType,
&item.PriceType,
&item.Tarih,
&item.FaturaKodu,
&item.FirmaKodu,
&item.FirmaAciklama,
&item.MasrafKodu,
&item.MasrafDetay,
&item.ColorCode,
&item.ColorDescription,
&item.ItemDim1Code,
&item.ItemDim1Description,
&item.Miktar,
&item.BIRIM,
&item.EvrakFiyat,
&item.EvrakTutar,
&item.EvrakDoviz,
); err != nil {
logger.Warn("purchase scan error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase scan error: %v", err)
continue
}
response.PurchaseRows = append(response.PurchaseRows, item)
}
if err := rows.Err(); err != nil {
logger.Warn("purchase rows error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase rows error: %v", err)
}
}
}
if allowLegacyAutoFallback && mssqlDB != nil && len(response.PurchaseRows) == 0 {
similarPrefix := queries.BuildProductionHasCostSimilarCodePrefix(sKodu)
if similarPrefix != "" {
logger.Info("purchase fallback start", "s_kodu", sKodu, "similar_prefix", similarPrefix, "maliyet_tarihi", costDate)
rows, err := queries.GetProductionHasCostPurchaseHistoryByCodePrefix(
ctx,
mssqlDB,
similarPrefix,
costDate,
LINE_HISTORY_ROW_LIMIT,
)
if err != nil {
logger.Warn("purchase fallback query error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase fallback query error: %v", err)
} else {
defer rows.Close()
for rows.Next() {
var item models.ProductionHasCostDetailPurchaseHistoryRow
if err := rows.Scan(
&item.SourceType,
&item.PriceType,
&item.Tarih,
&item.FaturaKodu,
&item.FirmaKodu,
&item.FirmaAciklama,
&item.MasrafKodu,
&item.MasrafDetay,
&item.ColorCode,
&item.ColorDescription,
&item.ItemDim1Code,
&item.ItemDim1Description,
&item.Miktar,
&item.BIRIM,
&item.EvrakFiyat,
&item.EvrakTutar,
&item.EvrakDoviz,
); err != nil {
logger.Warn("purchase fallback scan error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase fallback scan error: %v", err)
continue
}
response.PurchaseRows = append(response.PurchaseRows, item)
}
if err := rows.Err(); err != nil {
logger.Warn("purchase fallback rows error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase fallback rows error: %v", err)
}
}
}
}
if uretimDB != nil {
rows, err := queries.GetProductionHasCostRecipeHistoryByExpenseCode(
ctx,
uretimDB,
currentOnMLNo,
sKodu,
colorCode,
costDate,
LINE_HISTORY_ROW_LIMIT,
)
if err != nil {
logger.Warn("recipe query error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe query error: %v", err)
} else {
defer rows.Close()
for rows.Next() {
var item models.ProductionHasCostDetailRecipeHistoryRow
if err := rows.Scan(
&item.SourceType,
&item.PriceType,
&item.DteIslemTarihi,
&item.NOnMLNo,
&item.FirmaKodu,
&item.FirmaAciklama,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.LMiktar,
&item.SBirim,
&item.LDovizFiyati,
&item.LDovizTutari,
&item.USD,
&item.DUMMY,
); err != nil {
logger.Warn("recipe scan error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe scan error: %v", err)
continue
}
response.RecipeRows = append(response.RecipeRows, item)
}
if err := rows.Err(); err != nil {
logger.Warn("recipe rows error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe rows error: %v", err)
}
}
}
if allowLegacyAutoFallback && uretimDB != nil && len(response.RecipeRows) == 0 {
similarPrefix := queries.BuildProductionHasCostSimilarCodePrefix(sKodu)
if similarPrefix != "" {
logger.Info("recipe fallback prefix start", "s_kodu", sKodu, "similar_prefix", similarPrefix, "maliyet_tarihi", costDate)
rows, err := queries.GetProductionHasCostOnMLHistoryByCodePrefix(
ctx,
uretimDB,
similarPrefix,
costDate,
LINE_HISTORY_ROW_LIMIT,
)
if err != nil {
logger.Warn("recipe fallback prefix query error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback prefix query error: %v", err)
} else {
defer rows.Close()
for rows.Next() {
var item models.ProductionHasCostDetailRecipeHistoryRow
if err := rows.Scan(
&item.SourceType,
&item.PriceType,
&item.DteIslemTarihi,
&item.NOnMLNo,
&item.FirmaKodu,
&item.FirmaAciklama,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.LMiktar,
&item.SBirim,
&item.LDovizFiyati,
&item.LDovizTutari,
&item.USD,
&item.DUMMY,
); err != nil {
logger.Warn("recipe fallback prefix scan error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback prefix scan error: %v", err)
continue
}
response.RecipeRows = append(response.RecipeRows, item)
}
if err := rows.Err(); err != nil {
logger.Warn("recipe fallback prefix rows error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback prefix rows error: %v", err)
}
}
}
}
if allowLegacyAutoFallback && uretimDB != nil && len(response.RecipeRows) == 0 && nHammaddeTuruNo != "" {
logger.Info("recipe fallback hammadde-turu start", "n_hammadde_turu_no", nHammaddeTuruNo, "maliyet_tarihi", costDate)
rows, err := queries.GetProductionHasCostOnMLHistoryByHammaddeTuruNo(
ctx,
uretimDB,
nHammaddeTuruNo,
costDate,
LINE_HISTORY_ROW_LIMIT,
)
if err != nil {
logger.Warn("recipe fallback hammadde-turu query error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback hammadde-turu query error: %v", err)
} else {
defer rows.Close()
for rows.Next() {
var item models.ProductionHasCostDetailRecipeHistoryRow
if err := rows.Scan(
&item.SourceType,
&item.PriceType,
&item.DteIslemTarihi,
&item.NOnMLNo,
&item.FirmaKodu,
&item.FirmaAciklama,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.LMiktar,
&item.SBirim,
&item.LDovizFiyati,
&item.LDovizTutari,
&item.USD,
&item.DUMMY,
); err != nil {
logger.Warn("recipe fallback hammadde-turu scan error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback hammadde-turu scan error: %v", err)
continue
}
response.RecipeRows = append(response.RecipeRows, item)
}
if err := rows.Err(); err != nil {
logger.Warn("recipe fallback hammadde-turu rows error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback hammadde-turu rows error: %v", err)
}
}
}
logger.Info("request done", "s_kodu", sKodu, "purchase_count", len(response.PurchaseRows), "recipe_count", len(response.RecipeRows))
log.Printf("[ProductionHasCostDetailLineHistory] done s_kodu=%s purchase_rows=%d recipe_rows=%d", sKodu, len(response.PurchaseRows), len(response.RecipeRows))
_ = json.NewEncoder(w).Encode(response)
}
// GET /api/pricing/production-product-costing/has-cost-detail-similar-history
func GetProductionHasCostDetailSimilarHistoryHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
mssqlDB := db.GetDB()
uretimDB := db.GetUretimDB()
if mssqlDB == nil || uretimDB == nil {
http.Error(w, "Veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.similar-history")
q := r.URL.Query()
nHammaddeTuruNo := strings.TrimSpace(q.Get("n_hammadde_turu_no"))
sKodu := normalizeLookupValue(q.Get("s_kodu"))
costDate := strings.TrimSpace(q.Get("maliyet_tarihi"))
limit := parsePositiveIntOrDefault(q.Get("limit"), 500)
searchMode := strings.ToLower(strings.TrimSpace(q.Get("search_mode")))
if searchMode == "" || searchMode == "exact" {
searchMode = "prefix"
}
similarPrefix := queries.BuildProductionHasCostSimilarCodePrefix(sKodu)
if nHammaddeTuruNo == "" && sKodu == "" {
http.Error(w, "n_hammadde_turu_no veya s_kodu parametresi zorunludur", http.StatusBadRequest)
return
}
logger.Info("request start", "search_mode", searchMode, "n_hammadde_turu_no", nHammaddeTuruNo, "s_kodu", sKodu, "similar_prefix", similarPrefix, "maliyet_tarihi", costDate, "limit", limit)
response := models.ProductionHasCostDetailLineHistoryResponse{
PurchaseRows: make([]models.ProductionHasCostDetailPurchaseHistoryRow, 0, limit),
RecipeRows: make([]models.ProductionHasCostDetailRecipeHistoryRow, 0, limit),
}
purchaseMatchStage := searchMode
recipeMatchStage := searchMode
allowRecipeAutoFallback := false
if searchMode == "alternative" {
purchaseMatchStage = "skipped"
if nHammaddeTuruNo != "" && uretimDB != nil {
rows, err := queries.GetProductionHasCostOnMLHistoryByHammaddeTuruNo(ctx, uretimDB, nHammaddeTuruNo, costDate, limit)
if err != nil {
logger.Warn("alternative onml query error", "err", err)
http.Error(w, "Sorgu calistirilirken hata olustu", http.StatusInternalServerError)
return
}
defer rows.Close()
for rows.Next() {
var item models.ProductionHasCostDetailRecipeHistoryRow
if err := rows.Scan(
&item.SourceType,
&item.PriceType,
&item.DteIslemTarihi,
&item.NOnMLNo,
&item.FirmaKodu,
&item.FirmaAciklama,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.LMiktar,
&item.SBirim,
&item.LDovizFiyati,
&item.LDovizTutari,
&item.USD,
&item.DUMMY,
); err != nil {
logger.Warn("alternative onml scan error", "err", err)
continue
}
response.RecipeRows = append(response.RecipeRows, item)
}
if err := rows.Err(); err != nil {
logger.Warn("alternative onml rows error", "err", err)
}
}
if len(response.PurchaseRows) == 0 {
purchaseMatchStage = "empty"
}
if len(response.RecipeRows) == 0 {
recipeMatchStage = "empty"
}
logger.Info("request done",
"search_mode", searchMode,
"purchase_match_stage", purchaseMatchStage,
"recipe_match_stage", recipeMatchStage,
"similar_prefix", similarPrefix,
"purchase_count", len(response.PurchaseRows),
"recipe_count", len(response.RecipeRows),
)
_ = json.NewEncoder(w).Encode(map[string]any{
"purchaseRows": response.PurchaseRows,
"recipeRows": response.RecipeRows,
"purchase_match_stage": purchaseMatchStage,
"recipe_match_stage": recipeMatchStage,
"similar_code_prefix": similarPrefix,
"search_mode": searchMode,
})
return
}
if similarPrefix != "" {
if mssqlDB != nil {
rows, err := queries.GetProductionHasCostPurchaseHistoryByCodePrefix(ctx, mssqlDB, similarPrefix, costDate, limit)
if err != nil {
logger.Warn("prefix purchase query error", "err", err)
} else {
defer rows.Close()
for rows.Next() {
var item models.ProductionHasCostDetailPurchaseHistoryRow
if err := rows.Scan(
&item.SourceType,
&item.PriceType,
&item.Tarih,
&item.FaturaKodu,
&item.FirmaKodu,
&item.FirmaAciklama,
&item.MasrafKodu,
&item.MasrafDetay,
&item.ColorCode,
&item.ColorDescription,
&item.ItemDim1Code,
&item.ItemDim1Description,
&item.Miktar,
&item.BIRIM,
&item.EvrakFiyat,
&item.EvrakTutar,
&item.EvrakDoviz,
); err != nil {
logger.Warn("prefix purchase scan error", "err", err)
continue
}
response.PurchaseRows = append(response.PurchaseRows, item)
}
if err := rows.Err(); err != nil {
logger.Warn("prefix purchase rows error", "err", err)
}
}
}
if uretimDB != nil {
rows, err := queries.GetProductionHasCostOnMLHistoryByCodePrefix(ctx, uretimDB, similarPrefix, costDate, limit)
if err != nil {
logger.Warn("prefix onml query error", "err", err)
} else {
defer rows.Close()
for rows.Next() {
var item models.ProductionHasCostDetailRecipeHistoryRow
if err := rows.Scan(
&item.SourceType,
&item.PriceType,
&item.DteIslemTarihi,
&item.NOnMLNo,
&item.FirmaKodu,
&item.FirmaAciklama,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.LMiktar,
&item.SBirim,
&item.LDovizFiyati,
&item.LDovizTutari,
&item.USD,
&item.DUMMY,
); err != nil {
logger.Warn("prefix onml scan error", "err", err)
continue
}
response.RecipeRows = append(response.RecipeRows, item)
}
if err := rows.Err(); err != nil {
logger.Warn("prefix onml rows error", "err", err)
}
}
}
}
if allowRecipeAutoFallback && len(response.RecipeRows) == 0 && nHammaddeTuruNo != "" && uretimDB != nil {
recipeMatchStage = "hammadde-turu-fallback"
rows, err := queries.GetProductionHasCostOnMLHistoryByHammaddeTuruNo(ctx, uretimDB, nHammaddeTuruNo, costDate, limit)
if err != nil {
logger.Warn("fallback onml query error", "err", err)
http.Error(w, "Sorgu calistirilirken hata olustu", http.StatusInternalServerError)
return
}
defer rows.Close()
for rows.Next() {
var item models.ProductionHasCostDetailRecipeHistoryRow
if err := rows.Scan(
&item.SourceType,
&item.PriceType,
&item.DteIslemTarihi,
&item.NOnMLNo,
&item.FirmaKodu,
&item.FirmaAciklama,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.LMiktar,
&item.SBirim,
&item.LDovizFiyati,
&item.LDovizTutari,
&item.USD,
&item.DUMMY,
); err != nil {
logger.Warn("fallback onml scan error", "err", err)
continue
}
response.RecipeRows = append(response.RecipeRows, item)
}
if err := rows.Err(); err != nil {
logger.Warn("fallback onml rows error", "err", err)
}
}
if len(response.PurchaseRows) == 0 {
purchaseMatchStage = "empty"
}
if len(response.RecipeRows) == 0 {
recipeMatchStage = "empty"
}
logger.Info("request done",
"search_mode", searchMode,
"purchase_match_stage", purchaseMatchStage,
"recipe_match_stage", recipeMatchStage,
"similar_prefix", similarPrefix,
"purchase_count", len(response.PurchaseRows),
"recipe_count", len(response.RecipeRows),
)
_ = json.NewEncoder(w).Encode(map[string]any{
"purchaseRows": response.PurchaseRows,
"recipeRows": response.RecipeRows,
"purchase_match_stage": purchaseMatchStage,
"recipe_match_stage": recipeMatchStage,
"similar_code_prefix": similarPrefix,
"search_mode": searchMode,
})
}
func parsePositiveIntOrDefault(raw string, fallback int) int {
v, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil || v < 0 {
return fallback
}
return v
}
func normalizeLookupValue(raw string) string {
return strings.ToUpper(strings.TrimSpace(raw))
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" {
return value
}
}
return ""
}
// ============================================================
// MT BOLUM MAPPING (URETIM DB)
// ============================================================
// GET /api/pricing/production-product-costing/options/urun-ana-grup
func GetProductionProductCostingUrunAnaGrupOptionsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
mssqlDB := db.GetDB()
if mssqlDB == nil {
http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
search := strings.TrimSpace(r.URL.Query().Get("search"))
limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 50)
if limit > 500 {
limit = 500
}
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.options.urun-ana-grup",
"search", search,
"limit", limit,
)
logger.Info("request start")
rows, err := queries.GetProductionProductCostingAnaGrupOptions(ctx, mssqlDB, search, limit)
if err != nil {
logger.Error("query error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
out := make([]models.ProductionProductCostingLookupOption, 0, limit)
for rows.Next() {
var v string
if err := rows.Scan(&v); err != nil {
logger.Warn("scan error", "err", err)
continue
}
v = strings.TrimSpace(v)
if v == "" {
continue
}
out = append(out, models.ProductionProductCostingLookupOption{Value: v, Label: v})
}
if err := rows.Err(); err != nil {
logger.Error("rows error", "err", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "row_count", len(out))
_ = json.NewEncoder(w).Encode(out)
}
// GET /api/pricing/production-product-costing/options/urun-alt-grup
func GetProductionProductCostingUrunAltGrupOptionsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
mssqlDB := db.GetDB()
if mssqlDB == nil {
http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
urunAnaGrubu := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
search := strings.TrimSpace(r.URL.Query().Get("search"))
limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 50)
if limit > 500 {
limit = 500
}
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.options.urun-alt-grup",
"urun_ana_grubu", urunAnaGrubu,
"search", search,
"limit", limit,
)
logger.Info("request start")
rows, err := queries.GetProductionProductCostingAltGrupOptions(ctx, mssqlDB, urunAnaGrubu, search, limit)
if err != nil {
logger.Error("query error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
out := make([]models.ProductionProductCostingLookupOption, 0, limit)
for rows.Next() {
var v string
if err := rows.Scan(&v); err != nil {
logger.Warn("scan error", "err", err)
continue
}
v = strings.TrimSpace(v)
if v == "" {
continue
}
out = append(out, models.ProductionProductCostingLookupOption{Value: v, Label: v})
}
if err := rows.Err(); err != nil {
logger.Error("rows error", "err", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "row_count", len(out))
_ = json.NewEncoder(w).Encode(out)
}
// GET /api/pricing/production-product-costing/options/urun-ana-alt-combos
func GetProductionProductCostingUrunAnaAltCombosHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
mssqlDB := db.GetDB()
if mssqlDB == nil {
http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
search := strings.TrimSpace(r.URL.Query().Get("search"))
limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 2000)
if limit > 5000 {
limit = 5000
}
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.options.urun-ana-alt-combos",
"search", search,
"limit", limit,
)
logger.Info("request start")
rows, err := queries.GetProductionProductCostingAnaAltComboRows(ctx, mssqlDB, search, limit)
if err != nil {
// Keep the response generic, but log the underlying SQL driver error for diagnostics.
logger.Error("query error", "err", err, "search", search, "limit", limit, "trace_id", traceID)
log.Printf("❌ [ProductionProductCostingAnaAltCombos] query error trace_id=%s search=%q limit=%d err=%v", traceID, search, limit, err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
out := make([]models.ProductionProductCostingAnaAltComboRow, 0, 1024)
for rows.Next() {
var item models.ProductionProductCostingAnaAltComboRow
if err := rows.Scan(&item.UrunIlkGrubu, &item.UrunAnaGrubu, &item.UrunAltGrubu); err != nil {
logger.Warn("scan error", "err", err)
continue
}
item.UrunIlkGrubu = strings.TrimSpace(item.UrunIlkGrubu)
item.UrunAnaGrubu = strings.TrimSpace(item.UrunAnaGrubu)
item.UrunAltGrubu = strings.TrimSpace(item.UrunAltGrubu)
if item.UrunIlkGrubu == "" || item.UrunAnaGrubu == "" || item.UrunAltGrubu == "" {
continue
}
out = append(out, item)
}
if err := rows.Err(); err != nil {
logger.Error("rows error", "err", err, "trace_id", traceID)
log.Printf("⚠️ [ProductionProductCostingAnaAltCombos] rows error trace_id=%s err=%v", traceID, err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "row_count", len(out))
_ = json.NewEncoder(w).Encode(out)
}
// GET /api/pricing/production-product-costing/options/mtbolum
func GetProductionProductCostingMTBolumOptionsHandler(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)
search := strings.TrimSpace(r.URL.Query().Get("search"))
limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 50)
if limit > 500 {
limit = 500
}
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.options.mtbolum",
"search", search,
"limit", limit,
)
logger.Info("request start")
rows, err := queries.GetProductionProductCostingMTBolumOptions(ctx, uretimDB, search, limit)
if err != nil {
logger.Error("query error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
out := make([]models.ProductionProductCostingLookupOption, 0, limit)
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
logger.Warn("scan error", "err", err)
continue
}
label := strings.TrimSpace(strconv.Itoa(id) + " - " + strings.TrimSpace(name))
out = append(out, models.ProductionProductCostingLookupOption{
Value: strconv.Itoa(id),
Label: label,
})
}
if err := rows.Err(); err != nil {
logger.Error("rows error", "err", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "row_count", len(out))
_ = json.NewEncoder(w).Encode(out)
}
// GET /api/pricing/production-product-costing/maliyet-parca-eslestirme
func GetProductionProductCostingParcaMappingsHandler(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)
urunIlkGrubu := strings.TrimSpace(r.URL.Query().Get("urun_ilk_grubu"))
urunAnaGrubu := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
urunAltGrubu := strings.TrimSpace(r.URL.Query().Get("urun_alt_grubu"))
nUrtMTBolumID := parsePositiveIntOrDefault(r.URL.Query().Get("n_urt_mt_bolum_id"), 0)
rawOnlyActive := strings.TrimSpace(r.URL.Query().Get("only_active"))
var onlyActive *bool = nil
if rawOnlyActive != "" {
v := rawOnlyActive == "1" || strings.EqualFold(rawOnlyActive, "true")
onlyActive = &v
}
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.maliyet-parca-eslestirme.list",
"urun_ilk_grubu", urunIlkGrubu,
"urun_ana_grubu", urunAnaGrubu,
"urun_alt_grubu", urunAltGrubu,
"n_urt_mt_bolum_id", nUrtMTBolumID,
"only_active", rawOnlyActive,
)
logger.Info("request start")
rows, err := queries.ListProductionProductCostingParcaMappings(ctx, uretimDB, urunIlkGrubu, urunAnaGrubu, urunAltGrubu, nUrtMTBolumID, onlyActive)
if err != nil {
logger.Error("query error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
out := make([]models.ProductionProductCostingParcaMappingRow, 0, 200)
for rows.Next() {
var row models.ProductionProductCostingParcaMappingRow
var bAktif sql.NullBool
var hammaddeCsv sql.NullString
if err := rows.Scan(
&row.ID,
&row.UrunIlkGrubu,
&row.UrunAnaGrubu,
&row.UrunAltGrubu,
&row.NUrtMTBolumID,
&row.ParcaBolumAdi,
&hammaddeCsv,
&bAktif,
&row.DteIslem,
&row.SKullaniciAdi,
); err != nil {
logger.Warn("scan error", "err", err)
continue
}
row.BAktif = bAktif.Valid && bAktif.Bool
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)
}
}
}
out = append(out, row)
}
if err := rows.Err(); err != nil {
logger.Error("rows error", "err", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "row_count", len(out))
_ = json.NewEncoder(w).Encode(out)
}
// POST /api/pricing/production-product-costing/maliyet-parca-eslestirme/upsert
func PostProductionProductCostingParcaMappingUpsertHandler(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
}
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.maliyet-parca-eslestirme.upsert")
logger.Info("request start")
var req models.ProductionProductCostingParcaMappingUpsertRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logger.Warn("invalid json", "err", err)
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
req.UrunAnaGrubu = strings.TrimSpace(req.UrunAnaGrubu)
req.UrunAltGrubu = strings.TrimSpace(req.UrunAltGrubu)
req.UrunIlkGrubu = strings.TrimSpace(req.UrunIlkGrubu)
if req.UrunIlkGrubu == "" || req.UrunAnaGrubu == "" || req.UrunAltGrubu == "" || req.NUrtMTBolumID <= 0 {
http.Error(w, "urunIlkGrubu, urunAnaGrubu, urunAltGrubu ve nUrtMTBolumID zorunlu", http.StatusBadRequest)
return
}
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)
return
}
logger.Info("request done", "id", id, "user", user, "bAktif", req.BAktif)
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 {
ID int `json:"id"`
BAktif bool `json:"bAktif"`
}
// POST /api/pricing/production-product-costing/maliyet-parca-eslestirme/set-active
func PostProductionProductCostingParcaMappingSetActiveHandler(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
}
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.maliyet-parca-eslestirme.set-active")
logger.Info("request start")
var req productionProductCostingSetActiveRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logger.Warn("invalid json", "err", err)
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
if req.ID <= 0 {
http.Error(w, "id zorunlu", http.StatusBadRequest)
return
}
if err := queries.SetProductionProductCostingParcaMappingActive(ctx, uretimDB, req.ID, req.BAktif, user); err != nil {
logger.Error("exec error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "id", req.ID, "bAktif", req.BAktif, "user", user)
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}
// DELETE /api/pricing/production-product-costing/maliyet-parca-eslestirme?id=123
func DeleteProductionProductCostingParcaMappingHandler(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)
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.maliyet-parca-eslestirme.delete")
logger.Info("request start")
id := parsePositiveIntOrDefault(r.URL.Query().Get("id"), 0)
if id <= 0 {
http.Error(w, "id zorunlu", http.StatusBadRequest)
return
}
if err := queries.DeleteProductionProductCostingParcaMapping(ctx, uretimDB, id); err != nil {
logger.Error("exec error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "id", id)
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}