refactor: redesign stocktake flow to require manual discrepancy audit and individual adjustments
This commit is contained in:
@ -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:
|
||||
|
||||
Reference in New Issue
Block a user