Merge remote-tracking branch 'origin/master'
This commit is contained in:
15
svc/main.go
15
svc/main.go
@@ -796,11 +796,26 @@ 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/has-cost-detail/last-detail", "POST",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingHasCostDetailLastDetailHandler)),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/production-product-costing/options/hammadde-by-nos", "POST",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOptionsHammaddeByNosHandler)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/production-product-costing/onml/save", "POST",
|
"/api/pricing/production-product-costing/onml/save", "POST",
|
||||||
"order", "view",
|
"order", "view",
|
||||||
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLSaveHandler)),
|
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLSaveHandler)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/production-product-costing/onml/delete", "POST",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLDeleteHandler)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/production-product-costing/default-quantities", "GET",
|
"/api/pricing/production-product-costing/default-quantities", "GET",
|
||||||
"order", "view",
|
"order", "view",
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ type ProductionProductCostingOnMLSaveDetailUpsertRow struct {
|
|||||||
FiyatDoviz string `json:"fiyat_doviz"`
|
FiyatDoviz string `json:"fiyat_doviz"`
|
||||||
MaliyeteDahil int `json:"maliyete_dahil"`
|
MaliyeteDahil int `json:"maliyete_dahil"`
|
||||||
CMPriceTypeID *int `json:"cm_price_type_id"`
|
CMPriceTypeID *int `json:"cm_price_type_id"`
|
||||||
|
SAciklama3 string `json:"s_aciklama3"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductionProductCostingOnMLSaveDetailDeleteRow struct {
|
type ProductionProductCostingOnMLSaveDetailDeleteRow struct {
|
||||||
@@ -170,6 +171,10 @@ type ProductionProductCostingOnMLSaveResponse struct {
|
|||||||
NOnMLNo int `json:"n_onml_no"`
|
NOnMLNo int `json:"n_onml_no"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductionProductCostingOnMLDeleteRequest struct {
|
||||||
|
NOnMLNo int `json:"n_onml_no"`
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
|
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -209,9 +214,39 @@ type ProductionProductCostingDefaultQtyLookupRequest struct {
|
|||||||
|
|
||||||
type ProductionProductCostingDefaultQtyLookupItem struct {
|
type ProductionProductCostingDefaultQtyLookupItem struct {
|
||||||
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
|
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
|
||||||
|
SAciklama string `json:"sAciklama"`
|
||||||
LDefaultMiktar float64 `json:"lDefaultMiktar"`
|
LDefaultMiktar float64 `json:"lDefaultMiktar"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductionProductCostingLastOnMLDetLookupRequest struct {
|
||||||
|
NHammaddeTuruNos []int `json:"nHammaddeTuruNos"`
|
||||||
|
BeforeDate string `json:"before_date"` // YYYY-MM-DD (optional)
|
||||||
|
ExcludeOnMLNo int `json:"exclude_onml_no"` // optional
|
||||||
|
NFirmaID int `json:"n_firma_id"` // optional
|
||||||
|
OnlyICode bool `json:"only_i_code"` // optional: restrict to sKodu like 'I.%'
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductionProductCostingLastOnMLDetLookupItem struct {
|
||||||
|
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
|
||||||
|
SKodu string `json:"sKodu"`
|
||||||
|
SAciklama string `json:"sAciklama"`
|
||||||
|
SBirim string `json:"sBirim"`
|
||||||
|
FiyatDoviz string `json:"fiyat_doviz"`
|
||||||
|
FiyatGirilen float64 `json:"fiyat_girilen"`
|
||||||
|
IsSameFirma bool `json:"is_same_firma"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductionProductCostingHammaddeByNosRequest struct {
|
||||||
|
NHammaddeTuruNos []int `json:"nHammaddeTuruNos"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductionProductCostingHammaddeByNosItem struct {
|
||||||
|
NHammaddeTuruNo int `json:"nHammaddeTuruNo"`
|
||||||
|
SAciklama string `json:"sAciklama"`
|
||||||
|
MTUrtMTBolumID int `json:"mtUrtMTBolumID"`
|
||||||
|
SParcaAdi string `json:"sParcaAdi"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProductionHasCostDetailExchangeRates struct {
|
type ProductionHasCostDetailExchangeRates struct {
|
||||||
RateDate string `json:"rateDate"`
|
RateDate string `json:"rateDate"`
|
||||||
TRYRate float64 `json:"tryRate"`
|
TRYRate float64 `json:"tryRate"`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package queries
|
package queries
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bssapp-backend/models"
|
||||||
"bssapp-backend/utils"
|
"bssapp-backend/utils"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
@@ -227,17 +228,47 @@ func UpsertV3ItemBasePriceUSD(
|
|||||||
return fmt.Errorf("missing params for base price upsert")
|
return fmt.Errorf("missing params for base price upsert")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: In this DB, PRIMARY KEY is on:
|
// 1. Find a CountryCode that is NOT yet used for this specific item in prItemBasePrice.
|
||||||
// (ItemTypeCode, ItemCode, CountryCode, SeasonCode, BasePriceCode)
|
// We query cdCountry for a code that has no matching entry in prItemBasePrice for this item.
|
||||||
// so we cannot insert multiple rows for different dates under the same base price.
|
// We exclude 'TR' to keep the original record safe.
|
||||||
// We update the single row's PriceDate/Price to reflect latest costing.
|
var targetCountry string
|
||||||
|
err := mssqlDB.QueryRowContext(ctx, `
|
||||||
|
SELECT TOP 1 C.CountryCode
|
||||||
|
FROM dbo.cdCountry C WITH (NOLOCK)
|
||||||
|
WHERE C.CountryCode <> 'TR'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM dbo.prItemBasePrice P WITH (NOLOCK)
|
||||||
|
WHERE P.ItemTypeCode = 1
|
||||||
|
AND LTRIM(RTRIM(P.ItemCode)) = LTRIM(RTRIM(@p1))
|
||||||
|
AND P.BasePriceCode = 1
|
||||||
|
AND P.CountryCode = C.CountryCode
|
||||||
|
)
|
||||||
|
ORDER BY NEWID() -- Randomly pick one of the available country codes
|
||||||
|
`, itemCode).Scan(&targetCountry)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
// Fallback: If ALL countries are exhausted (unlikely), default to 'AD' as a last resort.
|
||||||
|
targetCountry = "AD"
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to find available country code: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appUser := user
|
||||||
|
if strings.TrimSpace(appUser) == "" {
|
||||||
|
appUser = "BSSAPP"
|
||||||
|
} else {
|
||||||
|
appUser = "BSSAPP:" + strings.TrimSpace(appUser)
|
||||||
|
}
|
||||||
|
|
||||||
sqlText := `
|
sqlText := `
|
||||||
MERGE dbo.prItemBasePrice AS T
|
MERGE dbo.prItemBasePrice AS T
|
||||||
USING (
|
USING (
|
||||||
SELECT
|
SELECT
|
||||||
@p1 AS ItemTypeCode,
|
@p1 AS ItemTypeCode,
|
||||||
@p2 AS ItemCode,
|
@p2 AS ItemCode,
|
||||||
'TR' AS CountryCode,
|
@p6 AS CountryCode,
|
||||||
'' AS SeasonCode,
|
'' AS SeasonCode,
|
||||||
1 AS BasePriceCode,
|
1 AS BasePriceCode,
|
||||||
CONVERT(date, @p3, 23) AS PriceDate,
|
CONVERT(date, @p3, 23) AS PriceDate,
|
||||||
@@ -266,7 +297,7 @@ WHEN NOT MATCHED THEN
|
|||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err := mssqlDB.ExecContext(ctx, sqlText, 1, itemCode, priceDate, priceUSD, user)
|
_, err = mssqlDB.ExecContext(ctx, sqlText, 1, itemCode, priceDate, priceUSD, appUser, targetCountry)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1228,6 +1259,73 @@ ORDER BY
|
|||||||
return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike)
|
return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetProductionHammaddeByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]models.ProductionProductCostingHammaddeByNosItem, error) {
|
||||||
|
clean := make([]int, 0, len(nos))
|
||||||
|
seen := make(map[int]struct{}, len(nos))
|
||||||
|
for _, n := range nos {
|
||||||
|
if n <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[n]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[n] = struct{}{}
|
||||||
|
clean = append(clean, n)
|
||||||
|
}
|
||||||
|
if len(clean) == 0 {
|
||||||
|
return []models.ProductionProductCostingHammaddeByNosItem{}, nil
|
||||||
|
}
|
||||||
|
if len(clean) > 5000 {
|
||||||
|
return nil, fmt.Errorf("too many nos")
|
||||||
|
}
|
||||||
|
|
||||||
|
valueRows := make([]string, 0, len(clean))
|
||||||
|
args := make([]any, 0, len(clean))
|
||||||
|
for i, n := range clean {
|
||||||
|
valueRows = append(valueRows, "(@p"+strconv.Itoa(i+1)+")")
|
||||||
|
args = append(args, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlText := `
|
||||||
|
WITH req(nHammaddeTuruNo) AS (
|
||||||
|
SELECT DISTINCT v.nHammaddeTuruNo
|
||||||
|
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ISNULL(T.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
|
||||||
|
ISNULL(T.sAciklama, '') AS sAciklama,
|
||||||
|
ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
|
||||||
|
ISNULL(B.sAdi, '') AS sParcaAdi
|
||||||
|
FROM dbo.spUrtOnMLHammaddeTuru T WITH (NOLOCK)
|
||||||
|
INNER JOIN req R
|
||||||
|
ON R.nHammaddeTuruNo = T.nHammaddeTuruNo
|
||||||
|
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
|
||||||
|
ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
|
||||||
|
AND ISNULL(B.nUrtTipiID, 0) = 1
|
||||||
|
WHERE ISNULL(T.bAktif, 0) = 1
|
||||||
|
ORDER BY T.nHammaddeTuruNo;
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]models.ProductionProductCostingHammaddeByNosItem, 0, len(clean))
|
||||||
|
for rows.Next() {
|
||||||
|
var it models.ProductionProductCostingHammaddeByNosItem
|
||||||
|
if err := rows.Scan(&it.NHammaddeTuruNo, &it.SAciklama, &it.MTUrtMTBolumID, &it.SParcaAdi); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, it)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
|
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -1248,7 +1346,9 @@ SELECT TOP (@p3)
|
|||||||
ISNULL(H.sAciklama, '') AS sAciklama,
|
ISNULL(H.sAciklama, '') AS sAciklama,
|
||||||
ISNULL(V.lDefaultMiktar, 0) AS lDefaultMiktar,
|
ISNULL(V.lDefaultMiktar, 0) AS lDefaultMiktar,
|
||||||
CONVERT(VARCHAR(16), V.dteCalcTarihi, 120) AS dteCalcTarihi,
|
CONVERT(VARCHAR(16), V.dteCalcTarihi, 120) AS dteCalcTarihi,
|
||||||
CAST(CASE WHEN ISNULL(V.bAktif, 0) = 1 THEN 1 ELSE 0 END AS bit) AS bAktif
|
-- We intentionally don't depend on mk_MaliyetParcaEslestirme_vmiktarlar.bAktif
|
||||||
|
-- to keep this feature robust across schema changes; only active hammadde types are listed.
|
||||||
|
CAST(1 AS bit) AS bAktif
|
||||||
FROM dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK)
|
FROM dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK)
|
||||||
LEFT JOIN dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
|
LEFT JOIN dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
|
||||||
ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
|
ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
|
||||||
@@ -1264,26 +1364,16 @@ ORDER BY
|
|||||||
|
|
||||||
func UpsertProductionProductCostingDefaultQtyRow(ctx context.Context, uretimDB *sql.DB, nHammaddeTuruNo int, lDefaultMiktar float64, bAktif *bool) error {
|
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.
|
// 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 := `
|
sqlText := `
|
||||||
UPDATE dbo.mk_MaliyetParcaEslestirme_vmiktarlar
|
UPDATE dbo.mk_MaliyetParcaEslestirme_vmiktarlar
|
||||||
SET
|
SET
|
||||||
lDefaultMiktar = @p2,
|
lDefaultMiktar = @p2,
|
||||||
dteCalcTarihi = GETDATE(),
|
dteCalcTarihi = GETDATE()
|
||||||
bAktif = CASE WHEN @p3 < 0 THEN ISNULL(bAktif, 1) ELSE @p3 END
|
|
||||||
WHERE nHammaddeTuruNo = @p1;
|
WHERE nHammaddeTuruNo = @p1;
|
||||||
SELECT @@ROWCOUNT;
|
SELECT @@ROWCOUNT;
|
||||||
`
|
`
|
||||||
var affected int
|
var affected int
|
||||||
if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, lDefaultMiktar, activeVal).Scan(&affected); err != nil {
|
if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, lDefaultMiktar).Scan(&affected); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if affected == 0 {
|
if affected == 0 {
|
||||||
@@ -1333,11 +1423,10 @@ USING agg AS S
|
|||||||
WHEN MATCHED THEN
|
WHEN MATCHED THEN
|
||||||
UPDATE SET
|
UPDATE SET
|
||||||
T.lDefaultMiktar = S.lDefaultMiktar,
|
T.lDefaultMiktar = S.lDefaultMiktar,
|
||||||
T.dteCalcTarihi = GETDATE(),
|
T.dteCalcTarihi = GETDATE()
|
||||||
T.bAktif = 1
|
|
||||||
WHEN NOT MATCHED THEN
|
WHEN NOT MATCHED THEN
|
||||||
INSERT (nHammaddeTuruNo, lDefaultMiktar, dteCalcTarihi, bAktif)
|
INSERT (nHammaddeTuruNo, lDefaultMiktar, dteCalcTarihi)
|
||||||
VALUES (S.nHammaddeTuruNo, S.lDefaultMiktar, GETDATE(), 1);
|
VALUES (S.nHammaddeTuruNo, S.lDefaultMiktar, GETDATE());
|
||||||
`
|
`
|
||||||
_, err := uretimDB.ExecContext(ctx, sqlText, topN)
|
_, err := uretimDB.ExecContext(ctx, sqlText, topN)
|
||||||
return err
|
return err
|
||||||
@@ -1377,7 +1466,11 @@ FROM ranked;
|
|||||||
return avg, cnt, nil
|
return avg, cnt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB *sql.DB, nos []int) (map[int]float64, error) {
|
func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]struct {
|
||||||
|
No int
|
||||||
|
Aciklama string
|
||||||
|
Qty float64
|
||||||
|
}, error) {
|
||||||
clean := make([]int, 0, len(nos))
|
clean := make([]int, 0, len(nos))
|
||||||
seen := make(map[int]struct{}, len(nos))
|
seen := make(map[int]struct{}, len(nos))
|
||||||
for _, n := range nos {
|
for _, n := range nos {
|
||||||
@@ -1391,7 +1484,11 @@ func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB
|
|||||||
clean = append(clean, n)
|
clean = append(clean, n)
|
||||||
}
|
}
|
||||||
if len(clean) == 0 {
|
if len(clean) == 0 {
|
||||||
return map[int]float64{}, nil
|
return []struct {
|
||||||
|
No int
|
||||||
|
Aciklama string
|
||||||
|
Qty float64
|
||||||
|
}{}, nil
|
||||||
}
|
}
|
||||||
if len(clean) > 1000 {
|
if len(clean) > 1000 {
|
||||||
return nil, fmt.Errorf("too many hammadde nos")
|
return nil, fmt.Errorf("too many hammadde nos")
|
||||||
@@ -1409,8 +1506,9 @@ WITH req(nHammaddeTuruNo) AS (
|
|||||||
SELECT DISTINCT v.nHammaddeTuruNo
|
SELECT DISTINCT v.nHammaddeTuruNo
|
||||||
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
|
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
ISNULL(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
|
ISNULL(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
|
||||||
|
ISNULL(H.sAciklama, '') AS sAciklama,
|
||||||
CAST(ISNULL(V.lDefaultMiktar, 0) AS FLOAT) AS lDefaultMiktar
|
CAST(ISNULL(V.lDefaultMiktar, 0) AS FLOAT) AS lDefaultMiktar
|
||||||
FROM req R
|
FROM req R
|
||||||
INNER JOIN dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK)
|
INNER JOIN dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK)
|
||||||
@@ -1426,15 +1524,24 @@ WHERE ISNULL(H.bAktif, 0) = 1;
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
out := make(map[int]float64, len(clean))
|
out := make([]struct {
|
||||||
|
No int
|
||||||
|
Aciklama string
|
||||||
|
Qty float64
|
||||||
|
}, 0, len(clean))
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var no int
|
var no int
|
||||||
|
var aciklama sql.NullString
|
||||||
var qty float64
|
var qty float64
|
||||||
if err := rows.Scan(&no, &qty); err != nil {
|
if err := rows.Scan(&no, &aciklama, &qty); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if no > 0 && qty > 0 {
|
if no > 0 && qty > 0 {
|
||||||
out[no] = qty
|
out = append(out, struct {
|
||||||
|
No int
|
||||||
|
Aciklama string
|
||||||
|
Qty float64
|
||||||
|
}{No: no, Aciklama: strings.TrimSpace(aciklama.String), Qty: qty})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
@@ -1443,6 +1550,128 @@ WHERE ISNULL(H.bAktif, 0) = 1;
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LookupLastOnMLMasDetByHammaddeNos(
|
||||||
|
ctx context.Context,
|
||||||
|
uretimDB *sql.DB,
|
||||||
|
nos []int,
|
||||||
|
beforeDate string, // YYYY-MM-DD optional
|
||||||
|
excludeOnMLNo int,
|
||||||
|
nFirmaID int,
|
||||||
|
onlyICode bool,
|
||||||
|
limitPerType int,
|
||||||
|
) ([]models.ProductionProductCostingLastOnMLDetLookupItem, error) {
|
||||||
|
if limitPerType <= 0 {
|
||||||
|
limitPerType = 1
|
||||||
|
}
|
||||||
|
if limitPerType > 1 {
|
||||||
|
limitPerType = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
clean := make([]int, 0, len(nos))
|
||||||
|
seen := make(map[int]struct{}, len(nos))
|
||||||
|
for _, n := range nos {
|
||||||
|
if n <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[n]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[n] = struct{}{}
|
||||||
|
clean = append(clean, n)
|
||||||
|
}
|
||||||
|
if len(clean) == 0 {
|
||||||
|
return []models.ProductionProductCostingLastOnMLDetLookupItem{}, nil
|
||||||
|
}
|
||||||
|
if len(clean) > 500 {
|
||||||
|
return nil, fmt.Errorf("too many hammadde nos")
|
||||||
|
}
|
||||||
|
|
||||||
|
valueRows := make([]string, 0, len(clean))
|
||||||
|
args := make([]any, 0, len(clean)+2)
|
||||||
|
for i, n := range clean {
|
||||||
|
valueRows = append(valueRows, "(@p"+strconv.Itoa(i+1)+")")
|
||||||
|
args = append(args, n)
|
||||||
|
}
|
||||||
|
// extra params at the end
|
||||||
|
args = append(args, strings.TrimSpace(beforeDate))
|
||||||
|
args = append(args, excludeOnMLNo)
|
||||||
|
args = append(args, nFirmaID)
|
||||||
|
if onlyICode {
|
||||||
|
args = append(args, 1)
|
||||||
|
} else {
|
||||||
|
args = append(args, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeParam := "@p" + strconv.Itoa(len(clean)+1)
|
||||||
|
excludeParam := "@p" + strconv.Itoa(len(clean)+2)
|
||||||
|
firmaParam := "@p" + strconv.Itoa(len(clean)+3)
|
||||||
|
onlyIParam := "@p" + strconv.Itoa(len(clean)+4)
|
||||||
|
|
||||||
|
sqlText := `
|
||||||
|
WITH req(nHammaddeTuruNo) AS (
|
||||||
|
SELECT DISTINCT v.nHammaddeTuruNo
|
||||||
|
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
|
||||||
|
),
|
||||||
|
ranked AS (
|
||||||
|
SELECT
|
||||||
|
D.nHammaddeTuruNo,
|
||||||
|
LTRIM(RTRIM(ISNULL(D.sKodu, ''))) AS sKodu,
|
||||||
|
LTRIM(RTRIM(ISNULL(D.sAciklama, ''))) AS sAciklama,
|
||||||
|
LTRIM(RTRIM(ISNULL(D.sBirim, ''))) AS sBirim,
|
||||||
|
LTRIM(RTRIM(ISNULL(D.fiyat_doviz, ''))) AS fiyat_doviz,
|
||||||
|
CAST(ISNULL(D.fiyat_girilen, 0) AS FLOAT) AS fiyat_girilen,
|
||||||
|
CAST(CASE WHEN ISNULL(` + firmaParam + `, 0) > 0 AND ISNULL(M.nFirmaID, 0) = ISNULL(` + firmaParam + `, 0) THEN 1 ELSE 0 END AS bit) AS is_same_firma,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY D.nHammaddeTuruNo
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN ISNULL(` + firmaParam + `, 0) > 0 AND ISNULL(M.nFirmaID, 0) = ISNULL(` + firmaParam + `, 0) THEN 0 ELSE 1 END,
|
||||||
|
COALESCE(M.Tarihi, M.dteKayitTarihi) DESC,
|
||||||
|
M.nOnMLNo DESC,
|
||||||
|
D.nOnMLDetNo DESC
|
||||||
|
) AS rn
|
||||||
|
FROM dbo.spUrtOnMLMasDet D WITH (NOLOCK)
|
||||||
|
INNER JOIN dbo.spUrtOnMLMas M WITH (NOLOCK)
|
||||||
|
ON M.nOnMLNo = D.nOnMLNo
|
||||||
|
INNER JOIN req R
|
||||||
|
ON R.nHammaddeTuruNo = D.nHammaddeTuruNo
|
||||||
|
WHERE ISNULL(D.fiyat_girilen, 0) > 0
|
||||||
|
AND LTRIM(RTRIM(ISNULL(D.sKodu, ''))) <> ''
|
||||||
|
AND (ISNULL(` + onlyIParam + `, 0) = 0 OR LTRIM(RTRIM(ISNULL(D.sKodu, ''))) LIKE 'I.%')
|
||||||
|
AND (` + beforeParam + ` = '' OR CONVERT(date, COALESCE(M.Tarihi, M.dteKayitTarihi)) < CONVERT(date, ` + beforeParam + `, 23))
|
||||||
|
AND (ISNULL(` + excludeParam + `, 0) <= 0 OR M.nOnMLNo <> ` + excludeParam + `)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ISNULL(nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
|
||||||
|
ISNULL(sKodu, '') AS sKodu,
|
||||||
|
ISNULL(sAciklama, '') AS sAciklama,
|
||||||
|
ISNULL(sBirim, '') AS sBirim,
|
||||||
|
ISNULL(fiyat_doviz, '') AS fiyat_doviz,
|
||||||
|
ISNULL(fiyat_girilen, 0) AS fiyat_girilen,
|
||||||
|
CAST(ISNULL(is_same_firma, 0) AS bit) AS is_same_firma
|
||||||
|
FROM ranked
|
||||||
|
WHERE rn = 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]models.ProductionProductCostingLastOnMLDetLookupItem, 0, len(clean))
|
||||||
|
for rows.Next() {
|
||||||
|
var item models.ProductionProductCostingLastOnMLDetLookupItem
|
||||||
|
if err := rows.Scan(&item.NHammaddeTuruNo, &item.SKodu, &item.SAciklama, &item.SBirim, &item.FiyatDoviz, &item.FiyatGirilen, &item.IsSameFirma); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func buildSQLServerFullTextPrefixQuery(search string) string {
|
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))
|
||||||
|
|||||||
@@ -496,6 +496,13 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep mk_mail in sync for downstream mail mapping screens.
|
||||||
|
if err := ensureMkMail(tx, payload.Email); err != nil {
|
||||||
|
log.Printf("USER CREATE ensureMkMail ERROR user_id=%d email=%q err=%v", newID, payload.Email, err)
|
||||||
|
http.Error(w, "Mail kaydi guncellenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ROLES
|
// ROLES
|
||||||
for _, role := range payload.Roles {
|
for _, role := range payload.Roles {
|
||||||
role = strings.TrimSpace(role)
|
role = strings.TrimSpace(role)
|
||||||
|
|||||||
68
svc/routes/mk_mail_helper.go
Normal file
68
svc/routes/mk_mail_helper.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/utils"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var simpleEmailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
|
||||||
|
|
||||||
|
// ensureMkMail makes sure the given email exists in mk_mail.
|
||||||
|
// If it already exists (case-insensitive match), it is re-activated and normalized.
|
||||||
|
// This is intentionally tolerant (no ON CONFLICT) to avoid relying on a specific unique constraint.
|
||||||
|
func ensureMkMail(tx *sql.Tx, email string) error {
|
||||||
|
mail := strings.ToLower(strings.TrimSpace(email))
|
||||||
|
if mail == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !simpleEmailRe.MatchString(mail) {
|
||||||
|
// user email field can be free-form; don't hard fail user save because of mk_mail bookkeeping
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var id string
|
||||||
|
err := tx.QueryRow(`
|
||||||
|
SELECT m.id::text
|
||||||
|
FROM mk_mail m
|
||||||
|
WHERE LOWER(TRIM(m.email)) = LOWER(TRIM($1))
|
||||||
|
LIMIT 1
|
||||||
|
`, mail).Scan(&id)
|
||||||
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
newID := utils.NewUUID()
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
INSERT INTO mk_mail (
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
display_name,
|
||||||
|
"type",
|
||||||
|
is_primary,
|
||||||
|
external_id,
|
||||||
|
is_active,
|
||||||
|
created_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, '', 'user', true, true, true, NOW())
|
||||||
|
`, newID, mail)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists: normalize + activate. Avoid touching created_at.
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
UPDATE mk_mail
|
||||||
|
SET
|
||||||
|
email = $2,
|
||||||
|
display_name = COALESCE(display_name, ''),
|
||||||
|
"type" = 'user',
|
||||||
|
is_primary = true,
|
||||||
|
external_id = true,
|
||||||
|
is_active = true
|
||||||
|
WHERE id::text = $1
|
||||||
|
`, id, mail)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -327,6 +327,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
groupName string
|
groupName string
|
||||||
groupTotal float64
|
groupTotal float64
|
||||||
groupTotalUSD float64
|
groupTotalUSD float64
|
||||||
|
nOnMLNoStr string
|
||||||
|
nOnMLDetNoStr string
|
||||||
|
hNoStr string
|
||||||
fiyatGirilen sql.NullFloat64
|
fiyatGirilen sql.NullFloat64
|
||||||
fiyatDoviz sql.NullString
|
fiyatDoviz sql.NullString
|
||||||
maliyeteDahil sql.NullBool
|
maliyeteDahil sql.NullBool
|
||||||
@@ -338,9 +341,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
&groupName,
|
&groupName,
|
||||||
&groupTotal,
|
&groupTotal,
|
||||||
&groupTotalUSD,
|
&groupTotalUSD,
|
||||||
&item.NOnMLNo,
|
&nOnMLNoStr,
|
||||||
&item.NOnMLDetNo,
|
&nOnMLDetNoStr,
|
||||||
&item.NHammaddeTuruNo,
|
&hNoStr,
|
||||||
&item.SKodu,
|
&item.SKodu,
|
||||||
&item.SAciklama,
|
&item.SAciklama,
|
||||||
&item.SRenk,
|
&item.SRenk,
|
||||||
@@ -371,6 +374,10 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
scannedRows += 1
|
scannedRows += 1
|
||||||
|
|
||||||
|
item.NOnMLNo, _ = strconv.Atoi(nOnMLNoStr)
|
||||||
|
item.NOnMLDetNo, _ = strconv.Atoi(nOnMLDetNoStr)
|
||||||
|
item.NHammaddeTuruNo, _ = strconv.Atoi(hNoStr)
|
||||||
|
|
||||||
if fiyatGirilen.Valid {
|
if fiyatGirilen.Valid {
|
||||||
item.FiyatGirilen = new(float64)
|
item.FiyatGirilen = new(float64)
|
||||||
*item.FiyatGirilen = fiyatGirilen.Float64
|
*item.FiyatGirilen = fiyatGirilen.Float64
|
||||||
@@ -426,8 +433,8 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo)
|
rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("query error", "err", err)
|
logger.Error("query error", "err", err)
|
||||||
log.Printf("⌠[ProductionHasCostDetailGroups] query error: %v", err)
|
log.Printf("❌ [ProductionHasCostDetailGroups] query error: %v", err)
|
||||||
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
|
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@@ -442,6 +449,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
groupName string
|
groupName string
|
||||||
groupTotal float64
|
groupTotal float64
|
||||||
groupTotalUSD float64
|
groupTotalUSD float64
|
||||||
|
nOnMLNoStr string
|
||||||
|
nOnMLDetNoStr string
|
||||||
|
hNoStr string
|
||||||
fiyatGirilen sql.NullFloat64
|
fiyatGirilen sql.NullFloat64
|
||||||
fiyatDoviz sql.NullString
|
fiyatDoviz sql.NullString
|
||||||
maliyeteDahil sql.NullBool
|
maliyeteDahil sql.NullBool
|
||||||
@@ -453,9 +463,9 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
&groupName,
|
&groupName,
|
||||||
&groupTotal,
|
&groupTotal,
|
||||||
&groupTotalUSD,
|
&groupTotalUSD,
|
||||||
&item.NOnMLNo,
|
&nOnMLNoStr,
|
||||||
&item.NOnMLDetNo,
|
&nOnMLDetNoStr,
|
||||||
&item.NHammaddeTuruNo,
|
&hNoStr,
|
||||||
&item.SKodu,
|
&item.SKodu,
|
||||||
&item.SAciklama,
|
&item.SAciklama,
|
||||||
&item.SRenk,
|
&item.SRenk,
|
||||||
@@ -479,10 +489,16 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
&item.SHammaddeTuruAdi,
|
&item.SHammaddeTuruAdi,
|
||||||
&item.SParcaAdi,
|
&item.SParcaAdi,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Printf("âš ï¸ [ProductionHasCostDetailGroups] scan error: %v", err)
|
scanErrors++
|
||||||
|
logger.Warn("scan error", "scan_index", scannedRows+scanErrors, "err", err)
|
||||||
|
log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
scannedRows += 1
|
scannedRows++
|
||||||
|
|
||||||
|
item.NOnMLNo, _ = strconv.Atoi(nOnMLNoStr)
|
||||||
|
item.NOnMLDetNo, _ = strconv.Atoi(nOnMLDetNoStr)
|
||||||
|
item.NHammaddeTuruNo, _ = strconv.Atoi(hNoStr)
|
||||||
|
|
||||||
if fiyatGirilen.Valid {
|
if fiyatGirilen.Valid {
|
||||||
item.FiyatGirilen = new(float64)
|
item.FiyatGirilen = new(float64)
|
||||||
@@ -1093,21 +1109,74 @@ func PostProductionProductCostingDefaultQuantitiesLookupHandler(w http.ResponseW
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos)
|
rows, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(m))
|
out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(rows))
|
||||||
for no, qty := range m {
|
for _, r0 := range rows {
|
||||||
out = append(out, models.ProductionProductCostingDefaultQtyLookupItem{
|
out = append(out, models.ProductionProductCostingDefaultQtyLookupItem{
|
||||||
NHammaddeTuruNo: no,
|
NHammaddeTuruNo: r0.No,
|
||||||
LDefaultMiktar: qty,
|
SAciklama: r0.Aciklama,
|
||||||
|
LDefaultMiktar: r0.Qty,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ = json.NewEncoder(w).Encode(out)
|
_ = json.NewEncoder(w).Encode(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /api/pricing/production-product-costing/has-cost-detail/last-detail
|
||||||
|
func PostProductionProductCostingHasCostDetailLastDetailHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
uretimDB := db.GetUretimDB()
|
||||||
|
if uretimDB == nil {
|
||||||
|
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
|
||||||
|
var req models.ProductionProductCostingLastOnMLDetLookupRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := queries.LookupLastOnMLMasDetByHammaddeNos(ctx, uretimDB, req.NHammaddeTuruNos, req.BeforeDate, req.ExcludeOnMLNo, req.NFirmaID, req.OnlyICode, 1)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/pricing/production-product-costing/options/hammadde-by-nos
|
||||||
|
func PostProductionProductCostingOptionsHammaddeByNosHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
uretimDB := db.GetUretimDB()
|
||||||
|
if uretimDB == nil {
|
||||||
|
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
|
||||||
|
var req models.ProductionProductCostingHammaddeByNosRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := queries.GetProductionHammaddeByNos(ctx, uretimDB, req.NHammaddeTuruNos)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(rows)
|
||||||
|
}
|
||||||
|
|
||||||
// POST /api/pricing/production-product-costing/default-quantities/refresh
|
// POST /api/pricing/production-product-costing/default-quantities/refresh
|
||||||
func PostProductionProductCostingDefaultQuantitiesRefreshHandler(w http.ResponseWriter, r *http.Request) {
|
func PostProductionProductCostingDefaultQuantitiesRefreshHandler(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")
|
||||||
@@ -1166,6 +1235,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
req.Header.FirmaKodu = strings.TrimSpace(req.Header.FirmaKodu)
|
req.Header.FirmaKodu = strings.TrimSpace(req.Header.FirmaKodu)
|
||||||
|
|
||||||
if req.Header.UrunKodu == "" || req.Header.MaliyetTarihi == "" {
|
if req.Header.UrunKodu == "" || req.Header.MaliyetTarihi == "" {
|
||||||
|
logger.Warn("validation failed: missing required header fields",
|
||||||
|
"trace_id", traceID,
|
||||||
|
"urun_kodu", req.Header.UrunKodu,
|
||||||
|
"maliyet_tarihi", req.Header.MaliyetTarihi,
|
||||||
|
)
|
||||||
http.Error(w, "urun_kodu ve maliyet_tarihi zorunlu", http.StatusBadRequest)
|
http.Error(w, "urun_kodu ve maliyet_tarihi zorunlu", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1210,6 +1284,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
firmaID = id
|
firmaID = id
|
||||||
}
|
}
|
||||||
if firmaID <= 0 {
|
if firmaID <= 0 {
|
||||||
|
logger.Warn("validation failed: missing firma",
|
||||||
|
"trace_id", traceID,
|
||||||
|
"n_firma_id", req.Header.NFirmaID,
|
||||||
|
"firma_kodu", req.Header.FirmaKodu,
|
||||||
|
)
|
||||||
http.Error(w, "Firma secilmeden kaydedilemez (n_firma_id / firma_kodu)", http.StatusBadRequest)
|
http.Error(w, "Firma secilmeden kaydedilemez (n_firma_id / firma_kodu)", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1264,6 +1343,11 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
// Parse Tarihi
|
// Parse Tarihi
|
||||||
tarihi, err := time.Parse("2006-01-02", req.Header.MaliyetTarihi)
|
tarihi, err := time.Parse("2006-01-02", req.Header.MaliyetTarihi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Warn("validation failed: maliyet_tarihi parse error",
|
||||||
|
"trace_id", traceID,
|
||||||
|
"maliyet_tarihi", req.Header.MaliyetTarihi,
|
||||||
|
"err", err,
|
||||||
|
)
|
||||||
http.Error(w, "maliyet_tarihi YYYY-MM-DD formatinda olmali", http.StatusBadRequest)
|
http.Error(w, "maliyet_tarihi YYYY-MM-DD formatinda olmali", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1274,7 +1358,23 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
http.Error(w, "Islem baslatilamadi", http.StatusInternalServerError)
|
http.Error(w, "Islem baslatilamadi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = tx.Rollback() }()
|
committed := false
|
||||||
|
logger.Info("tx begin",
|
||||||
|
"trace_id", traceID,
|
||||||
|
"n_onml_no", nOnMLNo,
|
||||||
|
"urun_kodu", req.Header.UrunKodu,
|
||||||
|
"maliyet_tarihi", req.Header.MaliyetTarihi,
|
||||||
|
)
|
||||||
|
defer func() {
|
||||||
|
if committed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := tx.Rollback(); err == nil {
|
||||||
|
logger.Info("tx rollback", "trace_id", traceID, "n_onml_no", nOnMLNo)
|
||||||
|
} else {
|
||||||
|
logger.Warn("tx rollback failed", "trace_id", traceID, "n_onml_no", nOnMLNo, "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Determine mamul turu inside same tx (to keep create atomic)
|
// Determine mamul turu inside same tx (to keep create atomic)
|
||||||
mamulLabel := ""
|
mamulLabel := ""
|
||||||
@@ -1299,6 +1399,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if mt <= 0 {
|
if mt <= 0 {
|
||||||
|
logger.Warn("validation failed: mamul turu not found", "trace_id", traceID, "n_onml_no", nOnMLNo, "mamul_label", mamulLabel)
|
||||||
http.Error(w, "Mamul turu bulunamadi: "+mamulLabel, http.StatusBadRequest)
|
http.Error(w, "Mamul turu bulunamadi: "+mamulLabel, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1320,6 +1421,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
sAciklama = sql.NullString{String: req.Header.SAciklama, Valid: true}
|
sAciklama = sql.NullString{String: req.Header.SAciklama, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "upsert_header")
|
||||||
if err := queries.UpsertOnMLHeader(tx, ctx, queries.OnMLHeaderUpsertArgs{
|
if err := queries.UpsertOnMLHeader(tx, ctx, queries.OnMLHeaderUpsertArgs{
|
||||||
NOnMLNo: nOnMLNo,
|
NOnMLNo: nOnMLNo,
|
||||||
UrunKodu: req.Header.UrunKodu,
|
UrunKodu: req.Header.UrunKodu,
|
||||||
@@ -1343,6 +1445,7 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deletes
|
// Deletes
|
||||||
|
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_deletes", "count", len(req.Detail.Deletes))
|
||||||
for _, d := range req.Detail.Deletes {
|
for _, d := range req.Detail.Deletes {
|
||||||
if d.NOnMLDetNo <= 0 {
|
if d.NOnMLDetNo <= 0 {
|
||||||
continue
|
continue
|
||||||
@@ -1355,14 +1458,34 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upserts
|
// Upserts
|
||||||
|
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_upserts", "count", len(req.Detail.Upserts))
|
||||||
for _, row := range req.Detail.Upserts {
|
for _, row := range req.Detail.Upserts {
|
||||||
if row.NOnMLDetNo <= 0 {
|
if row.NOnMLDetNo <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
|
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
|
||||||
http.Error(w, "Detay satirinda n_hammadde_turu_no ve s_kodu zorunlu", http.StatusBadRequest)
|
// FALLBACK: If nHammaddeTuruNo is missing but sKodu is present, default to 1 (General/Labor)
|
||||||
|
// to avoid blocking the user, especially for labor items.
|
||||||
|
if strings.TrimSpace(row.SKodu) != "" && row.NHammaddeTuruNo <= 0 {
|
||||||
|
logger.Warn("n_hammadde_turu_no <= 0, falling back to 1",
|
||||||
|
"trace_id", traceID,
|
||||||
|
"n_onml_no", nOnMLNo,
|
||||||
|
"n_onml_det_no", row.NOnMLDetNo,
|
||||||
|
"s_kodu", strings.TrimSpace(row.SKodu),
|
||||||
|
)
|
||||||
|
row.NHammaddeTuruNo = 1
|
||||||
|
} else {
|
||||||
|
logger.Warn("validation failed: missing required detail fields (s_kodu empty)",
|
||||||
|
"trace_id", traceID,
|
||||||
|
"n_onml_no", nOnMLNo,
|
||||||
|
"n_onml_det_no", row.NOnMLDetNo,
|
||||||
|
"n_hammadde_turu_no", row.NHammaddeTuruNo,
|
||||||
|
"s_kodu", strings.TrimSpace(row.SKodu),
|
||||||
|
)
|
||||||
|
http.Error(w, "Detay satirinda s_kodu zorunlu", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
qty := row.LMiktar
|
qty := row.LMiktar
|
||||||
if qty < 0 {
|
if qty < 0 {
|
||||||
qty = 0
|
qty = 0
|
||||||
@@ -1387,9 +1510,21 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
lTutar := unitTRY * qty
|
lTutar := unitTRY * qty
|
||||||
lDovizTutari := unitUSD * qty
|
lDovizTutari := unitUSD * qty
|
||||||
|
|
||||||
|
// Debug log for price resolution
|
||||||
|
logger.Info("price debug",
|
||||||
|
"s_kodu", strings.TrimSpace(row.SKodu),
|
||||||
|
"qty", qty,
|
||||||
|
"fiyat_girilen", row.FiyatGirilen,
|
||||||
|
"fiyat_doviz", strings.TrimSpace(row.FiyatDoviz),
|
||||||
|
"unitTRY", unitTRY,
|
||||||
|
"lTutar", lTutar,
|
||||||
|
"lDovizTutari", lDovizTutari,
|
||||||
|
)
|
||||||
|
|
||||||
// Resolve stock type id from tbStok by sKodu (exact), then fallback to model-based match.
|
// 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).
|
// Note: In this DB, stock type is stored as tbStok.nStokTipi but spUrtOnMLMasDet expects nStokTipiID (int).
|
||||||
rawSKodu := strings.TrimSpace(row.SKodu)
|
rawSKodu := strings.TrimSpace(row.SKodu)
|
||||||
|
logger.Info("resolving stock type", "s_kodu", rawSKodu)
|
||||||
var nStokTipiID int
|
var nStokTipiID int
|
||||||
err := tx.QueryRowContext(ctx, `
|
err := tx.QueryRowContext(ctx, `
|
||||||
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
|
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
|
||||||
@@ -1411,16 +1546,32 @@ ORDER BY
|
|||||||
`, rawSKodu).Scan(&nStokTipiID)
|
`, rawSKodu).Scan(&nStokTipiID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
|
// FALLBACK: If stock item not found in tbStok at all, default to 1.
|
||||||
return
|
logger.Warn("stok tipi not found in tbStok, falling back to 1",
|
||||||
}
|
"trace_id", traceID,
|
||||||
|
"n_onml_no", nOnMLNo,
|
||||||
|
"n_onml_det_no", row.NOnMLDetNo,
|
||||||
|
"s_kodu", rawSKodu,
|
||||||
|
)
|
||||||
|
nStokTipiID = 1
|
||||||
|
} else {
|
||||||
logger.Error("stok tipi lookup error", "err", err)
|
logger.Error("stok tipi lookup error", "err", err)
|
||||||
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
|
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
logger.Info("stock type resolved", "s_kodu", rawSKodu, "n_stok_tipi_id", nStokTipiID)
|
||||||
if nStokTipiID <= 0 {
|
if nStokTipiID <= 0 {
|
||||||
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
|
// FALLBACK: If stock type is missing or 0 in tbStok, default to 1 (usually 'Raw Material' or 'General').
|
||||||
return
|
// This prevents blocking the save process for items not fully configured in tbStok.
|
||||||
|
logger.Warn("stok tipi <= 0, falling back to 1",
|
||||||
|
"trace_id", traceID,
|
||||||
|
"n_onml_no", nOnMLNo,
|
||||||
|
"n_onml_det_no", row.NOnMLDetNo,
|
||||||
|
"s_kodu", rawSKodu,
|
||||||
|
"n_stok_tipi_id_orig", nStokTipiID,
|
||||||
|
)
|
||||||
|
nStokTipiID = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy/system-mapped required fields:
|
// Dummy/system-mapped required fields:
|
||||||
@@ -1469,7 +1620,8 @@ WHEN MATCHED THEN
|
|||||||
Maliyete_dahil = @p29,
|
Maliyete_dahil = @p29,
|
||||||
cm_price_type_id = @p30,
|
cm_price_type_id = @p30,
|
||||||
sKullaniciAdiDeg = @p31,
|
sKullaniciAdiDeg = @p31,
|
||||||
dteIslemTarihiDeg = GETDATE()
|
dteIslemTarihiDeg = GETDATE(),
|
||||||
|
sAciklama3 = NULLIF(@p32, '')
|
||||||
WHEN NOT MATCHED THEN
|
WHEN NOT MATCHED THEN
|
||||||
INSERT (
|
INSERT (
|
||||||
nOnMLNo,nOnMLDetNo,nHammaddeTuruNo,sKodu,sAciklama,sRenk,lMiktar,lFiyat,lTutar,
|
nOnMLNo,nOnMLDetNo,nHammaddeTuruNo,sKodu,sAciklama,sRenk,lMiktar,lFiyat,lTutar,
|
||||||
@@ -1480,7 +1632,7 @@ WHEN NOT MATCHED THEN
|
|||||||
VALUES (
|
VALUES (
|
||||||
@p1,@p2,@p3,@p4,@p5,@p6,@p9,@p10,@p11,
|
@p1,@p2,@p3,@p4,@p5,@p6,@p9,@p10,@p11,
|
||||||
@p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19,
|
@p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19,
|
||||||
@p20,NULLIF(@p8,''),@p21,@p22,@p31,GETDATE(),NULLIF(@p7,''),NULL,@p23,@p24,
|
@p20,NULLIF(@p8,''),@p21,@p22,@p31,GETDATE(),NULLIF(@p7,''),NULLIF(@p32, ''),@p23,@p24,
|
||||||
@p25,@p26,NULLIF(@p27,0),NULLIF(@p28,''),@p29,@p30
|
@p25,@p26,NULLIF(@p27,0),NULLIF(@p28,''),@p29,@p30
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
@@ -1519,8 +1671,21 @@ WHEN NOT MATCHED THEN
|
|||||||
row.MaliyeteDahil,
|
row.MaliyeteDahil,
|
||||||
row.CMPriceTypeID,
|
row.CMPriceTypeID,
|
||||||
user,
|
user,
|
||||||
|
strings.TrimSpace(row.SAciklama3), // p32: sAciklama3 (Grup Adi)
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Error("detail merge error", "err", err)
|
logger.Error("detail merge error",
|
||||||
|
"trace_id", traceID,
|
||||||
|
"n_onml_no", nOnMLNo,
|
||||||
|
"n_onml_det_no", row.NOnMLDetNo,
|
||||||
|
"n_hammadde_turu_no", row.NHammaddeTuruNo,
|
||||||
|
"n_urt_mt_bolum_id", row.NUrtMTBolumID,
|
||||||
|
"s_kodu", strings.TrimSpace(row.SKodu),
|
||||||
|
"fiyat_girilen", row.FiyatGirilen,
|
||||||
|
"fiyat_doviz", strings.TrimSpace(row.FiyatDoviz),
|
||||||
|
"l_miktar", qty,
|
||||||
|
"n_stok_tipi_id", nStokTipiID,
|
||||||
|
"err", err,
|
||||||
|
)
|
||||||
http.Error(w, "Detay kaydedilemedi", http.StatusInternalServerError)
|
http.Error(w, "Detay kaydedilemedi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1533,6 +1698,7 @@ WHEN NOT MATCHED THEN
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
if req.Header.NUrtReceteID > 0 {
|
if req.Header.NUrtReceteID > 0 {
|
||||||
receteID := req.Header.NUrtReceteID
|
receteID := req.Header.NUrtReceteID
|
||||||
|
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "recipe_sync", "n_urt_recete_id", receteID)
|
||||||
|
|
||||||
// Determine next available recipe detail id (nUrtRecMBolumID)
|
// Determine next available recipe detail id (nUrtRecMBolumID)
|
||||||
nextRecDetID := 0
|
nextRecDetID := 0
|
||||||
@@ -1551,94 +1717,72 @@ WHERE RMik.nUrtReceteID = @p1
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// CM1/CM2 rows are cost-only and must NOT be written back into recipe tables.
|
// 1. FILTER: CM1/CM2 (Labor/Service) rows must NOT be written back into recipe tables.
|
||||||
// Source of truth: spUrtOnMLHammaddeTuru.sAciklama3 (e.g. 'CM2').
|
// We check the group label (sAciklama3) from the row itself.
|
||||||
var grp sql.NullString
|
g := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
|
||||||
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" {
|
if g == "CM1" || g == "CM2" {
|
||||||
|
logger.Info("recipe sync skip: labor item", "s_kodu", row.SKodu, "group", g)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve nHStokID from tbStok using sKodu (exact), then sModel fallback.
|
// In this version of URETIM DB, the table name is dbo.spUrtRecMBolumMik
|
||||||
// IMPORTANT: nHStokID must be resolved; otherwise we'd update/insert too broadly.
|
// but it uses _G suffixes for quantity and amount columns.
|
||||||
|
// Also nHStokID_G stores the stock code string rather than just an ID.
|
||||||
rawSKodu := strings.TrimSpace(row.SKodu)
|
rawSKodu := strings.TrimSpace(row.SKodu)
|
||||||
var nHStokID int
|
|
||||||
err := tx.QueryRowContext(ctx, `
|
// Ensure a section entry (spUrtRecMBolum) exists for this hNo (Hammadde Turu)
|
||||||
SELECT TOP 1 ISNULL(S.nStokID, 0)
|
// in the current recipe, otherwise detail rows (Mik) cannot be linked properly.
|
||||||
FROM dbo.tbStok S WITH (NOLOCK)
|
var sectionExists int
|
||||||
WHERE ISNULL(S.IsBlocked, 0) = 0
|
_ = tx.QueryRowContext(ctx, `
|
||||||
AND (
|
SELECT COUNT(1) FROM dbo.spUrtRecMBolum WITH (NOLOCK)
|
||||||
REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '')
|
WHERE nUrtReceteID = @p1 AND nUrtMBolumID = @p2
|
||||||
OR LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1
|
`, receteID, hNo).Scan(§ionExists)
|
||||||
OR @p1 LIKE LTRIM(RTRIM(ISNULL(S.sModel,''))) + '%'
|
|
||||||
)
|
if sectionExists <= 0 {
|
||||||
ORDER BY
|
logger.Info("creating missing recipe section", "n_urt_recete_id", receteID, "n_urt_m_bolum_id", hNo)
|
||||||
CASE
|
_, _ = tx.ExecContext(ctx, `
|
||||||
WHEN REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '') THEN 0
|
INSERT INTO dbo.spUrtRecMBolum (nUrtReceteID, nUrtUBolumID, nUrtMBolumID, nUrtMTBolumID, sKullaniciAdi, dteIslemTarihi)
|
||||||
WHEN LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1 THEN 1
|
VALUES (@p1, 13, @p2, @p3, @p4, GETDATE())
|
||||||
ELSE 2
|
`, receteID, hNo, row.NUrtMTBolumID, user)
|
||||||
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.
|
// Update quantity and prices if row already exists for (recete, hammadde, stok_code)
|
||||||
// Some recipes may omit nHStokID; but we prefer filling it when possible.
|
// Using nHStokID_G (string code) for matching as per user screenshot.
|
||||||
|
|
||||||
// Update quantity if row already exists for (recete, hammadde, stok)
|
|
||||||
var exists int
|
var exists int
|
||||||
if err := tx.QueryRowContext(ctx, `
|
if err := tx.QueryRowContext(ctx, `
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
FROM dbo.spUrtRecMBolumMik RMik WITH (NOLOCK)
|
FROM dbo.spUrtRecMBolumMik RMik WITH (NOLOCK)
|
||||||
WHERE RMik.nUrtReceteID = @p1
|
WHERE RMik.nUrtReceteID = @p1
|
||||||
AND RMik.nUrtMBolumID = @p2
|
AND RMik.nUrtMBolumID = @p2
|
||||||
AND RMik.nHStokID = @p3
|
AND LTRIM(RTRIM(RMik.nHStokID_G)) = @p3
|
||||||
`, receteID, hNo, nHStokID).Scan(&exists); err == nil && exists > 0 {
|
`, receteID, hNo, rawSKodu).Scan(&exists); err == nil && exists > 0 {
|
||||||
_, _ = tx.ExecContext(ctx, `
|
_, _ = tx.ExecContext(ctx, `
|
||||||
UPDATE dbo.spUrtRecMBolumMik
|
UPDATE dbo.spUrtRecMBolumMik
|
||||||
SET lHMiktar = @p4,
|
SET lHMiktar_G = @p4,
|
||||||
sKullaniciAdiDeg = @p5,
|
lHMaliyet_G = @p5,
|
||||||
dteIslemTarihiDeg = GETDATE()
|
sKullaniciAdiDeg = @p6,
|
||||||
|
dteIslemTarihiDeg = GETDATE(),
|
||||||
|
bIslem = @p7
|
||||||
WHERE nUrtReceteID = @p1
|
WHERE nUrtReceteID = @p1
|
||||||
AND nUrtMBolumID = @p2
|
AND nUrtMBolumID = @p2
|
||||||
AND nHStokID = @p3
|
AND LTRIM(RTRIM(nHStokID_G)) = @p3
|
||||||
`, receteID, hNo, nHStokID, row.LMiktar, user)
|
`, receteID, hNo, rawSKodu, row.LMiktar, unitTRY, user, row.MaliyeteDahil)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert missing: best-effort with minimal columns.
|
// Insert missing: using _G columns and storing code in nHStokID_G.
|
||||||
// NOTE: This assumes nUrtRecMBolumID can be a sequential int and that other columns are nullable/have defaults.
|
|
||||||
_, insertErr := tx.ExecContext(ctx, `
|
_, insertErr := tx.ExecContext(ctx, `
|
||||||
INSERT INTO dbo.spUrtRecMBolumMik (
|
INSERT INTO dbo.spUrtRecMBolumMik (
|
||||||
nUrtReceteID,
|
nUrtReceteID,
|
||||||
nUrtUBolumID,
|
nUrtUBolumID,
|
||||||
nUrtRecMBolumID,
|
nUrtRecMBolumID,
|
||||||
nStokID,
|
nStokID,
|
||||||
nHStokID,
|
nHStokID_G,
|
||||||
lHMiktar,
|
lHMiktar_G,
|
||||||
lHFire,
|
lHFire_G,
|
||||||
nMaliyetTipiID,
|
nMaliyetTipiID,
|
||||||
lHMaliyet,
|
lHMaliyet_G,
|
||||||
lMiktar,
|
lMiktar_G,
|
||||||
sIslemKodu,
|
sIslemKodu,
|
||||||
nUrtMBolumID,
|
nUrtMBolumID,
|
||||||
nUrtMTBolumID,
|
nUrtMTBolumID,
|
||||||
@@ -1646,56 +1790,52 @@ INSERT INTO dbo.spUrtRecMBolumMik (
|
|||||||
bIslem,
|
bIslem,
|
||||||
nSure,
|
nSure,
|
||||||
sAciklama,
|
sAciklama,
|
||||||
dteDovizTarihi,
|
|
||||||
sDovizCinsi,
|
|
||||||
lDovizOran,
|
|
||||||
lDovizFiyat,
|
|
||||||
sKullaniciAdi,
|
sKullaniciAdi,
|
||||||
dteIslemTarihi,
|
dteIslemTarihi,
|
||||||
nMBolumSarfTipiNo
|
nMBolumSarfTipiNo
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@p1,
|
@p1,
|
||||||
0,
|
13,
|
||||||
@p2,
|
@p2,
|
||||||
0,
|
0,
|
||||||
@p3,
|
@p3, -- nHStokID_G (Code)
|
||||||
@p4,
|
@p4, -- lHMiktar_G
|
||||||
0,
|
0,
|
||||||
6,
|
6,
|
||||||
0,
|
@p5, -- lHMaliyet_G
|
||||||
1,
|
1, -- lMiktar_G
|
||||||
'',
|
'',
|
||||||
@p5,
|
|
||||||
@p6,
|
@p6,
|
||||||
1,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
@p7,
|
@p7,
|
||||||
|
1,
|
||||||
|
@p8, -- bIslem
|
||||||
|
0,
|
||||||
|
NULL,
|
||||||
|
@p9,
|
||||||
GETDATE(),
|
GETDATE(),
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
`, receteID, nextRecDetID, nHStokID, row.LMiktar, hNo, row.NUrtMTBolumID, user)
|
`, receteID, nextRecDetID, rawSKodu, row.LMiktar, unitTRY, hNo, row.NUrtMTBolumID, row.MaliyeteDahil, user)
|
||||||
if insertErr == nil {
|
if insertErr == nil {
|
||||||
nextRecDetID += 1
|
nextRecDetID += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "commit")
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
logger.Error("tx commit error", "err", err)
|
logger.Error("tx commit error", "err", err)
|
||||||
http.Error(w, "Kaydetme tamamlanamadi", http.StatusInternalServerError)
|
http.Error(w, "Kaydetme tamamlanamadi", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
committed = true
|
||||||
|
logger.Info("tx commit ok", "trace_id", traceID, "n_onml_no", nOnMLNo)
|
||||||
|
|
||||||
// V3: update base price table so pricing screens reflect latest costing.
|
// V3: update base price table so pricing screens reflect latest costing.
|
||||||
// Not transactional with URETIM DB; if this fails, URETIM save has already succeeded.
|
// Not transactional with URETIM DB; if this fails, URETIM save has already succeeded.
|
||||||
if mssqlDB != nil {
|
if mssqlDB != nil {
|
||||||
|
logger.Info("post-commit step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "v3_base_price_upsert")
|
||||||
if err := queries.UpsertV3ItemBasePriceUSD(ctx, mssqlDB, req.Header.UrunKodu, req.Header.MaliyetTarihi, totalUSD, user); err != nil {
|
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)
|
logger.Error("v3 base price upsert error", "err", err)
|
||||||
http.Error(w, "URETIM kaydedildi ama V3 maliyet guncellenemedi", http.StatusInternalServerError)
|
http.Error(w, "URETIM kaydedildi ama V3 maliyet guncellenemedi", http.StatusInternalServerError)
|
||||||
@@ -1706,6 +1846,131 @@ VALUES (
|
|||||||
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingOnMLSaveResponse{NOnMLNo: nOnMLNo})
|
_ = json.NewEncoder(w).Encode(models.ProductionProductCostingOnMLSaveResponse{NOnMLNo: nOnMLNo})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /api/pricing/production-product-costing/onml/delete
|
||||||
|
// Deletes costing records created in URETIM (OnML header + details) and, if created by this app, V3 base price row.
|
||||||
|
// IMPORTANT: Recipe tables are NOT touched.
|
||||||
|
func PostProductionProductCostingOnMLDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
uretimDB := db.GetUretimDB()
|
||||||
|
if uretimDB == nil {
|
||||||
|
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mssqlDB := db.GetDB()
|
||||||
|
|
||||||
|
claims, _ := auth.GetClaimsFromContext(r.Context())
|
||||||
|
user := ""
|
||||||
|
if claims != nil {
|
||||||
|
user = strings.TrimSpace(claims.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.onml.delete")
|
||||||
|
|
||||||
|
var req models.ProductionProductCostingOnMLDeleteRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Gecersiz JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.NOnMLNo <= 0 {
|
||||||
|
http.Error(w, "n_onml_no zorunlu", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load header info for safe deletes and redirect behavior.
|
||||||
|
var urunKodu string
|
||||||
|
var maliyetTarihi time.Time
|
||||||
|
err := uretimDB.QueryRowContext(ctx, `
|
||||||
|
SELECT TOP 1
|
||||||
|
LTRIM(RTRIM(ISNULL(UrunKodu,''))) AS UrunKodu,
|
||||||
|
COALESCE(Tarihi, dteKayitTarihi, GETDATE()) AS Tarihi
|
||||||
|
FROM dbo.spUrtOnMLMas WITH (NOLOCK)
|
||||||
|
WHERE nOnMLNo = @p1
|
||||||
|
`, req.NOnMLNo).Scan(&urunKodu, &maliyetTarihi)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
http.Error(w, "Kayit bulunamadi", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Error("header lookup error", "err", err)
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
urunKodu = strings.TrimSpace(urunKodu)
|
||||||
|
|
||||||
|
tx, err := uretimDB.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
// Delete details first, then header.
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo = @p1
|
||||||
|
`, req.NOnMLNo); err != nil {
|
||||||
|
logger.Error("delete detail error", "err", err)
|
||||||
|
http.Error(w, "Detay silinemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
DELETE FROM dbo.spUrtOnMLMas WHERE nOnMLNo = @p1
|
||||||
|
`, req.NOnMLNo); err != nil {
|
||||||
|
logger.Error("delete header error", "err", err)
|
||||||
|
http.Error(w, "Header silinemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
logger.Error("tx commit error", "err", err)
|
||||||
|
http.Error(w, "Silme tamamlanamadi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// V3: Delete base price ONLY if row was created by this app (CreatedUserName starts with BSSAPP).
|
||||||
|
deletedBasePrice := false
|
||||||
|
if mssqlDB != nil && urunKodu != "" {
|
||||||
|
var createdBy sql.NullString
|
||||||
|
var lastBy sql.NullString
|
||||||
|
_ = mssqlDB.QueryRowContext(ctx, `
|
||||||
|
SELECT TOP 1
|
||||||
|
ISNULL(CreatedUserName,'') AS CreatedUserName,
|
||||||
|
ISNULL(LastUpdatedUserName,'') AS LastUpdatedUserName
|
||||||
|
FROM dbo.prItemBasePrice WITH (NOLOCK)
|
||||||
|
WHERE ItemTypeCode = 1
|
||||||
|
AND LTRIM(RTRIM(ItemCode)) = @p1
|
||||||
|
AND ISNULL(CountryCode,'') = 'TR'
|
||||||
|
AND ISNULL(SeasonCode,'') = ''
|
||||||
|
AND ISNULL(BasePriceCode,0) = 1
|
||||||
|
`, urunKodu).Scan(&createdBy, &lastBy)
|
||||||
|
|
||||||
|
created := strings.ToUpper(strings.TrimSpace(createdBy.String))
|
||||||
|
last := strings.ToUpper(strings.TrimSpace(lastBy.String))
|
||||||
|
if strings.HasPrefix(created, "BSSAPP") && strings.HasPrefix(last, "BSSAPP") {
|
||||||
|
if _, err := mssqlDB.ExecContext(ctx, `
|
||||||
|
DELETE FROM dbo.prItemBasePrice
|
||||||
|
WHERE ItemTypeCode = 1
|
||||||
|
AND LTRIM(RTRIM(ItemCode)) = @p1
|
||||||
|
AND ISNULL(CountryCode,'') = 'TR'
|
||||||
|
AND ISNULL(SeasonCode,'') = ''
|
||||||
|
AND ISNULL(BasePriceCode,0) = 1
|
||||||
|
`, urunKodu); err == nil {
|
||||||
|
deletedBasePrice = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("delete done", "n_onml_no", req.NOnMLNo, "urun_kodu", urunKodu, "deleted_base_price", deletedBasePrice, "user", user)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"ok": true,
|
||||||
|
"n_onml_no": req.NOnMLNo,
|
||||||
|
"urun_kodu": urunKodu,
|
||||||
|
"deleted_baseprice": deletedBasePrice,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/pricing/production-product-costing/has-cost-detail-exchange-rates
|
// 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")
|
||||||
|
|||||||
@@ -251,6 +251,13 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep mk_mail in sync for downstream mail mapping screens.
|
||||||
|
if err := ensureMkMail(tx, payload.Email); err != nil {
|
||||||
|
log.Printf("ERROR [UserDetail] ensureMkMail failed user_id=%d email=%q err=%v", userID, payload.Email, err)
|
||||||
|
http.Error(w, "Mail kaydi guncellenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID); err != nil {
|
if _, err := tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID); err != nil {
|
||||||
log.Printf("❌ [UserDetail] delete roles failed user_id=%d err=%v", userID, err)
|
log.Printf("❌ [UserDetail] delete roles failed user_id=%d err=%v", userID, err)
|
||||||
http.Error(w, "Roller temizlenemedi", http.StatusInternalServerError)
|
http.Error(w, "Roller temizlenemedi", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
/* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
/* 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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/* 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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/* 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} }
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page class="q-pa-md">
|
<q-page class="pcdq-page q-pa-md">
|
||||||
<div class="sticky-stack">
|
<div class="pcdq-top sticky-top">
|
||||||
<div class="save-toolbar">
|
<div class="save-toolbar">
|
||||||
<div class="row items-center justify-between q-col-gutter-sm">
|
<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 text-weight-bold">Maliyet Varsayilan Miktarlar</div>
|
||||||
@@ -52,7 +52,9 @@
|
|||||||
{{ error }}
|
{{ error }}
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
|
<div class="pcdq-table-wrap">
|
||||||
<q-table
|
<q-table
|
||||||
|
class="pcdq-table"
|
||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
dense
|
dense
|
||||||
@@ -62,6 +64,7 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
hide-bottom
|
hide-bottom
|
||||||
|
sticky-header
|
||||||
>
|
>
|
||||||
<template #body-cell-actions="props">
|
<template #body-cell-actions="props">
|
||||||
<q-td :props="props">
|
<q-td :props="props">
|
||||||
@@ -89,15 +92,8 @@
|
|||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</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-table>
|
||||||
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -120,7 +116,6 @@ const columns = [
|
|||||||
{ name: 'sAciklama', label: 'Aciklama', field: 'sAciklama', 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: 'lDefaultMiktar', label: 'Varsayilan Miktar', field: 'lDefaultMiktar', align: 'right', sortable: true },
|
||||||
{ name: 'dteCalcTarihi', label: 'Hesap Tarihi', field: 'dteCalcTarihi', align: 'left', 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 }
|
{ name: 'actions', label: '', field: '__actions', align: 'right', sortable: false }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -145,13 +140,6 @@ function onEditQty (row, val) {
|
|||||||
row.lDefaultMiktar = qty
|
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) {
|
async function onCalcAvg (row) {
|
||||||
const no = Number(row?.nHammaddeTuruNo || 0)
|
const no = Number(row?.nHammaddeTuruNo || 0)
|
||||||
if (!(no > 0)) return
|
if (!(no > 0)) return
|
||||||
@@ -204,7 +192,7 @@ function ensureBeforeUnloadGuard (enabled) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(hasUnsavedChanges, (v) => ensureBeforeUnloadGuard(Boolean(v)))
|
watch(() => Boolean(hasUnsavedChanges.value), (v) => ensureBeforeUnloadGuard(Boolean(v)))
|
||||||
|
|
||||||
onBeforeRouteLeave((to, from, next) => {
|
onBeforeRouteLeave((to, from, next) => {
|
||||||
if (!hasUnsavedChanges.value) return next()
|
if (!hasUnsavedChanges.value) return next()
|
||||||
@@ -225,11 +213,62 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.sticky-stack {
|
.pcdq-page {
|
||||||
|
background: #fafafa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 56px);
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-top {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
z-index: 10;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcdq-top {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcdq-table-wrap {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcdq-table {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcdq-table :deep(.q-table__container) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcdq-table :deep(.q-table__middle) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcdq-table :deep(.q-table thead th) {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcdq-table :deep(.q-table__middle thead tr th) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--header-h);
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 10;
|
||||||
background: #fff;
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
class="pcd-toolbar-btn"
|
class="pcd-toolbar-btn"
|
||||||
:loading="detailLoading"
|
:loading="detailLoading"
|
||||||
@click="fetchDetail({ clearDraft: true, hydrateDraft: false })"
|
@click="confirmRefresh"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Toplu Fiyat Cagir"
|
label="Toplu Fiyat Cagir"
|
||||||
@@ -84,6 +84,19 @@
|
|||||||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="isNoCostDetail"
|
||||||
|
label="Kaydi Sil"
|
||||||
|
icon="delete"
|
||||||
|
dense
|
||||||
|
color="negative"
|
||||||
|
outline
|
||||||
|
class="pcd-toolbar-btn"
|
||||||
|
:disable="!canDeleteCosting || detailLoading || saveLoading || bulkPriceLoading"
|
||||||
|
@click="deleteCosting"
|
||||||
|
>
|
||||||
|
<q-tooltip v-if="!canDeleteCosting">Once maliyet olusturulunca aktif olur</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,13 +372,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body-cell-sKodu="props">
|
<template #body-cell-sKodu="props">
|
||||||
<q-td :props="props">
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
:class="resolveAutoOrICodeHighlightClass(props.row)"
|
||||||
|
>
|
||||||
<span>{{ props.value }}</span>
|
<span>{{ props.value }}</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body-cell-sAciklama="props">
|
<template #body-cell-sAciklama="props">
|
||||||
<q-td :props="props">
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
:class="resolveAutoOrICodeHighlightClass(props.row)"
|
||||||
|
>
|
||||||
<span>{{ props.value }}</span>
|
<span>{{ props.value }}</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
@@ -443,9 +462,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
|
||||||
<q-dialog v-model="rowEditorDialogOpen" persistent>
|
<q-dialog v-model="rowEditorDialogOpen" persistent @show="resetRowEditorDialogPosition" @hide="stopRowEditorDialogDrag">
|
||||||
<q-card class="pcd-row-editor-dialog">
|
<q-card class="pcd-row-editor-dialog" :style="rowEditorDialogStyle">
|
||||||
<q-card-section class="row items-center justify-between q-pb-sm">
|
<q-card-section
|
||||||
|
class="row items-center justify-between q-pb-sm pcd-row-editor-drag-handle"
|
||||||
|
@mousedown.prevent.stop="startRowEditorDialogDrag"
|
||||||
|
>
|
||||||
<div class="text-subtitle1 text-weight-bold">
|
<div class="text-subtitle1 text-weight-bold">
|
||||||
{{ rowEditorMode === 'edit' ? 'Satir Duzenle' : 'Yeni Satir Ekle' }}
|
{{ rowEditorMode === 'edit' ? 'Satir Duzenle' : 'Yeni Satir Ekle' }}
|
||||||
</div>
|
</div>
|
||||||
@@ -752,6 +774,47 @@ const rowEditorDialogOpen = ref(false)
|
|||||||
const rowEditorMode = ref('new')
|
const rowEditorMode = ref('new')
|
||||||
const rowEditorTargetRowKey = ref('')
|
const rowEditorTargetRowKey = ref('')
|
||||||
const rowEditorForm = ref(createRowEditorForm())
|
const rowEditorForm = ref(createRowEditorForm())
|
||||||
|
|
||||||
|
// Draggable "Satir Duzenle" dialog (mouse drag by header).
|
||||||
|
const rowEditorDialogPos = ref({ x: 0, y: 0 })
|
||||||
|
const rowEditorDialogPanBase = ref({ x: 0, y: 0 })
|
||||||
|
const rowEditorDialogDragging = ref(false)
|
||||||
|
const rowEditorDialogDragMouseStart = ref({ x: 0, y: 0 })
|
||||||
|
const rowEditorDialogDragPosStart = ref({ x: 0, y: 0 })
|
||||||
|
const rowEditorDialogStyle = computed(() => ({
|
||||||
|
transform: `translate(${Number(rowEditorDialogPos.value?.x || 0)}px, ${Number(rowEditorDialogPos.value?.y || 0)}px)`
|
||||||
|
}))
|
||||||
|
|
||||||
|
function resetRowEditorDialogPosition () {
|
||||||
|
rowEditorDialogPos.value = { x: 0, y: 0 }
|
||||||
|
rowEditorDialogPanBase.value = { x: 0, y: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRowEditorDialogDrag (e) {
|
||||||
|
if (!e) return
|
||||||
|
rowEditorDialogDragging.value = true
|
||||||
|
rowEditorDialogDragMouseStart.value = { x: Number(e.clientX || 0), y: Number(e.clientY || 0) }
|
||||||
|
rowEditorDialogDragPosStart.value = { x: Number(rowEditorDialogPos.value?.x || 0), y: Number(rowEditorDialogPos.value?.y || 0) }
|
||||||
|
window.addEventListener('mousemove', onRowEditorDialogDragMove, true)
|
||||||
|
window.addEventListener('mouseup', stopRowEditorDialogDrag, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRowEditorDialogDragMove (e) {
|
||||||
|
if (!rowEditorDialogDragging.value) return
|
||||||
|
const startMouse = rowEditorDialogDragMouseStart.value || { x: 0, y: 0 }
|
||||||
|
const startPos = rowEditorDialogDragPosStart.value || { x: 0, y: 0 }
|
||||||
|
const dx = Number(e?.clientX || 0) - Number(startMouse.x || 0)
|
||||||
|
const dy = Number(e?.clientY || 0) - Number(startMouse.y || 0)
|
||||||
|
rowEditorDialogPos.value = { x: Number(startPos.x || 0) + dx, y: Number(startPos.y || 0) + dy }
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRowEditorDialogDrag () {
|
||||||
|
rowEditorDialogDragging.value = false
|
||||||
|
try {
|
||||||
|
window.removeEventListener('mousemove', onRowEditorDialogDragMove, true)
|
||||||
|
window.removeEventListener('mouseup', stopRowEditorDialogDrag, true)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
const rowEditorHammaddeOptions = ref([])
|
const rowEditorHammaddeOptions = ref([])
|
||||||
const rowEditorHammaddeAllOptions = ref([])
|
const rowEditorHammaddeAllOptions = ref([])
|
||||||
const rowEditorHammaddeLoading = ref(false)
|
const rowEditorHammaddeLoading = ref(false)
|
||||||
@@ -843,6 +906,13 @@ const hasUnsavedChanges = computed(() => {
|
|||||||
return flatDetailRows.value.some(row => Boolean(row?.draftChanged))
|
return flatDetailRows.value.some(row => Boolean(row?.draftChanged))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canDeleteCosting = computed(() => {
|
||||||
|
// Only allow delete for records created from no-cost flow.
|
||||||
|
// UX expectation: delete button becomes available after a costing record exists.
|
||||||
|
const n = parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0
|
||||||
|
return n > 0 && String(detailSource.value || '').trim().toLowerCase() === 'no-cost'
|
||||||
|
})
|
||||||
|
|
||||||
function persistLocalDraftNow () {
|
function persistLocalDraftNow () {
|
||||||
const key = draftStorageKey.value
|
const key = draftStorageKey.value
|
||||||
if (!key) return
|
if (!key) return
|
||||||
@@ -1796,6 +1866,19 @@ function resolveInputCurrency (row) {
|
|||||||
return normalizePriceCurrency(row?.inputPricePrBr || row?.fiyat_doviz) || 'USD'
|
return normalizePriceCurrency(row?.inputPricePrBr || row?.fiyat_doviz) || 'USD'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAutoOrICodeHighlightClass (row) {
|
||||||
|
// Priority: auto-filled from previous costing (already firm-aware).
|
||||||
|
if (row?.__autoFilledFromPrev) {
|
||||||
|
return row?.__autoFilledFromPrevSameFirma ? 'text-positive text-weight-bold' : 'text-negative text-weight-bold'
|
||||||
|
}
|
||||||
|
// Manual selection for I.* codes: highlight same/other firm like the auto-fill convention.
|
||||||
|
const code = String(row?.sKodu || '').trim().toUpperCase()
|
||||||
|
if (code.startsWith('I.') && row?.__iCodeSelected) {
|
||||||
|
return row?.__iCodeSameFirma ? 'text-positive text-weight-bold' : 'text-negative text-weight-bold'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
function resolveNumericRowQuantity (row) {
|
function resolveNumericRowQuantity (row) {
|
||||||
return parseMoneyInput(row?.miktarInput ?? row?.lMiktar)
|
return parseMoneyInput(row?.miktarInput ?? row?.lMiktar)
|
||||||
}
|
}
|
||||||
@@ -1826,17 +1909,23 @@ function resolveUSDUnitPriceByInput (inputPrice, inputCurrency, usdRate, eurRate
|
|||||||
|
|
||||||
function normalizeDetailRows (items, groupName = '') {
|
function normalizeDetailRows (items, groupName = '') {
|
||||||
const list = Array.isArray(items) ? items : []
|
const list = Array.isArray(items) ? items : []
|
||||||
return list.map((x, i) => ({
|
return list.map((x, i) => {
|
||||||
|
// Determine the original currency from backend fields.
|
||||||
|
const originalCurrency = normalizePriceCurrency(x?.fiyat_doviz || x?.sDovizCinsi || x?.inputPricePrBr) || 'USD'
|
||||||
|
return {
|
||||||
...x,
|
...x,
|
||||||
__rowKey: x?.__rowKey || `${x?.nOnMLNo || ''}-${x?.nOnMLDetNo || ''}-${i}`,
|
__rowKey: x?.__rowKey || `${x?.nOnMLNo || ''}-${x?.nOnMLDetNo || ''}-${i}`,
|
||||||
miktarInput: x?.miktarInput ?? normalizeQuantityInput(x?.lMiktar),
|
miktarInput: x?.miktarInput ?? normalizeQuantityInput(x?.lMiktar),
|
||||||
|
// If inputPrice is missing, it means we are loading fresh from backend.
|
||||||
|
// Use fiyat_girilen which is the unit price in original currency.
|
||||||
inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen),
|
inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen),
|
||||||
inputPricePrBr: normalizePriceCurrency(x?.inputPricePrBr || x?.fiyat_doviz || x?.sDovizCinsi) || 'USD',
|
inputPricePrBr: originalCurrency,
|
||||||
maliyeteDahil: x?.maliyeteDahil ?? normalizeBooleanFlag(x?.maliyete_dahil ?? x?.Maliyete_dahil),
|
maliyeteDahil: x?.maliyeteDahil ?? normalizeBooleanFlag(x?.maliyete_dahil ?? x?.Maliyete_dahil),
|
||||||
cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3),
|
cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3),
|
||||||
draftChanged: Boolean(x?.draftChanged),
|
draftChanged: Boolean(x?.draftChanged),
|
||||||
priceUpdateState: String(x?.priceUpdateState || '').trim()
|
priceUpdateState: String(x?.priceUpdateState || '').trim()
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDetailGroups (groups) {
|
function normalizeDetailGroups (groups) {
|
||||||
@@ -2253,10 +2342,17 @@ function buildDetailItemRequestPayload (row) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPriceSelectionToRow (targetRowKey, price, currency, priceType) {
|
function applyPriceSelectionToRow (targetRowKey, price, currency, priceType, itemCode, itemDescription, historyCompanyCode) {
|
||||||
const normalizedCurrency = normalizePriceCurrency(currency) || 'USD'
|
const normalizedCurrency = normalizePriceCurrency(currency) || 'USD'
|
||||||
const normalizedPrice = parseMoneyInput(price)
|
const normalizedPrice = parseMoneyInput(price)
|
||||||
const finalPriceType = priceType || 'SAF'
|
const finalPriceType = priceType || 'SAF'
|
||||||
|
const normalizedCode = normalizeCodeValue(itemCode)
|
||||||
|
const normalizedDesc = String(itemDescription || '').trim()
|
||||||
|
|
||||||
|
const isICode = String(normalizedCode || '').toUpperCase().startsWith('I.')
|
||||||
|
const selectedFirma = String(detailHeader.value?.FirmaKodu || '').trim().toUpperCase()
|
||||||
|
const historyFirma = String(historyCompanyCode || '').trim().toUpperCase()
|
||||||
|
const isSameFirma = Boolean(selectedFirma && historyFirma && selectedFirma === historyFirma)
|
||||||
|
|
||||||
detailGroups.value = detailGroups.value.map(grp => ({
|
detailGroups.value = detailGroups.value.map(grp => ({
|
||||||
...grp,
|
...grp,
|
||||||
@@ -2264,6 +2360,9 @@ function applyPriceSelectionToRow (targetRowKey, price, currency, priceType) {
|
|||||||
if (row.__rowKey !== targetRowKey) return row
|
if (row.__rowKey !== targetRowKey) return row
|
||||||
return recalculateDetailRow({
|
return recalculateDetailRow({
|
||||||
...row,
|
...row,
|
||||||
|
...(normalizedCode ? { sKodu: normalizedCode } : {}),
|
||||||
|
...(normalizedDesc ? { sAciklama: normalizedDesc } : {}),
|
||||||
|
...(isICode ? { __iCodeSelected: true, __iCodeSameFirma: isSameFirma } : { __iCodeSelected: false, __iCodeSameFirma: false }),
|
||||||
inputPrice: normalizeInputPrice(normalizedPrice),
|
inputPrice: normalizeInputPrice(normalizedPrice),
|
||||||
fiyat_girilen: normalizedPrice,
|
fiyat_girilen: normalizedPrice,
|
||||||
inputPricePrBr: normalizedCurrency,
|
inputPricePrBr: normalizedCurrency,
|
||||||
@@ -2481,6 +2580,94 @@ async function fetchBulkItemPrices () {
|
|||||||
: 'Donen veriler satirlarla eslestirilemedi.',
|
: 'Donen veriler satirlarla eslestirilemedi.',
|
||||||
position: 'top-right'
|
position: 'top-right'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Autofill missing required codes from last OnML history (URETIM spUrtOnMLMasDet)
|
||||||
|
try {
|
||||||
|
const flatNow = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
|
||||||
|
const targets = flatNow.filter(r =>
|
||||||
|
Boolean(r?.requiredPlaceholder) &&
|
||||||
|
// CM1/CM2 and FABRIC rows must be chosen by user; don't auto-fill from previous costing.
|
||||||
|
!isCMGroupName(r?.sAciklama3) &&
|
||||||
|
normalizeGroupName(r?.sAciklama3) !== 'FABRIC' &&
|
||||||
|
(String(r?.sKodu || '').trim() === '' || Number(resolveNumericRowInputPrice(r) || 0) <= 0)
|
||||||
|
)
|
||||||
|
const hNos = Array.from(new Set(targets
|
||||||
|
.map(r => parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10))
|
||||||
|
.filter(n => n > 0)
|
||||||
|
))
|
||||||
|
if (hNos.length > 0) {
|
||||||
|
const lastRows = await post('/pricing/production-product-costing/has-cost-detail/last-detail', {
|
||||||
|
nHammaddeTuruNos: hNos,
|
||||||
|
before_date: normalizeDateInput(costDate.value),
|
||||||
|
exclude_onml_no: parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0,
|
||||||
|
n_firma_id: parseInt(String(detailHeader.value?.nFirmaID || detailHeader.value?.NFirmaID || '0').trim() || '0', 10) || 0
|
||||||
|
}, { params: { trace_id: traceId.value } })
|
||||||
|
|
||||||
|
const list = Array.isArray(lastRows) ? lastRows : []
|
||||||
|
const byNo = {}
|
||||||
|
list.forEach(x => {
|
||||||
|
const no = parseInt(String(x?.nHammaddeTuruNo || '0'), 10) || 0
|
||||||
|
if (no > 0) byNo[no] = x
|
||||||
|
})
|
||||||
|
|
||||||
|
let filled = 0
|
||||||
|
detailGroups.value = detailGroups.value.map(grp => ({
|
||||||
|
...grp,
|
||||||
|
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
|
||||||
|
if (!row?.requiredPlaceholder) return row
|
||||||
|
if (isCMGroupName(row?.sAciklama3) || normalizeGroupName(row?.sAciklama3) === 'FABRIC') return row
|
||||||
|
const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
|
||||||
|
const hit = byNo[no]
|
||||||
|
if (!hit) return row
|
||||||
|
|
||||||
|
const next = { ...row }
|
||||||
|
const hasCode = String(next.sKodu || '').trim() !== ''
|
||||||
|
const hasPrice = Number(resolveNumericRowInputPrice(next) || 0) > 0
|
||||||
|
|
||||||
|
if (!hasCode && String(hit?.sKodu || '').trim()) {
|
||||||
|
next.sKodu = String(hit.sKodu || '').trim()
|
||||||
|
next.sAciklama = String(hit?.sAciklama || '').trim()
|
||||||
|
}
|
||||||
|
if (!hasPrice && Number(hit?.fiyat_girilen || 0) > 0) {
|
||||||
|
next.inputPrice = String(hit.fiyat_girilen)
|
||||||
|
next.fiyat_girilen = Number(hit.fiyat_girilen)
|
||||||
|
const pr = String(hit?.fiyat_doviz || '').trim() || 'USD'
|
||||||
|
next.inputPricePrBr = pr
|
||||||
|
next.fiyat_doviz = pr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only mark if something changed
|
||||||
|
const changed = (next.sKodu !== row.sKodu) || (next.sAciklama !== row.sAciklama) || (next.inputPrice !== row.inputPrice) || (next.inputPricePrBr !== row.inputPricePrBr)
|
||||||
|
if (!changed) return row
|
||||||
|
filled += 1
|
||||||
|
return recalculateDetailRow({
|
||||||
|
...next,
|
||||||
|
__autoFilledFromPrev: true
|
||||||
|
,
|
||||||
|
__autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma)
|
||||||
|
}, {
|
||||||
|
priceType: 'PREV',
|
||||||
|
updateState: 'autofill',
|
||||||
|
markChanged: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (filled > 0) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'info',
|
||||||
|
message: `${filled} satirda kod/aciklama ve fiyat bilgisi onceki maliyetten otomatik getirildi.`,
|
||||||
|
position: 'top-right'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Non-blocking
|
||||||
|
slog.error('production-product-costing.detail', 'bulk:autofill-prev:error', {
|
||||||
|
trace_id: traceId.value,
|
||||||
|
error: String(e?.message || e || '')
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
@@ -2542,11 +2729,28 @@ function onDetailRowClick (evt, row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyLineHistorySelection (historyRow) {
|
function applyLineHistorySelection (historyRow) {
|
||||||
applyPriceSelectionToRow(lineHistoryTargetRowKey.value, historyRow?.price, historyRow?.currency, historyRow?.priceType)
|
const targetKey = String(lineHistoryTargetRowKey.value || '').trim()
|
||||||
|
if (!targetKey) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Hedef satir bulunamadi (rowKey bos).',
|
||||||
|
position: 'top-right'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applyPriceSelectionToRow(
|
||||||
|
targetKey,
|
||||||
|
historyRow?.price,
|
||||||
|
historyRow?.currency,
|
||||||
|
historyRow?.priceType,
|
||||||
|
historyRow?.itemCode,
|
||||||
|
historyRow?.itemDescription,
|
||||||
|
historyRow?.companyCode
|
||||||
|
)
|
||||||
lineHistoryDialogOpen.value = false
|
lineHistoryDialogOpen.value = false
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Secilen history fiyati satira uygulandi.',
|
message: `Secilen fiyat satira uygulandi: ${formatMoney(historyRow?.price)} ${String(historyRow?.currency || '').trim() || 'USD'}`,
|
||||||
position: 'top-right'
|
position: 'top-right'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2759,8 +2963,10 @@ function normalizeGroupName (value) {
|
|||||||
async function fetchRequiredParcaMappings () {
|
async function fetchRequiredParcaMappings () {
|
||||||
const ilk = String(detailHeader.value?.UrunIlkGrubu || '').trim()
|
const ilk = String(detailHeader.value?.UrunIlkGrubu || '').trim()
|
||||||
const ana = String(detailHeader.value?.UrunAnaGrubu || '').trim()
|
const ana = String(detailHeader.value?.UrunAnaGrubu || '').trim()
|
||||||
const alt = String(detailHeader.value?.UrunAltGrubu || '').trim()
|
// Some sources return NULL/'' for "no alt group". Mapping screen stores it as '-'.
|
||||||
if (!ilk || !ana || !alt) return []
|
const altRaw = String(detailHeader.value?.UrunAltGrubu || '').trim()
|
||||||
|
const alt = altRaw || '-'
|
||||||
|
if (!ilk || !ana) return []
|
||||||
|
|
||||||
const data = await get('/pricing/production-product-costing/maliyet-parca-eslestirme', {
|
const data = await get('/pricing/production-product-costing/maliyet-parca-eslestirme', {
|
||||||
trace_id: traceId.value,
|
trace_id: traceId.value,
|
||||||
@@ -2956,6 +3162,81 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
|||||||
applyEditorRowToGroups(placeholder)
|
applyEditorRowToGroups(placeholder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CM2 special case:
|
||||||
|
// Bulk autofill is disabled for CM2 (and FABRIC) to prevent wrong price pulls,
|
||||||
|
// but we still want the I.* code templates (code + description) to appear on page open.
|
||||||
|
// So we prefill ONLY code + description for required CM2 placeholders from last OnML history.
|
||||||
|
try {
|
||||||
|
const flatNow = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
|
||||||
|
const cm2Targets = flatNow.filter(r =>
|
||||||
|
Boolean(r?.requiredPlaceholder) &&
|
||||||
|
normalizeGroupName(r?.sAciklama3) === 'CM2' &&
|
||||||
|
String(r?.sKodu || '').trim() === ''
|
||||||
|
)
|
||||||
|
const cm2Nos = Array.from(new Set(cm2Targets
|
||||||
|
.map(r => parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10))
|
||||||
|
.filter(n => n > 0)
|
||||||
|
))
|
||||||
|
if (cm2Nos.length > 0) {
|
||||||
|
const lastRows = await post('/pricing/production-product-costing/has-cost-detail/last-detail', {
|
||||||
|
nHammaddeTuruNos: cm2Nos,
|
||||||
|
before_date: normalizeDateInput(costDate.value),
|
||||||
|
exclude_onml_no: parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0,
|
||||||
|
n_firma_id: parseInt(String(detailHeader.value?.nFirmaID || detailHeader.value?.NFirmaID || '0').trim() || '0', 10) || 0,
|
||||||
|
only_i_code: true
|
||||||
|
}, { params: { trace_id: traceId.value } })
|
||||||
|
|
||||||
|
const list = Array.isArray(lastRows) ? lastRows : []
|
||||||
|
const byNo = {}
|
||||||
|
list.forEach(x => {
|
||||||
|
const no = parseInt(String(x?.nHammaddeTuruNo || '0'), 10) || 0
|
||||||
|
if (no > 0) byNo[no] = x
|
||||||
|
})
|
||||||
|
|
||||||
|
let filled = 0
|
||||||
|
detailGroups.value = detailGroups.value.map(grp => ({
|
||||||
|
...grp,
|
||||||
|
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
|
||||||
|
if (!row?.requiredPlaceholder) return row
|
||||||
|
if (normalizeGroupName(row?.sAciklama3) !== 'CM2') return row
|
||||||
|
if (String(row?.sKodu || '').trim() !== '') return row
|
||||||
|
const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
|
||||||
|
const hit = byNo[no]
|
||||||
|
if (!hit) return row
|
||||||
|
const code = String(hit?.sKodu || '').trim()
|
||||||
|
if (!code) return row
|
||||||
|
|
||||||
|
const next = recalculateDetailRow({
|
||||||
|
...row,
|
||||||
|
sKodu: code,
|
||||||
|
sAciklama: String(hit?.sAciklama || '').trim(),
|
||||||
|
__autoFilledFromPrev: true,
|
||||||
|
__autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma)
|
||||||
|
}, {
|
||||||
|
priceType: 'PREV',
|
||||||
|
updateState: 'autofill-icode',
|
||||||
|
markChanged: true
|
||||||
|
})
|
||||||
|
filled += 1
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
if (filled > 0) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'info',
|
||||||
|
message: `${filled} CM2 satirinda I.* kod/aciklama onceki maliyetten otomatik getirildi.`,
|
||||||
|
position: 'top-right'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Non-blocking
|
||||||
|
slog.error('production-product-costing.detail', 'cm2:autofill-icode:error', {
|
||||||
|
trace_id: traceId.value,
|
||||||
|
error: String(e?.message || e)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeMissingRequiredSlots () {
|
function computeMissingRequiredSlots () {
|
||||||
@@ -2964,19 +3245,30 @@ function computeMissingRequiredSlots () {
|
|||||||
if (list.length === 0) return missing
|
if (list.length === 0) return missing
|
||||||
|
|
||||||
list.forEach(mapping => {
|
list.forEach(mapping => {
|
||||||
const groupName = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || mapping?.sAciklama3)
|
// Required slots are defined per (ParcaBolum / MTBolumID) + (HammaddeTuruNo).
|
||||||
|
// Do NOT match by row.sAciklama3, because that field is the "group header" (DT/TP/CM2/FABRIC), not the part.
|
||||||
|
const mappingMtBolumID = parseInt(String(mapping?.nUrtMTBolumID ?? mapping?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||||
|
const mappingParcaAdi = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || '')
|
||||||
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
||||||
hList.forEach(hNoRaw => {
|
hList.forEach(hNoRaw => {
|
||||||
const hNo = normalizeHammaddeNo(hNoRaw)
|
const hNo = normalizeHammaddeNo(hNoRaw)
|
||||||
if (!hNo) return
|
if (!hNo) return
|
||||||
|
|
||||||
const match = flatDetailRows.value.find(r =>
|
const match = flatDetailRows.value.find(r => {
|
||||||
normalizeGroupName(r?.sAciklama3) === groupName &&
|
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||||||
normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo
|
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||||
)
|
if (mappingMtBolumID > 0 && rowMtBolumID > 0) return mappingMtBolumID === rowMtBolumID
|
||||||
|
// Fallback: older rows might not have mtBolumID; use part name match.
|
||||||
|
return normalizeGroupName(r?.sParcaAdi) === mappingParcaAdi
|
||||||
|
})
|
||||||
const price = resolveNumericRowInputPrice(match)
|
const price = resolveNumericRowInputPrice(match)
|
||||||
if (!match || !(price > 0)) {
|
if (!match || !(price > 0)) {
|
||||||
missing.push({ groupName, nHammaddeTuruNo: hNo, rowKey: match?.__rowKey || '' })
|
missing.push({
|
||||||
|
mtBolumID: mappingMtBolumID,
|
||||||
|
parcaAdi: mappingParcaAdi,
|
||||||
|
nHammaddeTuruNo: hNo,
|
||||||
|
rowKey: match?.__rowKey || ''
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -3131,6 +3423,16 @@ function round4 (n) {
|
|||||||
return Math.round(x * 10000) / 10000
|
return Math.round(x * 10000) / 10000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml (input) {
|
||||||
|
const s = String(input ?? '')
|
||||||
|
return s
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''')
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmDefaultQtyDeviationIfNeeded () {
|
async function confirmDefaultQtyDeviationIfNeeded () {
|
||||||
// Compare entered qty vs default qty (mk_MaliyetParcaEslestirme_vmiktarlar) per hammadde type.
|
// Compare entered qty vs default qty (mk_MaliyetParcaEslestirme_vmiktarlar) per hammadde type.
|
||||||
// Rule: if deviation > 10% (abs), require user confirmation.
|
// Rule: if deviation > 10% (abs), require user confirmation.
|
||||||
@@ -3154,10 +3456,15 @@ async function confirmDefaultQtyDeviationIfNeeded () {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const defMap = {}
|
const defMap = {}
|
||||||
|
const descMap = {}
|
||||||
;(Array.isArray(defaults) ? defaults : []).forEach(it => {
|
;(Array.isArray(defaults) ? defaults : []).forEach(it => {
|
||||||
const no = parseInt(String(it?.nHammaddeTuruNo || '0'), 10) || 0
|
const no = parseInt(String(it?.nHammaddeTuruNo || '0'), 10) || 0
|
||||||
const d = Number(it?.lDefaultMiktar || 0)
|
const d = Number(it?.lDefaultMiktar || 0)
|
||||||
if (no > 0 && d > 0) defMap[no] = d
|
const desc = String(it?.sAciklama || '').trim()
|
||||||
|
if (no > 0 && d > 0) {
|
||||||
|
defMap[no] = d
|
||||||
|
if (desc) descMap[no] = desc
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const outliers = []
|
const outliers = []
|
||||||
@@ -3176,18 +3483,53 @@ async function confirmDefaultQtyDeviationIfNeeded () {
|
|||||||
outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct))
|
outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct))
|
||||||
|
|
||||||
const maxRows = 30
|
const maxRows = 30
|
||||||
const lines = outliers.slice(0, maxRows).map(x => {
|
const rowsHtml = outliers.slice(0, maxRows).map(x => {
|
||||||
const sign = x.pct >= 0 ? '+' : ''
|
const sign = x.pct >= 0 ? '+' : ''
|
||||||
return `${x.no}: varsayilan ${round4(x.defQty)} | girilen ${round4(x.enteredQty)} | fark ${sign}${round1(x.pct)}%`
|
const pct = `${sign}${round1(x.pct)}%`
|
||||||
})
|
const cls = x.pct >= 0 ? 'color:#b71c1c;' : 'color:#1b5e20;'
|
||||||
const truncated = outliers.length > maxRows
|
const desc = String(descMap[x.no] || '').trim()
|
||||||
? `\n... (Toplam ${outliers.length} satir. Ilk ${maxRows} gosterildi.)`
|
const noLabel = desc ? `${x.no} - ${escapeHtml(desc)}` : String(x.no)
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 8px; white-space:nowrap; font-weight:600;">${noLabel}</td>
|
||||||
|
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${round4(x.defQty)}</td>
|
||||||
|
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${round4(x.enteredQty)}</td>
|
||||||
|
<td style="padding:6px 8px; text-align:right; white-space:nowrap; ${cls} font-weight:600;">${pct}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
}).join('')
|
||||||
|
const truncatedNote = outliers.length > maxRows
|
||||||
|
? `<div style="margin-top:8px; color:#666;">Toplam ${outliers.length} satir var. Ilk ${maxRows} gosterildi.</div>`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const ok = await new Promise(resolve => {
|
const ok = await new Promise(resolve => {
|
||||||
$q.dialog({
|
$q.dialog({
|
||||||
title: 'Varsayilan Miktar Kontrolu',
|
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.`,
|
html: true,
|
||||||
|
message: `
|
||||||
|
<div style="margin-bottom:10px;">
|
||||||
|
Bazi hammadde turlerinde varsayilan miktardan <b>%10</b>'dan fazla sapma var.
|
||||||
|
</div>
|
||||||
|
<div style="max-height: 360px; overflow:auto; border:1px solid #e0e0e0; border-radius:6px;">
|
||||||
|
<table style="width:100%; border-collapse:collapse; font-size:13px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#f5f5f5; position: sticky; top: 0;">
|
||||||
|
<th style="text-align:left; padding:6px 8px;">Hammadde</th>
|
||||||
|
<th style="text-align:right; padding:6px 8px;">Varsayilan</th>
|
||||||
|
<th style="text-align:right; padding:6px 8px;">Girilen</th>
|
||||||
|
<th style="text-align:right; padding:6px 8px;">Fark %</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${rowsHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
${truncatedNote}
|
||||||
|
<div style="margin-top:10px;">
|
||||||
|
Onayliyorsaniz <b>Onayla ve Kaydet</b>'e basın. Duzenlemek icin <b>Geri Don</b>.
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
cancel: { label: 'Geri Don' },
|
cancel: { label: 'Geri Don' },
|
||||||
ok: { label: 'Onayla ve Kaydet', color: 'primary' },
|
ok: { label: 'Onayla ve Kaydet', color: 'primary' },
|
||||||
persistent: true
|
persistent: true
|
||||||
@@ -3196,6 +3538,57 @@ async function confirmDefaultQtyDeviationIfNeeded () {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteCosting () {
|
||||||
|
if (!detailHeader.value) return
|
||||||
|
const n = parseInt(String(detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || onMLNo.value || '0'), 10) || 0
|
||||||
|
if (!(n > 0)) {
|
||||||
|
$q.notify({ type: 'warning', message: 'Silinecek kayit bulunamadi.', position: 'top-right' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await new Promise(resolve => {
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Kaydi Sil',
|
||||||
|
message: `Bu maliyet kaydi silinecek. (OnMLNo=${n}) Devam edilsin mi?`,
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
|
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
await post('/pricing/production-product-costing/onml/delete', {
|
||||||
|
n_onml_no: n
|
||||||
|
})
|
||||||
|
clearLocalDraft()
|
||||||
|
$q.notify({ type: 'positive', message: 'Kayit silindi.', position: 'top-right' })
|
||||||
|
router.replace({ name: 'production-product-costing-no-cost' })
|
||||||
|
} catch (e) {
|
||||||
|
$q.notify({ type: 'negative', message: String(e?.message || e || 'Silinemedi'), position: 'top-right' })
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRefresh () {
|
||||||
|
if (detailLoading.value) return
|
||||||
|
const ok = await new Promise(resolve => {
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Yenile',
|
||||||
|
message: hasUnsavedChanges.value
|
||||||
|
? 'Bu islem sayfayi sifirlar ve kaydedilmemis degisiklikler kaybolur. Devam edilsin mi?'
|
||||||
|
: 'Bu islem sayfayi sifirlar. Devam edilsin mi?',
|
||||||
|
cancel: { label: 'Geri Don' },
|
||||||
|
ok: { label: 'Onayla', color: 'negative' },
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
|
clearLocalDraft()
|
||||||
|
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
||||||
|
}
|
||||||
|
|
||||||
async function saveChanges () {
|
async function saveChanges () {
|
||||||
saveLoading.value = true
|
saveLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -3206,7 +3599,7 @@ async function saveChanges () {
|
|||||||
const ok = await new Promise(resolve => {
|
const ok = await new Promise(resolve => {
|
||||||
$q.dialog({
|
$q.dialog({
|
||||||
title: 'Eksik Maliyet Parcalari',
|
title: 'Eksik Maliyet Parcalari',
|
||||||
message: `Eslestirilen parcalarda (fiyat > 0) girilmemis satirlar var. Devam etmek istiyor musunuz? (Eksik: ${missing.length})`,
|
message: `Eslestirilen parcalarda fiyat girilmemis satirlar var. Devam etmek istiyor musunuz? (Eksik: ${missing.length})`,
|
||||||
cancel: true,
|
cancel: true,
|
||||||
persistent: true
|
persistent: true
|
||||||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||||||
@@ -3250,7 +3643,8 @@ async function saveChanges () {
|
|||||||
fiyat_girilen: Number(resolveNumericRowInputPrice(r) || 0),
|
fiyat_girilen: Number(resolveNumericRowInputPrice(r) || 0),
|
||||||
fiyat_doviz: String(resolveInputCurrency(r) || '').trim(),
|
fiyat_doviz: String(resolveInputCurrency(r) || '').trim(),
|
||||||
maliyete_dahil: (r?.maliyeteDahil || r?.maliyete_dahil) ? 1 : 0,
|
maliyete_dahil: (r?.maliyeteDahil || r?.maliyete_dahil) ? 1 : 0,
|
||||||
cm_price_type_id: r?.cmPriceTypeId ?? r?.cm_price_type_id ?? null
|
cm_price_type_id: r?.cmPriceTypeId ?? r?.cm_price_type_id ?? null,
|
||||||
|
s_aciklama3: String(r?.sAciklama3 || '').trim()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const deletes = (Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []).map(d => ({
|
const deletes = (Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []).map(d => ({
|
||||||
@@ -3278,6 +3672,9 @@ async function saveChanges () {
|
|||||||
|
|
||||||
$q.notify({ type: 'positive', message: 'Kaydedildi.', position: 'top-right' })
|
$q.notify({ type: 'positive', message: 'Kaydedildi.', position: 'top-right' })
|
||||||
|
|
||||||
|
// Force clear local draft before fetching fresh data to ensure we don't re-hydrate old inputs.
|
||||||
|
clearLocalDraft()
|
||||||
|
|
||||||
// If we created a new OnML (no-cost), switch to has-cost detail mode.
|
// If we created a new OnML (no-cost), switch to has-cost detail mode.
|
||||||
if (isNoCostDetail.value && newOnMLNo > 0) {
|
if (isNoCostDetail.value && newOnMLNo > 0) {
|
||||||
router.replace({
|
router.replace({
|
||||||
@@ -3289,6 +3686,25 @@ async function saveChanges () {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For existing costing, just refresh the detail.
|
||||||
|
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
||||||
|
} catch (e) {
|
||||||
|
// Surface backend message (http.Error text) when available.
|
||||||
|
const msg = String(
|
||||||
|
e?.response?.data?.message ||
|
||||||
|
e?.response?.data ||
|
||||||
|
e?.message ||
|
||||||
|
e ||
|
||||||
|
'Kaydedilemedi'
|
||||||
|
)
|
||||||
|
slog.error('production-product-costing.detail', 'save:error', {
|
||||||
|
trace_id: traceId.value,
|
||||||
|
status: e?.response?.status,
|
||||||
|
error: msg
|
||||||
|
})
|
||||||
|
$q.notify({ type: 'negative', message: msg, position: 'top-right' })
|
||||||
|
return
|
||||||
} finally {
|
} finally {
|
||||||
saveLoading.value = false
|
saveLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -3489,6 +3905,11 @@ watch(
|
|||||||
max-width: 96vw;
|
max-width: 96vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pcd-row-editor-drag-handle {
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__control) {
|
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__control) {
|
||||||
background: color-mix(in srgb, var(--q-secondary) 18%, white) !important;
|
background: color-mix(in srgb, var(--q-secondary) 18%, white) !important;
|
||||||
border: 1px solid var(--q-secondary) !important;
|
border: 1px solid var(--q-secondary) !important;
|
||||||
|
|||||||
@@ -87,6 +87,43 @@
|
|||||||
|
|
||||||
<template #top-left>
|
<template #top-left>
|
||||||
<div class="row items-center q-gutter-sm">
|
<div class="row items-center q-gutter-sm">
|
||||||
|
<q-select
|
||||||
|
v-model="commonHammaddeSelected"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
clearable
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
:options="commonHammaddeOptions"
|
||||||
|
:disable="loading || saving || commonHammaddeOptions.length === 0"
|
||||||
|
class="pcmm-multi-select"
|
||||||
|
style="min-width: 420px"
|
||||||
|
>
|
||||||
|
<template #selected-item="scope">
|
||||||
|
<q-chip
|
||||||
|
class="q-mr-xs"
|
||||||
|
dense
|
||||||
|
removable
|
||||||
|
@remove="scope.removeAtIndex(scope.index)"
|
||||||
|
>
|
||||||
|
{{ scope.opt.label }}
|
||||||
|
</q-chip>
|
||||||
|
</template>
|
||||||
|
<template #hint>
|
||||||
|
Ekrandaki satirlarda bulunan hammadde turleri (distinct)
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
<q-btn
|
||||||
|
color="negative"
|
||||||
|
icon="delete_sweep"
|
||||||
|
label="Butun Parcalardan Kaldir"
|
||||||
|
:disable="loading || saving || (commonHammaddeSelected || []).length === 0"
|
||||||
|
@click="confirmRemoveCommonHammadde"
|
||||||
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Kolon Filtreleri"
|
label="Kolon Filtreleri"
|
||||||
icon="filter_alt_off"
|
icon="filter_alt_off"
|
||||||
@@ -327,6 +364,8 @@ const hammaddeLoading = ref(false)
|
|||||||
|
|
||||||
const bolumByKey = ref({})
|
const bolumByKey = ref({})
|
||||||
const hammaddeByKey = ref({})
|
const hammaddeByKey = ref({})
|
||||||
|
const commonHammaddeSelected = ref([])
|
||||||
|
const hammaddeLabelCache = ref({}) // no -> "NO - ACIKLAMA"
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'copy_select', label: '', field: 'copy_select', align: 'center' },
|
{ name: 'copy_select', label: '', field: 'copy_select', align: 'center' },
|
||||||
@@ -345,6 +384,22 @@ const canCopySelected = computed(() => copySelectedCount.value >= 2)
|
|||||||
const saveSelectedCount = computed(() => Object.keys(saveSelectedKeyMap.value || {}).length)
|
const saveSelectedCount = computed(() => Object.keys(saveSelectedKeyMap.value || {}).length)
|
||||||
const canSaveSelected = computed(() => saveSelectedCount.value > 0)
|
const canSaveSelected = computed(() => saveSelectedCount.value > 0)
|
||||||
|
|
||||||
|
function findHammaddeLabel (no) {
|
||||||
|
const n = Number(no || 0)
|
||||||
|
if (!(n > 0)) return ''
|
||||||
|
const cached = String(hammaddeLabelCache.value?.[String(n)] || '').trim()
|
||||||
|
if (cached) return cached
|
||||||
|
const opt = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).find(o => Number(o?.value) === n)
|
||||||
|
const raw = String(opt?.label || '').trim()
|
||||||
|
if (raw) {
|
||||||
|
// Ensure "NO - ACIKLAMA" format. Some option sources may only provide the description.
|
||||||
|
if (/^\d+\s*-\s*/.test(raw)) return raw
|
||||||
|
if (/^\d+\s*$/.test(raw)) return `${n}`
|
||||||
|
return `${n} - ${raw}`
|
||||||
|
}
|
||||||
|
return `${n}`
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSearch (value) {
|
function normalizeSearch (value) {
|
||||||
const s = String(value ?? '').trim()
|
const s = String(value ?? '').trim()
|
||||||
if (!s) return ''
|
if (!s) return ''
|
||||||
@@ -459,6 +514,33 @@ const rows = computed(() => {
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const commonHammaddeOptions = computed(() => {
|
||||||
|
const listRows = Array.isArray(rows.value) ? rows.value : []
|
||||||
|
if (listRows.length === 0) return []
|
||||||
|
const keys = listRows.map(r => String(r?.__key || '').trim()).filter(Boolean)
|
||||||
|
if (keys.length === 0) return []
|
||||||
|
|
||||||
|
// DISTINCT across visible rows (union).
|
||||||
|
const set = new Set()
|
||||||
|
for (const k of keys) {
|
||||||
|
const ham = normalizeIntList(hammaddeByKey.value?.[k] || [])
|
||||||
|
ham.forEach(n => { if (n > 0) set.add(n) })
|
||||||
|
}
|
||||||
|
return Array.from(set)
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
.map(n => ({ value: n, label: findHammaddeLabel(n) }))
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(commonHammaddeOptions, (opts) => {
|
||||||
|
// Ensure labels for large/rare nos that won't be in the first 200 option fetch.
|
||||||
|
try {
|
||||||
|
const nos = (Array.isArray(opts) ? opts : []).map(o => Number(o?.value || 0)).filter(n => n > 0)
|
||||||
|
ensureHammaddeLabelsForNos(nos)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
function hardResetAndRefresh () {
|
function hardResetAndRefresh () {
|
||||||
// reset view state (filters + selections + dirty)
|
// reset view state (filters + selections + dirty)
|
||||||
clearAllColumnFilters()
|
clearAllColumnFilters()
|
||||||
@@ -626,9 +708,14 @@ function updateBolumSelection (key, newValue) {
|
|||||||
function updateHammaddeSelection (key, newValue) {
|
function updateHammaddeSelection (key, newValue) {
|
||||||
const k = String(key || '').trim()
|
const k = String(key || '').trim()
|
||||||
if (!k) return
|
if (!k) return
|
||||||
hammaddeByKey.value = {
|
const nextList = normalizeIntList(newValue)
|
||||||
...(hammaddeByKey.value || {}),
|
hammaddeByKey.value = { ...(hammaddeByKey.value || {}), [k]: nextList }
|
||||||
[k]: normalizeIntList(newValue)
|
// Keep table field in sync for column filtering/sorting/export behavior.
|
||||||
|
const idx = (Array.isArray(mappings.value) ? mappings.value : []).findIndex(r => String(r?.__key || '') === k)
|
||||||
|
if (idx >= 0) {
|
||||||
|
const copy = [...mappings.value]
|
||||||
|
copy[idx] = { ...(copy[idx] || {}), nHammaddeTurleri: [...nextList] }
|
||||||
|
mappings.value = copy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,6 +740,62 @@ function pruneHammaddeSelection (rowKey, list) {
|
|||||||
return allowed
|
return allowed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmRemoveCommonHammadde () {
|
||||||
|
const selected = normalizeIntList(commonHammaddeSelected.value || [])
|
||||||
|
if (selected.length === 0) return
|
||||||
|
|
||||||
|
const listRows = Array.isArray(rows.value) ? rows.value : []
|
||||||
|
const keys = listRows.map(r => String(r?.__key || '').trim()).filter(Boolean)
|
||||||
|
if (keys.length === 0) return
|
||||||
|
|
||||||
|
// Affected "parcalar": only rows that currently contain at least one selected hammadde.
|
||||||
|
const affectedRows = listRows.filter(r => {
|
||||||
|
const k = String(r?.__key || '').trim()
|
||||||
|
if (!k) return false
|
||||||
|
const current = normalizeIntList(hammaddeByKey.value?.[k] || [])
|
||||||
|
return current.some(v => selected.includes(v))
|
||||||
|
})
|
||||||
|
if (affectedRows.length === 0) {
|
||||||
|
$q.notify({ type: 'info', message: 'Secilen hammadde turleri bu ekrandaki satirlarda bulunmuyor.', position: 'top-right' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const affected = affectedRows
|
||||||
|
.map(r => `${String(r?.urunIlkGrubu || '').trim() || '-'} | ${String(r?.urunAnaGrubu || '').trim() || '-'} | ${String(r?.urunAltGrubu || '').trim() || '-'}`)
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const removedLabels = selected.map(n => findHammaddeLabel(n))
|
||||||
|
const htmlList = affected.slice(0, 30).map(x => `<div>${x}</div>`).join('')
|
||||||
|
const more = affected.length > 30 ? `<div class="text-grey-7 q-mt-sm">(+${affected.length - 30} satir daha)</div>` : ''
|
||||||
|
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Butun Parcalardan Kaldir',
|
||||||
|
message: `
|
||||||
|
<div class="q-mb-sm"><b>Kaldirilacak hammadde turleri:</b></div>
|
||||||
|
<div class="q-mb-sm">${removedLabels.map(x => `<div>${x}</div>`).join('')}</div>
|
||||||
|
<div class="q-mb-sm"><b>Etkilenecek parcalar:</b></div>
|
||||||
|
<div style="max-height: 240px; overflow:auto; border: 1px solid #eee; padding: 8px;">${htmlList}${more}</div>
|
||||||
|
<div class="q-mt-sm text-grey-7">Onaylarsaniz secilen hammadde turleri sadece bu satirlardan kaldirilacak ve Degisenleri Kaydet ile kaydedilebilecek.</div>
|
||||||
|
`,
|
||||||
|
html: true,
|
||||||
|
cancel: true,
|
||||||
|
persistent: true,
|
||||||
|
ok: { label: 'Onayla', color: 'negative' },
|
||||||
|
cancelLabel: 'Geri Don'
|
||||||
|
}).onOk(() => {
|
||||||
|
const affectedKeys = affectedRows.map(r => String(r?.__key || '').trim()).filter(Boolean)
|
||||||
|
for (const k of affectedKeys) {
|
||||||
|
const current = normalizeIntList(hammaddeByKey.value?.[k] || [])
|
||||||
|
const next = current.filter(v => !selected.includes(v))
|
||||||
|
if (String(next) === String(current)) continue
|
||||||
|
updateHammaddeSelection(k, next)
|
||||||
|
const row = listRows.find(r => String(r?.__key || '') === k) || (Array.isArray(mappings.value) ? mappings.value : []).find(r => String(r?.__key || '') === k)
|
||||||
|
if (row) markDirty(row)
|
||||||
|
}
|
||||||
|
commonHammaddeSelected.value = []
|
||||||
|
$q.notify({ type: 'positive', message: 'Secilen hammadde turleri etkilenen satirlardan kaldirildi (taslak).', position: 'top-right' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// label resolution now handled by options' `label` field + selected-item slot (see UserDetail.vue "Piyasalar").
|
// label resolution now handled by options' `label` field + selected-item slot (see UserDetail.vue "Piyasalar").
|
||||||
async function fetchMappings () {
|
async function fetchMappings () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -816,11 +959,55 @@ async function fetchHammaddeOptions (search) {
|
|||||||
.filter(x => Number.isFinite(x.value) && x.value > 0)
|
.filter(x => Number.isFinite(x.value) && x.value > 0)
|
||||||
.sort((a, b) => a.value - b.value)
|
.sort((a, b) => a.value - b.value)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
// Prime cache with currently loaded options.
|
||||||
|
try {
|
||||||
|
const next = { ...(hammaddeLabelCache.value || {}) }
|
||||||
|
;(Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).forEach(opt => {
|
||||||
|
const no = Number(opt?.value || 0)
|
||||||
|
const label = String(opt?.label || '').trim()
|
||||||
|
if (no > 0 && label) next[String(no)] = label
|
||||||
|
})
|
||||||
|
hammaddeLabelCache.value = next
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
hammaddeLoading.value = false
|
hammaddeLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureHammaddeLabelsForNos (nos) {
|
||||||
|
const list = normalizeIntList(nos || [])
|
||||||
|
if (list.length === 0) return
|
||||||
|
const missing = list.filter(n => {
|
||||||
|
const cached = String(hammaddeLabelCache.value?.[String(n)] || '').trim()
|
||||||
|
if (cached) return false
|
||||||
|
const opt = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).find(o => Number(o?.value) === n)
|
||||||
|
return !String(opt?.label || '').trim()
|
||||||
|
})
|
||||||
|
if (missing.length === 0) return
|
||||||
|
try {
|
||||||
|
const data = await post('/pricing/production-product-costing/options/hammadde-by-nos', {
|
||||||
|
nHammaddeTuruNos: missing
|
||||||
|
}, { trace_id: traceId })
|
||||||
|
const rows = Array.isArray(data) ? data : []
|
||||||
|
const next = { ...(hammaddeLabelCache.value || {}) }
|
||||||
|
rows.forEach(r => {
|
||||||
|
const no = Number(r?.nHammaddeTuruNo || 0)
|
||||||
|
const name = String(r?.sAciklama || '').trim()
|
||||||
|
if (no > 0 && name) next[String(no)] = `${no} - ${name}`
|
||||||
|
})
|
||||||
|
hammaddeLabelCache.value = next
|
||||||
|
} catch (e) {
|
||||||
|
// Non-blocking: fallback will show only the number
|
||||||
|
slog.error('production-product-costing.mtbolum-map', 'hammadde-by-nos:error', {
|
||||||
|
trace_id: traceId,
|
||||||
|
detail: await extractApiErrorDetail(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onFilterMTBolum (val, update) {
|
function onFilterMTBolum (val, update) {
|
||||||
update(async () => {
|
update(async () => {
|
||||||
await fetchMTBolumOptions(val)
|
await fetchMTBolumOptions(val)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const useProductionProductCostingDefaultQtyStore = defineStore('productio
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
// draftByNo: { [nHammaddeTuruNo]: { lDefaultMiktar, bAktif } }
|
// draftByNo: { [nHammaddeTuruNo]: { lDefaultMiktar } }
|
||||||
const draftByNo = ref({})
|
const draftByNo = ref({})
|
||||||
const persistTimer = ref(0)
|
const persistTimer = ref(0)
|
||||||
|
|
||||||
@@ -94,12 +94,11 @@ export const useProductionProductCostingDefaultQtyStore = defineStore('productio
|
|||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
lDefaultMiktar: typeof draft.lDefaultMiktar === 'number' ? draft.lDefaultMiktar : row?.lDefaultMiktar,
|
lDefaultMiktar: typeof draft.lDefaultMiktar === 'number' ? draft.lDefaultMiktar : row?.lDefaultMiktar,
|
||||||
bAktif: typeof draft.bAktif === 'boolean' ? draft.bAktif : row?.bAktif,
|
|
||||||
__dirty: true
|
__dirty: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetch ({ search = '', onlyActive = true, limit = 2000 } = {}) {
|
async function fetch ({ search = '', limit = 2000 } = {}) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
try {
|
try {
|
||||||
@@ -136,8 +135,7 @@ export const useProductionProductCostingDefaultQtyStore = defineStore('productio
|
|||||||
const patch = draftByNo.value?.[String(no)] || {}
|
const patch = draftByNo.value?.[String(no)] || {}
|
||||||
return {
|
return {
|
||||||
nHammaddeTuruNo: Number(no),
|
nHammaddeTuruNo: Number(no),
|
||||||
lDefaultMiktar: Number(patch.lDefaultMiktar || 0),
|
lDefaultMiktar: Number(patch.lDefaultMiktar || 0)
|
||||||
bAktif: typeof patch.bAktif === 'boolean' ? patch.bAktif : undefined
|
|
||||||
}
|
}
|
||||||
}).filter(it => it.nHammaddeTuruNo > 0 && it.lDefaultMiktar > 0)
|
}).filter(it => it.nHammaddeTuruNo > 0 && it.lDefaultMiktar > 0)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user