322 lines
9.8 KiB
Python
322 lines
9.8 KiB
Python
"""CRM 报价单审批 CRUD — 对照 CrmQuoteApprovalServiceImpl.java"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.utils import generate_uuid
|
|
from app.models.crm_models import CrmQuote, CrmQuoteApproval
|
|
from app.models.models import SysUser
|
|
|
|
_FMT = "%Y-%m-%d %H:%M:%S"
|
|
|
|
|
|
def _row_to_dict(row: CrmQuoteApproval) -> Dict[str, Any]:
|
|
return {
|
|
"id": row.id,
|
|
"quoteId": row.quote_id or "",
|
|
"quoteNo": row.quote_no or "",
|
|
"step": row.step if row.step is not None else 0,
|
|
"approverId": row.approver_id or "",
|
|
"approverName": row.approver_name or "",
|
|
"approvalResult": row.approval_result or "pending",
|
|
"approvalOpinion": row.approval_opinion or "",
|
|
"approvalTime": row.approval_time.strftime(_FMT) if row.approval_time else "",
|
|
"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 list_quote_approvals(
|
|
db: Session,
|
|
quote_id: str,
|
|
*,
|
|
tenant_no: str = None,
|
|
user_info=None,
|
|
) -> List[Dict[str, Any]]:
|
|
"""Java: queryListByQuoteId(含审批人昵称)"""
|
|
if tenant_no and user_info:
|
|
from app.crud.crm.quote import _get_quote_in_scope
|
|
|
|
_get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
|
|
q = db.query(CrmQuoteApproval).filter(
|
|
CrmQuoteApproval.quote_id == quote_id,
|
|
CrmQuoteApproval.del_flag == 0,
|
|
)
|
|
if tenant_no:
|
|
q = q.filter(CrmQuoteApproval.tenant_no == tenant_no)
|
|
rows = q.order_by(CrmQuoteApproval.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 get_quote_approval(
|
|
db: Session,
|
|
approval_id: str,
|
|
*,
|
|
tenant_no: str = None,
|
|
) -> Optional[CrmQuoteApproval]:
|
|
q = db.query(CrmQuoteApproval).filter(
|
|
CrmQuoteApproval.id == approval_id,
|
|
CrmQuoteApproval.del_flag == 0,
|
|
)
|
|
if tenant_no:
|
|
q = q.filter(CrmQuoteApproval.tenant_no == tenant_no)
|
|
return q.first()
|
|
|
|
|
|
def create_approval_records(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
quote_id: str,
|
|
approver_ids: List[str],
|
|
user_id: str = None,
|
|
dept_id: str = "",
|
|
) -> None:
|
|
"""Java: createApprovalRecords"""
|
|
quote = (
|
|
db.query(CrmQuote)
|
|
.filter(CrmQuote.id == quote_id, CrmQuote.del_flag == 0, CrmQuote.tenant_no == tenant_no)
|
|
.first()
|
|
)
|
|
if not quote:
|
|
raise ValueError("报价单不存在")
|
|
|
|
step = 1
|
|
for approver_id in approver_ids:
|
|
aid = (approver_id or "").strip()
|
|
if not aid:
|
|
continue
|
|
nick = _resolve_user_display_name(db, aid) or f"用户{aid}"
|
|
entity = CrmQuoteApproval(
|
|
id=generate_uuid(),
|
|
quote_id=quote_id,
|
|
quote_no=quote.quote_no or "",
|
|
step=step,
|
|
approver_id=aid,
|
|
approver_name=nick,
|
|
approval_result="pending",
|
|
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 approve_quote(
|
|
db: Session,
|
|
approval_id: str,
|
|
*,
|
|
tenant_no: str = None,
|
|
approval_result: str,
|
|
approval_opinion: str = "",
|
|
remark: str = "",
|
|
user_id: str = None,
|
|
) -> bool:
|
|
"""Java: approveQuote"""
|
|
entity = get_quote_approval(db, approval_id, tenant_no=tenant_no)
|
|
if not entity:
|
|
raise ValueError("审批记录不存在")
|
|
if user_id and entity.approver_id != user_id:
|
|
raise ValueError("只有审批人才能审批")
|
|
if entity.approval_result != "pending":
|
|
raise ValueError("该审批已处理")
|
|
if user_id and not can_user_approve(db, entity.quote_id, user_id):
|
|
raise ValueError("前置审批步骤尚未完成,暂时无法审批")
|
|
|
|
entity.approval_result = approval_result
|
|
entity.approval_opinion = approval_opinion or ""
|
|
entity.approval_time = datetime.now()
|
|
entity.remark = remark or entity.remark or ""
|
|
entity.update_by = user_id
|
|
entity.update_time = datetime.now()
|
|
|
|
quote_q = db.query(CrmQuote).filter(
|
|
CrmQuote.id == entity.quote_id,
|
|
CrmQuote.del_flag == 0,
|
|
)
|
|
if tenant_no:
|
|
quote_q = quote_q.filter(CrmQuote.tenant_no == tenant_no)
|
|
quote = quote_q.first()
|
|
if quote:
|
|
if approval_result == "approved":
|
|
pending_count = (
|
|
db.query(CrmQuoteApproval)
|
|
.filter(
|
|
CrmQuoteApproval.quote_id == entity.quote_id,
|
|
CrmQuoteApproval.approval_result == "pending",
|
|
CrmQuoteApproval.del_flag == 0,
|
|
CrmQuoteApproval.id != entity.id,
|
|
)
|
|
.count()
|
|
)
|
|
if pending_count == 0:
|
|
quote.status = "accepted"
|
|
quote.update_by = user_id
|
|
quote.update_time = datetime.now()
|
|
elif approval_result == "rejected":
|
|
quote.status = "rejected"
|
|
quote.reject_reason = f"审批拒绝: {approval_opinion or ''}".strip()
|
|
quote.update_by = user_id
|
|
quote.update_time = datetime.now()
|
|
|
|
db.commit()
|
|
return True
|
|
|
|
|
|
def withdraw_approval(
|
|
db: Session,
|
|
quote_id: str,
|
|
*,
|
|
tenant_no: str = None,
|
|
remark: str = "",
|
|
user_id: str = None,
|
|
) -> bool:
|
|
"""Java: CrmQuoteApprovalServiceImpl.withdrawApproval(不校验创建人)"""
|
|
quote_q = db.query(CrmQuote).filter(
|
|
CrmQuote.id == quote_id,
|
|
CrmQuote.del_flag == 0,
|
|
)
|
|
if tenant_no:
|
|
quote_q = quote_q.filter(CrmQuote.tenant_no == tenant_no)
|
|
quote = quote_q.first()
|
|
if not quote:
|
|
raise ValueError("报价单不存在")
|
|
if quote.status != "submitted":
|
|
raise ValueError("只能撤回已提交的报价单")
|
|
|
|
delete_pending_approvals(db, quote_id)
|
|
quote.status = "draft"
|
|
if remark:
|
|
quote.remark = remark
|
|
quote.update_by = user_id
|
|
quote.update_time = datetime.now()
|
|
db.commit()
|
|
return True
|
|
|
|
|
|
def delete_pending_approvals(db: Session, quote_id: str) -> int:
|
|
"""Java: deletePendingApprovals"""
|
|
rows = (
|
|
db.query(CrmQuoteApproval)
|
|
.filter(
|
|
CrmQuoteApproval.quote_id == quote_id,
|
|
CrmQuoteApproval.approval_result == "pending",
|
|
CrmQuoteApproval.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, quote_id: str) -> int:
|
|
"""作废该报价单全部有效审批记录(含已通过/已拒绝),用于再编辑或重新提审。"""
|
|
rows = (
|
|
db.query(CrmQuoteApproval)
|
|
.filter(
|
|
CrmQuoteApproval.quote_id == quote_id,
|
|
CrmQuoteApproval.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 refresh_approver_names(db: Session, quote_id: str = None) -> int:
|
|
"""Java: refreshApproverNames"""
|
|
q = db.query(CrmQuoteApproval).filter(CrmQuoteApproval.del_flag == 0)
|
|
if quote_id:
|
|
q = q.filter(CrmQuoteApproval.quote_id == quote_id)
|
|
q = q.filter(
|
|
(CrmQuoteApproval.approver_name.like("审批人%"))
|
|
| (CrmQuoteApproval.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 can_user_approve(db: Session, quote_id: str, user_id: str) -> bool:
|
|
"""Java: canUserApprove
|
|
|
|
同一审批人若同时存在历史已处理记录与当前轮 pending,优先取 pending(再提审场景)。
|
|
前序校验仅看「当前轮」(以最早一条 pending 的创建时间为界),忽略历史轮次。
|
|
"""
|
|
if not quote_id or not user_id:
|
|
return False
|
|
|
|
approvals = (
|
|
db.query(CrmQuoteApproval)
|
|
.filter(
|
|
CrmQuoteApproval.quote_id == quote_id,
|
|
CrmQuoteApproval.del_flag == 0,
|
|
)
|
|
.order_by(CrmQuoteApproval.step.asc(), CrmQuoteApproval.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":
|
|
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
|