Merge remote-tracking branch 'origin/master'
This commit is contained in:
35
svc/main.go
35
svc/main.go
@@ -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",
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
75
ui/.quasar/prod-spa/app.js
Normal file
75
ui/.quasar/prod-spa/app.js
Normal 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
|
||||
}
|
||||
}
|
||||
158
ui/.quasar/prod-spa/client-entry.js
Normal file
158
ui/.quasar/prod-spa/client-entry.js
Normal 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)
|
||||
})
|
||||
})
|
||||
|
||||
116
ui/.quasar/prod-spa/client-prefetch.js
Normal file
116
ui/.quasar/prod-spa/client-prefetch.js
Normal 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()
|
||||
})
|
||||
})
|
||||
}
|
||||
23
ui/.quasar/prod-spa/quasar-user-options.js
Normal file
23
ui/.quasar/prod-spa/quasar-user-options.js
Normal 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} }
|
||||
|
||||
@@ -344,6 +344,11 @@ const menuItems = [
|
||||
label: 'Maliyet Parça Eşleştirme',
|
||||
to: '/app/pricing/production-product-costing/maliyet-parca-eslestirme',
|
||||
permission: 'order:view'
|
||||
},
|
||||
{
|
||||
label: 'Maliyet Varsayilan Miktarlar',
|
||||
to: '/app/pricing/production-product-costing/default-quantities',
|
||||
permission: 'order:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
235
ui/src/pages/ProductionProductCostingDefaultQuantities.vue
Normal file
235
ui/src/pages/ProductionProductCostingDefaultQuantities.vue
Normal 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>
|
||||
@@ -2818,6 +2818,39 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
// for multiple parts (Ceket/Pantolon/Yelek...), so de-duping only by hNo is incorrect.
|
||||
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
|
||||
for (const mapping of list) {
|
||||
// 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: '',
|
||||
ColorCode: '',
|
||||
ColorDescription: '',
|
||||
lMiktar: 1,
|
||||
miktarInput: '1',
|
||||
lMiktar: (Number(defaultQtyByNo[parseInt(hNo, 10)] || 0) > 0 ? Number(defaultQtyByNo[parseInt(hNo, 10)]) : 1),
|
||||
miktarInput: (Number(defaultQtyByNo[parseInt(hNo, 10)] || 0) > 0 ? String(defaultQtyByNo[parseInt(hNo, 10)]) : '1'),
|
||||
inputPrice: '0',
|
||||
inputPricePrBr: 'USD',
|
||||
fiyat_girilen: 0,
|
||||
@@ -3086,6 +3119,83 @@ function saveRowEditor () {
|
||||
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 () {
|
||||
saveLoading.value = true
|
||||
try {
|
||||
@@ -3117,11 +3227,68 @@ async function saveChanges () {
|
||||
}
|
||||
}
|
||||
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Kaydetme endpointi henuz eklenmedi. Buton hazir, backend baglantisi bir sonraki adimda eklenebilir.',
|
||||
position: 'top-right'
|
||||
})
|
||||
const okDefaultQty = await confirmDefaultQtyDeviationIfNeeded()
|
||||
if (!okDefaultQty) return
|
||||
|
||||
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 {
|
||||
saveLoading.value = false
|
||||
}
|
||||
@@ -3158,6 +3325,8 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateStickyTop)
|
||||
ensureBeforeUnloadGuard(false)
|
||||
if (localDraftTimer.value) window.clearTimeout(localDraftTimer.value)
|
||||
// Defensive: if component unmounts without router guard (rare), drop the draft.
|
||||
clearLocalDraft()
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -3169,6 +3338,8 @@ watch(
|
||||
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
// Always clear local draft on page exit; backend is source of truth.
|
||||
clearLocalDraft()
|
||||
next()
|
||||
return
|
||||
}
|
||||
@@ -3177,20 +3348,13 @@ onBeforeRouteLeave((to, from, next) => {
|
||||
title: 'Sayfadan ayriliyorsunuz',
|
||||
message: pageMode.value === 'edit'
|
||||
? 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?'
|
||||
: 'Taslak korunacak. Sayfadan cikmak istiyor musunuz?',
|
||||
: 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?',
|
||||
ok: { label: 'Evet', color: 'negative' },
|
||||
cancel: { label: 'Hayir' },
|
||||
persistent: true
|
||||
})
|
||||
.onOk(() => {
|
||||
// NEW (no-cost): always persist draft so it can be resumed
|
||||
if (pageMode.value === 'new') {
|
||||
persistLocalDraftNow()
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// EDIT (has-cost): allow exit, keep draft for now (later we can clear it after successful save)
|
||||
clearLocalDraft()
|
||||
next()
|
||||
})
|
||||
.onCancel(() => next(false))
|
||||
|
||||
@@ -371,6 +371,12 @@ const routes = [
|
||||
component: () => import('pages/ProductionProductCostingMTBolumMapping.vue'),
|
||||
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 ================= */
|
||||
|
||||
171
ui/src/stores/productionProductCostingDefaultQtyStore.js
Normal file
171
ui/src/stores/productionProductCostingDefaultQtyStore.js
Normal 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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user