Compare commits
8 Commits
d5d12a8df5
...
46dd8f1c3a
| Author | SHA1 | Date | |
|---|---|---|---|
| 46dd8f1c3a | |||
| f9edb5f1f7 | |||
| 7421ef3231 | |||
| ac7774e0e3 | |||
| a8119dd577 | |||
| 663a4fc9ae | |||
| 57bdb6273b | |||
| bd20852739 |
BIN
deploy.tar.gz
BIN
deploy.tar.gz
Binary file not shown.
0
deploy_code.sh
Executable file → Normal file
0
deploy_code.sh
Executable file → Normal file
0
deploy_full.sh
Executable file → Normal file
0
deploy_full.sh
Executable file → Normal file
@ -309,8 +309,13 @@ def delete_buy(id):
|
|||||||
try:
|
try:
|
||||||
material_name = BuyInboundService.delete_inbound(id)
|
material_name = BuyInboundService.delete_inbound(id)
|
||||||
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
|
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
|
||||||
|
except ValueError as ve:
|
||||||
|
# 捕获业务拦截的报错,返回友好的 msg
|
||||||
|
return jsonify({"code": 400, "msg": str(ve)})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
import traceback
|
||||||
|
traceback.print_exc() # 在控制台打印真实错误堆栈
|
||||||
|
return jsonify({"code": 500, "msg": f"服务器内部错误详情: {str(e)}"}), 500
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@ -185,9 +185,12 @@ def delete(id):
|
|||||||
try:
|
try:
|
||||||
material_name = ProductInboundService.delete_inbound(id)
|
material_name = ProductInboundService.delete_inbound(id)
|
||||||
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
|
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
|
||||||
|
except ValueError as ve:
|
||||||
|
return jsonify({"code": 400, "msg": str(ve)})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": f"服务器内部错误详情: {str(e)}"}), 500
|
||||||
|
|
||||||
@inbound_product_bp.route('/<int:id>/history', methods=['GET'])
|
@inbound_product_bp.route('/<int:id>/history', methods=['GET'])
|
||||||
@permission_required('inbound_product')
|
@permission_required('inbound_product')
|
||||||
|
|||||||
@ -180,9 +180,12 @@ def delete_semi(id):
|
|||||||
try:
|
try:
|
||||||
material_name = SemiInboundService.delete_inbound(id)
|
material_name = SemiInboundService.delete_inbound(id)
|
||||||
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
|
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
|
||||||
|
except ValueError as ve:
|
||||||
|
return jsonify({"code": 400, "msg": str(ve)})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": f"服务器内部错误详情: {str(e)}"}), 500
|
||||||
|
|
||||||
@inbound_semi_bp.route('/<int:id>/history', methods=['GET'])
|
@inbound_semi_bp.route('/<int:id>/history', methods=['GET'])
|
||||||
@permission_required('inbound_semi')
|
@permission_required('inbound_semi')
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class SysUser(db.Model):
|
|||||||
前端需要的是 '张三(zhangsan)'
|
前端需要的是 '张三(zhangsan)'
|
||||||
"""
|
"""
|
||||||
raw_name = self.username
|
raw_name = self.username
|
||||||
display_name = raw_name
|
real_name = ''
|
||||||
account_id = raw_name
|
account_id = raw_name
|
||||||
|
|
||||||
# 解析存储格式: Name/ID
|
# 解析存储格式: Name/ID
|
||||||
@ -51,11 +51,15 @@ class SysUser(db.Model):
|
|||||||
display_name = f"{real_name}({acc_id})"
|
display_name = f"{real_name}({acc_id})"
|
||||||
# 单独提取账号ID (如果前端需要单独用)
|
# 单独提取账号ID (如果前端需要单独用)
|
||||||
account_id = acc_id
|
account_id = acc_id
|
||||||
|
else:
|
||||||
|
display_name = raw_name
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'username': display_name, # 列表显示: 张三(zhangsan01)
|
'username': display_name, # 列表显示: 张三(zhangsan01)
|
||||||
'raw_username': self.username, # 原始数据
|
'raw_username': self.username, # 原始数据
|
||||||
|
'real_name': real_name, # 真实姓名: 张三
|
||||||
|
'display_name': display_name, # 显示名: 张三(zhangsan01)
|
||||||
'account_id': account_id, # 纯账号ID: zhangsan01
|
'account_id': account_id, # 纯账号ID: zhangsan01
|
||||||
'email': self.email,
|
'email': self.email,
|
||||||
'department': self.department,
|
'department': self.department,
|
||||||
|
|||||||
@ -124,7 +124,8 @@ class AuthService:
|
|||||||
identity=user_id,
|
identity=user_id,
|
||||||
additional_claims={
|
additional_claims={
|
||||||
'role': user_role,
|
'role': user_role,
|
||||||
'username': account_id
|
'username': account_id,
|
||||||
|
'display_name': user_info.get('display_name', account_id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -153,11 +154,19 @@ class AuthService:
|
|||||||
user_id = decoded.get('sub')
|
user_id = decoded.get('sub')
|
||||||
role = decoded.get('role')
|
role = decoded.get('role')
|
||||||
username = decoded.get('username')
|
username = decoded.get('username')
|
||||||
display_name = decoded.get('display_name')
|
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
raise ValueError("无效的 refresh_token")
|
raise ValueError("无效的 refresh_token")
|
||||||
|
|
||||||
|
# 重新查询数据库获取用户的 display_name,避免刷新后丢失
|
||||||
|
from app.models.system import SysUser
|
||||||
|
user = SysUser.query.get(user_id)
|
||||||
|
if user:
|
||||||
|
user_info = user.to_dict()
|
||||||
|
display_name = user_info.get('display_name', username)
|
||||||
|
else:
|
||||||
|
display_name = username
|
||||||
|
|
||||||
# 生成新的 access_token
|
# 生成新的 access_token
|
||||||
new_access_token = create_access_token(
|
new_access_token = create_access_token(
|
||||||
identity=user_id,
|
identity=user_id,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from app.models.inbound.buy import StockBuy
|
|||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from sqlalchemy import or_, func, text, and_
|
from sqlalchemy import or_, func, text, and_
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -280,11 +281,14 @@ class BuyInboundService:
|
|||||||
try:
|
try:
|
||||||
stock = StockBuy.query.get(stock_id)
|
stock = StockBuy.query.get(stock_id)
|
||||||
if not stock: raise ValueError("记录不存在")
|
if not stock: raise ValueError("记录不存在")
|
||||||
# 提前获取物料名称用于审计日志
|
# 提前获取物料名称用于审计日志(通过外键关系 base.name 获取)
|
||||||
material_name = stock.material_name
|
material_name = stock.base.name if stock.base else '未知物料'
|
||||||
db.session.delete(stock)
|
db.session.delete(stock)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return material_name
|
return material_name
|
||||||
|
except IntegrityError:
|
||||||
|
db.session.rollback()
|
||||||
|
raise ValueError("该入库单已被出库、盘点或借还等业务关联,为保证账目完整,禁止删除!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from app.models.base import MaterialBase
|
|||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from sqlalchemy import or_, func, text, and_
|
from sqlalchemy import or_, func, text, and_
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -252,12 +253,15 @@ class ProductInboundService:
|
|||||||
try:
|
try:
|
||||||
stock = StockProduct.query.get(stock_id)
|
stock = StockProduct.query.get(stock_id)
|
||||||
if stock:
|
if stock:
|
||||||
# 提前获取物料名称用于审计日志
|
# 提前获取物料名称用于审计日志(通过外键关系 base.name 获取)
|
||||||
material_name = stock.material_name
|
material_name = stock.base.name if stock.base else '未知物料'
|
||||||
db.session.delete(stock)
|
db.session.delete(stock)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return material_name
|
return material_name
|
||||||
return None
|
return None
|
||||||
|
except IntegrityError:
|
||||||
|
db.session.rollback()
|
||||||
|
raise ValueError("该入库单已被出库、盘点或借还等业务关联,为保证账目完整,禁止删除!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from app.models.base import MaterialBase
|
|||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from sqlalchemy import or_, func, text, and_
|
from sqlalchemy import or_, func, text, and_
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -341,11 +342,14 @@ class SemiInboundService:
|
|||||||
stock = StockSemi.query.get(stock_id)
|
stock = StockSemi.query.get(stock_id)
|
||||||
if not stock:
|
if not stock:
|
||||||
raise ValueError("记录不存在")
|
raise ValueError("记录不存在")
|
||||||
# 提前获取物料名称用于审计日志
|
# 提前获取物料名称用于审计日志(通过外键关系 base.name 获取)
|
||||||
material_name = stock.material_name
|
material_name = stock.base.name if stock.base else '未知物料'
|
||||||
db.session.delete(stock)
|
db.session.delete(stock)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return material_name
|
return material_name
|
||||||
|
except IntegrityError:
|
||||||
|
db.session.rollback()
|
||||||
|
raise ValueError("该入库单已被出库、盘点或借还等业务关联,为保证账目完整,禁止删除!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from app.models.system import SysMenu, SysElement, SysRolePermission
|
from app.models.system import SysMenu, SysElement, SysRolePermission
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
|
||||||
class PermissionService:
|
class PermissionService:
|
||||||
|
|||||||
@ -205,6 +205,17 @@ def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target
|
|||||||
username = claims.get('username', '')
|
username = claims.get('username', '')
|
||||||
display_name = claims.get('display_name', '')
|
display_name = claims.get('display_name', '')
|
||||||
|
|
||||||
|
# 兜底:如果 display_name 为空,查询数据库获取
|
||||||
|
if not display_name and user_id:
|
||||||
|
try:
|
||||||
|
from app.models.system import SysUser
|
||||||
|
user = SysUser.query.get(user_id)
|
||||||
|
if user:
|
||||||
|
user_info = user.to_dict()
|
||||||
|
display_name = user_info.get('display_name', username)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 获取IP
|
# 获取IP
|
||||||
ip_address = request.headers.get('X-Forwarded-For') or request.remote_addr or ''
|
ip_address = request.headers.get('X-Forwarded-For') or request.remote_addr or ''
|
||||||
if ip_address and ',' in ip_address:
|
if ip_address and ',' in ip_address:
|
||||||
|
|||||||
@ -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.5(3.24BOM表修改版)
|
当前版本:V3.6(3.25审计导致的入库修改错误)
|
||||||
</span>
|
</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div v-if="userStore.hasPermission('scrap_list:view')" class="app-container">
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-input v-model="sku" placeholder="SKU" style="width: 200px" @keyup.enter="fetchData" />
|
<el-input v-model="sku" placeholder="SKU" style="width: 200px" @keyup.enter="fetchData" />
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
@ -66,6 +66,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { getScrapRecords } from '@/api/scrap'
|
import { getScrapRecords } from '@/api/scrap'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const list = ref<any[]>([])
|
const list = ref<any[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div v-if="userStore.hasPermission('outbound_list:view')" class="app-container">
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="listQuery.keyword"
|
v-model="listQuery.keyword"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div v-if="userStore.hasPermission('stock_adjustment:view')" class="app-container">
|
||||||
<!-- 筛选条件 -->
|
<!-- 筛选条件 -->
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-input v-model="keyword" placeholder="搜索单号/SKU/物料名称" style="width: 220px" @keyup.enter="fetchData" clearable />
|
<el-input v-model="keyword" placeholder="搜索单号/SKU/物料名称" style="width: 220px" @keyup.enter="fetchData" clearable />
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div v-if="userStore.hasPermission('inbound_summary:view')" class="app-container">
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="listQuery.keyword"
|
v-model="listQuery.keyword"
|
||||||
@ -101,6 +101,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { getInboundSummaryList } from '@/api/inbound/inbound_summary'
|
import { getInboundSummaryList } from '@/api/inbound/inbound_summary'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|||||||
@ -386,7 +386,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 { getAllStock } from '@/api/inbound/stock'
|
import { getStockList } 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'
|
||||||
@ -456,7 +456,8 @@ let countdownTimer: any = null
|
|||||||
// ★ 新增: 防呆确认弹窗显示状态
|
// ★ 新增: 防呆确认弹窗显示状态
|
||||||
const showConfirmDialog = ref(false)
|
const showConfirmDialog = ref(false)
|
||||||
|
|
||||||
const allData = ref<StockItem[]>([])
|
// ★★★ 核心修改:只存储已扫码的物料列表,不再缓存全量库存 ★★★
|
||||||
|
const tableData = 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>>({})
|
||||||
|
|
||||||
@ -465,63 +466,11 @@ const currentSessionId = ref<string>('')
|
|||||||
|
|
||||||
const varianceLoading = ref(false)
|
const varianceLoading = ref(false)
|
||||||
|
|
||||||
const filterType = ref('all')
|
|
||||||
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 inputRemark = ref('')
|
||||||
const qtyInputRef = ref()
|
const qtyInputRef = ref()
|
||||||
|
|
||||||
// ★ 新增: 静默刷新数据(不弹loading)
|
|
||||||
const syncData = async () => {
|
|
||||||
try {
|
|
||||||
// 仅刷新差异列表数据,不显示loading
|
|
||||||
const res = await getAllStock()
|
|
||||||
if (!res) return
|
|
||||||
|
|
||||||
const processItem = (item: any, type: string) => {
|
|
||||||
const stock = parseFloat(item.stock_quantity || item.qty_stock || 0)
|
|
||||||
if (stock <= 0) return
|
|
||||||
const name = item.material_name || item.product_name || item.name || '未知物品'
|
|
||||||
const uuid = item.uuid || item.sku || ''
|
|
||||||
const isScanned = scannedMap.value.has(uuid)
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
name: name,
|
|
||||||
standard: item.spec_model || item.standard || item.model || '',
|
|
||||||
sku: item.sku || '',
|
|
||||||
uuid: uuid,
|
|
||||||
bar_code: item.bar_code || item.barcode || '',
|
|
||||||
qty_stock: stock,
|
|
||||||
qty_actual: isScanned ? scannedMap.value.get(uuid)! : 0,
|
|
||||||
scanned: isScanned,
|
|
||||||
uniqueKey: `${type}_${item.id}`,
|
|
||||||
source_table: typeToSourceTable(type),
|
|
||||||
stock_id: item.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const list: StockItem[] = []
|
|
||||||
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.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)
|
|
||||||
allData.value = list
|
|
||||||
await fetchBorrowedQuantities(list)
|
|
||||||
} catch (e) {
|
|
||||||
// 静默失败,不弹错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
getDrafts: (sessionId?: string) => request({
|
getDrafts: (sessionId?: string) => request({
|
||||||
url: '/v1/inbound/stock/draft/list',
|
url: '/v1/inbound/stock/draft/list',
|
||||||
@ -632,7 +581,7 @@ const doStartNewSession = async () => {
|
|||||||
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()
|
scannedMap.value.clear()
|
||||||
await loadData()
|
tableData.value = [] // 清空已扫码列表
|
||||||
isSessionActive.value = true
|
isSessionActive.value = true
|
||||||
// ★ 标记当前阶段为 scanning(扫码中)
|
// ★ 标记当前阶段为 scanning(扫码中)
|
||||||
localStorage.setItem('stocktake_phase', 'scanning')
|
localStorage.setItem('stocktake_phase', 'scanning')
|
||||||
@ -690,8 +639,8 @@ const resumeSession = async () => {
|
|||||||
})
|
})
|
||||||
scannedMap.value = map
|
scannedMap.value = map
|
||||||
|
|
||||||
// 加载完整库存数据
|
// 清空本地列表,用户扫码时会实时添加
|
||||||
await loadData()
|
tableData.value = []
|
||||||
|
|
||||||
// ★ 智能路由:根据本地记忆的阶段决定下一步
|
// ★ 智能路由:根据本地记忆的阶段决定下一步
|
||||||
const phase = localStorage.getItem('stocktake_phase')
|
const phase = localStorage.getItem('stocktake_phase')
|
||||||
@ -724,59 +673,8 @@ const returnToScan = () => {
|
|||||||
ElMessage.info('继续扫码,发现漏扫的物料')
|
ElMessage.info('继续扫码,发现漏扫的物料')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★★★ 核心修改:数据加载映射修复 ★★★
|
// ★★★ 核心修改:扫码成功后实时查询后端匹配 ★★★
|
||||||
const loadData = async () => {
|
const onScanSuccess = async (code: string) => {
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getAllStock()
|
|
||||||
const list: StockItem[] = []
|
|
||||||
|
|
||||||
const processItem = (item: any, type: string) => {
|
|
||||||
const stock = parseFloat(item.stock_quantity || item.qty_stock || 0)
|
|
||||||
if (stock <= 0) return
|
|
||||||
|
|
||||||
const name = item.material_name || item.product_name || item.name || '未知物品'
|
|
||||||
const uuid = item.uuid || item.sku || ''
|
|
||||||
const isScanned = scannedMap.value.has(uuid)
|
|
||||||
|
|
||||||
list.push({
|
|
||||||
...item,
|
|
||||||
name: name,
|
|
||||||
// ★ 修复点:优先读取 spec_model,因为数据库是这个字段
|
|
||||||
standard: item.spec_model || item.standard || item.model || '',
|
|
||||||
sku: item.sku || '',
|
|
||||||
batch_no: item.batch_no || item.batch_number || '',
|
|
||||||
serial_number: item.serial_number || '',
|
|
||||||
uuid: uuid,
|
|
||||||
bar_code: item.bar_code || item.barcode || '',
|
|
||||||
qty_stock: stock,
|
|
||||||
qty_actual: isScanned ? scannedMap.value.get(uuid)! : 0,
|
|
||||||
scanned: isScanned,
|
|
||||||
uniqueKey: `${type}_${item.id}`,
|
|
||||||
source_table: typeToSourceTable(type),
|
|
||||||
stock_id: item.id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.materials) res.materials.forEach((i: any) => processItem(i, 'material'))
|
|
||||||
if (res.semis) res.semis.forEach((i: any) => processItem(i, 'semi'))
|
|
||||||
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
|
|
||||||
await fetchBorrowedQuantities(list)
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error('数据加载失败')
|
|
||||||
} finally { loading.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const onScanSuccess = (code: string) => {
|
|
||||||
if (!code || loading.value) return
|
if (!code || loading.value) return
|
||||||
const trimCode = code.trim()
|
const trimCode = code.trim()
|
||||||
|
|
||||||
@ -790,19 +688,61 @@ const onScanSuccess = (code: string) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = allData.value.find(i => i.uuid === trimCode || i.bar_code === trimCode)
|
// 实时查询后端匹配
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await getStockList({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
keyword: trimCode
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res || !res.data || !res.data.list || res.data.list.length === 0) {
|
||||||
|
ElMessage.error(`未找到该物料库存: ${trimCode}`)
|
||||||
|
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找匹配的物料
|
||||||
|
const foundItem = res.data.list.find((i: any) =>
|
||||||
|
i.uuid === trimCode || i.sku === trimCode || i.barcode === trimCode || i.bar_code === trimCode
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!foundItem) {
|
||||||
|
ElMessage.error(`未找到该物料库存: ${trimCode}`)
|
||||||
|
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (item) {
|
|
||||||
if (navigator.vibrate) navigator.vibrate(100)
|
if (navigator.vibrate) navigator.vibrate(100)
|
||||||
|
|
||||||
// ★★★ 核心修改:扫码成功后立即关闭全屏扫码,弹出填数对话框
|
// 关闭全屏扫码,弹出填数对话框
|
||||||
showCamera.value = false
|
showCamera.value = false
|
||||||
|
|
||||||
// 无论是否多批次,都弹出对话框让用户确认数量
|
// 处理数据格式
|
||||||
|
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
|
||||||
|
const type = foundItem.stock_type || foundItem.type || 'material'
|
||||||
|
const item: StockItem = {
|
||||||
|
...foundItem,
|
||||||
|
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
|
||||||
|
standard: foundItem.spec_model || foundItem.standard || foundItem.model || '',
|
||||||
|
sku: foundItem.sku || '',
|
||||||
|
uuid: foundItem.uuid || foundItem.sku || '',
|
||||||
|
bar_code: foundItem.bar_code || foundItem.barcode || '',
|
||||||
|
qty_stock: stock,
|
||||||
|
qty_actual: scannedMap.value.get(foundItem.uuid || foundItem.sku) || 0,
|
||||||
|
scanned: true,
|
||||||
|
uniqueKey: `${type}_${foundItem.id}`,
|
||||||
|
source_table: typeToSourceTable(type),
|
||||||
|
stock_id: foundItem.id
|
||||||
|
}
|
||||||
|
|
||||||
openQtyDialog(item)
|
openQtyDialog(item)
|
||||||
} else {
|
} catch (e) {
|
||||||
ElMessage.error(`不在库条码: ${trimCode}`)
|
ElMessage.error('查询库存失败')
|
||||||
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -820,21 +760,61 @@ const closeScanner = () => {
|
|||||||
showCamera.value = false
|
showCamera.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动输入条码
|
// 手动输入条码 - 实时查询后端
|
||||||
const handleManualInput = async () => {
|
const handleManualInput = async () => {
|
||||||
const code = barcodeInput.value.trim()
|
const code = barcodeInput.value.trim()
|
||||||
if (!code) return
|
if (!code) return
|
||||||
|
|
||||||
|
if (code.length < 3) {
|
||||||
|
ElMessage.warning('输入内容过短,请输入完整条码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const item = allData.value.find(i => i.uuid === code || i.bar_code === code)
|
const res: any = await getStockList({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
keyword: code
|
||||||
|
})
|
||||||
|
|
||||||
if (item) {
|
if (!res || !res.data || !res.data.list || res.data.list.length === 0) {
|
||||||
if (navigator.vibrate) navigator.vibrate(100)
|
ElMessage.error(`未找到该物料库存: ${code}`)
|
||||||
openQtyDialog(item)
|
return
|
||||||
} else {
|
|
||||||
ElMessage.error(`不在库条码: ${code}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const foundItem = res.data.list.find((i: any) =>
|
||||||
|
i.uuid === code || i.sku === code || i.barcode === code || i.bar_code === code
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!foundItem) {
|
||||||
|
ElMessage.error(`未找到该物料库存: ${code}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.vibrate) navigator.vibrate(100)
|
||||||
|
|
||||||
|
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
|
||||||
|
const type = foundItem.stock_type || foundItem.type || 'material'
|
||||||
|
const item: StockItem = {
|
||||||
|
...foundItem,
|
||||||
|
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
|
||||||
|
standard: foundItem.spec_model || foundItem.standard || foundItem.model || '',
|
||||||
|
sku: foundItem.sku || '',
|
||||||
|
uuid: foundItem.uuid || foundItem.sku || '',
|
||||||
|
bar_code: foundItem.bar_code || foundItem.barcode || '',
|
||||||
|
qty_stock: stock,
|
||||||
|
qty_actual: scannedMap.value.get(foundItem.uuid || foundItem.sku) || 0,
|
||||||
|
scanned: true,
|
||||||
|
uniqueKey: `${type}_${foundItem.id}`,
|
||||||
|
source_table: typeToSourceTable(type),
|
||||||
|
stock_id: foundItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
openQtyDialog(item)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('查询库存失败')
|
||||||
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -854,23 +834,25 @@ const openQtyDialog = (item: StockItem) => {
|
|||||||
const handleManualConfirm = () => {
|
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
|
||||||
|
const remark = inputRemark.value
|
||||||
|
|
||||||
// ★★★ 乐观更新:立即本地更新 UI,不等待后端响应 ★★★
|
// ★★★ 更新已扫码物料列表 ★★★
|
||||||
currentItem.value.scanned = true
|
currentItem.value.scanned = true
|
||||||
currentItem.value.qty_actual = val
|
currentItem.value.qty_actual = val
|
||||||
scannedMap.value.set(currentItem.value.uuid, val)
|
scannedMap.value.set(currentItem.value.uuid, val)
|
||||||
|
|
||||||
// 保存备注用于异步提交
|
// 检查是否已存在于 tableData,如果存在则更新,否则添加
|
||||||
const remark = inputRemark.value
|
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}`)
|
||||||
|
|
||||||
// ★★★ 取消自动弹摄像头,把开启摄像头的控制权完全交还给用户
|
|
||||||
// 用户需主动点击才能开启摄像头
|
|
||||||
|
|
||||||
// ★★★ 异步保存到后端,不阻塞 UI(fire-and-forget)★★★
|
// ★★★ 异步保存到后端,不阻塞 UI(fire-and-forget)★★★
|
||||||
syncToBackend(currentItem.value.uuid, val, remark)
|
syncToBackend(currentItem.value.uuid, val, remark)
|
||||||
}
|
}
|
||||||
@ -888,10 +870,17 @@ 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)
|
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 = () => {
|
||||||
@ -935,34 +924,17 @@ const exportToExcel = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredList = computed(() => {
|
|
||||||
let result = allData.value
|
|
||||||
if (filterType.value === 'scanned') result = result.filter(i => i.scanned)
|
|
||||||
else if (filterType.value === 'missing') result = result.filter(i => !i.scanned)
|
|
||||||
|
|
||||||
if (searchKeyword.value) {
|
|
||||||
const kw = searchKeyword.value.toLowerCase()
|
|
||||||
result = result.filter(i =>
|
|
||||||
i.name.toLowerCase().includes(kw) ||
|
|
||||||
i.uuid.includes(kw) ||
|
|
||||||
(i.sku && i.sku.toLowerCase().includes(kw)) ||
|
|
||||||
(i.batch_no && i.batch_no.toLowerCase().includes(kw))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
const stats = computed(() => {
|
const stats = computed(() => {
|
||||||
const total = allData.value.length
|
const total = tableData.value.length
|
||||||
const scanned = allData.value.filter(i => i.scanned).length
|
const scanned = tableData.value.filter(i => i.scanned).length
|
||||||
const varianceItems = allData.value.filter(i =>
|
const varianceItems = tableData.value.filter(i =>
|
||||||
!i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any))
|
!i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any))
|
||||||
).length
|
).length
|
||||||
return { total, scanned, varianceItems }
|
return { total, scanned, varianceItems }
|
||||||
})
|
})
|
||||||
|
|
||||||
const varianceList = computed(() => {
|
const varianceList = computed(() => {
|
||||||
return allData.value
|
return tableData.value
|
||||||
.filter(i => !i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any)))
|
.filter(i => !i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any)))
|
||||||
.map(i => ({
|
.map(i => ({
|
||||||
...i,
|
...i,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div v-if="userStore.hasPermission('transaction_records:view')" class="app-container">
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-radio-group v-model="status" @change="fetchData" style="margin-right: 20px">
|
<el-radio-group v-model="status" @change="fetchData" style="margin-right: 20px">
|
||||||
<el-radio-button label="all">全部</el-radio-button>
|
<el-radio-button label="all">全部</el-radio-button>
|
||||||
|
|||||||
Reference in New Issue
Block a user