Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user