时间口径(与归还流水同时引入,故随本轮一并修) ---- process_return 与 scrap_sources.deduct 原用 datetime.now() 写 return_time, 而 datetime.now() 取的是**容器本地时间**(Docker 下为 UTC);同一行的 borrow_time 却由 beijing_time() 写入 —— 两个字段差 8 小时,台账时间线 自相矛盾,并会让新做的流转时间线出现「先借出、后归还,却显示归还更早」 的倒序假象。 统一改用 beijing_time(),并与新增的归还流水共用同一个时间戳,保证主表快照 与流水逐笔完全对齐。 (与 db_migrations/unify_approval_timezone.sql 处理的是同一类问题:aware/naive 与本地/北京时间的口径混用。) 注释修正 ---- borrow_service.submit_approval 的 docstring 写着「仅存储意向,不扣库存」, 但 Phase 1 起该函数已调用 reserve_for_items() 预占库存(扣 available_quantity)。 过期注释会误导后续开发者(尤其是做转交的人以为库存没动过),已改写为实际的 预占语义与三种释放路径(驳回 / 撤回 / 扫码执行)。
584 lines
24 KiB
Python
584 lines
24 KiB
Python
from datetime import datetime
|
||
import pytz
|
||
from app.extensions import db, beijing_time
|
||
from app.models.borrow import BorrowApproval
|
||
from app.models.system import SysUser
|
||
|
||
|
||
class BorrowApprovalService:
|
||
"""借库审批服务"""
|
||
|
||
@staticmethod
|
||
def generate_request_no():
|
||
"""
|
||
生成审批单号: APR-BOR-yyyyMMdd-HHmm-当日流水(4位)
|
||
"""
|
||
beijing_tz = pytz.timezone('Asia/Shanghai')
|
||
now = datetime.now(beijing_tz)
|
||
|
||
date_str = now.strftime('%Y%m%d')
|
||
time_str = now.strftime('%H%M')
|
||
|
||
prefix = f"APR-BOR-{date_str}-"
|
||
|
||
latest = db.session.query(BorrowApproval.request_no).filter(
|
||
BorrowApproval.request_no.like(f"{prefix}%")
|
||
).order_by(BorrowApproval.id.desc()).first()
|
||
|
||
if latest:
|
||
last_seq = int(latest[0].split('-')[-1])
|
||
sequence = last_seq + 1
|
||
else:
|
||
sequence = 1
|
||
|
||
return f"APR-BOR-{date_str}-{time_str}-{sequence:04d}"
|
||
|
||
@staticmethod
|
||
def _items_require_approval(items):
|
||
"""
|
||
明细中任一物料命中“出库/借库需审批”→ 需要审批。
|
||
申请明细只存 name/spec_model,故按 (name, spec_model) 反查启用物料判定。
|
||
"""
|
||
from app.models.base import MaterialBase
|
||
seen = set()
|
||
for item in items:
|
||
name = str(item.get('name') or '').strip()
|
||
spec = str(item.get('spec_model') or '').strip()
|
||
if not name:
|
||
continue
|
||
key = (name, spec)
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
hit = MaterialBase.query.filter(
|
||
MaterialBase.name == name,
|
||
MaterialBase.spec_model == spec,
|
||
MaterialBase.is_enabled == True,
|
||
MaterialBase.is_approval_required == True
|
||
).first()
|
||
if hit:
|
||
return True
|
||
return False
|
||
|
||
@staticmethod
|
||
def submit_approval(applicant_id, items, allowed_approvers, remark=None, approver_id=None,
|
||
borrower_name=None, force_approval=False):
|
||
"""
|
||
提交借库申请。
|
||
|
||
★ 库存语义:**提交即预占**(Phase 1 起)。
|
||
本函数会调用 reserve_for_items() 锁定具体批次并扣减 available_quantity,
|
||
不动 stock_quantity —— 借出期间该批货不可再被他人领用。
|
||
预占的释放时机:
|
||
· 驳回 / 撤回 → release_reserved() 全额归还;
|
||
· 扫码执行 → restore_then_deduct() 先全量释放、再按实扫批次扣减。
|
||
(原注释写「仅存储意向,不扣库存」,是 Phase 1 改造前的旧描述,
|
||
与下方实现不符,已修正。)
|
||
|
||
Args:
|
||
applicant_id: 申请人ID
|
||
items: 借库物品明细列表,每个物品应包含:
|
||
- name: 物料名称 (必填)
|
||
- spec_model: 规格型号 (必填)
|
||
- quantity: 计划借库数量 (必填)
|
||
- warehouse_location: 库位 (可选)
|
||
- remark: 物品备注 (可选)
|
||
allowed_approvers: 允许审批的人员/角色列表
|
||
approver_id: 指定审批人ID(可选)
|
||
remark: 申请说明
|
||
borrower_name: 借库人姓名(必填)
|
||
|
||
Returns:
|
||
BorrowApproval 实例
|
||
|
||
Raises:
|
||
ValueError: 当 items 为空或缺少必填字段时抛出
|
||
"""
|
||
if not items:
|
||
raise ValueError("借库物品列表不能为空")
|
||
|
||
# ★ 借库申请必填:申请原因;预计归还日期(除非勾选长期借用/不限归还)
|
||
if not str(remark or '').strip():
|
||
raise ValueError("借库申请原因不能为空")
|
||
_has_indefinite = any(bool(item.get('is_indefinite')) for item in items)
|
||
_has_date = any(str(item.get('expected_return_time') or '').strip() for item in items)
|
||
if not _has_indefinite and not _has_date:
|
||
raise ValueError("请填写预计归还日期,或勾选长期借用(不限归还)")
|
||
|
||
required_fields = ['name', 'spec_model', 'quantity']
|
||
for idx, item in enumerate(items):
|
||
missing_fields = [f for f in required_fields if f not in item or str(item.get(f) or '').strip() == '']
|
||
if missing_fields:
|
||
raise ValueError(
|
||
f"第 {idx + 1} 条物品缺少必填字段: {', '.join(missing_fields)}。"
|
||
f"必须包含: name, spec_model, quantity"
|
||
)
|
||
try:
|
||
qty = float(item.get('quantity', 0))
|
||
if qty <= 0:
|
||
raise ValueError(f"第 {idx + 1} 条物品的借库数量必须大于0")
|
||
except (TypeError, ValueError) as e:
|
||
raise ValueError(f"第 {idx + 1} 条物品的 quantity 格式无效: {str(e)}")
|
||
|
||
# ★ 需审批判定:库管代建(force_approval)或明细含需审批物料 → 走审批;否则默认自动通过
|
||
from app.services.approval_control import resolve_approval_control
|
||
need_approval, flagged_materials = resolve_approval_control(items)
|
||
if force_approval:
|
||
need_approval = True
|
||
if need_approval and not approver_id:
|
||
if force_approval:
|
||
raise ValueError("库管代建借库申请必须选择审批人后再提交")
|
||
_names = ";".join(f"{m['name']}({m['spec_model'] or '-'})" for m in flagged_materials)
|
||
raise ValueError(f"以下物料需审批出库/借库:{_names}。请选择审批人后再提交")
|
||
|
||
if approver_id:
|
||
allowed_approvers = [{"type": "user", "value": int(approver_id)}]
|
||
elif not need_approval:
|
||
allowed_approvers = [] # 免审批单不绑定审批人
|
||
|
||
request_no = BorrowApprovalService.generate_request_no()
|
||
|
||
if need_approval:
|
||
approval = BorrowApproval(
|
||
request_no=request_no,
|
||
applicant_id=applicant_id,
|
||
remark=remark,
|
||
borrower_name=borrower_name,
|
||
status=0, # 待审批
|
||
)
|
||
else:
|
||
# 默认不审批:创建即已通过(status=1),直接进入“待库管执行”
|
||
# ★ approved_at 必须与 created_at(beijing_time) 同为 naive 本地时间:
|
||
# approved_at 列是 timestamp without time zone,传 aware 时间会被驱动转成 UTC 存库,导致早 8 小时
|
||
approval = BorrowApproval(
|
||
request_no=request_no,
|
||
applicant_id=applicant_id,
|
||
remark=remark,
|
||
borrower_name=borrower_name,
|
||
status=1,
|
||
actual_approver_id=applicant_id,
|
||
approved_at=beijing_time(),
|
||
)
|
||
|
||
# ==================================================================
|
||
# ★ Phase 1:库存预占
|
||
# 借出同样会占用库存(借出期间该批货不可再被他人领用),
|
||
# 因此与出库一致:提交即锁定具体批次并扣减 available_quantity。
|
||
# 借出归还时再归还,见 process_return()。
|
||
# ==================================================================
|
||
from app.services.inventory_reservation import reserve_for_items
|
||
from app.utils.decorators import get_current_company_filter
|
||
|
||
reserved_items, _shortages = reserve_for_items(
|
||
items, company_limit=get_current_company_filter(), strict=True,
|
||
)
|
||
|
||
approval.set_items(reserved_items)
|
||
if allowed_approvers:
|
||
approval.set_allowed_approvers(allowed_approvers)
|
||
else:
|
||
approval.allowed_approvers = '[]'
|
||
|
||
db.session.add(approval)
|
||
db.session.commit()
|
||
|
||
# 仅需审批单通知审批人;免审批单静默进入“待库管执行”
|
||
if need_approval:
|
||
BorrowApprovalService._notify_new_request(approval, applicant_id, approver_id=approver_id)
|
||
|
||
return approval
|
||
|
||
@staticmethod
|
||
def _get_emails_by_identifiers(applicant_id=None, role_codes=None):
|
||
"""
|
||
根据用户ID或角色列表查询邮箱地址
|
||
|
||
Args:
|
||
applicant_id: 用户ID (按 SysUser.id 查找)
|
||
role_codes: 角色代码列表,如 ['SUPERVISOR', 'SUPER_ADMIN']
|
||
|
||
Returns:
|
||
去重后的邮箱地址列表
|
||
"""
|
||
emails = []
|
||
|
||
if applicant_id:
|
||
user = SysUser.query.get(int(applicant_id))
|
||
if user and user.email:
|
||
emails.append(user.email)
|
||
|
||
if role_codes:
|
||
for code in role_codes:
|
||
users = SysUser.query.filter_by(role=code).all()
|
||
for u in users:
|
||
if u.email:
|
||
emails.append(u.email)
|
||
|
||
return list(set(emails))
|
||
|
||
@staticmethod
|
||
def _notify_new_request(approval, applicant_id, approver_id=None):
|
||
"""发送新借库申请通知邮件给审批人和申请人(静默处理,不阻断主流程)"""
|
||
try:
|
||
from flask import current_app
|
||
from app.utils.email_service import send_borrow_new_request_notify
|
||
from app.models.system import SysUser
|
||
|
||
applicant_name = ''
|
||
applicant_emails = []
|
||
|
||
# 1. 收集申请人信息
|
||
if applicant_id:
|
||
user = SysUser.query.get(int(applicant_id))
|
||
if user and user.email:
|
||
applicant_emails.append(user.email)
|
||
applicant_name = str(user.username).split('/')[0] if '/' in (user.username or '') else (user.username or str(applicant_id))
|
||
|
||
# 2. 收集审批人信息
|
||
approver_emails = []
|
||
if approver_id:
|
||
user = SysUser.query.get(int(approver_id))
|
||
if user and user.email:
|
||
approver_emails.append(user.email)
|
||
else:
|
||
# 兜底:按角色查询
|
||
approvers = approval.get_allowed_approvers()
|
||
role_codes = []
|
||
for a in approvers:
|
||
if a.get('type') == 'role':
|
||
role_codes.append(a.get('value', ''))
|
||
approver_emails = BorrowApprovalService._get_emails_by_identifiers(role_codes=role_codes)
|
||
|
||
# 去重
|
||
all_emails = list(set(applicant_emails + approver_emails))
|
||
if not all_emails:
|
||
current_app.logger.info(f"[Email] 借库审批单 {approval.request_no} 无收件人邮箱,跳过通知")
|
||
return
|
||
|
||
# 3. 获取物料明细
|
||
items = approval.get_items()
|
||
|
||
# 4. 分别发送邮件
|
||
if applicant_emails:
|
||
try:
|
||
send_borrow_new_request_notify(
|
||
to_emails=applicant_emails,
|
||
request_no=approval.request_no,
|
||
applicant_name=applicant_name,
|
||
remark=f"您的借库申请已提交,等待审批。{approval.remark or ''}",
|
||
items=items,
|
||
is_applicant_notify=True
|
||
)
|
||
except Exception as e:
|
||
current_app.logger.error(f"[Email] 通知申请人失败: {e}")
|
||
|
||
if approver_emails:
|
||
try:
|
||
send_borrow_new_request_notify(
|
||
to_emails=approver_emails,
|
||
request_no=approval.request_no,
|
||
applicant_name=applicant_name,
|
||
remark=approval.remark or '',
|
||
items=items,
|
||
is_applicant_notify=False
|
||
)
|
||
except Exception as e:
|
||
current_app.logger.error(f"[Email] 通知审批人失败: {e}")
|
||
|
||
except Exception as e:
|
||
try:
|
||
from flask import current_app
|
||
current_app.logger.error(f"[Email] 发送新借库申请通知邮件失败: {e}")
|
||
except RuntimeError:
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
@staticmethod
|
||
def _notify_approval_result(approval, approver_id, action):
|
||
"""发送借库审批结果通知邮件(静默处理,不阻断主流程)"""
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
|
||
try:
|
||
from app.utils.email_service import send_borrow_approval_result_notify, send_borrow_dispatch_notify
|
||
from app.models.system import SysUser as SU
|
||
|
||
# 1. 提取申请人信息
|
||
applicant_name = ''
|
||
applicant_emails = []
|
||
if approval.applicant_id:
|
||
user = SU.query.get(approval.applicant_id)
|
||
if user:
|
||
applicant_name = str(user.username).split('/')[0] if '/' in (user.username or '') else (user.username or '')
|
||
if user.email:
|
||
applicant_emails.append(user.email)
|
||
|
||
# 2. 提取物料明细
|
||
items = approval.get_items() if approval else []
|
||
|
||
# 3. 分支逻辑
|
||
if action == 'approve':
|
||
# 3.1 通知申请人(审批已通过,明确告知结果)
|
||
if applicant_emails:
|
||
try:
|
||
send_borrow_approval_result_notify(
|
||
to_emails=applicant_emails,
|
||
request_no=approval.request_no,
|
||
is_passed=True,
|
||
reject_reason='',
|
||
applicant_name=applicant_name
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"[Email] 通知申请人(通过)失败: {e}")
|
||
else:
|
||
logger.warning("[Email] 申请人无邮箱,无法发送审批通过通知")
|
||
|
||
# 3.2 通知库管(请备货)
|
||
warehouse_role_codes = ['WAREHOUSE_MGR', 'OUTBOUND']
|
||
warehouse_emails = BorrowApprovalService._get_emails_by_identifiers(role_codes=warehouse_role_codes)
|
||
|
||
if warehouse_emails:
|
||
try:
|
||
send_borrow_dispatch_notify(
|
||
to_emails=warehouse_emails,
|
||
request_no=approval.request_no,
|
||
applicant_name=applicant_name,
|
||
items=items
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"[Email] 通知库管失败: {e}")
|
||
else:
|
||
logger.warning("[Email] 无库管角色邮箱,无法发送备货通知")
|
||
|
||
elif action == 'reject':
|
||
# 3.3 通知申请人(已驳回)
|
||
if applicant_emails:
|
||
try:
|
||
send_borrow_approval_result_notify(
|
||
to_emails=applicant_emails,
|
||
request_no=approval.request_no,
|
||
is_passed=False,
|
||
reject_reason=approval.reject_reason or '未说明原因',
|
||
applicant_name=applicant_name
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"[Email] 通知申请人驳回失败: {e}")
|
||
else:
|
||
logger.warning("[Email] 申请人无邮箱,无法发送驳回通知")
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
traceback.print_exc()
|
||
logger.error(f"[Email] 外层发送异常: {e}")
|
||
|
||
@staticmethod
|
||
def can_approve(approval, user_id, user_role):
|
||
"""
|
||
检查用户是否有权限审批
|
||
"""
|
||
approvers = approval.get_allowed_approvers()
|
||
|
||
if user_role and user_role.upper() == 'SUPER_ADMIN':
|
||
return True
|
||
|
||
# 拥有 op_borrow_approval:operation 权限的用户可以审批任意借库单
|
||
# (前端按钮以此权限控制显示,后端也必须对齐)
|
||
from app.models.system import SysRolePermission
|
||
has_op = SysRolePermission.query.filter(
|
||
SysRolePermission.role_code == user_role,
|
||
SysRolePermission.target_code.in_(['op_borrow_approval:operation', 'op_borrow_approval:*'])
|
||
).first() is not None
|
||
if has_op:
|
||
return True
|
||
|
||
for approver in approvers:
|
||
approver_type = approver.get('type', '')
|
||
approver_value = approver.get('value', '')
|
||
|
||
if approver_type == 'user' and str(approver_value) == str(user_id):
|
||
return True
|
||
|
||
if approver_type == 'role' and approver_value == user_role:
|
||
return True
|
||
|
||
return False
|
||
|
||
@staticmethod
|
||
def approve(request_id, user_id, user_role, action='approve', reject_reason=None):
|
||
"""
|
||
执行审批操作
|
||
|
||
Returns:
|
||
(success: bool, message: str, approval: BorrowApproval or None)
|
||
"""
|
||
# ★ 统一取 naive 北京时间,与 created_at(beijing_time) 同口径
|
||
current_time = beijing_time()
|
||
|
||
approval = BorrowApproval.query.get(request_id)
|
||
if not approval:
|
||
return False, "审批单不存在", None
|
||
|
||
if approval.status != 0:
|
||
return False, f"审批单状态已更新,无法重复审批 (当前状态: {approval.status})", None
|
||
|
||
if not BorrowApprovalService.can_approve(approval, user_id, user_role):
|
||
return False, "您没有审批此单的权限", None
|
||
|
||
try:
|
||
if action == 'approve':
|
||
approval.status = 1 # 已通过
|
||
approval.actual_approver_id = user_id
|
||
approval.approved_at = current_time
|
||
# 通过后预占继续保留,直到实际借出执行时才释放并扣减
|
||
elif action == 'reject':
|
||
# ★ Phase 2:驳回即释放预占,避免货被永不执行的单占住
|
||
from app.services.inventory_reservation import release_reserved
|
||
release_reserved(approval.get_items())
|
||
approval.status = 2 # 已驳回
|
||
approval.reject_reason = reject_reason
|
||
else:
|
||
return False, "无效的审批操作", None
|
||
|
||
db.session.commit()
|
||
|
||
# ★ 审批后,发送邮件通知(静默处理,不阻断主流程)
|
||
BorrowApprovalService._notify_approval_result(approval, user_id, action)
|
||
|
||
return True, "审批成功", approval
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return False, f"审批失败: {str(e)}", None
|
||
|
||
@staticmethod
|
||
def get_request_list(page=1, per_page=10, applicant_id=None, status=None):
|
||
"""
|
||
获取审批单列表
|
||
"""
|
||
from app.models.system import SysUser
|
||
from app.utils.decorators import get_current_company_filter
|
||
from sqlalchemy import desc
|
||
|
||
query = BorrowApproval.query
|
||
|
||
if applicant_id:
|
||
query = query.filter(BorrowApproval.applicant_id == applicant_id)
|
||
|
||
if status is not None:
|
||
query = query.filter(BorrowApproval.status == status)
|
||
|
||
# 【行级数据隔离】通过申请人关联用户表过滤公司
|
||
company_limit = get_current_company_filter()
|
||
if company_limit is not None:
|
||
query = query.join(SysUser, BorrowApproval.applicant_id == SysUser.id) \
|
||
.filter(SysUser.department == company_limit)
|
||
|
||
query = query.order_by(desc(BorrowApproval.created_at))
|
||
|
||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||
|
||
return {
|
||
'items': [item.to_dict() for item in pagination.items],
|
||
'total': pagination.total,
|
||
'pages': pagination.pages,
|
||
'current_page': page
|
||
}
|
||
|
||
@staticmethod
|
||
def get_request_by_id(request_id):
|
||
"""根据ID获取审批单"""
|
||
return BorrowApproval.query.get(request_id)
|
||
|
||
@staticmethod
|
||
def mark_completed(request_id):
|
||
"""
|
||
撤回 / 作废已通过的借库审批单(status 1-已通过 → 4-已完结)。
|
||
|
||
注意:真正「扫码借出执行完成」走 trans_service.execute_dispatch,
|
||
会将 status 置为 3(已完成);本方法只处理「已通过但尚未执行」的撤回。
|
||
|
||
★ 修复:原先只改状态、不释放预占 —— 申请阶段 reserve_for_items()
|
||
扣掉的 available_quantity 会永久泄漏,货被一张作废单锁死。
|
||
现调用 release_reserved() 按 items_json 原样归还。
|
||
|
||
执行守卫:执行成功后 status 会被置为 3,故「仅 status==1 可撤回」
|
||
本身就是执行守卫,已执行或已撤回的单都无法再次操作。
|
||
"""
|
||
approval = BorrowApproval.query.get(request_id)
|
||
if not approval:
|
||
return False, "审批单不存在", None
|
||
|
||
if approval.status != 1:
|
||
status_map = {0: '待审批', 1: '已通过', 2: '已驳回', 3: '已完成', 4: '已撤回'}
|
||
return False, (
|
||
f"只有已通过(待执行)的审批单才能撤回 "
|
||
f"(当前状态: {status_map.get(approval.status, approval.status)})"
|
||
), None
|
||
|
||
return BorrowApprovalService._release_and_close(approval)
|
||
|
||
# ------------------------------------------------------------------
|
||
# ★ 申请人撤回自己的借库申请单(严格职责分离)
|
||
#
|
||
# 与 mark_completed(管理路径)的区别:
|
||
# · mark_completed —— 需 op_borrow_approval 权限,仅 status==1
|
||
# · withdraw_request —— 仅校验「单据归属」,status 0 或 1 均可
|
||
#
|
||
# 两条路径共用底层 _release_and_close,释放逻辑只有一份实现。
|
||
# ------------------------------------------------------------------
|
||
WITHDRAWABLE_STATUS = (0, 1)
|
||
|
||
@staticmethod
|
||
def withdraw_request(request_id, user_id):
|
||
"""
|
||
申请人撤回自己的借库申请单。
|
||
|
||
权限:仅单据本人;库管/主管/超管(is_privileged_viewer)可代撤。
|
||
返回 (success, message, approval)
|
||
"""
|
||
from app.utils.decorators import is_privileged_viewer
|
||
|
||
approval = BorrowApproval.query.get(request_id)
|
||
if not approval:
|
||
return False, "申请单不存在", None
|
||
|
||
# ★ 归属校验:非本人且非特权 → 拒绝(不漏出任何单据信息)
|
||
if not is_privileged_viewer() and int(approval.applicant_id) != int(user_id):
|
||
return False, "无权撤回他人的申请单", None
|
||
|
||
if approval.status not in BorrowApprovalService.WITHDRAWABLE_STATUS:
|
||
status_map = {0: '待审批', 1: '已通过(待执行)', 2: '已驳回', 3: '已完成', 4: '已撤回'}
|
||
return False, (
|
||
f"当前状态不可撤回:{status_map.get(approval.status, approval.status)}"
|
||
), None
|
||
|
||
return BorrowApprovalService._release_and_close(approval)
|
||
|
||
@staticmethod
|
||
def _release_and_close(approval):
|
||
"""
|
||
撤回的共用底层:释放预占 + 置为已撤回。
|
||
|
||
两条入口(管理路径 mark_completed / 申请人路径 withdraw_request)
|
||
各自完成权限与状态校验后调用本函数,确保释放逻辑不会因修改
|
||
其中一处而漏掉另一处。
|
||
"""
|
||
try:
|
||
# ★ 释放全部预占,把 available_quantity 还回池子
|
||
from app.services.inventory_reservation import release_reserved
|
||
restored = release_reserved(approval.get_items())
|
||
|
||
# ★ 清理该单据的扫码草稿(含其他用户的)。
|
||
# 撤回后单据状态变为 4,不再出现在扫码页的下拉里(那里只拉 status=1),
|
||
# 草稿从此成为永远读不到的死数据。
|
||
from app.models.scan_draft import ScanDraft
|
||
ScanDraft.query.filter_by(
|
||
biz_type='borrow', request_id=approval.id
|
||
).delete(synchronize_session=False)
|
||
|
||
approval.status = 4 # 已撤回/已完结
|
||
db.session.commit()
|
||
return True, f"申请单已撤回,释放 {restored} 项预占库存", approval
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return False, f"撤回归还失败: {str(e)}", None |