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