Compare commits
6 Commits
1e2f4953b6
...
43e1d0aa55
| Author | SHA1 | Date | |
|---|---|---|---|
| 43e1d0aa55 | |||
| 6aec571775 | |||
| c361d25ea0 | |||
| dbcb7d0d92 | |||
| a52ced0375 | |||
| edf09508f6 |
@ -27,6 +27,9 @@ UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
|
||||
# 允许上传的文件后缀
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx'}
|
||||
|
||||
# ★ 文件上传安全加固:限制最大文件大小 (10MB)
|
||||
MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10MB
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and \
|
||||
@ -58,6 +61,16 @@ def upload_file():
|
||||
if file.filename == '':
|
||||
return jsonify({"code": 400, "msg": "未选择文件"}), 400
|
||||
|
||||
# ★ 文件上传安全加固:检查文件大小
|
||||
file.seek(0, os.SEEK_END)
|
||||
file_size = file.tell()
|
||||
file.seek(0) # 重置文件指针到开头
|
||||
if file_size > MAX_CONTENT_LENGTH:
|
||||
return jsonify({
|
||||
"code": 400,
|
||||
"msg": f"文件大小超过限制 ({MAX_CONTENT_LENGTH // (1024*1024)}MB)"
|
||||
}), 400
|
||||
|
||||
if file and allowed_file(file.filename):
|
||||
try:
|
||||
# 获取后缀并生成唯一文件名
|
||||
|
||||
@ -3,6 +3,7 @@ from app.extensions import db, beijing_time
|
||||
from datetime import datetime, timedelta
|
||||
from flask_jwt_extended import jwt_required, get_jwt, get_jwt_identity
|
||||
from app.utils.decorators import permission_required
|
||||
from sqlalchemy.orm import joinedload
|
||||
import uuid as uuid_module
|
||||
import io
|
||||
from openpyxl import Workbook
|
||||
@ -715,8 +716,12 @@ def export_stocktake():
|
||||
1. 盘点差异明细 (diff_qty != 0)
|
||||
2. 账实相符明细 (diff_qty == 0)
|
||||
3. 外借在用资产明细 (未归还的借出记录)
|
||||
4. 未盘点明细(疑似漏盘)
|
||||
"""
|
||||
try:
|
||||
# ★ 获取 session_id 参数,用于过滤当前会话的扫描记录
|
||||
session_id = request.args.get('session_id', '', type=str)
|
||||
|
||||
# 创建工作簿
|
||||
wb = Workbook()
|
||||
wb.remove(wb.active)
|
||||
@ -932,12 +937,17 @@ def export_stocktake():
|
||||
# ===== Sheet 5: 未盘点明细(疑似漏盘) =====
|
||||
# 逻辑:获取已盘点的集合,遍历库存表,找出未盘点且有库存的物资
|
||||
ws5 = wb.create_sheet("未盘点明细(疑似漏盘)")
|
||||
unscanned_headers = ["物料名称", "SKU", "规格型号", "库位", "调整后账面数", "实盘数", "差异数", "状态"]
|
||||
unscanned_headers = ["物料名称", "SKU", "规格型号", "库位", "批号", "调整后账面数", "实盘数", "差异数", "状态"]
|
||||
set_header_row(ws5, unscanned_headers)
|
||||
|
||||
# 获取已盘点的 (source_table, stock_id) 集合
|
||||
all_drafts = StocktakeDraft.query.all()
|
||||
scanned_set = {(d.source_table, d.stock_id) for d in all_drafts}
|
||||
# ★ 修复:只查询当前 session_id 的扫描记录,避免历史记录干扰
|
||||
if session_id:
|
||||
session_drafts = StocktakeDraft.query.filter_by(session_id=session_id).all()
|
||||
else:
|
||||
# 如果没有传 session_id,使用所有记录(兼容旧行为)
|
||||
session_drafts = StocktakeDraft.query.all()
|
||||
scanned_set = {(d.source_table, d.stock_id) for d in session_drafts}
|
||||
|
||||
def get_borrowed_qty(source_table, stock_id):
|
||||
"""获取某库存的借出未还数量"""
|
||||
@ -954,24 +964,33 @@ def export_stocktake():
|
||||
|
||||
unscanned_items = []
|
||||
|
||||
# 遍历 StockBuy
|
||||
for stock in StockBuy.query.all():
|
||||
# ★ 修复 N+1 查询:使用 joinedload 预加载 base 关系,同时过滤 stock_quantity > 0
|
||||
for stock in StockBuy.query.filter(StockBuy.stock_quantity > 0).options(joinedload(StockBuy.base)).all():
|
||||
key = ('stock_buy', stock.id)
|
||||
if key in scanned_set:
|
||||
continue
|
||||
stock_qty = float(stock.stock_quantity or 0)
|
||||
if stock_qty <= 0:
|
||||
continue
|
||||
# 扣除外借数量
|
||||
borrowed_qty = get_borrowed_qty('stock_buy', stock.id)
|
||||
stock_qty = float(stock.stock_quantity or 0)
|
||||
expected_qty = stock_qty - borrowed_qty
|
||||
if expected_qty > 0:
|
||||
mat_info = get_material_info('stock_buy', stock.id)
|
||||
# ★ 直接使用预加载的 base 关系,避免额外查询
|
||||
material = stock.base
|
||||
# ★ 安全提取批号/序列号:使用 getattr 降级链
|
||||
batch_sn = getattr(stock, 'batch_number', None) or getattr(stock, 'sn', None) or getattr(stock, 'serial_number', None) or '-'
|
||||
mat_info = {
|
||||
'name': material.name if material else '-',
|
||||
'sku': getattr(stock, 'sku', None) or '-',
|
||||
'spec': getattr(material, 'spec_model', None) if material else '-',
|
||||
'location': getattr(stock, 'warehouse_location', None) or '-',
|
||||
'batch_no': batch_sn
|
||||
}
|
||||
unscanned_items.append({
|
||||
'name': mat_info['name'],
|
||||
'sku': mat_info['sku'],
|
||||
'spec': mat_info['spec'],
|
||||
'location': mat_info['location'],
|
||||
'batch_no': mat_info['batch_no'],
|
||||
'stock_qty': expected_qty,
|
||||
'actual_qty': 0,
|
||||
'diff_qty': -expected_qty,
|
||||
@ -980,22 +999,31 @@ def export_stocktake():
|
||||
|
||||
# 遍历 StockSemi
|
||||
if StockSemi:
|
||||
for stock in StockSemi.query.all():
|
||||
for stock in StockSemi.query.filter(StockSemi.stock_quantity > 0).options(joinedload(StockSemi.base)).all():
|
||||
key = ('stock_semi', stock.id)
|
||||
if key in scanned_set:
|
||||
continue
|
||||
stock_qty = float(stock.stock_quantity or 0)
|
||||
if stock_qty <= 0:
|
||||
continue
|
||||
borrowed_qty = get_borrowed_qty('stock_semi', stock.id)
|
||||
stock_qty = float(stock.stock_quantity or 0)
|
||||
expected_qty = stock_qty - borrowed_qty
|
||||
if expected_qty > 0:
|
||||
mat_info = get_material_info('stock_semi', stock.id)
|
||||
# ★ 直接使用预加载的 base 关系,避免额外查询
|
||||
material = stock.base
|
||||
# ★ 安全提取批号/序列号:使用 getattr 降级链
|
||||
batch_sn = getattr(stock, 'batch_number', None) or getattr(stock, 'sn', None) or getattr(stock, 'serial_number', None) or '-'
|
||||
mat_info = {
|
||||
'name': material.name if material else '-',
|
||||
'sku': getattr(stock, 'sku', None) or '-',
|
||||
'spec': getattr(material, 'spec_model', None) if material else '-',
|
||||
'location': getattr(stock, 'warehouse_location', None) or '-',
|
||||
'batch_no': batch_sn
|
||||
}
|
||||
unscanned_items.append({
|
||||
'name': mat_info['name'],
|
||||
'sku': mat_info['sku'],
|
||||
'spec': mat_info['spec'],
|
||||
'location': mat_info['location'],
|
||||
'batch_no': mat_info['batch_no'],
|
||||
'stock_qty': expected_qty,
|
||||
'actual_qty': 0,
|
||||
'diff_qty': -expected_qty,
|
||||
@ -1004,7 +1032,7 @@ def export_stocktake():
|
||||
|
||||
# 遍历 StockProduct
|
||||
if StockProduct:
|
||||
for stock in StockProduct.query.all():
|
||||
for stock in StockProduct.query.filter(StockProduct.stock_quantity > 0).options(joinedload(StockProduct.base)).all():
|
||||
key = ('stock_product', stock.id)
|
||||
if key in scanned_set:
|
||||
continue
|
||||
@ -1014,12 +1042,23 @@ def export_stocktake():
|
||||
borrowed_qty = get_borrowed_qty('stock_product', stock.id)
|
||||
expected_qty = stock_qty - borrowed_qty
|
||||
if expected_qty > 0:
|
||||
mat_info = get_material_info('stock_product', stock.id)
|
||||
# ★ 直接使用预加载的 base 关系,避免额外查询
|
||||
material = stock.base
|
||||
# ★ 安全提取批号/序列号:使用 getattr 降级链 (成品可能无此字段)
|
||||
batch_sn = getattr(stock, 'batch_number', None) or getattr(stock, 'sn', None) or getattr(stock, 'serial_number', None) or '-'
|
||||
mat_info = {
|
||||
'name': material.name if material else '-',
|
||||
'sku': getattr(stock, 'sku', None) or '-',
|
||||
'spec': getattr(material, 'spec_model', None) if material else '-',
|
||||
'location': getattr(stock, 'warehouse_location', None) or '-',
|
||||
'batch_no': batch_sn
|
||||
}
|
||||
unscanned_items.append({
|
||||
'name': mat_info['name'],
|
||||
'sku': mat_info['sku'],
|
||||
'spec': mat_info['spec'],
|
||||
'location': mat_info['location'],
|
||||
'batch_no': mat_info['batch_no'],
|
||||
'stock_qty': expected_qty,
|
||||
'actual_qty': 0,
|
||||
'diff_qty': -expected_qty,
|
||||
@ -1033,10 +1072,11 @@ def export_stocktake():
|
||||
ws5.cell(row=row_idx, column=2, value=item['sku']).border = thin_border
|
||||
ws5.cell(row=row_idx, column=3, value=item['spec']).border = thin_border
|
||||
ws5.cell(row=row_idx, column=4, value=item['location']).border = thin_border
|
||||
ws5.cell(row=row_idx, column=5, value=float(item['stock_qty'])).border = thin_border
|
||||
ws5.cell(row=row_idx, column=6, value=float(item['actual_qty'])).border = thin_border
|
||||
ws5.cell(row=row_idx, column=7, value=float(item['diff_qty'])).border = thin_border
|
||||
ws5.cell(row=row_idx, column=8, value=item['status']).border = thin_border
|
||||
ws5.cell(row=row_idx, column=5, value=item.get('batch_no', '-')).border = thin_border # ★ 批号
|
||||
ws5.cell(row=row_idx, column=6, value=float(item['stock_qty'])).border = thin_border
|
||||
ws5.cell(row=row_idx, column=7, value=float(item['actual_qty'])).border = thin_border
|
||||
ws5.cell(row=row_idx, column=8, value=float(item['diff_qty'])).border = thin_border
|
||||
ws5.cell(row=row_idx, column=9, value=item['status']).border = thin_border
|
||||
# 同时写入 Sheet 1 (汇总表) - 盘点人和时间留空
|
||||
ws1.cell(row=master_row_idx, column=1, value=item['name']).border = thin_border
|
||||
ws1.cell(row=master_row_idx, column=2, value=item['sku']).border = thin_border
|
||||
@ -1091,15 +1131,22 @@ def export_stocktake():
|
||||
def generate_missing_stocktake():
|
||||
"""
|
||||
生成漏盘数据:
|
||||
找出所有真实库存 > 0,但未被盘点扫描到的物料,
|
||||
找出所有真实库存 > 0,但未被当前会话盘点扫描到的物料,
|
||||
自动生成盘点草稿,标记为盘亏(实盘=0,差异=-库存数)
|
||||
"""
|
||||
try:
|
||||
# 1. 获取所有已有盘点记录的 (source_table, stock_id) 集合
|
||||
# ★ 获取 session_id 参数,用于隔离当前会话
|
||||
data = request.get_json() or {}
|
||||
session_id = data.get('session_id', '')
|
||||
|
||||
if not session_id:
|
||||
return jsonify({'code': 400, 'msg': '缺少 session_id 参数'}), 400
|
||||
|
||||
# 1. 获取当前会话已有盘点记录的 (source_table, stock_id) 集合
|
||||
existing_records = db.session.query(
|
||||
StocktakeDraft.source_table,
|
||||
StocktakeDraft.stock_id
|
||||
).distinct().all()
|
||||
).filter(StocktakeDraft.session_id == session_id).distinct().all()
|
||||
|
||||
scanned_keys = set()
|
||||
for src_table, stock_id in existing_records:
|
||||
@ -1205,6 +1252,8 @@ def get_all_stocktake_items():
|
||||
'id': item.id,
|
||||
'sku': item.sku or '',
|
||||
'barcode': item.barcode or '',
|
||||
# ★ 安全提取批号/序列号:使用 getattr 降级
|
||||
'batch_no': getattr(item, 'batch_number', None) or getattr(item, 'sn', None) or getattr(item, 'serial_number', None) 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),
|
||||
@ -1229,6 +1278,8 @@ def get_all_stocktake_items():
|
||||
'id': item.id,
|
||||
'sku': item.sku or '',
|
||||
'barcode': item.barcode or '',
|
||||
# ★ 安全提取批号/序列号:使用 getattr 降级
|
||||
'batch_no': getattr(item, 'batch_number', None) or getattr(item, 'sn', None) or getattr(item, 'serial_number', None) 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),
|
||||
@ -1253,6 +1304,8 @@ def get_all_stocktake_items():
|
||||
'id': item.id,
|
||||
'sku': item.sku or '',
|
||||
'barcode': item.barcode or '',
|
||||
# ★ 安全提取批号/序列号:使用 getattr 降级 (成品无此字段则为空)
|
||||
'batch_no': getattr(item, 'batch_number', None) or getattr(item, 'sn', None) or getattr(item, 'serial_number', None) 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),
|
||||
|
||||
@ -132,7 +132,8 @@ def create():
|
||||
if not StockModel:
|
||||
return jsonify({'code': 400, 'msg': '无效的库存类型'}), 400
|
||||
|
||||
stock = StockModel.query.get(stock_id)
|
||||
# ★ 修复并发冲突:使用悲观锁锁定行,防止多人同时修改导致的数据覆盖
|
||||
stock = StockModel.query.filter(StockModel.id == stock_id).with_for_update().first()
|
||||
if not stock:
|
||||
return jsonify({'code': 404, 'msg': '库存记录不存在'}), 404
|
||||
|
||||
|
||||
@ -205,18 +205,18 @@ def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target
|
||||
username = claims.get('username', '')
|
||||
display_name = claims.get('display_name', '')
|
||||
|
||||
# 兜底:如果 display_name 为空,查询数据库获取
|
||||
# ★ 修复 DetachedInstanceError:在 fn() 执行前预先获取用户完整信息
|
||||
# 这样可以避免在 fn() 提交 session 后再访问 User 对象导致游离
|
||||
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)
|
||||
display_name = user.display_name or username
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 获取IP
|
||||
# 预先获取 IP(避免后续访问 request 对象异常)
|
||||
ip_address = request.headers.get('X-Forwarded-For') or request.remote_addr or ''
|
||||
if ip_address and ',' in ip_address:
|
||||
ip_address = ip_address.split(',')[0].strip()
|
||||
@ -235,7 +235,7 @@ def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target
|
||||
raw_payload = _get_payload()
|
||||
filtered_payload = _filter_payload(raw_payload) if raw_payload else None
|
||||
|
||||
# 执行原函数
|
||||
# 执行原函数(此时 Session 可能被提交或回滚)
|
||||
response = fn(*args, **kwargs)
|
||||
|
||||
# 只记录成功的请求(响应状态码 200/201)
|
||||
@ -249,6 +249,9 @@ def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target
|
||||
from app.extensions import db
|
||||
from flask import current_app
|
||||
|
||||
# ★ 已在上方预先获取 display_name,此处无需再查询 User 对象
|
||||
# 使用预先获取的字符串数据,避免 DetachedInstanceError
|
||||
|
||||
# 获取 target_id
|
||||
target_id = None
|
||||
if get_target_id_fn:
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
v-model="visible"
|
||||
title="智能扫码"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 打印机配置弹窗 -->
|
||||
<el-dialog v-model="printerDialogVisible" title="打印机 IP 配置" width="500px">
|
||||
<el-dialog v-model="printerDialogVisible" title="打印机 IP 配置" width="500px" destroy-on-close>
|
||||
<el-form :model="printerForm" label-width="120px">
|
||||
<el-form-item label="标签打印机 IP">
|
||||
<el-input v-model="printerForm.label_ip" placeholder="例如 192.168.9.221" />
|
||||
@ -69,7 +69,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 库位管理弹窗 -->
|
||||
<el-dialog v-model="warehouseDialogVisible" title="库位管理" width="700px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="warehouseDialogVisible" title="库位管理" width="700px" destroy-on-close :close-on-click-modal="false">
|
||||
<div class="warehouse-dialog">
|
||||
<div class="warehouse-header">
|
||||
<!-- 非批量模式 -->
|
||||
@ -145,7 +145,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增/编辑库位弹窗 -->
|
||||
<el-dialog v-model="locationFormVisible" :title="locationFormTitle" width="400px">
|
||||
<el-dialog v-model="locationFormVisible" :title="locationFormTitle" width="400px" destroy-on-close>
|
||||
<el-form :model="locationForm" label-width="80px">
|
||||
<el-form-item label="上级库位">
|
||||
<el-input :value="locationForm.parentName" disabled />
|
||||
@ -164,7 +164,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量生成库位弹窗 -->
|
||||
<el-dialog v-model="batchGenerateVisible" title="批量生成库位" width="800px">
|
||||
<el-dialog v-model="batchGenerateVisible" title="批量生成库位" width="800px" destroy-on-close>
|
||||
<el-form :model="batchGenerateForm" label-width="100px">
|
||||
<el-form-item label="父级库位">
|
||||
<el-input :value="batchGenerateForm.parentName" disabled />
|
||||
|
||||
@ -194,13 +194,14 @@
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
row-key="id"
|
||||
:size="tableSize"
|
||||
:row-class-name="tableRowClassName"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%; margin-top: 15px"
|
||||
>
|
||||
<el-table-column v-if="isBatchMode" type="selection" width="55" />
|
||||
<el-table-column v-if="isBatchMode" type="selection" width="55" :reserve-selection="true" />
|
||||
<el-table-column v-if="columns.id.visible" prop="id" label="ID" min-width="80" align="center" fixed="left" />
|
||||
|
||||
<el-table-column v-if="columns.companyName.visible" prop="companyName" label="所属公司" min-width="100" align="center" show-overflow-tooltip sortable="custom">
|
||||
@ -323,6 +324,7 @@
|
||||
:title="dialog.title"
|
||||
width="700px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@close="cancel"
|
||||
:close-on-click-modal="!isUploading"
|
||||
:close-on-press-escape="!isUploading"
|
||||
|
||||
@ -86,7 +86,7 @@
|
||||
/>
|
||||
|
||||
<!-- 新增调整单弹窗 -->
|
||||
<el-dialog v-model="showDialog" title="新增盘盈盘亏调整单" width="700px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="showDialog" title="新增盘盈盘亏调整单" width="700px" destroy-on-close :close-on-click-modal="false">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="选择物料" prop="stock_id">
|
||||
<el-select
|
||||
@ -128,7 +128,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 物料选择弹窗 -->
|
||||
<el-dialog v-model="showStockDialog" title="选择物料" width="800px">
|
||||
<el-dialog v-model="showStockDialog" title="选择物料" width="800px" destroy-on-close>
|
||||
<div class="filter-container" style="margin-bottom: 15px">
|
||||
<el-input v-model="stockKeyword" placeholder="搜索SKU/条码" style="width: 200px" @keyup.enter="fetchStocks" clearable />
|
||||
<el-button type="primary" @click="fetchStocks">搜索</el-button>
|
||||
@ -152,7 +152,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 盘点差异审核弹窗 -->
|
||||
<el-dialog v-model="showReviewDialog" title="盘点差异审核" width="1200px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="showReviewDialog" title="盘点差异审核" width="1200px" destroy-on-close :close-on-click-modal="false">
|
||||
<div v-loading="reviewLoading">
|
||||
<!-- 搜索栏 -->
|
||||
<div style="margin-bottom: 16px; display: flex; gap: 12px;">
|
||||
@ -171,8 +171,8 @@
|
||||
<el-button type="primary" @click="handleReviewSearch">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="reviewList" border stripe ref="reviewTableRef" @selection-change="handleReviewSelectionChange">
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table :data="reviewList" border stripe ref="reviewTableRef" row-key="id" @selection-change="handleReviewSelectionChange">
|
||||
<el-table-column type="selection" width="50" :reserve-selection="true" />
|
||||
<el-table-column prop="sku" label="SKU" width="140" />
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="spec_model" label="规格" width="120" show-overflow-tooltip />
|
||||
@ -226,7 +226,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 处理调整单弹窗 -->
|
||||
<el-dialog v-model="showProcessDialog" title="处理调整单" width="500px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="showProcessDialog" title="处理调整单" width="500px" destroy-on-close :close-on-click-modal="false">
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="调整单号">
|
||||
<el-input v-model="processForm.order_no" disabled />
|
||||
|
||||
@ -84,6 +84,7 @@
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="700px"
|
||||
destroy-on-close
|
||||
@close="resetDialog"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
v-model="showConfirmDialog"
|
||||
title="⚠️ 确认清除盘点数据"
|
||||
width="400"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
show-close
|
||||
@ -234,10 +235,10 @@
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
:data="filteredListData"
|
||||
height="100%"
|
||||
height="600"
|
||||
stripe
|
||||
border
|
||||
row-key="id"
|
||||
row-key="uniqueKey"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
|
||||
@ -659,7 +660,7 @@ const resumeSession = async () => {
|
||||
const res: any = await request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
method: 'get',
|
||||
params: { page: 1, limit: 10000 } // 获取足够多的数据
|
||||
params: { page: 1, limit: 500 } // ★ 限制单次加载数量,防止内存溢出
|
||||
})
|
||||
|
||||
const drafts = res && res.items ? res.items : []
|
||||
@ -916,9 +917,11 @@ const exportToExcel = async () => {
|
||||
// ===== 调试结束 =====
|
||||
|
||||
ElMessage.info('正在生成盘点报告,请稍候...');
|
||||
// ★ 传递 session_id 参数,用于导出当前会话的未盘点明细
|
||||
const sessionParam = currentSessionId.value ? `?session_id=${encodeURIComponent(currentSessionId.value)}` : '';
|
||||
// 使用项目封装的 request 发送请求,确保自动携带 JWT Token
|
||||
const res: any = await request({
|
||||
url: '/v1/inbound/stock/export-stocktake',
|
||||
url: '/v1/inbound/stock/export-stocktake' + sessionParam,
|
||||
method: 'get',
|
||||
responseType: 'blob' as any, // 核心:接收二进制文件流
|
||||
headers: {
|
||||
@ -982,12 +985,15 @@ const fetchInventoryList = async (silent = false) => {
|
||||
method: 'get',
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 10000, // 获取全部已盘点记录
|
||||
keyword: listKeyword.value
|
||||
limit: 500, // ★ 限制单次加载数量,防止内存溢出
|
||||
keyword: listKeyword.value,
|
||||
session_id: currentSessionId.value // ★ 必须传递 session_id 隔离会话
|
||||
}
|
||||
})
|
||||
|
||||
const scannedDrafts = res?.items || []
|
||||
// ★ 使用返回的 total 获取真实已盘数量,而不是受限的数组长度
|
||||
const totalScanned = res?.total || scannedDrafts.length
|
||||
|
||||
// 保存全量草稿记录用于全局统计
|
||||
allScannedDrafts.value = scannedDrafts
|
||||
@ -1004,6 +1010,7 @@ const fetchInventoryList = async (silent = false) => {
|
||||
id: draft?.id || null,
|
||||
stock_id: item.id,
|
||||
source_table: item.source_table,
|
||||
uniqueKey: `${item.source_table}_${item.id}`, // ★ 绝对唯一键,解决row-key冲突
|
||||
sku: item.sku,
|
||||
material_name: item.material_name,
|
||||
spec_model: item.spec_model,
|
||||
@ -1129,7 +1136,10 @@ const handleGenerateMissing = async () => {
|
||||
btnLoading.value = true
|
||||
const res = await request({
|
||||
url: '/v1/inbound/stock/stocktake/generate-missing',
|
||||
method: 'post'
|
||||
method: 'post',
|
||||
data: {
|
||||
session_id: currentSessionId.value // ★ 必须传递 session_id 隔离会话
|
||||
}
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
|
||||
@ -85,7 +85,7 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailDialogVisible" title="操作详情" width="700px">
|
||||
<el-dialog v-model="detailDialogVisible" title="操作详情" width="700px" destroy-on-close>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ID">{{ currentLog.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ currentLog.username }} ({{ currentLog.display_name }})</el-descriptions-item>
|
||||
|
||||
@ -59,6 +59,7 @@
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑员工账号' : '新增员工账号'"
|
||||
width="500px"
|
||||
destroy-on-close
|
||||
@close="resetForm"
|
||||
>
|
||||
<el-form
|
||||
|
||||
Reference in New Issue
Block a user