refactor: redesign stocktake flow to require manual discrepancy audit and individual adjustments

This commit is contained in:
DXC
2026-03-13 09:59:01 +08:00
parent 9b290506da
commit 7e23141870
3 changed files with 654 additions and 56 deletions

View File

@ -3,10 +3,12 @@ from app.extensions import db
# ★★★ 修复点:必须引入 datetime否则下方更新时间时会报错 500 ★★★ # ★★★ 修复点:必须引入 datetime否则下方更新时间时会报错 500 ★★★
from datetime import datetime from datetime import datetime
from app.utils.decorators import permission_required from app.utils.decorators import permission_required
import uuid as uuid_module
# 导入模型 # 导入模型
from app.models.inbound.buy import StockBuy from app.models.inbound.buy import StockBuy
from app.models.inbound.stocktake import StocktakeDraft from app.models.inbound.stocktake import StocktakeDraft
from app.models.transaction import TransBorrow
# 尝试导入半成品和成品 # 尝试导入半成品和成品
try: try:
@ -24,6 +26,52 @@ from app.services.print.network_print_service import NetworkPrintService
bp = Blueprint('stock_ops', __name__) 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']) @bp.route('/all', methods=['GET'])
@permission_required('inventory_stocktake') @permission_required('inventory_stocktake')
def get_all_stock(): def get_all_stock():
@ -67,59 +115,379 @@ def get_all_stock():
@bp.route('/draft/list', methods=['GET']) @bp.route('/draft/list', methods=['GET'])
@permission_required('inventory_stocktake') @permission_required('inventory_stocktake')
def get_drafts(): def get_drafts():
"""获取当前用户的盘点进度""" """
获取当前用户的盘点进度
支持过滤: session_id, is_finished, is_processed
"""
user_id = request.args.get('user_id', 'admin') 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 return jsonify([d.to_dict() for d in drafts]), 200
@bp.route('/draft/add', methods=['POST']) @bp.route('/draft/add', methods=['POST'])
@permission_required('inventory_stocktake:operation') @permission_required('inventory_stocktake:operation')
def add_draft(): def add_draft():
"""扫码同步 (支持更新数量)""" """
扫码同步 (支持更新数量)
如果 session_id 不存在则创建新的会话
"""
try: try:
data = request.json data = request.json
user_id = data.get('user_id', 'admin') user_id = data.get('user_id', 'admin')
uuid = data.get('uuid') 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: if draft:
# 如果已存在,更新数量和时间 # 如果已存在,更新数量和时间
draft.quantity = quantity draft.quantity = quantity
# ★ 修复点:这里需要 datetime 对象
draft.scan_time = datetime.now() 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: 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.add(draft)
db.session.commit() 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: except Exception as e:
print(f"Add Draft Error: {e}") print(f"Add Draft Error: {e}")
db.session.rollback()
return jsonify({"message": str(e)}), 500 return jsonify({"message": str(e)}), 500
@bp.route('/draft/clear', methods=['POST']) @bp.route('/draft/clear', methods=['POST'])
@permission_required('inventory_stocktake:operation') @permission_required('inventory_stocktake:operation')
def clear_draft(): 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 data = request.json
user_id = data.get('user_id', 'admin') user_id = data.get('user_id', 'admin')
StocktakeDraft.query.filter_by(user_id=user_id).delete() try:
db.session.commit() # 清除旧的未处理盘点数据
return jsonify({"message": "Cleared"}), 200 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']) @bp.route('/borrowed-quantities', methods=['POST'])
@permission_required('inventory_stocktake') @permission_required('inventory_stocktake')
def get_borrowed_quantities(): def get_borrowed_quantities():
"""批量获取借出未还数量""" """批量获取借出未还数量"""
from app.models.transaction import TransBorrow
data = request.json.get('items', []) data = request.json.get('items', [])
result = {} result = {}
for item in data: for item in data:

View File

@ -1,22 +1,55 @@
from app.extensions import db, beijing_time # .material -> .base refactor checked from app.extensions import db, beijing_time # .material -> .base refactor checked
from datetime import datetime from datetime import datetime
class StocktakeDraft(db.Model): class StocktakeDraft(db.Model):
"""
盘点草稿表
支持多轮盘点,保留历史记录
"""
__tablename__ = 'stocktake_draft' __tablename__ = 'stocktake_draft'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.String(100), default='admin') user_id = db.Column(db.String(100), default='admin')
# 关联的库存UUID (sku/barcode)
uuid = db.Column(db.String(100)) uuid = db.Column(db.String(100))
# ★ 新增 quantity 字段 # 实际盘点数量
quantity = db.Column(db.Numeric(19, 4), default=1) quantity = db.Column(db.Numeric(19, 4), default=1)
scan_time = db.Column(db.DateTime, default=beijing_time) 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): def to_dict(self):
return { return {
'id': self.id, 'id': self.id,
'user_id': self.user_id, 'user_id': self.user_id,
'uuid': self.uuid, 'uuid': self.uuid,
# ★ 返回 quantity
'quantity': float(self.quantity or 1), '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
} }

View File

@ -25,6 +25,17 @@
> >
继续上次盘点 <span class="sub-text">({{ serverDraftCount }})</span> 继续上次盘点 <span class="sub-text">({{ serverDraftCount }})</span>
</el-button> </el-button>
<!-- 新增: 查看差异报告按钮 -->
<el-button
type="info"
plain
size="large"
class="action-btn-full"
@click="goToVarianceReview"
>
📋 差异审核 <span class="sub-text">(查看历史差异)</span>
</el-button>
</div> </div>
<div class="safe-tip"> <div class="safe-tip">
@ -247,37 +258,96 @@
</div> </div>
</div> </div>
<el-dialog v-model="showFinishDialog" title="📊 盘点结算" width="90%" align-center :close-on-click-modal="false" class="preview-dialog"> <!-- 新增: 盘点差异审核对话框 -->
<div class="report-summary"> <el-dialog
<div class="summary-row"><span>截止时间:</span><span>{{ new Date().toLocaleString() }}</span></div> v-model="showVarianceDialog"
<div class="summary-stats"> title="📋 盘点差异审核"
<div class="s-item"><div class="num">{{ stats.total }}</div><div class="txt">总数</div></div> width="95%"
<div class="s-item success"><div class="num">{{ stats.scanned }}</div><div class="txt">已盘</div></div> align-center
<div class="s-item error"><div class="num">{{ stats.total - stats.scanned }}</div><div class="txt">未盘</div></div> :close-on-click-modal="false"
</div> destroy-on-close
</div> class="variance-dialog"
>
<div class="missing-list-header">差异/未盘预览</div> <div v-loading="varianceLoading">
<el-table :data="varianceList" height="300" border size="small" style="margin-bottom: 10px;"> <el-alert
<el-table-column prop="name" label="名称" show-overflow-tooltip /> title="差异审核说明"
<el-table-column prop="batch_no" label="批次" width="90" show-overflow-tooltip /> type="info"
<el-table-column label="账/实" width="80" align="center"> :closable="false"
<template #default="scope"> show-icon
{{ parseFloat(scope.row.qty_stock) }} / style="margin-bottom: 15px;"
<span :class="{'text-red': scope.row.scanned && scope.row.qty_actual !== scope.row.qty_stock}"> >
{{ scope.row.scanned ? scope.row.qty_actual : '未' }} <template #default>
</span> 以下列表显示所有已结束盘点但尚未平账的差异记录请逐条核实后点击"确认平账"按钮调整系统库存
</template> </template>
</el-table-column> </el-alert>
</el-table>
<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> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="showFinishDialog = false">返回修改</el-button> <el-button @click="showVarianceDialog = false">关闭</el-button>
<div class="footer-right"> <el-button type="success" @click="exportToExcel" :icon="Download">导出差异报告</el-button>
<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>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -337,10 +407,20 @@ const showList = ref(false)
const showFinishDialog = ref(false) const showFinishDialog = ref(false)
const showQtyDialog = ref(false) const showQtyDialog = ref(false)
// ★ 新增: 差异审核对话框
const showVarianceDialog = ref(false)
const allData = ref<StockItem[]>([]) const allData = ref<StockItem[]>([])
const scannedMap = ref<Map<string, number>>(new Map()) const scannedMap = ref<Map<string, number>>(new Map())
const borrowedQuantities = ref<Record<string, number>>({}) const borrowedQuantities = ref<Record<string, number>>({})
// ★ 新增: 会话ID
const currentSessionId = ref<string>('')
// ★ 新增: 差异报告列表
const varianceList = ref<any[]>([])
const varianceLoading = ref(false)
const filterType = ref('all') const filterType = ref('all')
const searchKeyword = ref('') const searchKeyword = ref('')
@ -349,9 +429,46 @@ const inputQty = ref<number | undefined>(undefined)
const qtyInputRef = ref() const qtyInputRef = ref()
const api = { const api = {
getDrafts: () => request({ url: '/v1/inbound/stock/draft/list', method: 'get', params: { user_id: currentUser } }), getDrafts: (sessionId?: string) => request({
addDraft: (data: any) => request({ url: '/v1/inbound/stock/draft/add', method: 'post', data: { ...data, user_id: currentUser } }), url: '/v1/inbound/stock/draft/list',
clearDraft: () => request({ url: '/v1/inbound/stock/draft/clear', method: 'post', data: { user_id: currentUser } }) 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 => { const typeToSourceTable = (type: string): string => {
@ -388,31 +505,62 @@ onMounted(async () => {
const checkServerDraft = async () => { const checkServerDraft = async () => {
try { 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 serverDraftCount.value = (res && res.length) || 0
} catch (e) {} } catch (e) {}
} }
// ★ 重写: 开始新盘点 - 使用新 API
const startNewSession = async () => { const startNewSession = async () => {
try { try {
if (serverDraftCount.value > 0) { if (serverDraftCount.value > 0) {
await ElMessageBox.confirm('存在未完成记录,开始新盘点将清除它们,确定吗?', '警告', { type: 'warning' }) await ElMessageBox.confirm('存在未完成记录,开始新盘点将清除它们,确定吗?', '警告', { type: 'warning' })
} }
btnLoading.value = true btnLoading.value = true
await api.clearDraft() // 调用新 API 开始新会话
const res: any = await api.startNewSession()
currentSessionId.value = res.session_id || ''
scannedMap.value.clear() scannedMap.value.clear()
await loadData() await loadData()
isSessionActive.value = true 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 () => { const resumeSession = async () => {
btnLoading.value = true btnLoading.value = true
try { 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>() const map = new Map<string, number>()
drafts.forEach((d: any) => { 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 scannedMap.value = map
await loadData() await loadData()
@ -695,22 +843,71 @@ const openFinishDialog = () => {
const finishStocktake = async () => { const finishStocktake = async () => {
try { try {
await ElMessageBox.confirm('确定要结束本次盘点吗?结束后当前进度将清空。', '结束确认', { await ElMessageBox.confirm('确定要结束本次盘点吗?结束后将进入差异审核流程。', '结束确认', {
type: 'warning', confirmButtonText: '确定结束', cancelButtonText: '取消' type: 'warning', confirmButtonText: '确定结束', cancelButtonText: '取消'
}) })
printing.value = true printing.value = true
await api.clearDraft()
// ★ 修改: 调用结束盘点 API不再删除草稿
const res: any = await api.finishStocktake()
// 结束会话
scannedMap.value.clear() scannedMap.value.clear()
isSessionActive.value = false isSessionActive.value = false
showFinishDialog.value = false showFinishDialog.value = false
checkServerDraft() checkServerDraft()
ElMessage.success('盘点已完成,会话已结束') ElMessage.success('盘点已结束,请查看差异报告进行审核')
} catch (e) {
if (e !== 'cancel') ElMessage.error('操作失败') // ★ 新增: 自动打开差异审核对话框
await openVarianceDialog()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
} finally { printing.value = false } } 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> </script>
<style scoped> <style scoped>