diff --git a/svc/queries/production_product_costing_last10_warnings_pg.go b/svc/queries/production_product_costing_last10_warnings_pg.go index 83e3097..d6a5294 100644 --- a/svc/queries/production_product_costing_last10_warnings_pg.go +++ b/svc/queries/production_product_costing_last10_warnings_pg.go @@ -9,21 +9,22 @@ import ( ) type ProductionCostingLast10WarningRow struct { - NOnMLNo int `json:"n_onml_no"` - UrunKodu string `json:"urun_kodu"` - MaliyetTarihi string `json:"maliyet_tarihi"` // YYYY-MM-DD - ItemCode string `json:"item_code"` - CurrencyCode string `json:"currency_code"` - InputPrice float64 `json:"input_price"` - AvgDocPrice float64 `json:"avg_doc_price"` - InputUSD float64 `json:"input_usd"` - AvgUSD float64 `json:"avg_usd"` - DiffRatio float64 `json:"diff_ratio"` // e.g. 0.12 means 12% - SampleCount int `json:"sample_count"` - MinInvoice string `json:"min_invoice_date,omitempty"` - MaxInvoice string `json:"max_invoice_date,omitempty"` - CreatedAt string `json:"created_at,omitempty"` - CreatedBy string `json:"created_by,omitempty"` + NOnMLNo int `json:"n_onml_no"` + UrunKodu string `json:"urun_kodu"` + MaliyetTarihi string `json:"maliyet_tarihi"` // YYYY-MM-DD + ItemCode string `json:"item_code"` + ItemDescription string `json:"item_description,omitempty"` + CurrencyCode string `json:"currency_code"` + InputPrice float64 `json:"input_price"` + AvgDocPrice float64 `json:"avg_doc_price"` + InputUSD float64 `json:"input_usd"` + AvgUSD float64 `json:"avg_usd"` + DiffRatio float64 `json:"diff_ratio"` // e.g. 0.12 means 12% + SampleCount int `json:"sample_count"` + MinInvoice string `json:"min_invoice_date,omitempty"` + MaxInvoice string `json:"max_invoice_date,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + CreatedBy string `json:"created_by,omitempty"` } func EnsureProductionCostingLast10WarningTables(pg *sql.DB) error { @@ -32,25 +33,28 @@ func EnsureProductionCostingLast10WarningTables(pg *sql.DB) error { } stmts := []string{ ` -CREATE TABLE IF NOT EXISTS mk_costing_last10_warning ( - n_onml_no INT NOT NULL, - item_code TEXT NOT NULL, - currency_code TEXT NOT NULL, - urun_kodu TEXT NOT NULL DEFAULT '', - maliyet_tarihi DATE, - input_price DOUBLE PRECISION NOT NULL DEFAULT 0, - avg_doc_price DOUBLE PRECISION NOT NULL DEFAULT 0, - input_usd DOUBLE PRECISION NOT NULL DEFAULT 0, - avg_usd DOUBLE PRECISION NOT NULL DEFAULT 0, +CREATE TABLE IF NOT EXISTS mk_costing_last10_warning ( + n_onml_no INT NOT NULL, + item_code TEXT NOT NULL, + currency_code TEXT NOT NULL, + urun_kodu TEXT NOT NULL DEFAULT '', + item_description TEXT NOT NULL DEFAULT '', + maliyet_tarihi DATE, + input_price DOUBLE PRECISION NOT NULL DEFAULT 0, + avg_doc_price DOUBLE PRECISION NOT NULL DEFAULT 0, + input_usd DOUBLE PRECISION NOT NULL DEFAULT 0, + avg_usd DOUBLE PRECISION NOT NULL DEFAULT 0, diff_ratio DOUBLE PRECISION NOT NULL DEFAULT 0, sample_count INT NOT NULL DEFAULT 0, min_invoice_date DATE, max_invoice_date DATE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - created_by TEXT NOT NULL DEFAULT '', - PRIMARY KEY (n_onml_no, item_code, currency_code) -) + created_by TEXT NOT NULL DEFAULT '', + PRIMARY KEY (n_onml_no, item_code, currency_code) +) `, + // Best-effort forward migration for existing DBs. + `ALTER TABLE mk_costing_last10_warning ADD COLUMN IF NOT EXISTS item_description TEXT NOT NULL DEFAULT ''`, `CREATE INDEX IF NOT EXISTS ix_costing_last10_warning_onml ON mk_costing_last10_warning (n_onml_no)`, `CREATE INDEX IF NOT EXISTS ix_costing_last10_warning_item ON mk_costing_last10_warning (item_code, currency_code)`, } @@ -73,6 +77,7 @@ func ReplaceProductionCostingLast10Warnings( inputByKey map[string]float64, // key = ITEM|CUR (input in doc currency) inputUSDByKey map[string]float64, // key = ITEM|CUR (input converted to USD basis) avgUSDByKey map[string]float64, // key = ITEM|CUR (avg converted to USD basis) + descByCode map[string]string, // code -> description (UI text) ) error { if pg == nil { return fmt.Errorf("pg db is nil") @@ -128,34 +133,37 @@ func ReplaceProductionCostingLast10Warnings( continue } - _, err := tx.ExecContext(ctx, ` -INSERT INTO mk_costing_last10_warning ( - n_onml_no, item_code, currency_code, - urun_kodu, maliyet_tarihi, - input_price, avg_doc_price, input_usd, avg_usd, diff_ratio, - sample_count, min_invoice_date, max_invoice_date, - created_by -) VALUES ( - $1,$2,$3, - $4,$5, - $6,$7,$8,$9,$10, - $11,$12,$13, - $14 -) -ON CONFLICT (n_onml_no, item_code, currency_code) DO UPDATE SET - urun_kodu = EXCLUDED.urun_kodu, - maliyet_tarihi = EXCLUDED.maliyet_tarihi, - input_price = EXCLUDED.input_price, - avg_doc_price = EXCLUDED.avg_doc_price, - input_usd = EXCLUDED.input_usd, - avg_usd = EXCLUDED.avg_usd, + desc := strings.TrimSpace(descByCode[code]) + + _, err := tx.ExecContext(ctx, ` +INSERT INTO mk_costing_last10_warning ( + n_onml_no, item_code, currency_code, + urun_kodu, item_description, maliyet_tarihi, + input_price, avg_doc_price, input_usd, avg_usd, diff_ratio, + sample_count, min_invoice_date, max_invoice_date, + created_by +) VALUES ( + $1,$2,$3, + $4,$5,$6, + $7,$8,$9,$10,$11, + $12,$13,$14, + $15 +) +ON CONFLICT (n_onml_no, item_code, currency_code) DO UPDATE SET + urun_kodu = EXCLUDED.urun_kodu, + item_description = EXCLUDED.item_description, + maliyet_tarihi = EXCLUDED.maliyet_tarihi, + input_price = EXCLUDED.input_price, + avg_doc_price = EXCLUDED.avg_doc_price, + input_usd = EXCLUDED.input_usd, + avg_usd = EXCLUDED.avg_usd, diff_ratio = EXCLUDED.diff_ratio, sample_count = EXCLUDED.sample_count, min_invoice_date = EXCLUDED.min_invoice_date, max_invoice_date = EXCLUDED.max_invoice_date, - created_at = now(), - created_by = EXCLUDED.created_by -`, nOnMLNo, code, cur, urunKodu, mtDate, in, ar.AvgDocPrice, inUSD, avgUSD, diff, ar.SampleCount, ar.MinInvoiceDate, ar.MaxInvoiceDate, createdBy) + created_at = now(), + created_by = EXCLUDED.created_by +`, nOnMLNo, code, cur, urunKodu, desc, mtDate, in, ar.AvgDocPrice, inUSD, avgUSD, diff, ar.SampleCount, ar.MinInvoiceDate, ar.MaxInvoiceDate, createdBy) if err != nil { return err } @@ -172,16 +180,17 @@ func ListProductionCostingLast10WarningsByOnMLNo(ctx context.Context, pg *sql.DB return nil, fmt.Errorf("pg db is nil") } rows, err := pg.QueryContext(ctx, ` -SELECT - n_onml_no, - COALESCE(urun_kodu,'') AS urun_kodu, - COALESCE(TO_CHAR(maliyet_tarihi, 'YYYY-MM-DD'),'') AS maliyet_tarihi, - item_code, - currency_code, - input_price, - avg_doc_price, - input_usd, - avg_usd, +SELECT + n_onml_no, + COALESCE(urun_kodu,'') AS urun_kodu, + COALESCE(TO_CHAR(maliyet_tarihi, 'YYYY-MM-DD'),'') AS maliyet_tarihi, + item_code, + COALESCE(item_description,'') AS item_description, + currency_code, + input_price, + avg_doc_price, + input_usd, + avg_usd, diff_ratio, sample_count, COALESCE(TO_CHAR(min_invoice_date, 'YYYY-MM-DD'),'') AS min_invoice_date, @@ -205,6 +214,7 @@ ORDER BY diff_ratio DESC, item_code ASC, currency_code ASC &r.UrunKodu, &r.MaliyetTarihi, &r.ItemCode, + &r.ItemDescription, &r.CurrencyCode, &r.InputPrice, &r.AvgDocPrice, diff --git a/svc/routes/production_product_costing.go b/svc/routes/production_product_costing.go index ccba9b8..329c1a7 100644 --- a/svc/routes/production_product_costing.go +++ b/svc/routes/production_product_costing.go @@ -2266,6 +2266,7 @@ VALUES ( // dedupe input by code+currency (USD basis comparison) inputByKey := map[string]float64{} inputUSDByKey := map[string]float64{} + descByCode := map[string]string{} codes := make([]string, 0, len(reqCopy.Detail.Upserts)) seenCode := map[string]struct{}{} @@ -2310,6 +2311,13 @@ VALUES ( } inputUSDByKey[key] = inUSD + // Best-effort description from UI payload (for excel export/readability). + if d := strings.TrimSpace(row.SAciklama); d != "" { + if _, ok := descByCode[code]; !ok { + descByCode[code] = d + } + } + if _, ok := seenCode[code]; !ok { seenCode[code] = struct{}{} codes = append(codes, code) @@ -2366,6 +2374,7 @@ VALUES ( inputByKey, inputUSDByKey, avgUSDByKey, + descByCode, ) cancelWrite() if err != nil { diff --git a/ui/src/pages/ProductionProductCostingHasCostDetail.vue b/ui/src/pages/ProductionProductCostingHasCostDetail.vue index 439c63c..1fdbfa9 100644 --- a/ui/src/pages/ProductionProductCostingHasCostDetail.vue +++ b/ui/src/pages/ProductionProductCostingHasCostDetail.vue @@ -274,45 +274,57 @@ - - - -
Son 10 Ort. Fiyat Sapmalari
- - -
- - - - - - Kod - Doviz - Giris - Ort10 - Sapma % - Sample - Tarih Araligi - - - - - {{ w.item_code }} - {{ w.currency_code }} - {{ formatMoney(w.input_price) }} - {{ formatMoney(w.avg_doc_price) }} - {{ formatPercent(w.diff_ratio) }} - {{ w.sample_count }} - {{ (w.min_invoice_date || '-') + ' / ' + (w.max_invoice_date || '-') }} - - - Kayit yok - - - - -
-
+ + + +
Son 10 Ort. Fiyat Sapmalari
+ + + +
+ + + + + + Kod + Aciklama + Doviz + Giris + Ort10 + Sapma % + Sample + Tarih Araligi + + + + + {{ w.item_code }} + {{ w.item_description || '' }} + {{ w.currency_code }} + {{ formatMoney(w.input_price) }} + {{ formatMoney(w.avg_doc_price) }} + {{ formatPercent(w.diff_ratio) }} + {{ w.sample_count }} + {{ (w.min_invoice_date || '-') + ' / ' + (w.max_invoice_date || '-') }} + + + Kayit yok + + + + +
+
@@ -1149,11 +1161,76 @@ async function refreshTbStokMissingCodes () { missingTbStokCodesMap.value = {} } -function formatPercent (ratio) { - const n = Number(ratio) - if (!Number.isFinite(n)) return '-' - return `${(n * 100).toFixed(0)}%` -} +function formatPercent (ratio) { + const n = Number(ratio) + if (!Number.isFinite(n)) return '-' + return `${(n * 100).toFixed(0)}%` +} + +function downloadLast10WarningsExcel () { + const rows = Array.isArray(last10Warnings.value) ? last10Warnings.value : [] + if (rows.length === 0) return + + // Excel-friendly CSV (UTF-8 with BOM). + const header = [ + 'Kod', + 'Aciklama', + 'Doviz', + 'Giris', + 'Ort10', + 'SapmaOran', + 'SapmaYuzde', + 'Sample', + 'MinTarih', + 'MaxTarih' + ] + const lines = [header.join(';')] + for (const w of rows) { + const code = String(w?.item_code || '').trim() + const desc = String(w?.item_description || '').replaceAll('\n', ' ').replaceAll('\r', ' ').trim() + const cur = String(w?.currency_code || '').trim() + const inP = Number(w?.input_price || 0) + const avgP = Number(w?.avg_doc_price || 0) + const diff = Number(w?.diff_ratio || 0) + const sample = Number(w?.sample_count || 0) + const minD = String(w?.min_invoice_date || '').trim() + const maxD = String(w?.max_invoice_date || '').trim() + + const safe = (s) => { + s = String(s ?? '') + if (s.includes(';') || s.includes('\"')) { + s = '\"' + s.replaceAll('\"', '\"\"') + '\"' + } + return s + } + lines.push([ + safe(code), + safe(desc), + safe(cur), + String(Number.isFinite(inP) ? inP : 0), + String(Number.isFinite(avgP) ? avgP : 0), + String(Number.isFinite(diff) ? diff : 0), + `${Number.isFinite(diff) ? (diff * 100).toFixed(0) : '0'}%`, + String(Number.isFinite(sample) ? sample : 0), + safe(minD), + safe(maxD) + ].join(';')) + } + + const bom = '\uFEFF' + const blob = new Blob([bom + lines.join('\n')], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + const urun = String(detailHeader.value?.UrunKodu || productCode.value || '').trim() + const onml = String(onMLNo.value || '').trim() + const date = String(costDate.value || '').trim() + a.href = url + a.download = `fiyat_uyari_${urun || 'urun'}_${onml || 'onml'}_${date || 'tarih'}.csv` + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) +} async function refreshLast10Warnings () { if (!onMLNo.value) {