Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-20 21:24:17 +03:00
parent c1c1ed99c7
commit c46a934bc9
5 changed files with 526 additions and 48 deletions

View File

@@ -61,6 +61,7 @@ type ProductionHasCostDetailGroupItem struct {
NOnMLNo string `json:"nOnMLNo"` NOnMLNo string `json:"nOnMLNo"`
NOnMLDetNo string `json:"nOnMLDetNo"` NOnMLDetNo string `json:"nOnMLDetNo"`
NHammaddeTuruNo string `json:"nHammaddeTuruNo"` NHammaddeTuruNo string `json:"nHammaddeTuruNo"`
NUrtMTBolumID string `json:"nUrtMTBolumID"`
SKodu string `json:"sKodu"` SKodu string `json:"sKodu"`
SAciklama string `json:"sAciklama"` SAciklama string `json:"sAciklama"`
SRenk string `json:"sRenk"` SRenk string `json:"sRenk"`

View File

@@ -922,12 +922,13 @@ func GetProductionHasCostDetailRowsByOnMLNo(
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(D.sAciklama3)), ''), 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,
-- Normalize code to variantless (tbStok.sModel) when D.sKodu is a variant-coded stock record. RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nUrtMTBolumID, 0))) AS nUrtMTBolumID,
ISNULL(NULLIF(LTRIM(RTRIM(SX.sModel)), ''), ISNULL(D.sKodu, '')) AS sKodu, -- Normalize code to variantless (tbStok.sModel) when D.sKodu is a variant-coded stock record.
ISNULL(NULLIF(LTRIM(RTRIM(SX.sAciklama)), ''), 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,
@@ -1141,6 +1142,7 @@ HammaddeTekil AS (
ISNULL(S.sBirimCinsi1, '') AS sBirim, ISNULL(S.sBirimCinsi1, '') AS sBirim,
ISNULL(RMik.lHMiktar, 0) AS lMiktar, ISNULL(RMik.lHMiktar, 0) AS lMiktar,
ISNULL(HT.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID, ISNULL(HT.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID,
RTRIM(CONVERT(VARCHAR(32), ISNULL(HT.MTnUrtMTBolumID, 0))) AS nUrtMTBolumID,
ISNULL(B.sAdi, '') AS sParcaAdi, ISNULL(B.sAdi, '') AS sParcaAdi,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
PARTITION BY HT.nHammaddeTuruNo PARTITION BY HT.nHammaddeTuruNo
@@ -1174,18 +1176,19 @@ HammaddeTekil AS (
AND ISNULL(B.nUrtTipiID, 0) = 1 AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE HT.nHammaddeTuruNo IS NOT NULL WHERE HT.nHammaddeTuruNo IS NOT NULL
) )
SELECT SELECT
HT.sAciklama3, HT.sAciklama3,
0.0 AS GroupTotalTutar, 0.0 AS GroupTotalTutar,
0.0 AS GroupTotalUSDTutar, 0.0 AS GroupTotalUSDTutar,
'' AS nOnMLNo, '' AS nOnMLNo,
RTRIM(CONVERT(VARCHAR(32), ISNULL(HT.rowNo, 0))) AS nOnMLDetNo, RTRIM(CONVERT(VARCHAR(32), ISNULL(HT.rowNo, 0))) AS nOnMLDetNo,
HT.nHammaddeTuruNo, HT.nHammaddeTuruNo,
HT.sKodu, HT.nUrtMTBolumID,
HT.sAciklama, HT.sKodu,
HT.sRenk AS sRenk, HT.sAciklama,
'' AS sBeden, HT.sRenk AS sRenk,
'' AS sAciklama2, '' AS sBeden,
'' AS sAciklama2,
HT.lMiktar, HT.lMiktar,
0.0 AS lFiyat, 0.0 AS lFiyat,
0.0 AS lTutar, 0.0 AS lTutar,

View File

@@ -330,6 +330,7 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
nOnMLNoStr string nOnMLNoStr string
nOnMLDetNoStr string nOnMLDetNoStr string
hNoStr string hNoStr string
mtBolumStr string
fiyatGirilen sql.NullFloat64 fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool maliyeteDahil sql.NullBool
@@ -344,6 +345,7 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
&nOnMLNoStr, &nOnMLNoStr,
&nOnMLDetNoStr, &nOnMLDetNoStr,
&hNoStr, &hNoStr,
&mtBolumStr,
&item.SKodu, &item.SKodu,
&item.SAciklama, &item.SAciklama,
&item.SRenk, &item.SRenk,
@@ -378,6 +380,7 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
item.NOnMLNo = strings.TrimSpace(nOnMLNoStr) item.NOnMLNo = strings.TrimSpace(nOnMLNoStr)
item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr) item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr)
item.NHammaddeTuruNo = strings.TrimSpace(hNoStr) item.NHammaddeTuruNo = strings.TrimSpace(hNoStr)
item.NUrtMTBolumID = strings.TrimSpace(mtBolumStr)
if fiyatGirilen.Valid { if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64) item.FiyatGirilen = new(float64)
@@ -453,6 +456,7 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
nOnMLNoStr string nOnMLNoStr string
nOnMLDetNoStr string nOnMLDetNoStr string
hNoStr string hNoStr string
mtBolumStr string
fiyatGirilen sql.NullFloat64 fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool maliyeteDahil sql.NullBool
@@ -467,6 +471,7 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
&nOnMLNoStr, &nOnMLNoStr,
&nOnMLDetNoStr, &nOnMLDetNoStr,
&hNoStr, &hNoStr,
&mtBolumStr,
&item.SKodu, &item.SKodu,
&item.SAciklama, &item.SAciklama,
&item.SRenk, &item.SRenk,
@@ -501,6 +506,7 @@ func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Requ
item.NOnMLNo = strings.TrimSpace(nOnMLNoStr) item.NOnMLNo = strings.TrimSpace(nOnMLNoStr)
item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr) item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr)
item.NHammaddeTuruNo = strings.TrimSpace(hNoStr) item.NHammaddeTuruNo = strings.TrimSpace(hNoStr)
item.NUrtMTBolumID = strings.TrimSpace(mtBolumStr)
if fiyatGirilen.Valid { if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64) item.FiyatGirilen = new(float64)
@@ -1497,6 +1503,14 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
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 skippedUpserts := 0
skippedUpsertsSample := 0 skippedUpsertsSample := 0
// Cache hammadde_turu -> mt_bolum_id so we don't query master table for every row.
mtBolumByHammadde := map[int]int{}
// Collect source rows for recipe sync (variantless, non-CM2 only).
type recipeKey struct {
nUrtMBolumID int
sKodu string
}
recipeQtyByKey := map[recipeKey]float64{}
for _, row := range req.Detail.Upserts { for _, row := range req.Detail.Upserts {
if row.NOnMLDetNo <= 0 { if row.NOnMLDetNo <= 0 {
skippedUpserts += 1 skippedUpserts += 1
@@ -1537,10 +1551,74 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
return return
} }
} }
// Guard: keep part/section binding stable.
// UI sometimes doesn't send n_urt_mt_bolum_id; if we write 0 into spUrtOnMLMasDet,
// joins to spUrtMTBolum will break and "parca adi" will render as "-".
if row.NUrtMTBolumID <= 0 && row.NHammaddeTuruNo > 0 {
if cached, ok := mtBolumByHammadde[row.NHammaddeTuruNo]; ok {
if cached > 0 {
row.NUrtMTBolumID = cached
}
} else {
var mtID int
err := tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID
FROM dbo.spUrtOnMLHammaddeTuru WITH (NOLOCK)
WHERE nHammaddeTuruNo = @p1
`, row.NHammaddeTuruNo).Scan(&mtID)
if err != nil && err != sql.ErrNoRows {
logger.Warn("mt bolum lookup error (will keep incoming value)",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"n_hammadde_turu_no", row.NHammaddeTuruNo,
"err", err,
)
mtBolumByHammadde[row.NHammaddeTuruNo] = 0
} else {
mtBolumByHammadde[row.NHammaddeTuruNo] = mtID
if mtID > 0 {
row.NUrtMTBolumID = mtID
logger.Info("mt bolum auto-filled from hammadde master",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"n_hammadde_turu_no", row.NHammaddeTuruNo,
"n_urt_mt_bolum_id", mtID,
)
} else {
logger.Warn("mt bolum missing on hammadde master (keeping 0)",
"trace_id", traceID,
"n_onml_no", nOnMLNo,
"n_onml_det_no", row.NOnMLDetNo,
"n_hammadde_turu_no", row.NHammaddeTuruNo,
)
}
}
}
}
qty := row.LMiktar qty := row.LMiktar
if qty < 0 { if qty < 0 {
qty = 0 qty = 0
} }
// Build recipe sync source data:
// - never include CM2 / labor groups
// - never include empty codes
// - use variantless code (we already normalize sKodu on read; here we trust request)
if req.Header.NUrtReceteID > 0 {
group := strings.ToUpper(strings.TrimSpace(row.SAciklama3))
code := strings.TrimSpace(row.SKodu)
if code != "" && group != "CM2" && !strings.Contains(strings.ToUpper(code), " CM2") {
if row.NHammaddeTuruNo > 0 {
k := recipeKey{nUrtMBolumID: row.NHammaddeTuruNo, sKodu: code}
recipeQtyByKey[k] += qty
}
}
}
cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz)) cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz))
in := row.FiyatGirilen in := row.FiyatGirilen
unitTRY := in unitTRY := in
@@ -1745,8 +1823,150 @@ WHEN NOT MATCHED THEN
logger.Warn("detail upserts skipped summary (det_no<=0)", "trace_id", traceID, "n_onml_no", nOnMLNo, "skipped", skippedUpserts) logger.Warn("detail upserts skipped summary (det_no<=0)", "trace_id", traceID, "n_onml_no", nOnMLNo, "skipped", skippedUpserts)
} }
// NOTE: Recipe tables are intentionally NOT synced from OnML saves. // Recipe sync (spUrtRecMBolum): insert missing rows, update qty when changed.
// This costing screen is the source of truth only for dbo.spUrtOnMLMas / dbo.spUrtOnMLMasDet. // IMPORTANT: We sync only variantless item codes (sModel-like) from OnML and never write CM2 items.
if req.Header.NUrtReceteID > 0 && len(recipeQtyByKey) > 0 {
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "recipe_sync", "n_urt_recete_id", req.Header.NUrtReceteID, "src_count", len(recipeQtyByKey))
// Determine default nUrtUBolumID from existing recipe rows; fallback to 13 (matches current data).
nUrtUBolumID := 13
_ = tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(CONVERT(int, nUrtUBolumID), 0) AS nUrtUBolumID
FROM dbo.spUrtRecMBolum WITH (NOLOCK)
WHERE nUrtReceteID = @p1
ORDER BY nUrtRecMBolumID ASC
`, req.Header.NUrtReceteID).Scan(&nUrtUBolumID)
if nUrtUBolumID <= 0 {
nUrtUBolumID = 13
}
// Load existing rows for quick compare.
existingQty := map[recipeKey]float64{}
if rows, err := tx.QueryContext(ctx, `
SELECT
ISNULL(CONVERT(int, nUrtMBolumID), 0) AS nUrtMBolumID,
LTRIM(RTRIM(ISNULL(nHStokID_G,''))) AS nHStokID_G,
ISNULL(CONVERT(float, lHMiktar_G), 0) AS lHMiktar_G
FROM dbo.spUrtRecMBolum WITH (NOLOCK)
WHERE nUrtReceteID = @p1
`, req.Header.NUrtReceteID); err == nil {
for rows.Next() {
var bolumID int
var code string
var q float64
if err := rows.Scan(&bolumID, &code, &q); err != nil {
continue
}
code = strings.TrimSpace(code)
if bolumID > 0 && code != "" {
existingQty[recipeKey{nUrtMBolumID: bolumID, sKodu: code}] = q
}
}
_ = rows.Close()
}
// Update changed quantities.
updated := 0
for k, q := range recipeQtyByKey {
old, ok := existingQty[k]
if !ok {
continue
}
if old == q {
continue
}
if _, err := tx.ExecContext(ctx, `
UPDATE dbo.spUrtRecMBolum
SET
lHMiktar_G = @p4,
sKullaniciAdiDeg = @p5,
dteIslemTarihiDeg = GETDATE()
WHERE nUrtReceteID = @p1
AND nUrtMBolumID = @p2
AND LTRIM(RTRIM(ISNULL(nHStokID_G,''))) = @p3
`, req.Header.NUrtReceteID, k.nUrtMBolumID, k.sKodu, q, user); err != nil {
logger.Warn("recipe qty update failed", "trace_id", traceID, "n_urt_recete_id", req.Header.NUrtReceteID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu, "err", err)
continue
}
updated++
}
// Insert missing rows.
// We must generate nUrtRecMBolumID (smallint, non-identity) manually.
var baseID int
if err := tx.QueryRowContext(ctx, `
SELECT ISNULL(MAX(CONVERT(int, nUrtRecMBolumID)), 0) AS MaxID
FROM dbo.spUrtRecMBolum WITH (UPDLOCK, HOLDLOCK)
`).Scan(&baseID); err != nil {
logger.Warn("recipe base id lookup failed (skipping inserts)", "trace_id", traceID, "err", err)
} else {
inserted := 0
nextID := baseID
for k, q := range recipeQtyByKey {
if _, ok := existingQty[k]; ok {
continue
}
// FK guard: only insert if nUrtMBolumID exists in spUrtMBolum.
var bolumExists int
if err := tx.QueryRowContext(ctx, `
SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.spUrtMBolum WITH (NOLOCK) WHERE nUrtMBolumID = @p1) THEN 1 ELSE 0 END
`, k.nUrtMBolumID).Scan(&bolumExists); err != nil || bolumExists != 1 {
logger.Warn("recipe insert skipped (missing spUrtMBolum FK)", "trace_id", traceID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu)
continue
}
nextID++
if nextID > 32767 {
logger.Warn("recipe insert stopped (nUrtRecMBolumID overflow)", "trace_id", traceID, "base_id", baseID, "next_id", nextID)
break
}
// NOTE: sIslemKodu is NOT NULL; keep empty string as default.
// Keep lMiktar_G at 0 (NOT NULL) to avoid producing NULL rows.
if _, err := 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,
lMiktar_G,
nSure,
sIslemKodu,
lHMiktar_GHedef,
nMBolumSarfTipiNo,
sKullaniciAdi,
dteIslemTarihi
)
VALUES (
@p1,@p2,@p3,@p4,
0,1,@p5,
@p6,0,1,
6,0,2,
0,0,0,
'',
0,1,
@p7,GETDATE()
)
`, nextID, req.Header.NUrtReceteID, nUrtUBolumID, k.nUrtMBolumID, k.sKodu, q, user); err != nil {
logger.Warn("recipe insert failed", "trace_id", traceID, "n_urt_recete_id", req.Header.NUrtReceteID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu, "err", err)
continue
}
inserted++
}
logger.Info("recipe sync done", "trace_id", traceID, "n_onml_no", nOnMLNo, "n_urt_recete_id", req.Header.NUrtReceteID, "updated", updated, "inserted", inserted)
}
}
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 {

View File

@@ -136,6 +136,7 @@ func loadHasCostDetailGroups(ctx context.Context, uretimDB *sql.DB, nOnMLNo int)
nOnMLNoStr string nOnMLNoStr string
nOnMLDetNoStr string nOnMLDetNoStr string
hNoStr string hNoStr string
mtBolumStr string
fiyatGirilen sql.NullFloat64 fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool maliyeteDahil sql.NullBool
@@ -150,6 +151,7 @@ func loadHasCostDetailGroups(ctx context.Context, uretimDB *sql.DB, nOnMLNo int)
&nOnMLNoStr, &nOnMLNoStr,
&nOnMLDetNoStr, &nOnMLDetNoStr,
&hNoStr, &hNoStr,
&mtBolumStr,
&item.SKodu, &item.SKodu,
&item.SAciklama, &item.SAciklama,
&item.SRenk, &item.SRenk,
@@ -179,6 +181,7 @@ func loadHasCostDetailGroups(ctx context.Context, uretimDB *sql.DB, nOnMLNo int)
item.NOnMLNo = strings.TrimSpace(nOnMLNoStr) item.NOnMLNo = strings.TrimSpace(nOnMLNoStr)
item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr) item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr)
item.NHammaddeTuruNo = strings.TrimSpace(hNoStr) item.NHammaddeTuruNo = strings.TrimSpace(hNoStr)
item.NUrtMTBolumID = strings.TrimSpace(mtBolumStr)
if fiyatGirilen.Valid { if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64) item.FiyatGirilen = new(float64)
*item.FiyatGirilen = fiyatGirilen.Float64 *item.FiyatGirilen = fiyatGirilen.Float64
@@ -248,7 +251,11 @@ func (c *costingPDF) drawHeaderFull() {
line2 := fmt.Sprintf("Urun: %s - %s", strings.TrimSpace(c.header.UrunKodu), strings.TrimSpace(c.header.UrunAdi)) 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, "") 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)) firmaLabel := strings.TrimSpace(c.header.FirmaKodu)
if strings.TrimSpace(c.header.UretimiYapanFirma) != "" {
firmaLabel = fmt.Sprintf("%s - %s", firmaLabel, strings.TrimSpace(c.header.UretimiYapanFirma))
}
line3 := fmt.Sprintf("Firma: %s | Kaydeden: %s | Guncelleme: %s (%s)", firmaLabel, 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.CellFormat(0, 5, line3, "", 1, "L", false, 0, "")
pdf.SetTextColor(0, 0, 0) pdf.SetTextColor(0, 0, 0)
@@ -258,7 +265,11 @@ func (c *costingPDF) drawHeaderFull() {
func (c *costingPDF) drawHeaderCompact() { func (c *costingPDF) drawHeaderCompact() {
pdf := c.pdf pdf := c.pdf
pdf.SetFont("dejavu", "B", 10.5) 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) firmaLabel := strings.TrimSpace(c.header.FirmaKodu)
if strings.TrimSpace(c.header.UretimiYapanFirma) != "" {
firmaLabel = fmt.Sprintf("%s - %s", firmaLabel, strings.TrimSpace(c.header.UretimiYapanFirma))
}
title := fmt.Sprintf("OnML %s | %s - %s | %s | %s", c.header.NOnMLNo, strings.TrimSpace(c.header.UrunKodu), strings.TrimSpace(c.header.UrunAdi), c.header.DteKayitTarihi, firmaLabel)
pdf.CellFormat(0, 6, title, "", 1, "L", false, 0, "") pdf.CellFormat(0, 6, title, "", 1, "L", false, 0, "")
pdf.Ln(1) pdf.Ln(1)
} }
@@ -270,8 +281,23 @@ func (c *costingPDF) drawGroup(g models.ProductionHasCostDetailGroup, firstGroup
c.drawGroupBar(g, false) c.drawGroupBar(g, false)
// Columns // Columns
cols := []string{"No", "Parca", "Hammadde", "Kod", "Aciklama", "Renk", "Miktar", "Br", "Fiyat", "Pr.Br", "Tutar(TRY)"} // Keep total width <= A4 landscape printable width (297 - left/right margins).
wn := []float64{10, 24, 24, 40, 90, 18, 18, 12, 20, 14, 24} // sum ~294 (A4 landscape width minus margins) // Also force USD/TRY unit+total columns to always be visible.
cols := []string{
"No",
"Parca",
"Hammadde",
"Kod",
"Aciklama",
"Renk",
"Miktar",
"Br",
"USD\nFiyat",
"USD\nTutar",
"TRY\nFiyat",
"TRY\nTutar",
}
wn := []float64{8, 20, 22, 32, 70, 14, 14, 10, 16, 16, 16, 16} // sum = 250
c.drawTableHeader(cols, wn) c.drawTableHeader(cols, wn)
for _, it := range g.Items { for _, it := range g.Items {
@@ -297,10 +323,29 @@ func (c *costingPDF) drawTableHeader(cols []string, wn []float64) {
pdf.SetFont("dejavu", "B", 8) pdf.SetFont("dejavu", "B", 8)
pdf.SetFillColor(30, 30, 30) pdf.SetFillColor(30, 30, 30)
pdf.SetTextColor(255, 255, 255) pdf.SetTextColor(255, 255, 255)
// Compute a stable header height based on wrapped labels.
maxLines := 1
for i, col := range cols { for i, col := range cols {
pdf.CellFormat(wn[i], 5.5, col, "1", 0, "C", true, 0, "") lines := pdf.SplitLines([]byte(col), wn[i]-1.6)
if len(lines) > maxLines {
maxLines = len(lines)
}
} }
pdf.Ln(5.5) lineH := 3.5
headerH := float64(maxLines)*lineH + 1.2
if headerH < 7.0 {
headerH = 7.0
}
x0 := pdf.GetX()
y0 := pdf.GetY()
x := x0
for i, col := range cols {
c.drawHeaderCellWrap(x, y0, wn[i], headerH, col)
x += wn[i]
}
pdf.SetXY(x0, y0+headerH)
pdf.SetTextColor(0, 0, 0) pdf.SetTextColor(0, 0, 0)
} }
@@ -319,9 +364,28 @@ func (c *costingPDF) drawRowWithGroup(it models.ProductionHasCostDetailGroupItem
pdf := c.pdf pdf := c.pdf
pdf.SetFont("dejavu", "", 7.2) pdf.SetFont("dejavu", "", 7.2)
// Compute row height based on description wrapping. // Compute row height based on key wrapped cells.
parcaLines := pdf.SplitLines([]byte(strings.TrimSpace(it.SParcaAdi)), wn[1]-2)
hLabel := strings.TrimSpace(it.NHammaddeTuruNo)
if strings.TrimSpace(it.SHammaddeTuruAdi) != "" {
hLabel = hLabel + " " + strings.TrimSpace(it.SHammaddeTuruAdi)
}
hLines := pdf.SplitLines([]byte(hLabel), wn[2]-2)
kodLines := pdf.SplitLines([]byte(strings.TrimSpace(it.SKodu)), wn[3]-2)
descLines := pdf.SplitLines([]byte(strings.TrimSpace(it.SAciklama)), wn[4]-2) descLines := pdf.SplitLines([]byte(strings.TrimSpace(it.SAciklama)), wn[4]-2)
rowH := float64(len(descLines)) * 3.5
maxLines := len(descLines)
if len(parcaLines) > maxLines {
maxLines = len(parcaLines)
}
if len(hLines) > maxLines {
maxLines = len(hLines)
}
if len(kodLines) > maxLines {
maxLines = len(kodLines)
}
rowH := float64(maxLines) * 3.5
if rowH < 5.0 { if rowH < 5.0 {
rowH = 5.0 rowH = 5.0
} }
@@ -336,15 +400,11 @@ func (c *costingPDF) drawRowWithGroup(it models.ProductionHasCostDetailGroupItem
c.drawCell(x0, y0, wn[0], rowH, it.NOnMLDetNo, "R") c.drawCell(x0, y0, wn[0], rowH, it.NOnMLDetNo, "R")
x := x0 + wn[0] x := x0 + wn[0]
c.drawCell(x, y0, wn[1], rowH, strings.TrimSpace(it.SParcaAdi), "L") c.drawCellWrap(x, y0, wn[1], rowH, strings.TrimSpace(it.SParcaAdi), "L")
x += wn[1] x += wn[1]
hLabel := strings.TrimSpace(it.NHammaddeTuruNo) c.drawCellWrap(x, y0, wn[2], rowH, hLabel, "L")
if strings.TrimSpace(it.SHammaddeTuruAdi) != "" {
hLabel = hLabel + " " + strings.TrimSpace(it.SHammaddeTuruAdi)
}
c.drawCell(x, y0, wn[2], rowH, hLabel, "L")
x += wn[2] x += wn[2]
c.drawCell(x, y0, wn[3], rowH, strings.TrimSpace(it.SKodu), "L") c.drawCellWrap(x, y0, wn[3], rowH, strings.TrimSpace(it.SKodu), "L")
x += wn[3] x += wn[3]
c.drawCellWrap(x, y0, wn[4], rowH, strings.TrimSpace(it.SAciklama), "L") c.drawCellWrap(x, y0, wn[4], rowH, strings.TrimSpace(it.SAciklama), "L")
x += wn[4] x += wn[4]
@@ -355,24 +415,38 @@ func (c *costingPDF) drawRowWithGroup(it models.ProductionHasCostDetailGroupItem
c.drawCell(x, y0, wn[7], rowH, strings.TrimSpace(it.SBirim), "C") c.drawCell(x, y0, wn[7], rowH, strings.TrimSpace(it.SBirim), "C")
x += wn[7] x += wn[7]
// Prefer input price if present; otherwise lFiyat. // Always show USD/TRY unit+total.
price := it.LFiyat // In URETIM schema: lFiyat/lTutar are in TRY, lDovizFiyati/usdTutar are in USD.
cur := strings.TrimSpace(it.SDovizCinsi) c.drawCell(x, y0, wn[8], rowH, pdfMoney(it.LDovizFiyati), "R")
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] x += wn[8]
c.drawCell(x, y0, wn[9], rowH, cur, "C") usdTotal := it.USDTutar
if usdTotal == 0 && it.LMiktar != 0 && it.LDovizFiyati != 0 {
usdTotal = it.LMiktar * it.LDovizFiyati
}
c.drawCell(x, y0, wn[9], rowH, pdfMoney(usdTotal), "R")
x += wn[9] x += wn[9]
c.drawCell(x, y0, wn[10], rowH, pdfMoney(it.LTutar), "R")
// Prefer input price if present; otherwise lFiyat.
unitTRY := it.LFiyat
if it.FiyatGirilen != nil && *it.FiyatGirilen > 0 && strings.EqualFold(strings.TrimSpace(it.FiyatDoviz), "TRY") {
unitTRY = *it.FiyatGirilen
}
c.drawCell(x, y0, wn[10], rowH, pdfMoney(unitTRY), "R")
x += wn[10]
c.drawCell(x, y0, wn[11], rowH, pdfMoney(it.LTutar), "R")
pdf.SetXY(x0, y0+rowH) pdf.SetXY(x0, y0+rowH)
} }
func (c *costingPDF) drawHeaderCellWrap(x, y, w, h float64, txt string) {
pdf := c.pdf
pdf.Rect(x, y, w, h, "DF")
pdf.SetXY(x+0.8, y+0.6)
pdf.MultiCell(w-1.6, 3.5, txt, "", "C", true)
// restore cursor (MultiCell moves Y)
pdf.SetXY(x+w, y)
}
func (c *costingPDF) drawCell(x, y, w, h float64, txt, align string) { func (c *costingPDF) drawCell(x, y, w, h float64, txt, align string) {
pdf := c.pdf pdf := c.pdf
pdf.Rect(x, y, w, h, "") pdf.Rect(x, y, w, h, "")

View File

@@ -254,7 +254,7 @@
{{ 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"> <span v-if="normalizeGroupName(grp.sAciklama3) === 'FABRIC'" class="pcd-sub-mt-qty">
Toplam Miktar: {{ formatBarQuantity(resolveGroupQuantity(grp)) }} MT | Toplam Miktar: {{ formatBarQuantity(resolveGroupQuantity(grp)) }} MT |
</span> </span>
Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }} Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }}
@@ -894,6 +894,7 @@ const lineHistoryTargetSummary = ref('')
const lineHistorySearchMode = ref('exact') const lineHistorySearchMode = ref('exact')
const lineHistoryLastPurchaseMatchStage = ref('') const lineHistoryLastPurchaseMatchStage = ref('')
const lineHistoryLastRecipeMatchStage = ref('') const lineHistoryLastRecipeMatchStage = ref('')
const purchaseAvgUSDCachedByCode = ref({})
const headerInfoCollapsed = ref(false) const headerInfoCollapsed = ref(false)
const subHeaderTop = ref(140) const subHeaderTop = ref(140)
const stickyStackRef = ref(null) const stickyStackRef = ref(null)
@@ -1299,6 +1300,35 @@ function formatBarQuantity (value) {
return formatQuantity(roundedValue) return formatQuantity(roundedValue)
} }
function convertPriceToUSD (price, currency) {
const p = Number(price || 0)
if (!Number.isFinite(p) || p <= 0) return 0
const cur = String(currency || '').trim().toUpperCase()
const usdRate = Number(exchangeRates.value?.usdRate || 0) || 0
const eurRate = Number(exchangeRates.value?.eurRate || 0) || 0
const gbpRate = Number(exchangeRates.value?.gbpRate || 0) || 0
// If we don't have rates, fall back to assuming USD.
if (!(usdRate > 0)) return p
switch (cur) {
case 'USD':
return p
case 'TRY':
case 'TL':
case '':
return p / usdRate
case 'EUR': {
const tryVal = (eurRate > 0 ? p * eurRate : p)
return tryVal / usdRate
}
case 'GBP': {
const tryVal = (gbpRate > 0 ? p * gbpRate : p)
return tryVal / usdRate
}
default:
return p
}
}
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 : ''
@@ -3881,6 +3911,142 @@ async function confirmDefaultQtyDeviationIfNeeded () {
return ok return ok
} }
async function getPurchaseAvgUSDForCode (code) {
const normalized = String(code || '').trim()
if (!normalized) return { ok: false, code: '', avgUSD: 0, n: 0 }
const cached = purchaseAvgUSDCachedByCode.value?.[normalized]
if (cached && cached.ok) return cached
try {
const response = await get('/pricing/production-product-costing/has-cost-detail-line-history', {
n_onml_no: parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0,
s_kodu: normalized,
maliyet_tarihi: normalizeDateInput(costDate.value),
trace_id: traceId.value
})
const rows = Array.isArray(response?.purchaseRows) ? response.purchaseRows : []
const picked = []
for (const r of rows) {
const p = Number(r?.EvrakFiyat || 0)
if (!(p > 0)) continue
const cur = String(r?.EvrakDoviz || '').trim().toUpperCase() || 'USD'
const usd = convertPriceToUSD(p, cur)
if (!(usd > 0)) continue
picked.push(usd)
if (picked.length >= 10) break
}
const avgUSD = picked.length > 0 ? (picked.reduce((a, b) => a + b, 0) / picked.length) : 0
const result = { ok: picked.length > 0, code: normalized, avgUSD, n: picked.length }
purchaseAvgUSDCachedByCode.value = { ...(purchaseAvgUSDCachedByCode.value || {}), [normalized]: result }
return result
} catch (e) {
const result = { ok: false, code: normalized, avgUSD: 0, n: 0, error: String(e?.message || e) }
purchaseAvgUSDCachedByCode.value = { ...(purchaseAvgUSDCachedByCode.value || {}), [normalized]: result }
return result
}
}
async function confirmBrPriceDeviationIfNeeded () {
const rows = Array.isArray(flatDetailRows.value) ? flatDetailRows.value : []
if (rows.length === 0) return true
// Only consider rows that have a code + a non-zero entered price.
const candidates = rows
.filter(r => String(r?.sKodu || '').trim() !== '')
.map(r => {
const price = Number(resolveNumericRowInputPrice(r) || 0)
const cur = String(resolveInputCurrency(r) || '').trim().toUpperCase() || 'USD'
return { row: r, code: String(r?.sKodu || '').trim(), price, cur }
})
.filter(x => x.price > 0)
if (candidates.length === 0) return true
const uniqueCodes = Array.from(new Set(candidates.map(x => x.code)))
// Fetch averages with simple batching to avoid hammering the API.
const avgByCode = {}
const batchSize = 8
for (let i = 0; i < uniqueCodes.length; i += batchSize) {
const batch = uniqueCodes.slice(i, i + batchSize)
const results = await Promise.all(batch.map(c => getPurchaseAvgUSDForCode(c)))
results.forEach(r => { avgByCode[r.code] = r })
}
const outliers = []
for (const c of candidates) {
const avg = avgByCode[c.code]
if (!avg || !avg.ok || !(avg.avgUSD > 0) || avg.n < 3) continue // too little history -> ignore
const enteredUSD = convertPriceToUSD(c.price, c.cur)
if (!(enteredUSD > 0)) continue
const pct = ((enteredUSD - avg.avgUSD) / avg.avgUSD) * 100
if (Math.abs(pct) > 10) {
outliers.push({
code: c.code,
avgUSD: avg.avgUSD,
enteredUSD,
pct
})
}
}
if (outliers.length === 0) return true
outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct))
const maxRows = 30
const rowsHtml = outliers.slice(0, maxRows).map(x => {
const sign = x.pct >= 0 ? '+' : ''
const pct = `${sign}${round1(x.pct)}%`
const cls = x.pct >= 0 ? 'color:#b71c1c;' : 'color:#1b5e20;'
return `
<tr>
<td style="padding:6px 8px; white-space:nowrap; font-weight:600;">${escapeHtml(x.code)}</td>
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${formatMoney(x.avgUSD)}</td>
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${formatMoney(x.enteredUSD)}</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 => {
$q.dialog({
title: 'Fiyat Kontrolu (Satinalma Ortalama)',
html: true,
message: `
<div style="margin-bottom:10px;">
Bazı satırlarda girilen fiyat, BAGGI_V3 satınalma geçmişindeki <b>son 10</b> kaydın USD ortalamasından <b>%10</b>'dan fazla sapıyor.
</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;">Kod</th>
<th style="text-align:right; padding:6px 8px;">Ort USD</th>
<th style="text-align:right; padding:6px 8px;">Girilen USD</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' },
ok: { label: 'Onayla ve Kaydet', color: 'primary' },
persistent: true
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
})
return ok
}
async function deleteCosting () { async function deleteCosting () {
if (!detailHeader.value) return if (!detailHeader.value) return
const n = parseInt(String(detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || onMLNo.value || '0'), 10) || 0 const n = parseInt(String(detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || onMLNo.value || '0'), 10) || 0
@@ -4030,6 +4196,9 @@ async function saveChanges () {
const okDefaultQty = await confirmDefaultQtyDeviationIfNeeded() const okDefaultQty = await confirmDefaultQtyDeviationIfNeeded()
if (!okDefaultQty) return if (!okDefaultQty) return
const okBrPrice = await confirmBrPriceDeviationIfNeeded()
if (!okBrPrice) return
if (!detailHeader.value) { if (!detailHeader.value) {
$q.notify({ type: 'negative', message: 'Header bulunamadi.', position: 'top-right' }) $q.notify({ type: 'negative', message: 'Header bulunamadi.', position: 'top-right' })
return return
@@ -4507,6 +4676,10 @@ watch(
font-size: 12px; font-size: 12px;
text-transform: uppercase; text-transform: uppercase;
text-align: right; text-align: right;
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: nowrap;
} }
.pcd-sub-right-clickable { .pcd-sub-right-clickable {
cursor: pointer; cursor: pointer;
@@ -4514,6 +4687,13 @@ watch(
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
flex-wrap: nowrap;
white-space: nowrap;
}
.pcd-sub-mt-qty {
flex: 0 0 auto;
white-space: nowrap;
opacity: 0.9;
} }
.pcd-detail-table :deep(.q-table__middle) { .pcd-detail-table :deep(.q-table__middle) {