Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -84,6 +84,16 @@
|
||||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||||
@click="saveChanges"
|
||||
/>
|
||||
<q-btn
|
||||
label="PDF"
|
||||
icon="picture_as_pdf"
|
||||
dense
|
||||
color="grey-9"
|
||||
outline
|
||||
class="pcd-toolbar-btn"
|
||||
:disable="!detailHeader || detailLoading || saveLoading"
|
||||
@click="exportCostingPDF"
|
||||
/>
|
||||
<q-btn
|
||||
label="Kaydi Sil"
|
||||
icon="delete"
|
||||
@@ -244,6 +254,9 @@
|
||||
{{ 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">
|
||||
Toplam Miktar: {{ formatBarQuantity(resolveGroupQuantity(grp)) }} MT |
|
||||
</span>
|
||||
Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }}
|
||||
<q-icon
|
||||
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
|
||||
@@ -529,6 +542,16 @@
|
||||
@filter="filterRowEditorItemOptions"
|
||||
@update:model-value="onRowEditorItemChange"
|
||||
/>
|
||||
<div v-if="showFabricCopyBtn" class="q-mt-xs row items-center q-gutter-xs">
|
||||
<q-btn
|
||||
dense
|
||||
outline
|
||||
color="primary"
|
||||
icon="content_copy"
|
||||
label="Kumastan Kopyala"
|
||||
@click="openFabricCopyDialog"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-select
|
||||
@@ -599,6 +622,41 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- FABRIC copy helper: pick another fabric row and copy Code + Price + Pr.Br into editor -->
|
||||
<q-dialog v-model="fabricCopyDialogOpen">
|
||||
<q-card style="min-width: min(720px, 92vw);">
|
||||
<q-card-section class="row items-center justify-between q-pb-sm">
|
||||
<div class="text-subtitle1 text-weight-bold">Kumastan Kopyala</div>
|
||||
<q-btn flat round dense icon="close" v-close-popup />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section class="q-pa-md">
|
||||
<q-select
|
||||
v-model="fabricCopySelectedKey"
|
||||
dense
|
||||
filled
|
||||
label="Kaynak Kumaş"
|
||||
:options="fabricCopyOptions"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-actions align="right" class="q-pa-sm">
|
||||
<q-btn flat label="Iptal" color="grey-7" v-close-popup />
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
label="Kopyala"
|
||||
:disable="!fabricCopySelectedKey"
|
||||
@click="applyFabricCopySelection"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="lineHistoryDialogOpen" maximized>
|
||||
<q-card class="pcd-history-dialog">
|
||||
<q-card-section class="row items-center justify-between q-gutter-sm">
|
||||
@@ -744,7 +802,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { get, post, extractApiErrorDetail } from 'src/services/api'
|
||||
import { get, post, download, extractApiErrorDetail } from 'src/services/api'
|
||||
import { createTraceId, slog } from 'src/utils/slog'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -773,6 +831,8 @@ const rowEditorDialogOpen = ref(false)
|
||||
const rowEditorMode = ref('new')
|
||||
const rowEditorTargetRowKey = ref('')
|
||||
const rowEditorForm = ref(createRowEditorForm())
|
||||
const fabricCopyDialogOpen = ref(false)
|
||||
const fabricCopySelectedKey = ref('')
|
||||
|
||||
// Draggable "Satir Duzenle" dialog (mouse drag by header).
|
||||
const rowEditorDialogPos = ref({ x: 0, y: 0 })
|
||||
@@ -872,10 +932,53 @@ const priceCurrencyOptions = [
|
||||
]
|
||||
const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : []))
|
||||
|
||||
const showFabricCopyBtn = computed(() => normalizeGroupName(rowEditorForm.value?.sAciklama3 || '') === 'FABRIC')
|
||||
|
||||
const fabricCopySourceRows = computed(() => {
|
||||
const currentKey = String(rowEditorTargetRowKey.value || '').trim()
|
||||
return (Array.isArray(flatDetailRows.value) ? flatDetailRows.value : [])
|
||||
.filter(r => normalizeGroupName(r?.sAciklama3 || '') === 'FABRIC')
|
||||
.filter(r => String(r?.__rowKey || '').trim() && String(r?.__rowKey || '').trim() !== currentKey)
|
||||
})
|
||||
|
||||
const fabricCopyOptions = computed(() => {
|
||||
// Deduplicate for UX: group by (code + color + hammaddeNo). Keep most recently changed first.
|
||||
const list = (Array.isArray(fabricCopySourceRows.value) ? fabricCopySourceRows.value : [])
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const da = new Date(a?.dteIslemTarihiDeg || a?.dteIslemTarihi || 0).getTime()
|
||||
const db = new Date(b?.dteIslemTarihiDeg || b?.dteIslemTarihi || 0).getTime()
|
||||
return db - da
|
||||
})
|
||||
|
||||
const seen = new Set()
|
||||
const out = []
|
||||
for (const r of list) {
|
||||
const code = String(r?.sKodu || '').trim()
|
||||
if (!code) continue
|
||||
const color = String(resolveRowColorCode(r) || '').trim()
|
||||
const hNo = String(r?.nHammaddeTuruNo || '').trim()
|
||||
const hName = String(r?.sHammaddeTuruAdi || '').trim()
|
||||
const key = `${hNo}|${code}|${color}`
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
const price = resolveNumericRowInputPrice(r)
|
||||
const cur = resolveInputCurrency(r)
|
||||
const desc = String(r?.sAciklama || '').trim()
|
||||
const hLabel = `${hNo}${hName ? ` - ${hName}` : ''}`
|
||||
const label = `${hLabel} | ${code}${desc ? ` - ${desc}` : ''}${color ? ` | ${color}` : ''}${price > 0 ? ` | ${price} ${cur}` : ''}`
|
||||
out.push({ value: key, label })
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
// no-cost: required parca slots (from Maliyet Parca Eslestirme)
|
||||
const requiredParcaMappings = ref([])
|
||||
const requiredAttentionRowKeys = ref({})
|
||||
const requiredHammaddeMetaCache = ref({}) // hammaddeNo -> { groupName, parcaAdi }
|
||||
// When user explicitly deletes a requiredPlaceholder row, we should not keep warning/recreating it
|
||||
// for this draft/session. Key format: `${mtBolumID}:${hNo}`.
|
||||
const suppressedRequiredKeys = ref({})
|
||||
|
||||
// Bump this when draft payload semantics change, to avoid hydrating stale rows after backend logic updates.
|
||||
const DRAFT_STORAGE_VERSION = 'v2'
|
||||
@@ -929,6 +1032,7 @@ function persistLocalDraftNow () {
|
||||
UretimSekli: String(detailHeader.value?.UretimSekli || '').trim()
|
||||
} : null,
|
||||
deletedDetailRows: Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : [],
|
||||
suppressedRequiredKeys: suppressedRequiredKeys.value || {},
|
||||
detailGroups: Array.isArray(detailGroups.value) ? detailGroups.value : []
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(payload))
|
||||
@@ -972,6 +1076,7 @@ function tryHydrateFromLocalDraft () {
|
||||
const payload = JSON.parse(raw)
|
||||
if (!payload || typeof payload !== 'object') return false
|
||||
if (Array.isArray(payload.deletedDetailRows)) deletedDetailRows.value = payload.deletedDetailRows
|
||||
if (payload.suppressedRequiredKeys && typeof payload.suppressedRequiredKeys === 'object') suppressedRequiredKeys.value = payload.suppressedRequiredKeys
|
||||
if (Array.isArray(payload.detailGroups)) detailGroups.value = normalizeDetailGroups(payload.detailGroups)
|
||||
if (payload.header && detailHeader.value) {
|
||||
detailHeader.value.UretimSekliID = String(payload.header.UretimSekliID || '').trim()
|
||||
@@ -1189,6 +1294,11 @@ function formatBarMoney (value) {
|
||||
return formatMoney(roundedValue)
|
||||
}
|
||||
|
||||
function formatBarQuantity (value) {
|
||||
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 10000) / 10000
|
||||
return formatQuantity(roundedValue)
|
||||
}
|
||||
|
||||
function normalizePriceCurrency (value) {
|
||||
const normalizedValue = String(value || '').trim().toUpperCase()
|
||||
return ['USD', 'TRY', 'EUR', 'GBP'].includes(normalizedValue) ? normalizedValue : ''
|
||||
@@ -2091,6 +2201,11 @@ function resolveGroupUSDTutar (grp) {
|
||||
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowUSDTutar(row) : 0), 0)
|
||||
}
|
||||
|
||||
function resolveGroupQuantity (grp) {
|
||||
const items = Array.isArray(grp?.items) ? grp.items : []
|
||||
return items.reduce((acc, row) => acc + (Number(row?.lMiktar || 0) || 0), 0)
|
||||
}
|
||||
|
||||
function groupKey (grp, gi) {
|
||||
return `${String(grp?.sAciklama3 || 'TANIMSIZ')}-${gi}`
|
||||
}
|
||||
@@ -2264,15 +2379,20 @@ async function fetchDetail (options = {}) {
|
||||
if (hydrateDraft) {
|
||||
tryHydrateFromLocalDraft()
|
||||
}
|
||||
// ensure required placeholder rows exist (based on mapping screen)
|
||||
try {
|
||||
const mappings = await fetchRequiredParcaMappings()
|
||||
await ensureNoCostRequiredRowsFromMappings(mappings)
|
||||
} catch (err) {
|
||||
slog.error('production-product-costing.detail', 'required-mapping:error', {
|
||||
trace_id: traceId.value,
|
||||
detail: await extractApiErrorDetail(err)
|
||||
})
|
||||
// Ensure required placeholder rows exist (based on mapping screen) only for the "new/no-cost" flow.
|
||||
// For has-cost records (existing), we do not inject placeholders after load.
|
||||
// Placeholders should be created only when detail_source=no-cost (route query),
|
||||
// so saved records always render exactly what backend has.
|
||||
if (isNoCostDetail.value) {
|
||||
try {
|
||||
const mappings = await fetchRequiredParcaMappings()
|
||||
await ensureNoCostRequiredRowsFromMappings(mappings)
|
||||
} catch (err) {
|
||||
slog.error('production-product-costing.detail', 'required-mapping:error', {
|
||||
trace_id: traceId.value,
|
||||
detail: await extractApiErrorDetail(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
const initialOpen = {}
|
||||
detailGroups.value.forEach((grp, gi) => {
|
||||
@@ -2821,6 +2941,44 @@ function onRowEditorHammaddeChange (value) {
|
||||
}
|
||||
}
|
||||
|
||||
function openFabricCopyDialog () {
|
||||
fabricCopySelectedKey.value = ''
|
||||
fabricCopyDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function applyFabricCopySelection () {
|
||||
const key = String(fabricCopySelectedKey.value || '').trim()
|
||||
if (!key) return
|
||||
const match = fabricCopySourceRows.value.find(r => {
|
||||
const code = String(r?.sKodu || '').trim()
|
||||
const color = String(resolveRowColorCode(r) || '').trim()
|
||||
const hNo = String(r?.nHammaddeTuruNo || '').trim()
|
||||
return `${hNo}|${code}|${color}` === key
|
||||
})
|
||||
if (!match) {
|
||||
fabricCopyDialogOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Copy: Code + Price + Currency. Leave color as-is (user may be copying price only).
|
||||
rowEditorForm.value.sKodu = String(match.sKodu || '').trim()
|
||||
// Ensure select can render the chosen value even if it isn't in the current options list.
|
||||
upsertEditorOption(rowEditorItemAllOptions, buildRowEditorItemOption({ ...match, value: String(match.sKodu || '').trim() }))
|
||||
upsertEditorOption(rowEditorItemOptions, buildRowEditorItemOption({ ...match, value: String(match.sKodu || '').trim() }))
|
||||
try {
|
||||
await onRowEditorItemChange(String(match.sKodu || '').trim())
|
||||
} catch {}
|
||||
|
||||
const price = resolveNumericRowInputPrice(match)
|
||||
const cur = resolveInputCurrency(match) || 'USD'
|
||||
rowEditorForm.value.inputPrice = normalizeInputPrice(price)
|
||||
rowEditorForm.value.fiyat_girilen = parseMoneyInput(price)
|
||||
rowEditorForm.value.inputPricePrBr = normalizePriceCurrency(cur) || 'USD'
|
||||
rowEditorForm.value.fiyat_doviz = normalizePriceCurrency(cur) || 'USD'
|
||||
|
||||
fabricCopyDialogOpen.value = false
|
||||
}
|
||||
|
||||
async function onRowEditorItemChange (value) {
|
||||
const selected = rowEditorItemOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||||
rowEditorForm.value.sKodu = String(value || '').trim()
|
||||
@@ -2980,8 +3138,9 @@ async function fetchRequiredParcaMappings () {
|
||||
async function resolveHammaddeMetaForRequired (hNo) {
|
||||
const key = String(hNo || '').trim()
|
||||
if (!key) return null
|
||||
// Always attempt a fresh resolve; backend rules for active/inactive hammadde types can change.
|
||||
// We still store into cache as a last-known-good for the current page load.
|
||||
const cached = requiredHammaddeMetaCache.value?.[key]
|
||||
if (cached) return cached
|
||||
|
||||
try {
|
||||
const rows = await get('/pricing/production-product-costing/detail-editor-options', {
|
||||
@@ -3009,7 +3168,7 @@ async function resolveHammaddeMetaForRequired (hNo) {
|
||||
return meta
|
||||
} catch {
|
||||
requiredHammaddeMetaCache.value = { ...(requiredHammaddeMetaCache.value || {}), [key]: { groupName: '', hammaddeAdi: '', mtBolumID: 0, parcaAdi: '' } }
|
||||
return null
|
||||
return cached || null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3067,6 +3226,7 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
const hNo = normalizeHammaddeNo(hNoRaw)
|
||||
if (!hNo) continue
|
||||
const reqKey = `${mappingMtBolumID}:${hNo}`
|
||||
if (suppressedRequiredKeys.value?.[reqKey]) continue
|
||||
if (processedRequiredKeys.has(reqKey)) continue
|
||||
processedRequiredKeys.add(reqKey)
|
||||
|
||||
@@ -3093,12 +3253,19 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
if (anyMatch) {
|
||||
// If we previously created a placeholder under a wrong group name, move it instead of duplicating.
|
||||
if (anyMatch?.requiredPlaceholder) {
|
||||
const ensuredDetNo = String(anyMatch?.nOnMLDetNo || '').trim() || getNextDetailNo()
|
||||
const currentGroup = normalizeGroupName(anyMatch?.sAciklama3)
|
||||
const currentParcaAdi = normalizeGroupName(anyMatch?.sParcaAdi || '')
|
||||
const currentHammaddeAdi = String(anyMatch?.sHammaddeTuruAdi || '').trim()
|
||||
if (currentGroup !== effectiveGroupName || currentParcaAdi !== desiredParcaAdi || (hammaddeAdi && currentHammaddeAdi !== hammaddeAdi)) {
|
||||
if (
|
||||
String(anyMatch?.nOnMLDetNo || '').trim() !== ensuredDetNo ||
|
||||
currentGroup !== effectiveGroupName ||
|
||||
currentParcaAdi !== desiredParcaAdi ||
|
||||
(hammaddeAdi && currentHammaddeAdi !== hammaddeAdi)
|
||||
) {
|
||||
const moved = recalculateDetailRow({
|
||||
...anyMatch,
|
||||
nOnMLDetNo: ensuredDetNo,
|
||||
sAciklama3: effectiveGroupName,
|
||||
sParcaAdi: desiredParcaAdi,
|
||||
nUrtMTBolumID: mtBolumID || anyMatch?.nUrtMTBolumID || anyMatch?.NUrtMTBolumID || 0,
|
||||
@@ -3124,7 +3291,8 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
isNew: true,
|
||||
nUrtMTBolumID: mtBolumID,
|
||||
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||||
nOnMLDetNo: '',
|
||||
// IMPORTANT: Backend skips upserts where nOnMLDetNo <= 0. Required placeholders must have a real number.
|
||||
nOnMLDetNo: getNextDetailNo(),
|
||||
// Group header key
|
||||
sAciklama3: effectiveGroupName,
|
||||
// Parca adi column value (CEKET/PANTOLON/YELEK...)
|
||||
@@ -3162,6 +3330,142 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
}
|
||||
}
|
||||
|
||||
// CM2 auto-required: include all active CM2 hammadde types (bAktif=1) as required placeholders too.
|
||||
// This is independent of the mapping screen, because CM2 labor lines are managed in OnML detail.
|
||||
try {
|
||||
// Filter CM2 types by the parts that exist for this product (MTBolumIDs coming from mapping list).
|
||||
// This prevents irrelevant CM2 types (e.g. GML/KBN/MNT) from showing up for products that don't have those parts.
|
||||
const allowedMtBolumIDs = new Set()
|
||||
for (const m of list) {
|
||||
const mt = parseInt(String(m?.nUrtMTBolumID ?? m?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||
if (mt > 0) allowedMtBolumIDs.add(mt)
|
||||
}
|
||||
if (allowedMtBolumIDs.size === 0) {
|
||||
// No parts to scope CM2 by -> don't add global CM2 placeholders.
|
||||
return
|
||||
}
|
||||
|
||||
const cm2Types = await get('/pricing/production-product-costing/detail-editor-options', {
|
||||
kind: 'hammadde',
|
||||
group: 'CM2',
|
||||
only_active: 1,
|
||||
search: '',
|
||||
limit: 200,
|
||||
trace_id: traceId.value
|
||||
})
|
||||
const cm2List = Array.isArray(cm2Types) ? cm2Types : []
|
||||
for (const it of cm2List) {
|
||||
const hNo = normalizeHammaddeNo(it?.nHammaddeTuruNo ?? it?.value)
|
||||
if (!hNo) continue
|
||||
const meta = {
|
||||
groupName: String(it?.sAciklama3 || it?.sAciklama2 || 'CM2').trim(),
|
||||
hammaddeAdi: String(it?.sHammaddeTuruAdi || it?.sAciklama || '').trim(),
|
||||
mtBolumID: parseInt(String(it?.mtUrtMTBolumID ?? it?.MTUrtMTBolumID ?? it?.nUrtMTBolumID ?? it?.NUrtMTBolumID ?? '0'), 10) || 0,
|
||||
parcaAdi: String(it?.sParcaAdi || '').trim()
|
||||
}
|
||||
if (!(meta.mtBolumID > 0) || !allowedMtBolumIDs.has(meta.mtBolumID)) continue
|
||||
const effectiveGroupName = normalizeGroupName(meta.groupName || 'CM2') || 'CM2'
|
||||
const mtBolumID = meta.mtBolumID || 0
|
||||
const desiredParcaAdi = normalizeGroupName(meta.parcaAdi || '') || 'CM2'
|
||||
const reqKey = `${mtBolumID}:${hNo}`
|
||||
if (mtBolumID > 0 && suppressedRequiredKeys.value?.[reqKey]) continue
|
||||
|
||||
const anyMatch = flatDetailRows.value.find(r => {
|
||||
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||||
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||
if (rowMtBolumID > 0 && mtBolumID > 0) return rowMtBolumID === mtBolumID
|
||||
return normalizeGroupName(r?.sParcaAdi) === desiredParcaAdi
|
||||
})
|
||||
if (anyMatch) {
|
||||
// Ensure existing required CM2 placeholder has a valid detNo, otherwise backend will skip it on save.
|
||||
if (anyMatch?.requiredPlaceholder && String(anyMatch?.nOnMLDetNo || '').trim() === '') {
|
||||
const patched = recalculateDetailRow({
|
||||
...anyMatch,
|
||||
nOnMLDetNo: getNextDetailNo(),
|
||||
draftChanged: true
|
||||
}, {
|
||||
preserveInputs: true,
|
||||
priceType: 'REQ',
|
||||
updateState: 'required-cm2',
|
||||
markChanged: true
|
||||
})
|
||||
applyEditorRowToGroups(patched)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
newRowSequence.value += 1
|
||||
const rowKey = `req-auto-cm2-row-${newRowSequence.value}`
|
||||
const placeholder = recalculateDetailRow({
|
||||
__rowKey: rowKey,
|
||||
isNew: true,
|
||||
nUrtMTBolumID: mtBolumID,
|
||||
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||||
// IMPORTANT: Backend skips upserts where nOnMLDetNo <= 0. Required placeholders must have a real number.
|
||||
nOnMLDetNo: getNextDetailNo(),
|
||||
sAciklama3: effectiveGroupName,
|
||||
sParcaAdi: desiredParcaAdi,
|
||||
nHammaddeTuruNo: hNo,
|
||||
sHammaddeTuruAdi: meta.hammaddeAdi,
|
||||
sKodu: '',
|
||||
sAciklama: '',
|
||||
sRenk: '',
|
||||
ColorCode: '',
|
||||
ColorDescription: '',
|
||||
lMiktar: 1,
|
||||
miktarInput: '1',
|
||||
inputPrice: '0',
|
||||
inputPricePrBr: 'USD',
|
||||
fiyat_girilen: 0,
|
||||
fiyat_doviz: 'USD',
|
||||
maliyeteDahil: true,
|
||||
maliyete_dahil: 1,
|
||||
Maliyete_dahil: 1,
|
||||
cmPriceTypeId: normalizeCMPriceTypeId(1, effectiveGroupName),
|
||||
cm_price_type_id: normalizeCMPriceTypeId(1, effectiveGroupName),
|
||||
sBirim: 'AD',
|
||||
draftChanged: true,
|
||||
requiredPlaceholder: true
|
||||
}, {
|
||||
preserveInputs: true,
|
||||
priceType: 'REQ',
|
||||
updateState: 'required-cm2',
|
||||
markChanged: true
|
||||
})
|
||||
applyEditorRowToGroups(placeholder)
|
||||
}
|
||||
} catch (e) {
|
||||
slog.error('production-product-costing.detail', 'cm2:required:error', {
|
||||
trace_id: traceId.value,
|
||||
error: String(e?.message || e)
|
||||
})
|
||||
}
|
||||
|
||||
// Debug: required placeholders must always have a valid detNo.
|
||||
try {
|
||||
const flatNow = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
|
||||
const reqRows = flatNow.filter(r => Boolean(r?.requiredPlaceholder))
|
||||
const missingDetNo = reqRows.filter(r => (parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0) <= 0)
|
||||
slog.info('production-product-costing.detail', 'required:placeholders:ensured', {
|
||||
trace_id: traceId.value,
|
||||
required_count: reqRows.length,
|
||||
required_missing_detno: missingDetNo.length,
|
||||
required_cm2_count: reqRows.filter(r => normalizeGroupName(r?.sAciklama3) === 'CM2').length
|
||||
})
|
||||
if (missingDetNo.length > 0) {
|
||||
slog.warn('production-product-costing.detail', 'required:placeholders:missing-detno', {
|
||||
trace_id: traceId.value,
|
||||
sample: missingDetNo.slice(0, 10).map(r => ({
|
||||
detNo: String(r?.nOnMLDetNo || ''),
|
||||
hNo: String(r?.nHammaddeTuruNo || ''),
|
||||
mtBolumID: String(r?.nUrtMTBolumID || ''),
|
||||
group: String(r?.sAciklama3 || ''),
|
||||
rowKey: String(r?.__rowKey || '')
|
||||
}))
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// CM2 special case:
|
||||
// Bulk autofill is disabled for CM2 (and FABRIC) to prevent wrong price pulls,
|
||||
// but we still want the I.* code templates (code + description) to appear on page open.
|
||||
@@ -3243,6 +3547,9 @@ function computeMissingRequiredSlots () {
|
||||
const missing = []
|
||||
if (list.length === 0) return missing
|
||||
|
||||
// Expand suppression keys: support both mappingMtBolumID and meta-derived mtBolumID.
|
||||
const suppressed = suppressedRequiredKeys.value || {}
|
||||
|
||||
list.forEach(mapping => {
|
||||
// Required slots are defined per (ParcaBolum / MTBolumID) + (HammaddeTuruNo).
|
||||
// Do NOT match by row.sAciklama3, because that field is the "group header" (DT/TP/CM2/FABRIC), not the part.
|
||||
@@ -3252,6 +3559,33 @@ function computeMissingRequiredSlots () {
|
||||
hList.forEach(hNoRaw => {
|
||||
const hNo = normalizeHammaddeNo(hNoRaw)
|
||||
if (!hNo) return
|
||||
const reqKeyByMapping = `${mappingMtBolumID}:${hNo}`
|
||||
// Also allow suppression by the mtBolumID that the placeholder row itself uses (meta override).
|
||||
const reqKeyByRowMt = (() => {
|
||||
const rowLike = flatDetailRows.value.find(r => normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo && Boolean(r?.requiredPlaceholder))
|
||||
const rowMt = parseInt(String(rowLike?.nUrtMTBolumID ?? rowLike?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||
return rowMt > 0 ? `${rowMt}:${hNo}` : ''
|
||||
})()
|
||||
|
||||
// User explicitly removed this required placeholder in the UI; don't keep warning about it.
|
||||
if (suppressed?.[reqKeyByMapping] || (reqKeyByRowMt && suppressed?.[reqKeyByRowMt])) {
|
||||
// Auto-clear suppression if a real row exists again.
|
||||
const existing = flatDetailRows.value.find(r => {
|
||||
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||||
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||
if (mappingMtBolumID > 0 && rowMtBolumID > 0) return mappingMtBolumID === rowMtBolumID
|
||||
return normalizeGroupName(r?.sParcaAdi) === mappingParcaAdi
|
||||
})
|
||||
if (existing) {
|
||||
const next = { ...(suppressedRequiredKeys.value || {}) }
|
||||
delete next[reqKeyByMapping]
|
||||
if (reqKeyByRowMt) delete next[reqKeyByRowMt]
|
||||
suppressedRequiredKeys.value = next
|
||||
schedulePersistLocalDraft()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const match = flatDetailRows.value.find(r => {
|
||||
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||||
@@ -3294,6 +3628,16 @@ function removeDetailRowByKey (rowKey) {
|
||||
|
||||
detailGroups.value = nextGroups
|
||||
syncAllGroupsOpen()
|
||||
// If user deletes an auto-created required placeholder, suppress the corresponding required key
|
||||
// so we don't keep recreating/warning about it for this draft.
|
||||
if (existingRow?.requiredPlaceholder) {
|
||||
const hNo = normalizeHammaddeNo(existingRow?.nHammaddeTuruNo)
|
||||
const mtBolumID = parseInt(String(existingRow?.nUrtMTBolumID ?? existingRow?.NUrtMTBolumID ?? '0'), 10) || 0
|
||||
if (hNo && mtBolumID > 0) {
|
||||
const reqKey = `${mtBolumID}:${hNo}`
|
||||
suppressedRequiredKeys.value = { ...(suppressedRequiredKeys.value || {}), [reqKey]: true }
|
||||
}
|
||||
}
|
||||
if (existingRow && !existingRow?.isNew) {
|
||||
deletedDetailRows.value = [
|
||||
...(Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []),
|
||||
@@ -3588,17 +3932,79 @@ async function confirmRefresh () {
|
||||
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
||||
}
|
||||
|
||||
function scrollToFirstRequiredAttentionRow () {
|
||||
// Best-effort DOM scroll: q-table renders rowClass on the <tr>.
|
||||
// This avoids users having to hunt for missing rows after we block save.
|
||||
try {
|
||||
const el = document.querySelector('.pcd-detail-row-required')
|
||||
if (el && typeof el.scrollIntoView === 'function') {
|
||||
el.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function saveChanges () {
|
||||
saveLoading.value = true
|
||||
try {
|
||||
requiredAttentionRowKeys.value = {}
|
||||
if (isNoCostDetail.value) {
|
||||
const missing = computeMissingRequiredSlots()
|
||||
if (missing.length > 0) {
|
||||
slog.info('production-product-costing.detail', 'required:missing:computed', {
|
||||
trace_id: traceId.value,
|
||||
missing_count: missing.length,
|
||||
sample: missing.slice(0, 20).map(m => ({
|
||||
mtBolumID: m?.mtBolumID,
|
||||
parcaAdi: String(m?.parcaAdi || '').trim(),
|
||||
hNo: String(m?.nHammaddeTuruNo || '').trim(),
|
||||
rowKey: String(m?.rowKey || '').trim()
|
||||
}))
|
||||
})
|
||||
if (missing.length > 0) {
|
||||
const rowsHtml = missing.slice(0, 40).map((m, i) => {
|
||||
const key = String(m?.rowKey || '').trim()
|
||||
const row = key ? flatDetailRows.value.find(r => String(r?.__rowKey || '').trim() === key) : null
|
||||
const code = String(row?.sKodu || '').trim()
|
||||
const desc = String(row?.sAciklama || '').trim()
|
||||
return `
|
||||
<tr>
|
||||
<td style="padding:4px 6px; border-bottom:1px solid #eee;">${i + 1}</td>
|
||||
<td style="padding:4px 6px; border-bottom:1px solid #eee;">${String(m?.parcaAdi || '').trim()}</td>
|
||||
<td style="padding:4px 6px; border-bottom:1px solid #eee;">${String(m?.nHammaddeTuruNo || '').trim()}</td>
|
||||
<td style="padding:4px 6px; border-bottom:1px solid #eee; color:#b71c1c;">${code || '-'}</td>
|
||||
<td style="padding:4px 6px; border-bottom:1px solid #eee; color:#b71c1c;">${desc || '-'}</td>
|
||||
</tr>
|
||||
`
|
||||
}).join('')
|
||||
const truncatedNote = missing.length > 40
|
||||
? `<div style="margin-top:8px; color:#777;">Ilk 40 satir gosterildi. Toplam: ${missing.length}</div>`
|
||||
: ''
|
||||
const ok = await new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: 'Eksik Maliyet Parcalari',
|
||||
message: `Eslestirilen parcalarda fiyat girilmemis satirlar var. Devam etmek istiyor musunuz? (Eksik: ${missing.length})`,
|
||||
html: true,
|
||||
message: `
|
||||
<div>
|
||||
Eslestirilen parcalarda fiyat girilmemis satirlar var. Devam etmek istiyor musunuz?
|
||||
<div style="margin-top:6px; font-weight:600;">Eksik: ${missing.length}</div>
|
||||
<div style="margin-top:8px; max-height:280px; overflow:auto; border:1px solid #eee;">
|
||||
<table style="width:100%; border-collapse:collapse; font-size:12px;">
|
||||
<thead>
|
||||
<tr style="text-align:left; background:#fafafa;">
|
||||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">No</th>
|
||||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Parca</th>
|
||||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Hammadde</th>
|
||||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Kod</th>
|
||||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Aciklama</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rowsHtml}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${truncatedNote}
|
||||
</div>
|
||||
`,
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||||
@@ -3614,6 +4020,8 @@ async function saveChanges () {
|
||||
message: 'Eksik parcalar isaretlendi. Fiyat girip tekrar deneyin.',
|
||||
position: 'top-right'
|
||||
})
|
||||
// Scroll to first highlighted row.
|
||||
nextTick(() => scrollToFirstRequiredAttentionRow())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -3627,6 +4035,17 @@ async function saveChanges () {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required header fields
|
||||
const selectedUretimSekliID = parseInt(String(detailHeader.value?.UretimSekliID || '0').trim() || '0', 10) || 0
|
||||
if (!(selectedUretimSekliID > 0)) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Uretim Sekli secilmeden kayit yapilamaz.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate: "Kod" (sKodu) is mandatory for any row that has a hammadde type.
|
||||
// We block saving if there are rows with empty/whitespace code, to avoid sending blank rows to backend.
|
||||
const blankCodeRows = (Array.isArray(flatDetailRows.value) ? flatDetailRows.value : [])
|
||||
@@ -3651,6 +4070,24 @@ async function saveChanges () {
|
||||
}
|
||||
|
||||
const header = detailHeader.value
|
||||
// Debug: backend skips rows where n_onml_det_no <= 0. Log this before sending.
|
||||
try {
|
||||
const rows = Array.isArray(flatDetailRows.value) ? flatDetailRows.value : []
|
||||
const detNoZero = rows.filter(r => (parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0) <= 0)
|
||||
slog.info('production-product-costing.detail', 'save:payload:summary', {
|
||||
trace_id: traceId.value,
|
||||
row_count: rows.length,
|
||||
det_no_zero_count: detNoZero.length,
|
||||
det_no_zero_sample: detNoZero.slice(0, 10).map(r => ({
|
||||
rowKey: String(r?.__rowKey || ''),
|
||||
detNo: String(r?.nOnMLDetNo || ''),
|
||||
hNo: String(r?.nHammaddeTuruNo || ''),
|
||||
group: String(r?.sAciklama3 || ''),
|
||||
code: String(r?.sKodu || '').trim()
|
||||
}))
|
||||
})
|
||||
} catch {}
|
||||
|
||||
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,
|
||||
@@ -3734,6 +4171,27 @@ async function saveChanges () {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportCostingPDF () {
|
||||
try {
|
||||
const n = parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0
|
||||
if (!(n > 0)) {
|
||||
$q.notify({ type: 'warning', message: 'OnML No bulunamadi.', position: 'top-right' })
|
||||
return
|
||||
}
|
||||
const blob = await download('/pricing/production-product-costing/onml/pdf', {
|
||||
n_onml_no: n,
|
||||
trace_id: traceId.value
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
window.open(url, '_blank')
|
||||
window.setTimeout(() => {
|
||||
try { URL.revokeObjectURL(url) } catch {}
|
||||
}, 60_000)
|
||||
} catch (e) {
|
||||
$q.notify({ type: 'negative', message: await extractApiErrorDetail(e), position: 'top-right' })
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [onMLNo.value, productCode.value, recipeCode.value, detailSource.value],
|
||||
() => {
|
||||
@@ -4062,6 +4520,22 @@ watch(
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table) {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table th),
|
||||
.pcd-detail-table :deep(.q-table td) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table tbody td) {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table thead) {
|
||||
display: table-header-group !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user