Files
shanxia2.0/backend/app/crud/crm/quote_approval.py
T
2026-08-11 07:54:28 +08:00

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