Compare commits

5 Commits

2 changed files with 154 additions and 73 deletions

View File

@ -165,7 +165,7 @@ def add_draft():
""" """
扫码同步 (支持更新数量) 扫码同步 (支持更新数量)
如果 session_id 不存在则创建新的会话 如果 session_id 不存在则创建新的会话
差异计算逻辑调整: 差异计算逻辑调整:
- adjusted_stock_qty = 账面总库存 - 借出未还数量 - adjusted_stock_qty = 账面总库存 - 借出未还数量
- diff_qty = 实盘数量 - adjusted_stock_qty - diff_qty = 实盘数量 - adjusted_stock_qty
@ -176,6 +176,8 @@ def add_draft():
uuid = data.get('uuid') uuid = data.get('uuid')
quantity = float(data.get('quantity', 1)) quantity = float(data.get('quantity', 1))
session_id = data.get('session_id') session_id = data.get('session_id')
# ★ 新增: 提取备注字段
remark = data.get('remark')
if not uuid: if not uuid:
return jsonify({"message": "UUID不能为空"}), 400 return jsonify({"message": "UUID不能为空"}), 400
@ -216,6 +218,9 @@ def add_draft():
draft.diff_qty = quantity - adjusted_stock_qty draft.diff_qty = quantity - adjusted_stock_qty
draft.source_table = source_table draft.source_table = source_table
draft.stock_id = stock_id draft.stock_id = stock_id
# ★ 新增: 保存备注
if remark is not None:
draft.remark = remark.strip() if isinstance(remark, str) else remark
else: else:
# 如果不存在,创建新的 # 如果不存在,创建新的
draft = StocktakeDraft( draft = StocktakeDraft(
@ -226,7 +231,9 @@ def add_draft():
stock_qty=adjusted_stock_qty, stock_qty=adjusted_stock_qty,
diff_qty=quantity - adjusted_stock_qty, diff_qty=quantity - adjusted_stock_qty,
source_table=source_table, source_table=source_table,
stock_id=stock_id stock_id=stock_id,
# ★ 新增: 保存备注
remark=remark.strip() if isinstance(remark, str) and remark else (remark if remark else None)
) )
db.session.add(draft) db.session.add(draft)

View File

@ -111,6 +111,32 @@
</div> </div>
</el-card> </el-card>
<!-- 新增: 防呆确认弹窗 -->
<el-dialog
v-model="showConfirmDialog"
title="⚠️ 确认清除盘点数据"
width="400"
:close-on-click-modal="false"
:close-on-press-escape="false"
show-close
align-center
>
<div class="confirm-content">
<el-icon :size="48" color="#E6A23C"><WarningFilled /></el-icon>
<p class="confirm-text">存在未完成记录开始新盘点将清除它们确定吗</p>
</div>
<template #footer>
<el-button @click="cancelConfirm">取消</el-button>
<el-button
type="danger"
:disabled="countdown > 0"
@click="confirmClear"
>
{{ countdown > 0 ? `确认清除 (${countdown}s)` : '确认清除' }}
</el-button>
</template>
</el-dialog>
<el-dialog <el-dialog
v-model="showQtyDialog" v-model="showQtyDialog"
title="🔢 录入实盘数量" title="🔢 录入实盘数量"
@ -155,6 +181,19 @@
/> />
<p class="unit-text">单位: {{ currentItem.unit || '个' }}</p> <p class="unit-text">单位: {{ currentItem.unit || '个' }}</p>
</div> </div>
<!-- 新增: 备注输入框 -->
<div class="remark-area" style="margin-top: 15px;">
<el-form-item label="备注:" label-width="60px">
<el-input
v-model="inputRemark"
placeholder="选填,可填写差异原因说明"
type="textarea"
:rows="2"
clearable
/>
</el-form-item>
</div>
</div> </div>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@ -194,9 +233,9 @@
<el-table-column prop="name" label="名称" min-width="90" show-overflow-tooltip /> <el-table-column prop="name" label="名称" min-width="90" show-overflow-tooltip />
<el-table-column prop="sku" label="SKU" width="110" show-overflow-tooltip /> <el-table-column prop="sku" label="SKU" width="110" show-overflow-tooltip />
<el-table-column prop="batch_no" label="批次" width="85" show-overflow-tooltip> <el-table-column label="序列号/批次" min-width="150" show-overflow-tooltip>
<template #default="scope"> <template #default="{ row }">
{{ scope.row.serial_number || scope.row.batch_no || '-' }} <span>{{ row.serial_number || row.batch_number || row.batch_no || '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
@ -266,12 +305,24 @@
style="margin-bottom: 15px;" style="margin-bottom: 15px;"
> >
<template #default> <template #default>
以下列表显示所有已结束盘点但尚未平账的差异记录逐条核实后点击"确认平账"按钮调整系统库存 以下列表显示所有已结束盘点但尚未平账的差异记录请核实后进行后续处理
</template> </template>
</el-alert> </el-alert>
<!-- 新增: 差异列表搜索 -->
<el-input
v-model="searchSku"
placeholder="输入 SKU/条码/名称快速定位"
clearable
style="width: 250px; margin-bottom: 15px;"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-table <el-table
:data="varianceList" :data="filteredVarianceList"
height="500" height="500"
border border
stripe stripe
@ -315,22 +366,9 @@
<el-tag v-else type="warning">待审核</el-tag> <el-tag v-else type="warning">待审核</el-tag>
</template> </template>
</el-table-column> </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-table>
<el-empty v-if="varianceList.length === 0" description="暂无待审核的差异记录" /> <el-empty v-if="filteredVarianceList.length === 0" :description="searchSku ? '未找到匹配的差异记录' : '暂无待审核的差异记录'" />
</div> </div>
<template #footer> <template #footer>
@ -353,7 +391,7 @@ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { getAllStock } from '@/api/inbound/stock' import { getAllStock } from '@/api/inbound/stock'
import QrScanner from '@/components/QrScanner/index.vue' import QrScanner from '@/components/QrScanner/index.vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close } from '@element-plus/icons-vue' import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close, WarningFilled } from '@element-plus/icons-vue'
import request from '@/utils/request' import request from '@/utils/request'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
@ -402,6 +440,16 @@ const showQtyDialog = ref(false)
// ★ 新增: 差异审核对话框 // ★ 新增: 差异审核对话框
const showVarianceDialog = ref(false) const showVarianceDialog = ref(false)
// ★ 新增: 差异列表搜索
const searchSku = ref('')
// ★ 新增: 盘点开始防呆倒计时
const countdown = ref(0)
let countdownTimer: any = null
// ★ 新增: 防呆确认弹窗显示状态
const showConfirmDialog = ref(false)
const allData = ref<StockItem[]>([]) const allData = ref<StockItem[]>([])
const scannedMap = ref<Map<string, number>>(new Map()) const scannedMap = ref<Map<string, number>>(new Map())
const borrowedQuantities = ref<Record<string, number>>({}) const borrowedQuantities = ref<Record<string, number>>({})
@ -416,6 +464,7 @@ const searchKeyword = ref('')
const currentItem = ref<StockItem | null>(null) const currentItem = ref<StockItem | null>(null)
const inputQty = ref<number | undefined>(undefined) const inputQty = ref<number | undefined>(undefined)
const inputRemark = ref('')
const qtyInputRef = ref() const qtyInputRef = ref()
// ★ 新增: 多人协同心跳刷新定时器 // ★ 新增: 多人协同心跳刷新定时器
@ -454,7 +503,14 @@ const syncData = async () => {
if (res.materials) res.materials.forEach((i: any) => { const item = processItem(i, 'material'); if (item) list.push(item) }) if (res.materials) res.materials.forEach((i: any) => { const item = processItem(i, 'material'); if (item) list.push(item) })
if (res.semis) res.semis.forEach((i: any) => { const item = processItem(i, 'semi'); if (item) list.push(item) }) if (res.semis) res.semis.forEach((i: any) => { const item = processItem(i, 'semi'); if (item) list.push(item) })
if (res.products) res.products.forEach((i: any) => { const item = processItem(i, 'product'); if (item) list.push(item) }) if (res.products) res.products.forEach((i: any) => { const item = processItem(i, 'product'); if (item) list.push(item) })
// ★ 强制按 SKU 数字+字符串升序排序
list.sort((a, b) => {
const skuA = a.sku || '';
const skuB = b.sku || '';
return skuA.localeCompare(skuB, undefined, { numeric: true, sensitivity: 'base' });
});
// 静默更新数据不触发loading // 静默更新数据不触发loading
allData.value = list allData.value = list
await fetchBorrowedQuantities(list) await fetchBorrowedQuantities(list)
@ -492,19 +548,6 @@ const api = {
method: 'get', method: 'get',
params: {} params: {}
}), }),
// ★ 新增: 单条库存调整
adjustStock: (draftId: number, stockId: number, diffQty: number, sourceTable: string, remark: string) => request({
url: '/v1/inbound/stock/adjust',
method: 'post',
data: {
draft_id: draftId,
stock_id: stockId, // 库存项ID
diff_qty: diffQty, // 差异数量(支持无草稿模式)
source_table: sourceTable, // 必须stock_buy / stock_semi / stock_product
operator_name: currentUser,
remark: remark
}
}),
// ★ 保留清除功能(用于兼容性) // ★ 保留清除功能(用于兼容性)
clearDraft: () => request({ clearDraft: () => request({
url: '/v1/inbound/stock/draft/clear', url: '/v1/inbound/stock/draft/clear',
@ -571,11 +614,27 @@ const checkServerDraft = async () => {
// ★ 重写: 开始新盘点 - 使用新 API // ★ 重写: 开始新盘点 - 使用新 API
const startNewSession = async () => { const startNewSession = async () => {
// ★ 新增: 防呆确认弹窗
if (serverDraftCount.value > 0) {
showConfirmDialog.value = true
countdown.value = 10
if (countdownTimer) clearInterval(countdownTimer)
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(countdownTimer!)
countdownTimer = null
}
}, 1000)
return
}
await doStartNewSession()
}
const doStartNewSession = async () => {
showConfirmDialog.value = false
btnLoading.value = true
try { try {
if (serverDraftCount.value > 0) {
await ElMessageBox.confirm('存在未完成记录,开始新盘点将清除它们,确定吗?', '警告', { type: 'warning' })
}
btnLoading.value = true
// 调用新 API 开始新会话 // 调用新 API 开始新会话
const res: any = await api.startNewSession() const res: any = await api.startNewSession()
currentSessionId.value = res.session_id || '' currentSessionId.value = res.session_id || ''
@ -592,6 +651,20 @@ const startNewSession = async () => {
} finally { btnLoading.value = false } } finally { btnLoading.value = false }
} }
const confirmClear = () => {
if (countdown.value > 0) return
doStartNewSession()
}
const cancelConfirm = () => {
showConfirmDialog.value = false
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
countdown.value = 0
}
// ★ 重写: 继续上次盘点 - 恢复扫码作业 // ★ 重写: 继续上次盘点 - 恢复扫码作业
const resumeSession = async () => { const resumeSession = async () => {
btnLoading.value = true btnLoading.value = true
@ -693,6 +766,13 @@ const loadData = async () => {
if (res.semis) res.semis.forEach((i: any) => processItem(i, 'semi')) if (res.semis) res.semis.forEach((i: any) => processItem(i, 'semi'))
if (res.products) res.products.forEach((i: any) => processItem(i, 'product')) if (res.products) res.products.forEach((i: any) => processItem(i, 'product'))
// ★ 强制按 SKU 数字+字符串升序排序
list.sort((a, b) => {
const skuA = a.sku || '';
const skuB = b.sku || '';
return skuA.localeCompare(skuB, undefined, { numeric: true, sensitivity: 'base' });
});
allData.value = list allData.value = list
await fetchBorrowedQuantities(list) await fetchBorrowedQuantities(list)
} catch (e) { } catch (e) {
@ -760,7 +840,6 @@ const handleManualInput = async () => {
ElMessage.error(`不在库条码: ${code}`) ElMessage.error(`不在库条码: ${code}`)
} }
} finally { } finally {
barcodeInput.value = ''
loading.value = false loading.value = false
} }
} }
@ -780,8 +859,10 @@ const handleManualConfirm = () => {
if (!currentItem.value) return if (!currentItem.value) return
const val = inputQty.value === undefined ? 0 : inputQty.value const val = inputQty.value === undefined ? 0 : inputQty.value
updateAndSync(currentItem.value, val) updateAndSync(currentItem.value, val, inputRemark.value)
showQtyDialog.value = false showQtyDialog.value = false
// 重置备注
inputRemark.value = ''
ElMessage.success(`已记录实盘: ${val}`) ElMessage.success(`已记录实盘: ${val}`)
// ★★★ 核心修改:确认数量后自动重新打开全屏扫码,实现无缝闭环 // ★★★ 核心修改:确认数量后自动重新打开全屏扫码,实现无缝闭环
@ -792,14 +873,14 @@ const handleManualConfirm = () => {
}) })
} }
const updateAndSync = async (item: StockItem, quantity: number) => { const updateAndSync = async (item: StockItem, quantity: number, remark: string = '') => {
item.scanned = true item.scanned = true
item.qty_actual = quantity item.qty_actual = quantity
scannedMap.value.set(item.uuid, quantity) scannedMap.value.set(item.uuid, quantity)
syncStatus.value = 'syncing' syncStatus.value = 'syncing'
try { try {
await api.addDraft({ uuid: item.uuid, quantity: quantity }) await api.addDraft({ uuid: item.uuid, quantity: quantity, remark: remark })
syncStatus.value = 'success' syncStatus.value = 'success'
} catch (e) { } catch (e) {
syncStatus.value = 'failed' syncStatus.value = 'failed'
@ -888,6 +969,18 @@ const varianceList = computed(() => {
})) }))
}) })
// ★ 新增: 本地搜索过滤后的差异列表
const filteredVarianceList = computed(() => {
if (!searchSku.value) return varianceList.value
const kw = searchSku.value.toLowerCase()
return varianceList.value.filter(i =>
(i.uuid && i.uuid.toLowerCase().includes(kw)) ||
(i.sku && i.sku.toLowerCase().includes(kw)) ||
(i.stock_name && i.stock_name.toLowerCase().includes(kw)) ||
(i.stock_spec && i.stock_spec.toLowerCase().includes(kw))
)
})
const openInventoryList = () => { showList.value = true } const openInventoryList = () => { showList.value = true }
// ★ 修改:结束盘点按钮直接调用 finishStocktake跳过二次确认弹窗 // ★ 修改:结束盘点按钮直接调用 finishStocktake跳过二次确认弹窗
@ -946,35 +1039,6 @@ const openVarianceDialog = async () => {
varianceLoading.value = false varianceLoading.value = false
} }
// ★ 新增: 确认平账
const handleAdjust = async (row: any) => {
try {
// ===== 调试代码 =====
console.warn('---- 准备平账参数检查 ----');
console.warn('当前点击行的完整数据:', row);
console.warn(`将要发送的 draftId: ${row.id}, stockId: ${row.stock_id}, sourceTable: ${row.source_table}`);
// ===== 调试结束 =====
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, row.stock_id, row.diff_qty, row.source_table || 'stock_buy', remark)
ElMessage.success(res.message || '调整成功')
// 刷新数据并重新打开差异列表
await loadData()
await openVarianceDialog()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
// ★ 新增: 跳转到差异审核页面 // ★ 新增: 跳转到差异审核页面
const goToVarianceReview = () => { const goToVarianceReview = () => {
openVarianceDialog() openVarianceDialog()
@ -1239,4 +1303,14 @@ const goToVarianceReview = () => {
width: 100%; width: 100%;
} }
} }
.confirm-content {
text-align: center;
padding: 20px 0;
}
.confirm-content .confirm-text {
margin-top: 20px;
font-size: 16px;
color: #303133;
}
</style> </style>