Compare commits
10 Commits
46dd8f1c3a
...
b43edc9068
| Author | SHA1 | Date | |
|---|---|---|---|
| b43edc9068 | |||
| 8259a74c57 | |||
| dc42335ae0 | |||
| f6b055d9c4 | |||
| 8135a222f0 | |||
| edb49468a0 | |||
| 2a27f2e0df | |||
| db0444cc13 | |||
| c8810891d8 | |||
| d58b002340 |
BIN
deploy.tar.gz
BIN
deploy.tar.gz
Binary file not shown.
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user