Compare commits

10 Commits

7 changed files with 375 additions and 123 deletions

Binary file not shown.

View File

@ -169,8 +169,8 @@ def get_stock_list():
if keyword: if keyword:
q = q.filter( q = q.filter(
db.or_( db.or_(
StockBuy.material_name.ilike(f'%{keyword}%'), StockBuy.base.has(MaterialBase.name.ilike(f'%{keyword}%')),
StockBuy.spec_model.ilike(f'%{keyword}%'), StockBuy.base.has(MaterialBase.spec_model.ilike(f'%{keyword}%')),
StockBuy.sku.ilike(f'%{keyword}%') StockBuy.sku.ilike(f'%{keyword}%')
) )
) )
@ -192,8 +192,8 @@ def get_stock_list():
if keyword: if keyword:
q = q.filter( q = q.filter(
db.or_( db.or_(
StockSemi.material_name.ilike(f'%{keyword}%'), StockSemi.base.has(MaterialBase.name.ilike(f'%{keyword}%')),
StockSemi.spec_model.ilike(f'%{keyword}%'), StockSemi.base.has(MaterialBase.spec_model.ilike(f'%{keyword}%')),
StockSemi.sku.ilike(f'%{keyword}%') StockSemi.sku.ilike(f'%{keyword}%')
) )
) )
@ -1167,3 +1167,154 @@ def generate_missing_stocktake():
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return jsonify({'code': 500, 'msg': f'生成漏盘数据失败: {str(e)}'}), 500 return jsonify({'code': 500, 'msg': f'生成漏盘数据失败: {str(e)}'}), 500
# --------------------------------------------------------
# 获取应盘物资清单(盘点基数)
# GET /api/v1/inbound/stock/stocktake/all-items
# --------------------------------------------------------
@bp.route('/stocktake/all-items', methods=['GET'])
@permission_required('inventory_stocktake')
def get_all_stocktake_items():
"""
获取所有应盘物资清单(库存 > 0 的物料)
作为盘点基数,用于统计已盘/未盘数量
"""
try:
keyword = request.args.get('keyword', '', type=str)
all_items = []
# 1. 采购件
buy_query = StockBuy.query.filter(StockBuy.stock_quantity > 0)
if keyword:
buy_query = buy_query.join(MaterialBase, StockBuy.base_id == MaterialBase.id).filter(
db.or_(
StockBuy.sku.ilike(f'%{keyword}%'),
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
)
for item in buy_query.all():
all_items.append({
'id': item.id,
'sku': item.sku or '',
'barcode': item.barcode or '',
'material_name': item.base.name if item.base else '',
'spec_model': item.base.spec_model if item.base else '',
'stock_qty': float(item.stock_quantity or 0),
'available_qty': float(item.available_quantity or 0),
'source_table': 'stock_buy',
'warehouse_location': item.warehouse_location or ''
})
# 2. 半成品
if StockSemi:
semi_query = StockSemi.query.filter(StockSemi.stock_quantity > 0)
if keyword:
semi_query = semi_query.join(MaterialBase, StockSemi.base_id == MaterialBase.id).filter(
db.or_(
StockSemi.sku.ilike(f'%{keyword}%'),
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
)
for item in semi_query.all():
all_items.append({
'id': item.id,
'sku': item.sku or '',
'barcode': item.barcode or '',
'material_name': item.base.name if item.base else '',
'spec_model': item.base.spec_model if item.base else '',
'stock_qty': float(item.stock_quantity or 0),
'available_qty': float(item.available_quantity or 0),
'source_table': 'stock_semi',
'warehouse_location': item.warehouse_location or ''
})
# 3. 成品
if StockProduct:
product_query = StockProduct.query.filter(StockProduct.stock_quantity > 0)
if keyword:
product_query = product_query.join(MaterialBase, StockProduct.base_id == MaterialBase.id).filter(
db.or_(
StockProduct.sku.ilike(f'%{keyword}%'),
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
)
for item in product_query.all():
all_items.append({
'id': item.id,
'sku': item.sku or '',
'barcode': item.barcode or '',
'material_name': item.base.name if item.base else '',
'spec_model': item.base.spec_model if item.base else '',
'stock_qty': float(item.stock_quantity or 0),
'available_qty': float(item.available_quantity or 0),
'source_table': 'stock_product',
'warehouse_location': item.warehouse_location or ''
})
# 按 SKU 排序
all_items.sort(key=lambda x: (x['sku'] or '').lower())
return jsonify({
'code': 200,
'data': {
'items': all_items,
'total': len(all_items)
}
}), 200
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'code': 500, 'msg': f'获取应盘清单失败: {str(e)}'}), 500
# --------------------------------------------------------
# 更新盘点实盘数(手动修改)
# POST /api/v1/inbound/stock/stocktake/update-quantity
# --------------------------------------------------------
@bp.route('/stocktake/update-quantity', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def update_stocktake_quantity():
"""
更新盘点实盘数
用于手动修改盘点数量
"""
try:
data = request.json
stock_id = data.get('stock_id')
source_table = data.get('source_table')
quantity = float(data.get('quantity', 0))
if not stock_id or not source_table:
return jsonify({'code': 400, 'msg': '缺少必要参数'}), 400
# 查找对应的盘点记录
draft = StocktakeDraft.query.filter_by(
stock_id=stock_id,
source_table=source_table
).first()
if not draft:
return jsonify({'code': 404, 'msg': '未找到盘点记录'}), 404
# 更新数量
draft.quantity = quantity
draft.scan_time = beijing_time()
# 计算差异
draft.diff_qty = quantity - float(draft.stock_qty or 0)
db.session.commit()
return jsonify({'code': 200, 'msg': '更新成功'}), 200
except Exception as e:
db.session.rollback()
import traceback
traceback.print_exc()
return jsonify({'code': 500, 'msg': f'更新失败: {str(e)}'}), 500

View File

@ -50,8 +50,9 @@ def scan_barcode():
else: else:
return jsonify({'code': 404, 'msg': '未找到对应的库存记录'}), 404 return jsonify({'code': 404, 'msg': '未找到对应的库存记录'}), 404
except Exception as e: except Exception as e:
traceback.print_exc() import traceback
return jsonify({'code': 500, 'msg': f'扫描查询出错: {str(e)}'}), 500 traceback.print_exc() # 强制在控制台打印真实错误堆栈
return jsonify({"code": 500, "msg": f"服务器内部错误详情: {str(e)}"}), 500
# -------------------------------------------------------- # --------------------------------------------------------
@ -175,21 +176,18 @@ class ScrapService:
@staticmethod @staticmethod
def _format_stock(item, table_type): def _format_stock(item, table_type):
"""格式化库存查询结果""" """格式化库存查询结果 - 使用安全 getattr 防止属性错误"""
stock_qty = float(item.stock_quantity) if item.stock_quantity else 0
avail_qty = float(item.available_quantity) if item.available_quantity else 0
return { return {
'id': item.id, 'id': getattr(item, 'id', None),
'sku': item.sku, 'sku': getattr(item, 'sku', ''),
'barcode': item.barcode, 'barcode': getattr(item, 'barcode', getattr(item, 'bar_code', '')),
'name': item.material_base.name if item.material_base else '', 'name': item.base.name if getattr(item, 'base', None) else '',
'spec': item.material_base.spec_model if item.material_base else '', 'spec': item.base.spec_model if getattr(item, 'base', None) else '',
'category': item.material_base.category if item.material_base else '', 'category': item.base.category if getattr(item, 'base', None) else '',
'material_type': item.material_base.material_type if item.material_base else '', 'material_type': item.base.material_type if getattr(item, 'base', None) else '',
'warehouse_loc': item.warehouse_loc or '', 'warehouse_loc': getattr(item, 'warehouse_location', ''),
'stock_quantity': stock_qty, 'stock_quantity': float(getattr(item, 'stock_quantity', getattr(item, 'qty_stock', 0)) or 0),
'available_quantity': avail_qty, 'available_quantity': float(getattr(item, 'available_quantity', getattr(item, 'qty_available', 0)) or 0),
'source_table': table_type, 'source_table': table_type,
} }

View File

@ -101,8 +101,8 @@ class OutboundService:
except Exception: except Exception:
pass pass
if not base_name and hasattr(item, 'material_name'): if not base_name and hasattr(item, 'base') and item.base:
base_name = item.material_name base_name = item.base.name
stock_qty = float(item.stock_quantity) if item.stock_quantity else 0 stock_qty = float(item.stock_quantity) if item.stock_quantity else 0
avail_qty = float(item.available_quantity) if item.available_quantity else 0 avail_qty = float(item.available_quantity) if item.available_quantity else 0

View File

@ -176,7 +176,7 @@ const handleLogout = () => {
<footer v-if="!isLoginPage" class="app-footer"> <footer v-if="!isLoginPage" class="app-footer">
<span class="version-tag"> <span class="version-tag">
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon> <el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
当前版本:V3.6(3.25审计导致的入库修改错误 当前版本:V3.7(3.27盘库错误修改
</span> </span>
</footer> </footer>

View File

@ -72,3 +72,21 @@ export function getBom(parentId: number) {
method: 'get' method: 'get'
}) })
} }
// 获取应盘物资清单(盘点基数)
export function getAllStocktakeItems(params?: { keyword?: string }) {
return request({
url: '/v1/inbound/stock/stocktake/all-items',
method: 'get',
params
})
}
// 更新盘点实盘数(手动修改)
export function updateStocktakeQuantity(data: { stock_id: number; source_table: string; quantity: number }) {
return request({
url: '/v1/inbound/stock/stocktake/update-quantity',
method: 'post',
data
})
}

View File

@ -79,7 +79,7 @@
</div> </div>
</div> </div>
<div class="stats-dashboard" @click="openInventoryList"> <div class="stats-dashboard" @click="openInventoryList" v-loading="listLoading">
<div class="stat-card"> <div class="stat-card">
<div class="stat-val">{{ stats.total }}</div> <div class="stat-val">{{ stats.total }}</div>
<div class="stat-label">总品项</div> <div class="stat-label">总品项</div>
@ -210,21 +210,30 @@
<el-drawer <el-drawer
v-model="showList" v-model="showList"
title="📦 盘点清单 (点击修改)" title="📦 盘点清单"
direction="btt" direction="btt"
size="100%" size="100%"
destroy-on-close destroy-on-close
class="inventory-drawer" class="inventory-drawer"
> >
<div class="drawer-layout" v-loading="listLoading"> <div class="drawer-layout" v-loading="listLoading">
<div class="search-bar"> <!-- 搜索和状态筛选 -->
<el-input v-model="listKeyword" placeholder="搜索 SKU..." :prefix-icon="Search" clearable @keyup.enter="handleListSearch" @clear="handleListSearch" style="width: 240px" /> <div class="search-bar" style="display: flex; align-items: center; gap: 10px; margin-bottom: 15px;">
<el-input v-model="listKeyword" placeholder="搜索 SKU/名称..." :prefix-icon="Search" clearable @keyup.enter="handleListSearch" @clear="handleListSearch" style="width: 240px" />
<el-button type="primary" @click="handleListSearch">搜索</el-button> <el-button type="primary" @click="handleListSearch">搜索</el-button>
<el-radio-group v-model="listStatusFilter" @change="handleListSearch" size="small">
<el-radio-button value="all">全部</el-radio-button>
<el-radio-button value="counted">已盘</el-radio-button>
<el-radio-button value="uncounted">未盘</el-radio-button>
</el-radio-group>
<span style="margin-left: auto; color: #909399; font-size: 13px;">
总品项: {{ stats.total }} | 已盘: {{ stats.scanned }} | 未盘: {{ stats.total - stats.scanned }}
</span>
</div> </div>
<div class="table-container"> <div class="table-container">
<el-table <el-table
:data="listData" :data="filteredListData"
height="100%" height="100%"
stripe stripe
border border
@ -234,16 +243,33 @@
<el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip /> <el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
<el-table-column prop="material_name" label="名称" min-width="120" show-overflow-tooltip /> <el-table-column prop="material_name" label="名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="spec_model" label="规格" width="120" show-overflow-tooltip /> <el-table-column prop="spec_model" label="规格" width="120" show-overflow-tooltip />
<el-table-column prop="stock_qty" label="账面数" width="80" align="center" /> <!-- 盲盘隐藏账面数和差异列 -->
<el-table-column prop="quantity" label="实盘数" width="80" align="center" /> <!-- <el-table-column prop="stock_qty" label="账面数" width="80" align="center" /> -->
<el-table-column label="差异" width="80" align="center"> <el-table-column label="实盘数" width="100" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.quantity"
:min="0"
:step="1"
size="small"
controls-position="right"
@change="(val) => handleQuantityChange(row, val)"
:disabled="!userStore.hasPermission('inventory_stocktake:operation')"
/>
</template>
</el-table-column>
<!-- <el-table-column label="差异" width="80" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span :style="{ color: row.diff_qty > 0 ? '#67C23A' : row.diff_qty < 0 ? '#F56C6C' : '#909399' }"> <span :style="{ color: row.diff_qty > 0 ? '#67C23A' : row.diff_qty < 0 ? '#F56C6C' : '#909399' }">
{{ row.diff_qty > 0 ? '+' : '' }}{{ row.diff_qty || 0 }} {{ row.diff_qty > 0 ? '+' : '' }}{{ row.diff_qty || 0 }}
</span> </span>
</template> </template>
</el-table-column> -->
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" size="small" @blur="handleRemarkChange(row)" />
</template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
</el-table> </el-table>
</div> </div>
@ -253,7 +279,7 @@
v-model:current-page="listPage" v-model:current-page="listPage"
v-model:page-size="listLimit" v-model:page-size="listLimit"
:page-sizes="[20, 50, 100, 200]" :page-sizes="[20, 50, 100, 200]"
:total="listTotal" :total="listTotalFiltered"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleListLimitChange" @size-change="handleListLimitChange"
@current-change="handleListPageChange" @current-change="handleListPageChange"
@ -386,7 +412,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { getStockList } from '@/api/inbound/stock' import { getStockList, getAllStocktakeItems, updateStocktakeQuantity } 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, WarningFilled } from '@element-plus/icons-vue' import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close, WarningFilled } from '@element-plus/icons-vue'
@ -435,6 +461,13 @@ const showList = ref(false)
const showFinishDialog = ref(false) const showFinishDialog = ref(false)
const showQtyDialog = ref(false) const showQtyDialog = ref(false)
// ★ 新增: 防呆确认弹窗
const showConfirmDialog = ref(false)
// ★ 新增: 盘点开始防呆倒计时
const countdown = ref(0)
let countdownTimer: any = null
// ★ 新增: 差异审核对话框 // ★ 新增: 差异审核对话框
const showVarianceDialog = ref(false) const showVarianceDialog = ref(false)
@ -448,22 +481,50 @@ const listTotal = ref(0)
const listKeyword = ref('') const listKeyword = ref('')
const listLoading = ref(false) const listLoading = ref(false)
const listData = ref<any[]>([]) const listData = ref<any[]>([])
const listStatusFilter = ref<'all' | 'counted' | 'uncounted'>('all')
// ★ 新增: 盘点开始防呆倒计时 const allStockItems = ref<any[]>([]) // 全量应盘物资(盘点基数)
const countdown = ref(0) const allScannedDrafts = ref<any[]>([]) // 全量草稿记录(脱离分页和过滤)
let countdownTimer: any = null const listTotalFiltered = ref(0) // 过滤后的总数
// ★ 新增: 防呆确认弹窗显示状态
const showConfirmDialog = ref(false)
// ★★★ 核心修改:只存储已扫码的物料列表,不再缓存全量库存 ★★★
const tableData = ref<StockItem[]>([])
const scannedMap = ref<Map<string, number>>(new Map())
const borrowedQuantities = ref<Record<string, number>>({})
// ★ 新增: 会话ID // ★ 新增: 会话ID
const currentSessionId = ref<string>('') const currentSessionId = ref<string>('')
// 获取应盘物资清单(盘点基数)
const fetchAllStockItems = async () => {
try {
const res: any = await getAllStocktakeItems()
if (res && res.code === 200) {
allStockItems.value = res.data.items || []
}
} catch (e) {
console.error('获取应盘物资清单失败', e)
}
}
// 过滤后的列表数据(直接使用已过滤的 listData
const filteredListData = computed(() => listData.value)
// 统计信息:从全量数据中计算(脱离视图依赖)
const stats = computed(() => {
const total = allStockItems.value.length
if (total === 0) return { total: 0, scanned: 0, varianceItems: 0 }
// 使用完整的 allScannedDrafts 来计算"已盘"数量,绝对不依赖视图数据
const countedItems = new Set()
allScannedDrafts.value.forEach((d: any) => {
// 只要有实盘记录就算已盘
if (d.quantity !== undefined && d.quantity !== null) {
countedItems.add(`${d.source_table}-${d.stock_id}`)
}
})
return {
total,
scanned: countedItems.size,
varianceItems: 0
}
})
const varianceLoading = ref(false) const varianceLoading = ref(false)
const currentItem = ref<StockItem | null>(null) const currentItem = ref<StockItem | null>(null)
@ -517,25 +578,6 @@ const typeToSourceTable = (type: string): string => {
} }
} }
async function fetchBorrowedQuantities(items: StockItem[]): Promise<void> {
const payload = items.filter(i => i.source_table && i.stock_id).map(i => ({
source_table: i.source_table,
stock_id: i.stock_id
}))
if (payload.length === 0) return
try {
const res = await request({
url: '/v1/inbound/stock/borrowed-quantities',
method: 'post',
data: { items: payload }
})
// res is map of key->qty
borrowedQuantities.value = { ...borrowedQuantities.value, ...res }
} catch (e) {
console.error('获取借出数量失败', e)
}
}
onMounted(async () => { onMounted(async () => {
await checkServerDraft() await checkServerDraft()
}) })
@ -580,11 +622,12 @@ const doStartNewSession = async () => {
// 调用新 API 开始新会话 // 调用新 API 开始新会话
const res: any = await api.startNewSession() const res: any = await api.startNewSession()
currentSessionId.value = res.session_id || '' currentSessionId.value = res.session_id || ''
scannedMap.value.clear()
tableData.value = [] // 清空已扫码列表
isSessionActive.value = true isSessionActive.value = true
// ★ 标记当前阶段为 scanning扫码中 // ★ 标记当前阶段为 scanning扫码中
localStorage.setItem('stocktake_phase', 'scanning') localStorage.setItem('stocktake_phase', 'scanning')
// ★ 立即加载统计基数
await fetchAllStockItems()
await fetchInventoryList()
ElMessage.success('新盘点会话已开始') ElMessage.success('新盘点会话已开始')
} catch (e: any) { } catch (e: any) {
if (e !== 'cancel') { if (e !== 'cancel') {
@ -626,21 +669,17 @@ const resumeSession = async () => {
return return
} }
// 恢复已扫描的数据 - 简化为直接激活会话
// 从草稿中获取 session_id // 从草稿中获取 session_id
const sessionIds = [...new Set(drafts.map((d: any) => d.session_id))] const sessionIds = [...new Set(drafts.map((d: any) => d.session_id))]
currentSessionId.value = sessionIds[0] currentSessionId.value = sessionIds[0]
// 恢复已扫描的数据 // 直接恢复会话状态
const map = new Map<string, number>() isSessionActive.value = true
drafts.forEach((d: any) => {
if (d.session_id === currentSessionId.value) {
map.set(d.uuid, d.quantity !== undefined ? parseFloat(d.quantity) : 1)
}
})
scannedMap.value = map
// 清空本地列表,用户扫码时会实时添加 // ★ 立即加载统计基数
tableData.value = [] await fetchAllStockItems()
await fetchInventoryList()
// ★ 智能路由:根据本地记忆的阶段决定下一步 // ★ 智能路由:根据本地记忆的阶段决定下一步
const phase = localStorage.getItem('stocktake_phase') const phase = localStorage.getItem('stocktake_phase')
@ -650,7 +689,6 @@ const resumeSession = async () => {
ElMessage.info('已恢复差异审核') ElMessage.info('已恢复差异审核')
} else { } else {
// 默认打开扫码镜头 // 默认打开扫码镜头
isSessionActive.value = true
ElMessage.success('已恢复扫码,继续盘点') ElMessage.success('已恢复扫码,继续盘点')
} }
} catch (e) { } catch (e) {
@ -730,7 +768,7 @@ const onScanSuccess = async (code: string) => {
uuid: foundItem.uuid || foundItem.sku || '', uuid: foundItem.uuid || foundItem.sku || '',
bar_code: foundItem.bar_code || foundItem.barcode || '', bar_code: foundItem.bar_code || foundItem.barcode || '',
qty_stock: stock, qty_stock: stock,
qty_actual: scannedMap.value.get(foundItem.uuid || foundItem.sku) || 0, qty_actual: 1,
scanned: true, scanned: true,
uniqueKey: `${type}_${foundItem.id}`, uniqueKey: `${type}_${foundItem.id}`,
source_table: typeToSourceTable(type), source_table: typeToSourceTable(type),
@ -804,7 +842,7 @@ const handleManualInput = async () => {
uuid: foundItem.uuid || foundItem.sku || '', uuid: foundItem.uuid || foundItem.sku || '',
bar_code: foundItem.bar_code || foundItem.barcode || '', bar_code: foundItem.bar_code || foundItem.barcode || '',
qty_stock: stock, qty_stock: stock,
qty_actual: scannedMap.value.get(foundItem.uuid || foundItem.sku) || 0, qty_actual: 1,
scanned: true, scanned: true,
uniqueKey: `${type}_${foundItem.id}`, uniqueKey: `${type}_${foundItem.id}`,
source_table: typeToSourceTable(type), source_table: typeToSourceTable(type),
@ -836,19 +874,7 @@ const handleManualConfirm = () => {
const val = inputQty.value === undefined ? 0 : inputQty.value const val = inputQty.value === undefined ? 0 : inputQty.value
const remark = inputRemark.value const remark = inputRemark.value
// ★★★ 更新已扫码物料列表 ★★★ // ★★★ 直接保存到后端,不使用本地缓存 ★★★
currentItem.value.scanned = true
currentItem.value.qty_actual = val
scannedMap.value.set(currentItem.value.uuid, val)
// 检查是否已存在于 tableData如果存在则更新否则添加
const existingIndex = tableData.value.findIndex(i => i.uuid === currentItem.value!.uuid)
if (existingIndex >= 0) {
tableData.value[existingIndex] = { ...currentItem.value }
} else {
tableData.value.push({ ...currentItem.value })
}
showQtyDialog.value = false showQtyDialog.value = false
inputRemark.value = '' inputRemark.value = ''
ElMessage.success(`已记录实盘: ${val}`) ElMessage.success(`已记录实盘: ${val}`)
@ -870,17 +896,9 @@ const syncToBackend = (uuid: string, quantity: number, remark: string) => {
} }
const updateAndSync = async (item: StockItem, quantity: number, remark: string = '') => { 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)
// 更新 tableData
const existingIndex = tableData.value.findIndex(i => i.uuid === item.uuid)
if (existingIndex >= 0) {
tableData.value[existingIndex] = { ...item }
} else {
tableData.value.push({ ...item })
}
} }
const closeOverlays = () => { const closeOverlays = () => {
@ -924,27 +942,18 @@ const exportToExcel = async () => {
} }
} }
const stats = computed(() => {
const total = tableData.value.length
const scanned = tableData.value.filter(i => i.scanned).length
const varianceItems = tableData.value.filter(i =>
!i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any))
).length
return { total, scanned, varianceItems }
})
const varianceList = computed(() => { const varianceList = computed(() => {
return tableData.value return listData.value
.filter(i => !i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any))) .filter(i => !i.quantity || i.quantity !== i.stock_quantity)
.map(i => ({ .map(i => ({
...i, ...i,
// 映射字段名以匹配模板 // 映射字段名以匹配模板
stock_name: i.name, stock_name: i.name,
stock_spec: i.standard, stock_spec: i.standard,
stock_location: i.location || i.warehouse_loc || '', stock_location: i.location || i.warehouse_loc || '',
stock_qty: i.qty_stock, stock_qty: i.stock_quantity,
quantity: i.qty_actual, quantity: i.quantity,
diff_qty: i.qty_actual - parseFloat(i.qty_stock as any) diff_qty: i.quantity - i.stock_quantity
})) }))
}) })
@ -960,23 +969,70 @@ const filteredVarianceList = computed(() => {
) )
}) })
// ★ 新增: 获取盘点清单数据(后端分页 // ★ 新增: 获取盘点清单数据(合并全量应盘物资 + 已盘点记录
const fetchInventoryList = async () => { const fetchInventoryList = async () => {
listLoading.value = true listLoading.value = true
try { try {
// 1. 获取已盘点记录
const res: any = await request({ const res: any = await request({
url: '/v1/inbound/stock/draft/list', url: '/v1/inbound/stock/draft/list',
method: 'get', method: 'get',
params: { params: {
page: listPage.value, page: 1,
limit: listLimit.value, limit: 10000, // 获取全部已盘点记录
keyword: listKeyword.value keyword: listKeyword.value
} }
}) })
if (res) {
listData.value = res.items || [] const scannedDrafts = res?.items || []
listTotal.value = res.total || 0
// 保存全量草稿记录用于全局统计
allScannedDrafts.value = scannedDrafts
// 2. 使用全量应盘物资列表
// 对于每个应盘物资,检查是否有对应的盘点记录
let mergedData = allStockItems.value.map(item => {
// 查找对应的盘点记录
const draft = scannedDrafts.find((d: any) =>
d.source_table === item.source_table && d.stock_id === item.id
)
return {
id: draft?.id || null,
stock_id: item.id,
source_table: item.source_table,
sku: item.sku,
material_name: item.material_name,
spec_model: item.spec_model,
stock_qty: item.stock_qty, // 账面数(盲盘时隐藏)
quantity: draft?.quantity || 0, // 实盘数
diff_qty: draft ? (draft.quantity - item.stock_qty) : -item.stock_qty, // 差异
remark: draft?.remark || '',
warehouse_location: item.warehouse_location
} }
})
// 3. 关键词过滤
if (listKeyword.value) {
const kw = listKeyword.value.toLowerCase()
mergedData = mergedData.filter((item: any) =>
item.sku?.toLowerCase().includes(kw) ||
item.material_name?.toLowerCase().includes(kw)
)
}
// 4. 状态过滤
if (listStatusFilter.value === 'counted') {
mergedData = mergedData.filter((item: any) => item.quantity > 0)
} else if (listStatusFilter.value === 'uncounted') {
mergedData = mergedData.filter((item: any) => !item.quantity || item.quantity === 0)
}
// 5. 分页
listTotalFiltered.value = mergedData.length
const start = (listPage.value - 1) * listLimit.value
listData.value = mergedData.slice(start, start + listLimit.value)
} catch (e) { } catch (e) {
ElMessage.error('获取盘点清单失败') ElMessage.error('获取盘点清单失败')
} finally { } finally {
@ -989,6 +1045,11 @@ const openInventoryList = async () => {
showList.value = true showList.value = true
listPage.value = 1 listPage.value = 1
listKeyword.value = '' listKeyword.value = ''
listStatusFilter.value = 'all'
// 如果基数未加载则先加载,否则只刷新已盘点记录
if (allStockItems.value.length === 0) {
await fetchAllStockItems()
}
await fetchInventoryList() await fetchInventoryList()
} }
@ -1010,6 +1071,30 @@ const handleListLimitChange = (limit: number) => {
fetchInventoryList() fetchInventoryList()
} }
// ★ 新增: 更新实盘数量(手动修改)
const handleQuantityChange = async (row: any, val: number) => {
try {
await updateStocktakeQuantity({
stock_id: row.stock_id,
source_table: row.source_table,
quantity: val
})
ElMessage.success('实盘数已更新')
} catch (e) {
console.error('更新实盘数失败', e)
ElMessage.error('更新失败')
// 重新获取列表数据
fetchInventoryList()
}
}
// ★ 新增: 更新备注
const handleRemarkChange = async (row: any) => {
// 备注更新可以批量处理或直接调用后端接口
// 这里暂时只更新本地数据,实际项目中可以调用后端保存
console.log('备注更新:', row.remark, row.id)
}
// ★ 修改:结束盘点按钮直接调用 finishStocktake跳过二次确认弹窗 // ★ 修改:结束盘点按钮直接调用 finishStocktake跳过二次确认弹窗
const openFinishDialog = () => { const openFinishDialog = () => {
if (stats.value.total === 0) { if (stats.value.total === 0) {