Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-22 17:12:45 +03:00
parent 1f90b9f9ce
commit 43bb76da9a
3 changed files with 202 additions and 106 deletions

View File

@@ -9,21 +9,22 @@ import (
) )
type ProductionCostingLast10WarningRow struct { type ProductionCostingLast10WarningRow struct {
NOnMLNo int `json:"n_onml_no"` NOnMLNo int `json:"n_onml_no"`
UrunKodu string `json:"urun_kodu"` UrunKodu string `json:"urun_kodu"`
MaliyetTarihi string `json:"maliyet_tarihi"` // YYYY-MM-DD MaliyetTarihi string `json:"maliyet_tarihi"` // YYYY-MM-DD
ItemCode string `json:"item_code"` ItemCode string `json:"item_code"`
CurrencyCode string `json:"currency_code"` ItemDescription string `json:"item_description,omitempty"`
InputPrice float64 `json:"input_price"` CurrencyCode string `json:"currency_code"`
AvgDocPrice float64 `json:"avg_doc_price"` InputPrice float64 `json:"input_price"`
InputUSD float64 `json:"input_usd"` AvgDocPrice float64 `json:"avg_doc_price"`
AvgUSD float64 `json:"avg_usd"` InputUSD float64 `json:"input_usd"`
DiffRatio float64 `json:"diff_ratio"` // e.g. 0.12 means 12% AvgUSD float64 `json:"avg_usd"`
SampleCount int `json:"sample_count"` DiffRatio float64 `json:"diff_ratio"` // e.g. 0.12 means 12%
MinInvoice string `json:"min_invoice_date,omitempty"` SampleCount int `json:"sample_count"`
MaxInvoice string `json:"max_invoice_date,omitempty"` MinInvoice string `json:"min_invoice_date,omitempty"`
CreatedAt string `json:"created_at,omitempty"` MaxInvoice string `json:"max_invoice_date,omitempty"`
CreatedBy string `json:"created_by,omitempty"` CreatedAt string `json:"created_at,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
} }
func EnsureProductionCostingLast10WarningTables(pg *sql.DB) error { func EnsureProductionCostingLast10WarningTables(pg *sql.DB) error {
@@ -32,25 +33,28 @@ func EnsureProductionCostingLast10WarningTables(pg *sql.DB) error {
} }
stmts := []string{ stmts := []string{
` `
CREATE TABLE IF NOT EXISTS mk_costing_last10_warning ( CREATE TABLE IF NOT EXISTS mk_costing_last10_warning (
n_onml_no INT NOT NULL, n_onml_no INT NOT NULL,
item_code TEXT NOT NULL, item_code TEXT NOT NULL,
currency_code TEXT NOT NULL, currency_code TEXT NOT NULL,
urun_kodu TEXT NOT NULL DEFAULT '', urun_kodu TEXT NOT NULL DEFAULT '',
maliyet_tarihi DATE, item_description TEXT NOT NULL DEFAULT '',
input_price DOUBLE PRECISION NOT NULL DEFAULT 0, maliyet_tarihi DATE,
avg_doc_price DOUBLE PRECISION NOT NULL DEFAULT 0, input_price DOUBLE PRECISION NOT NULL DEFAULT 0,
input_usd DOUBLE PRECISION NOT NULL DEFAULT 0, avg_doc_price DOUBLE PRECISION NOT NULL DEFAULT 0,
avg_usd 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, diff_ratio DOUBLE PRECISION NOT NULL DEFAULT 0,
sample_count INT NOT NULL DEFAULT 0, sample_count INT NOT NULL DEFAULT 0,
min_invoice_date DATE, min_invoice_date DATE,
max_invoice_date DATE, max_invoice_date DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by TEXT NOT NULL DEFAULT '', created_by TEXT NOT NULL DEFAULT '',
PRIMARY KEY (n_onml_no, item_code, currency_code) 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_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)`, `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) inputByKey map[string]float64, // key = ITEM|CUR (input in doc currency)
inputUSDByKey map[string]float64, // key = ITEM|CUR (input converted to USD basis) inputUSDByKey map[string]float64, // key = ITEM|CUR (input converted to USD basis)
avgUSDByKey map[string]float64, // key = ITEM|CUR (avg 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 { ) error {
if pg == nil { if pg == nil {
return fmt.Errorf("pg db is nil") return fmt.Errorf("pg db is nil")
@@ -128,34 +133,37 @@ func ReplaceProductionCostingLast10Warnings(
continue continue
} }
_, err := tx.ExecContext(ctx, ` desc := strings.TrimSpace(descByCode[code])
INSERT INTO mk_costing_last10_warning (
n_onml_no, item_code, currency_code, _, err := tx.ExecContext(ctx, `
urun_kodu, maliyet_tarihi, INSERT INTO mk_costing_last10_warning (
input_price, avg_doc_price, input_usd, avg_usd, diff_ratio, n_onml_no, item_code, currency_code,
sample_count, min_invoice_date, max_invoice_date, urun_kodu, item_description, maliyet_tarihi,
created_by input_price, avg_doc_price, input_usd, avg_usd, diff_ratio,
) VALUES ( sample_count, min_invoice_date, max_invoice_date,
$1,$2,$3, created_by
$4,$5, ) VALUES (
$6,$7,$8,$9,$10, $1,$2,$3,
$11,$12,$13, $4,$5,$6,
$14 $7,$8,$9,$10,$11,
) $12,$13,$14,
ON CONFLICT (n_onml_no, item_code, currency_code) DO UPDATE SET $15
urun_kodu = EXCLUDED.urun_kodu, )
maliyet_tarihi = EXCLUDED.maliyet_tarihi, ON CONFLICT (n_onml_no, item_code, currency_code) DO UPDATE SET
input_price = EXCLUDED.input_price, urun_kodu = EXCLUDED.urun_kodu,
avg_doc_price = EXCLUDED.avg_doc_price, item_description = EXCLUDED.item_description,
input_usd = EXCLUDED.input_usd, maliyet_tarihi = EXCLUDED.maliyet_tarihi,
avg_usd = EXCLUDED.avg_usd, 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, diff_ratio = EXCLUDED.diff_ratio,
sample_count = EXCLUDED.sample_count, sample_count = EXCLUDED.sample_count,
min_invoice_date = EXCLUDED.min_invoice_date, min_invoice_date = EXCLUDED.min_invoice_date,
max_invoice_date = EXCLUDED.max_invoice_date, max_invoice_date = EXCLUDED.max_invoice_date,
created_at = now(), created_at = now(),
created_by = EXCLUDED.created_by created_by = EXCLUDED.created_by
`, nOnMLNo, code, cur, urunKodu, mtDate, in, ar.AvgDocPrice, inUSD, avgUSD, diff, ar.SampleCount, ar.MinInvoiceDate, ar.MaxInvoiceDate, createdBy) `, nOnMLNo, code, cur, urunKodu, desc, mtDate, in, ar.AvgDocPrice, inUSD, avgUSD, diff, ar.SampleCount, ar.MinInvoiceDate, ar.MaxInvoiceDate, createdBy)
if err != nil { if err != nil {
return err return err
} }
@@ -172,16 +180,17 @@ func ListProductionCostingLast10WarningsByOnMLNo(ctx context.Context, pg *sql.DB
return nil, fmt.Errorf("pg db is nil") return nil, fmt.Errorf("pg db is nil")
} }
rows, err := pg.QueryContext(ctx, ` rows, err := pg.QueryContext(ctx, `
SELECT SELECT
n_onml_no, n_onml_no,
COALESCE(urun_kodu,'') AS urun_kodu, COALESCE(urun_kodu,'') AS urun_kodu,
COALESCE(TO_CHAR(maliyet_tarihi, 'YYYY-MM-DD'),'') AS maliyet_tarihi, COALESCE(TO_CHAR(maliyet_tarihi, 'YYYY-MM-DD'),'') AS maliyet_tarihi,
item_code, item_code,
currency_code, COALESCE(item_description,'') AS item_description,
input_price, currency_code,
avg_doc_price, input_price,
input_usd, avg_doc_price,
avg_usd, input_usd,
avg_usd,
diff_ratio, diff_ratio,
sample_count, sample_count,
COALESCE(TO_CHAR(min_invoice_date, 'YYYY-MM-DD'),'') AS min_invoice_date, 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.UrunKodu,
&r.MaliyetTarihi, &r.MaliyetTarihi,
&r.ItemCode, &r.ItemCode,
&r.ItemDescription,
&r.CurrencyCode, &r.CurrencyCode,
&r.InputPrice, &r.InputPrice,
&r.AvgDocPrice, &r.AvgDocPrice,

View File

@@ -2266,6 +2266,7 @@ VALUES (
// dedupe input by code+currency (USD basis comparison) // dedupe input by code+currency (USD basis comparison)
inputByKey := map[string]float64{} inputByKey := map[string]float64{}
inputUSDByKey := map[string]float64{} inputUSDByKey := map[string]float64{}
descByCode := map[string]string{}
codes := make([]string, 0, len(reqCopy.Detail.Upserts)) codes := make([]string, 0, len(reqCopy.Detail.Upserts))
seenCode := map[string]struct{}{} seenCode := map[string]struct{}{}
@@ -2310,6 +2311,13 @@ VALUES (
} }
inputUSDByKey[key] = inUSD 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 { if _, ok := seenCode[code]; !ok {
seenCode[code] = struct{}{} seenCode[code] = struct{}{}
codes = append(codes, code) codes = append(codes, code)
@@ -2366,6 +2374,7 @@ VALUES (
inputByKey, inputByKey,
inputUSDByKey, inputUSDByKey,
avgUSDByKey, avgUSDByKey,
descByCode,
) )
cancelWrite() cancelWrite()
if err != nil { if err != nil {

View File

@@ -274,45 +274,57 @@
</div> </div>
</q-expansion-item> </q-expansion-item>
<q-dialog v-model="last10WarningDialogOpen" persistent> <q-dialog v-model="last10WarningDialogOpen" persistent>
<q-card style="min-width: 860px; max-width: 92vw;"> <q-card style="min-width: 860px; max-width: 92vw;">
<q-card-section class="row items-center"> <q-card-section class="row items-center">
<div class="text-h6">Son 10 Ort. Fiyat Sapmalari</div> <div class="text-h6">Son 10 Ort. Fiyat Sapmalari</div>
<q-space /> <q-space />
<q-btn icon="close" flat round dense v-close-popup /> <q-btn
</q-card-section> dense
<q-separator /> outline
<q-card-section style="max-height: 70vh; overflow: auto;"> color="secondary"
<q-markup-table dense flat bordered> icon="download"
<thead> label="Excel"
<tr> class="q-mr-sm"
<th class="text-left">Kod</th> :disable="!last10Warnings || last10Warnings.length === 0"
<th class="text-left">Doviz</th> @click="downloadLast10WarningsExcel"
<th class="text-right">Giris</th> />
<th class="text-right">Ort10</th> <q-btn icon="close" flat round dense v-close-popup />
<th class="text-right">Sapma %</th> </q-card-section>
<th class="text-right">Sample</th> <q-separator />
<th class="text-left">Tarih Araligi</th> <q-card-section style="max-height: 70vh; overflow: auto;">
</tr> <q-markup-table dense flat bordered>
</thead> <thead>
<tbody> <tr>
<tr v-for="w in last10Warnings" :key="w.item_code + '|' + w.currency_code"> <th class="text-left">Kod</th>
<td class="text-left">{{ w.item_code }}</td> <th class="text-left">Aciklama</th>
<td class="text-left">{{ w.currency_code }}</td> <th class="text-left">Doviz</th>
<td class="text-right">{{ formatMoney(w.input_price) }}</td> <th class="text-right">Giris</th>
<td class="text-right">{{ formatMoney(w.avg_doc_price) }}</td> <th class="text-right">Ort10</th>
<td class="text-right">{{ formatPercent(w.diff_ratio) }}</td> <th class="text-right">Sapma %</th>
<td class="text-right">{{ w.sample_count }}</td> <th class="text-right">Sample</th>
<td class="text-left">{{ (w.min_invoice_date || '-') + ' / ' + (w.max_invoice_date || '-') }}</td> <th class="text-left">Tarih Araligi</th>
</tr> </tr>
<tr v-if="last10Warnings.length === 0"> </thead>
<td colspan="7" class="text-center text-grey-7">Kayit yok</td> <tbody>
</tr> <tr v-for="w in last10Warnings" :key="w.item_code + '|' + w.currency_code">
</tbody> <td class="text-left">{{ w.item_code }}</td>
</q-markup-table> <td class="text-left">{{ w.item_description || '' }}</td>
</q-card-section> <td class="text-left">{{ w.currency_code }}</td>
</q-card> <td class="text-right">{{ formatMoney(w.input_price) }}</td>
</q-dialog> <td class="text-right">{{ formatMoney(w.avg_doc_price) }}</td>
<td class="text-right">{{ formatPercent(w.diff_ratio) }}</td>
<td class="text-right">{{ w.sample_count }}</td>
<td class="text-left">{{ (w.min_invoice_date || '-') + ' / ' + (w.max_invoice_date || '-') }}</td>
</tr>
<tr v-if="last10Warnings.length === 0">
<td colspan="8" class="text-center text-grey-7">Kayit yok</td>
</tr>
</tbody>
</q-markup-table>
</q-card-section>
</q-card>
</q-dialog>
<div v-if="detailHeader && !detailLoading && !headerInfoCollapsed" class="filter-bar pcd-detail-header-bar q-mx-md q-mb-md"> <div v-if="detailHeader && !detailLoading && !headerInfoCollapsed" class="filter-bar pcd-detail-header-bar q-mx-md q-mb-md">
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
@@ -1149,11 +1161,76 @@ async function refreshTbStokMissingCodes () {
missingTbStokCodesMap.value = {} missingTbStokCodesMap.value = {}
} }
function formatPercent (ratio) { function formatPercent (ratio) {
const n = Number(ratio) const n = Number(ratio)
if (!Number.isFinite(n)) return '-' if (!Number.isFinite(n)) return '-'
return `${(n * 100).toFixed(0)}%` 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 () { async function refreshLast10Warnings () {
if (!onMLNo.value) { if (!onMLNo.value) {