Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -767,7 +767,15 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
|
||||
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 {
|
||||
logger.Error("hammadde query error", "err", err)
|
||||
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde query error: %v", err)
|
||||
@@ -839,8 +847,14 @@ func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *ht
|
||||
continue
|
||||
}
|
||||
item.Kind = "item"
|
||||
item.Value = item.SKodu
|
||||
item.Label = strings.TrimSpace(item.SKodu + " - " + item.SAciklama)
|
||||
// Use variantless code (tbStok.sModel) as the value for costing/recipe.
|
||||
// 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)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -1237,6 +1251,12 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
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.Header.UrunKodu = strings.TrimSpace(req.Header.UrunKodu)
|
||||
@@ -1457,8 +1477,10 @@ func PostProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
|
||||
|
||||
// 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 {
|
||||
if d.NOnMLDetNo <= 0 {
|
||||
skippedDeletes += 1
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if skippedDeletes > 0 {
|
||||
logger.Warn("detail deletes skipped (det_no<=0)", "trace_id", traceID, "n_onml_no", nOnMLNo, "skipped", skippedDeletes)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
|
||||
// FALLBACK: If nHammaddeTuruNo is missing but sKodu is present, default to 1 (General/Labor)
|
||||
// to avoid blocking the user, especially for labor items.
|
||||
@@ -1701,141 +1741,13 @@ WHEN NOT MATCHED THEN
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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 {
|
||||
hNo := row.NHammaddeTuruNo
|
||||
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)
|
||||
}
|
||||
}
|
||||
if skippedUpserts > 0 {
|
||||
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.
|
||||
// This costing screen is the source of truth only for dbo.spUrtOnMLMas / dbo.spUrtOnMLMasDet.
|
||||
|
||||
logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "commit")
|
||||
if err := tx.Commit(); err != nil {
|
||||
logger.Error("tx commit error", "err", err)
|
||||
@@ -1942,67 +1854,49 @@ DELETE FROM dbo.spUrtOnMLMas WHERE nOnMLNo = @p1
|
||||
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.
|
||||
// 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
|
||||
deletedBasePriceCount := 0
|
||||
if mssqlDB != nil && urunKodu != "" {
|
||||
priceDate := maliyetTarihi.Format("2006-01-02")
|
||||
// Primary rule: delete only the row for this exact date and USD currency.
|
||||
// Safety: require that either CreatedUserName/LastUpdatedUserName matches current user, or one of them starts with BSSAPP.
|
||||
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 only rows owned by this app (Created/LastUpdated starts with BSSAPP).
|
||||
res, err := mssqlDB.ExecContext(ctx, `
|
||||
DELETE FROM dbo.prItemBasePrice
|
||||
WHERE ItemTypeCode = 1
|
||||
AND LTRIM(RTRIM(ItemCode)) = @p1
|
||||
AND ISNULL(CountryCode,'') = 'TR'
|
||||
AND ISNULL(SeasonCode,'') = ''
|
||||
AND ISNULL(BasePriceCode,0) = 1
|
||||
AND CONVERT(date, PriceDate) = CONVERT(date, @p2, 23)
|
||||
AND LTRIM(RTRIM(ISNULL(CurrencyCode,''))) = 'USD'
|
||||
`, urunKodu, priceDate); err == nil {
|
||||
deletedBasePrice = true
|
||||
} else {
|
||||
logger.Warn("v3 base price delete failed", "err", err, "urun_kodu", urunKodu, "price_date", priceDate)
|
||||
}
|
||||
AND (
|
||||
UPPER(LTRIM(RTRIM(ISNULL(CreatedUserName,'')))) LIKE 'BSSAPP%'
|
||||
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)
|
||||
} 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{
|
||||
"ok": true,
|
||||
"n_onml_no": req.NOnMLNo,
|
||||
"urun_kodu": urunKodu,
|
||||
"deleted_baseprice": deletedBasePrice,
|
||||
"ok": true,
|
||||
"n_onml_no": req.NOnMLNo,
|
||||
"urun_kodu": urunKodu,
|
||||
"deleted_baseprice": deletedBasePrice,
|
||||
"deleted_baseprice_count": deletedBasePriceCount,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3029,12 +2923,51 @@ func GetProductionProductCostingParcaMappingsHandler(w http.ResponseWriter, r *h
|
||||
continue
|
||||
}
|
||||
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)
|
||||
if hammaddeCsv.Valid {
|
||||
for _, part := range strings.Split(hammaddeCsv.String, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "" {
|
||||
row.NHammaddeTurleri = append(row.NHammaddeTurleri, part)
|
||||
n, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
// keep as-is
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user