Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-15 19:43:44 +03:00
parent dacd3aefa9
commit c9a2004248
16 changed files with 1500 additions and 601 deletions

View File

@@ -796,11 +796,26 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionHasCostDetailBulkPricesHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/has-cost-detail/last-detail", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingHasCostDetailLastDetailHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/options/hammadde-by-nos", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOptionsHammaddeByNosHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/onml/save", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLSaveHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/onml/delete", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLDeleteHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/default-quantities", "GET",
"order", "view",

View File

@@ -149,6 +149,7 @@ type ProductionProductCostingOnMLSaveDetailUpsertRow struct {
FiyatDoviz string `json:"fiyat_doviz"`
MaliyeteDahil int `json:"maliyete_dahil"`
CMPriceTypeID *int `json:"cm_price_type_id"`
SAciklama3 string `json:"s_aciklama3"`
}
type ProductionProductCostingOnMLSaveDetailDeleteRow struct {
@@ -170,6 +171,10 @@ type ProductionProductCostingOnMLSaveResponse struct {
NOnMLNo int `json:"n_onml_no"`
}
type ProductionProductCostingOnMLDeleteRequest struct {
NOnMLNo int `json:"n_onml_no"`
}
// ============================================================
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
// ============================================================
@@ -209,9 +214,39 @@ type ProductionProductCostingDefaultQtyLookupRequest struct {
type ProductionProductCostingDefaultQtyLookupItem struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
SAciklama string `json:"sAciklama"`
LDefaultMiktar float64 `json:"lDefaultMiktar"`
}
type ProductionProductCostingLastOnMLDetLookupRequest struct {
NHammaddeTuruNos []int `json:"nHammaddeTuruNos"`
BeforeDate string `json:"before_date"` // YYYY-MM-DD (optional)
ExcludeOnMLNo int `json:"exclude_onml_no"` // optional
NFirmaID int `json:"n_firma_id"` // optional
OnlyICode bool `json:"only_i_code"` // optional: restrict to sKodu like 'I.%'
}
type ProductionProductCostingLastOnMLDetLookupItem struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
SKodu string `json:"sKodu"`
SAciklama string `json:"sAciklama"`
SBirim string `json:"sBirim"`
FiyatDoviz string `json:"fiyat_doviz"`
FiyatGirilen float64 `json:"fiyat_girilen"`
IsSameFirma bool `json:"is_same_firma"`
}
type ProductionProductCostingHammaddeByNosRequest struct {
NHammaddeTuruNos []int `json:"nHammaddeTuruNos"`
}
type ProductionProductCostingHammaddeByNosItem struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
SAciklama string `json:"sAciklama"`
MTUrtMTBolumID int `json:"mtUrtMTBolumID"`
SParcaAdi string `json:"sParcaAdi"`
}
type ProductionHasCostDetailExchangeRates struct {
RateDate string `json:"rateDate"`
TRYRate float64 `json:"tryRate"`

View File

@@ -1,6 +1,7 @@
package queries
import (
"bssapp-backend/models"
"bssapp-backend/utils"
"context"
"database/sql"
@@ -227,17 +228,47 @@ func UpsertV3ItemBasePriceUSD(
return fmt.Errorf("missing params for base price upsert")
}
// NOTE: In this DB, PRIMARY KEY is on:
// (ItemTypeCode, ItemCode, CountryCode, SeasonCode, BasePriceCode)
// so we cannot insert multiple rows for different dates under the same base price.
// We update the single row's PriceDate/Price to reflect latest costing.
// 1. Find a CountryCode that is NOT yet used for this specific item in prItemBasePrice.
// We query cdCountry for a code that has no matching entry in prItemBasePrice for this item.
// We exclude 'TR' to keep the original record safe.
var targetCountry string
err := mssqlDB.QueryRowContext(ctx, `
SELECT TOP 1 C.CountryCode
FROM dbo.cdCountry C WITH (NOLOCK)
WHERE C.CountryCode <> 'TR'
AND NOT EXISTS (
SELECT 1 FROM dbo.prItemBasePrice P WITH (NOLOCK)
WHERE P.ItemTypeCode = 1
AND LTRIM(RTRIM(P.ItemCode)) = LTRIM(RTRIM(@p1))
AND P.BasePriceCode = 1
AND P.CountryCode = C.CountryCode
)
ORDER BY NEWID() -- Randomly pick one of the available country codes
`, itemCode).Scan(&targetCountry)
if err != nil {
if err == sql.ErrNoRows {
// Fallback: If ALL countries are exhausted (unlikely), default to 'AD' as a last resort.
targetCountry = "AD"
} else {
return fmt.Errorf("failed to find available country code: %w", err)
}
}
appUser := user
if strings.TrimSpace(appUser) == "" {
appUser = "BSSAPP"
} else {
appUser = "BSSAPP:" + strings.TrimSpace(appUser)
}
sqlText := `
MERGE dbo.prItemBasePrice AS T
USING (
SELECT
@p1 AS ItemTypeCode,
@p2 AS ItemCode,
'TR' AS CountryCode,
@p6 AS CountryCode,
'' AS SeasonCode,
1 AS BasePriceCode,
CONVERT(date, @p3, 23) AS PriceDate,
@@ -266,7 +297,7 @@ WHEN NOT MATCHED THEN
);
`
_, err := mssqlDB.ExecContext(ctx, sqlText, 1, itemCode, priceDate, priceUSD, user)
_, err = mssqlDB.ExecContext(ctx, sqlText, 1, itemCode, priceDate, priceUSD, appUser, targetCountry)
return err
}
@@ -1228,6 +1259,73 @@ ORDER BY
return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike)
}
func GetProductionHammaddeByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]models.ProductionProductCostingHammaddeByNosItem, error) {
clean := make([]int, 0, len(nos))
seen := make(map[int]struct{}, len(nos))
for _, n := range nos {
if n <= 0 {
continue
}
if _, ok := seen[n]; ok {
continue
}
seen[n] = struct{}{}
clean = append(clean, n)
}
if len(clean) == 0 {
return []models.ProductionProductCostingHammaddeByNosItem{}, nil
}
if len(clean) > 5000 {
return nil, fmt.Errorf("too many nos")
}
valueRows := make([]string, 0, len(clean))
args := make([]any, 0, len(clean))
for i, n := range clean {
valueRows = append(valueRows, "(@p"+strconv.Itoa(i+1)+")")
args = append(args, n)
}
sqlText := `
WITH req(nHammaddeTuruNo) AS (
SELECT DISTINCT v.nHammaddeTuruNo
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
)
SELECT
ISNULL(T.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
ISNULL(T.sAciklama, '') AS sAciklama,
ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
ISNULL(B.sAdi, '') AS sParcaAdi
FROM dbo.spUrtOnMLHammaddeTuru T WITH (NOLOCK)
INNER JOIN req R
ON R.nHammaddeTuruNo = T.nHammaddeTuruNo
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE ISNULL(T.bAktif, 0) = 1
ORDER BY T.nHammaddeTuruNo;
`
rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]models.ProductionProductCostingHammaddeByNosItem, 0, len(clean))
for rows.Next() {
var it models.ProductionProductCostingHammaddeByNosItem
if err := rows.Scan(&it.NHammaddeTuruNo, &it.SAciklama, &it.MTUrtMTBolumID, &it.SParcaAdi); err != nil {
return nil, err
}
out = append(out, it)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
// ============================================================
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
// ============================================================
@@ -1248,7 +1346,9 @@ SELECT TOP (@p3)
ISNULL(H.sAciklama, '') AS sAciklama,
ISNULL(V.lDefaultMiktar, 0) AS lDefaultMiktar,
CONVERT(VARCHAR(16), V.dteCalcTarihi, 120) AS dteCalcTarihi,
CAST(CASE WHEN ISNULL(V.bAktif, 0) = 1 THEN 1 ELSE 0 END AS bit) AS bAktif
-- We intentionally don't depend on mk_MaliyetParcaEslestirme_vmiktarlar.bAktif
-- to keep this feature robust across schema changes; only active hammadde types are listed.
CAST(1 AS bit) AS bAktif
FROM dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK)
LEFT JOIN dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
@@ -1264,26 +1364,16 @@ ORDER BY
func UpsertProductionProductCostingDefaultQtyRow(ctx context.Context, uretimDB *sql.DB, nHammaddeTuruNo int, lDefaultMiktar float64, bAktif *bool) error {
// NOTE: Legacy helper kept for backward-compat but now behaves as UPDATE-only.
activeVal := -1
if bAktif != nil {
if *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
dteCalcTarihi = GETDATE()
WHERE nHammaddeTuruNo = @p1;
SELECT @@ROWCOUNT;
`
var affected int
if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, lDefaultMiktar, activeVal).Scan(&affected); err != nil {
if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, lDefaultMiktar).Scan(&affected); err != nil {
return err
}
if affected == 0 {
@@ -1333,11 +1423,10 @@ USING agg AS S
WHEN MATCHED THEN
UPDATE SET
T.lDefaultMiktar = S.lDefaultMiktar,
T.dteCalcTarihi = GETDATE(),
T.bAktif = 1
T.dteCalcTarihi = GETDATE()
WHEN NOT MATCHED THEN
INSERT (nHammaddeTuruNo, lDefaultMiktar, dteCalcTarihi, bAktif)
VALUES (S.nHammaddeTuruNo, S.lDefaultMiktar, GETDATE(), 1);
INSERT (nHammaddeTuruNo, lDefaultMiktar, dteCalcTarihi)
VALUES (S.nHammaddeTuruNo, S.lDefaultMiktar, GETDATE());
`
_, err := uretimDB.ExecContext(ctx, sqlText, topN)
return err
@@ -1377,7 +1466,11 @@ FROM ranked;
return avg, cnt, nil
}
func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB *sql.DB, nos []int) (map[int]float64, error) {
func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]struct {
No int
Aciklama string
Qty float64
}, error) {
clean := make([]int, 0, len(nos))
seen := make(map[int]struct{}, len(nos))
for _, n := range nos {
@@ -1391,7 +1484,11 @@ func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB
clean = append(clean, n)
}
if len(clean) == 0 {
return map[int]float64{}, nil
return []struct {
No int
Aciklama string
Qty float64
}{}, nil
}
if len(clean) > 1000 {
return nil, fmt.Errorf("too many hammadde nos")
@@ -1409,9 +1506,10 @@ WITH req(nHammaddeTuruNo) AS (
SELECT DISTINCT v.nHammaddeTuruNo
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
)
SELECT
ISNULL(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
CAST(ISNULL(V.lDefaultMiktar, 0) AS FLOAT) AS lDefaultMiktar
SELECT
ISNULL(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
ISNULL(H.sAciklama, '') AS sAciklama,
CAST(ISNULL(V.lDefaultMiktar, 0) AS FLOAT) AS lDefaultMiktar
FROM req R
INNER JOIN dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK)
ON V.nHammaddeTuruNo = R.nHammaddeTuruNo
@@ -1426,15 +1524,24 @@ WHERE ISNULL(H.bAktif, 0) = 1;
}
defer rows.Close()
out := make(map[int]float64, len(clean))
out := make([]struct {
No int
Aciklama string
Qty float64
}, 0, len(clean))
for rows.Next() {
var no int
var aciklama sql.NullString
var qty float64
if err := rows.Scan(&no, &qty); err != nil {
if err := rows.Scan(&no, &aciklama, &qty); err != nil {
return nil, err
}
if no > 0 && qty > 0 {
out[no] = qty
out = append(out, struct {
No int
Aciklama string
Qty float64
}{No: no, Aciklama: strings.TrimSpace(aciklama.String), Qty: qty})
}
}
if err := rows.Err(); err != nil {
@@ -1443,6 +1550,128 @@ WHERE ISNULL(H.bAktif, 0) = 1;
return out, nil
}
func LookupLastOnMLMasDetByHammaddeNos(
ctx context.Context,
uretimDB *sql.DB,
nos []int,
beforeDate string, // YYYY-MM-DD optional
excludeOnMLNo int,
nFirmaID int,
onlyICode bool,
limitPerType int,
) ([]models.ProductionProductCostingLastOnMLDetLookupItem, error) {
if limitPerType <= 0 {
limitPerType = 1
}
if limitPerType > 1 {
limitPerType = 1
}
clean := make([]int, 0, len(nos))
seen := make(map[int]struct{}, len(nos))
for _, n := range nos {
if n <= 0 {
continue
}
if _, ok := seen[n]; ok {
continue
}
seen[n] = struct{}{}
clean = append(clean, n)
}
if len(clean) == 0 {
return []models.ProductionProductCostingLastOnMLDetLookupItem{}, nil
}
if len(clean) > 500 {
return nil, fmt.Errorf("too many hammadde nos")
}
valueRows := make([]string, 0, len(clean))
args := make([]any, 0, len(clean)+2)
for i, n := range clean {
valueRows = append(valueRows, "(@p"+strconv.Itoa(i+1)+")")
args = append(args, n)
}
// extra params at the end
args = append(args, strings.TrimSpace(beforeDate))
args = append(args, excludeOnMLNo)
args = append(args, nFirmaID)
if onlyICode {
args = append(args, 1)
} else {
args = append(args, 0)
}
beforeParam := "@p" + strconv.Itoa(len(clean)+1)
excludeParam := "@p" + strconv.Itoa(len(clean)+2)
firmaParam := "@p" + strconv.Itoa(len(clean)+3)
onlyIParam := "@p" + strconv.Itoa(len(clean)+4)
sqlText := `
WITH req(nHammaddeTuruNo) AS (
SELECT DISTINCT v.nHammaddeTuruNo
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
),
ranked AS (
SELECT
D.nHammaddeTuruNo,
LTRIM(RTRIM(ISNULL(D.sKodu, ''))) AS sKodu,
LTRIM(RTRIM(ISNULL(D.sAciklama, ''))) AS sAciklama,
LTRIM(RTRIM(ISNULL(D.sBirim, ''))) AS sBirim,
LTRIM(RTRIM(ISNULL(D.fiyat_doviz, ''))) AS fiyat_doviz,
CAST(ISNULL(D.fiyat_girilen, 0) AS FLOAT) AS fiyat_girilen,
CAST(CASE WHEN ISNULL(` + firmaParam + `, 0) > 0 AND ISNULL(M.nFirmaID, 0) = ISNULL(` + firmaParam + `, 0) THEN 1 ELSE 0 END AS bit) AS is_same_firma,
ROW_NUMBER() OVER (
PARTITION BY D.nHammaddeTuruNo
ORDER BY
CASE WHEN ISNULL(` + firmaParam + `, 0) > 0 AND ISNULL(M.nFirmaID, 0) = ISNULL(` + firmaParam + `, 0) THEN 0 ELSE 1 END,
COALESCE(M.Tarihi, M.dteKayitTarihi) DESC,
M.nOnMLNo DESC,
D.nOnMLDetNo DESC
) AS rn
FROM dbo.spUrtOnMLMasDet D WITH (NOLOCK)
INNER JOIN dbo.spUrtOnMLMas M WITH (NOLOCK)
ON M.nOnMLNo = D.nOnMLNo
INNER JOIN req R
ON R.nHammaddeTuruNo = D.nHammaddeTuruNo
WHERE ISNULL(D.fiyat_girilen, 0) > 0
AND LTRIM(RTRIM(ISNULL(D.sKodu, ''))) <> ''
AND (ISNULL(` + onlyIParam + `, 0) = 0 OR LTRIM(RTRIM(ISNULL(D.sKodu, ''))) LIKE 'I.%')
AND (` + beforeParam + ` = '' OR CONVERT(date, COALESCE(M.Tarihi, M.dteKayitTarihi)) < CONVERT(date, ` + beforeParam + `, 23))
AND (ISNULL(` + excludeParam + `, 0) <= 0 OR M.nOnMLNo <> ` + excludeParam + `)
)
SELECT
ISNULL(nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
ISNULL(sKodu, '') AS sKodu,
ISNULL(sAciklama, '') AS sAciklama,
ISNULL(sBirim, '') AS sBirim,
ISNULL(fiyat_doviz, '') AS fiyat_doviz,
ISNULL(fiyat_girilen, 0) AS fiyat_girilen,
CAST(ISNULL(is_same_firma, 0) AS bit) AS is_same_firma
FROM ranked
WHERE rn = 1;
`
rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]models.ProductionProductCostingLastOnMLDetLookupItem, 0, len(clean))
for rows.Next() {
var item models.ProductionProductCostingLastOnMLDetLookupItem
if err := rows.Scan(&item.NHammaddeTuruNo, &item.SKodu, &item.SAciklama, &item.SBirim, &item.FiyatDoviz, &item.FiyatGirilen, &item.IsSameFirma); err != nil {
return nil, err
}
out = append(out, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func buildSQLServerFullTextPrefixQuery(search string) string {
terms := strings.Fields(strings.TrimSpace(search))
parts := make([]string, 0, len(terms))

View File

@@ -496,6 +496,13 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
return
}
// Keep mk_mail in sync for downstream mail mapping screens.
if err := ensureMkMail(tx, payload.Email); err != nil {
log.Printf("USER CREATE ensureMkMail ERROR user_id=%d email=%q err=%v", newID, payload.Email, err)
http.Error(w, "Mail kaydi guncellenemedi", http.StatusInternalServerError)
return
}
// ROLES
for _, role := range payload.Roles {
role = strings.TrimSpace(role)

View File

@@ -0,0 +1,68 @@
package routes
import (
"bssapp-backend/utils"
"database/sql"
"errors"
"regexp"
"strings"
)
var simpleEmailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
// ensureMkMail makes sure the given email exists in mk_mail.
// If it already exists (case-insensitive match), it is re-activated and normalized.
// This is intentionally tolerant (no ON CONFLICT) to avoid relying on a specific unique constraint.
func ensureMkMail(tx *sql.Tx, email string) error {
mail := strings.ToLower(strings.TrimSpace(email))
if mail == "" {
return nil
}
if !simpleEmailRe.MatchString(mail) {
// user email field can be free-form; don't hard fail user save because of mk_mail bookkeeping
return nil
}
var id string
err := tx.QueryRow(`
SELECT m.id::text
FROM mk_mail m
WHERE LOWER(TRIM(m.email)) = LOWER(TRIM($1))
LIMIT 1
`, mail).Scan(&id)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
if errors.Is(err, sql.ErrNoRows) {
newID := utils.NewUUID()
_, err = tx.Exec(`
INSERT INTO mk_mail (
id,
email,
display_name,
"type",
is_primary,
external_id,
is_active,
created_at
)
VALUES ($1, $2, '', 'user', true, true, true, NOW())
`, newID, mail)
return err
}
// Exists: normalize + activate. Avoid touching created_at.
_, err = tx.Exec(`
UPDATE mk_mail
SET
email = $2,
display_name = COALESCE(display_name, ''),
"type" = 'user',
is_primary = true,
external_id = true,
is_active = true
WHERE id::text = $1
`, id, mail)
return err
}

View File

@@ -327,6 +327,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
groupName string
groupTotal float64
groupTotalUSD float64
nOnMLNoStr string
nOnMLDetNoStr string
hNoStr string
fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool
@@ -338,9 +341,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
&groupName,
&groupTotal,
&groupTotalUSD,
&item.NOnMLNo,
&item.NOnMLDetNo,
&item.NHammaddeTuruNo,
&nOnMLNoStr,
&nOnMLDetNoStr,
&hNoStr,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
@@ -371,6 +374,10 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
}
scannedRows += 1
item.NOnMLNo, _ = strconv.Atoi(nOnMLNoStr)
item.NOnMLDetNo, _ = strconv.Atoi(nOnMLDetNoStr)
item.NHammaddeTuruNo, _ = strconv.Atoi(hNoStr)
if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64)
*item.FiyatGirilen = fiyatGirilen.Float64
@@ -426,8 +433,8 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo)
if err != nil {
logger.Error("query error", "err", err)
log.Printf("❌ [ProductionHasCostDetailGroups] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
log.Printf(" [ProductionHasCostDetailGroups] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
@@ -442,6 +449,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
groupName string
groupTotal float64
groupTotalUSD float64
nOnMLNoStr string
nOnMLDetNoStr string
hNoStr string
fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool
@@ -453,9 +463,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
&groupName,
&groupTotal,
&groupTotalUSD,
&item.NOnMLNo,
&item.NOnMLDetNo,
&item.NHammaddeTuruNo,
&nOnMLNoStr,
&nOnMLDetNoStr,
&hNoStr,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
@@ -479,10 +489,16 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
&item.SHammaddeTuruAdi,
&item.SParcaAdi,
); err != nil {
log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err)
scanErrors++
logger.Warn("scan error", "scan_index", scannedRows+scanErrors, "err", err)
log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err)
continue
}
scannedRows += 1
scannedRows++
item.NOnMLNo, _ = strconv.Atoi(nOnMLNoStr)
item.NOnMLDetNo, _ = strconv.Atoi(nOnMLDetNoStr)
item.NHammaddeTuruNo, _ = strconv.Atoi(hNoStr)
if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64)
@@ -1093,21 +1109,74 @@ func PostProductionProductCostingDefaultQuantitiesLookupHandler(w http.ResponseW
return
}
m, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos)
rows, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(m))
for no, qty := range m {
out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(rows))
for _, r0 := range rows {
out = append(out, models.ProductionProductCostingDefaultQtyLookupItem{
NHammaddeTuruNo: no,
LDefaultMiktar: qty,
NHammaddeTuruNo: r0.No,
SAciklama: r0.Aciklama,
LDefaultMiktar: r0.Qty,
})
}
_ = json.NewEncoder(w).Encode(out)
}
// POST /api/pricing/production-product-costing/has-cost-detail/last-detail
func PostProductionProductCostingHasCostDetailLastDetailHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
var req models.ProductionProductCostingLastOnMLDetLookupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
rows, err := queries.LookupLastOnMLMasDetByHammaddeNos(ctx, uretimDB, req.NHammaddeTuruNos, req.BeforeDate, req.ExcludeOnMLNo, req.NFirmaID, req.OnlyICode, 1)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
// POST /api/pricing/production-product-costing/options/hammadde-by-nos
func PostProductionProductCostingOptionsHammaddeByNosHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
var req models.ProductionProductCostingHammaddeByNosRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
rows, err := queries.GetProductionHammaddeByNos(ctx, uretimDB, req.NHammaddeTuruNos)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
// POST /api/pricing/production-product-costing/default-quantities/refresh
func PostProductionProductCostingDefaultQuantitiesRefreshHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
@@ -1166,6 +1235,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
req.Header.FirmaKodu = strings.TrimSpace(req.Header.FirmaKodu)
if req.Header.UrunKodu == "" || req.Header.MaliyetTarihi == "" {
logger.Warn("validation failed: missing required header fields",
"trace_id", traceID,
"urun_kodu", req.Header.UrunKodu,
"maliyet_tarihi", req.Header.MaliyetTarihi,
)
http.Error(w, "urun_kodu ve maliyet_tarihi zorunlu", http.StatusBadRequest)
return
}
@@ -1210,6 +1284,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
firmaID = id
}
if firmaID <= 0 {
logger.Warn("validation failed: missing firma",
"trace_id", traceID,
"n_firma_id", req.Header.NFirmaID,
"firma_kodu", req.Header.FirmaKodu,
)
http.Error(w, "Firma secilmeden kaydedilemez (n_firma_id / firma_kodu)", http.StatusBadRequest)
return
}
@@ -1264,6 +1343,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
// Parse Tarihi
tarihi, err := time.Parse("2006-01-02", req.Header.MaliyetTarihi)
if err != nil {
logger.Warn("validation failed: maliyet_tarihi parse error",
"trace_id", traceID,
"maliyet_tarihi", req.Header.MaliyetTarihi,
"err", err,
)
http.Error(w, "maliyet_tarihi YYYY-MM-DD formatinda olmali", http.StatusBadRequest)
return
}
@@ -1274,7 +1358,23 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
http.Error(w, "Islem baslatilamadi", http.StatusInternalServerError)
return
}
defer func() { _ = tx.Rollback() }()
committed := false
logger.Info("tx begin",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"urun_kodu", req.Header.UrunKodu,
"maliyet_tarihi", req.Header.MaliyetTarihi,
)
defer func() {
if committed {
return
}
if err := tx.Rollback(); err == nil {
logger.Info("tx rollback", "trace_id", traceID, "n_onml_no", nOnMLNo)
} else {
logger.Warn("tx rollback failed", "trace_id", traceID, "n_onml_no", nOnMLNo, "err", err)
}
}()
// Determine mamul turu inside same tx (to keep create atomic)
mamulLabel := ""
@@ -1299,6 +1399,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
return
}
if mt <= 0 {
logger.Warn("validation failed: mamul turu not found", "trace_id", traceID, "n_onml_no", nOnMLNo, "mamul_label", mamulLabel)
http.Error(w, "Mamul turu bulunamadi: "+mamulLabel, http.StatusBadRequest)
return
}
@@ -1320,6 +1421,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
sAciklama = sql.NullString{String: req.Header.SAciklama, Valid: true}
}
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "upsert_header")
if err := queries.UpsertOnMLHeader(tx, ctx, queries.OnMLHeaderUpsertArgs{
NOnMLNo: nOnMLNo,
UrunKodu: req.Header.UrunKodu,
@@ -1343,6 +1445,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
}
// Deletes
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_deletes", "count", len(req.Detail.Deletes))
for _, d := range req.Detail.Deletes {
if d.NOnMLDetNo <= 0 {
continue
@@ -1355,13 +1458,33 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
}
// Upserts
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_upserts", "count", len(req.Detail.Upserts))
for _, row := range req.Detail.Upserts {
if row.NOnMLDetNo <= 0 {
continue
}
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
http.Error(w, "Detay satirinda n_hammadde_turu_no ve s_kodu zorunlu", http.StatusBadRequest)
return
// FALLBACK: If nHammaddeTuruNo is missing but sKodu is present, default to 1 (General/Labor)
// to avoid blocking the user, especially for labor items.
if strings.TrimSpace(row.SKodu) != "" && row.NHammaddeTuruNo <= 0 {
logger.Warn("n_hammadde_turu_no <= 0, falling back to 1",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"s_kodu", strings.TrimSpace(row.SKodu),
)
row.NHammaddeTuruNo = 1
} else {
logger.Warn("validation failed: missing required detail fields (s_kodu empty)",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"n_hammadde_turu_no", row.NHammaddeTuruNo,
"s_kodu", strings.TrimSpace(row.SKodu),
)
http.Error(w, "Detay satirinda s_kodu zorunlu", http.StatusBadRequest)
return
}
}
qty := row.LMiktar
if qty < 0 {
@@ -1387,9 +1510,21 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
lTutar := unitTRY * qty
lDovizTutari := unitUSD * qty
// Debug log for price resolution
logger.Info("price debug",
"s_kodu", strings.TrimSpace(row.SKodu),
"qty", qty,
"fiyat_girilen", row.FiyatGirilen,
"fiyat_doviz", strings.TrimSpace(row.FiyatDoviz),
"unitTRY", unitTRY,
"lTutar", lTutar,
"lDovizTutari", lDovizTutari,
)
// Resolve stock type id from tbStok by sKodu (exact), then fallback to model-based match.
// Note: In this DB, stock type is stored as tbStok.nStokTipi but spUrtOnMLMasDet expects nStokTipiID (int).
rawSKodu := strings.TrimSpace(row.SKodu)
logger.Info("resolving stock type", "s_kodu", rawSKodu)
var nStokTipiID int
err := tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
@@ -1411,16 +1546,32 @@ ORDER BY
`, rawSKodu).Scan(&nStokTipiID)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
// FALLBACK: If stock item not found in tbStok at all, default to 1.
logger.Warn("stok tipi not found in tbStok, falling back to 1",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"s_kodu", rawSKodu,
)
nStokTipiID = 1
} else {
logger.Error("stok tipi lookup error", "err", err)
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
return
}
logger.Error("stok tipi lookup error", "err", err)
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
return
}
logger.Info("stock type resolved", "s_kodu", rawSKodu, "n_stok_tipi_id", nStokTipiID)
if nStokTipiID <= 0 {
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
// FALLBACK: If stock type is missing or 0 in tbStok, default to 1 (usually 'Raw Material' or 'General').
// This prevents blocking the save process for items not fully configured in tbStok.
logger.Warn("stok tipi <= 0, falling back to 1",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"s_kodu", rawSKodu,
"n_stok_tipi_id_orig", nStokTipiID,
)
nStokTipiID = 1
}
// Dummy/system-mapped required fields:
@@ -1469,7 +1620,8 @@ WHEN MATCHED THEN
Maliyete_dahil = @p29,
cm_price_type_id = @p30,
sKullaniciAdiDeg = @p31,
dteIslemTarihiDeg = GETDATE()
dteIslemTarihiDeg = GETDATE(),
sAciklama3 = NULLIF(@p32, '')
WHEN NOT MATCHED THEN
INSERT (
nOnMLNo,nOnMLDetNo,nHammaddeTuruNo,sKodu,sAciklama,sRenk,lMiktar,lFiyat,lTutar,
@@ -1480,7 +1632,7 @@ WHEN NOT MATCHED THEN
VALUES (
@p1,@p2,@p3,@p4,@p5,@p6,@p9,@p10,@p11,
@p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19,
@p20,NULLIF(@p8,''),@p21,@p22,@p31,GETDATE(),NULLIF(@p7,''),NULL,@p23,@p24,
@p20,NULLIF(@p8,''),@p21,@p22,@p31,GETDATE(),NULLIF(@p7,''),NULLIF(@p32, ''),@p23,@p24,
@p25,@p26,NULLIF(@p27,0),NULLIF(@p28,''),@p29,@p30
);
`
@@ -1519,8 +1671,21 @@ WHEN NOT MATCHED THEN
row.MaliyeteDahil,
row.CMPriceTypeID,
user,
strings.TrimSpace(row.SAciklama3), // p32: sAciklama3 (Grup Adi)
); err != nil {
logger.Error("detail merge error", "err", err)
logger.Error("detail merge error",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"n_hammadde_turu_no", row.NHammaddeTuruNo,
"n_urt_mt_bolum_id", row.NUrtMTBolumID,
"s_kodu", strings.TrimSpace(row.SKodu),
"fiyat_girilen", row.FiyatGirilen,
"fiyat_doviz", strings.TrimSpace(row.FiyatDoviz),
"l_miktar", qty,
"n_stok_tipi_id", nStokTipiID,
"err", err,
)
http.Error(w, "Detay kaydedilemedi", http.StatusInternalServerError)
return
}
@@ -1533,6 +1698,7 @@ WHEN NOT MATCHED THEN
// ============================================================
if req.Header.NUrtReceteID > 0 {
receteID := req.Header.NUrtReceteID
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "recipe_sync", "n_urt_recete_id", receteID)
// Determine next available recipe detail id (nUrtRecMBolumID)
nextRecDetID := 0
@@ -1551,94 +1717,72 @@ WHERE RMik.nUrtReceteID = @p1
continue
}
// CM1/CM2 rows are cost-only and must NOT be written back into recipe tables.
// Source of truth: spUrtOnMLHammaddeTuru.sAciklama3 (e.g. 'CM2').
var grp sql.NullString
if err := tx.QueryRowContext(ctx, `
SELECT TOP 1 LTRIM(RTRIM(ISNULL(H.sAciklama3, '')))
FROM dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
WHERE H.nHammaddeTuruNo = @p1
`, hNo).Scan(&grp); err == nil {
g := strings.ToUpper(strings.TrimSpace(grp.String))
if g == "CM1" || g == "CM2" {
continue
}
// 1. FILTER: CM1/CM2 (Labor/Service) rows must NOT be written back into recipe tables.
// We check the group label (sAciklama3) from the row itself.
g := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
if g == "CM1" || g == "CM2" {
logger.Info("recipe sync skip: labor item", "s_kodu", row.SKodu, "group", g)
continue
}
// Resolve nHStokID from tbStok using sKodu (exact), then sModel fallback.
// IMPORTANT: nHStokID must be resolved; otherwise we'd update/insert too broadly.
// In this version of URETIM DB, the table name is dbo.spUrtRecMBolumMik
// but it uses _G suffixes for quantity and amount columns.
// Also nHStokID_G stores the stock code string rather than just an ID.
rawSKodu := strings.TrimSpace(row.SKodu)
var nHStokID int
err := tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(S.nStokID, 0)
FROM dbo.tbStok S WITH (NOLOCK)
WHERE ISNULL(S.IsBlocked, 0) = 0
AND (
REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '')
OR LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1
OR @p1 LIKE LTRIM(RTRIM(ISNULL(S.sModel,''))) + '%'
)
ORDER BY
CASE
WHEN REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '') THEN 0
WHEN LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1 THEN 1
ELSE 2
END,
S.dteKayitTarihi DESC,
S.nStokID DESC
`, rawSKodu).Scan(&nHStokID)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Recete sync icin stok bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
}
logger.Error("recipe stok lookup error", "err", err)
http.Error(w, "Recete sync stok bulunamadi", http.StatusInternalServerError)
return
}
if nHStokID == 0 {
http.Error(w, "Recete sync icin stok bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
// Ensure a section entry (spUrtRecMBolum) exists for this hNo (Hammadde Turu)
// in the current recipe, otherwise detail rows (Mik) cannot be linked properly.
var sectionExists int
_ = tx.QueryRowContext(ctx, `
SELECT COUNT(1) FROM dbo.spUrtRecMBolum WITH (NOLOCK)
WHERE nUrtReceteID = @p1 AND nUrtMBolumID = @p2
`, receteID, hNo).Scan(&sectionExists)
if sectionExists <= 0 {
logger.Info("creating missing recipe section", "n_urt_recete_id", receteID, "n_urt_m_bolum_id", hNo)
_, _ = tx.ExecContext(ctx, `
INSERT INTO dbo.spUrtRecMBolum (nUrtReceteID, nUrtUBolumID, nUrtMBolumID, nUrtMTBolumID, sKullaniciAdi, dteIslemTarihi)
VALUES (@p1, 13, @p2, @p3, @p4, GETDATE())
`, receteID, hNo, row.NUrtMTBolumID, user)
}
// If we cannot resolve stock, we still try to insert by hammadde no only.
// Some recipes may omit nHStokID; but we prefer filling it when possible.
// Update quantity if row already exists for (recete, hammadde, stok)
// Update quantity and prices if row already exists for (recete, hammadde, stok_code)
// Using nHStokID_G (string code) for matching as per user screenshot.
var exists int
if err := tx.QueryRowContext(ctx, `
SELECT COUNT(1)
FROM dbo.spUrtRecMBolumMik RMik WITH (NOLOCK)
WHERE RMik.nUrtReceteID = @p1
AND RMik.nUrtMBolumID = @p2
AND RMik.nHStokID = @p3
`, receteID, hNo, nHStokID).Scan(&exists); err == nil && exists > 0 {
AND LTRIM(RTRIM(RMik.nHStokID_G)) = @p3
`, receteID, hNo, rawSKodu).Scan(&exists); err == nil && exists > 0 {
_, _ = tx.ExecContext(ctx, `
UPDATE dbo.spUrtRecMBolumMik
SET lHMiktar = @p4,
sKullaniciAdiDeg = @p5,
dteIslemTarihiDeg = GETDATE()
SET lHMiktar_G = @p4,
lHMaliyet_G = @p5,
sKullaniciAdiDeg = @p6,
dteIslemTarihiDeg = GETDATE(),
bIslem = @p7
WHERE nUrtReceteID = @p1
AND nUrtMBolumID = @p2
AND nHStokID = @p3
`, receteID, hNo, nHStokID, row.LMiktar, user)
AND LTRIM(RTRIM(nHStokID_G)) = @p3
`, receteID, hNo, rawSKodu, row.LMiktar, unitTRY, user, row.MaliyeteDahil)
continue
}
// Insert missing: best-effort with minimal columns.
// NOTE: This assumes nUrtRecMBolumID can be a sequential int and that other columns are nullable/have defaults.
// Insert missing: using _G columns and storing code in nHStokID_G.
_, insertErr := tx.ExecContext(ctx, `
INSERT INTO dbo.spUrtRecMBolumMik (
nUrtReceteID,
nUrtUBolumID,
nUrtRecMBolumID,
nStokID,
nHStokID,
lHMiktar,
lHFire,
nHStokID_G,
lHMiktar_G,
lHFire_G,
nMaliyetTipiID,
lHMaliyet,
lMiktar,
lHMaliyet_G,
lMiktar_G,
sIslemKodu,
nUrtMBolumID,
nUrtMTBolumID,
@@ -1646,56 +1790,52 @@ INSERT INTO dbo.spUrtRecMBolumMik (
bIslem,
nSure,
sAciklama,
dteDovizTarihi,
sDovizCinsi,
lDovizOran,
lDovizFiyat,
sKullaniciAdi,
dteIslemTarihi,
nMBolumSarfTipiNo
)
VALUES (
@p1,
0,
13,
@p2,
0,
@p3,
@p4,
@p3, -- nHStokID_G (Code)
@p4, -- lHMiktar_G
0,
6,
0,
1,
@p5, -- lHMaliyet_G
1, -- lMiktar_G
'',
@p5,
@p6,
1,
0,
0,
NULL,
NULL,
NULL,
NULL,
NULL,
@p7,
1,
@p8, -- bIslem
0,
NULL,
@p9,
GETDATE(),
1
)
`, receteID, nextRecDetID, nHStokID, row.LMiktar, hNo, row.NUrtMTBolumID, user)
`, receteID, nextRecDetID, rawSKodu, row.LMiktar, unitTRY, hNo, row.NUrtMTBolumID, row.MaliyeteDahil, user)
if insertErr == nil {
nextRecDetID += 1
}
}
}
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "commit")
if err := tx.Commit(); err != nil {
logger.Error("tx commit error", "err", err)
http.Error(w, "Kaydetme tamamlanamadi", http.StatusInternalServerError)
return
}
committed = true
logger.Info("tx commit ok", "trace_id", traceID, "n_onml_no", nOnMLNo)
// V3: update base price table so pricing screens reflect latest costing.
// Not transactional with URETIM DB; if this fails, URETIM save has already succeeded.
if mssqlDB != nil {
logger.Info("post-commit step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "v3_base_price_upsert")
if err := queries.UpsertV3ItemBasePriceUSD(ctx, mssqlDB, req.Header.UrunKodu, req.Header.MaliyetTarihi, totalUSD, user); err != nil {
logger.Error("v3 base price upsert error", "err", err)
http.Error(w, "URETIM kaydedildi ama V3 maliyet guncellenemedi", http.StatusInternalServerError)
@@ -1706,6 +1846,131 @@ VALUES (
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingOnMLSaveResponse{NOnMLNo: nOnMLNo})
}
// POST /api/pricing/production-product-costing/onml/delete
// Deletes costing records created in URETIM (OnML header + details) and, if created by this app, V3 base price row.
// IMPORTANT: Recipe tables are NOT touched.
func PostProductionProductCostingOnMLDeleteHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
mssqlDB := db.GetDB()
claims, _ := auth.GetClaimsFromContext(r.Context())
user := ""
if claims != nil {
user = strings.TrimSpace(claims.Username)
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.onml.delete")
var req models.ProductionProductCostingOnMLDeleteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
return
}
if req.NOnMLNo <= 0 {
http.Error(w, "n_onml_no zorunlu", http.StatusBadRequest)
return
}
// Load header info for safe deletes and redirect behavior.
var urunKodu string
var maliyetTarihi time.Time
err := uretimDB.QueryRowContext(ctx, `
SELECT TOP 1
LTRIM(RTRIM(ISNULL(UrunKodu,''))) AS UrunKodu,
COALESCE(Tarihi, dteKayitTarihi, GETDATE()) AS Tarihi
FROM dbo.spUrtOnMLMas WITH (NOLOCK)
WHERE nOnMLNo = @p1
`, req.NOnMLNo).Scan(&urunKodu, &maliyetTarihi)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Kayit bulunamadi", http.StatusNotFound)
return
}
logger.Error("header lookup error", "err", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
urunKodu = strings.TrimSpace(urunKodu)
tx, err := uretimDB.BeginTx(ctx, nil)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer func() { _ = tx.Rollback() }()
// Delete details first, then header.
if _, err := tx.ExecContext(ctx, `
DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo = @p1
`, req.NOnMLNo); err != nil {
logger.Error("delete detail error", "err", err)
http.Error(w, "Detay silinemedi", http.StatusInternalServerError)
return
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM dbo.spUrtOnMLMas WHERE nOnMLNo = @p1
`, req.NOnMLNo); err != nil {
logger.Error("delete header error", "err", err)
http.Error(w, "Header silinemedi", http.StatusInternalServerError)
return
}
if err := tx.Commit(); err != nil {
logger.Error("tx commit error", "err", err)
http.Error(w, "Silme tamamlanamadi", http.StatusInternalServerError)
return
}
// V3: Delete base price ONLY if row was created by this app (CreatedUserName starts with BSSAPP).
deletedBasePrice := false
if mssqlDB != nil && urunKodu != "" {
var createdBy sql.NullString
var lastBy sql.NullString
_ = mssqlDB.QueryRowContext(ctx, `
SELECT TOP 1
ISNULL(CreatedUserName,'') AS CreatedUserName,
ISNULL(LastUpdatedUserName,'') AS LastUpdatedUserName
FROM dbo.prItemBasePrice WITH (NOLOCK)
WHERE ItemTypeCode = 1
AND LTRIM(RTRIM(ItemCode)) = @p1
AND ISNULL(CountryCode,'') = 'TR'
AND ISNULL(SeasonCode,'') = ''
AND ISNULL(BasePriceCode,0) = 1
`, urunKodu).Scan(&createdBy, &lastBy)
created := strings.ToUpper(strings.TrimSpace(createdBy.String))
last := strings.ToUpper(strings.TrimSpace(lastBy.String))
if strings.HasPrefix(created, "BSSAPP") && strings.HasPrefix(last, "BSSAPP") {
if _, err := mssqlDB.ExecContext(ctx, `
DELETE FROM dbo.prItemBasePrice
WHERE ItemTypeCode = 1
AND LTRIM(RTRIM(ItemCode)) = @p1
AND ISNULL(CountryCode,'') = 'TR'
AND ISNULL(SeasonCode,'') = ''
AND ISNULL(BasePriceCode,0) = 1
`, urunKodu); err == nil {
deletedBasePrice = true
}
}
}
logger.Info("delete done", "n_onml_no", req.NOnMLNo, "urun_kodu", urunKodu, "deleted_base_price", deletedBasePrice, "user", user)
_ = json.NewEncoder(w).Encode(map[string]any{
"ok": true,
"n_onml_no": req.NOnMLNo,
"urun_kodu": urunKodu,
"deleted_baseprice": deletedBasePrice,
})
}
// GET /api/pricing/production-product-costing/has-cost-detail-exchange-rates
func GetProductionHasCostDetailExchangeRatesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")

View File

@@ -251,6 +251,13 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
return
}
// Keep mk_mail in sync for downstream mail mapping screens.
if err := ensureMkMail(tx, payload.Email); err != nil {
log.Printf("ERROR [UserDetail] ensureMkMail failed user_id=%d email=%q err=%v", userID, payload.Email, err)
http.Error(w, "Mail kaydi guncellenemedi", http.StatusInternalServerError)
return
}
if _, err := tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID); err != nil {
log.Printf("❌ [UserDetail] delete roles failed user_id=%d err=%v", userID, err)
http.Error(w, "Roller temizlenemedi", http.StatusInternalServerError)