feat(借库审批流): 完整前后端实现
This commit is contained in:
@ -30,18 +30,65 @@ class TransService:
|
||||
return f"{prefix}{sequence:04d}"
|
||||
|
||||
@staticmethod
|
||||
def create_borrow(data, operator_name='System'):
|
||||
def execute_dispatch(approval_id, items, operator_name='System', borrower_name=None,
|
||||
signature=None, remark=None, expected_return_time=None):
|
||||
"""
|
||||
借库逻辑:减少可用库存,不减总库存
|
||||
执行借库扣减(审批通过后调用)
|
||||
流程:锁审批单 → 超额校验 → 锁库存行 → 扣减库存 → 生成 TransBorrow 记录 → 标记审批单完成
|
||||
"""
|
||||
items = data.get('items', [])
|
||||
borrower_name = data.get('borrower_name')
|
||||
signature = data.get('signature_path') # 借用人签字
|
||||
from app.models.borrow import BorrowApproval
|
||||
|
||||
if not items: raise ValueError("物品列表为空")
|
||||
if not borrower_name: raise ValueError("请输入借用人")
|
||||
if not signature: raise ValueError("借用人必须签字")
|
||||
|
||||
# ==============================================
|
||||
# ★ 防线1:并发防重复执行 - 用 SELECT FOR UPDATE 锁住审批单
|
||||
# ==============================================
|
||||
approval = BorrowApproval.query.with_for_update().get(approval_id)
|
||||
if not approval:
|
||||
raise ValueError("审批单不存在")
|
||||
if approval.status != 1:
|
||||
status_map = {0: '待审批', 1: '已通过', 2: '已驳回', 3: '已完成'}
|
||||
raise ValueError(f"审批单状态为【{status_map.get(approval.status, approval.status)}】,无法执行借库")
|
||||
|
||||
# ★ borrower_name 兜底:优先用前端传参,其次从审批单读取(申请时填写的姓名)
|
||||
if not borrower_name:
|
||||
borrower_name = approval.borrower_name
|
||||
if not borrower_name:
|
||||
raise ValueError("审批单中未记录借库人姓名,请联系管理员补录")
|
||||
|
||||
# ==============================================
|
||||
# ★ 防线2:超额篡改校验 - 交叉比对前端传来的实际扣减量与审批单上限
|
||||
# ==============================================
|
||||
approved_items = approval.get_items()
|
||||
if not approved_items:
|
||||
raise ValueError("审批单中无物料明细,请联系管理员检查")
|
||||
|
||||
# 构建审批上限字典:key=(source_table, sku) → approved_total_qty
|
||||
approved_limit = {}
|
||||
for ai in approved_items:
|
||||
key = (ai.get('source_table', ''), ai.get('sku', ''))
|
||||
qty = float(ai.get('quantity', 0))
|
||||
approved_limit[key] = approved_limit.get(key, 0) + qty
|
||||
|
||||
# 汇总前端传来的实际出库量(按 source_table+sku 聚合)
|
||||
dispatch_qty = {}
|
||||
for item in items:
|
||||
key = (item.get('source_table', ''), item.get('sku', ''))
|
||||
qty = float(item.get('out_quantity', 0))
|
||||
dispatch_qty[key] = dispatch_qty.get(key, 0) + qty
|
||||
|
||||
# 逐条比对:任意一条实际出库量 > 审批上限 → 直接拒绝
|
||||
for key, actual_qty in dispatch_qty.items():
|
||||
limit_qty = approved_limit.get(key, 0)
|
||||
if actual_qty > limit_qty:
|
||||
source_table, sku = key
|
||||
raise ValueError(
|
||||
f"实际出库数量超出了审批单允许的上限: "
|
||||
f"SKU={sku or '(无)'}({source_table}) "
|
||||
f"审批上限={limit_qty}, 实际出库={actual_qty}"
|
||||
)
|
||||
|
||||
borrow_no = TransService.generate_borrow_no()
|
||||
model_map = {'stock_buy': StockBuy, 'stock_semi': StockSemi, 'stock_product': StockProduct}
|
||||
|
||||
@ -54,6 +101,9 @@ class TransService:
|
||||
ModelClass = model_map.get(source_table)
|
||||
if not ModelClass: continue
|
||||
|
||||
# ==============================================
|
||||
# ★ 防线3:并发超卖与负库存 - 锁行后再查可用库存
|
||||
# ==============================================
|
||||
stock = ModelClass.query.with_for_update().get(stock_id)
|
||||
if not stock: raise ValueError(f"库存不存在 ID:{stock_id}")
|
||||
|
||||
@ -63,7 +113,7 @@ class TransService:
|
||||
# 1. 冻结库存 (只减可用)
|
||||
stock.available_quantity = float(stock.available_quantity) - qty
|
||||
|
||||
# 2. 创建借用单
|
||||
# 2. 创建借用记录
|
||||
record = TransBorrow(
|
||||
borrow_no=borrow_no,
|
||||
sku=stock.sku,
|
||||
@ -73,19 +123,39 @@ class TransService:
|
||||
quantity=qty,
|
||||
borrower_name=borrower_name,
|
||||
borrow_signature=signature,
|
||||
remark=data.get('remark'),
|
||||
expected_return_time=data.get('expected_return_time'),
|
||||
remark=remark,
|
||||
expected_return_time=expected_return_time,
|
||||
status='borrowed',
|
||||
is_returned=False
|
||||
)
|
||||
db.session.add(record)
|
||||
|
||||
# ★ 3. 标记审批单为已完成
|
||||
approval.status = 3
|
||||
|
||||
db.session.commit()
|
||||
return borrow_no
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
# ★ 兼容旧入口(不走审批流的直接借库,保留以便平滑过渡)
|
||||
@staticmethod
|
||||
def create_borrow(data, operator_name='System'):
|
||||
"""
|
||||
借库逻辑(兼容旧模式):减少可用库存,不减总库存
|
||||
@deprecated 请优先使用 execute_dispatch 走审批流
|
||||
"""
|
||||
return TransService.execute_dispatch(
|
||||
approval_id=0,
|
||||
items=data.get('items', []),
|
||||
operator_name=operator_name,
|
||||
borrower_name=data.get('borrower_name'),
|
||||
signature=data.get('signature_path'),
|
||||
remark=data.get('remark'),
|
||||
expected_return_time=data.get('expected_return_time')
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def scan_for_return(barcode):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user