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",
|
"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",
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
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',
|
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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
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.
|
// 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))
|
||||||
|
|||||||
@@ -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 ================= */
|
||||||
|
|||||||
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