<template>
|
<div class="out-stock-page art-full-height">
|
<ArtSearchBar
|
v-model="searchForm"
|
:items="searchItems"
|
:showExpand="true"
|
@search="handleSearch"
|
@reset="handleReset"
|
/>
|
|
<ElCard class="art-table-card">
|
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
<template #left>
|
<ElSpace wrap>
|
<ElButton :loading="testCreating" @click="handleCreateTestOrder">
|
{{ t('pages.orders.outStock.actions.createTest') }}
|
</ElButton>
|
<ElButton
|
type="primary"
|
:loading="createWaveLoading"
|
:disabled="loading || selectedRows.length === 0"
|
@click="handleCreateWave"
|
>
|
{{ t('pages.orders.outStock.actions.createWave') }}
|
</ElButton>
|
<ListExportPrint
|
:preview-visible="previewVisible"
|
@update:previewVisible="handlePreviewVisibleChange"
|
:report-title="reportTitle"
|
:selected-rows="selectedRows"
|
:query-params="reportQueryParams"
|
:columns="columns"
|
:preview-rows="previewRows"
|
:preview-meta="resolvedPreviewMeta"
|
:total="pagination.total"
|
:disabled="loading"
|
@export="handleExport"
|
@print="handlePrint"
|
/>
|
</ElSpace>
|
</template>
|
</ArtTableHeader>
|
|
<ArtTable
|
:loading="loading"
|
:data="data"
|
:columns="columns"
|
:pagination="pagination"
|
@selection-change="handleSelectionChange"
|
@pagination:size-change="handleSizeChange"
|
@pagination:current-change="handleCurrentChange"
|
/>
|
</ElCard>
|
|
<OutStockDetailDrawer
|
v-model:visible="detailDrawerVisible"
|
:loading="detailLoading"
|
:items-loading="detailItemsLoading"
|
:detail="detailData"
|
:item-rows="detailItemRows"
|
:item-columns="detailItemColumns"
|
:pagination="detailItemPagination"
|
@refresh="loadDetailResources"
|
@size-change="handleDetailSizeChange"
|
@current-change="handleDetailCurrentChange"
|
/>
|
</div>
|
</template>
|
|
<script setup>
|
import { computed, reactive, ref } from 'vue'
|
import { ElButton, ElMessage, ElMessageBox, ElSpace } from 'element-plus'
|
import { useRouter } from 'vue-router'
|
import { useI18n } from 'vue-i18n'
|
import { useUserStore } from '@/store/modules/user'
|
import { useTable } from '@/hooks/core/useTable'
|
import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
|
import { fetchDictDataPage } from '@/api/system-manage'
|
import { fetchMatnrPage } from '@/api/wh-mat'
|
import ListExportPrint from '@/components/biz/list-export-print/index.vue'
|
import { defaultResponseAdapter } from '@/utils/table/tableUtils'
|
import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
|
import { fetchOutStockItemPage } from '@/api/out-stock-item'
|
import {
|
fetchCancelOutStock,
|
fetchCompleteOutStock,
|
fetchDeleteOutStock,
|
fetchExportOutStockReport,
|
fetchGetOutStockDetail,
|
fetchGetOutStockMany,
|
fetchOutStockPage,
|
fetchSaveOutStockWithItems
|
} from '@/api/out-stock'
|
import { fetchCreateOutStockWave } from '@/api/wave'
|
import OutStockDetailDrawer from './modules/out-stock-detail-drawer.vue'
|
import {
|
OUT_STOCK_REPORT_STYLE,
|
buildCreateWavePayload,
|
buildOutStockPageQueryParams,
|
buildOutStockPrintRows,
|
buildOutStockReportMeta,
|
buildOutStockSearchParams,
|
createOutStockSearchState,
|
getCreateWaveValidationMessage,
|
getOutStockReportTitle,
|
normalizeOutStockRow
|
} from './outStockPage.helpers'
|
import { createOutStockTableColumns } from './outStockTable.columns'
|
import { createOutStockItemTableColumns } from '../out-stock-item/outStockItemTable.columns.js'
|
import { normalizeOutStockItemRow } from '../out-stock-item/outStockItemPage.helpers.js'
|
|
defineOptions({ name: 'OutStock' })
|
const { t } = useI18n()
|
|
const userStore = useUserStore()
|
const router = useRouter()
|
const reportTitle = computed(() => getOutStockReportTitle(t))
|
const searchForm = ref(createOutStockSearchState())
|
const selectedRows = ref([])
|
const detailDrawerVisible = ref(false)
|
const detailLoading = ref(false)
|
const detailItemsLoading = ref(false)
|
const detailData = ref({})
|
const detailItemRows = ref([])
|
const activeOutStockId = ref(null)
|
const createWaveLoading = ref(false)
|
const testCreating = ref(false)
|
const detailItemPagination = reactive({
|
current: 1,
|
size: 20,
|
total: 0
|
})
|
const detailItemColumns = computed(() =>
|
createOutStockItemTableColumns({ t }).filter((column) => column.prop !== 'operation')
|
)
|
|
const reportQueryParams = computed(() => buildOutStockSearchParams(searchForm.value))
|
const searchItems = computed(() => [
|
{
|
label: t('pages.orders.outStock.search.condition'),
|
key: 'condition',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: t('pages.orders.outStock.search.conditionPlaceholder')
|
}
|
},
|
{
|
label: t('pages.orders.outStock.search.code'),
|
key: 'code',
|
type: 'input',
|
props: { clearable: true, placeholder: t('pages.orders.outStock.search.codePlaceholder') }
|
},
|
{
|
label: t('pages.orders.outStock.search.poCode'),
|
key: 'poCode',
|
type: 'input',
|
props: { clearable: true, placeholder: t('pages.orders.outStock.search.poCodePlaceholder') }
|
},
|
{
|
label: t('pages.orders.outStock.search.wkType'),
|
key: 'wkType',
|
type: 'select',
|
props: {
|
clearable: true,
|
placeholder: t('pages.orders.outStock.search.wkTypePlaceholder'),
|
options: [
|
{ label: t('pages.orders.outStock.businessType.salesOut'), value: 'sales_out' },
|
{ label: t('pages.orders.outStock.businessType.transferOut'), value: 'transfer_out' },
|
{ label: t('pages.orders.outStock.businessType.stockOut'), value: 'stock_out' },
|
{ label: t('pages.orders.outStock.businessType.preOut'), value: 'pre_out' }
|
]
|
}
|
},
|
{
|
label: t('pages.orders.outStock.search.exceStatus'),
|
key: 'exceStatus',
|
type: 'select',
|
props: {
|
clearable: true,
|
placeholder: t('pages.orders.outStock.search.exceStatusPlaceholder'),
|
options: [
|
{ label: t('pages.orders.outStock.status.initialized'), value: 10 },
|
{ label: t('pages.orders.outStock.status.pending'), value: 11 },
|
{ label: t('pages.orders.outStock.status.generated'), value: 13 },
|
{ label: t('pages.orders.outStock.status.running'), value: 14 },
|
{ label: t('pages.orders.outStock.status.completed'), value: 15 },
|
{ label: t('pages.orders.outStock.status.cancelled'), value: 8 }
|
]
|
}
|
},
|
{
|
label: t('pages.orders.outStock.search.rleStatus'),
|
key: 'rleStatus',
|
type: 'select',
|
props: {
|
clearable: true,
|
placeholder: t('pages.orders.outStock.search.rleStatusPlaceholder'),
|
options: [
|
{ label: t('common.status.normal'), value: 0 },
|
{ label: t('pages.orders.outStock.status.released'), value: 1 }
|
]
|
}
|
},
|
{
|
label: t('pages.orders.outStock.search.logisNo'),
|
key: 'logisNo',
|
type: 'input',
|
props: { clearable: true, placeholder: t('pages.orders.outStock.search.logisNoPlaceholder') }
|
},
|
{
|
label: t('pages.orders.outStock.search.customerName'),
|
key: 'customerName',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: t('pages.orders.outStock.search.customerNamePlaceholder')
|
}
|
},
|
{
|
label: t('pages.orders.outStock.search.saleOrgName'),
|
key: 'saleOrgName',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: t('pages.orders.outStock.search.saleOrgNamePlaceholder')
|
}
|
},
|
{
|
label: t('pages.orders.outStock.search.memo'),
|
key: 'memo',
|
type: 'input',
|
props: { clearable: true, placeholder: t('pages.orders.outStock.search.memoPlaceholder') }
|
}
|
])
|
|
async function loadDetailResources() {
|
if (!activeOutStockId.value) {
|
return
|
}
|
|
detailLoading.value = true
|
detailItemsLoading.value = true
|
try {
|
const [detailResponse, itemResponse] = await Promise.all([
|
guardRequestWithMessage(
|
fetchGetOutStockDetail(activeOutStockId.value),
|
{},
|
{ timeoutMessage: t('pages.orders.outStock.messages.detailTimeout') }
|
),
|
guardRequestWithMessage(
|
fetchOutStockItemPage({
|
orderId: activeOutStockId.value,
|
current: detailItemPagination.current,
|
pageSize: detailItemPagination.size
|
}),
|
{
|
records: [],
|
total: 0,
|
current: detailItemPagination.current,
|
size: detailItemPagination.size
|
},
|
{ timeoutMessage: t('pages.orders.outStock.messages.itemsTimeout') }
|
)
|
])
|
|
detailData.value = normalizeOutStockRow(detailResponse, t)
|
const normalized = defaultResponseAdapter(itemResponse)
|
detailItemRows.value = normalized.records.map((item) => normalizeOutStockItemRow(item, t))
|
detailItemPagination.total = Number(normalized.total || 0)
|
detailItemPagination.current = Number(normalized.current || detailItemPagination.current || 1)
|
detailItemPagination.size = Number(normalized.size || detailItemPagination.size || 20)
|
} finally {
|
detailLoading.value = false
|
detailItemsLoading.value = false
|
}
|
}
|
|
async function openDetail(row) {
|
activeOutStockId.value = row.id
|
detailDrawerVisible.value = true
|
detailItemPagination.current = 1
|
detailData.value = {}
|
detailItemRows.value = []
|
try {
|
await loadDetailResources()
|
} catch (error) {
|
detailDrawerVisible.value = false
|
detailData.value = {}
|
detailItemRows.value = []
|
ElMessage.error(error?.message || t('pages.orders.outStock.messages.detailLoadFailed'))
|
}
|
}
|
|
function handleDetailSizeChange(size) {
|
detailItemPagination.size = size
|
detailItemPagination.current = 1
|
loadDetailResources()
|
}
|
|
function handleDetailCurrentChange(current) {
|
detailItemPagination.current = current
|
loadDetailResources()
|
}
|
|
async function handleCreateWave() {
|
const validationMessage = getCreateWaveValidationMessage(selectedRows.value, t)
|
if (validationMessage) {
|
ElMessage.warning(validationMessage)
|
return
|
}
|
|
try {
|
await ElMessageBox.confirm(
|
t('pages.orders.outStock.messages.createWaveConfirm', {
|
count: selectedRows.value.length
|
}),
|
t('pages.orders.outStock.messages.createWaveTitle'),
|
{
|
confirmButtonText: t('common.confirm'),
|
cancelButtonText: t('common.cancel'),
|
type: 'warning'
|
}
|
)
|
|
createWaveLoading.value = true
|
await fetchCreateOutStockWave(buildCreateWavePayload(selectedRows.value))
|
ElMessage.success(t('pages.orders.outStock.messages.createWaveSuccess'))
|
selectedRows.value = []
|
await refreshData()
|
router.push('/orders/wave')
|
} catch (error) {
|
if (error === 'cancel' || error?.message === 'cancel') return
|
ElMessage.error(error?.message || t('pages.orders.outStock.messages.createWaveFailed'))
|
} finally {
|
createWaveLoading.value = false
|
}
|
}
|
|
async function loadFirstDictValue(dictTypeCode) {
|
const response = await fetchDictDataPage({
|
current: 1,
|
pageSize: 1,
|
dictTypeCode,
|
group: '2',
|
status: 1
|
})
|
const record = defaultResponseAdapter(response).records[0]
|
return record?.value ?? record?.dictValue ?? record?.id ?? ''
|
}
|
|
async function handleCreateTestOrder() {
|
testCreating.value = true
|
try {
|
const [wkType, materialResponse] = await Promise.all([
|
loadFirstDictValue('sys_business_type'),
|
fetchMatnrPage({ current: 1, pageSize: 3, status: 1 })
|
])
|
let materials = defaultResponseAdapter(materialResponse).records
|
|
if (!wkType) {
|
ElMessage.warning(t('pages.orders.outStock.messages.testCreateOptionsRequired'))
|
return
|
}
|
|
if (!materials.length) {
|
const fallbackMaterialResponse = await fetchMatnrPage({ current: 1, pageSize: 3 })
|
materials = defaultResponseAdapter(fallbackMaterialResponse).records
|
}
|
|
if (!materials.length) {
|
ElMessage.warning(t('pages.orders.outStock.messages.testCreateMaterialRequired'))
|
return
|
}
|
|
const timestamp = Date.now()
|
const batchPrefix = new Date().toISOString().slice(5, 10).replace('-', '')
|
const requestPayload = {
|
orders: {
|
type: 'out',
|
wkType,
|
poCode: `TEST-OUT-PO-${timestamp}`,
|
logisNo: `TEST-OUT-LN-${timestamp}`,
|
arrTime: new Date().toISOString(),
|
memo: t('pages.orders.outStock.messages.testCreateMemo')
|
},
|
items: Array.from({ length: 3 }, (_, index) => {
|
const material = materials[index] || materials[0]
|
return {
|
matnrId: material.id,
|
matnrCode: material.code || material.matnrCode || '',
|
maktx: material.name || material.maktx || '',
|
stockUnit: material.stockUnit || material.unit || '',
|
purUnit: material.purUnit || material.stockUnit || material.unit || '',
|
poDetlCode: `TEST-OUT-ITEM-${timestamp}-${index + 1}`,
|
splrBatch: `${batchPrefix}${String(index + 1).padStart(4, '0')}`,
|
anfme: 100,
|
qty: 0
|
}
|
})
|
}
|
|
await fetchSaveOutStockWithItems(requestPayload)
|
ElMessage.success(t('pages.orders.outStock.messages.testCreateSuccess'))
|
await refreshData()
|
} catch (error) {
|
ElMessage.error(error?.message || t('pages.orders.outStock.messages.testCreateFailed'))
|
} finally {
|
testCreating.value = false
|
}
|
}
|
|
async function handleComplete(row) {
|
try {
|
await ElMessageBox.confirm(
|
t('pages.orders.outStock.messages.completeConfirm', { code: row.code || '' }),
|
t('pages.orders.outStock.messages.completeTitle'),
|
{
|
confirmButtonText: t('common.confirm'),
|
cancelButtonText: t('common.cancel'),
|
type: 'warning'
|
}
|
)
|
await fetchCompleteOutStock(row.id)
|
ElMessage.success(t('pages.orders.outStock.messages.completeSuccess'))
|
await refreshData()
|
if (detailDrawerVisible.value && activeOutStockId.value === row.id) {
|
await loadDetailResources()
|
}
|
} catch (error) {
|
if (error === 'cancel' || error?.message === 'cancel') return
|
ElMessage.error(error?.message || t('pages.orders.outStock.messages.actionFailed'))
|
}
|
}
|
|
async function handleCancel(row) {
|
try {
|
await ElMessageBox.confirm(
|
t('pages.orders.outStock.messages.cancelConfirm', { code: row.code || '' }),
|
t('pages.orders.outStock.messages.cancelTitle'),
|
{
|
confirmButtonText: t('common.confirm'),
|
cancelButtonText: t('common.cancel'),
|
type: 'warning'
|
}
|
)
|
await fetchCancelOutStock(row.id)
|
ElMessage.success(t('pages.orders.outStock.messages.cancelSuccess'))
|
await refreshData()
|
if (detailDrawerVisible.value && activeOutStockId.value === row.id) {
|
await loadDetailResources()
|
}
|
} catch (error) {
|
if (error === 'cancel' || error?.message === 'cancel') return
|
ElMessage.error(error?.message || t('pages.orders.outStock.messages.actionFailed'))
|
}
|
}
|
|
async function handleDelete(row) {
|
try {
|
await ElMessageBox.confirm(
|
t('crud.confirm.deleteMessage', {
|
entity: t('pages.orders.outStock.entity'),
|
label: row.code || ''
|
}),
|
t('crud.confirm.deleteTitle'),
|
{
|
confirmButtonText: t('common.confirm'),
|
cancelButtonText: t('common.cancel'),
|
type: 'warning'
|
}
|
)
|
await fetchDeleteOutStock(row.id)
|
ElMessage.success(t('crud.messages.deleteSuccess'))
|
await refreshData()
|
} catch (error) {
|
if (error === 'cancel' || error?.message === 'cancel') return
|
ElMessage.error(error?.message || t('crud.messages.deleteFailed'))
|
}
|
}
|
|
async function handleActionClick(action, row) {
|
if (action?.disabled) return
|
if (action.key === 'view') {
|
openDetail(row)
|
return
|
}
|
if (action.key === 'items') {
|
router.push({
|
path: '/orders/out-stock-item',
|
query: {
|
orderId: String(row.id)
|
}
|
})
|
return
|
}
|
if (action.key === 'print') {
|
await handlePrint({ ids: [row.id], pageSize: 1 })
|
return
|
}
|
if (action.key === 'complete') {
|
await handleComplete(row)
|
return
|
}
|
if (action.key === 'cancel') {
|
await handleCancel(row)
|
return
|
}
|
if (action.key === 'delete') {
|
await handleDelete(row)
|
}
|
}
|
|
const {
|
columns,
|
columnChecks,
|
data,
|
loading,
|
pagination,
|
replaceSearchParams,
|
resetSearchParams,
|
handleSizeChange,
|
handleCurrentChange,
|
refreshData,
|
getData
|
} = useTable({
|
core: {
|
apiFn: fetchOutStockPage,
|
apiParams: buildOutStockPageQueryParams(searchForm.value),
|
columnsFactory: () => createOutStockTableColumns({ handleActionClick, t })
|
},
|
transform: {
|
dataTransformer: (records) =>
|
Array.isArray(records) ? records.map((item) => normalizeOutStockRow(item, t)) : []
|
}
|
})
|
|
const resolvePrintRecords = async (payload) => {
|
if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
|
return defaultResponseAdapter(await fetchGetOutStockMany(payload.ids)).records
|
}
|
return defaultResponseAdapter(
|
await fetchOutStockPage({
|
...reportQueryParams.value,
|
current: 1,
|
pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
|
})
|
).records
|
}
|
|
const {
|
previewVisible,
|
previewRows,
|
previewMeta: rawPreviewMeta,
|
handlePreviewVisibleChange,
|
handleExport,
|
handlePrint
|
} = usePrintExportPage({
|
downloadFileName: 'out-stock.xlsx',
|
requestExport: (payload) =>
|
fetchExportOutStockReport(payload, {
|
headers: {
|
Authorization: userStore.accessToken || ''
|
}
|
}),
|
resolvePrintRecords,
|
buildPreviewRows: (records) => buildOutStockPrintRows(records, t),
|
buildPreviewMeta: (rows) => {
|
const now = new Date()
|
return {
|
reportTitle: reportTitle.value,
|
reportDate: now.toLocaleDateString('zh-CN'),
|
printedAt: now.toLocaleString('zh-CN', { hour12: false }),
|
operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
|
count: rows.length,
|
reportStyle: { ...OUT_STOCK_REPORT_STYLE }
|
}
|
}
|
})
|
|
const resolvedPreviewMeta = computed(() =>
|
buildOutStockReportMeta({
|
previewMeta: rawPreviewMeta.value,
|
count: previewRows.value.length,
|
orientation:
|
rawPreviewMeta.value?.reportStyle?.orientation || OUT_STOCK_REPORT_STYLE.orientation,
|
t
|
})
|
)
|
|
function handleSelectionChange(rows) {
|
selectedRows.value = Array.isArray(rows) ? rows : []
|
}
|
|
function handleSearch(params) {
|
searchForm.value = { ...searchForm.value, ...params }
|
replaceSearchParams(buildOutStockPageQueryParams(searchForm.value))
|
getData()
|
}
|
|
function handleReset() {
|
searchForm.value = createOutStockSearchState()
|
resetSearchParams()
|
}
|
</script>
|