470 lines
15 KiB
Python
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
|