Compare commits

4 Commits

3 changed files with 63 additions and 140 deletions

View File

@ -10,6 +10,14 @@ 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 from app.models.transaction import TransBorrow
def _normalize_user_id(user_id):
"""规范化 user_id确保是有效字符串"""
if not user_id or not isinstance(user_id, str) or len(user_id) > 100:
return 'admin'
return user_id.strip()
# 尝试导入半成品和成品 # 尝试导入半成品和成品
try: try:
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
@ -117,21 +125,14 @@ def get_all_stock():
def get_drafts(): def get_drafts():
""" """
获取当前用户的盘点进度 获取当前用户的盘点进度
支持过滤: session_id, is_finished, is_processed 支持过滤: session_id
""" """
user_id = request.args.get('user_id', 'admin')
session_id = request.args.get('session_id') 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) query = StocktakeDraft.query
if session_id: if session_id:
query = query.filter_by(session_id=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() 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
@ -146,7 +147,7 @@ def add_draft():
""" """
try: try:
data = request.json data = request.json
user_id = data.get('user_id', 'admin') user_id = _normalize_user_id(data.get('user_id', 'admin'))
uuid = data.get('uuid') uuid = data.get('uuid')
quantity = float(data.get('quantity', 1)) quantity = float(data.get('quantity', 1))
session_id = data.get('session_id') session_id = data.get('session_id')
@ -186,9 +187,7 @@ def add_draft():
stock_qty=stock_qty, stock_qty=stock_qty,
diff_qty=quantity - stock_qty, diff_qty=quantity - stock_qty,
source_table=source_table, source_table=source_table,
stock_id=stock_id, stock_id=stock_id
is_finished=False,
is_processed=False
) )
db.session.add(draft) db.session.add(draft)
@ -209,21 +208,17 @@ def add_draft():
def clear_draft(): def clear_draft():
""" """
清除盘点草稿 清除盘点草稿
支持清除指定 session_id 的记录,或清除所有未完成的记录 支持清除指定 session_id 的记录,或清除所有记录
""" """
data = request.json data = request.json
user_id = data.get('user_id', 'admin')
session_id = data.get('session_id') session_id = data.get('session_id')
try: try:
query = StocktakeDraft.query.filter_by(user_id=user_id) query = StocktakeDraft.query
if session_id: if session_id:
# 清除指定会话 # 清除指定会话
query = query.filter_by(session_id=session_id) query = query.filter_by(session_id=session_id)
else:
# 默认只清除未完成的记录
query = query.filter_by(is_finished=False)
count = query.delete() count = query.delete()
db.session.commit() db.session.commit()
@ -239,28 +234,20 @@ def clear_draft():
def start_new_session(): def start_new_session():
""" """
开始新一轮盘点 开始新一轮盘点
1. 先清除用户所有未处理的旧盘点数据 清空整张草稿表,返回新的 session_id
2. 返回新的 session_id
""" """
data = request.json
user_id = data.get('user_id', 'admin')
try: try:
# 清除旧的未处理盘点数据 # 清空整张草稿表
old_count = StocktakeDraft.query.filter_by( deleted_count = StocktakeDraft.query.delete()
user_id=user_id,
is_processed=False
).delete()
db.session.commit() db.session.commit()
# 生成新的 session_id # 生成新的 session_id
new_session_id = f"STK-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid_module.uuid4().hex[:6]}" new_session_id = f"STK-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid_module.uuid4().hex[:6]}"
return jsonify({ return jsonify({
"message": f"已清除 {old_count} 条旧记录", "message": f"已清除 {deleted_count} 条旧记录",
"session_id": new_session_id, "session_id": new_session_id,
"cleared_count": old_count "cleared_count": deleted_count
}), 200 }), 200
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
@ -274,52 +261,13 @@ def start_new_session():
def finish_stocktake(): def finish_stocktake():
""" """
结束盘点 结束盘点
1. 将指定 session 的草稿标记为 is_finished=True 直接返回成功,前端会跳转到差异列表
2. 计算差异数量 草稿数据保留在表中
3. 不删除任何草稿数据,保留历史
""" """
data = request.json return jsonify({
user_id = data.get('user_id', 'admin') "message": "盘点已结束",
session_id = data.get('session_id') "code": 200
}), 200
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']) @bp.route('/variance-report', methods=['GET'])
@ -327,25 +275,19 @@ def finish_stocktake():
def get_variance_report(): def get_variance_report():
""" """
获取盘点差异报告 获取盘点差异报告
返回所有 is_finished=True 且 is_processed=False 的记录 返回所有有差异的记录diff_qty != 0
即:已结束盘点但尚未手动平账的差异记录
""" """
user_id = request.args.get('user_id', 'admin')
session_id = request.args.get('session_id') session_id = request.args.get('session_id')
try: try:
query = StocktakeDraft.query.filter_by( query = StocktakeDraft.query
user_id=user_id,
is_finished=True,
is_processed=False
)
if session_id: if session_id:
query = query.filter_by(session_id=session_id) query = query.filter_by(session_id=session_id)
# 只返回有差异的记录 # 只返回有差异的记录
drafts = query.filter(StocktakeDraft.diff_qty != 0).order_by( drafts = query.filter(StocktakeDraft.diff_qty != 0).order_by(
StocktakeDraft.finish_time.desc(), StocktakeDraft.scan_time.desc(),
StocktakeDraft.diff_qty.desc() StocktakeDraft.diff_qty.desc()
).all() ).all()
@ -402,12 +344,6 @@ def adjust_stock():
if not draft: if not draft:
return jsonify({"message": "草稿记录不存在"}), 404 return jsonify({"message": "草稿记录不存在"}), 404
if not draft.is_finished:
return jsonify({"message": "该记录尚未结束盘点,无法调整"}), 400
if draft.is_processed:
return jsonify({"message": "该记录已处理过平账"}), 400
# 2. 获取库存记录 # 2. 获取库存记录
if not draft.stock_id or not draft.source_table: if not draft.stock_id or not draft.source_table:
return jsonify({"message": "草稿记录缺少库存关联信息"}), 400 return jsonify({"message": "草稿记录缺少库存关联信息"}), 400
@ -463,9 +399,8 @@ def adjust_stock():
) )
db.session.add(trans_record) db.session.add(trans_record)
# 6. 标记草稿为已处理 # 6. 删除草稿记录(平账后从工作台移除)
draft.is_processed = True db.session.delete(draft)
draft.processed_time = datetime.now()
db.session.commit() db.session.commit()

View File

@ -5,7 +5,7 @@ from datetime import datetime
class StocktakeDraft(db.Model): class StocktakeDraft(db.Model):
""" """
盘点草稿表 盘点草稿表
支持多轮盘点,保留历史记录 临时工作台,盘点时使用,会话结束后清空
""" """
__tablename__ = 'stocktake_draft' __tablename__ = 'stocktake_draft'
@ -16,25 +16,16 @@ class StocktakeDraft(db.Model):
# 实际盘点数量 # 实际盘点数量
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)) session_id = db.Column(db.String(100))
# ★ 新增: 是否已结束盘点 # 关联的库存类型 (stock_buy/stock_semi/stock_product)
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)) source_table = db.Column(db.String(50))
# ★ 新增: 关联的库存ID # 关联的库存ID
stock_id = db.Column(db.Integer) stock_id = db.Column(db.Integer)
# 账面库存数量 (记录盘点时的账面数量)
stock_qty = db.Column(db.Numeric(19, 4), default=0)
# 差异数量 (实盘 - 账面, 正=盘盈, 负=盘亏)
diff_qty = db.Column(db.Numeric(19, 4), default=0)
def to_dict(self): def to_dict(self):
return { return {
@ -44,12 +35,8 @@ class StocktakeDraft(db.Model):
'quantity': float(self.quantity or 1), 'quantity': float(self.quantity or 1),
'scan_time': self.scan_time.strftime('%Y-%m-%d %H:%M:%S') if self.scan_time else None, 'scan_time': self.scan_time.strftime('%Y-%m-%d %H:%M:%S') if self.scan_time else None,
'session_id': self.session_id, '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, 'source_table': self.source_table,
'stock_id': self.stock_id 'stock_id': self.stock_id,
'stock_qty': float(self.stock_qty or 0),
'diff_qty': float(self.diff_qty or 0)
} }

View File

@ -417,8 +417,6 @@ const borrowedQuantities = ref<Record<string, number>>({})
// ★ 新增: 会话ID // ★ 新增: 会话ID
const currentSessionId = ref<string>('') const currentSessionId = ref<string>('')
// ★ 新增: 差异报告列表
const varianceList = ref<any[]>([])
const varianceLoading = ref(false) const varianceLoading = ref(false)
const filterType = ref('all') const filterType = ref('all')
@ -432,30 +430,30 @@ const api = {
getDrafts: (sessionId?: string) => request({ getDrafts: (sessionId?: string) => request({
url: '/v1/inbound/stock/draft/list', url: '/v1/inbound/stock/draft/list',
method: 'get', method: 'get',
params: { user_id: currentUser, session_id: sessionId } params: { session_id: sessionId }
}), }),
addDraft: (data: any) => request({ addDraft: (data: any) => request({
url: '/v1/inbound/stock/draft/add', url: '/v1/inbound/stock/draft/add',
method: 'post', method: 'post',
data: { ...data, user_id: currentUser, session_id: currentSessionId.value } data: { ...data, session_id: currentSessionId.value }
}), }),
// ★ 新增: 开始新会话 // ★ 新增: 开始新会话
startNewSession: () => request({ startNewSession: () => request({
url: '/v1/inbound/stock/draft/start-new', url: '/v1/inbound/stock/draft/start-new',
method: 'post', method: 'post',
data: { user_id: currentUser } data: {}
}), }),
// ★ 新增: 结束盘点 // ★ 新增: 结束盘点
finishStocktake: () => request({ finishStocktake: () => request({
url: '/v1/inbound/stock/finish', url: '/v1/inbound/stock/finish',
method: 'post', method: 'post',
data: { user_id: currentUser, session_id: currentSessionId.value } data: { session_id: currentSessionId.value }
}), }),
// ★ 新增: 获取差异报告 // ★ 新增: 获取差异报告
getVarianceReport: () => request({ getVarianceReport: () => request({
url: '/v1/inbound/stock/variance-report', url: '/v1/inbound/stock/variance-report',
method: 'get', method: 'get',
params: { user_id: currentUser } params: {}
}), }),
// ★ 新增: 单条库存调整 // ★ 新增: 单条库存调整
adjustStock: (draftId: number, remark: string) => request({ adjustStock: (draftId: number, remark: string) => request({
@ -467,7 +465,7 @@ const api = {
clearDraft: () => request({ clearDraft: () => request({
url: '/v1/inbound/stock/draft/clear', url: '/v1/inbound/stock/draft/clear',
method: 'post', method: 'post',
data: { user_id: currentUser } data: {}
}) })
} }
@ -509,7 +507,7 @@ const checkServerDraft = 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: { user_id: currentUser, is_finished: 'false' } params: { is_finished: 'false' }
}) })
serverDraftCount.value = (res && res.length) || 0 serverDraftCount.value = (res && res.length) || 0
} catch (e) {} } catch (e) {}
@ -544,7 +542,7 @@ const resumeSession = async () => {
const drafts: any = await request({ const drafts: any = await request({
url: '/v1/inbound/stock/draft/list', url: '/v1/inbound/stock/draft/list',
method: 'get', method: 'get',
params: { user_id: currentUser, is_finished: 'false' } params: { is_finished: 'false' }
}) })
if (!drafts || drafts.length === 0) { if (!drafts || drafts.length === 0) {
@ -830,9 +828,18 @@ const stats = computed(() => {
}) })
const varianceList = computed(() => { const varianceList = computed(() => {
return allData.value.filter(i => return allData.value
!i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any)) .filter(i => !i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any)))
) .map(i => ({
...i,
// 映射字段名以匹配模板
stock_name: i.name,
stock_spec: i.standard,
stock_location: i.location || i.warehouse_loc || '',
stock_qty: i.qty_stock,
quantity: i.qty_actual,
diff_qty: i.qty_actual - parseFloat(i.qty_stock as any)
}))
}) })
const openInventoryList = () => { showList.value = true } const openInventoryList = () => { showList.value = true }
@ -872,14 +879,8 @@ const finishStocktake = async () => {
const openVarianceDialog = async () => { const openVarianceDialog = async () => {
varianceLoading.value = true varianceLoading.value = true
showVarianceDialog.value = true showVarianceDialog.value = true
try { // varianceList 已通过 computed 自动计算,无需额外 API 调用
const res: any = await api.getVarianceReport() varianceLoading.value = false
varianceList.value = res.list || []
} catch (e: any) {
ElMessage.error(e?.message || '获取差异报告失败')
} finally {
varianceLoading.value = false
}
} }
// ★ 新增: 确认平账 // ★ 新增: 确认平账