Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-15 14:33:35 +03:00
parent 562d397480
commit dacd3aefa9
14 changed files with 2409 additions and 17 deletions

View File

@@ -796,6 +796,41 @@ 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/onml/save", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLSaveHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/default-quantities", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductionProductCostingDefaultQuantitiesHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/default-quantities/upsert", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingDefaultQuantitiesUpsertHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/default-quantities/update-bulk", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingDefaultQuantitiesBulkUpdateHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/default-quantities/calc-avg", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingDefaultQuantitiesCalcAvgHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/default-quantities/lookup", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingDefaultQuantitiesLookupHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/default-quantities/refresh", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingDefaultQuantitiesRefreshHandler)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/options/urun-ana-grup", "GET",
"order", "view",

View File

@@ -103,6 +103,8 @@ type ProductionHasCostDetailHeader struct {
UrunAltGrubu string `json:"UrunAltGrubu"`
UretimSekliID string `json:"UretimSekliID"`
UretimSekli string `json:"UretimSekli"`
FirmaKodu string `json:"FirmaKodu"`
NFirmaID int `json:"nFirmaID"`
DteKayitTarihi string `json:"dteKayitTarihi"`
SKullaniciAdi string `json:"sKullaniciAdi"`
LTutarTL float64 `json:"lTutarTL"`
@@ -116,6 +118,100 @@ type ProductionHasCostDetailHeader struct {
NUrtReceteID string `json:"nUrtReceteID"`
}
// ============================================================
// Save (INSERT/UPDATE/DELETE/UPSERT) spUrtOnMLMas + spUrtOnMLMasDet
// ============================================================
type ProductionProductCostingOnMLSaveHeader struct {
NOnMLNo int `json:"n_onml_no"`
UrunKodu string `json:"urun_kodu"`
UrunAdi string `json:"urun_adi"`
MaliyetTarihi string `json:"maliyet_tarihi"` // YYYY-MM-DD
NUrtReceteID int `json:"n_urt_recete_id"`
UretimSekliID int `json:"uretim_sekli_id"`
SAciklama string `json:"s_aciklama"`
FirmaKodu string `json:"firma_kodu"`
NFirmaID int `json:"n_firma_id"`
}
type ProductionProductCostingOnMLSaveDetailUpsertRow struct {
NOnMLDetNo int `json:"n_onml_det_no"`
NHammaddeTuruNo int `json:"n_hammadde_turu_no"`
NUrtMTBolumID int `json:"n_urt_mt_bolum_id"`
SKodu string `json:"s_kodu"`
SAciklama string `json:"s_aciklama"`
SRenk string `json:"s_renk"`
SBeden string `json:"s_beden"`
SAciklama2 string `json:"s_aciklama2"`
SBirim string `json:"s_birim"`
LMiktar float64 `json:"l_miktar"`
FiyatGirilen float64 `json:"fiyat_girilen"`
FiyatDoviz string `json:"fiyat_doviz"`
MaliyeteDahil int `json:"maliyete_dahil"`
CMPriceTypeID *int `json:"cm_price_type_id"`
}
type ProductionProductCostingOnMLSaveDetailDeleteRow struct {
NOnMLDetNo int `json:"n_onml_det_no"`
}
type ProductionProductCostingOnMLSaveDetail struct {
Upserts []ProductionProductCostingOnMLSaveDetailUpsertRow `json:"upserts"`
Deletes []ProductionProductCostingOnMLSaveDetailDeleteRow `json:"deletes"`
}
type ProductionProductCostingOnMLSaveRequest struct {
DetailSource string `json:"detail_source"`
Header ProductionProductCostingOnMLSaveHeader `json:"header"`
Detail ProductionProductCostingOnMLSaveDetail `json:"detail"`
}
type ProductionProductCostingOnMLSaveResponse struct {
NOnMLNo int `json:"n_onml_no"`
}
// ============================================================
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
// ============================================================
type ProductionProductCostingDefaultQtyRow struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
SAciklama string `json:"sAciklama"`
LDefaultMiktar float64 `json:"lDefaultMiktar"`
DteCalcTarihi string `json:"dteCalcTarihi"`
BAktif bool `json:"bAktif"`
}
type ProductionProductCostingDefaultQtyUpdateRequest struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
LDefaultMiktar float64 `json:"lDefaultMiktar"`
BAktif *bool `json:"bAktif"`
}
type ProductionProductCostingDefaultQtyBulkUpdateRequest struct {
Items []ProductionProductCostingDefaultQtyUpdateRequest `json:"items"`
}
type ProductionProductCostingDefaultQtyCalcRequest struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
TopN int `json:"topN"`
}
type ProductionProductCostingDefaultQtyCalcResponse struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
LDefaultMiktar float64 `json:"lDefaultMiktar"`
NSampleCount int `json:"nSampleCount"`
}
type ProductionProductCostingDefaultQtyLookupRequest struct {
NHammaddeTuruNos []int `json:"nHammaddeTuruNos"`
}
type ProductionProductCostingDefaultQtyLookupItem struct {
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
LDefaultMiktar float64 `json:"lDefaultMiktar"`
}
type ProductionHasCostDetailExchangeRates struct {
RateDate string `json:"rateDate"`
TRYRate float64 `json:"tryRate"`

View File

@@ -7,9 +7,269 @@ import (
"fmt"
"strconv"
"strings"
"time"
"unicode"
)
func GetNextOnMLNoFrom100k(ctx context.Context, uretimDB *sql.DB) (int, error) {
// nOnMLNo is NOT identity. Generate next number safely.
// Requirement: always continue from MAX, but never below 100001.
sqlText := `
SELECT
ISNULL(MAX(M.nOnMLNo), 100000) + 1 AS NextNo
FROM dbo.spUrtOnMLMas M WITH (UPDLOCK, HOLDLOCK)
`
var next int
if err := uretimDB.QueryRowContext(ctx, sqlText).Scan(&next); err != nil {
return 0, err
}
if next < 100001 {
next = 100001
}
return next, nil
}
func GetOnMLMamulTuruNoByAciklama(ctx context.Context, tx *sql.Tx, sAciklama string) (int, error) {
sAciklama = strings.TrimSpace(sAciklama)
if sAciklama == "" {
return 1, nil
}
var existing int
err := tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(nMamulTuruNo, 0)
FROM dbo.spUrtOnMLMamulTuru WITH (UPDLOCK, HOLDLOCK)
WHERE LTRIM(RTRIM(ISNULL(sAciklama,''))) = @p1
ORDER BY nMamulTuruNo
`, sAciklama).Scan(&existing)
if err == sql.ErrNoRows {
return 0, nil
}
if err != nil {
return 0, err
}
return existing, nil
}
func LookupFirmaIDByKodu(ctx context.Context, uretimDB *sql.DB, firmaKodu string) (int, error) {
firmaKodu = strings.TrimSpace(firmaKodu)
if firmaKodu == "" {
return 0, nil
}
sqlText := `
SELECT TOP 1 ISNULL(F.nFirmaID, 0) AS nFirmaID
FROM dbo.tbFirma F WITH (NOLOCK)
WHERE LTRIM(RTRIM(ISNULL(F.sKodu, ''))) = @p1
ORDER BY F.nFirmaID
`
var id int
if err := uretimDB.QueryRowContext(ctx, sqlText, firmaKodu).Scan(&id); err != nil {
if err == sql.ErrNoRows {
return 0, nil
}
return 0, err
}
return id, nil
}
type OnMLHeaderUpsertArgs struct {
NOnMLNo int
UrunKodu string
UrunAdi string
Tarihi time.Time
NMamulTuruNo int
NUrtReceteID sql.NullInt64
UretimSekliID sql.NullInt64
SAciklama sql.NullString
NFirmaID int
SUser string
LTutarTL float64
LTutarUSD float64
LTutarEURO float64
SDovizCinsi string
LTutarDoviz float64
}
func UpsertOnMLHeader(tx *sql.Tx, ctx context.Context, args OnMLHeaderUpsertArgs) error {
// Note: Many NOT NULL columns exist. We use dummy/system values as per business rules.
sqlText := `
IF EXISTS (SELECT 1 FROM dbo.spUrtOnMLMas WITH (UPDLOCK, HOLDLOCK) WHERE nOnMLNo = @p1)
BEGIN
UPDATE dbo.spUrtOnMLMas
SET
UrunKodu = @p2,
UrunAdi = @p3,
Tarihi = @p4,
nDonemNo = 1,
nMamulTuruNo = @p16,
nBolgeNo = 2,
nZorlukNo = 1,
bDurum = 1,
lTutarTL = @p5,
lTutarUSD = @p6,
lTutarEURO = @p7,
sDovizCinsi = @p8,
lTutarDoviz = @p9,
bSablon = 0,
dteGuncellemeTarihi = GETDATE(),
sGuncellemeKullaniciAdi = @p10,
nUrtReceteID = @p11,
sAciklama = @p12,
lMasMiktar = 0,
sRenk = NULLIF(LTRIM(RTRIM(@p13)), ''),
nFirmaID = @p14,
uretim_sekli_id = @p15
WHERE nOnMLNo = @p1
END
ELSE
BEGIN
INSERT INTO dbo.spUrtOnMLMas (
nOnMLNo,
UrunKodu,
UrunAdi,
Tarihi,
nDonemNo,
nMamulTuruNo,
nBolgeNo,
nZorlukNo,
dteKayitTarihi,
sKullaniciAdi,
bDurum,
lTutarTL,
lTutarUSD,
lTutarEURO,
sDovizCinsi,
lTutarDoviz,
bSablon,
dteGuncellemeTarihi,
sGuncellemeKullaniciAdi,
nUrtReceteID,
sAciklama,
lMasMiktar,
nFirmaID,
bVarsayilan,
bOnay,
bIptal,
bRParcaTakip,
uretim_sekli_id
)
VALUES (
@p1,
@p2,
@p3,
@p4,
1,
@p16,
2,
1,
GETDATE(),
@p10,
1,
@p5,
@p6,
@p7,
@p8,
@p9,
0,
GETDATE(),
@p10,
@p11,
@p12,
0,
@p14,
0,
0,
0,
0,
@p15
)
END
`
_, err := tx.ExecContext(
ctx,
sqlText,
args.NOnMLNo,
strings.TrimSpace(args.UrunKodu),
strings.TrimSpace(args.UrunAdi),
args.Tarihi,
args.LTutarTL,
args.LTutarUSD,
args.LTutarEURO,
strings.TrimSpace(args.SDovizCinsi),
args.LTutarDoviz,
strings.TrimSpace(args.SUser),
args.NUrtReceteID,
args.SAciklama,
"", // sRenk (header) currently not driven from UI
args.NFirmaID,
args.UretimSekliID,
args.NMamulTuruNo,
)
return err
}
// ============================================================
// V3 (Nebim) base price update
// ============================================================
func UpsertV3ItemBasePriceUSD(
ctx context.Context,
mssqlDB *sql.DB,
itemCode string,
priceDate string, // YYYY-MM-DD
priceUSD float64,
user string,
) error {
itemCode = strings.TrimSpace(itemCode)
priceDate = strings.TrimSpace(priceDate)
user = strings.TrimSpace(user)
if mssqlDB == nil || itemCode == "" || priceDate == "" {
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.
sqlText := `
MERGE dbo.prItemBasePrice AS T
USING (
SELECT
@p1 AS ItemTypeCode,
@p2 AS ItemCode,
'TR' AS CountryCode,
'' AS SeasonCode,
1 AS BasePriceCode,
CONVERT(date, @p3, 23) AS PriceDate,
'USD' AS CurrencyCode
) AS S
ON T.ItemTypeCode = S.ItemTypeCode
AND LTRIM(RTRIM(T.ItemCode)) = LTRIM(RTRIM(S.ItemCode))
AND ISNULL(T.CountryCode,'') = S.CountryCode
AND ISNULL(T.SeasonCode,'') = S.SeasonCode
AND ISNULL(T.BasePriceCode,0) = S.BasePriceCode
WHEN MATCHED THEN
UPDATE SET
PriceDate = S.PriceDate,
CurrencyCode = S.CurrencyCode,
Price = @p4,
LastUpdatedUserName = @p5,
LastUpdatedDate = GETDATE()
WHEN NOT MATCHED THEN
INSERT (
ItemTypeCode, ItemCode, CountryCode, SeasonCode, BasePriceCode,
PriceDate, CurrencyCode, Price, CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate
)
VALUES (
S.ItemTypeCode, S.ItemCode, S.CountryCode, S.SeasonCode, S.BasePriceCode,
S.PriceDate, S.CurrencyCode, @p4, @p5, GETDATE(), @p5, GETDATE()
);
`
_, err := mssqlDB.ExecContext(ctx, sqlText, 1, itemCode, priceDate, priceUSD, user)
return err
}
func GetProductAnaAltGrupByUrunKodu(ctx context.Context, mssqlDB *sql.DB, urunKodu string) (urunAnaGrubu string, urunAltGrubu string, err error) {
urunKodu = strings.TrimSpace(urunKodu)
if mssqlDB == nil || urunKodu == "" {
@@ -449,7 +709,7 @@ SELECT
END AS UretimSekli,
RTRIM(CONVERT(VARCHAR(32), ISNULL(SonIsEmri.nUrtSiparisNo, 0))) AS nUrtSiparisNo,
CONVERT(VARCHAR(10), SonIsEmri.dteIslemTarihi, 23) AS dteIslemTarihi,
ISNULL(CONVERT(VARCHAR(10), SonIsEmri.dteIslemTarihi, 23), '') AS dteIslemTarihi,
ISNULL(SonIsEmri.FirmaKodu, '') AS FirmaKodu,
ISNULL(SonIsEmri.FirmaAdi, '') AS FirmaAdi,
ISNULL(SonIsEmri.sVeren, '') AS SonIsEmriVeren,
@@ -679,6 +939,8 @@ func GetProductionHasCostDetailHeaderByOnMLNo(
SELECT TOP 1
ISNULL(UF.UretimiYapanFirma, '') AS UretimiYapanFirma,
ISNULL(UF.SonIsEmriVeren, '') AS SonIsEmriVeren,
ISNULL(UF.FirmaKodu, '') AS FirmaKodu,
ISNULL(UF.nFirmaID, 0) AS nFirmaID,
RTRIM(CONVERT(VARCHAR(32), ISNULL(M.nOnMLNo, 0))) AS nOnMLNo,
LTRIM(RTRIM(ISNULL(M.UrunKodu, ''))) AS UrunKodu,
ISNULL(M.UrunAdi, '') AS UrunAdi,
@@ -701,6 +963,8 @@ LEFT JOIN dbo.mk_uretim_sekli US
OUTER APPLY (
SELECT TOP 1
ISNULL(F.sAciklama, '') AS UretimiYapanFirma,
ISNULL(F.sKodu, '') AS FirmaKodu,
ISNULL(F.nFirmaID, 0) AS nFirmaID,
ISNULL(SM.sVeren, '') AS SonIsEmriVeren
FROM dbo.spUrtSiparisDet SD
INNER JOIN dbo.spUrtSiparis SM
@@ -757,6 +1021,8 @@ WITH RecipeMatch AS (
SELECT TOP 1
ISNULL(SonIsEmri.FirmaAdi, '') AS UretimiYapanFirma,
ISNULL(SonIsEmri.SonIsEmriVeren, '') AS SonIsEmriVeren,
ISNULL(SonIsEmri.FirmaKodu, '') AS FirmaKodu,
ISNULL(SonIsEmri.nFirmaID, 0) AS nFirmaID,
'' AS nOnMLNo,
RM.UrunKodu,
RM.UrunAdi,
@@ -777,6 +1043,8 @@ FROM RecipeMatch RM
OUTER APPLY (
SELECT TOP 1
ISNULL(F.sAciklama, '') AS FirmaAdi,
ISNULL(F.sKodu, '') AS FirmaKodu,
ISNULL(F.nFirmaID, 0) AS nFirmaID,
ISNULL(SM.sVeren, '') AS SonIsEmriVeren,
CONVERT(VARCHAR(16), SD.dteIslemTarihi, 120) AS dteIslemTarihi
FROM dbo.spUrtSiparisDet SD
@@ -960,6 +1228,221 @@ ORDER BY
return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike)
}
// ============================================================
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
// ============================================================
func ListProductionProductCostingDefaultQtyRows(ctx context.Context, uretimDB *sql.DB, search string, limit int) (*sql.Rows, error) {
search = strings.TrimSpace(search)
if limit <= 0 {
limit = 500
}
if limit > 5000 {
limit = 5000
}
searchLike := "%" + search + "%"
sqlText := `
SELECT TOP (@p3)
ISNULL(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
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
FROM dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK)
LEFT JOIN dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
WHERE
(@p1 = '' OR CONVERT(VARCHAR(32), ISNULL(V.nHammaddeTuruNo, 0)) LIKE @p2)
AND ISNULL(H.bAktif, 0) = 1
ORDER BY
V.nHammaddeTuruNo ASC
`
return uretimDB.QueryContext(ctx, sqlText, search, searchLike, limit)
}
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
WHERE nHammaddeTuruNo = @p1;
SELECT @@ROWCOUNT;
`
var affected int
if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, lDefaultMiktar, activeVal).Scan(&affected); err != nil {
return err
}
if affected == 0 {
return fmt.Errorf("default qty row not found: nHammaddeTuruNo=%d", nHammaddeTuruNo)
}
return nil
}
func RefreshProductionProductCostingDefaultQty(ctx context.Context, uretimDB *sql.DB, topN int) error {
if topN <= 0 {
topN = 10
}
if topN > 50 {
topN = 50
}
sqlText := `
;WITH ranked AS (
SELECT
D.nHammaddeTuruNo,
D.lMiktar,
ROW_NUMBER() OVER (
PARTITION BY D.nHammaddeTuruNo
ORDER BY
ISNULL(M.Tarihi, '19000101') DESC,
ISNULL(M.dteGuncellemeTarihi, '19000101') DESC,
D.nOnMLNo DESC,
D.dteIslemTarihi DESC,
D.nOnMLDetNo DESC
) AS rn
FROM dbo.spUrtOnMLMasDet D WITH (NOLOCK)
INNER JOIN dbo.spUrtOnMLMas M WITH (NOLOCK)
ON M.nOnMLNo = D.nOnMLNo
WHERE ISNULL(D.lMiktar, 0) > 0
AND ISNULL(M.bIptal, 0) = 0
),
agg AS (
SELECT
nHammaddeTuruNo,
CAST(AVG(CAST(lMiktar AS float)) AS numeric(14,4)) AS lDefaultMiktar
FROM ranked
WHERE rn <= @p1
GROUP BY nHammaddeTuruNo
)
MERGE dbo.mk_MaliyetParcaEslestirme_vmiktarlar AS T
USING agg AS S
ON T.nHammaddeTuruNo = S.nHammaddeTuruNo
WHEN MATCHED THEN
UPDATE SET
T.lDefaultMiktar = S.lDefaultMiktar,
T.dteCalcTarihi = GETDATE(),
T.bAktif = 1
WHEN NOT MATCHED THEN
INSERT (nHammaddeTuruNo, lDefaultMiktar, dteCalcTarihi, bAktif)
VALUES (S.nHammaddeTuruNo, S.lDefaultMiktar, GETDATE(), 1);
`
_, err := uretimDB.ExecContext(ctx, sqlText, topN)
return err
}
func CalcProductionProductCostingDefaultQtyFromLastOnML(ctx context.Context, uretimDB *sql.DB, nHammaddeTuruNo int, topN int) (float64, int, error) {
if nHammaddeTuruNo <= 0 {
return 0, 0, fmt.Errorf("nHammaddeTuruNo required")
}
if topN <= 0 {
topN = 10
}
if topN > 50 {
topN = 50
}
sqlText := `
;WITH ranked AS (
SELECT TOP (@p2)
ISNULL(D.lMiktar, 0) AS lMiktar
FROM dbo.spUrtOnMLMasDet D WITH (NOLOCK)
INNER JOIN dbo.spUrtOnMLMas M WITH (NOLOCK)
ON M.nOnMLNo = D.nOnMLNo
WHERE D.nHammaddeTuruNo = @p1
AND ISNULL(D.lMiktar, 0) > 0
ORDER BY ISNULL(M.Tarihi, M.dteKayitTarihi) DESC, D.nOnMLNo DESC, D.nOnMLDetNo DESC
)
SELECT
CAST(ISNULL(AVG(CAST(lMiktar AS DECIMAL(18,4))), 0) AS FLOAT) AS avgQty,
COUNT(1) AS sampleCount
FROM ranked;
`
var avg float64
var cnt int
if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, topN).Scan(&avg, &cnt); err != nil {
return 0, 0, err
}
return avg, cnt, nil
}
func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB *sql.DB, nos []int) (map[int]float64, 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 map[int]float64{}, nil
}
if len(clean) > 1000 {
return nil, fmt.Errorf("too many hammadde 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(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
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
INNER JOIN dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
WHERE ISNULL(H.bAktif, 0) = 1;
`
rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[int]float64, len(clean))
for rows.Next() {
var no int
var qty float64
if err := rows.Scan(&no, &qty); err != nil {
return nil, err
}
if no > 0 && qty > 0 {
out[no] = qty
}
}
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

@@ -566,6 +566,8 @@ func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Requ
if err := row.Scan(
&item.UretimiYapanFirma,
&item.SonIsEmriVeren,
&item.FirmaKodu,
&item.NFirmaID,
&item.NOnMLNo,
&item.UrunKodu,
&item.UrunAdi,
@@ -631,6 +633,8 @@ func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Requ
if err := row.Scan(
&item.UretimiYapanFirma,
&item.SonIsEmriVeren,
&item.FirmaKodu,
&item.NFirmaID,
&item.NOnMLNo,
&item.UrunKodu,
&item.UrunAdi,
@@ -881,6 +885,827 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
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
}
m, 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 = append(out, models.ProductionProductCostingDefaultQtyLookupItem{
NHammaddeTuruNo: no,
LDefaultMiktar: qty,
})
}
_ = json.NewEncoder(w).Encode(out)
}
// 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
}
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 == "" {
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 {
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 {
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
}
defer func() { _ = tx.Rollback() }()
// 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 {
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}
}
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
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
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
}
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
// 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)
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 {
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
}
logger.Error("stok tipi lookup error", "err", err)
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
return
}
if nStokTipiID <= 0 {
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
}
// 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()
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,''),NULL,@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,
); err != nil {
logger.Error("detail merge error", "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.
// Table observed in queries: dbo.spUrtRecMBolumMik (nUrtMBolumID stores nHammaddeTuruNo).
// ============================================================
if req.Header.NUrtReceteID > 0 {
receteID := req.Header.NUrtReceteID
// Determine next available recipe detail id (nUrtRecMBolumID)
nextRecDetID := 0
_ = tx.QueryRowContext(ctx, `
SELECT ISNULL(MAX(RMik.nUrtRecMBolumID), 0) + 1
FROM dbo.spUrtRecMBolumMik RMik WITH (UPDLOCK, HOLDLOCK)
WHERE RMik.nUrtReceteID = @p1
`, receteID).Scan(&nextRecDetID)
if nextRecDetID <= 0 {
nextRecDetID = 1
}
for _, row := range req.Detail.Upserts {
hNo := row.NHammaddeTuruNo
if hNo <= 0 {
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
}
}
// Resolve nHStokID from tbStok using sKodu (exact), then sModel fallback.
// IMPORTANT: nHStokID must be resolved; otherwise we'd update/insert too broadly.
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
}
// 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)
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 {
_, _ = tx.ExecContext(ctx, `
UPDATE dbo.spUrtRecMBolumMik
SET lHMiktar = @p4,
sKullaniciAdiDeg = @p5,
dteIslemTarihiDeg = GETDATE()
WHERE nUrtReceteID = @p1
AND nUrtMBolumID = @p2
AND nHStokID = @p3
`, receteID, hNo, nHStokID, row.LMiktar, user)
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.
_, insertErr := tx.ExecContext(ctx, `
INSERT INTO dbo.spUrtRecMBolumMik (
nUrtReceteID,
nUrtUBolumID,
nUrtRecMBolumID,
nStokID,
nHStokID,
lHMiktar,
lHFire,
nMaliyetTipiID,
lHMaliyet,
lMiktar,
sIslemKodu,
nUrtMBolumID,
nUrtMTBolumID,
lHCarpan,
bIslem,
nSure,
sAciklama,
dteDovizTarihi,
sDovizCinsi,
lDovizOran,
lDovizFiyat,
sKullaniciAdi,
dteIslemTarihi,
nMBolumSarfTipiNo
)
VALUES (
@p1,
0,
@p2,
0,
@p3,
@p4,
0,
6,
0,
1,
'',
@p5,
@p6,
1,
0,
0,
NULL,
NULL,
NULL,
NULL,
NULL,
@p7,
GETDATE(),
1
)
`, receteID, nextRecDetID, nHStokID, row.LMiktar, hNo, row.NUrtMTBolumID, user)
if insertErr == nil {
nextRecDetID += 1
}
}
}
if err := tx.Commit(); err != nil {
logger.Error("tx commit error", "err", err)
http.Error(w, "Kaydetme tamamlanamadi", http.StatusInternalServerError)
return
}
// 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 {
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})
}
// 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")