refactor: redesign stocktake flow to require manual discrepancy audit and individual adjustments

This commit is contained in:
DXC
2026-03-13 09:59:01 +08:00
parent 9b290506da
commit 7e23141870
3 changed files with 654 additions and 56 deletions

View File

@ -25,6 +25,17 @@
>
继续上次盘点 <span class="sub-text">({{ serverDraftCount }})</span>
</el-button>
<!-- 新增: 查看差异报告按钮 -->
<el-button
type="info"
plain
size="large"
class="action-btn-full"
@click="goToVarianceReview"
>
📋 差异审核 <span class="sub-text">(查看历史差异)</span>
</el-button>
</div>
<div class="safe-tip">
@ -247,37 +258,96 @@
</div>
</div>
<el-dialog v-model="showFinishDialog" title="📊 盘点结算" width="90%" align-center :close-on-click-modal="false" class="preview-dialog">
<div class="report-summary">
<div class="summary-row"><span>截止时间:</span><span>{{ new Date().toLocaleString() }}</span></div>
<div class="summary-stats">
<div class="s-item"><div class="num">{{ stats.total }}</div><div class="txt">总数</div></div>
<div class="s-item success"><div class="num">{{ stats.scanned }}</div><div class="txt">已盘</div></div>
<div class="s-item error"><div class="num">{{ stats.total - stats.scanned }}</div><div class="txt">未盘</div></div>
</div>
</div>
<div class="missing-list-header">差异/未盘预览</div>
<el-table :data="varianceList" height="300" border size="small" style="margin-bottom: 10px;">
<el-table-column prop="name" label="名称" show-overflow-tooltip />
<el-table-column prop="batch_no" label="批次" width="90" show-overflow-tooltip />
<el-table-column label="账/实" width="80" align="center">
<template #default="scope">
{{ parseFloat(scope.row.qty_stock) }} /
<span :class="{'text-red': scope.row.scanned && scope.row.qty_actual !== scope.row.qty_stock}">
{{ scope.row.scanned ? scope.row.qty_actual : '未' }}
</span>
<!-- 新增: 盘点差异审核对话框 -->
<el-dialog
v-model="showVarianceDialog"
title="📋 盘点差异审核"
width="95%"
align-center
:close-on-click-modal="false"
destroy-on-close
class="variance-dialog"
>
<div v-loading="varianceLoading">
<el-alert
title="差异审核说明"
type="info"
:closable="false"
show-icon
style="margin-bottom: 15px;"
>
<template #default>
以下列表显示所有已结束盘点但尚未平账的差异记录请逐条核实后点击"确认平账"按钮调整系统库存
</template>
</el-table-column>
</el-table>
</el-alert>
<el-table
:data="varianceList"
height="500"
border
stripe
style="width: 100%"
>
<el-table-column prop="uuid" label="SKU/条码" width="140" show-overflow-tooltip />
<el-table-column prop="stock_name" label="物品名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="stock_spec" label="规格型号" width="120" show-overflow-tooltip />
<el-table-column prop="stock_location" label="库位" width="100" />
<el-table-column label="账面库存" width="80" align="center">
<template #default="scope">
{{ scope.row.stock_qty }}
</template>
</el-table-column>
<el-table-column label="实盘数量" width="80" align="center">
<template #default="scope">
{{ scope.row.quantity }}
</template>
</el-table-column>
<el-table-column label="差异" width="100" align="center">
<template #default="scope">
<el-tag
:type="scope.row.diff_qty > 0 ? 'success' : 'danger'"
size="large"
effect="dark"
>
{{ scope.row.diff_qty > 0 ? '+' : '' }}{{ scope.row.diff_qty }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="差异类型" width="80" align="center">
<template #default="scope">
<el-tag :type="scope.row.diff_qty > 0 ? 'success' : 'danger'">
{{ scope.row.diff_qty > 0 ? '盘盈' : '盘亏' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template #default="scope">
<el-tag v-if="scope.row.is_processed" type="info">已处理</el-tag>
<el-tag v-else type="warning">待审核</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<el-button
v-if="!scope.row.is_processed && userStore.hasPermission('inventory_stocktake:operation')"
type="primary"
size="small"
@click="handleAdjust(scope.row)"
>
确认平账
</el-button>
<span v-else class="text-gray">-</span>
</template>
</el-table-column>
</el-table>
<el-empty v-if="varianceList.length === 0" description="暂无待审核的差异记录" />
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="showFinishDialog = false">返回修改</el-button>
<div class="footer-right">
<el-button type="success" @click="exportToExcel" :icon="Download">导出Excel</el-button>
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="danger" @click="finishStocktake" :loading="printing" :icon="Checked">结束</el-button>
</div>
<el-button @click="showVarianceDialog = false">关闭</el-button>
<el-button type="success" @click="exportToExcel" :icon="Download">导出差异报告</el-button>
</div>
</template>
</el-dialog>
@ -337,10 +407,20 @@ const showList = ref(false)
const showFinishDialog = ref(false)
const showQtyDialog = ref(false)
// ★ 新增: 差异审核对话框
const showVarianceDialog = ref(false)
const allData = ref<StockItem[]>([])
const scannedMap = ref<Map<string, number>>(new Map())
const borrowedQuantities = ref<Record<string, number>>({})
// ★ 新增: 会话ID
const currentSessionId = ref<string>('')
// ★ 新增: 差异报告列表
const varianceList = ref<any[]>([])
const varianceLoading = ref(false)
const filterType = ref('all')
const searchKeyword = ref('')
@ -349,9 +429,46 @@ const inputQty = ref<number | undefined>(undefined)
const qtyInputRef = ref()
const api = {
getDrafts: () => request({ url: '/v1/inbound/stock/draft/list', method: 'get', params: { user_id: currentUser } }),
addDraft: (data: any) => request({ url: '/v1/inbound/stock/draft/add', method: 'post', data: { ...data, user_id: currentUser } }),
clearDraft: () => request({ url: '/v1/inbound/stock/draft/clear', method: 'post', data: { user_id: currentUser } })
getDrafts: (sessionId?: string) => request({
url: '/v1/inbound/stock/draft/list',
method: 'get',
params: { user_id: currentUser, session_id: sessionId }
}),
addDraft: (data: any) => request({
url: '/v1/inbound/stock/draft/add',
method: 'post',
data: { ...data, user_id: currentUser, session_id: currentSessionId.value }
}),
// ★ 新增: 开始新会话
startNewSession: () => request({
url: '/v1/inbound/stock/draft/start-new',
method: 'post',
data: { user_id: currentUser }
}),
// ★ 新增: 结束盘点
finishStocktake: () => request({
url: '/v1/inbound/stock/finish',
method: 'post',
data: { user_id: currentUser, session_id: currentSessionId.value }
}),
// ★ 新增: 获取差异报告
getVarianceReport: () => request({
url: '/v1/inbound/stock/variance-report',
method: 'get',
params: { user_id: currentUser }
}),
// ★ 新增: 单条库存调整
adjustStock: (draftId: number, remark: string) => request({
url: '/v1/inbound/stock/adjust',
method: 'post',
data: { draft_id: draftId, operator_name: currentUser, remark: remark }
}),
// ★ 保留清除功能(用于兼容性)
clearDraft: () => request({
url: '/v1/inbound/stock/draft/clear',
method: 'post',
data: { user_id: currentUser }
})
}
const typeToSourceTable = (type: string): string => {
@ -388,31 +505,62 @@ onMounted(async () => {
const checkServerDraft = async () => {
try {
const res: any = await api.getDrafts()
// 只获取未完成的草稿数量
const res: any = await request({
url: '/v1/inbound/stock/draft/list',
method: 'get',
params: { user_id: currentUser, is_finished: 'false' }
})
serverDraftCount.value = (res && res.length) || 0
} catch (e) {}
}
// ★ 重写: 开始新盘点 - 使用新 API
const startNewSession = async () => {
try {
if (serverDraftCount.value > 0) {
await ElMessageBox.confirm('存在未完成记录,开始新盘点将清除它们,确定吗?', '警告', { type: 'warning' })
}
btnLoading.value = true
await api.clearDraft()
// 调用新 API 开始新会话
const res: any = await api.startNewSession()
currentSessionId.value = res.session_id || ''
scannedMap.value.clear()
await loadData()
isSessionActive.value = true
} catch (e) {} finally { btnLoading.value = false }
ElMessage.success('新盘点会话已开始')
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e?.message || '操作失败')
}
} finally { btnLoading.value = false }
}
// ★ 重写: 继续上次盘点
const resumeSession = async () => {
btnLoading.value = true
try {
const drafts: any = await api.getDrafts()
// 获取最新的未完成会话
const drafts: any = await request({
url: '/v1/inbound/stock/draft/list',
method: 'get',
params: { user_id: currentUser, is_finished: 'false' }
})
if (!drafts || drafts.length === 0) {
ElMessage.warning('没有找到未完成的盘点记录')
return
}
// 从草稿中获取 session_id
const sessionIds = [...new Set(drafts.map((d: any) => d.session_id))]
currentSessionId.value = sessionIds[0]
const map = new Map<string, number>()
drafts.forEach((d: any) => {
map.set(d.uuid, d.quantity !== undefined ? parseFloat(d.quantity) : 1)
if (d.session_id === currentSessionId.value) {
map.set(d.uuid, d.quantity !== undefined ? parseFloat(d.quantity) : 1)
}
})
scannedMap.value = map
await loadData()
@ -695,22 +843,71 @@ const openFinishDialog = () => {
const finishStocktake = async () => {
try {
await ElMessageBox.confirm('确定要结束本次盘点吗?结束后当前进度将清空。', '结束确认', {
await ElMessageBox.confirm('确定要结束本次盘点吗?结束后将进入差异审核流程。', '结束确认', {
type: 'warning', confirmButtonText: '确定结束', cancelButtonText: '取消'
})
printing.value = true
await api.clearDraft()
// ★ 修改: 调用结束盘点 API不再删除草稿
const res: any = await api.finishStocktake()
// 结束会话
scannedMap.value.clear()
isSessionActive.value = false
showFinishDialog.value = false
checkServerDraft()
ElMessage.success('盘点已完成,会话已结束')
} catch (e) {
if (e !== 'cancel') ElMessage.error('操作失败')
ElMessage.success('盘点已结束,请查看差异报告进行审核')
// ★ 新增: 自动打开差异审核对话框
await openVarianceDialog()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
} finally { printing.value = false }
}
// ★ 新增: 打开差异审核对话框
const openVarianceDialog = async () => {
varianceLoading.value = true
showVarianceDialog.value = true
try {
const res: any = await api.getVarianceReport()
varianceList.value = res.list || []
} catch (e: any) {
ElMessage.error(e?.message || '获取差异报告失败')
} finally {
varianceLoading.value = false
}
}
// ★ 新增: 确认平账
const handleAdjust = async (row: any) => {
try {
await ElMessageBox.confirm(
`确定要对 "${row.uuid}" 进行平账调整吗?\n\n差异: ${row.diff_qty > 0 ? '盘盈 +' : '盘亏 '}${row.diff_qty}`,
'确认平账',
{ type: 'warning', confirmButtonText: '确认调整', cancelButtonText: '取消' }
)
const remark = `盘点差异调整 - ${row.diff_qty > 0 ? '盘盈入库' : '盘亏出库'}`
const res: any = await api.adjustStock(row.id, remark)
ElMessage.success(res.message || '调整成功')
// 刷新差异列表
await openVarianceDialog()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
// ★ 新增: 跳转到差异审核页面
const goToVarianceReview = () => {
openVarianceDialog()
}
</script>
<style scoped>