Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -811,6 +811,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"order", "view",
|
"order", "view",
|
||||||
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLSaveHandler)),
|
wrapV3(http.HandlerFunc(routes.PostProductionProductCostingOnMLSaveHandler)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/production-product-costing/onml/pdf", "GET",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.GetProductionProductCostingOnMLPDFHandler)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/production-product-costing/onml/delete", "POST",
|
"/api/pricing/production-product-costing/onml/delete", "POST",
|
||||||
"order", "view",
|
"order", "view",
|
||||||
|
|||||||
@@ -912,19 +912,22 @@ func GetProductionHasCostDetailRowsByOnMLNo(
|
|||||||
nOnMLNo int,
|
nOnMLNo int,
|
||||||
) (*sql.Rows, error) {
|
) (*sql.Rows, error) {
|
||||||
sqlText := `
|
sqlText := `
|
||||||
SELECT
|
SELECT
|
||||||
ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ') AS sAciklama3,
|
-- Prefer the group label stored on the OnML detail row (D.sAciklama3),
|
||||||
|
-- because some hammadde type master rows may have empty/legacy group labels.
|
||||||
|
ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ')) AS sAciklama3,
|
||||||
SUM(ISNULL(D.lTutar, 0)) OVER (
|
SUM(ISNULL(D.lTutar, 0)) OVER (
|
||||||
PARTITION BY ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ')
|
PARTITION BY ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ'))
|
||||||
) AS GroupTotalTutar,
|
) AS GroupTotalTutar,
|
||||||
SUM(ISNULL(D.lMiktar, 0) * ISNULL(D.lDovizFiyati, 0)) OVER (
|
SUM(ISNULL(D.lMiktar, 0) * ISNULL(D.lDovizFiyati, 0)) OVER (
|
||||||
PARTITION BY ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ')
|
PARTITION BY ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ'))
|
||||||
) AS GroupTotalUSDTutar,
|
) AS GroupTotalUSDTutar,
|
||||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLNo, 0))) AS nOnMLNo,
|
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLNo, 0))) AS nOnMLNo,
|
||||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLDetNo, 0))) AS nOnMLDetNo,
|
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLDetNo, 0))) AS nOnMLDetNo,
|
||||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
|
RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
|
||||||
ISNULL(D.sKodu, '') AS sKodu,
|
-- Normalize code to variantless (tbStok.sModel) when D.sKodu is a variant-coded stock record.
|
||||||
ISNULL(D.sAciklama, '') AS sAciklama,
|
ISNULL(NULLIF(LTRIM(RTRIM(SX.sModel)), ''), ISNULL(D.sKodu, '')) AS sKodu,
|
||||||
|
ISNULL(NULLIF(LTRIM(RTRIM(SX.sAciklama)), ''), ISNULL(D.sAciklama, '')) AS sAciklama,
|
||||||
ISNULL(D.sRenk, '') AS sRenk,
|
ISNULL(D.sRenk, '') AS sRenk,
|
||||||
ISNULL(D.sBeden, '') AS sBeden,
|
ISNULL(D.sBeden, '') AS sBeden,
|
||||||
ISNULL(D.sAciklama2, '') AS sAciklama2,
|
ISNULL(D.sAciklama2, '') AS sAciklama2,
|
||||||
@@ -945,13 +948,20 @@ SELECT
|
|||||||
ISNULL(D.sBirim, '') AS sBirim,
|
ISNULL(D.sBirim, '') AS sBirim,
|
||||||
ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
|
ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
|
||||||
ISNULL(B.sAdi, '') AS sParcaAdi
|
ISNULL(B.sAdi, '') AS sParcaAdi
|
||||||
FROM dbo.spUrtOnMLMasDet D
|
FROM dbo.spUrtOnMLMasDet D
|
||||||
LEFT JOIN dbo.spUrtOnMLHammaddeTuru T
|
LEFT JOIN dbo.spUrtOnMLHammaddeTuru T
|
||||||
ON T.nHammaddeTuruNo = D.nHammaddeTuruNo
|
ON T.nHammaddeTuruNo = D.nHammaddeTuruNo
|
||||||
LEFT JOIN dbo.spUrtMTBolum B
|
LEFT JOIN dbo.spUrtMTBolum B
|
||||||
ON B.nUrtMTBolumID = D.nUrtMTBolumID
|
ON B.nUrtMTBolumID = D.nUrtMTBolumID
|
||||||
WHERE D.nOnMLNo = @p1
|
OUTER APPLY (
|
||||||
ORDER BY
|
SELECT TOP 1
|
||||||
|
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) AS sModel,
|
||||||
|
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sAciklama, '')))) AS sAciklama
|
||||||
|
FROM dbo.tbStok S WITH (NOLOCK)
|
||||||
|
WHERE LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) = LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(D.sKodu, ''))))
|
||||||
|
) SX
|
||||||
|
WHERE D.nOnMLNo = @p1
|
||||||
|
ORDER BY
|
||||||
GroupTotalTutar DESC,
|
GroupTotalTutar DESC,
|
||||||
sAciklama3 ASC,
|
sAciklama3 ASC,
|
||||||
ISNULL(D.lTutar, 0) DESC,
|
ISNULL(D.lTutar, 0) DESC,
|
||||||
@@ -1219,26 +1229,40 @@ func GetProductionHasCostDetailHammaddeTypeOptions(
|
|||||||
uretimDB *sql.DB,
|
uretimDB *sql.DB,
|
||||||
search string,
|
search string,
|
||||||
limit int,
|
limit int,
|
||||||
|
group string,
|
||||||
|
onlyActive *bool,
|
||||||
) (*sql.Rows, error) {
|
) (*sql.Rows, error) {
|
||||||
search = strings.TrimSpace(search)
|
search = strings.TrimSpace(search)
|
||||||
|
group = strings.TrimSpace(group)
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 50
|
limit = 50
|
||||||
}
|
}
|
||||||
searchLike := "%" + search + "%"
|
searchLike := "%" + search + "%"
|
||||||
|
|
||||||
sqlText := `
|
sqlText := `
|
||||||
SELECT TOP (@p2)
|
SELECT TOP (@p2)
|
||||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
|
RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
|
||||||
ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
|
ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
|
||||||
COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
|
COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
|
||||||
ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
|
ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
|
||||||
ISNULL(B.sAdi, '') AS sParcaAdi
|
ISNULL(B.sAdi, '') AS sParcaAdi
|
||||||
FROM dbo.spUrtOnMLHammaddeTuru T WITH (NOLOCK)
|
FROM dbo.spUrtOnMLHammaddeTuru T WITH (NOLOCK)
|
||||||
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
|
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK)
|
||||||
ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
|
ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
|
||||||
AND ISNULL(B.nUrtTipiID, 0) = 1
|
AND ISNULL(B.nUrtTipiID, 0) = 1
|
||||||
WHERE
|
WHERE
|
||||||
ISNULL(T.bAktif, 0) = 1
|
-- Active filtering:
|
||||||
|
-- - By default, list active rows.
|
||||||
|
-- - If searching by exact number, allow inactive too (legacy CM2 etc.).
|
||||||
|
-- - If onlyActive flag is provided, honor it explicitly.
|
||||||
|
(
|
||||||
|
(@p5 IS NULL AND (ISNULL(T.bAktif, 0) = 1 OR (@p1 <> '' AND RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) = @p1)))
|
||||||
|
OR (@p5 IS NOT NULL AND ISNULL(T.bAktif, 0) = @p5)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
@p4 = ''
|
||||||
|
OR COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') = @p4
|
||||||
|
)
|
||||||
AND (
|
AND (
|
||||||
@p1 = ''
|
@p1 = ''
|
||||||
OR RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) LIKE @p3
|
OR RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) LIKE @p3
|
||||||
@@ -1256,7 +1280,7 @@ ORDER BY
|
|||||||
T.nHammaddeTuruNo
|
T.nHammaddeTuruNo
|
||||||
`
|
`
|
||||||
|
|
||||||
return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike)
|
return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike, group, onlyActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetProductionHammaddeByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]models.ProductionProductCostingHammaddeByNosItem, error) {
|
func GetProductionHammaddeByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]models.ProductionProductCostingHammaddeByNosItem, error) {
|
||||||
@@ -1745,30 +1769,56 @@ FROM dbo.tbStok S`)
|
|||||||
useFullText := searchLen >= 3 && fullTextSearch != "" && hasProductionHasCostDetailItemFullTextIndex(ctx, uretimDB)
|
useFullText := searchLen >= 3 && fullTextSearch != "" && hasProductionHasCostDetailItemFullTextIndex(ctx, uretimDB)
|
||||||
|
|
||||||
baseSelect := `
|
baseSelect := `
|
||||||
SELECT TOP (@p2)
|
WITH c AS (
|
||||||
|
SELECT
|
||||||
RTRIM(CONVERT(VARCHAR(32), ISNULL(S.nStokID, 0))) AS nStokID,
|
RTRIM(CONVERT(VARCHAR(32), ISNULL(S.nStokID, 0))) AS nStokID,
|
||||||
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) AS sKodu,
|
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) AS sKodu,
|
||||||
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sAciklama, '')))) AS sAciklama,
|
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sAciklama, '')))) AS sAciklama,
|
||||||
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) AS sModel,
|
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) AS sModel,
|
||||||
LTRIM(RTRIM(CONVERT(NVARCHAR(64), ISNULL(S.sBirimCinsi1, '')))) AS sBirim
|
LTRIM(RTRIM(CONVERT(NVARCHAR(64), ISNULL(S.sBirimCinsi1, '')))) AS sBirim,
|
||||||
FROM dbo.tbStok S
|
CASE
|
||||||
WHERE
|
WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) = @p1 THEN 0
|
||||||
|
WHEN (@p5 = 1 AND S.nStokID = @p6) THEN 1
|
||||||
|
WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) = @p1 THEN 2
|
||||||
|
WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE @p3 THEN 3
|
||||||
|
WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) LIKE @p3 THEN 4
|
||||||
|
ELSE 5
|
||||||
|
END AS match_rank,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, ''))))
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) = @p1 THEN 0
|
||||||
|
WHEN (@p5 = 1 AND S.nStokID = @p6) THEN 1
|
||||||
|
WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) = @p1 THEN 2
|
||||||
|
WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE @p3 THEN 3
|
||||||
|
WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) LIKE @p3 THEN 4
|
||||||
|
ELSE 5
|
||||||
|
END,
|
||||||
|
LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, ''))))
|
||||||
|
) AS rn
|
||||||
|
FROM dbo.tbStok S
|
||||||
|
WHERE
|
||||||
(ISNULL(S.IsBlocked, 0) = 0)
|
(ISNULL(S.IsBlocked, 0) = 0)
|
||||||
AND S.sModel LIKE '_.%%'
|
AND S.sModel LIKE '_.%%'
|
||||||
AND (
|
AND (
|
||||||
(@p5 = 1 AND S.nStokID = @p6)
|
(@p5 = 1 AND S.nStokID = @p6)
|
||||||
|
OR S.sModel = @p1
|
||||||
|
OR S.sModel LIKE @p3
|
||||||
OR S.sKodu = @p1
|
OR S.sKodu = @p1
|
||||||
OR S.sKodu LIKE @p3
|
OR S.sKodu LIKE @p3
|
||||||
OR %s
|
OR %s
|
||||||
)
|
)
|
||||||
ORDER BY
|
)
|
||||||
CASE
|
SELECT TOP (@p2)
|
||||||
WHEN S.sKodu = @p1 THEN 0
|
nStokID,
|
||||||
WHEN (@p5 = 1 AND S.nStokID = @p6) THEN 1
|
sKodu,
|
||||||
WHEN S.sKodu LIKE @p3 THEN 2
|
sAciklama,
|
||||||
ELSE 3
|
sModel,
|
||||||
END,
|
sBirim
|
||||||
S.sKodu
|
FROM c
|
||||||
|
WHERE rn = 1
|
||||||
|
ORDER BY match_rank, sModel, sKodu
|
||||||
OPTION (RECOMPILE)
|
OPTION (RECOMPILE)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -767,7 +767,15 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := queries.GetProductionHasCostDetailHammaddeTypeOptions(ctx, uretimDB, search, limit)
|
group := strings.TrimSpace(r.URL.Query().Get("group"))
|
||||||
|
rawOnlyActive := strings.TrimSpace(r.URL.Query().Get("only_active"))
|
||||||
|
var onlyActive *bool = nil
|
||||||
|
if rawOnlyActive != "" {
|
||||||
|
v := rawOnlyActive == "1" || strings.EqualFold(rawOnlyActive, "true")
|
||||||
|
onlyActive = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := queries.GetProductionHasCostDetailHammaddeTypeOptions(ctx, uretimDB, search, limit, group, onlyActive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("hammadde query error", "err", err)
|
logger.Error("hammadde query error", "err", err)
|
||||||
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde query error: %v", err)
|
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde query error: %v", err)
|
||||||
@@ -839,8 +847,14 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item.Kind = "item"
|
item.Kind = "item"
|
||||||
item.Value = item.SKodu
|
// Use variantless code (tbStok.sModel) as the value for costing/recipe.
|
||||||
item.Label = strings.TrimSpace(item.SKodu + " - " + item.SAciklama)
|
// Variant-coded tbStok.sKodu can include color/variant suffixes; costing uses base model codes.
|
||||||
|
code := strings.TrimSpace(item.SModel)
|
||||||
|
if code == "" {
|
||||||
|
code = strings.TrimSpace(item.SKodu)
|
||||||
|
}
|
||||||
|
item.Value = code
|
||||||
|
item.Label = strings.TrimSpace(code + " - " + item.SAciklama)
|
||||||
list = append(list, item)
|
list = append(list, item)
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
@@ -1237,6 +1251,12 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Header validation: uretim sekli must be selected.
|
||||||
|
if req.Header.UretimSekliID <= 0 {
|
||||||
|
logger.Warn("validation failed: uretim_sekli_id <= 0", "uretim_sekli_id", req.Header.UretimSekliID)
|
||||||
|
http.Error(w, "Uretim sekli secilmeden kayit yapilamaz", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
req.DetailSource = strings.ToLower(strings.TrimSpace(req.DetailSource))
|
req.DetailSource = strings.ToLower(strings.TrimSpace(req.DetailSource))
|
||||||
req.Header.UrunKodu = strings.TrimSpace(req.Header.UrunKodu)
|
req.Header.UrunKodu = strings.TrimSpace(req.Header.UrunKodu)
|
||||||
@@ -1457,8 +1477,10 @@ 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))
|
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_deletes", "count", len(req.Detail.Deletes))
|
||||||
|
skippedDeletes := 0
|
||||||
for _, d := range req.Detail.Deletes {
|
for _, d := range req.Detail.Deletes {
|
||||||
if d.NOnMLDetNo <= 0 {
|
if d.NOnMLDetNo <= 0 {
|
||||||
|
skippedDeletes += 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := tx.ExecContext(ctx, `DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo=@p1 AND nOnMLDetNo=@p2`, nOnMLNo, d.NOnMLDetNo); err != nil {
|
if _, err := tx.ExecContext(ctx, `DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo=@p1 AND nOnMLDetNo=@p2`, nOnMLNo, d.NOnMLDetNo); err != nil {
|
||||||
@@ -1467,13 +1489,31 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if skippedDeletes > 0 {
|
||||||
|
logger.Warn("detail deletes skipped (det_no<=0)", "trace_id", traceID, "n_onml_no", nOnMLNo, "skipped", skippedDeletes)
|
||||||
|
}
|
||||||
|
|
||||||
// Upserts
|
// Upserts
|
||||||
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_upserts", "count", len(req.Detail.Upserts))
|
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_upserts", "count", len(req.Detail.Upserts))
|
||||||
|
skippedUpserts := 0
|
||||||
|
skippedUpsertsSample := 0
|
||||||
for _, row := range req.Detail.Upserts {
|
for _, row := range req.Detail.Upserts {
|
||||||
if row.NOnMLDetNo <= 0 {
|
if row.NOnMLDetNo <= 0 {
|
||||||
|
skippedUpserts += 1
|
||||||
|
if skippedUpsertsSample < 5 {
|
||||||
|
skippedUpsertsSample += 1
|
||||||
|
logger.Warn("detail upsert skipped (det_no<=0)",
|
||||||
|
"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),
|
||||||
|
"s_aciklama3", strings.TrimSpace(row.SAciklama3),
|
||||||
|
)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
|
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
|
||||||
// FALLBACK: If nHammaddeTuruNo is missing but sKodu is present, default to 1 (General/Labor)
|
// FALLBACK: If nHammaddeTuruNo is missing but sKodu is present, default to 1 (General/Labor)
|
||||||
// to avoid blocking the user, especially for labor items.
|
// to avoid blocking the user, especially for labor items.
|
||||||
@@ -1701,140 +1741,12 @@ WHEN NOT MATCHED THEN
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if skippedUpserts > 0 {
|
||||||
// ============================================================
|
logger.Warn("detail upserts skipped summary (det_no<=0)", "trace_id", traceID, "n_onml_no", nOnMLNo, "skipped", skippedUpserts)
|
||||||
// Recipe sync (URETIM): ensure recipe contains all OnML hammadde rows
|
|
||||||
// so future no-cost loads don't keep showing them as missing.
|
|
||||||
// IMPORTANT: In current URETIM DB, the detail table is dbo.spUrtRecMBolum (NOT NULL cols, FK to dbo.spUrtMBolum).
|
|
||||||
// We must:
|
|
||||||
// 1) Skip hammadde types that do not exist in dbo.spUrtMBolum (FK safety),
|
|
||||||
// 2) Upsert by (nUrtReceteID, nUrtMBolumID, nHStokID_G=sKodu),
|
|
||||||
// 3) When inserting, generate nUrtRecMBolumID (smallint, not identity) and fill required columns incl. sIslemKodu=''.
|
|
||||||
// ============================================================
|
|
||||||
if req.Header.NUrtReceteID > 0 {
|
|
||||||
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) globally.
|
|
||||||
// NOTE: nUrtRecMBolumID is smallint and not identity in this schema.
|
|
||||||
nextRecDetID := 0
|
|
||||||
_ = tx.QueryRowContext(ctx, `
|
|
||||||
SELECT ISNULL(MAX(R.nUrtRecMBolumID), 0) + 1
|
|
||||||
FROM dbo.spUrtRecMBolum R WITH (UPDLOCK, HOLDLOCK)
|
|
||||||
`).Scan(&nextRecDetID)
|
|
||||||
if nextRecDetID <= 0 {
|
|
||||||
nextRecDetID = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, row := range req.Detail.Upserts {
|
// NOTE: Recipe tables are intentionally NOT synced from OnML saves.
|
||||||
hNo := row.NHammaddeTuruNo
|
// This costing screen is the source of truth only for dbo.spUrtOnMLMas / dbo.spUrtOnMLMasDet.
|
||||||
if hNo <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Legacy mapping: merge deprecated hammadde types into canonical ones.
|
|
||||||
// We migrated 1104 -> 1105 historically; keep runtime mapping to avoid FK issues.
|
|
||||||
if hNo == 1104 {
|
|
||||||
hNo = 1105
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. FILTER: CM1/CM2 (Labor/Service) rows must NOT be written back into recipe tables.
|
|
||||||
// We check the group label (sAciklama3) from the row itself.
|
|
||||||
g := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
|
|
||||||
if g == "CM1" || g == "CM2" {
|
|
||||||
logger.Info("recipe sync skip: labor item", "s_kodu", row.SKodu, "group", g)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// FK safety: nUrtMBolumID must exist in dbo.spUrtMBolum.
|
|
||||||
var bolumExists int
|
|
||||||
if err := tx.QueryRowContext(ctx, `
|
|
||||||
SELECT COUNT(1) FROM dbo.spUrtMBolum WITH (NOLOCK)
|
|
||||||
WHERE nUrtMBolumID = @p1
|
|
||||||
`, hNo).Scan(&bolumExists); err != nil || bolumExists <= 0 {
|
|
||||||
logger.Warn("recipe sync skip: missing spUrtMBolum", "n_urt_m_bolum_id", hNo, "s_kodu", strings.TrimSpace(row.SKodu))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upsert target key: (receteID, hNo, sKodu).
|
|
||||||
rawSKodu := strings.TrimSpace(row.SKodu)
|
|
||||||
if rawSKodu == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update qty if exists.
|
|
||||||
var exists int
|
|
||||||
if err := tx.QueryRowContext(ctx, `
|
|
||||||
SELECT COUNT(1)
|
|
||||||
FROM dbo.spUrtRecMBolum R WITH (NOLOCK)
|
|
||||||
WHERE R.nUrtReceteID = @p1
|
|
||||||
AND R.nUrtMBolumID = @p2
|
|
||||||
AND LTRIM(RTRIM(R.nHStokID_G)) = @p3
|
|
||||||
`, receteID, hNo, rawSKodu).Scan(&exists); err == nil && exists > 0 {
|
|
||||||
_, _ = tx.ExecContext(ctx, `
|
|
||||||
UPDATE dbo.spUrtRecMBolum
|
|
||||||
SET lHMiktar_G = @p4
|
|
||||||
WHERE nUrtReceteID = @p1
|
|
||||||
AND nUrtMBolumID = @p2
|
|
||||||
AND LTRIM(RTRIM(nHStokID_G)) = @p3
|
|
||||||
`, receteID, hNo, rawSKodu, row.LMiktar)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert missing into dbo.spUrtRecMBolum.
|
|
||||||
// nUrtRecMBolumID is not identity; keep incrementing, but guard against smallint overflow.
|
|
||||||
if nextRecDetID > 32767 {
|
|
||||||
logger.Warn("recipe sync skip: nUrtRecMBolumID overflow risk", "next_id", nextRecDetID, "n_urt_recete_id", receteID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, insertErr := tx.ExecContext(ctx, `
|
|
||||||
INSERT INTO dbo.spUrtRecMBolum (
|
|
||||||
nUrtRecMBolumID,
|
|
||||||
nUrtReceteID,
|
|
||||||
nUrtUBolumID,
|
|
||||||
nUrtMBolumID,
|
|
||||||
nUrtMTBolumID,
|
|
||||||
nStokTipiID,
|
|
||||||
nHStokID_G,
|
|
||||||
lHMiktar_G,
|
|
||||||
lHFire_G,
|
|
||||||
lHCarpan,
|
|
||||||
nMaliyetTipiID,
|
|
||||||
lHMaliyet_G,
|
|
||||||
nMTalimat_G,
|
|
||||||
bIslem,
|
|
||||||
nSure,
|
|
||||||
sIslemKodu,
|
|
||||||
lHMiktar_GHedef,
|
|
||||||
nMBolumSarfTipiNo
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
@p1, -- nUrtRecMBolumID (smallint)
|
|
||||||
@p2, -- nUrtReceteID
|
|
||||||
@p3, -- nUrtUBolumID
|
|
||||||
@p4, -- nUrtMBolumID
|
|
||||||
0, -- nUrtMTBolumID (tinyint)
|
|
||||||
1, -- nStokTipiID
|
|
||||||
@p5, -- nHStokID_G (sKodu)
|
|
||||||
@p6, -- lHMiktar_G
|
|
||||||
0, -- lHFire_G
|
|
||||||
1, -- lHCarpan
|
|
||||||
6, -- nMaliyetTipiID
|
|
||||||
0, -- lHMaliyet_G
|
|
||||||
2, -- nMTalimat_G
|
|
||||||
0, -- bIslem
|
|
||||||
0, -- nSure
|
|
||||||
'', -- sIslemKodu (NOT NULL)
|
|
||||||
0, -- lHMiktar_GHedef
|
|
||||||
1 -- nMBolumSarfTipiNo
|
|
||||||
)
|
|
||||||
`, nextRecDetID, receteID, 13, hNo, rawSKodu, row.LMiktar)
|
|
||||||
if insertErr == nil {
|
|
||||||
nextRecDetID += 1
|
|
||||||
} else {
|
|
||||||
logger.Warn("recipe sync insert error", "err", insertErr, "n_urt_recete_id", receteID, "n_urt_m_bolum_id", hNo, "s_kodu", rawSKodu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "commit")
|
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 {
|
||||||
@@ -1942,67 +1854,49 @@ DELETE FROM dbo.spUrtOnMLMas WHERE nOnMLNo = @p1
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// V3: Delete the base price row we created for this costing date (PriceDate = maliyetTarihi).
|
// V3: Delete base price rows we created for this costing date (PriceDate = maliyetTarihi).
|
||||||
// We intentionally do NOT delete older base prices for the same item.
|
// We intentionally do NOT delete older base prices for the same item.
|
||||||
|
// NOTE: UpsertV3ItemBasePriceUSD intentionally uses a non-TR CountryCode to avoid touching the original TR base price.
|
||||||
|
// Therefore delete must NOT filter by CountryCode='TR'; instead, delete by (ItemCode, PriceDate, Currency=USD, BasePriceCode=1)
|
||||||
|
// and only if it was created/updated by this app (Created/LastUpdated starts with BSSAPP).
|
||||||
deletedBasePrice := false
|
deletedBasePrice := false
|
||||||
|
deletedBasePriceCount := 0
|
||||||
if mssqlDB != nil && urunKodu != "" {
|
if mssqlDB != nil && urunKodu != "" {
|
||||||
priceDate := maliyetTarihi.Format("2006-01-02")
|
priceDate := maliyetTarihi.Format("2006-01-02")
|
||||||
// Primary rule: delete only the row for this exact date and USD currency.
|
// Delete only rows owned by this app (Created/LastUpdated starts with BSSAPP).
|
||||||
// Safety: require that either CreatedUserName/LastUpdatedUserName matches current user, or one of them starts with BSSAPP.
|
res, err := mssqlDB.ExecContext(ctx, `
|
||||||
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
|
|
||||||
AND CONVERT(date, PriceDate) = CONVERT(date, @p2, 23)
|
|
||||||
AND LTRIM(RTRIM(ISNULL(CurrencyCode,''))) = 'USD'
|
|
||||||
`, urunKodu, priceDate).Scan(&createdBy, &lastBy)
|
|
||||||
|
|
||||||
created := strings.ToUpper(strings.TrimSpace(createdBy.String))
|
|
||||||
last := strings.ToUpper(strings.TrimSpace(lastBy.String))
|
|
||||||
u := strings.ToUpper(strings.TrimSpace(user))
|
|
||||||
|
|
||||||
allowed := false
|
|
||||||
if u != "" && (strings.ToUpper(strings.TrimSpace(createdBy.String)) == u || strings.ToUpper(strings.TrimSpace(lastBy.String)) == u) {
|
|
||||||
allowed = true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(created, "BSSAPP") || strings.HasPrefix(last, "BSSAPP") {
|
|
||||||
allowed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if allowed {
|
|
||||||
if _, err := mssqlDB.ExecContext(ctx, `
|
|
||||||
DELETE FROM dbo.prItemBasePrice
|
DELETE FROM dbo.prItemBasePrice
|
||||||
WHERE ItemTypeCode = 1
|
WHERE ItemTypeCode = 1
|
||||||
AND LTRIM(RTRIM(ItemCode)) = @p1
|
AND LTRIM(RTRIM(ItemCode)) = @p1
|
||||||
AND ISNULL(CountryCode,'') = 'TR'
|
|
||||||
AND ISNULL(SeasonCode,'') = ''
|
AND ISNULL(SeasonCode,'') = ''
|
||||||
AND ISNULL(BasePriceCode,0) = 1
|
AND ISNULL(BasePriceCode,0) = 1
|
||||||
AND CONVERT(date, PriceDate) = CONVERT(date, @p2, 23)
|
AND CONVERT(date, PriceDate) = CONVERT(date, @p2, 23)
|
||||||
AND LTRIM(RTRIM(ISNULL(CurrencyCode,''))) = 'USD'
|
AND LTRIM(RTRIM(ISNULL(CurrencyCode,''))) = 'USD'
|
||||||
`, urunKodu, priceDate); err == nil {
|
AND (
|
||||||
deletedBasePrice = true
|
UPPER(LTRIM(RTRIM(ISNULL(CreatedUserName,'')))) LIKE 'BSSAPP%'
|
||||||
} else {
|
OR UPPER(LTRIM(RTRIM(ISNULL(LastUpdatedUserName,'')))) LIKE 'BSSAPP%'
|
||||||
|
)
|
||||||
|
`, urunKodu, priceDate)
|
||||||
|
if err != nil {
|
||||||
logger.Warn("v3 base price delete failed", "err", err, "urun_kodu", urunKodu, "price_date", priceDate)
|
logger.Warn("v3 base price delete failed", "err", err, "urun_kodu", urunKodu, "price_date", priceDate)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.Info("v3 base price delete skipped (not owned)", "urun_kodu", urunKodu, "price_date", priceDate, "created_by", createdBy.String, "last_by", lastBy.String, "user", user)
|
if rows, rerr := res.RowsAffected(); rerr == nil {
|
||||||
|
deletedBasePriceCount = int(rows)
|
||||||
|
deletedBasePrice = deletedBasePriceCount > 0
|
||||||
|
} else {
|
||||||
|
// Unknown affected rows, still mark as attempted.
|
||||||
|
deletedBasePrice = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("delete done", "n_onml_no", req.NOnMLNo, "urun_kodu", urunKodu, "deleted_base_price", deletedBasePrice, "user", user)
|
logger.Info("delete done", "n_onml_no", req.NOnMLNo, "urun_kodu", urunKodu, "deleted_base_price", deletedBasePrice, "deleted_base_price_count", deletedBasePriceCount, "user", user)
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"n_onml_no": req.NOnMLNo,
|
"n_onml_no": req.NOnMLNo,
|
||||||
"urun_kodu": urunKodu,
|
"urun_kodu": urunKodu,
|
||||||
"deleted_baseprice": deletedBasePrice,
|
"deleted_baseprice": deletedBasePrice,
|
||||||
|
"deleted_baseprice_count": deletedBasePriceCount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3029,12 +2923,51 @@ func GetProductionProductCostingParcaMappingsHandler(w http.ResponseWriter, r *h
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
row.BAktif = bAktif.Valid && bAktif.Bool
|
row.BAktif = bAktif.Valid && bAktif.Bool
|
||||||
|
// Normalize legacy/duplicate hammadde type numbers so UI doesn't miss required CM2 slots.
|
||||||
|
// Some environments have both inactive and active equivalents (e.g. 463->500, 464->3900, 466->12700).
|
||||||
|
normalizeHNo := func(v int) int {
|
||||||
|
switch v {
|
||||||
|
case 108, 463:
|
||||||
|
return 500 // CKT CM2
|
||||||
|
case 109, 464:
|
||||||
|
return 3900 // PNT CM2
|
||||||
|
case 110, 465:
|
||||||
|
return 8300 // YLK CM2
|
||||||
|
case 466:
|
||||||
|
return 12700 // YKA CM2
|
||||||
|
case 467:
|
||||||
|
return 12100 // AKS CM2
|
||||||
|
case 468:
|
||||||
|
return 13500 // GML CM2
|
||||||
|
case 488:
|
||||||
|
return 15300 // KBN CM2
|
||||||
|
case 493:
|
||||||
|
return 15301 // MNT CM2
|
||||||
|
default:
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seenH := make(map[int]struct{}, 16)
|
||||||
row.NHammaddeTurleri = make([]string, 0, 8)
|
row.NHammaddeTurleri = make([]string, 0, 8)
|
||||||
if hammaddeCsv.Valid {
|
if hammaddeCsv.Valid {
|
||||||
for _, part := range strings.Split(hammaddeCsv.String, ",") {
|
for _, part := range strings.Split(hammaddeCsv.String, ",") {
|
||||||
part = strings.TrimSpace(part)
|
part = strings.TrimSpace(part)
|
||||||
if part != "" {
|
if part != "" {
|
||||||
|
n, err := strconv.Atoi(part)
|
||||||
|
if err != nil {
|
||||||
|
// keep as-is
|
||||||
row.NHammaddeTurleri = append(row.NHammaddeTurleri, part)
|
row.NHammaddeTurleri = append(row.NHammaddeTurleri, part)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n = normalizeHNo(n)
|
||||||
|
if n <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seenH[n]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenH[n] = struct{}{}
|
||||||
|
row.NHammaddeTurleri = append(row.NHammaddeTurleri, strconv.Itoa(n))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
400
svc/routes/production_product_costing_pdf.go
Normal file
400
svc/routes/production_product_costing_pdf.go
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"bssapp-backend/db"
|
||||||
|
"bssapp-backend/models"
|
||||||
|
"bssapp-backend/queries"
|
||||||
|
"bssapp-backend/utils"
|
||||||
|
|
||||||
|
"github.com/jung-kurt/gofpdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /api/pricing/production-product-costing/onml/pdf?n_onml_no=100001
|
||||||
|
// Generates a PDF export for the costing detail screen (has-cost).
|
||||||
|
func GetProductionProductCostingOnMLPDFHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/pdf")
|
||||||
|
|
||||||
|
uretimDB := db.GetUretimDB()
|
||||||
|
if uretimDB == nil {
|
||||||
|
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nOnMLNo := parsePositiveIntOrDefault(r.URL.Query().Get("n_onml_no"), 0)
|
||||||
|
if nOnMLNo <= 0 {
|
||||||
|
http.Error(w, "n_onml_no zorunlu", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.onml.pdf", "n_onml_no", nOnMLNo)
|
||||||
|
logger.Info("request start")
|
||||||
|
|
||||||
|
// Header
|
||||||
|
hRow, err := queries.GetProductionHasCostDetailHeaderByOnMLNo(ctx, uretimDB, nOnMLNo)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("header query prepare error", "err", err)
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var header models.ProductionHasCostDetailHeader
|
||||||
|
if err := hRow.Scan(
|
||||||
|
&header.UretimiYapanFirma,
|
||||||
|
&header.SonIsEmriVeren,
|
||||||
|
&header.FirmaKodu,
|
||||||
|
&header.NFirmaID,
|
||||||
|
&header.NOnMLNo,
|
||||||
|
&header.UrunKodu,
|
||||||
|
&header.UrunAdi,
|
||||||
|
&header.UretimSekliID,
|
||||||
|
&header.UretimSekli,
|
||||||
|
&header.DteKayitTarihi,
|
||||||
|
&header.SKullaniciAdi,
|
||||||
|
&header.LTutarTL,
|
||||||
|
&header.LTutarUSD,
|
||||||
|
&header.LTutarEURO,
|
||||||
|
&header.LTutarGBP,
|
||||||
|
&header.SDovizCinsi,
|
||||||
|
&header.LTutarDoviz,
|
||||||
|
&header.DteGuncellemeTarihi,
|
||||||
|
&header.SGuncellemeKullaniciAdi,
|
||||||
|
&header.NUrtReceteID,
|
||||||
|
); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
http.Error(w, "Kayit bulunamadi", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Error("header scan error", "err", err)
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail groups
|
||||||
|
groups, err := loadHasCostDetailGroups(ctx, uretimDB, nOnMLNo)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("groups load error", "err", err)
|
||||||
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf := gofpdf.New("L", "mm", "A4", "")
|
||||||
|
pdf.SetMargins(8, 8, 8)
|
||||||
|
pdf.SetAutoPageBreak(false, 10)
|
||||||
|
if err := registerDejavuFonts(pdf, "dejavu"); err != nil {
|
||||||
|
http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
export := &costingPDF{
|
||||||
|
pdf: pdf,
|
||||||
|
header: header,
|
||||||
|
groups: groups,
|
||||||
|
}
|
||||||
|
export.draw()
|
||||||
|
|
||||||
|
if err := pdf.Error(); err != nil {
|
||||||
|
http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := pdf.Output(&buf); err != nil {
|
||||||
|
http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("onml-%d.pdf", nOnMLNo)
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, filename))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(buf.Bytes())
|
||||||
|
logger.Info("request done")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadHasCostDetailGroups(ctx context.Context, uretimDB *sql.DB, nOnMLNo int) ([]models.ProductionHasCostDetailGroup, error) {
|
||||||
|
rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
groups := make([]models.ProductionHasCostDetailGroup, 0, 16)
|
||||||
|
groupIndexByName := map[string]int{}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
groupName string
|
||||||
|
groupTotal float64
|
||||||
|
groupTotalUSD float64
|
||||||
|
nOnMLNoStr string
|
||||||
|
nOnMLDetNoStr string
|
||||||
|
hNoStr string
|
||||||
|
fiyatGirilen sql.NullFloat64
|
||||||
|
fiyatDoviz sql.NullString
|
||||||
|
maliyeteDahil sql.NullBool
|
||||||
|
cmPriceTypeID sql.NullInt64
|
||||||
|
item models.ProductionHasCostDetailGroupItem
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := rows.Scan(
|
||||||
|
&groupName,
|
||||||
|
&groupTotal,
|
||||||
|
&groupTotalUSD,
|
||||||
|
&nOnMLNoStr,
|
||||||
|
&nOnMLDetNoStr,
|
||||||
|
&hNoStr,
|
||||||
|
&item.SKodu,
|
||||||
|
&item.SAciklama,
|
||||||
|
&item.SRenk,
|
||||||
|
&item.SBeden,
|
||||||
|
&item.SAciklama2,
|
||||||
|
&item.LMiktar,
|
||||||
|
&item.LFiyat,
|
||||||
|
&item.LTutar,
|
||||||
|
&item.SFiyatTipi,
|
||||||
|
&item.SDovizCinsi,
|
||||||
|
&item.LDovizKuru,
|
||||||
|
&item.LDovizFiyati,
|
||||||
|
&fiyatGirilen,
|
||||||
|
&fiyatDoviz,
|
||||||
|
&maliyeteDahil,
|
||||||
|
&cmPriceTypeID,
|
||||||
|
&item.USDTutar,
|
||||||
|
&item.EURTutar,
|
||||||
|
&item.GBPTutar,
|
||||||
|
&item.SBirim,
|
||||||
|
&item.SHammaddeTuruAdi,
|
||||||
|
&item.SParcaAdi,
|
||||||
|
); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
item.NOnMLNo = strings.TrimSpace(nOnMLNoStr)
|
||||||
|
item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr)
|
||||||
|
item.NHammaddeTuruNo = strings.TrimSpace(hNoStr)
|
||||||
|
if fiyatGirilen.Valid {
|
||||||
|
item.FiyatGirilen = new(float64)
|
||||||
|
*item.FiyatGirilen = fiyatGirilen.Float64
|
||||||
|
}
|
||||||
|
if fiyatDoviz.Valid {
|
||||||
|
item.FiyatDoviz = strings.TrimSpace(fiyatDoviz.String)
|
||||||
|
}
|
||||||
|
item.MaliyeteDahil = maliyeteDahil.Valid && maliyeteDahil.Bool
|
||||||
|
if cmPriceTypeID.Valid {
|
||||||
|
v := int(cmPriceTypeID.Int64)
|
||||||
|
item.CMPriceTypeID = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, ok := groupIndexByName[groupName]
|
||||||
|
if !ok {
|
||||||
|
groups = append(groups, models.ProductionHasCostDetailGroup{
|
||||||
|
SAciklama3: groupName,
|
||||||
|
TotalTutar: groupTotal,
|
||||||
|
TotalUSDTutar: groupTotalUSD,
|
||||||
|
Items: make([]models.ProductionHasCostDetailGroupItem, 0, 24),
|
||||||
|
})
|
||||||
|
idx = len(groups) - 1
|
||||||
|
groupIndexByName[groupName] = idx
|
||||||
|
}
|
||||||
|
groups[idx].Items = append(groups[idx].Items, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PDF drawing ---
|
||||||
|
|
||||||
|
type costingPDF struct {
|
||||||
|
pdf *gofpdf.Fpdf
|
||||||
|
header models.ProductionHasCostDetailHeader
|
||||||
|
groups []models.ProductionHasCostDetailGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) draw() {
|
||||||
|
c.addPage(true)
|
||||||
|
for gi, g := range c.groups {
|
||||||
|
c.drawGroup(g, gi == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) addPage(fullHeader bool) {
|
||||||
|
c.pdf.AddPage()
|
||||||
|
if fullHeader {
|
||||||
|
c.drawHeaderFull()
|
||||||
|
} else {
|
||||||
|
c.drawHeaderCompact()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) drawHeaderFull() {
|
||||||
|
pdf := c.pdf
|
||||||
|
pdf.SetFont("dejavu", "B", 14)
|
||||||
|
pdf.CellFormat(0, 7, "Maliyet Detay", "", 1, "L", false, 0, "")
|
||||||
|
|
||||||
|
pdf.SetFont("dejavu", "", 9)
|
||||||
|
pdf.SetTextColor(60, 60, 60)
|
||||||
|
line1 := fmt.Sprintf("OnML No: %s | Tarih: %s | Uretim Sekli: %s", c.header.NOnMLNo, c.header.DteKayitTarihi, strings.TrimSpace(c.header.UretimSekli))
|
||||||
|
pdf.CellFormat(0, 5, line1, "", 1, "L", false, 0, "")
|
||||||
|
|
||||||
|
line2 := fmt.Sprintf("Urun: %s - %s", strings.TrimSpace(c.header.UrunKodu), strings.TrimSpace(c.header.UrunAdi))
|
||||||
|
pdf.CellFormat(0, 5, line2, "", 1, "L", false, 0, "")
|
||||||
|
|
||||||
|
line3 := fmt.Sprintf("Firma: %s | Kaydeden: %s | Guncelleme: %s (%s)", strings.TrimSpace(c.header.FirmaKodu), strings.TrimSpace(c.header.SKullaniciAdi), strings.TrimSpace(c.header.DteGuncellemeTarihi), strings.TrimSpace(c.header.SGuncellemeKullaniciAdi))
|
||||||
|
pdf.CellFormat(0, 5, line3, "", 1, "L", false, 0, "")
|
||||||
|
|
||||||
|
pdf.SetTextColor(0, 0, 0)
|
||||||
|
pdf.Ln(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) drawHeaderCompact() {
|
||||||
|
pdf := c.pdf
|
||||||
|
pdf.SetFont("dejavu", "B", 10.5)
|
||||||
|
title := fmt.Sprintf("OnML %s | %s - %s | %s", c.header.NOnMLNo, strings.TrimSpace(c.header.UrunKodu), strings.TrimSpace(c.header.UrunAdi), c.header.DteKayitTarihi)
|
||||||
|
pdf.CellFormat(0, 6, title, "", 1, "L", false, 0, "")
|
||||||
|
pdf.Ln(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) drawGroup(g models.ProductionHasCostDetailGroup, firstGroup bool) {
|
||||||
|
pdf := c.pdf
|
||||||
|
|
||||||
|
// Group bar
|
||||||
|
c.drawGroupBar(g, false)
|
||||||
|
|
||||||
|
// Columns
|
||||||
|
cols := []string{"No", "Parca", "Hammadde", "Kod", "Aciklama", "Renk", "Miktar", "Br", "Fiyat", "Pr.Br", "Tutar(TRY)"}
|
||||||
|
wn := []float64{10, 24, 24, 40, 90, 18, 18, 12, 20, 14, 24} // sum ~294 (A4 landscape width minus margins)
|
||||||
|
|
||||||
|
c.drawTableHeader(cols, wn)
|
||||||
|
for _, it := range g.Items {
|
||||||
|
c.drawRowWithGroup(it, wn, cols, g)
|
||||||
|
}
|
||||||
|
pdf.Ln(2)
|
||||||
|
_ = firstGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) drawGroupBar(g models.ProductionHasCostDetailGroup, continued bool) {
|
||||||
|
pdf := c.pdf
|
||||||
|
pdf.SetFont("dejavu", "B", 10)
|
||||||
|
pdf.SetFillColor(245, 245, 245)
|
||||||
|
name := strings.TrimSpace(g.SAciklama3)
|
||||||
|
if continued {
|
||||||
|
name = name + " (devam)"
|
||||||
|
}
|
||||||
|
pdf.CellFormat(0, 6, fmt.Sprintf("%s | Toplam TRY: %s | Toplam USD: %s", name, pdfMoney(g.TotalTutar), pdfMoney(g.TotalUSDTutar)), "1", 1, "L", true, 0, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) drawTableHeader(cols []string, wn []float64) {
|
||||||
|
pdf := c.pdf
|
||||||
|
pdf.SetFont("dejavu", "B", 8)
|
||||||
|
pdf.SetFillColor(30, 30, 30)
|
||||||
|
pdf.SetTextColor(255, 255, 255)
|
||||||
|
for i, col := range cols {
|
||||||
|
pdf.CellFormat(wn[i], 5.5, col, "1", 0, "C", true, 0, "")
|
||||||
|
}
|
||||||
|
pdf.Ln(5.5)
|
||||||
|
pdf.SetTextColor(0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) ensureSpace(h float64) (newPage bool) {
|
||||||
|
pdf := c.pdf
|
||||||
|
_, pageH := pdf.GetPageSize()
|
||||||
|
_, _, _, mb := pdf.GetMargins()
|
||||||
|
if pdf.GetY()+h > pageH-mb {
|
||||||
|
c.addPage(false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) drawRowWithGroup(it models.ProductionHasCostDetailGroupItem, wn []float64, cols []string, g models.ProductionHasCostDetailGroup) {
|
||||||
|
pdf := c.pdf
|
||||||
|
pdf.SetFont("dejavu", "", 7.2)
|
||||||
|
|
||||||
|
// Compute row height based on description wrapping.
|
||||||
|
descLines := pdf.SplitLines([]byte(strings.TrimSpace(it.SAciklama)), wn[4]-2)
|
||||||
|
rowH := float64(len(descLines)) * 3.5
|
||||||
|
if rowH < 5.0 {
|
||||||
|
rowH = 5.0
|
||||||
|
}
|
||||||
|
if c.ensureSpace(rowH + 12.0) {
|
||||||
|
// Redraw group bar + table header on new page.
|
||||||
|
c.drawGroupBar(g, true)
|
||||||
|
c.drawTableHeader(cols, wn)
|
||||||
|
}
|
||||||
|
|
||||||
|
x0 := pdf.GetX()
|
||||||
|
y0 := pdf.GetY()
|
||||||
|
|
||||||
|
c.drawCell(x0, y0, wn[0], rowH, it.NOnMLDetNo, "R")
|
||||||
|
x := x0 + wn[0]
|
||||||
|
c.drawCell(x, y0, wn[1], rowH, strings.TrimSpace(it.SParcaAdi), "L")
|
||||||
|
x += wn[1]
|
||||||
|
hLabel := strings.TrimSpace(it.NHammaddeTuruNo)
|
||||||
|
if strings.TrimSpace(it.SHammaddeTuruAdi) != "" {
|
||||||
|
hLabel = hLabel + " " + strings.TrimSpace(it.SHammaddeTuruAdi)
|
||||||
|
}
|
||||||
|
c.drawCell(x, y0, wn[2], rowH, hLabel, "L")
|
||||||
|
x += wn[2]
|
||||||
|
c.drawCell(x, y0, wn[3], rowH, strings.TrimSpace(it.SKodu), "L")
|
||||||
|
x += wn[3]
|
||||||
|
c.drawCellWrap(x, y0, wn[4], rowH, strings.TrimSpace(it.SAciklama), "L")
|
||||||
|
x += wn[4]
|
||||||
|
c.drawCell(x, y0, wn[5], rowH, strings.TrimSpace(it.SRenk), "L")
|
||||||
|
x += wn[5]
|
||||||
|
c.drawCell(x, y0, wn[6], rowH, pdfQty(it.LMiktar), "R")
|
||||||
|
x += wn[6]
|
||||||
|
c.drawCell(x, y0, wn[7], rowH, strings.TrimSpace(it.SBirim), "C")
|
||||||
|
x += wn[7]
|
||||||
|
|
||||||
|
// Prefer input price if present; otherwise lFiyat.
|
||||||
|
price := it.LFiyat
|
||||||
|
cur := strings.TrimSpace(it.SDovizCinsi)
|
||||||
|
if it.FiyatGirilen != nil && *it.FiyatGirilen > 0 {
|
||||||
|
price = *it.FiyatGirilen
|
||||||
|
if strings.TrimSpace(it.FiyatDoviz) != "" {
|
||||||
|
cur = strings.TrimSpace(it.FiyatDoviz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.drawCell(x, y0, wn[8], rowH, pdfMoney(price), "R")
|
||||||
|
x += wn[8]
|
||||||
|
c.drawCell(x, y0, wn[9], rowH, cur, "C")
|
||||||
|
x += wn[9]
|
||||||
|
c.drawCell(x, y0, wn[10], rowH, pdfMoney(it.LTutar), "R")
|
||||||
|
|
||||||
|
pdf.SetXY(x0, y0+rowH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) drawCell(x, y, w, h float64, txt, align string) {
|
||||||
|
pdf := c.pdf
|
||||||
|
pdf.Rect(x, y, w, h, "")
|
||||||
|
pdf.SetXY(x+0.8, y+(h-3.5)/2)
|
||||||
|
pdf.CellFormat(w-1.6, 3.5, txt, "", 0, align, false, 0, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *costingPDF) drawCellWrap(x, y, w, h float64, txt, align string) {
|
||||||
|
pdf := c.pdf
|
||||||
|
pdf.Rect(x, y, w, h, "")
|
||||||
|
pdf.SetXY(x+0.8, y+0.6)
|
||||||
|
pdf.MultiCell(w-1.6, 3.5, txt, "", align, false)
|
||||||
|
// restore cursor (MultiCell moves Y)
|
||||||
|
pdf.SetXY(x+w, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pdfMoney(v float64) string {
|
||||||
|
// 2 decimals, dot
|
||||||
|
return fmt.Sprintf("%.2f", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pdfQty(v float64) string {
|
||||||
|
// 4 decimals (screen-like)
|
||||||
|
return fmt.Sprintf("%.4f", v)
|
||||||
|
}
|
||||||
@@ -84,6 +84,16 @@
|
|||||||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
label="PDF"
|
||||||
|
icon="picture_as_pdf"
|
||||||
|
dense
|
||||||
|
color="grey-9"
|
||||||
|
outline
|
||||||
|
class="pcd-toolbar-btn"
|
||||||
|
:disable="!detailHeader || detailLoading || saveLoading"
|
||||||
|
@click="exportCostingPDF"
|
||||||
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Kaydi Sil"
|
label="Kaydi Sil"
|
||||||
icon="delete"
|
icon="delete"
|
||||||
@@ -244,6 +254,9 @@
|
|||||||
{{ grp.sAciklama3 || 'TANIMSIZ' }}
|
{{ grp.sAciklama3 || 'TANIMSIZ' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)">
|
<div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)">
|
||||||
|
<span v-if="normalizeGroupName(grp.sAciklama3) === 'FABRIC'" class="q-mr-sm">
|
||||||
|
Toplam Miktar: {{ formatBarQuantity(resolveGroupQuantity(grp)) }} MT |
|
||||||
|
</span>
|
||||||
Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }}
|
Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }}
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
|
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
|
||||||
@@ -529,6 +542,16 @@
|
|||||||
@filter="filterRowEditorItemOptions"
|
@filter="filterRowEditorItemOptions"
|
||||||
@update:model-value="onRowEditorItemChange"
|
@update:model-value="onRowEditorItemChange"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="showFabricCopyBtn" class="q-mt-xs row items-center q-gutter-xs">
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
outline
|
||||||
|
color="primary"
|
||||||
|
icon="content_copy"
|
||||||
|
label="Kumastan Kopyala"
|
||||||
|
@click="openFabricCopyDialog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<q-select
|
<q-select
|
||||||
@@ -599,6 +622,41 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<!-- FABRIC copy helper: pick another fabric row and copy Code + Price + Pr.Br into editor -->
|
||||||
|
<q-dialog v-model="fabricCopyDialogOpen">
|
||||||
|
<q-card style="min-width: min(720px, 92vw);">
|
||||||
|
<q-card-section class="row items-center justify-between q-pb-sm">
|
||||||
|
<div class="text-subtitle1 text-weight-bold">Kumastan Kopyala</div>
|
||||||
|
<q-btn flat round dense icon="close" v-close-popup />
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
<q-card-section class="q-pa-md">
|
||||||
|
<q-select
|
||||||
|
v-model="fabricCopySelectedKey"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
label="Kaynak Kumaş"
|
||||||
|
:options="fabricCopyOptions"
|
||||||
|
option-value="value"
|
||||||
|
option-label="label"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
<q-card-actions align="right" class="q-pa-sm">
|
||||||
|
<q-btn flat label="Iptal" color="grey-7" v-close-popup />
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
label="Kopyala"
|
||||||
|
:disable="!fabricCopySelectedKey"
|
||||||
|
@click="applyFabricCopySelection"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="lineHistoryDialogOpen" maximized>
|
<q-dialog v-model="lineHistoryDialogOpen" maximized>
|
||||||
<q-card class="pcd-history-dialog">
|
<q-card class="pcd-history-dialog">
|
||||||
<q-card-section class="row items-center justify-between q-gutter-sm">
|
<q-card-section class="row items-center justify-between q-gutter-sm">
|
||||||
@@ -744,7 +802,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
import { get, post, extractApiErrorDetail } from 'src/services/api'
|
import { get, post, download, extractApiErrorDetail } from 'src/services/api'
|
||||||
import { createTraceId, slog } from 'src/utils/slog'
|
import { createTraceId, slog } from 'src/utils/slog'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -773,6 +831,8 @@ 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())
|
||||||
|
const fabricCopyDialogOpen = ref(false)
|
||||||
|
const fabricCopySelectedKey = ref('')
|
||||||
|
|
||||||
// Draggable "Satir Duzenle" dialog (mouse drag by header).
|
// Draggable "Satir Duzenle" dialog (mouse drag by header).
|
||||||
const rowEditorDialogPos = ref({ x: 0, y: 0 })
|
const rowEditorDialogPos = ref({ x: 0, y: 0 })
|
||||||
@@ -872,10 +932,53 @@ const priceCurrencyOptions = [
|
|||||||
]
|
]
|
||||||
const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : []))
|
const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : []))
|
||||||
|
|
||||||
|
const showFabricCopyBtn = computed(() => normalizeGroupName(rowEditorForm.value?.sAciklama3 || '') === 'FABRIC')
|
||||||
|
|
||||||
|
const fabricCopySourceRows = computed(() => {
|
||||||
|
const currentKey = String(rowEditorTargetRowKey.value || '').trim()
|
||||||
|
return (Array.isArray(flatDetailRows.value) ? flatDetailRows.value : [])
|
||||||
|
.filter(r => normalizeGroupName(r?.sAciklama3 || '') === 'FABRIC')
|
||||||
|
.filter(r => String(r?.__rowKey || '').trim() && String(r?.__rowKey || '').trim() !== currentKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fabricCopyOptions = computed(() => {
|
||||||
|
// Deduplicate for UX: group by (code + color + hammaddeNo). Keep most recently changed first.
|
||||||
|
const list = (Array.isArray(fabricCopySourceRows.value) ? fabricCopySourceRows.value : [])
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => {
|
||||||
|
const da = new Date(a?.dteIslemTarihiDeg || a?.dteIslemTarihi || 0).getTime()
|
||||||
|
const db = new Date(b?.dteIslemTarihiDeg || b?.dteIslemTarihi || 0).getTime()
|
||||||
|
return db - da
|
||||||
|
})
|
||||||
|
|
||||||
|
const seen = new Set()
|
||||||
|
const out = []
|
||||||
|
for (const r of list) {
|
||||||
|
const code = String(r?.sKodu || '').trim()
|
||||||
|
if (!code) continue
|
||||||
|
const color = String(resolveRowColorCode(r) || '').trim()
|
||||||
|
const hNo = String(r?.nHammaddeTuruNo || '').trim()
|
||||||
|
const hName = String(r?.sHammaddeTuruAdi || '').trim()
|
||||||
|
const key = `${hNo}|${code}|${color}`
|
||||||
|
if (seen.has(key)) continue
|
||||||
|
seen.add(key)
|
||||||
|
const price = resolveNumericRowInputPrice(r)
|
||||||
|
const cur = resolveInputCurrency(r)
|
||||||
|
const desc = String(r?.sAciklama || '').trim()
|
||||||
|
const hLabel = `${hNo}${hName ? ` - ${hName}` : ''}`
|
||||||
|
const label = `${hLabel} | ${code}${desc ? ` - ${desc}` : ''}${color ? ` | ${color}` : ''}${price > 0 ? ` | ${price} ${cur}` : ''}`
|
||||||
|
out.push({ value: key, label })
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
// no-cost: required parca slots (from Maliyet Parca Eslestirme)
|
// no-cost: required parca slots (from Maliyet Parca Eslestirme)
|
||||||
const requiredParcaMappings = ref([])
|
const requiredParcaMappings = ref([])
|
||||||
const requiredAttentionRowKeys = ref({})
|
const requiredAttentionRowKeys = ref({})
|
||||||
const requiredHammaddeMetaCache = ref({}) // hammaddeNo -> { groupName, parcaAdi }
|
const requiredHammaddeMetaCache = ref({}) // hammaddeNo -> { groupName, parcaAdi }
|
||||||
|
// When user explicitly deletes a requiredPlaceholder row, we should not keep warning/recreating it
|
||||||
|
// for this draft/session. Key format: `${mtBolumID}:${hNo}`.
|
||||||
|
const suppressedRequiredKeys = ref({})
|
||||||
|
|
||||||
// Bump this when draft payload semantics change, to avoid hydrating stale rows after backend logic updates.
|
// Bump this when draft payload semantics change, to avoid hydrating stale rows after backend logic updates.
|
||||||
const DRAFT_STORAGE_VERSION = 'v2'
|
const DRAFT_STORAGE_VERSION = 'v2'
|
||||||
@@ -929,6 +1032,7 @@ function persistLocalDraftNow () {
|
|||||||
UretimSekli: String(detailHeader.value?.UretimSekli || '').trim()
|
UretimSekli: String(detailHeader.value?.UretimSekli || '').trim()
|
||||||
} : null,
|
} : null,
|
||||||
deletedDetailRows: Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : [],
|
deletedDetailRows: Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : [],
|
||||||
|
suppressedRequiredKeys: suppressedRequiredKeys.value || {},
|
||||||
detailGroups: Array.isArray(detailGroups.value) ? detailGroups.value : []
|
detailGroups: Array.isArray(detailGroups.value) ? detailGroups.value : []
|
||||||
}
|
}
|
||||||
localStorage.setItem(key, JSON.stringify(payload))
|
localStorage.setItem(key, JSON.stringify(payload))
|
||||||
@@ -972,6 +1076,7 @@ function tryHydrateFromLocalDraft () {
|
|||||||
const payload = JSON.parse(raw)
|
const payload = JSON.parse(raw)
|
||||||
if (!payload || typeof payload !== 'object') return false
|
if (!payload || typeof payload !== 'object') return false
|
||||||
if (Array.isArray(payload.deletedDetailRows)) deletedDetailRows.value = payload.deletedDetailRows
|
if (Array.isArray(payload.deletedDetailRows)) deletedDetailRows.value = payload.deletedDetailRows
|
||||||
|
if (payload.suppressedRequiredKeys && typeof payload.suppressedRequiredKeys === 'object') suppressedRequiredKeys.value = payload.suppressedRequiredKeys
|
||||||
if (Array.isArray(payload.detailGroups)) detailGroups.value = normalizeDetailGroups(payload.detailGroups)
|
if (Array.isArray(payload.detailGroups)) detailGroups.value = normalizeDetailGroups(payload.detailGroups)
|
||||||
if (payload.header && detailHeader.value) {
|
if (payload.header && detailHeader.value) {
|
||||||
detailHeader.value.UretimSekliID = String(payload.header.UretimSekliID || '').trim()
|
detailHeader.value.UretimSekliID = String(payload.header.UretimSekliID || '').trim()
|
||||||
@@ -1189,6 +1294,11 @@ function formatBarMoney (value) {
|
|||||||
return formatMoney(roundedValue)
|
return formatMoney(roundedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBarQuantity (value) {
|
||||||
|
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 10000) / 10000
|
||||||
|
return formatQuantity(roundedValue)
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePriceCurrency (value) {
|
function normalizePriceCurrency (value) {
|
||||||
const normalizedValue = String(value || '').trim().toUpperCase()
|
const normalizedValue = String(value || '').trim().toUpperCase()
|
||||||
return ['USD', 'TRY', 'EUR', 'GBP'].includes(normalizedValue) ? normalizedValue : ''
|
return ['USD', 'TRY', 'EUR', 'GBP'].includes(normalizedValue) ? normalizedValue : ''
|
||||||
@@ -2091,6 +2201,11 @@ function resolveGroupUSDTutar (grp) {
|
|||||||
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowUSDTutar(row) : 0), 0)
|
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowUSDTutar(row) : 0), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveGroupQuantity (grp) {
|
||||||
|
const items = Array.isArray(grp?.items) ? grp.items : []
|
||||||
|
return items.reduce((acc, row) => acc + (Number(row?.lMiktar || 0) || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
function groupKey (grp, gi) {
|
function groupKey (grp, gi) {
|
||||||
return `${String(grp?.sAciklama3 || 'TANIMSIZ')}-${gi}`
|
return `${String(grp?.sAciklama3 || 'TANIMSIZ')}-${gi}`
|
||||||
}
|
}
|
||||||
@@ -2264,7 +2379,11 @@ async function fetchDetail (options = {}) {
|
|||||||
if (hydrateDraft) {
|
if (hydrateDraft) {
|
||||||
tryHydrateFromLocalDraft()
|
tryHydrateFromLocalDraft()
|
||||||
}
|
}
|
||||||
// ensure required placeholder rows exist (based on mapping screen)
|
// Ensure required placeholder rows exist (based on mapping screen) only for the "new/no-cost" flow.
|
||||||
|
// For has-cost records (existing), we do not inject placeholders after load.
|
||||||
|
// Placeholders should be created only when detail_source=no-cost (route query),
|
||||||
|
// so saved records always render exactly what backend has.
|
||||||
|
if (isNoCostDetail.value) {
|
||||||
try {
|
try {
|
||||||
const mappings = await fetchRequiredParcaMappings()
|
const mappings = await fetchRequiredParcaMappings()
|
||||||
await ensureNoCostRequiredRowsFromMappings(mappings)
|
await ensureNoCostRequiredRowsFromMappings(mappings)
|
||||||
@@ -2274,6 +2393,7 @@ async function fetchDetail (options = {}) {
|
|||||||
detail: await extractApiErrorDetail(err)
|
detail: await extractApiErrorDetail(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const initialOpen = {}
|
const initialOpen = {}
|
||||||
detailGroups.value.forEach((grp, gi) => {
|
detailGroups.value.forEach((grp, gi) => {
|
||||||
initialOpen[groupKey(grp, gi)] = true
|
initialOpen[groupKey(grp, gi)] = true
|
||||||
@@ -2821,6 +2941,44 @@ function onRowEditorHammaddeChange (value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openFabricCopyDialog () {
|
||||||
|
fabricCopySelectedKey.value = ''
|
||||||
|
fabricCopyDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyFabricCopySelection () {
|
||||||
|
const key = String(fabricCopySelectedKey.value || '').trim()
|
||||||
|
if (!key) return
|
||||||
|
const match = fabricCopySourceRows.value.find(r => {
|
||||||
|
const code = String(r?.sKodu || '').trim()
|
||||||
|
const color = String(resolveRowColorCode(r) || '').trim()
|
||||||
|
const hNo = String(r?.nHammaddeTuruNo || '').trim()
|
||||||
|
return `${hNo}|${code}|${color}` === key
|
||||||
|
})
|
||||||
|
if (!match) {
|
||||||
|
fabricCopyDialogOpen.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy: Code + Price + Currency. Leave color as-is (user may be copying price only).
|
||||||
|
rowEditorForm.value.sKodu = String(match.sKodu || '').trim()
|
||||||
|
// Ensure select can render the chosen value even if it isn't in the current options list.
|
||||||
|
upsertEditorOption(rowEditorItemAllOptions, buildRowEditorItemOption({ ...match, value: String(match.sKodu || '').trim() }))
|
||||||
|
upsertEditorOption(rowEditorItemOptions, buildRowEditorItemOption({ ...match, value: String(match.sKodu || '').trim() }))
|
||||||
|
try {
|
||||||
|
await onRowEditorItemChange(String(match.sKodu || '').trim())
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const price = resolveNumericRowInputPrice(match)
|
||||||
|
const cur = resolveInputCurrency(match) || 'USD'
|
||||||
|
rowEditorForm.value.inputPrice = normalizeInputPrice(price)
|
||||||
|
rowEditorForm.value.fiyat_girilen = parseMoneyInput(price)
|
||||||
|
rowEditorForm.value.inputPricePrBr = normalizePriceCurrency(cur) || 'USD'
|
||||||
|
rowEditorForm.value.fiyat_doviz = normalizePriceCurrency(cur) || 'USD'
|
||||||
|
|
||||||
|
fabricCopyDialogOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
async function onRowEditorItemChange (value) {
|
async function onRowEditorItemChange (value) {
|
||||||
const selected = rowEditorItemOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
const selected = rowEditorItemOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||||||
rowEditorForm.value.sKodu = String(value || '').trim()
|
rowEditorForm.value.sKodu = String(value || '').trim()
|
||||||
@@ -2980,8 +3138,9 @@ async function fetchRequiredParcaMappings () {
|
|||||||
async function resolveHammaddeMetaForRequired (hNo) {
|
async function resolveHammaddeMetaForRequired (hNo) {
|
||||||
const key = String(hNo || '').trim()
|
const key = String(hNo || '').trim()
|
||||||
if (!key) return null
|
if (!key) return null
|
||||||
|
// Always attempt a fresh resolve; backend rules for active/inactive hammadde types can change.
|
||||||
|
// We still store into cache as a last-known-good for the current page load.
|
||||||
const cached = requiredHammaddeMetaCache.value?.[key]
|
const cached = requiredHammaddeMetaCache.value?.[key]
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rows = await get('/pricing/production-product-costing/detail-editor-options', {
|
const rows = await get('/pricing/production-product-costing/detail-editor-options', {
|
||||||
@@ -3009,7 +3168,7 @@ async function resolveHammaddeMetaForRequired (hNo) {
|
|||||||
return meta
|
return meta
|
||||||
} catch {
|
} catch {
|
||||||
requiredHammaddeMetaCache.value = { ...(requiredHammaddeMetaCache.value || {}), [key]: { groupName: '', hammaddeAdi: '', mtBolumID: 0, parcaAdi: '' } }
|
requiredHammaddeMetaCache.value = { ...(requiredHammaddeMetaCache.value || {}), [key]: { groupName: '', hammaddeAdi: '', mtBolumID: 0, parcaAdi: '' } }
|
||||||
return null
|
return cached || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3067,6 +3226,7 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
|||||||
const hNo = normalizeHammaddeNo(hNoRaw)
|
const hNo = normalizeHammaddeNo(hNoRaw)
|
||||||
if (!hNo) continue
|
if (!hNo) continue
|
||||||
const reqKey = `${mappingMtBolumID}:${hNo}`
|
const reqKey = `${mappingMtBolumID}:${hNo}`
|
||||||
|
if (suppressedRequiredKeys.value?.[reqKey]) continue
|
||||||
if (processedRequiredKeys.has(reqKey)) continue
|
if (processedRequiredKeys.has(reqKey)) continue
|
||||||
processedRequiredKeys.add(reqKey)
|
processedRequiredKeys.add(reqKey)
|
||||||
|
|
||||||
@@ -3093,12 +3253,19 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
|||||||
if (anyMatch) {
|
if (anyMatch) {
|
||||||
// If we previously created a placeholder under a wrong group name, move it instead of duplicating.
|
// If we previously created a placeholder under a wrong group name, move it instead of duplicating.
|
||||||
if (anyMatch?.requiredPlaceholder) {
|
if (anyMatch?.requiredPlaceholder) {
|
||||||
|
const ensuredDetNo = String(anyMatch?.nOnMLDetNo || '').trim() || getNextDetailNo()
|
||||||
const currentGroup = normalizeGroupName(anyMatch?.sAciklama3)
|
const currentGroup = normalizeGroupName(anyMatch?.sAciklama3)
|
||||||
const currentParcaAdi = normalizeGroupName(anyMatch?.sParcaAdi || '')
|
const currentParcaAdi = normalizeGroupName(anyMatch?.sParcaAdi || '')
|
||||||
const currentHammaddeAdi = String(anyMatch?.sHammaddeTuruAdi || '').trim()
|
const currentHammaddeAdi = String(anyMatch?.sHammaddeTuruAdi || '').trim()
|
||||||
if (currentGroup !== effectiveGroupName || currentParcaAdi !== desiredParcaAdi || (hammaddeAdi && currentHammaddeAdi !== hammaddeAdi)) {
|
if (
|
||||||
|
String(anyMatch?.nOnMLDetNo || '').trim() !== ensuredDetNo ||
|
||||||
|
currentGroup !== effectiveGroupName ||
|
||||||
|
currentParcaAdi !== desiredParcaAdi ||
|
||||||
|
(hammaddeAdi && currentHammaddeAdi !== hammaddeAdi)
|
||||||
|
) {
|
||||||
const moved = recalculateDetailRow({
|
const moved = recalculateDetailRow({
|
||||||
...anyMatch,
|
...anyMatch,
|
||||||
|
nOnMLDetNo: ensuredDetNo,
|
||||||
sAciklama3: effectiveGroupName,
|
sAciklama3: effectiveGroupName,
|
||||||
sParcaAdi: desiredParcaAdi,
|
sParcaAdi: desiredParcaAdi,
|
||||||
nUrtMTBolumID: mtBolumID || anyMatch?.nUrtMTBolumID || anyMatch?.NUrtMTBolumID || 0,
|
nUrtMTBolumID: mtBolumID || anyMatch?.nUrtMTBolumID || anyMatch?.NUrtMTBolumID || 0,
|
||||||
@@ -3124,7 +3291,8 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
|||||||
isNew: true,
|
isNew: true,
|
||||||
nUrtMTBolumID: mtBolumID,
|
nUrtMTBolumID: mtBolumID,
|
||||||
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||||||
nOnMLDetNo: '',
|
// IMPORTANT: Backend skips upserts where nOnMLDetNo <= 0. Required placeholders must have a real number.
|
||||||
|
nOnMLDetNo: getNextDetailNo(),
|
||||||
// Group header key
|
// Group header key
|
||||||
sAciklama3: effectiveGroupName,
|
sAciklama3: effectiveGroupName,
|
||||||
// Parca adi column value (CEKET/PANTOLON/YELEK...)
|
// Parca adi column value (CEKET/PANTOLON/YELEK...)
|
||||||
@@ -3162,6 +3330,142 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CM2 auto-required: include all active CM2 hammadde types (bAktif=1) as required placeholders too.
|
||||||
|
// This is independent of the mapping screen, because CM2 labor lines are managed in OnML detail.
|
||||||
|
try {
|
||||||
|
// Filter CM2 types by the parts that exist for this product (MTBolumIDs coming from mapping list).
|
||||||
|
// This prevents irrelevant CM2 types (e.g. GML/KBN/MNT) from showing up for products that don't have those parts.
|
||||||
|
const allowedMtBolumIDs = new Set()
|
||||||
|
for (const m of list) {
|
||||||
|
const mt = parseInt(String(m?.nUrtMTBolumID ?? m?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||||
|
if (mt > 0) allowedMtBolumIDs.add(mt)
|
||||||
|
}
|
||||||
|
if (allowedMtBolumIDs.size === 0) {
|
||||||
|
// No parts to scope CM2 by -> don't add global CM2 placeholders.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cm2Types = await get('/pricing/production-product-costing/detail-editor-options', {
|
||||||
|
kind: 'hammadde',
|
||||||
|
group: 'CM2',
|
||||||
|
only_active: 1,
|
||||||
|
search: '',
|
||||||
|
limit: 200,
|
||||||
|
trace_id: traceId.value
|
||||||
|
})
|
||||||
|
const cm2List = Array.isArray(cm2Types) ? cm2Types : []
|
||||||
|
for (const it of cm2List) {
|
||||||
|
const hNo = normalizeHammaddeNo(it?.nHammaddeTuruNo ?? it?.value)
|
||||||
|
if (!hNo) continue
|
||||||
|
const meta = {
|
||||||
|
groupName: String(it?.sAciklama3 || it?.sAciklama2 || 'CM2').trim(),
|
||||||
|
hammaddeAdi: String(it?.sHammaddeTuruAdi || it?.sAciklama || '').trim(),
|
||||||
|
mtBolumID: parseInt(String(it?.mtUrtMTBolumID ?? it?.MTUrtMTBolumID ?? it?.nUrtMTBolumID ?? it?.NUrtMTBolumID ?? '0'), 10) || 0,
|
||||||
|
parcaAdi: String(it?.sParcaAdi || '').trim()
|
||||||
|
}
|
||||||
|
if (!(meta.mtBolumID > 0) || !allowedMtBolumIDs.has(meta.mtBolumID)) continue
|
||||||
|
const effectiveGroupName = normalizeGroupName(meta.groupName || 'CM2') || 'CM2'
|
||||||
|
const mtBolumID = meta.mtBolumID || 0
|
||||||
|
const desiredParcaAdi = normalizeGroupName(meta.parcaAdi || '') || 'CM2'
|
||||||
|
const reqKey = `${mtBolumID}:${hNo}`
|
||||||
|
if (mtBolumID > 0 && suppressedRequiredKeys.value?.[reqKey]) continue
|
||||||
|
|
||||||
|
const anyMatch = flatDetailRows.value.find(r => {
|
||||||
|
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||||||
|
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||||
|
if (rowMtBolumID > 0 && mtBolumID > 0) return rowMtBolumID === mtBolumID
|
||||||
|
return normalizeGroupName(r?.sParcaAdi) === desiredParcaAdi
|
||||||
|
})
|
||||||
|
if (anyMatch) {
|
||||||
|
// Ensure existing required CM2 placeholder has a valid detNo, otherwise backend will skip it on save.
|
||||||
|
if (anyMatch?.requiredPlaceholder && String(anyMatch?.nOnMLDetNo || '').trim() === '') {
|
||||||
|
const patched = recalculateDetailRow({
|
||||||
|
...anyMatch,
|
||||||
|
nOnMLDetNo: getNextDetailNo(),
|
||||||
|
draftChanged: true
|
||||||
|
}, {
|
||||||
|
preserveInputs: true,
|
||||||
|
priceType: 'REQ',
|
||||||
|
updateState: 'required-cm2',
|
||||||
|
markChanged: true
|
||||||
|
})
|
||||||
|
applyEditorRowToGroups(patched)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newRowSequence.value += 1
|
||||||
|
const rowKey = `req-auto-cm2-row-${newRowSequence.value}`
|
||||||
|
const placeholder = recalculateDetailRow({
|
||||||
|
__rowKey: rowKey,
|
||||||
|
isNew: true,
|
||||||
|
nUrtMTBolumID: mtBolumID,
|
||||||
|
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||||||
|
// IMPORTANT: Backend skips upserts where nOnMLDetNo <= 0. Required placeholders must have a real number.
|
||||||
|
nOnMLDetNo: getNextDetailNo(),
|
||||||
|
sAciklama3: effectiveGroupName,
|
||||||
|
sParcaAdi: desiredParcaAdi,
|
||||||
|
nHammaddeTuruNo: hNo,
|
||||||
|
sHammaddeTuruAdi: meta.hammaddeAdi,
|
||||||
|
sKodu: '',
|
||||||
|
sAciklama: '',
|
||||||
|
sRenk: '',
|
||||||
|
ColorCode: '',
|
||||||
|
ColorDescription: '',
|
||||||
|
lMiktar: 1,
|
||||||
|
miktarInput: '1',
|
||||||
|
inputPrice: '0',
|
||||||
|
inputPricePrBr: 'USD',
|
||||||
|
fiyat_girilen: 0,
|
||||||
|
fiyat_doviz: 'USD',
|
||||||
|
maliyeteDahil: true,
|
||||||
|
maliyete_dahil: 1,
|
||||||
|
Maliyete_dahil: 1,
|
||||||
|
cmPriceTypeId: normalizeCMPriceTypeId(1, effectiveGroupName),
|
||||||
|
cm_price_type_id: normalizeCMPriceTypeId(1, effectiveGroupName),
|
||||||
|
sBirim: 'AD',
|
||||||
|
draftChanged: true,
|
||||||
|
requiredPlaceholder: true
|
||||||
|
}, {
|
||||||
|
preserveInputs: true,
|
||||||
|
priceType: 'REQ',
|
||||||
|
updateState: 'required-cm2',
|
||||||
|
markChanged: true
|
||||||
|
})
|
||||||
|
applyEditorRowToGroups(placeholder)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
slog.error('production-product-costing.detail', 'cm2:required:error', {
|
||||||
|
trace_id: traceId.value,
|
||||||
|
error: String(e?.message || e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: required placeholders must always have a valid detNo.
|
||||||
|
try {
|
||||||
|
const flatNow = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
|
||||||
|
const reqRows = flatNow.filter(r => Boolean(r?.requiredPlaceholder))
|
||||||
|
const missingDetNo = reqRows.filter(r => (parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0) <= 0)
|
||||||
|
slog.info('production-product-costing.detail', 'required:placeholders:ensured', {
|
||||||
|
trace_id: traceId.value,
|
||||||
|
required_count: reqRows.length,
|
||||||
|
required_missing_detno: missingDetNo.length,
|
||||||
|
required_cm2_count: reqRows.filter(r => normalizeGroupName(r?.sAciklama3) === 'CM2').length
|
||||||
|
})
|
||||||
|
if (missingDetNo.length > 0) {
|
||||||
|
slog.warn('production-product-costing.detail', 'required:placeholders:missing-detno', {
|
||||||
|
trace_id: traceId.value,
|
||||||
|
sample: missingDetNo.slice(0, 10).map(r => ({
|
||||||
|
detNo: String(r?.nOnMLDetNo || ''),
|
||||||
|
hNo: String(r?.nHammaddeTuruNo || ''),
|
||||||
|
mtBolumID: String(r?.nUrtMTBolumID || ''),
|
||||||
|
group: String(r?.sAciklama3 || ''),
|
||||||
|
rowKey: String(r?.__rowKey || '')
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
// CM2 special case:
|
// CM2 special case:
|
||||||
// Bulk autofill is disabled for CM2 (and FABRIC) to prevent wrong price pulls,
|
// 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.
|
// but we still want the I.* code templates (code + description) to appear on page open.
|
||||||
@@ -3243,6 +3547,9 @@ function computeMissingRequiredSlots () {
|
|||||||
const missing = []
|
const missing = []
|
||||||
if (list.length === 0) return missing
|
if (list.length === 0) return missing
|
||||||
|
|
||||||
|
// Expand suppression keys: support both mappingMtBolumID and meta-derived mtBolumID.
|
||||||
|
const suppressed = suppressedRequiredKeys.value || {}
|
||||||
|
|
||||||
list.forEach(mapping => {
|
list.forEach(mapping => {
|
||||||
// Required slots are defined per (ParcaBolum / MTBolumID) + (HammaddeTuruNo).
|
// 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.
|
// Do NOT match by row.sAciklama3, because that field is the "group header" (DT/TP/CM2/FABRIC), not the part.
|
||||||
@@ -3252,6 +3559,33 @@ function computeMissingRequiredSlots () {
|
|||||||
hList.forEach(hNoRaw => {
|
hList.forEach(hNoRaw => {
|
||||||
const hNo = normalizeHammaddeNo(hNoRaw)
|
const hNo = normalizeHammaddeNo(hNoRaw)
|
||||||
if (!hNo) return
|
if (!hNo) return
|
||||||
|
const reqKeyByMapping = `${mappingMtBolumID}:${hNo}`
|
||||||
|
// Also allow suppression by the mtBolumID that the placeholder row itself uses (meta override).
|
||||||
|
const reqKeyByRowMt = (() => {
|
||||||
|
const rowLike = flatDetailRows.value.find(r => normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo && Boolean(r?.requiredPlaceholder))
|
||||||
|
const rowMt = parseInt(String(rowLike?.nUrtMTBolumID ?? rowLike?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||||
|
return rowMt > 0 ? `${rowMt}:${hNo}` : ''
|
||||||
|
})()
|
||||||
|
|
||||||
|
// User explicitly removed this required placeholder in the UI; don't keep warning about it.
|
||||||
|
if (suppressed?.[reqKeyByMapping] || (reqKeyByRowMt && suppressed?.[reqKeyByRowMt])) {
|
||||||
|
// Auto-clear suppression if a real row exists again.
|
||||||
|
const existing = flatDetailRows.value.find(r => {
|
||||||
|
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||||||
|
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||||
|
if (mappingMtBolumID > 0 && rowMtBolumID > 0) return mappingMtBolumID === rowMtBolumID
|
||||||
|
return normalizeGroupName(r?.sParcaAdi) === mappingParcaAdi
|
||||||
|
})
|
||||||
|
if (existing) {
|
||||||
|
const next = { ...(suppressedRequiredKeys.value || {}) }
|
||||||
|
delete next[reqKeyByMapping]
|
||||||
|
if (reqKeyByRowMt) delete next[reqKeyByRowMt]
|
||||||
|
suppressedRequiredKeys.value = next
|
||||||
|
schedulePersistLocalDraft()
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const match = flatDetailRows.value.find(r => {
|
const match = flatDetailRows.value.find(r => {
|
||||||
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||||||
@@ -3294,6 +3628,16 @@ function removeDetailRowByKey (rowKey) {
|
|||||||
|
|
||||||
detailGroups.value = nextGroups
|
detailGroups.value = nextGroups
|
||||||
syncAllGroupsOpen()
|
syncAllGroupsOpen()
|
||||||
|
// If user deletes an auto-created required placeholder, suppress the corresponding required key
|
||||||
|
// so we don't keep recreating/warning about it for this draft.
|
||||||
|
if (existingRow?.requiredPlaceholder) {
|
||||||
|
const hNo = normalizeHammaddeNo(existingRow?.nHammaddeTuruNo)
|
||||||
|
const mtBolumID = parseInt(String(existingRow?.nUrtMTBolumID ?? existingRow?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||||
|
if (hNo && mtBolumID > 0) {
|
||||||
|
const reqKey = `${mtBolumID}:${hNo}`
|
||||||
|
suppressedRequiredKeys.value = { ...(suppressedRequiredKeys.value || {}), [reqKey]: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
if (existingRow && !existingRow?.isNew) {
|
if (existingRow && !existingRow?.isNew) {
|
||||||
deletedDetailRows.value = [
|
deletedDetailRows.value = [
|
||||||
...(Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []),
|
...(Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []),
|
||||||
@@ -3588,17 +3932,79 @@ async function confirmRefresh () {
|
|||||||
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToFirstRequiredAttentionRow () {
|
||||||
|
// Best-effort DOM scroll: q-table renders rowClass on the <tr>.
|
||||||
|
// This avoids users having to hunt for missing rows after we block save.
|
||||||
|
try {
|
||||||
|
const el = document.querySelector('.pcd-detail-row-required')
|
||||||
|
if (el && typeof el.scrollIntoView === 'function') {
|
||||||
|
el.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveChanges () {
|
async function saveChanges () {
|
||||||
saveLoading.value = true
|
saveLoading.value = true
|
||||||
try {
|
try {
|
||||||
requiredAttentionRowKeys.value = {}
|
requiredAttentionRowKeys.value = {}
|
||||||
if (isNoCostDetail.value) {
|
if (isNoCostDetail.value) {
|
||||||
const missing = computeMissingRequiredSlots()
|
const missing = computeMissingRequiredSlots()
|
||||||
|
slog.info('production-product-costing.detail', 'required:missing:computed', {
|
||||||
|
trace_id: traceId.value,
|
||||||
|
missing_count: missing.length,
|
||||||
|
sample: missing.slice(0, 20).map(m => ({
|
||||||
|
mtBolumID: m?.mtBolumID,
|
||||||
|
parcaAdi: String(m?.parcaAdi || '').trim(),
|
||||||
|
hNo: String(m?.nHammaddeTuruNo || '').trim(),
|
||||||
|
rowKey: String(m?.rowKey || '').trim()
|
||||||
|
}))
|
||||||
|
})
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
|
const rowsHtml = missing.slice(0, 40).map((m, i) => {
|
||||||
|
const key = String(m?.rowKey || '').trim()
|
||||||
|
const row = key ? flatDetailRows.value.find(r => String(r?.__rowKey || '').trim() === key) : null
|
||||||
|
const code = String(row?.sKodu || '').trim()
|
||||||
|
const desc = String(row?.sAciklama || '').trim()
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:4px 6px; border-bottom:1px solid #eee;">${i + 1}</td>
|
||||||
|
<td style="padding:4px 6px; border-bottom:1px solid #eee;">${String(m?.parcaAdi || '').trim()}</td>
|
||||||
|
<td style="padding:4px 6px; border-bottom:1px solid #eee;">${String(m?.nHammaddeTuruNo || '').trim()}</td>
|
||||||
|
<td style="padding:4px 6px; border-bottom:1px solid #eee; color:#b71c1c;">${code || '-'}</td>
|
||||||
|
<td style="padding:4px 6px; border-bottom:1px solid #eee; color:#b71c1c;">${desc || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
}).join('')
|
||||||
|
const truncatedNote = missing.length > 40
|
||||||
|
? `<div style="margin-top:8px; color:#777;">Ilk 40 satir gosterildi. Toplam: ${missing.length}</div>`
|
||||||
|
: ''
|
||||||
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 girilmemis satirlar var. Devam etmek istiyor musunuz? (Eksik: ${missing.length})`,
|
html: true,
|
||||||
|
message: `
|
||||||
|
<div>
|
||||||
|
Eslestirilen parcalarda fiyat girilmemis satirlar var. Devam etmek istiyor musunuz?
|
||||||
|
<div style="margin-top:6px; font-weight:600;">Eksik: ${missing.length}</div>
|
||||||
|
<div style="margin-top:8px; max-height:280px; overflow:auto; border:1px solid #eee;">
|
||||||
|
<table style="width:100%; border-collapse:collapse; font-size:12px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="text-align:left; background:#fafafa;">
|
||||||
|
<th style="padding:4px 6px; border-bottom:1px solid #eee;">No</th>
|
||||||
|
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Parca</th>
|
||||||
|
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Hammadde</th>
|
||||||
|
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Kod</th>
|
||||||
|
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Aciklama</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${rowsHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
${truncatedNote}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
cancel: true,
|
cancel: true,
|
||||||
persistent: true
|
persistent: true
|
||||||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||||||
@@ -3614,6 +4020,8 @@ async function saveChanges () {
|
|||||||
message: 'Eksik parcalar isaretlendi. Fiyat girip tekrar deneyin.',
|
message: 'Eksik parcalar isaretlendi. Fiyat girip tekrar deneyin.',
|
||||||
position: 'top-right'
|
position: 'top-right'
|
||||||
})
|
})
|
||||||
|
// Scroll to first highlighted row.
|
||||||
|
nextTick(() => scrollToFirstRequiredAttentionRow())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3627,6 +4035,17 @@ async function saveChanges () {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate required header fields
|
||||||
|
const selectedUretimSekliID = parseInt(String(detailHeader.value?.UretimSekliID || '0').trim() || '0', 10) || 0
|
||||||
|
if (!(selectedUretimSekliID > 0)) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Uretim Sekli secilmeden kayit yapilamaz.',
|
||||||
|
position: 'top-right'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Validate: "Kod" (sKodu) is mandatory for any row that has a hammadde type.
|
// Validate: "Kod" (sKodu) is mandatory for any row that has a hammadde type.
|
||||||
// We block saving if there are rows with empty/whitespace code, to avoid sending blank rows to backend.
|
// We block saving if there are rows with empty/whitespace code, to avoid sending blank rows to backend.
|
||||||
const blankCodeRows = (Array.isArray(flatDetailRows.value) ? flatDetailRows.value : [])
|
const blankCodeRows = (Array.isArray(flatDetailRows.value) ? flatDetailRows.value : [])
|
||||||
@@ -3651,6 +4070,24 @@ async function saveChanges () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const header = detailHeader.value
|
const header = detailHeader.value
|
||||||
|
// Debug: backend skips rows where n_onml_det_no <= 0. Log this before sending.
|
||||||
|
try {
|
||||||
|
const rows = Array.isArray(flatDetailRows.value) ? flatDetailRows.value : []
|
||||||
|
const detNoZero = rows.filter(r => (parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0) <= 0)
|
||||||
|
slog.info('production-product-costing.detail', 'save:payload:summary', {
|
||||||
|
trace_id: traceId.value,
|
||||||
|
row_count: rows.length,
|
||||||
|
det_no_zero_count: detNoZero.length,
|
||||||
|
det_no_zero_sample: detNoZero.slice(0, 10).map(r => ({
|
||||||
|
rowKey: String(r?.__rowKey || ''),
|
||||||
|
detNo: String(r?.nOnMLDetNo || ''),
|
||||||
|
hNo: String(r?.nHammaddeTuruNo || ''),
|
||||||
|
group: String(r?.sAciklama3 || ''),
|
||||||
|
code: String(r?.sKodu || '').trim()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const upserts = flatDetailRows.value.map(r => ({
|
const upserts = flatDetailRows.value.map(r => ({
|
||||||
n_onml_det_no: parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0,
|
n_onml_det_no: parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0,
|
||||||
n_hammadde_turu_no: parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0,
|
n_hammadde_turu_no: parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0,
|
||||||
@@ -3734,6 +4171,27 @@ async function saveChanges () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportCostingPDF () {
|
||||||
|
try {
|
||||||
|
const n = parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0
|
||||||
|
if (!(n > 0)) {
|
||||||
|
$q.notify({ type: 'warning', message: 'OnML No bulunamadi.', position: 'top-right' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const blob = await download('/pricing/production-product-costing/onml/pdf', {
|
||||||
|
n_onml_no: n,
|
||||||
|
trace_id: traceId.value
|
||||||
|
})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
window.setTimeout(() => {
|
||||||
|
try { URL.revokeObjectURL(url) } catch {}
|
||||||
|
}, 60_000)
|
||||||
|
} catch (e) {
|
||||||
|
$q.notify({ type: 'negative', message: await extractApiErrorDetail(e), position: 'top-right' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [onMLNo.value, productCode.value, recipeCode.value, detailSource.value],
|
() => [onMLNo.value, productCode.value, recipeCode.value, detailSource.value],
|
||||||
() => {
|
() => {
|
||||||
@@ -4062,6 +4520,22 @@ watch(
|
|||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pcd-detail-table :deep(.q-table) {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-detail-table :deep(.q-table th),
|
||||||
|
.pcd-detail-table :deep(.q-table td) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcd-detail-table :deep(.q-table tbody td) {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.pcd-detail-table :deep(.q-table thead) {
|
.pcd-detail-table :deep(.q-table thead) {
|
||||||
display: table-header-group !important;
|
display: table-header-group !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user