Files
shanxia2.0/backend/app/crud/crm/contract_approval.py
T
2026-08-21 16:50:47 +08:00

470 lines
15 KiB
Python

"""CRM 合同审批 CRUD — 对照 CrmContractApprovalServiceImpl.java"""
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import func
from sqlalchemy.orm import Query, Session
from app.core.utils import generate_uuid
from app.models.crm_models import CrmContract, CrmContractApproval
from app.models.models import SysUser
_FMT = "%Y-%m-%d %H:%M:%S"
def _row_to_dict(row: CrmContractApproval) -> Dict[str, Any]:
return {
"id": row.id,
"contractId": row.contract_id or "",
"flowId": row.flow_id or "",
"step": row.step if row.step is not None else 0,
"stepName": row.step_name or "",
"approverId": row.approver_id or "",
"approverName": row.approver_name or "",
"approverDept": row.approver_dept or "",
"approvalResult": row.approval_result or "pending",
"approvalOpinion": row.approval_opinion or "",
"approvalTime": row.approval_time.strftime(_FMT) if row.approval_time else "",
"approvalDuration": row.approval_duration,
"attachments": row.attachments or "",
"isCurrent": row.is_current if row.is_current is not None else 1,
"sort": row.sort if row.sort is not None else 0,
"remark": row.remark or "",
"createTime": row.create_time.strftime(_FMT) if row.create_time else "",
}
def _resolve_user_display_name(db: Session, user_id: str) -> str:
uid = (user_id or "").strip()
if not uid:
return ""
u = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
if not u:
return ""
return (u.nick_name or u.user_no or "").strip()
def get_contract_approval(
db: Session,
approval_id: str,
*,
tenant_no: str = None,
) -> Optional[CrmContractApproval]:
q = db.query(CrmContractApproval).filter(
CrmContractApproval.id == approval_id,
CrmContractApproval.del_flag == 0,
)
if tenant_no:
q = q.filter(CrmContractApproval.tenant_no == tenant_no)
return q.first()
def get_contract_approval_detail(
db: Session,
approval_id: str,
*,
tenant_no: str = None,
) -> Dict[str, Any]:
row = get_contract_approval(db, approval_id, tenant_no=tenant_no)
if not row:
raise ValueError("审批记录不存在")
detail = _row_to_dict(row)
if detail.get("approverId") and not detail.get("approverName"):
detail["approverName"] = _resolve_user_display_name(db, detail["approverId"])
return detail
def list_contract_approvals(
db: Session,
contract_id: str,
*,
tenant_no: str = None,
) -> List[Dict[str, Any]]:
q = db.query(CrmContractApproval).filter(
CrmContractApproval.contract_id == contract_id,
CrmContractApproval.del_flag == 0,
)
if tenant_no:
q = q.filter(CrmContractApproval.tenant_no == tenant_no)
rows = q.order_by(CrmContractApproval.step.asc()).all()
result = [_row_to_dict(r) for r in rows]
for item in result:
if item.get("approverId") and not item.get("approverName"):
item["approverName"] = _resolve_user_display_name(db, item["approverId"])
return result
def _build_approval_list_filters(q: Query, filters: dict, *, tenant_no: str) -> Query:
q = q.filter(CrmContractApproval.tenant_no == tenant_no, CrmContractApproval.del_flag == 0)
contract_id = (filters.get("contractId") or filters.get("contract_id") or "").strip()
if contract_id:
q = q.filter(CrmContractApproval.contract_id == contract_id)
approval_result = (filters.get("approvalResult") or filters.get("approval_result") or "").strip()
if approval_result:
q = q.filter(CrmContractApproval.approval_result == approval_result)
approver_id = (filters.get("approverId") or filters.get("approver_id") or "").strip()
if approver_id:
q = q.filter(CrmContractApproval.approver_id == approver_id)
return q.order_by(CrmContractApproval.create_time.desc())
def list_contract_approvals_page(
db: Session,
tenant_no: str,
*,
page: int = 1,
page_size: int = 10,
filters: dict = None,
) -> Tuple[List[Dict[str, Any]], int]:
q = db.query(CrmContractApproval)
q = _build_approval_list_filters(q, filters or {}, tenant_no=tenant_no)
total = q.order_by(None).with_entities(func.count(CrmContractApproval.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 500)
rows = q.offset((page - 1) * page_size).limit(page_size).all()
result = [_row_to_dict(r) for r in rows]
for item in result:
if item.get("approverId") and not item.get("approverName"):
item["approverName"] = _resolve_user_display_name(db, item["approverId"])
return result, int(total)
def create_approval_records(
db: Session,
*,
tenant_no: str,
contract_id: str,
approver_ids: List[str],
user_id: str = None,
dept_id: str = "",
) -> None:
"""Java: submitApproval 内创建审批记录"""
contract = (
db.query(CrmContract)
.filter(CrmContract.id == contract_id, CrmContract.del_flag == 0, CrmContract.tenant_no == tenant_no)
.first()
)
if not contract:
raise ValueError("合同不存在")
step = 1
for idx, approver_id in enumerate(approver_ids):
aid = (approver_id or "").strip()
if not aid:
continue
nick = _resolve_user_display_name(db, aid) or f"用户{aid}"
entity = CrmContractApproval(
id=generate_uuid(),
contract_id=contract_id,
step=step,
step_name=f"第{step}步审批",
approver_id=aid,
approver_name=nick,
approver_dept="",
approval_result="pending",
is_current=1 if idx == 0 else 0,
sort=step,
tenant_no=tenant_no,
create_by=user_id,
create_dept_id=dept_id or "",
del_flag=0,
)
step += 1
db.add(entity)
db.flush()
def delete_pending_approvals(db: Session, contract_id: str) -> int:
"""Java: withdraw — 软删除 pending 审批"""
rows = (
db.query(CrmContractApproval)
.filter(
CrmContractApproval.contract_id == contract_id,
CrmContractApproval.approval_result == "pending",
CrmContractApproval.del_flag == 0,
)
.all()
)
now = datetime.now()
for row in rows:
row.del_flag = 1
row.update_time = now
db.flush()
return len(rows)
def invalidate_all_approvals(db: Session, contract_id: str) -> int:
"""作废该合同全部有效审批记录(含已通过/已拒绝),用于再编辑或重新提审。"""
rows = (
db.query(CrmContractApproval)
.filter(
CrmContractApproval.contract_id == contract_id,
CrmContractApproval.del_flag == 0,
)
.all()
)
now = datetime.now()
for row in rows:
row.del_flag = 1
row.update_time = now
db.flush()
return len(rows)
def can_user_approve(db: Session, contract_id: str, user_id: str) -> bool:
"""顺序审批:当前步骤 pending 且 is_current=1。
同一审批人若同时存在历史已处理记录与当前轮 pending,优先取 pending(再提审场景)。
前序校验仅看「当前轮」(以最早一条 pending 的创建时间为界),忽略历史轮次。
"""
if not contract_id or not user_id:
return False
approvals = (
db.query(CrmContractApproval)
.filter(
CrmContractApproval.contract_id == contract_id,
CrmContractApproval.del_flag == 0,
)
.order_by(CrmContractApproval.step.asc(), CrmContractApproval.create_time.desc())
.all()
)
if not approvals:
return False
pending_rows = [a for a in approvals if a.approval_result == "pending"]
if not pending_rows:
return False
round_start = min((a.create_time for a in pending_rows if a.create_time), default=None)
if round_start is not None:
round_approvals = [
a for a in approvals if a.create_time is None or a.create_time >= round_start
]
else:
round_approvals = list(pending_rows)
user_approval = None
for approval in round_approvals:
if (
approval.approver_id == user_id
and approval.approval_result == "pending"
and approval.is_current == 1
):
user_approval = approval
break
if user_approval is None:
return False
for approval in round_approvals:
if approval.step >= user_approval.step:
continue
if approval.approval_result != "approved":
return False
return True
def _advance_contract_after_approval(
db: Session,
contract: CrmContract,
current_approval: CrmContractApproval,
approval_result: str,
) -> None:
if approval_result == "approved":
next_approval = (
db.query(CrmContractApproval)
.filter(
CrmContractApproval.contract_id == contract.id,
CrmContractApproval.approval_result == "pending",
CrmContractApproval.step > current_approval.step,
CrmContractApproval.del_flag == 0,
)
.order_by(CrmContractApproval.step.asc())
.first()
)
if next_approval:
next_approval.is_current = 1
next_approval.update_time = datetime.now()
else:
contract.status = "approved"
contract.approval_status = "approved"
contract.update_time = datetime.now()
elif approval_result == "rejected":
contract.status = "draft"
contract.approval_status = "rejected"
contract.update_time = datetime.now()
def approve_contract(
db: Session,
approval_id: str,
*,
approval_result: str,
approval_opinion: str = "",
attachments: str = "",
remark: str = "",
user_id: str = None,
) -> bool:
"""Java: approveContract"""
entity = get_contract_approval(db, approval_id)
if not entity:
raise ValueError("审批记录不存在")
if user_id and entity.approver_id != user_id:
raise ValueError("只有审批人才能审批")
if entity.approval_result != "pending":
raise ValueError("该审批已处理")
if not can_user_approve(db, entity.contract_id, entity.approver_id):
raise ValueError("前置审批步骤尚未完成,暂时无法审批")
now = datetime.now()
if entity.create_time:
duration_hours = int((now - entity.create_time).total_seconds() / 3600)
entity.approval_duration = duration_hours
entity.approval_result = approval_result
entity.approval_opinion = approval_opinion or ""
entity.approval_time = now
entity.attachments = attachments or entity.attachments or ""
entity.remark = remark or entity.remark or ""
entity.is_current = 0
entity.update_by = user_id
entity.update_time = now
contract = (
db.query(CrmContract)
.filter(CrmContract.id == entity.contract_id, CrmContract.del_flag == 0)
.first()
)
if not contract:
raise ValueError("合同不存在")
_advance_contract_after_approval(db, contract, entity, approval_result)
db.flush()
return True
def approve_contract_by_contract_id(
db: Session,
contract_id: str,
*,
approval_result: str,
approval_opinion: str = "",
attachments: str = "",
remark: str = "",
user_id: str = None,
) -> bool:
"""Java: approveContractByContractId"""
if not user_id:
raise ValueError("未找到当前用户")
approval = (
db.query(CrmContractApproval)
.filter(
CrmContractApproval.contract_id == contract_id,
CrmContractApproval.approver_id == user_id,
CrmContractApproval.approval_result == "pending",
CrmContractApproval.is_current == 1,
CrmContractApproval.del_flag == 0,
)
.first()
)
if not approval:
raise ValueError("未找到待审批的记录或您不是当前审批人")
return approve_contract(
db,
approval.id,
approval_result=approval_result,
approval_opinion=approval_opinion,
attachments=attachments,
remark=remark,
user_id=user_id,
)
def approve_pass(db: Session, approval_id: str, *, approval_opinion: str = "", user_id: str = None) -> bool:
return approve_contract(
db,
approval_id,
approval_result="approved",
approval_opinion=approval_opinion,
user_id=user_id,
)
def approve_reject(db: Session, approval_id: str, *, approval_opinion: str = "", user_id: str = None) -> bool:
return approve_contract(
db,
approval_id,
approval_result="rejected",
approval_opinion=approval_opinion,
user_id=user_id,
)
def refresh_approver_names(db: Session, contract_id: str = None) -> int:
"""Java: refreshApproverNames"""
q = db.query(CrmContractApproval).filter(CrmContractApproval.del_flag == 0)
if contract_id:
q = q.filter(CrmContractApproval.contract_id == contract_id)
q = q.filter(
(CrmContractApproval.approver_name.like("审批人%"))
| (CrmContractApproval.approver_name.like("用户%"))
)
rows = q.all()
update_count = 0
for row in rows:
nick = _resolve_user_display_name(db, row.approver_id)
if nick and nick != row.approver_name:
row.approver_name = nick
row.update_time = datetime.now()
update_count += 1
db.flush()
return update_count
def approve_transfer(
db: Session,
approval_id: str,
*,
target_approver_id: str,
transfer_reason: str = "",
user_id: str = None,
) -> bool:
"""Java: approveTransfer"""
entity = get_contract_approval(db, approval_id)
if not entity:
raise ValueError("审批记录不存在")
if user_id and entity.approver_id != user_id:
raise ValueError("只有审批人才能转审")
if entity.approval_result != "pending":
raise ValueError("该审批已处理,无法转审")
target_id = (target_approver_id or "").strip()
if not target_id:
raise ValueError("转审目标审批人不能为空")
entity.approver_id = target_id
entity.approver_name = _resolve_user_display_name(db, target_id) or f"用户{target_id}"
entity.remark = f"转审原因:{transfer_reason or ''}"
entity.update_by = user_id
entity.update_time = datetime.now()
db.flush()
return True
def export_contract_approvals(
db: Session,
tenant_no: str,
*,
filters: dict = None,
) -> List[Dict[str, Any]]:
rows, _ = list_contract_approvals_page(
db,
tenant_no,
page=1,
page_size=10000,
filters=filters or {},
)
return rows