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 {
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,

View File

@@ -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 {

View File

@@ -274,45 +274,57 @@
</div>
</q-expansion-item>
<q-dialog v-model="last10WarningDialogOpen" persistent>
<q-card style="min-width: 860px; max-width: 92vw;">
<q-card-section class="row items-center">
<div class="text-h6">Son 10 Ort. Fiyat Sapmalari</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-separator />
<q-card-section style="max-height: 70vh; overflow: auto;">
<q-markup-table dense flat bordered>
<thead>
<tr>
<th class="text-left">Kod</th>
<th class="text-left">Doviz</th>
<th class="text-right">Giris</th>
<th class="text-right">Ort10</th>
<th class="text-right">Sapma %</th>
<th class="text-right">Sample</th>
<th class="text-left">Tarih Araligi</th>
</tr>
</thead>
<tbody>
<tr v-for="w in last10Warnings" :key="w.item_code + '|' + w.currency_code">
<td class="text-left">{{ w.item_code }}</td>
<td class="text-left">{{ w.currency_code }}</td>
<td class="text-right">{{ formatMoney(w.input_price) }}</td>
<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="7" class="text-center text-grey-7">Kayit yok</td>
</tr>
</tbody>
</q-markup-table>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="last10WarningDialogOpen" persistent>
<q-card style="min-width: 860px; max-width: 92vw;">
<q-card-section class="row items-center">
<div class="text-h6">Son 10 Ort. Fiyat Sapmalari</div>
<q-space />
<q-btn
dense
outline
color="secondary"
icon="download"
label="Excel"
class="q-mr-sm"
:disable="!last10Warnings || last10Warnings.length === 0"
@click="downloadLast10WarningsExcel"
/>
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-separator />
<q-card-section style="max-height: 70vh; overflow: auto;">
<q-markup-table dense flat bordered>
<thead>
<tr>
<th class="text-left">Kod</th>
<th class="text-left">Aciklama</th>
<th class="text-left">Doviz</th>
<th class="text-right">Giris</th>
<th class="text-right">Ort10</th>
<th class="text-right">Sapma %</th>
<th class="text-right">Sample</th>
<th class="text-left">Tarih Araligi</th>
</tr>
</thead>
<tbody>
<tr v-for="w in last10Warnings" :key="w.item_code + '|' + w.currency_code">
<td class="text-left">{{ w.item_code }}</td>
<td class="text-left">{{ w.item_description || '' }}</td>
<td class="text-left">{{ w.currency_code }}</td>
<td class="text-right">{{ formatMoney(w.input_price) }}</td>
<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 class="row q-col-gutter-sm">
@@ -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) {