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

@@ -254,7 +254,7 @@
{{ grp.sAciklama3 || 'TANIMSIZ' }}
</div>
<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 |
</span>
Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }}
@@ -894,6 +894,7 @@ const lineHistoryTargetSummary = ref('')
const lineHistorySearchMode = ref('exact')
const lineHistoryLastPurchaseMatchStage = ref('')
const lineHistoryLastRecipeMatchStage = ref('')
const purchaseAvgUSDCachedByCode = ref({})
const headerInfoCollapsed = ref(false)
const subHeaderTop = ref(140)
const stickyStackRef = ref(null)
@@ -1299,6 +1300,35 @@ function formatBarQuantity (value) {
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) {
const normalizedValue = String(value || '').trim().toUpperCase()
return ['USD', 'TRY', 'EUR', 'GBP'].includes(normalizedValue) ? normalizedValue : ''
@@ -3881,6 +3911,142 @@ async function confirmDefaultQtyDeviationIfNeeded () {
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 () {
if (!detailHeader.value) return
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()
if (!okDefaultQty) return
const okBrPrice = await confirmBrPriceDeviationIfNeeded()
if (!okBrPrice) return
if (!detailHeader.value) {
$q.notify({ type: 'negative', message: 'Header bulunamadi.', position: 'top-right' })
return
@@ -4507,6 +4676,10 @@ watch(
font-size: 12px;
text-transform: uppercase;
text-align: right;
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: nowrap;
}
.pcd-sub-right-clickable {
cursor: pointer;
@@ -4514,6 +4687,13 @@ watch(
display: inline-flex;
align-items: center;
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) {