Compare commits
4 Commits
df2fa4baf1
...
3ba4e74a60
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ba4e74a60 | |||
| 7e23141870 | |||
| 9b290506da | |||
| 96122ed671 |
@ -3,10 +3,12 @@ from app.extensions import db
|
||||
# ★★★ 修复点:必须引入 datetime,否则下方更新时间时会报错 500 ★★★
|
||||
from datetime import datetime
|
||||
from app.utils.decorators import permission_required
|
||||
import uuid as uuid_module
|
||||
|
||||
# 导入模型
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.stocktake import StocktakeDraft
|
||||
from app.models.transaction import TransBorrow
|
||||
|
||||
# 尝试导入半成品和成品
|
||||
try:
|
||||
@ -24,6 +26,52 @@ from app.services.print.network_print_service import NetworkPrintService
|
||||
bp = Blueprint('stock_ops', __name__)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 辅助函数:获取库存记录
|
||||
# ============================================================
|
||||
def get_stock_record(source_table, stock_id):
|
||||
"""根据库存类型和ID获取库存记录"""
|
||||
if source_table == 'stock_buy' and StockBuy:
|
||||
return StockBuy.query.get(stock_id)
|
||||
elif source_table == 'stock_semi' and StockSemi:
|
||||
return StockSemi.query.get(stock_id)
|
||||
elif source_table == 'stock_product' and StockProduct:
|
||||
return StockProduct.query.get(stock_id)
|
||||
return None
|
||||
|
||||
|
||||
def get_stock_info(uuid_or_barcode):
|
||||
"""
|
||||
根据 uuid 或 barcode 查询库存信息
|
||||
返回: (item, source_table, stock_id)
|
||||
"""
|
||||
# 1. 成品
|
||||
if StockProduct:
|
||||
item = StockProduct.query.filter(
|
||||
db.or_(StockProduct.barcode == uuid_or_barcode, StockProduct.sku == uuid_or_barcode)
|
||||
).first()
|
||||
if item:
|
||||
return (item, 'stock_product', item.id)
|
||||
|
||||
# 2. 半成品
|
||||
if StockSemi:
|
||||
item = StockSemi.query.filter(
|
||||
db.or_(StockSemi.barcode == uuid_or_barcode, StockSemi.sku == uuid_or_barcode)
|
||||
).first()
|
||||
if item:
|
||||
return (item, 'stock_semi', item.id)
|
||||
|
||||
# 3. 采购件
|
||||
if StockBuy:
|
||||
item = StockBuy.query.filter(
|
||||
db.or_(StockBuy.barcode == uuid_or_barcode, StockBuy.sku == uuid_or_barcode)
|
||||
).first()
|
||||
if item:
|
||||
return (item, 'stock_buy', item.id)
|
||||
|
||||
return (None, None, None)
|
||||
|
||||
|
||||
@bp.route('/all', methods=['GET'])
|
||||
@permission_required('inventory_stocktake')
|
||||
def get_all_stock():
|
||||
@ -67,59 +115,379 @@ def get_all_stock():
|
||||
@bp.route('/draft/list', methods=['GET'])
|
||||
@permission_required('inventory_stocktake')
|
||||
def get_drafts():
|
||||
"""获取当前用户的盘点进度"""
|
||||
"""
|
||||
获取当前用户的盘点进度
|
||||
支持过滤: session_id, is_finished, is_processed
|
||||
"""
|
||||
user_id = request.args.get('user_id', 'admin')
|
||||
drafts = StocktakeDraft.query.filter_by(user_id=user_id).all()
|
||||
session_id = request.args.get('session_id')
|
||||
is_finished = request.args.get('is_finished')
|
||||
is_processed = request.args.get('is_processed')
|
||||
|
||||
query = StocktakeDraft.query.filter_by(user_id=user_id)
|
||||
|
||||
if session_id:
|
||||
query = query.filter_by(session_id=session_id)
|
||||
if is_finished is not None:
|
||||
query = query.filter_by(is_finished=is_finished.lower() in ['true', '1', 'yes'])
|
||||
if is_processed is not None:
|
||||
query = query.filter_by(is_processed=is_processed.lower() in ['true', '1', 'yes'])
|
||||
|
||||
drafts = query.order_by(StocktakeDraft.scan_time.desc()).all()
|
||||
return jsonify([d.to_dict() for d in drafts]), 200
|
||||
|
||||
|
||||
@bp.route('/draft/add', methods=['POST'])
|
||||
@permission_required('inventory_stocktake:operation')
|
||||
def add_draft():
|
||||
"""扫码同步 (支持更新数量)"""
|
||||
"""
|
||||
扫码同步 (支持更新数量)
|
||||
如果 session_id 不存在则创建新的会话
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
user_id = data.get('user_id', 'admin')
|
||||
uuid = data.get('uuid')
|
||||
quantity = data.get('quantity', 1)
|
||||
quantity = float(data.get('quantity', 1))
|
||||
session_id = data.get('session_id')
|
||||
|
||||
if not uuid:
|
||||
return jsonify({"message": "UUID不能为空"}), 400
|
||||
|
||||
# 如果没有 session_id,创建新的
|
||||
if not session_id:
|
||||
session_id = f"STK-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid_module.uuid4().hex[:6]}"
|
||||
|
||||
# 获取库存信息
|
||||
item, source_table, stock_id = get_stock_info(uuid)
|
||||
if not item:
|
||||
return jsonify({"message": "未找到对应的库存记录"}), 404
|
||||
|
||||
stock_qty = float(item.stock_quantity) if item.stock_quantity else 0
|
||||
|
||||
# 查找是否已存在
|
||||
draft = StocktakeDraft.query.filter_by(user_id=user_id, uuid=uuid).first()
|
||||
draft = StocktakeDraft.query.filter_by(user_id=user_id, uuid=uuid, session_id=session_id).first()
|
||||
|
||||
if draft:
|
||||
# 如果已存在,更新数量和时间
|
||||
draft.quantity = quantity
|
||||
# ★ 修复点:这里需要 datetime 对象
|
||||
draft.scan_time = datetime.now()
|
||||
draft.stock_qty = stock_qty
|
||||
draft.diff_qty = quantity - stock_qty
|
||||
draft.source_table = source_table
|
||||
draft.stock_id = stock_id
|
||||
else:
|
||||
# 如果不存在,创建新的
|
||||
draft = StocktakeDraft(user_id=user_id, uuid=uuid, quantity=quantity)
|
||||
draft = StocktakeDraft(
|
||||
user_id=user_id,
|
||||
uuid=uuid,
|
||||
quantity=quantity,
|
||||
session_id=session_id,
|
||||
stock_qty=stock_qty,
|
||||
diff_qty=quantity - stock_qty,
|
||||
source_table=source_table,
|
||||
stock_id=stock_id,
|
||||
is_finished=False,
|
||||
is_processed=False
|
||||
)
|
||||
db.session.add(draft)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({"message": "Saved"}), 200
|
||||
return jsonify({
|
||||
"message": "Saved",
|
||||
"session_id": session_id,
|
||||
"draft_id": draft.id
|
||||
}), 200
|
||||
except Exception as e:
|
||||
print(f"Add Draft Error: {e}")
|
||||
db.session.rollback()
|
||||
return jsonify({"message": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/draft/clear', methods=['POST'])
|
||||
@permission_required('inventory_stocktake:operation')
|
||||
def clear_draft():
|
||||
"""清空进度"""
|
||||
"""
|
||||
清除盘点草稿
|
||||
支持清除指定 session_id 的记录,或清除所有未完成的记录
|
||||
"""
|
||||
data = request.json
|
||||
user_id = data.get('user_id', 'admin')
|
||||
session_id = data.get('session_id')
|
||||
|
||||
try:
|
||||
query = StocktakeDraft.query.filter_by(user_id=user_id)
|
||||
|
||||
if session_id:
|
||||
# 清除指定会话
|
||||
query = query.filter_by(session_id=session_id)
|
||||
else:
|
||||
# 默认只清除未完成的记录
|
||||
query = query.filter_by(is_finished=False)
|
||||
|
||||
count = query.delete()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"message": f"已清除 {count} 条记录", "count": count}), 200
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"message": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/draft/start-new', methods=['POST'])
|
||||
@permission_required('inventory_stocktake:operation')
|
||||
def start_new_session():
|
||||
"""
|
||||
开始新一轮盘点
|
||||
1. 先清除用户所有未处理的旧盘点数据
|
||||
2. 返回新的 session_id
|
||||
"""
|
||||
data = request.json
|
||||
user_id = data.get('user_id', 'admin')
|
||||
|
||||
StocktakeDraft.query.filter_by(user_id=user_id).delete()
|
||||
db.session.commit()
|
||||
return jsonify({"message": "Cleared"}), 200
|
||||
try:
|
||||
# 清除旧的未处理盘点数据
|
||||
old_count = StocktakeDraft.query.filter_by(
|
||||
user_id=user_id,
|
||||
is_processed=False
|
||||
).delete()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 生成新的 session_id
|
||||
new_session_id = f"STK-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid_module.uuid4().hex[:6]}"
|
||||
|
||||
return jsonify({
|
||||
"message": f"已清除 {old_count} 条旧记录",
|
||||
"session_id": new_session_id,
|
||||
"cleared_count": old_count
|
||||
}), 200
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"message": str(e)}), 500
|
||||
|
||||
|
||||
# --- 盘点结束与差异报告 ---
|
||||
|
||||
@bp.route('/finish', methods=['POST'])
|
||||
@permission_required('inventory_stocktake:operation')
|
||||
def finish_stocktake():
|
||||
"""
|
||||
结束盘点
|
||||
1. 将指定 session 的草稿标记为 is_finished=True
|
||||
2. 计算差异数量
|
||||
3. 不删除任何草稿数据,保留历史
|
||||
"""
|
||||
data = request.json
|
||||
user_id = data.get('user_id', 'admin')
|
||||
session_id = data.get('session_id')
|
||||
|
||||
if not session_id:
|
||||
return jsonify({"message": "session_id 不能为空"}), 400
|
||||
|
||||
try:
|
||||
# 查找该 session 下所有未结束的草稿
|
||||
drafts = StocktakeDraft.query.filter_by(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
is_finished=False
|
||||
).all()
|
||||
|
||||
if not drafts:
|
||||
return jsonify({"message": "没有找到未结束的盘点记录"}), 404
|
||||
|
||||
# 更新每个草稿的状态
|
||||
now = datetime.now()
|
||||
for draft in drafts:
|
||||
draft.is_finished = True
|
||||
draft.finish_time = now
|
||||
|
||||
# 重新计算差异(以防库存已变化)
|
||||
if draft.stock_id and draft.source_table:
|
||||
stock = get_stock_record(draft.source_table, draft.stock_id)
|
||||
if stock:
|
||||
current_stock = float(stock.stock_quantity) if stock.stock_quantity else 0
|
||||
draft.stock_qty = current_stock
|
||||
draft.diff_qty = float(draft.quantity) - current_stock
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
"message": f"已结束盘点,共处理 {len(drafts)} 条记录",
|
||||
"finished_count": len(drafts),
|
||||
"session_id": session_id
|
||||
}), 200
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"message": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/variance-report', methods=['GET'])
|
||||
@permission_required('inventory_stocktake')
|
||||
def get_variance_report():
|
||||
"""
|
||||
获取盘点差异报告
|
||||
返回所有 is_finished=True 且 is_processed=False 的记录
|
||||
即:已结束盘点但尚未手动平账的差异记录
|
||||
"""
|
||||
user_id = request.args.get('user_id', 'admin')
|
||||
session_id = request.args.get('session_id')
|
||||
|
||||
try:
|
||||
query = StocktakeDraft.query.filter_by(
|
||||
user_id=user_id,
|
||||
is_finished=True,
|
||||
is_processed=False
|
||||
)
|
||||
|
||||
if session_id:
|
||||
query = query.filter_by(session_id=session_id)
|
||||
|
||||
# 只返回有差异的记录
|
||||
drafts = query.filter(StocktakeDraft.diff_qty != 0).order_by(
|
||||
StocktakeDraft.finish_time.desc(),
|
||||
StocktakeDraft.diff_qty.desc()
|
||||
).all()
|
||||
|
||||
# 补充库存详情
|
||||
result = []
|
||||
for draft in drafts:
|
||||
draft_dict = draft.to_dict()
|
||||
|
||||
# 获取库存详情
|
||||
if draft.stock_id and draft.source_table:
|
||||
stock = get_stock_record(draft.source_table, draft.stock_id)
|
||||
if stock:
|
||||
draft_dict['stock_name'] = getattr(stock, 'material_name', None) or \
|
||||
getattr(stock, 'product_name', None) or ''
|
||||
draft_dict['stock_spec'] = getattr(stock, 'spec_model', '') or \
|
||||
getattr(stock, 'standard', '') or ''
|
||||
draft_dict['stock_location'] = getattr(stock, 'warehouse_location', '') or ''
|
||||
draft_dict['stock_unit'] = getattr(stock, 'unit', '个')
|
||||
|
||||
result.append(draft_dict)
|
||||
|
||||
return jsonify({
|
||||
"list": result,
|
||||
"total": len(result)
|
||||
}), 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return jsonify({"message": str(e)}), 500
|
||||
|
||||
|
||||
# --- 单条库存调整 (手动平账) ---
|
||||
|
||||
@bp.route('/adjust', methods=['POST'])
|
||||
@permission_required('inventory_stocktake:operation')
|
||||
def adjust_stock():
|
||||
"""
|
||||
单条库存调整接口
|
||||
接收指定的草稿 ID,执行以下操作:
|
||||
1. 根据 diff_qty 调整库存 (stock_quantity 和 available_quantity)
|
||||
2. 生成流水账记录 (盘盈入库 / 盘亏出库)
|
||||
3. 标记草稿为 is_processed=True
|
||||
"""
|
||||
data = request.json
|
||||
draft_id = data.get('draft_id')
|
||||
operator_name = data.get('operator_name', 'System')
|
||||
remark = data.get('remark', '')
|
||||
|
||||
if not draft_id:
|
||||
return jsonify({"message": "draft_id 不能为空"}), 400
|
||||
|
||||
try:
|
||||
# 1. 获取草稿记录
|
||||
draft = StocktakeDraft.query.get(draft_id)
|
||||
if not draft:
|
||||
return jsonify({"message": "草稿记录不存在"}), 404
|
||||
|
||||
if not draft.is_finished:
|
||||
return jsonify({"message": "该记录尚未结束盘点,无法调整"}), 400
|
||||
|
||||
if draft.is_processed:
|
||||
return jsonify({"message": "该记录已处理过平账"}), 400
|
||||
|
||||
# 2. 获取库存记录
|
||||
if not draft.stock_id or not draft.source_table:
|
||||
return jsonify({"message": "草稿记录缺少库存关联信息"}), 400
|
||||
|
||||
stock = get_stock_record(draft.source_table, draft.stock_id)
|
||||
if not stock:
|
||||
return jsonify({"message": "库存记录不存在"}), 404
|
||||
|
||||
diff_qty = float(draft.diff_qty)
|
||||
|
||||
# 3. 计算调整
|
||||
if diff_qty > 0:
|
||||
# 盘盈:增加库存
|
||||
new_stock_qty = float(stock.stock_quantity or 0) + diff_qty
|
||||
new_avail_qty = float(stock.available_quantity or 0) + diff_qty
|
||||
action_type = '盘盈入库'
|
||||
elif diff_qty < 0:
|
||||
# 盘亏:减少库存
|
||||
abs_diff = abs(diff_qty)
|
||||
current_avail = float(stock.available_quantity or 0)
|
||||
if current_avail < abs_diff:
|
||||
return jsonify({
|
||||
"message": f"可用库存不足,当前可用: {current_avail},需要减少: {abs_diff}"
|
||||
}), 400
|
||||
|
||||
new_stock_qty = float(stock.stock_quantity or 0) - abs_diff
|
||||
new_avail_qty = current_avail - abs_diff
|
||||
action_type = '盘亏出库'
|
||||
else:
|
||||
return jsonify({"message": "差异为0,无需调整"}), 400
|
||||
|
||||
# 4. 执行库存调整
|
||||
stock.stock_quantity = new_stock_qty
|
||||
stock.available_quantity = new_avail_qty
|
||||
|
||||
# 5. 生成流水账记录
|
||||
# 导入流水账模型
|
||||
from app.models.outbound import TransOutbound
|
||||
|
||||
trans_record = TransOutbound(
|
||||
outbound_no=f"STKADJ-{datetime.now().strftime('%Y%m%d%H%M%S')}-{draft.id:04d}",
|
||||
sku=stock.sku,
|
||||
source_table=draft.source_table,
|
||||
stock_id=draft.stock_id,
|
||||
barcode=getattr(stock, 'barcode', ''),
|
||||
quantity=abs(diff_qty),
|
||||
unit_price=getattr(stock, 'pre_tax_unit_price', 0) or getattr(stock, 'manual_cost', 0) or 0,
|
||||
outbound_type=action_type, # 盘盈入库 / 盘亏出库
|
||||
consumer_name='盘点调整',
|
||||
operator_name=operator_name,
|
||||
remark=f"{action_type} - 盘点差异调整,备注: {remark}",
|
||||
outbound_time=datetime.now()
|
||||
)
|
||||
db.session.add(trans_record)
|
||||
|
||||
# 6. 标记草稿为已处理
|
||||
draft.is_processed = True
|
||||
draft.processed_time = datetime.now()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
"message": f"{action_type}成功",
|
||||
"action_type": action_type,
|
||||
"diff_qty": diff_qty,
|
||||
"new_stock_qty": new_stock_qty,
|
||||
"new_avail_qty": new_avail_qty,
|
||||
"draft_id": draft_id
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Adjust Stock Error: {e}")
|
||||
return jsonify({"message": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/borrowed-quantities', methods=['POST'])
|
||||
@permission_required('inventory_stocktake')
|
||||
def get_borrowed_quantities():
|
||||
"""批量获取借出未还数量"""
|
||||
from app.models.transaction import TransBorrow
|
||||
data = request.json.get('items', [])
|
||||
result = {}
|
||||
for item in data:
|
||||
|
||||
@ -1,22 +1,55 @@
|
||||
from app.extensions import db, beijing_time # .material -> .base refactor checked
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class StocktakeDraft(db.Model):
|
||||
"""
|
||||
盘点草稿表
|
||||
支持多轮盘点,保留历史记录
|
||||
"""
|
||||
__tablename__ = 'stocktake_draft'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.String(100), default='admin')
|
||||
# 关联的库存UUID (sku/barcode)
|
||||
uuid = db.Column(db.String(100))
|
||||
# ★ 新增 quantity 字段
|
||||
# 实际盘点数量
|
||||
quantity = db.Column(db.Numeric(19, 4), default=1)
|
||||
scan_time = db.Column(db.DateTime, default=beijing_time)
|
||||
|
||||
# ★ 新增: 盘点会话标识 (用于区分不同批次的盘点)
|
||||
session_id = db.Column(db.String(100))
|
||||
# ★ 新增: 是否已结束盘点
|
||||
is_finished = db.Column(db.Boolean, default=False)
|
||||
# ★ 新增: 盘点结束时间
|
||||
finish_time = db.Column(db.DateTime)
|
||||
# ★ 新增: 是否已处理差异 (手动平账)
|
||||
is_processed = db.Column(db.Boolean, default=False)
|
||||
# ★ 新增: 处理时间
|
||||
processed_time = db.Column(db.DateTime)
|
||||
# ★ 新增: 差异数量 (实盘 - 账面, 正=盘盈, 负=盘亏)
|
||||
diff_qty = db.Column(db.Numeric(19, 4), default=0)
|
||||
# ★ 新增: 账面库存数量
|
||||
stock_qty = db.Column(db.Numeric(19, 4), default=0)
|
||||
# ★ 新增: 关联的库存类型 (stock_buy/stock_semi/stock_product)
|
||||
source_table = db.Column(db.String(50))
|
||||
# ★ 新增: 关联的库存ID
|
||||
stock_id = db.Column(db.Integer)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
'uuid': self.uuid,
|
||||
# ★ 返回 quantity
|
||||
'quantity': float(self.quantity or 1),
|
||||
'scan_time': self.scan_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
'scan_time': self.scan_time.strftime('%Y-%m-%d %H:%M:%S') if self.scan_time else None,
|
||||
'session_id': self.session_id,
|
||||
'is_finished': self.is_finished,
|
||||
'finish_time': self.finish_time.strftime('%Y-%m-%d %H:%M:%S') if self.finish_time else None,
|
||||
'is_processed': self.is_processed,
|
||||
'processed_time': self.processed_time.strftime('%Y-%m-%d %H:%M:%S') if self.processed_time else None,
|
||||
'diff_qty': float(self.diff_qty or 0),
|
||||
'stock_qty': float(self.stock_qty or 0),
|
||||
'source_table': self.source_table,
|
||||
'stock_id': self.stock_id
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ const service = axios.create({
|
||||
// 设置为 '/api',请求会自动拼接成 http://localhost:5173/api/...
|
||||
// 然后被 Vite 代理转发到 http://127.0.0.1:8000/api/...
|
||||
baseURL: '/api',
|
||||
timeout: 5000
|
||||
timeout: 60000
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
|
||||
@ -323,6 +323,9 @@
|
||||
width="700px"
|
||||
append-to-body
|
||||
@close="cancel"
|
||||
:close-on-click-modal="!isUploading"
|
||||
:close-on-press-escape="!isUploading"
|
||||
:show-close="!isUploading"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
|
||||
@ -477,8 +480,8 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
|
||||
<el-button @click="cancel" :disabled="isUploading">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitLoading || isUploading">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -596,6 +599,10 @@ const total = ref(0);
|
||||
const tableData = ref<MaterialBaseVO[]>([]);
|
||||
const tableRef = ref<InstanceType<typeof ElTable>>();
|
||||
const submitLoading = ref(false);
|
||||
|
||||
// 上传锁定状态
|
||||
const isUploading = ref(false);
|
||||
|
||||
const tableSize = ref<'large' | 'default' | 'small'>('large');
|
||||
const advancedFilterVisible = ref(false);
|
||||
const advancedConditions = ref([{ field: '', operator: '', value: '' }]);
|
||||
@ -1278,6 +1285,7 @@ const customUpload = async (options: any, targetField: 'generalImage' | 'general
|
||||
const { file, onSuccess, onError } = options
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
isUploading.value = true
|
||||
try {
|
||||
const res: any = await uploadFile(formData)
|
||||
if (res.code === 200) {
|
||||
@ -1293,6 +1301,7 @@ const customUpload = async (options: any, targetField: 'generalImage' | 'general
|
||||
ElMessage.error('网络错误');
|
||||
onError(e)
|
||||
}
|
||||
finally { isUploading.value = false }
|
||||
}
|
||||
|
||||
const handleRemoveImage = async (uploadFile: any, targetField: 'generalImage' | 'generalManual') => {
|
||||
|
||||
@ -251,7 +251,9 @@
|
||||
:width="'min(1000px, 95vw)'"
|
||||
top="4vh"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
:close-on-click-modal="!isUploading"
|
||||
:close-on-press-escape="!isUploading"
|
||||
:show-close="!isUploading"
|
||||
class="stylish-dialog compact-layout"
|
||||
>
|
||||
<div class="dialog-scroll-container">
|
||||
@ -618,8 +620,8 @@
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false" size="large">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">{{ dialogStatus === 'create' ? '确认入库并打印' : '保存修改' }}</el-button>
|
||||
<el-button @click="visible = false" size="large" :disabled="isUploading">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting || isUploading" @click="submitForm" size="large" class="confirm-btn">{{ dialogStatus === 'create' ? '确认入库并打印' : '保存修改' }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -782,6 +784,9 @@ const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const formRef = ref()
|
||||
|
||||
// 上传锁定状态
|
||||
const isUploading = ref(false)
|
||||
|
||||
const categoryOptions = ref<string[]>([])
|
||||
const typeOptions = ref<string[]>([])
|
||||
const companyOptions = ref<string[]>([])
|
||||
@ -1247,6 +1252,14 @@ watch(() => [form.in_quantity, form.unit_price], () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 联动优化:当入库数量变化时,自动同步打印份数(除非用户已手动修改过)
|
||||
watch(() => form.in_quantity, (newQty) => {
|
||||
if (dialogStatus.value === 'create' && newQty && newQty > 0) {
|
||||
// 仅在创建模式下自动同步
|
||||
form.print_copies = newQty
|
||||
}
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@ -1423,6 +1436,7 @@ const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspec
|
||||
const { file, onSuccess, onError } = options
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
isUploading.value = true
|
||||
try {
|
||||
const res: any = await uploadFile(formData)
|
||||
if (res.code === 200) {
|
||||
@ -1448,6 +1462,8 @@ const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspec
|
||||
} catch (e) {
|
||||
ElMessage.error('网络错误')
|
||||
onError(e)
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -231,7 +231,7 @@
|
||||
|
||||
<el-pagination class="pagination-bar" v-model:current-page="queryParams.page" v-model:page-size="queryParams.pageSize" :total="total" layout="total, sizes, prev, pager, next" background @change="fetchData" />
|
||||
|
||||
<el-dialog v-model="visible" :title="dialogStatus === 'create' ? '成品入库' : '编辑成品'" width="min(1000px, 95vw)" top="5vh" :close-on-click-modal="false" class="stylish-dialog compact-layout">
|
||||
<el-dialog v-model="visible" :title="dialogStatus === 'create' ? '成品入库' : '编辑成品'" width="min(1000px, 95vw)" top="5vh" :close-on-click-modal="!isUploading" :close-on-press-escape="!isUploading" :show-close="!isUploading" class="stylish-dialog compact-layout">
|
||||
<div class="dialog-scroll-container">
|
||||
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
||||
|
||||
@ -337,6 +337,11 @@
|
||||
<el-input-number v-model="form.in_quantity" :min="1" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="dialogStatus === 'create'">
|
||||
<el-form-item label="打印份数" prop="print_copies">
|
||||
<el-input-number v-model="form.print_copies" :min="1" :max="999" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
@ -488,8 +493,8 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false" size="large">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">
|
||||
<el-button @click="visible = false" size="large" :disabled="isUploading">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting || isUploading" @click="submitForm" size="large" class="confirm-btn">
|
||||
{{ dialogStatus === 'create' ? '提交并打印' : '保存修改' }}
|
||||
</el-button>
|
||||
</div>
|
||||
@ -514,7 +519,12 @@
|
||||
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
||||
<div v-else class="empty-preview">正在生成预览...</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px; font-size: 14px; color: #666;"><p>打印机 IP: 192.168.9.205</p><p>尺寸: 40mm x 30mm</p></div>
|
||||
<div style="margin-top: 20px; font-size: 14px; color: #666;"><p>打印机 IP: 192.168.9.205</p><p>尺寸: 40mm x 30mm</p>
|
||||
<div style="margin-top: 15px; display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||
<span style="font-weight: bold; color: #303133;">打印份数:</span>
|
||||
<el-input-number v-model="printCopies" :min="1" :max="100" size="default" style="width: 120px;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@ -599,6 +609,10 @@ const dialogStatus = ref<'create' | 'update'>('create')
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const formRef = ref()
|
||||
|
||||
// 上传锁定状态
|
||||
const isUploading = ref(false)
|
||||
|
||||
const queryParams = reactive({ page: 1, pageSize: 50, keyword: '', searchField: 'all', sku: '', category: '', material_type: '', statuses: ['在库', '借库'], company: '', orderByColumn: '', isAsc: '', advancedFilters: [] })
|
||||
const categoryOptions = ref<string[]>([])
|
||||
const typeOptions = ref<string[]>([])
|
||||
@ -664,6 +678,7 @@ const printVisible = ref(false)
|
||||
const printLoading = ref(false)
|
||||
const printing = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const printCopies = ref(1)
|
||||
const currentPrintData = ref<any>({})
|
||||
|
||||
// 图片/拍照相关
|
||||
@ -800,7 +815,7 @@ const form = reactive({
|
||||
company_name: '', // [新增]
|
||||
material_name: '', spec_model: '', material_type: '', category: '', unit: '',
|
||||
sku: '', barcode: '', serial_number: '', in_date: '',
|
||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1, print_copies: 1,
|
||||
warehouse_location: '', status: '在库', quality_status: '合格',
|
||||
bom_code: '', bom_version: '', work_order_code: '', order_id: '',
|
||||
production_manager: '', production_time_range: [] as string[],
|
||||
@ -1137,12 +1152,14 @@ const beforeAvatarUpload = (rawFile: any) => { if (rawFile.type !== 'image/jpeg'
|
||||
const customUpload = async (options: any, targetField: 'product_photo' | 'quality_report_link' | 'inspection_report_link') => {
|
||||
const { file, onSuccess, onError } = options
|
||||
const formData = new FormData(); formData.append('file', file)
|
||||
isUploading.value = true
|
||||
try {
|
||||
const res: any = await uploadFile(formData)
|
||||
if (res.code === 200) {
|
||||
const newUrl = res.data.url; form[targetField].push(newUrl); ElMessage.success('上传成功'); onSuccess(res)
|
||||
} else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) }
|
||||
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
||||
finally { isUploading.value = false }
|
||||
}
|
||||
const handleRemoveImage = async (uploadFile: any, targetField: 'product_photo' | 'quality_report_link' | 'inspection_report_link') => {
|
||||
try {
|
||||
@ -1241,7 +1258,7 @@ const submitForm = async () => {
|
||||
const res: any = await createProductInbound(payload)
|
||||
ElMessage.success('入库成功')
|
||||
const newItem = res.data
|
||||
if (newItem) { ElMessage.info('发送打印...'); try { await executePrint(newItem); ElMessage.success('指令已发送') } catch (e: any) { ElMessage.warning('打印失败') } }
|
||||
if (newItem) { ElMessage.info('发送打印...'); try { await executePrint({ ...newItem, copies: form.print_copies }); ElMessage.success(`指令已发送 (x${form.print_copies})`) } catch (e: any) { ElMessage.warning('打印失败') } }
|
||||
} else { await updateProductInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||
visible.value = false; fetchData()
|
||||
} catch(e:any) {
|
||||
@ -1257,10 +1274,10 @@ const handlePrint = async (row: any) => {
|
||||
currentPrintData.value = { global_print_id: row.global_print_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category, material_type: row.material_type, warehouse_loc: row.warehouse_loc, serial_number: row.serial_number, sku: row.sku }
|
||||
try { const res: any = await getLabelPreview(currentPrintData.value); previewUrl.value = res.data } catch (e) { ElMessage.error('预览失败') } finally { printLoading.value = false }
|
||||
}
|
||||
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('已发送'); printVisible.value = false } catch (e: any) { ElMessage.error('打印失败') } finally { printing.value = false } }
|
||||
const confirmPrint = async () => { printing.value = true; try { await executePrint({ ...currentPrintData.value, copies: printCopies.value }); ElMessage.success(`已发送 (x${printCopies.value})`); printVisible.value = false } catch (e: any) { ElMessage.error('打印失败') } finally { printing.value = false } }
|
||||
const resetForm = () => {
|
||||
materialOptions.value = []; bomOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
|
||||
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', status: '在库', quality_status: '合格', bom_code: '', bom_version: '', work_order_code: '', order_id: '', production_manager: '', production_time_range: [], raw_material_cost: undefined, unit_total_cost: undefined, total_price: undefined, sale_price: undefined, quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: '' })
|
||||
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', in_quantity: 1, stock_quantity: 1, available_quantity: 1, print_copies: 1, warehouse_location: '', status: '在库', quality_status: '合格', bom_code: '', bom_version: '', work_order_code: '', order_id: '', production_manager: '', production_time_range: [], raw_material_cost: undefined, unit_total_cost: undefined, total_price: undefined, sale_price: undefined, quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: '' })
|
||||
}
|
||||
const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库':'warning','损耗':'danger'}[s]||'warning')
|
||||
const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info')
|
||||
|
||||
@ -275,7 +275,9 @@
|
||||
width="min(1000px, 95vw)"
|
||||
top="5vh"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
:close-on-click-modal="!isUploading"
|
||||
:close-on-press-escape="!isUploading"
|
||||
:show-close="!isUploading"
|
||||
class="stylish-dialog compact-layout"
|
||||
>
|
||||
<div class="dialog-scroll-container">
|
||||
@ -419,6 +421,12 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6" v-if="dialogStatus === 'create'">
|
||||
<el-form-item label="打印份数" prop="print_copies">
|
||||
<el-input-number v-model="form.print_copies" :min="1" :max="999" controls-position="right" style="width:100%"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<template v-if="dialogStatus === 'update'">
|
||||
<el-col :span="6"><el-form-item label="当前库存" prop="stock_quantity"><el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false"/></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="当前可用" prop="available_quantity"><el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false"/></el-form-item></el-col>
|
||||
@ -562,8 +570,8 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false" size="large">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">
|
||||
<el-button @click="visible = false" size="large" :disabled="isUploading">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting || isUploading" @click="submitForm" size="large" class="confirm-btn">
|
||||
{{ dialogStatus === 'create' ? '提交并打印' : '保存修改' }}
|
||||
</el-button>
|
||||
</div>
|
||||
@ -584,7 +592,12 @@
|
||||
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
||||
<div v-else class="empty-preview">正在生成预览...</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px; font-size: 14px; color: #666;"><p>打印机 IP: 192.168.9.205</p><p>尺寸: 40mm x 30mm</p></div>
|
||||
<div style="margin-top: 20px; font-size: 14px; color: #666;"><p>打印机 IP: 192.168.9.205</p><p>尺寸: 40mm x 30mm</p>
|
||||
<div style="margin-top: 15px; display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||
<span style="font-weight: bold; color: #303133;">打印份数:</span>
|
||||
<el-input-number v-model="printCopies" :min="1" :max="100" size="default" style="width: 120px;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer"><el-button @click="printVisible = false">取消</el-button><el-button type="primary" :loading="printing" @click="confirmPrint"><el-icon><Printer/></el-icon>确认打印</el-button></div>
|
||||
@ -666,6 +679,10 @@ const dialogStatus = ref<'create' | 'update'>('create')
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const formRef = ref()
|
||||
|
||||
// 上传锁定状态
|
||||
const isUploading = ref(false)
|
||||
|
||||
const queryParams = reactive({ page: 1, pageSize: 50, keyword: '', searchField: 'all', sku: '', category: '', material_type: '', statuses: ['在库', '借库'], company: '', orderByColumn: '', isAsc: '', advancedFilters: [] })
|
||||
const categoryOptions = ref<string[]>([])
|
||||
const typeOptions = ref<string[]>([])
|
||||
@ -731,6 +748,7 @@ const printVisible = ref(false)
|
||||
const printLoading = ref(false)
|
||||
const printing = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const printCopies = ref(1)
|
||||
const currentPrintData = ref<any>({})
|
||||
|
||||
// 图片/拍照相关
|
||||
@ -875,7 +893,7 @@ watch(visibleColumnProps, (newVal) => {
|
||||
const form = reactive({
|
||||
id: undefined, base_id: undefined as number | undefined,
|
||||
company_name: '',
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '',
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, print_copies: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '',
|
||||
raw_material_cost: undefined as number | undefined,
|
||||
unit_total_cost: undefined as number | undefined,
|
||||
total_price: undefined as number | undefined,
|
||||
@ -1269,12 +1287,14 @@ const beforeAvatarUpload = (rawFile: any) => {
|
||||
const customUpload = async (options: any, targetField: 'arrival_photo' | 'quality_report_link') => {
|
||||
const { file, onSuccess, onError } = options
|
||||
const formData = new FormData(); formData.append('file', file)
|
||||
isUploading.value = true
|
||||
try {
|
||||
const res: any = await uploadFile(formData)
|
||||
if (res.code === 200) {
|
||||
const newUrl = res.data.url; form[targetField].push(newUrl); ElMessage.success('上传成功'); onSuccess(res)
|
||||
} else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) }
|
||||
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
||||
finally { isUploading.value = false }
|
||||
}
|
||||
const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'quality_report_link') => {
|
||||
try {
|
||||
@ -1362,7 +1382,7 @@ const submitForm = async () => {
|
||||
const newItem = res.data
|
||||
if (newItem) {
|
||||
ElMessage.info('正在发送打印指令...')
|
||||
try { await executePrint(newItem); ElMessage.success('打印指令已发送') }
|
||||
try { await executePrint({ ...newItem, copies: form.print_copies }); ElMessage.success(`打印指令已发送 (x${form.print_copies})`) }
|
||||
catch (printErr: any) { ElMessage.warning('入库成功,但自动打印失败:' + (printErr.msg || '未知错误')) }
|
||||
}
|
||||
} else { await updateSemiInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||
@ -1380,13 +1400,13 @@ const handlePrint = async (row: any) => {
|
||||
currentPrintData.value = { global_print_id: row.global_print_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category, material_type: row.material_type, warehouse_loc: row.warehouse_loc, serial_number: row.serial_number, batch_number: row.batch_number, sku: row.sku }
|
||||
try { const res: any = await getLabelPreview(currentPrintData.value); previewUrl.value = res.data } catch (e) { ElMessage.error('预览生成失败') } finally { printLoading.value = false }
|
||||
}
|
||||
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('指令已发送'); printVisible.value = false } catch (e: any) { ElMessage.error(e.msg || '打印失败') } finally { printing.value = false } }
|
||||
const confirmPrint = async () => { printing.value = true; try { await executePrint({ ...currentPrintData.value, copies: printCopies.value }); ElMessage.success(`指令已发送 (x${printCopies.value})`); printVisible.value = false } catch (e: any) { ElMessage.error(e.msg || '打印失败') } finally { printing.value = false } }
|
||||
const resetForm = () => {
|
||||
materialOptions.value = []; bomOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; quality_report_url.value = ''
|
||||
Object.assign(form, {
|
||||
id: undefined, base_id: undefined,
|
||||
company_name: '', // [新增]
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '',
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, print_copies: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '',
|
||||
raw_material_cost: undefined, unit_total_cost: undefined, total_price: undefined,
|
||||
production_manager: '', production_time_range: [], arrival_photo: [], quality_report_link: [], detail_link: '' })
|
||||
}
|
||||
|
||||
@ -25,6 +25,17 @@
|
||||
>
|
||||
继续上次盘点 <span class="sub-text">({{ serverDraftCount }}项)</span>
|
||||
</el-button>
|
||||
|
||||
<!-- ★ 新增: 查看差异报告按钮 -->
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
size="large"
|
||||
class="action-btn-full"
|
||||
@click="goToVarianceReview"
|
||||
>
|
||||
📋 差异审核 <span class="sub-text">(查看历史差异)</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="safe-tip">
|
||||
@ -247,37 +258,96 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showFinishDialog" title="📊 盘点结算" width="90%" align-center :close-on-click-modal="false" class="preview-dialog">
|
||||
<div class="report-summary">
|
||||
<div class="summary-row"><span>截止时间:</span><span>{{ new Date().toLocaleString() }}</span></div>
|
||||
<div class="summary-stats">
|
||||
<div class="s-item"><div class="num">{{ stats.total }}</div><div class="txt">总数</div></div>
|
||||
<div class="s-item success"><div class="num">{{ stats.scanned }}</div><div class="txt">已盘</div></div>
|
||||
<div class="s-item error"><div class="num">{{ stats.total - stats.scanned }}</div><div class="txt">未盘</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="missing-list-header">差异/未盘预览</div>
|
||||
<el-table :data="varianceList" height="300" border size="small" style="margin-bottom: 10px;">
|
||||
<el-table-column prop="name" label="名称" show-overflow-tooltip />
|
||||
<el-table-column prop="batch_no" label="批次" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="账/实" width="80" align="center">
|
||||
<template #default="scope">
|
||||
{{ parseFloat(scope.row.qty_stock) }} /
|
||||
<span :class="{'text-red': scope.row.scanned && scope.row.qty_actual !== scope.row.qty_stock}">
|
||||
{{ scope.row.scanned ? scope.row.qty_actual : '未' }}
|
||||
</span>
|
||||
<!-- ★ 新增: 盘点差异审核对话框 -->
|
||||
<el-dialog
|
||||
v-model="showVarianceDialog"
|
||||
title="📋 盘点差异审核"
|
||||
width="95%"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
class="variance-dialog"
|
||||
>
|
||||
<div v-loading="varianceLoading">
|
||||
<el-alert
|
||||
title="差异审核说明"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 15px;"
|
||||
>
|
||||
<template #default>
|
||||
以下列表显示所有已结束盘点但尚未平账的差异记录。请逐条核实后,点击"确认平账"按钮调整系统库存。
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-alert>
|
||||
|
||||
<el-table
|
||||
:data="varianceList"
|
||||
height="500"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="uuid" label="SKU/条码" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="stock_name" label="物品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="stock_spec" label="规格型号" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="stock_location" label="库位" width="100" />
|
||||
<el-table-column label="账面库存" width="80" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.stock_qty }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实盘数量" width="80" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.quantity }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="差异" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.row.diff_qty > 0 ? 'success' : 'danger'"
|
||||
size="large"
|
||||
effect="dark"
|
||||
>
|
||||
{{ scope.row.diff_qty > 0 ? '+' : '' }}{{ scope.row.diff_qty }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="差异类型" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.diff_qty > 0 ? 'success' : 'danger'">
|
||||
{{ scope.row.diff_qty > 0 ? '盘盈' : '盘亏' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.is_processed" type="info">已处理</el-tag>
|
||||
<el-tag v-else type="warning">待审核</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="!scope.row.is_processed && userStore.hasPermission('inventory_stocktake:operation')"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleAdjust(scope.row)"
|
||||
>
|
||||
确认平账
|
||||
</el-button>
|
||||
<span v-else class="text-gray">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-empty v-if="varianceList.length === 0" description="暂无待审核的差异记录" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="showFinishDialog = false">返回修改</el-button>
|
||||
<div class="footer-right">
|
||||
<el-button type="success" @click="exportToExcel" :icon="Download">导出Excel</el-button>
|
||||
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="danger" @click="finishStocktake" :loading="printing" :icon="Checked">结束</el-button>
|
||||
</div>
|
||||
<el-button @click="showVarianceDialog = false">关闭</el-button>
|
||||
<el-button type="success" @click="exportToExcel" :icon="Download">导出差异报告</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -337,10 +407,20 @@ const showList = ref(false)
|
||||
const showFinishDialog = ref(false)
|
||||
const showQtyDialog = ref(false)
|
||||
|
||||
// ★ 新增: 差异审核对话框
|
||||
const showVarianceDialog = ref(false)
|
||||
|
||||
const allData = ref<StockItem[]>([])
|
||||
const scannedMap = ref<Map<string, number>>(new Map())
|
||||
const borrowedQuantities = ref<Record<string, number>>({})
|
||||
|
||||
// ★ 新增: 会话ID
|
||||
const currentSessionId = ref<string>('')
|
||||
|
||||
// ★ 新增: 差异报告列表
|
||||
const varianceList = ref<any[]>([])
|
||||
const varianceLoading = ref(false)
|
||||
|
||||
const filterType = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
@ -349,9 +429,46 @@ const inputQty = ref<number | undefined>(undefined)
|
||||
const qtyInputRef = ref()
|
||||
|
||||
const api = {
|
||||
getDrafts: () => request({ url: '/v1/inbound/stock/draft/list', method: 'get', params: { user_id: currentUser } }),
|
||||
addDraft: (data: any) => request({ url: '/v1/inbound/stock/draft/add', method: 'post', data: { ...data, user_id: currentUser } }),
|
||||
clearDraft: () => request({ url: '/v1/inbound/stock/draft/clear', method: 'post', data: { user_id: currentUser } })
|
||||
getDrafts: (sessionId?: string) => request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
method: 'get',
|
||||
params: { user_id: currentUser, session_id: sessionId }
|
||||
}),
|
||||
addDraft: (data: any) => request({
|
||||
url: '/v1/inbound/stock/draft/add',
|
||||
method: 'post',
|
||||
data: { ...data, user_id: currentUser, session_id: currentSessionId.value }
|
||||
}),
|
||||
// ★ 新增: 开始新会话
|
||||
startNewSession: () => request({
|
||||
url: '/v1/inbound/stock/draft/start-new',
|
||||
method: 'post',
|
||||
data: { user_id: currentUser }
|
||||
}),
|
||||
// ★ 新增: 结束盘点
|
||||
finishStocktake: () => request({
|
||||
url: '/v1/inbound/stock/finish',
|
||||
method: 'post',
|
||||
data: { user_id: currentUser, session_id: currentSessionId.value }
|
||||
}),
|
||||
// ★ 新增: 获取差异报告
|
||||
getVarianceReport: () => request({
|
||||
url: '/v1/inbound/stock/variance-report',
|
||||
method: 'get',
|
||||
params: { user_id: currentUser }
|
||||
}),
|
||||
// ★ 新增: 单条库存调整
|
||||
adjustStock: (draftId: number, remark: string) => request({
|
||||
url: '/v1/inbound/stock/adjust',
|
||||
method: 'post',
|
||||
data: { draft_id: draftId, operator_name: currentUser, remark: remark }
|
||||
}),
|
||||
// ★ 保留清除功能(用于兼容性)
|
||||
clearDraft: () => request({
|
||||
url: '/v1/inbound/stock/draft/clear',
|
||||
method: 'post',
|
||||
data: { user_id: currentUser }
|
||||
})
|
||||
}
|
||||
|
||||
const typeToSourceTable = (type: string): string => {
|
||||
@ -388,31 +505,62 @@ onMounted(async () => {
|
||||
|
||||
const checkServerDraft = async () => {
|
||||
try {
|
||||
const res: any = await api.getDrafts()
|
||||
// 只获取未完成的草稿数量
|
||||
const res: any = await request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
method: 'get',
|
||||
params: { user_id: currentUser, is_finished: 'false' }
|
||||
})
|
||||
serverDraftCount.value = (res && res.length) || 0
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ★ 重写: 开始新盘点 - 使用新 API
|
||||
const startNewSession = async () => {
|
||||
try {
|
||||
if (serverDraftCount.value > 0) {
|
||||
await ElMessageBox.confirm('存在未完成记录,开始新盘点将清除它们,确定吗?', '警告', { type: 'warning' })
|
||||
}
|
||||
btnLoading.value = true
|
||||
await api.clearDraft()
|
||||
// 调用新 API 开始新会话
|
||||
const res: any = await api.startNewSession()
|
||||
currentSessionId.value = res.session_id || ''
|
||||
scannedMap.value.clear()
|
||||
await loadData()
|
||||
isSessionActive.value = true
|
||||
} catch (e) {} finally { btnLoading.value = false }
|
||||
ElMessage.success('新盘点会话已开始')
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel') {
|
||||
ElMessage.error(e?.message || '操作失败')
|
||||
}
|
||||
} finally { btnLoading.value = false }
|
||||
}
|
||||
|
||||
// ★ 重写: 继续上次盘点
|
||||
const resumeSession = async () => {
|
||||
btnLoading.value = true
|
||||
try {
|
||||
const drafts: any = await api.getDrafts()
|
||||
// 获取最新的未完成会话
|
||||
const drafts: any = await request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
method: 'get',
|
||||
params: { user_id: currentUser, is_finished: 'false' }
|
||||
})
|
||||
|
||||
if (!drafts || drafts.length === 0) {
|
||||
ElMessage.warning('没有找到未完成的盘点记录')
|
||||
return
|
||||
}
|
||||
|
||||
// 从草稿中获取 session_id
|
||||
const sessionIds = [...new Set(drafts.map((d: any) => d.session_id))]
|
||||
currentSessionId.value = sessionIds[0]
|
||||
|
||||
const map = new Map<string, number>()
|
||||
drafts.forEach((d: any) => {
|
||||
map.set(d.uuid, d.quantity !== undefined ? parseFloat(d.quantity) : 1)
|
||||
if (d.session_id === currentSessionId.value) {
|
||||
map.set(d.uuid, d.quantity !== undefined ? parseFloat(d.quantity) : 1)
|
||||
}
|
||||
})
|
||||
scannedMap.value = map
|
||||
await loadData()
|
||||
@ -695,22 +843,71 @@ const openFinishDialog = () => {
|
||||
|
||||
const finishStocktake = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要结束本次盘点吗?结束后当前进度将清空。', '结束确认', {
|
||||
await ElMessageBox.confirm('确定要结束本次盘点吗?结束后将进入差异审核流程。', '结束确认', {
|
||||
type: 'warning', confirmButtonText: '确定结束', cancelButtonText: '取消'
|
||||
})
|
||||
|
||||
printing.value = true
|
||||
await api.clearDraft()
|
||||
|
||||
// ★ 修改: 调用结束盘点 API,不再删除草稿
|
||||
const res: any = await api.finishStocktake()
|
||||
|
||||
// 结束会话
|
||||
scannedMap.value.clear()
|
||||
isSessionActive.value = false
|
||||
showFinishDialog.value = false
|
||||
checkServerDraft()
|
||||
|
||||
ElMessage.success('盘点已完成,会话已结束')
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') ElMessage.error('操作失败')
|
||||
ElMessage.success('盘点已结束,请查看差异报告进行审核')
|
||||
|
||||
// ★ 新增: 自动打开差异审核对话框
|
||||
await openVarianceDialog()
|
||||
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
|
||||
} finally { printing.value = false }
|
||||
}
|
||||
|
||||
// ★ 新增: 打开差异审核对话框
|
||||
const openVarianceDialog = async () => {
|
||||
varianceLoading.value = true
|
||||
showVarianceDialog.value = true
|
||||
try {
|
||||
const res: any = await api.getVarianceReport()
|
||||
varianceList.value = res.list || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '获取差异报告失败')
|
||||
} finally {
|
||||
varianceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 新增: 确认平账
|
||||
const handleAdjust = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要对 "${row.uuid}" 进行平账调整吗?\n\n差异: ${row.diff_qty > 0 ? '盘盈 +' : '盘亏 '}${row.diff_qty}`,
|
||||
'确认平账',
|
||||
{ type: 'warning', confirmButtonText: '确认调整', cancelButtonText: '取消' }
|
||||
)
|
||||
|
||||
const remark = `盘点差异调整 - ${row.diff_qty > 0 ? '盘盈入库' : '盘亏出库'}`
|
||||
|
||||
const res: any = await api.adjustStock(row.id, remark)
|
||||
|
||||
ElMessage.success(res.message || '调整成功')
|
||||
|
||||
// 刷新差异列表
|
||||
await openVarianceDialog()
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 新增: 跳转到差异审核页面
|
||||
const goToVarianceReview = () => {
|
||||
openVarianceDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user