Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -2818,6 +2818,39 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
// for multiple parts (Ceket/Pantolon/Yelek...), so de-duping only by hNo is incorrect.
|
||||
const processedRequiredKeys = new Set()
|
||||
|
||||
// Prefetch default quantities for all required hammadde types (1 call).
|
||||
const allRequiredNos = []
|
||||
for (const mapping of list) {
|
||||
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
||||
for (const hNoRaw of hList) {
|
||||
const hNo = parseInt(String(hNoRaw || '').trim(), 10)
|
||||
if (hNo > 0) allRequiredNos.push(hNo)
|
||||
}
|
||||
}
|
||||
let defaultQtyByNo = {}
|
||||
try {
|
||||
const defaults = await post('/pricing/production-product-costing/default-quantities/lookup', {
|
||||
nHammaddeTuruNos: allRequiredNos
|
||||
})
|
||||
const arr = Array.isArray(defaults) ? defaults : []
|
||||
arr.forEach(it => {
|
||||
const no = parseInt(String(it?.nHammaddeTuruNo ?? '0'), 10) || 0
|
||||
const qty = Number(it?.lDefaultMiktar || 0)
|
||||
if (no > 0 && qty > 0) defaultQtyByNo[no] = qty
|
||||
})
|
||||
} catch (err) {
|
||||
defaultQtyByNo = {}
|
||||
slog.error('production-product-costing.detail', 'default-qty:lookup:error', {
|
||||
trace_id: traceId.value,
|
||||
error: String(err?.message || err)
|
||||
})
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Varsayilan miktarlar alinmadi (lookup hatasi). Miktarlar 1 olarak geldi.',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
// Add missing placeholder rows (qty=1, price=0) to remind user
|
||||
for (const mapping of list) {
|
||||
// Parca adi (CEKET/PANTOLON/YELEK...) comes from the MT bolum description (joined in backend as parcaBolumAdi).
|
||||
@@ -2899,8 +2932,8 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
sRenk: '',
|
||||
ColorCode: '',
|
||||
ColorDescription: '',
|
||||
lMiktar: 1,
|
||||
miktarInput: '1',
|
||||
lMiktar: (Number(defaultQtyByNo[parseInt(hNo, 10)] || 0) > 0 ? Number(defaultQtyByNo[parseInt(hNo, 10)]) : 1),
|
||||
miktarInput: (Number(defaultQtyByNo[parseInt(hNo, 10)] || 0) > 0 ? String(defaultQtyByNo[parseInt(hNo, 10)]) : '1'),
|
||||
inputPrice: '0',
|
||||
inputPricePrBr: 'USD',
|
||||
fiyat_girilen: 0,
|
||||
@@ -3086,6 +3119,83 @@ function saveRowEditor () {
|
||||
rowEditorDialogOpen.value = false
|
||||
}
|
||||
|
||||
function round1 (n) {
|
||||
const x = Number(n || 0)
|
||||
if (!Number.isFinite(x)) return 0
|
||||
return Math.round(x * 10) / 10
|
||||
}
|
||||
|
||||
function round4 (n) {
|
||||
const x = Number(n || 0)
|
||||
if (!Number.isFinite(x)) return 0
|
||||
return Math.round(x * 10000) / 10000
|
||||
}
|
||||
|
||||
async function confirmDefaultQtyDeviationIfNeeded () {
|
||||
// Compare entered qty vs default qty (mk_MaliyetParcaEslestirme_vmiktarlar) per hammadde type.
|
||||
// Rule: if deviation > 10% (abs), require user confirmation.
|
||||
const qtyByNo = {}
|
||||
flatDetailRows.value.forEach(r => {
|
||||
const no = parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
|
||||
const qty = Number(r?.lMiktar || 0)
|
||||
if (!(no > 0) || !(qty > 0)) return
|
||||
qtyByNo[no] = (qtyByNo[no] || 0) + qty
|
||||
})
|
||||
const nos = Object.keys(qtyByNo).map(k => parseInt(k, 10)).filter(n => n > 0)
|
||||
if (nos.length === 0) return true
|
||||
|
||||
let defaults = []
|
||||
try {
|
||||
defaults = await post('/pricing/production-product-costing/default-quantities/lookup', {
|
||||
nHammaddeTuruNos: nos
|
||||
})
|
||||
} catch {
|
||||
// If lookup fails, don't block save.
|
||||
return true
|
||||
}
|
||||
const defMap = {}
|
||||
;(Array.isArray(defaults) ? defaults : []).forEach(it => {
|
||||
const no = parseInt(String(it?.nHammaddeTuruNo || '0'), 10) || 0
|
||||
const d = Number(it?.lDefaultMiktar || 0)
|
||||
if (no > 0 && d > 0) defMap[no] = d
|
||||
})
|
||||
|
||||
const outliers = []
|
||||
Object.keys(defMap).forEach(k => {
|
||||
const no = parseInt(k, 10) || 0
|
||||
const defQty = Number(defMap[no] || 0)
|
||||
const enteredQty = Number(qtyByNo[no] || 0)
|
||||
if (!(defQty > 0) || !(enteredQty > 0)) return
|
||||
const pct = ((enteredQty - defQty) / defQty) * 100
|
||||
if (Math.abs(pct) > 10) {
|
||||
outliers.push({ no, defQty, enteredQty, pct })
|
||||
}
|
||||
})
|
||||
if (outliers.length === 0) return true
|
||||
|
||||
outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct))
|
||||
|
||||
const maxRows = 30
|
||||
const lines = outliers.slice(0, maxRows).map(x => {
|
||||
const sign = x.pct >= 0 ? '+' : ''
|
||||
return `${x.no}: varsayilan ${round4(x.defQty)} | girilen ${round4(x.enteredQty)} | fark ${sign}${round1(x.pct)}%`
|
||||
})
|
||||
const truncated = outliers.length > maxRows
|
||||
? `\n... (Toplam ${outliers.length} satir. Ilk ${maxRows} gosterildi.)`
|
||||
: ''
|
||||
|
||||
const ok = await new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: 'Varsayilan Miktar Kontrolu',
|
||||
message: `Bazi hammadde turlerinde varsayilan miktardan %10'dan fazla sapma var.\n\n${lines.join('\n')}${truncated}\n\nOnayliyorsaniz Kaydet'e basın. Duzenlemek icin Geri Don.`,
|
||||
cancel: { label: 'Geri Don' },
|
||||
ok: { label: 'Onayla ve Kaydet', color: 'primary' },
|
||||
persistent: true
|
||||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||||
})
|
||||
return ok
|
||||
}
|
||||
|
||||
async function saveChanges () {
|
||||
saveLoading.value = true
|
||||
try {
|
||||
@@ -3117,11 +3227,68 @@ async function saveChanges () {
|
||||
}
|
||||
}
|
||||
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Kaydetme endpointi henuz eklenmedi. Buton hazir, backend baglantisi bir sonraki adimda eklenebilir.',
|
||||
position: 'top-right'
|
||||
})
|
||||
const okDefaultQty = await confirmDefaultQtyDeviationIfNeeded()
|
||||
if (!okDefaultQty) return
|
||||
|
||||
if (!detailHeader.value) {
|
||||
$q.notify({ type: 'negative', message: 'Header bulunamadi.', position: 'top-right' })
|
||||
return
|
||||
}
|
||||
|
||||
const header = detailHeader.value
|
||||
const upserts = flatDetailRows.value.map(r => ({
|
||||
n_onml_det_no: parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0,
|
||||
n_hammadde_turu_no: parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0,
|
||||
n_urt_mt_bolum_id: parseInt(String(r?.nUrtMTBolumID || '0').trim() || '0', 10) || 0,
|
||||
s_kodu: String(r?.sKodu || '').trim(),
|
||||
s_aciklama: String(r?.sAciklama || '').trim(),
|
||||
s_renk: String(r?.sRenk || '').trim(),
|
||||
s_beden: String(r?.sBeden || '').trim(),
|
||||
s_aciklama2: String(r?.sAciklama2 || '').trim(),
|
||||
s_birim: String(r?.sBirim || '').trim(),
|
||||
l_miktar: Number(r?.lMiktar || 0),
|
||||
fiyat_girilen: Number(resolveNumericRowInputPrice(r) || 0),
|
||||
fiyat_doviz: String(resolveInputCurrency(r) || '').trim(),
|
||||
maliyete_dahil: (r?.maliyeteDahil || r?.maliyete_dahil) ? 1 : 0,
|
||||
cm_price_type_id: r?.cmPriceTypeId ?? r?.cm_price_type_id ?? null
|
||||
}))
|
||||
|
||||
const deletes = (Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []).map(d => ({
|
||||
n_onml_det_no: parseInt(String(d?.nOnMLDetNo || '').trim() || '0', 10) || 0
|
||||
})).filter(x => x.n_onml_det_no > 0)
|
||||
|
||||
const payload = {
|
||||
detail_source: isNoCostDetail.value ? 'no-cost' : 'has-cost',
|
||||
header: {
|
||||
n_onml_no: parseInt(String(header?.nOnMLNo || onMLNo.value || '0').trim() || '0', 10) || 0,
|
||||
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim(),
|
||||
urun_adi: String(header?.UrunAdi || '').trim(),
|
||||
maliyet_tarihi: normalizeDateInput(costDate.value),
|
||||
n_urt_recete_id: parseInt(String(header?.nUrtReceteID || recipeCode.value || '0').trim() || '0', 10) || 0,
|
||||
uretim_sekli_id: parseInt(String(header?.UretimSekliID || '0').trim() || '0', 10) || 0,
|
||||
s_aciklama: String(header?.sAciklama || header?.SAciklama || '').trim(),
|
||||
firma_kodu: String(header?.FirmaKodu || '').trim(),
|
||||
n_firma_id: parseInt(String(header?.nFirmaID || header?.NFirmaID || '0').trim() || '0', 10) || 0
|
||||
},
|
||||
detail: { upserts, deletes }
|
||||
}
|
||||
|
||||
const response = await post('/pricing/production-product-costing/onml/save', payload)
|
||||
const newOnMLNo = parseInt(String(response?.n_onml_no || '0'), 10) || 0
|
||||
|
||||
$q.notify({ type: 'positive', message: 'Kaydedildi.', position: 'top-right' })
|
||||
|
||||
// If we created a new OnML (no-cost), switch to has-cost detail mode.
|
||||
if (isNoCostDetail.value && newOnMLNo > 0) {
|
||||
router.replace({
|
||||
name: 'production-product-costing-has-cost-detail',
|
||||
query: {
|
||||
n_onml_no: String(newOnMLNo),
|
||||
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
@@ -3158,6 +3325,8 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateStickyTop)
|
||||
ensureBeforeUnloadGuard(false)
|
||||
if (localDraftTimer.value) window.clearTimeout(localDraftTimer.value)
|
||||
// Defensive: if component unmounts without router guard (rare), drop the draft.
|
||||
clearLocalDraft()
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -3169,6 +3338,8 @@ watch(
|
||||
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
// Always clear local draft on page exit; backend is source of truth.
|
||||
clearLocalDraft()
|
||||
next()
|
||||
return
|
||||
}
|
||||
@@ -3177,20 +3348,13 @@ onBeforeRouteLeave((to, from, next) => {
|
||||
title: 'Sayfadan ayriliyorsunuz',
|
||||
message: pageMode.value === 'edit'
|
||||
? 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?'
|
||||
: 'Taslak korunacak. Sayfadan cikmak istiyor musunuz?',
|
||||
: 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?',
|
||||
ok: { label: 'Evet', color: 'negative' },
|
||||
cancel: { label: 'Hayir' },
|
||||
persistent: true
|
||||
})
|
||||
.onOk(() => {
|
||||
// NEW (no-cost): always persist draft so it can be resumed
|
||||
if (pageMode.value === 'new') {
|
||||
persistLocalDraftNow()
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// EDIT (has-cost): allow exit, keep draft for now (later we can clear it after successful save)
|
||||
clearLocalDraft()
|
||||
next()
|
||||
})
|
||||
.onCancel(() => next(false))
|
||||
|
||||
Reference in New Issue
Block a user