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", "order", "view",
wrapV3(http.HandlerFunc(routes.PostProductionHasCostDetailBulkPricesHandler)), 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, bindV3(r, pgDB,
"/api/pricing/production-product-costing/options/urun-ana-grup", "GET", "/api/pricing/production-product-costing/options/urun-ana-grup", "GET",
"order", "view", "order", "view",

View File

@@ -103,6 +103,8 @@ type ProductionHasCostDetailHeader struct {
UrunAltGrubu string `json:"UrunAltGrubu"` UrunAltGrubu string `json:"UrunAltGrubu"`
UretimSekliID string `json:"UretimSekliID"` UretimSekliID string `json:"UretimSekliID"`
UretimSekli string `json:"UretimSekli"` UretimSekli string `json:"UretimSekli"`
FirmaKodu string `json:"FirmaKodu"`
NFirmaID int `json:"nFirmaID"`
DteKayitTarihi string `json:"dteKayitTarihi"` DteKayitTarihi string `json:"dteKayitTarihi"`
SKullaniciAdi string `json:"sKullaniciAdi"` SKullaniciAdi string `json:"sKullaniciAdi"`
LTutarTL float64 `json:"lTutarTL"` LTutarTL float64 `json:"lTutarTL"`
@@ -116,6 +118,100 @@ type ProductionHasCostDetailHeader struct {
NUrtReceteID string `json:"nUrtReceteID"` 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 { type ProductionHasCostDetailExchangeRates struct {
RateDate string `json:"rateDate"` RateDate string `json:"rateDate"`
TRYRate float64 `json:"tryRate"` TRYRate float64 `json:"tryRate"`

View File

@@ -7,9 +7,269 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
"unicode" "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) { func GetProductAnaAltGrupByUrunKodu(ctx context.Context, mssqlDB *sql.DB, urunKodu string) (urunAnaGrubu string, urunAltGrubu string, err error) {
urunKodu = strings.TrimSpace(urunKodu) urunKodu = strings.TrimSpace(urunKodu)
if mssqlDB == nil || urunKodu == "" { if mssqlDB == nil || urunKodu == "" {
@@ -449,7 +709,7 @@ SELECT
END AS UretimSekli, END AS UretimSekli,
RTRIM(CONVERT(VARCHAR(32), ISNULL(SonIsEmri.nUrtSiparisNo, 0))) AS nUrtSiparisNo, 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.FirmaKodu, '') AS FirmaKodu,
ISNULL(SonIsEmri.FirmaAdi, '') AS FirmaAdi, ISNULL(SonIsEmri.FirmaAdi, '') AS FirmaAdi,
ISNULL(SonIsEmri.sVeren, '') AS SonIsEmriVeren, ISNULL(SonIsEmri.sVeren, '') AS SonIsEmriVeren,
@@ -679,6 +939,8 @@ func GetProductionHasCostDetailHeaderByOnMLNo(
SELECT TOP 1 SELECT TOP 1
ISNULL(UF.UretimiYapanFirma, '') AS UretimiYapanFirma, ISNULL(UF.UretimiYapanFirma, '') AS UretimiYapanFirma,
ISNULL(UF.SonIsEmriVeren, '') AS SonIsEmriVeren, 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, RTRIM(CONVERT(VARCHAR(32), ISNULL(M.nOnMLNo, 0))) AS nOnMLNo,
LTRIM(RTRIM(ISNULL(M.UrunKodu, ''))) AS UrunKodu, LTRIM(RTRIM(ISNULL(M.UrunKodu, ''))) AS UrunKodu,
ISNULL(M.UrunAdi, '') AS UrunAdi, ISNULL(M.UrunAdi, '') AS UrunAdi,
@@ -701,6 +963,8 @@ LEFT JOIN dbo.mk_uretim_sekli US
OUTER APPLY ( OUTER APPLY (
SELECT TOP 1 SELECT TOP 1
ISNULL(F.sAciklama, '') AS UretimiYapanFirma, ISNULL(F.sAciklama, '') AS UretimiYapanFirma,
ISNULL(F.sKodu, '') AS FirmaKodu,
ISNULL(F.nFirmaID, 0) AS nFirmaID,
ISNULL(SM.sVeren, '') AS SonIsEmriVeren ISNULL(SM.sVeren, '') AS SonIsEmriVeren
FROM dbo.spUrtSiparisDet SD FROM dbo.spUrtSiparisDet SD
INNER JOIN dbo.spUrtSiparis SM INNER JOIN dbo.spUrtSiparis SM
@@ -757,6 +1021,8 @@ WITH RecipeMatch AS (
SELECT TOP 1 SELECT TOP 1
ISNULL(SonIsEmri.FirmaAdi, '') AS UretimiYapanFirma, ISNULL(SonIsEmri.FirmaAdi, '') AS UretimiYapanFirma,
ISNULL(SonIsEmri.SonIsEmriVeren, '') AS SonIsEmriVeren, ISNULL(SonIsEmri.SonIsEmriVeren, '') AS SonIsEmriVeren,
ISNULL(SonIsEmri.FirmaKodu, '') AS FirmaKodu,
ISNULL(SonIsEmri.nFirmaID, 0) AS nFirmaID,
'' AS nOnMLNo, '' AS nOnMLNo,
RM.UrunKodu, RM.UrunKodu,
RM.UrunAdi, RM.UrunAdi,
@@ -777,6 +1043,8 @@ FROM RecipeMatch RM
OUTER APPLY ( OUTER APPLY (
SELECT TOP 1 SELECT TOP 1
ISNULL(F.sAciklama, '') AS FirmaAdi, ISNULL(F.sAciklama, '') AS FirmaAdi,
ISNULL(F.sKodu, '') AS FirmaKodu,
ISNULL(F.nFirmaID, 0) AS nFirmaID,
ISNULL(SM.sVeren, '') AS SonIsEmriVeren, ISNULL(SM.sVeren, '') AS SonIsEmriVeren,
CONVERT(VARCHAR(16), SD.dteIslemTarihi, 120) AS dteIslemTarihi CONVERT(VARCHAR(16), SD.dteIslemTarihi, 120) AS dteIslemTarihi
FROM dbo.spUrtSiparisDet SD FROM dbo.spUrtSiparisDet SD
@@ -960,6 +1228,221 @@ ORDER BY
return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike) 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 { func buildSQLServerFullTextPrefixQuery(search string) string {
terms := strings.Fields(strings.TrimSpace(search)) terms := strings.Fields(strings.TrimSpace(search))
parts := make([]string, 0, len(terms)) parts := make([]string, 0, len(terms))

View File

@@ -566,6 +566,8 @@ func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Requ
if err := row.Scan( if err := row.Scan(
&item.UretimiYapanFirma, &item.UretimiYapanFirma,
&item.SonIsEmriVeren, &item.SonIsEmriVeren,
&item.FirmaKodu,
&item.NFirmaID,
&item.NOnMLNo, &item.NOnMLNo,
&item.UrunKodu, &item.UrunKodu,
&item.UrunAdi, &item.UrunAdi,
@@ -631,6 +633,8 @@ func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Requ
if err := row.Scan( if err := row.Scan(
&item.UretimiYapanFirma, &item.UretimiYapanFirma,
&item.SonIsEmriVeren, &item.SonIsEmriVeren,
&item.FirmaKodu,
&item.NFirmaID,
&item.NOnMLNo, &item.NOnMLNo,
&item.UrunKodu, &item.UrunKodu,
&item.UrunAdi, &item.UrunAdi,
@@ -881,6 +885,827 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
http.Error(w, "kind hammadde, item veya color olmali", http.StatusBadRequest) 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 // GET /api/pricing/production-product-costing/has-cost-detail-exchange-rates
func GetProductionHasCostDetailExchangeRatesHandler(w http.ResponseWriter, r *http.Request) { func GetProductionHasCostDetailExchangeRatesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")

View File

@@ -0,0 +1,75 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { Quasar } from 'quasar'
import { markRaw } from 'vue'
import RootComponent from 'app/src/App.vue'
import createStore from 'app/src/stores/index'
import createRouter from 'app/src/router/index'
export default async function (createAppFn, quasarUserOptions) {
// Create the app instance.
// Here we inject into it the Quasar UI, the router & possibly the store.
const app = createAppFn(RootComponent)
app.use(Quasar, quasarUserOptions)
const store = typeof createStore === 'function'
? await createStore({})
: createStore
app.use(store)
const router = markRaw(
typeof createRouter === 'function'
? await createRouter({store})
: createRouter
)
// make router instance available in store
store.use(({ store }) => { store.router = router })
// Expose the app, the router and the store.
// Note that we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {
app,
store,
router
}
}

View File

@@ -0,0 +1,158 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { createApp } from 'vue'
import '@quasar/extras/roboto-font/roboto-font.css'
import '@quasar/extras/material-icons/material-icons.css'
// We load Quasar stylesheet file
import 'quasar/dist/quasar.sass'
import 'src/css/app.css'
import createQuasarApp from './app.js'
import quasarUserOptions from './quasar-user-options.js'
const publicPath = `/`
async function start ({
app,
router
, store
}, bootFiles) {
let hasRedirected = false
const getRedirectUrl = url => {
try { return router.resolve(url).href }
catch (err) {}
return Object(url) === url
? null
: url
}
const redirect = url => {
hasRedirected = true
if (typeof url === 'string' && /^https?:\/\//.test(url)) {
window.location.href = url
return
}
const href = getRedirectUrl(url)
// continue if we didn't fail to resolve the url
if (href !== null) {
window.location.href = href
window.location.reload()
}
}
const urlPath = window.location.href.replace(window.location.origin, '')
for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) {
try {
await bootFiles[i]({
app,
router,
store,
ssrContext: null,
redirect,
urlPath,
publicPath
})
}
catch (err) {
if (err && err.url) {
redirect(err.url)
return
}
console.error('[Quasar] boot error:', err)
return
}
}
if (hasRedirected === true) return
app.use(router)
app.mount('#q-app')
}
createQuasarApp(createApp, quasarUserOptions)
.then(app => {
// eventually remove this when Cordova/Capacitor/Electron support becomes old
const [ method, mapFn ] = Promise.allSettled !== void 0
? [
'allSettled',
bootFiles => bootFiles.map(result => {
if (result.status === 'rejected') {
console.error('[Quasar] boot error:', result.reason)
return
}
return result.value.default
})
]
: [
'all',
bootFiles => bootFiles.map(entry => entry.default)
]
return Promise[ method ]([
import(/* webpackMode: "eager" */ 'boot/dayjs'),
import(/* webpackMode: "eager" */ 'boot/locale'),
import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard')
]).then(bootFiles => {
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
start(app, boot)
})
})

View File

@@ -0,0 +1,116 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import App from 'app/src/App.vue'
let appPrefetch = typeof App.preFetch === 'function'
? App.preFetch
: (
// Class components return the component options (and the preFetch hook) inside __c property
App.__c !== void 0 && typeof App.__c.preFetch === 'function'
? App.__c.preFetch
: false
)
function getMatchedComponents (to, router) {
const route = to
? (to.matched ? to : router.resolve(to).route)
: router.currentRoute.value
if (!route) { return [] }
const matched = route.matched.filter(m => m.components !== void 0)
if (matched.length === 0) { return [] }
return Array.prototype.concat.apply([], matched.map(m => {
return Object.keys(m.components).map(key => {
const comp = m.components[key]
return {
path: m.path,
c: comp
}
})
}))
}
export function addPreFetchHooks ({ router, store, publicPath }) {
// Add router hook for handling preFetch.
// Doing it after initial route is resolved so that we don't double-fetch
// the data that we already have. Using router.beforeResolve() so that all
// async components are resolved.
router.beforeResolve((to, from, next) => {
const
urlPath = window.location.href.replace(window.location.origin, ''),
matched = getMatchedComponents(to, router),
prevMatched = getMatchedComponents(from, router)
let diffed = false
const preFetchList = matched
.filter((m, i) => {
return diffed || (diffed = (
!prevMatched[i] ||
prevMatched[i].c !== m.c ||
m.path.indexOf('/:') > -1 // does it has params?
))
})
.filter(m => m.c !== void 0 && (
typeof m.c.preFetch === 'function'
// Class components return the component options (and the preFetch hook) inside __c property
|| (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function')
))
.map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch)
if (appPrefetch !== false) {
preFetchList.unshift(appPrefetch)
appPrefetch = false
}
if (preFetchList.length === 0) {
return next()
}
let hasRedirected = false
const redirect = url => {
hasRedirected = true
next(url)
}
const proceed = () => {
if (hasRedirected === false) { next() }
}
preFetchList.reduce(
(promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({
store,
currentRoute: to,
previousRoute: from,
redirect,
urlPath,
publicPath
})),
Promise.resolve()
)
.then(proceed)
.catch(e => {
console.error(e)
proceed()
})
})
}

View File

@@ -0,0 +1,23 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import lang from 'quasar/lang/tr.js'
import {Loading,Dialog,Notify} from 'quasar'
export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} }

View File

@@ -344,6 +344,11 @@ const menuItems = [
label: 'Maliyet Parça Eşleştirme', label: 'Maliyet Parça Eşleştirme',
to: '/app/pricing/production-product-costing/maliyet-parca-eslestirme', to: '/app/pricing/production-product-costing/maliyet-parca-eslestirme',
permission: 'order:view' permission: 'order:view'
},
{
label: 'Maliyet Varsayilan Miktarlar',
to: '/app/pricing/production-product-costing/default-quantities',
permission: 'order:view'
} }
] ]
}, },

View File

@@ -0,0 +1,235 @@
<template>
<q-page class="q-pa-md">
<div class="sticky-stack">
<div class="save-toolbar">
<div class="row items-center justify-between q-col-gutter-sm">
<div class="col-auto text-weight-bold">Maliyet Varsayilan Miktarlar</div>
<div class="col-auto row items-center q-gutter-sm">
<q-btn
dense
outline
color="grey-8"
icon="undo"
label="Taslagi Temizle"
:disable="!hasUnsavedChanges"
@click="onClearDraft"
/>
<q-btn
dense
color="primary"
icon="save"
label="Degisiklikleri Kaydet"
:loading="saving"
:disable="!hasUnsavedChanges"
@click="onSaveAll"
/>
<q-btn
dense
outline
color="grey-8"
icon="refresh"
label="Yenile"
:loading="loading || saving"
@click="fetchRows"
/>
</div>
</div>
</div>
<div class="filter-bar q-mb-md">
<div class="row q-col-gutter-sm items-center">
<div class="col-12 col-md-4">
<q-input v-model="search" dense filled clearable label="Ara (Hammadde No)" @update:model-value="debouncedFetch" />
</div>
<div class="col-12 col-md-auto text-grey-7">
Kayit: {{ Array.isArray(rows) ? rows.length : 0 }} | Degisen: {{ dirtyNos.length }}
</div>
</div>
</div>
</div>
<q-banner v-if="error" class="bg-red text-white q-mb-md">
{{ error }}
</q-banner>
<q-table
flat
bordered
dense
row-key="nHammaddeTuruNo"
:rows="rows"
:columns="columns"
:loading="loading"
:rows-per-page-options="[0]"
hide-bottom
>
<template #body-cell-actions="props">
<q-td :props="props">
<q-btn
dense
flat
icon="calculate"
color="primary"
@click="onCalcAvg(props.row)"
>
<q-tooltip>Son 10 Ortalama</q-tooltip>
</q-btn>
</q-td>
</template>
<template #body-cell-lDefaultMiktar="props">
<q-td :props="props">
<q-input
:model-value="props.row.lDefaultMiktar"
dense
filled
type="number"
step="0.0001"
@update:model-value="val => onEditQty(props.row, val)"
style="max-width: 140px"
/>
</q-td>
</template>
<template #body-cell-bAktif="props">
<q-td :props="props">
<q-toggle
:model-value="Boolean(props.row.bAktif)"
@update:model-value="val => onEditActive(props.row, val)"
/>
</q-td>
</template>
</q-table>
</q-page>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useQuasar } from 'quasar'
import { onBeforeRouteLeave } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useProductionProductCostingDefaultQtyStore } from 'src/stores/productionProductCostingDefaultQtyStore'
const $q = useQuasar()
const store = useProductionProductCostingDefaultQtyStore()
const { rows, loading, saving, error, dirtyNos, hasUnsavedChanges } = storeToRefs(store)
const search = ref('')
const columns = [
{ name: 'nHammaddeTuruNo', label: 'HammaddeTuruNo', field: 'nHammaddeTuruNo', align: 'left', sortable: true },
{ name: 'sAciklama', label: 'Aciklama', field: 'sAciklama', align: 'left', sortable: true },
{ name: 'lDefaultMiktar', label: 'Varsayilan Miktar', field: 'lDefaultMiktar', align: 'right', sortable: true },
{ name: 'dteCalcTarihi', label: 'Hesap Tarihi', field: 'dteCalcTarihi', align: 'left', sortable: true },
{ name: 'bAktif', label: 'Aktif', field: 'bAktif', align: 'center', sortable: true },
{ name: 'actions', label: '', field: '__actions', align: 'right', sortable: false }
]
let debounceTimer = null
function debouncedFetch () {
if (debounceTimer) window.clearTimeout(debounceTimer)
debounceTimer = window.setTimeout(() => fetchRows(), 250)
}
async function fetchRows () {
await store.fetch({
search: String(search.value || '').trim()
})
}
function onEditQty (row, val) {
const no = Number(row?.nHammaddeTuruNo || 0)
const qty = Number(val || 0)
if (!(no > 0)) return
store.setDraft(no, { lDefaultMiktar: qty })
// keep table row in sync visually
row.lDefaultMiktar = qty
}
function onEditActive (row, val) {
const no = Number(row?.nHammaddeTuruNo || 0)
if (!(no > 0)) return
store.setDraft(no, { bAktif: Boolean(val) })
row.bAktif = Boolean(val)
}
async function onCalcAvg (row) {
const no = Number(row?.nHammaddeTuruNo || 0)
if (!(no > 0)) return
try {
const resp = await store.calcAvgForRow({ nHammaddeTuruNo: no, topN: 10 })
const qty = Number(resp?.lDefaultMiktar || 0)
if (qty > 0) {
store.setDraft(no, { lDefaultMiktar: qty })
row.lDefaultMiktar = qty
$q.notify({ type: 'positive', message: `Ortalama yazildi (n=${Number(resp?.nSampleCount || 0)})`, position: 'top-right' })
} else {
$q.notify({ type: 'warning', message: 'Ortalama bulunamadi', position: 'top-right' })
}
} catch (e) {
$q.notify({ type: 'negative', message: String(e?.message || e || 'Hata'), position: 'top-right' })
}
}
async function onSaveAll () {
try {
const resp = await store.saveAll()
await fetchRows()
$q.notify({ type: 'positive', message: `Kaydedildi (${Number(resp?.updated || 0)})`, position: 'top-right' })
} catch (e) {
$q.notify({ type: 'negative', message: String(e?.message || e || 'Kaydedilemedi'), position: 'top-right' })
}
}
function onClearDraft () {
$q.dialog({
title: 'Taslak Temizlensin mi?',
message: 'Kaydedilmemis degisiklikler silinecek.',
cancel: true,
persistent: true
}).onOk(() => {
store.clearDraft()
fetchRows()
})
}
function ensureBeforeUnloadGuard (enabled) {
if (!enabled) {
window.onbeforeunload = null
return
}
window.onbeforeunload = (e) => {
e.preventDefault()
e.returnValue = ''
return ''
}
}
watch(hasUnsavedChanges, (v) => ensureBeforeUnloadGuard(Boolean(v)))
onBeforeRouteLeave((to, from, next) => {
if (!hasUnsavedChanges.value) return next()
$q.dialog({
title: 'Kaydedilmemis Degisiklikler Var',
message: 'Sayfadan cikmak istiyor musunuz?',
cancel: true,
persistent: true
}).onOk(() => next()).onCancel(() => next(false))
})
onMounted(() => {
fetchRows()
})
onUnmounted(() => {
window.onbeforeunload = null
})
</script>
<style scoped>
.sticky-stack {
position: sticky;
top: var(--header-h);
z-index: 100;
background: #fff;
padding-top: 8px;
}
</style>

View File

@@ -2818,6 +2818,39 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
// for multiple parts (Ceket/Pantolon/Yelek...), so de-duping only by hNo is incorrect. // for multiple parts (Ceket/Pantolon/Yelek...), so de-duping only by hNo is incorrect.
const processedRequiredKeys = new Set() const processedRequiredKeys = new Set()
// Prefetch default quantities for all required hammadde types (1 call).
const allRequiredNos = []
for (const mapping of list) {
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
for (const hNoRaw of hList) {
const hNo = parseInt(String(hNoRaw || '').trim(), 10)
if (hNo > 0) allRequiredNos.push(hNo)
}
}
let defaultQtyByNo = {}
try {
const defaults = await post('/pricing/production-product-costing/default-quantities/lookup', {
nHammaddeTuruNos: allRequiredNos
})
const arr = Array.isArray(defaults) ? defaults : []
arr.forEach(it => {
const no = parseInt(String(it?.nHammaddeTuruNo ?? '0'), 10) || 0
const qty = Number(it?.lDefaultMiktar || 0)
if (no > 0 && qty > 0) defaultQtyByNo[no] = qty
})
} catch (err) {
defaultQtyByNo = {}
slog.error('production-product-costing.detail', 'default-qty:lookup:error', {
trace_id: traceId.value,
error: String(err?.message || err)
})
$q.notify({
type: 'warning',
message: 'Varsayilan miktarlar alinmadi (lookup hatasi). Miktarlar 1 olarak geldi.',
position: 'top-right'
})
}
// Add missing placeholder rows (qty=1, price=0) to remind user // Add missing placeholder rows (qty=1, price=0) to remind user
for (const mapping of list) { for (const mapping of list) {
// Parca adi (CEKET/PANTOLON/YELEK...) comes from the MT bolum description (joined in backend as parcaBolumAdi). // Parca adi (CEKET/PANTOLON/YELEK...) comes from the MT bolum description (joined in backend as parcaBolumAdi).
@@ -2899,8 +2932,8 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
sRenk: '', sRenk: '',
ColorCode: '', ColorCode: '',
ColorDescription: '', ColorDescription: '',
lMiktar: 1, lMiktar: (Number(defaultQtyByNo[parseInt(hNo, 10)] || 0) > 0 ? Number(defaultQtyByNo[parseInt(hNo, 10)]) : 1),
miktarInput: '1', miktarInput: (Number(defaultQtyByNo[parseInt(hNo, 10)] || 0) > 0 ? String(defaultQtyByNo[parseInt(hNo, 10)]) : '1'),
inputPrice: '0', inputPrice: '0',
inputPricePrBr: 'USD', inputPricePrBr: 'USD',
fiyat_girilen: 0, fiyat_girilen: 0,
@@ -3086,6 +3119,83 @@ function saveRowEditor () {
rowEditorDialogOpen.value = false rowEditorDialogOpen.value = false
} }
function round1 (n) {
const x = Number(n || 0)
if (!Number.isFinite(x)) return 0
return Math.round(x * 10) / 10
}
function round4 (n) {
const x = Number(n || 0)
if (!Number.isFinite(x)) return 0
return Math.round(x * 10000) / 10000
}
async function confirmDefaultQtyDeviationIfNeeded () {
// Compare entered qty vs default qty (mk_MaliyetParcaEslestirme_vmiktarlar) per hammadde type.
// Rule: if deviation > 10% (abs), require user confirmation.
const qtyByNo = {}
flatDetailRows.value.forEach(r => {
const no = parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
const qty = Number(r?.lMiktar || 0)
if (!(no > 0) || !(qty > 0)) return
qtyByNo[no] = (qtyByNo[no] || 0) + qty
})
const nos = Object.keys(qtyByNo).map(k => parseInt(k, 10)).filter(n => n > 0)
if (nos.length === 0) return true
let defaults = []
try {
defaults = await post('/pricing/production-product-costing/default-quantities/lookup', {
nHammaddeTuruNos: nos
})
} catch {
// If lookup fails, don't block save.
return true
}
const defMap = {}
;(Array.isArray(defaults) ? defaults : []).forEach(it => {
const no = parseInt(String(it?.nHammaddeTuruNo || '0'), 10) || 0
const d = Number(it?.lDefaultMiktar || 0)
if (no > 0 && d > 0) defMap[no] = d
})
const outliers = []
Object.keys(defMap).forEach(k => {
const no = parseInt(k, 10) || 0
const defQty = Number(defMap[no] || 0)
const enteredQty = Number(qtyByNo[no] || 0)
if (!(defQty > 0) || !(enteredQty > 0)) return
const pct = ((enteredQty - defQty) / defQty) * 100
if (Math.abs(pct) > 10) {
outliers.push({ no, defQty, enteredQty, pct })
}
})
if (outliers.length === 0) return true
outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct))
const maxRows = 30
const lines = outliers.slice(0, maxRows).map(x => {
const sign = x.pct >= 0 ? '+' : ''
return `${x.no}: varsayilan ${round4(x.defQty)} | girilen ${round4(x.enteredQty)} | fark ${sign}${round1(x.pct)}%`
})
const truncated = outliers.length > maxRows
? `\n... (Toplam ${outliers.length} satir. Ilk ${maxRows} gosterildi.)`
: ''
const ok = await new Promise(resolve => {
$q.dialog({
title: 'Varsayilan Miktar Kontrolu',
message: `Bazi hammadde turlerinde varsayilan miktardan %10'dan fazla sapma var.\n\n${lines.join('\n')}${truncated}\n\nOnayliyorsaniz Kaydet'e basın. Duzenlemek icin Geri Don.`,
cancel: { label: 'Geri Don' },
ok: { label: 'Onayla ve Kaydet', color: 'primary' },
persistent: true
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
})
return ok
}
async function saveChanges () { async function saveChanges () {
saveLoading.value = true saveLoading.value = true
try { try {
@@ -3117,11 +3227,68 @@ async function saveChanges () {
} }
} }
$q.notify({ const okDefaultQty = await confirmDefaultQtyDeviationIfNeeded()
type: 'warning', if (!okDefaultQty) return
message: 'Kaydetme endpointi henuz eklenmedi. Buton hazir, backend baglantisi bir sonraki adimda eklenebilir.',
position: 'top-right' if (!detailHeader.value) {
}) $q.notify({ type: 'negative', message: 'Header bulunamadi.', position: 'top-right' })
return
}
const header = detailHeader.value
const upserts = flatDetailRows.value.map(r => ({
n_onml_det_no: parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0,
n_hammadde_turu_no: parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0,
n_urt_mt_bolum_id: parseInt(String(r?.nUrtMTBolumID || '0').trim() || '0', 10) || 0,
s_kodu: String(r?.sKodu || '').trim(),
s_aciklama: String(r?.sAciklama || '').trim(),
s_renk: String(r?.sRenk || '').trim(),
s_beden: String(r?.sBeden || '').trim(),
s_aciklama2: String(r?.sAciklama2 || '').trim(),
s_birim: String(r?.sBirim || '').trim(),
l_miktar: Number(r?.lMiktar || 0),
fiyat_girilen: Number(resolveNumericRowInputPrice(r) || 0),
fiyat_doviz: String(resolveInputCurrency(r) || '').trim(),
maliyete_dahil: (r?.maliyeteDahil || r?.maliyete_dahil) ? 1 : 0,
cm_price_type_id: r?.cmPriceTypeId ?? r?.cm_price_type_id ?? null
}))
const deletes = (Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []).map(d => ({
n_onml_det_no: parseInt(String(d?.nOnMLDetNo || '').trim() || '0', 10) || 0
})).filter(x => x.n_onml_det_no > 0)
const payload = {
detail_source: isNoCostDetail.value ? 'no-cost' : 'has-cost',
header: {
n_onml_no: parseInt(String(header?.nOnMLNo || onMLNo.value || '0').trim() || '0', 10) || 0,
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim(),
urun_adi: String(header?.UrunAdi || '').trim(),
maliyet_tarihi: normalizeDateInput(costDate.value),
n_urt_recete_id: parseInt(String(header?.nUrtReceteID || recipeCode.value || '0').trim() || '0', 10) || 0,
uretim_sekli_id: parseInt(String(header?.UretimSekliID || '0').trim() || '0', 10) || 0,
s_aciklama: String(header?.sAciklama || header?.SAciklama || '').trim(),
firma_kodu: String(header?.FirmaKodu || '').trim(),
n_firma_id: parseInt(String(header?.nFirmaID || header?.NFirmaID || '0').trim() || '0', 10) || 0
},
detail: { upserts, deletes }
}
const response = await post('/pricing/production-product-costing/onml/save', payload)
const newOnMLNo = parseInt(String(response?.n_onml_no || '0'), 10) || 0
$q.notify({ type: 'positive', message: 'Kaydedildi.', position: 'top-right' })
// If we created a new OnML (no-cost), switch to has-cost detail mode.
if (isNoCostDetail.value && newOnMLNo > 0) {
router.replace({
name: 'production-product-costing-has-cost-detail',
query: {
n_onml_no: String(newOnMLNo),
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim()
}
})
return
}
} finally { } finally {
saveLoading.value = false saveLoading.value = false
} }
@@ -3158,6 +3325,8 @@ onBeforeUnmount(() => {
window.removeEventListener('resize', updateStickyTop) window.removeEventListener('resize', updateStickyTop)
ensureBeforeUnloadGuard(false) ensureBeforeUnloadGuard(false)
if (localDraftTimer.value) window.clearTimeout(localDraftTimer.value) if (localDraftTimer.value) window.clearTimeout(localDraftTimer.value)
// Defensive: if component unmounts without router guard (rare), drop the draft.
clearLocalDraft()
}) })
watch( watch(
@@ -3169,6 +3338,8 @@ watch(
onBeforeRouteLeave((to, from, next) => { onBeforeRouteLeave((to, from, next) => {
if (!hasUnsavedChanges.value) { if (!hasUnsavedChanges.value) {
// Always clear local draft on page exit; backend is source of truth.
clearLocalDraft()
next() next()
return return
} }
@@ -3177,20 +3348,13 @@ onBeforeRouteLeave((to, from, next) => {
title: 'Sayfadan ayriliyorsunuz', title: 'Sayfadan ayriliyorsunuz',
message: pageMode.value === 'edit' message: pageMode.value === 'edit'
? 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?' ? 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?'
: 'Taslak korunacak. Sayfadan cikmak istiyor musunuz?', : 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?',
ok: { label: 'Evet', color: 'negative' }, ok: { label: 'Evet', color: 'negative' },
cancel: { label: 'Hayir' }, cancel: { label: 'Hayir' },
persistent: true persistent: true
}) })
.onOk(() => { .onOk(() => {
// NEW (no-cost): always persist draft so it can be resumed clearLocalDraft()
if (pageMode.value === 'new') {
persistLocalDraftNow()
next()
return
}
// EDIT (has-cost): allow exit, keep draft for now (later we can clear it after successful save)
next() next()
}) })
.onCancel(() => next(false)) .onCancel(() => next(false))

View File

@@ -371,6 +371,12 @@ const routes = [
component: () => import('pages/ProductionProductCostingMTBolumMapping.vue'), component: () => import('pages/ProductionProductCostingMTBolumMapping.vue'),
meta: { permission: 'order:view' } meta: { permission: 'order:view' }
}, },
{
path: 'pricing/production-product-costing/default-quantities',
name: 'production-product-costing-default-quantities',
component: () => import('pages/ProductionProductCostingDefaultQuantities.vue'),
meta: { permission: 'order:view' }
},
/* ================= PASSWORD ================= */ /* ================= PASSWORD ================= */

View File

@@ -0,0 +1,171 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { get, post, extractApiErrorDetail } from 'src/services/api'
const DRAFT_KEY = 'bssapp:production-product-costing:default-quantities:draft:v1'
function safeJsonParse (raw) {
try {
return JSON.parse(raw)
} catch {
return null
}
}
export const useProductionProductCostingDefaultQtyStore = defineStore('production_product_costing_default_qty', () => {
const rows = ref([])
const loading = ref(false)
const saving = ref(false)
const error = ref('')
// draftByNo: { [nHammaddeTuruNo]: { lDefaultMiktar, bAktif } }
const draftByNo = ref({})
const persistTimer = ref(0)
const dirtyNos = computed(() => Object.keys(draftByNo.value || {}).map(k => Number(k)).filter(n => n > 0))
const hasUnsavedChanges = computed(() => dirtyNos.value.length > 0)
function schedulePersistDraft () {
try {
if (persistTimer.value) window.clearTimeout(persistTimer.value)
persistTimer.value = window.setTimeout(() => {
persistTimer.value = 0
persistDraftNow()
}, 350)
} catch {
persistDraftNow()
}
}
function persistDraftNow () {
try {
const payload = {
version: 1,
savedAt: new Date().toISOString(),
draftByNo: draftByNo.value || {}
}
localStorage.setItem(DRAFT_KEY, JSON.stringify(payload))
} catch {
// ignore
}
}
function hydrateDraft () {
try {
const raw = localStorage.getItem(DRAFT_KEY)
if (!raw) return
const payload = safeJsonParse(raw)
if (!payload || typeof payload !== 'object') return
if (!payload.draftByNo || typeof payload.draftByNo !== 'object') return
draftByNo.value = payload.draftByNo
} catch {
// ignore
}
}
function clearDraft () {
draftByNo.value = {}
try {
localStorage.removeItem(DRAFT_KEY)
} catch {
// ignore
}
}
function setDraft (nHammaddeTuruNo, patch) {
const no = Number(nHammaddeTuruNo || 0)
if (!(no > 0)) return
const prev = draftByNo.value?.[String(no)] || {}
draftByNo.value = {
...(draftByNo.value || {}),
[String(no)]: {
...prev,
...(patch || {})
}
}
schedulePersistDraft()
}
function getEffectiveRow (row) {
const no = Number(row?.nHammaddeTuruNo || 0)
if (!(no > 0)) return row
const draft = draftByNo.value?.[String(no)]
if (!draft) return row
return {
...row,
lDefaultMiktar: typeof draft.lDefaultMiktar === 'number' ? draft.lDefaultMiktar : row?.lDefaultMiktar,
bAktif: typeof draft.bAktif === 'boolean' ? draft.bAktif : row?.bAktif,
__dirty: true
}
}
async function fetch ({ search = '', onlyActive = true, limit = 2000 } = {}) {
loading.value = true
error.value = ''
try {
const data = await get('/pricing/production-product-costing/default-quantities', {
search: String(search || '').trim(),
limit
})
const base = Array.isArray(data) ? data : []
rows.value = base.map(getEffectiveRow)
} catch (e) {
error.value = extractApiErrorDetail(e) || String(e?.message || e || 'Hata')
throw e
} finally {
loading.value = false
}
}
async function calcAvgForRow ({ nHammaddeTuruNo, topN = 10 } = {}) {
const no = Number(nHammaddeTuruNo || 0)
if (!(no > 0)) return null
const resp = await post('/pricing/production-product-costing/default-quantities/calc-avg', {
nHammaddeTuruNo: no,
topN: Number(topN || 10)
})
return resp
}
async function saveAll () {
if (!hasUnsavedChanges.value) return { updated: 0 }
saving.value = true
error.value = ''
try {
const items = dirtyNos.value.map(no => {
const patch = draftByNo.value?.[String(no)] || {}
return {
nHammaddeTuruNo: Number(no),
lDefaultMiktar: Number(patch.lDefaultMiktar || 0),
bAktif: typeof patch.bAktif === 'boolean' ? patch.bAktif : undefined
}
}).filter(it => it.nHammaddeTuruNo > 0 && it.lDefaultMiktar > 0)
const resp = await post('/pricing/production-product-costing/default-quantities/update-bulk', { items })
clearDraft()
return resp
} catch (e) {
error.value = extractApiErrorDetail(e) || String(e?.message || e || 'Hata')
throw e
} finally {
saving.value = false
}
}
hydrateDraft()
return {
rows,
loading,
saving,
error,
draftByNo,
dirtyNos,
hasUnsavedChanges,
fetch,
setDraft,
calcAvgForRow,
saveAll,
clearDraft
}
})