220 lines
7.9 KiB
Python
220 lines
7.9 KiB
Python
|
|
# inventory-backend/app/api/v1/scan_draft.py
|
|||
|
|
"""
|
|||
|
|
扫码草稿接口(出库 / 借库共用)。
|
|||
|
|
|
|||
|
|
场景
|
|||
|
|
----
|
|||
|
|
扫码作业可能很长(一张单几十项),工人常需中途暂停去处理更紧急的单据。
|
|||
|
|
改造前切换单据会清空已扫内容,刷新/退出页面则全部丢失。
|
|||
|
|
|
|||
|
|
由于库存在申请审批通过时已**预占**,暂停期间货不会被他人抢走 ——
|
|||
|
|
因此草稿只记录「扫到哪了」,**不涉及任何库存操作**。即使草稿丢失也只是
|
|||
|
|
需要重扫,不会造成库存错乱。
|
|||
|
|
|
|||
|
|
隔离
|
|||
|
|
----
|
|||
|
|
按 (user_id, biz_type, request_id) 隔离:一人一单,互不影响。
|
|||
|
|
user_id 一律取自 JWT,**不接受入参覆盖**,因此不可能读写他人的草稿。
|
|||
|
|
"""
|
|||
|
|
from flask import Blueprint, request, jsonify
|
|||
|
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|||
|
|
import traceback
|
|||
|
|
|
|||
|
|
from app.extensions import db
|
|||
|
|
from app.models.scan_draft import ScanDraft
|
|||
|
|
|
|||
|
|
scan_draft_bp = Blueprint('scan_draft', __name__)
|
|||
|
|
|
|||
|
|
BIZ_TYPES = {'outbound', 'borrow'}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _parse_identity():
|
|||
|
|
identity = get_jwt_identity()
|
|||
|
|
if not identity:
|
|||
|
|
return None, None, None, '用户未登录'
|
|||
|
|
|
|||
|
|
biz_type = (request.args.get('biz_type') or '').strip()
|
|||
|
|
if biz_type not in BIZ_TYPES:
|
|||
|
|
return None, None, None, f"biz_type 无效(仅支持 {', '.join(sorted(BIZ_TYPES))})"
|
|||
|
|
|
|||
|
|
request_id = request.args.get('request_id', type=int)
|
|||
|
|
if not request_id:
|
|||
|
|
return None, None, None, 'request_id 不能为空'
|
|||
|
|
|
|||
|
|
return int(identity), biz_type, request_id, None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _find(user_id, biz_type, request_id):
|
|||
|
|
return ScanDraft.query.filter_by(
|
|||
|
|
user_id=user_id, biz_type=biz_type, request_id=request_id
|
|||
|
|
).first()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------
|
|||
|
|
# 读取草稿:GET /api/v1/scan-draft?biz_type=outbound&request_id=123
|
|||
|
|
# --------------------------------------------------------
|
|||
|
|
@scan_draft_bp.route('', methods=['GET'])
|
|||
|
|
@jwt_required()
|
|||
|
|
def get_scan_draft():
|
|||
|
|
"""
|
|||
|
|
读取当前用户在某张单据上的扫码草稿。
|
|||
|
|
|
|||
|
|
返回 { items: [...完整购物车快照...], item_count, total_qty, updated_at }
|
|||
|
|
无草稿时 items 为空数组(非 404,便于前端直接使用)。
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
user_id, biz_type, request_id, err = _parse_identity()
|
|||
|
|
if err:
|
|||
|
|
return jsonify({'code': 400, 'msg': err}), 400
|
|||
|
|
|
|||
|
|
draft = _find(user_id, biz_type, request_id)
|
|||
|
|
items = draft.get_items() if draft else []
|
|||
|
|
|
|||
|
|
return jsonify({
|
|||
|
|
'code': 200, 'msg': 'success',
|
|||
|
|
'data': {
|
|||
|
|
'items': items,
|
|||
|
|
'item_count': len(items),
|
|||
|
|
'total_qty': round(sum(float(i.get('out_quantity') or 0) for i in items), 4),
|
|||
|
|
'updated_at': (
|
|||
|
|
draft.updated_at.strftime('%Y-%m-%d %H:%M:%S')
|
|||
|
|
if draft and draft.updated_at else None
|
|||
|
|
),
|
|||
|
|
}
|
|||
|
|
}), 200
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
traceback.print_exc()
|
|||
|
|
return jsonify({'code': 500, 'msg': f'读取草稿失败: {str(e)}'}), 500
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------
|
|||
|
|
# 保存草稿:POST /api/v1/scan-draft
|
|||
|
|
# Body: { biz_type, request_id, request_no?, items: [...完整购物车快照...] }
|
|||
|
|
#
|
|||
|
|
# ★ 全量覆盖语义:提交的 items 即当前完整清单。
|
|||
|
|
# 前端把 cartItems 整体发过来即可,不必逐条比对增删 —— 逻辑单一,
|
|||
|
|
# 也不会出现「已移除的物料在草稿里阴魂不散」。
|
|||
|
|
#
|
|||
|
|
# ★ 注意:items 为空时会**删除**该单据的草稿(而非存一个空草稿)。
|
|||
|
|
# 这符合「清空列表 = 放弃这次作业」的直觉,也避免残留空记录影响徽标。
|
|||
|
|
# --------------------------------------------------------
|
|||
|
|
@scan_draft_bp.route('', methods=['POST'])
|
|||
|
|
@jwt_required()
|
|||
|
|
def save_scan_draft():
|
|||
|
|
try:
|
|||
|
|
identity = get_jwt_identity()
|
|||
|
|
if not identity:
|
|||
|
|
return jsonify({'code': 401, 'msg': '用户未登录'}), 401
|
|||
|
|
user_id = int(identity)
|
|||
|
|
|
|||
|
|
data = request.get_json(silent=True) or {}
|
|||
|
|
biz_type = (data.get('biz_type') or '').strip()
|
|||
|
|
if biz_type not in BIZ_TYPES:
|
|||
|
|
return jsonify({'code': 400, 'msg': f"biz_type 无效(仅支持 {', '.join(sorted(BIZ_TYPES))})"}), 400
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
request_id = int(data.get('request_id'))
|
|||
|
|
except (TypeError, ValueError):
|
|||
|
|
return jsonify({'code': 400, 'msg': 'request_id 无效'}), 400
|
|||
|
|
|
|||
|
|
request_no = (data.get('request_no') or '')[:100]
|
|||
|
|
items = data.get('items') or []
|
|||
|
|
|
|||
|
|
draft = _find(user_id, biz_type, request_id)
|
|||
|
|
|
|||
|
|
# 空清单 → 删除草稿
|
|||
|
|
if not items:
|
|||
|
|
if draft:
|
|||
|
|
db.session.delete(draft)
|
|||
|
|
db.session.commit()
|
|||
|
|
return jsonify({'code': 200, 'msg': 'success',
|
|||
|
|
'data': {'saved': 0, 'cleared': True}}), 200
|
|||
|
|
|
|||
|
|
if draft is None:
|
|||
|
|
draft = ScanDraft(user_id=user_id, biz_type=biz_type, request_id=request_id)
|
|||
|
|
db.session.add(draft)
|
|||
|
|
|
|||
|
|
draft.request_no = request_no
|
|||
|
|
draft.set_items(items)
|
|||
|
|
|
|||
|
|
db.session.commit()
|
|||
|
|
return jsonify({
|
|||
|
|
'code': 200, 'msg': 'success',
|
|||
|
|
'data': {'saved': len(items)}
|
|||
|
|
}), 200
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
db.session.rollback()
|
|||
|
|
traceback.print_exc()
|
|||
|
|
return jsonify({'code': 500, 'msg': f'保存草稿失败: {str(e)}'}), 500
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------
|
|||
|
|
# 清除草稿:DELETE /api/v1/scan-draft?biz_type=outbound&request_id=123
|
|||
|
|
# 提交成功后调用,避免下次打开该单据时恢复出已提交的内容。
|
|||
|
|
# --------------------------------------------------------
|
|||
|
|
@scan_draft_bp.route('', methods=['DELETE'])
|
|||
|
|
@jwt_required()
|
|||
|
|
def clear_scan_draft():
|
|||
|
|
try:
|
|||
|
|
user_id, biz_type, request_id, err = _parse_identity()
|
|||
|
|
if err:
|
|||
|
|
return jsonify({'code': 400, 'msg': err}), 400
|
|||
|
|
|
|||
|
|
draft = _find(user_id, biz_type, request_id)
|
|||
|
|
deleted = 0
|
|||
|
|
if draft:
|
|||
|
|
db.session.delete(draft)
|
|||
|
|
db.session.commit()
|
|||
|
|
deleted = 1
|
|||
|
|
|
|||
|
|
return jsonify({'code': 200, 'msg': 'success', 'data': {'deleted': deleted}}), 200
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
db.session.rollback()
|
|||
|
|
traceback.print_exc()
|
|||
|
|
return jsonify({'code': 500, 'msg': f'清除草稿失败: {str(e)}'}), 500
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------
|
|||
|
|
# 草稿概览:GET /api/v1/scan-draft/overview?biz_type=outbound
|
|||
|
|
#
|
|||
|
|
# 供单据下拉显示「已扫 N 项」进度徽标 —— 工人扫开页面就能看到哪张单
|
|||
|
|
# 之前扫到一半,不必逐个点开试。
|
|||
|
|
# --------------------------------------------------------
|
|||
|
|
@scan_draft_bp.route('/overview', methods=['GET'])
|
|||
|
|
@jwt_required()
|
|||
|
|
def get_draft_overview():
|
|||
|
|
try:
|
|||
|
|
identity = get_jwt_identity()
|
|||
|
|
if not identity:
|
|||
|
|
return jsonify({'code': 401, 'msg': '用户未登录'}), 401
|
|||
|
|
|
|||
|
|
biz_type = (request.args.get('biz_type') or '').strip()
|
|||
|
|
if biz_type not in BIZ_TYPES:
|
|||
|
|
return jsonify({'code': 400, 'msg': 'biz_type 无效'}), 400
|
|||
|
|
|
|||
|
|
rows = ScanDraft.query.filter_by(
|
|||
|
|
user_id=int(identity), biz_type=biz_type
|
|||
|
|
).all()
|
|||
|
|
|
|||
|
|
drafts = []
|
|||
|
|
for r in rows:
|
|||
|
|
items = r.get_items()
|
|||
|
|
if not items:
|
|||
|
|
continue # 防御:残留的空草稿不参与展示
|
|||
|
|
drafts.append({
|
|||
|
|
'request_id': r.request_id,
|
|||
|
|
'request_no': r.request_no or '',
|
|||
|
|
'item_count': len(items),
|
|||
|
|
'total_qty': round(sum(float(i.get('out_quantity') or 0) for i in items), 4),
|
|||
|
|
'updated_at': r.updated_at.strftime('%Y-%m-%d %H:%M:%S') if r.updated_at else None,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return jsonify({'code': 200, 'msg': 'success', 'data': {'drafts': drafts}}), 200
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
traceback.print_exc()
|
|||
|
|
return jsonify({'code': 500, 'msg': f'获取草稿概览失败: {str(e)}'}), 500
|