from __future__ import annotations import uuid from datetime import UTC, datetime from typing import Any from app.api.deps import CurrentUserContext from app.services.expense_claim_workflow_constants import ( APPROVAL_DONE_STAGE, ARCHIVE_ACCOUNTING_STAGE, BUDGET_MANAGER_APPROVAL_STAGE, DIRECT_MANAGER_APPROVAL_STAGE, FINANCE_APPROVAL_STAGE, ) class ExpenseClaimApprovalFlowMixin: def approve_claim( self, claim_id: str, current_user: CurrentUserContext, *, opinion: str | None = None, ): claim = self.get_claim(claim_id, current_user) if claim is None: return None normalized_status = str(claim.status or "").strip().lower() if normalized_status != "submitted": raise ValueError("只有审批中的单据可以审批通过。") previous_stage = str(claim.approval_stage or "").strip() is_application_claim = self._is_expense_application_claim(claim) next_budget_manager = None if previous_stage == DIRECT_MANAGER_APPROVAL_STAGE: if not self._access_policy.can_approve_claim(current_user, claim): raise ValueError("只有当前直属领导审批人可以审批通过该单据。") approval_source = "manual_approval" event_type = "expense_application_approval" if is_application_claim else "expense_claim_approval" label = "领导审批通过" if is_application_claim: next_budget_manager = self._access_policy.resolve_department_budget_manager(claim) next_status = "submitted" next_stage = BUDGET_MANAGER_APPROVAL_STAGE default_message = "{operator} 已确认直属领导审核,流转至预算管理者审批。" else: next_status = "submitted" next_stage = FINANCE_APPROVAL_STAGE default_message = "{operator} 已审批通过,流转至{next_stage}。" elif previous_stage == BUDGET_MANAGER_APPROVAL_STAGE: if not is_application_claim: raise ValueError("只有费用申请需要预算管理者审批。") if not self._access_policy.can_approve_claim(current_user, claim): raise ValueError("只有当前预算管理者可以审批通过该费用申请。") approval_source = "budget_approval" event_type = "expense_application_budget_approval" label = "预算管理者审核通过" next_status = "approved" next_stage = APPROVAL_DONE_STAGE default_message = "{operator} 已完成预算审核,申请流程完成并生成报销草稿。" elif previous_stage == FINANCE_APPROVAL_STAGE: if is_application_claim: raise ValueError("费用申请需先完成预算管理者审批。") if not self._access_policy.can_approve_claim(current_user, claim): raise ValueError("只有财务人员可以完成财务终审。") approval_source = "finance_approval" event_type = "expense_claim_finance_approval" label = "财务审核通过" next_status = "approved" next_stage = ARCHIVE_ACCOUNTING_STAGE default_message = "{operator} 已完成财务审核,进入归档入账。" else: raise ValueError("当前节点不支持审批通过。") approval_opinion = str(opinion or "").strip() if previous_stage in {DIRECT_MANAGER_APPROVAL_STAGE, BUDGET_MANAGER_APPROVAL_STAGE} and not approval_opinion: approval_opinion = "同意" before_json = self._serialize_claim(claim) operator = self._access_policy.resolve_current_user_display_name(current_user) budget_flags: list[dict[str, Any]] = [] if approval_source == "finance_approval" and not is_application_claim: consumed_budget_flag = self._consume_budget_for_finance_approval(claim, current_user) if consumed_budget_flag is not None: budget_flags.append(consumed_budget_flag) approval_flag = { "source": approval_source, "event_type": event_type, "approval_event_id": str(uuid.uuid4()), "severity": "info", "label": label, "message": approval_opinion or default_message.format(operator=operator, next_stage=next_stage), "opinion": approval_opinion, "operator": operator, "operator_username": current_user.username, "operator_role_codes": [ str(item).strip().lower() for item in current_user.role_codes if str(item).strip() ], "previous_status": str(claim.status or "").strip(), "previous_approval_stage": previous_stage, "next_status": next_status, "next_approval_stage": next_stage, "created_at": datetime.now(UTC).isoformat(), } if next_budget_manager is not None: approval_flag.update( { "next_approver_name": str(next_budget_manager.name or "").strip(), "next_approver_employee_id": next_budget_manager.id, "next_approver_grade": str(next_budget_manager.grade or "").strip(), "next_approver_role_code": "budget_monitor", } ) claim.status = next_status claim.approval_stage = next_stage if claim.submitted_at is None: claim.submitted_at = datetime.now(UTC) if is_application_claim and previous_stage == BUDGET_MANAGER_APPROVAL_STAGE: approval_flag["leader_opinion"] = self._resolve_latest_approval_opinion( claim, source="manual_approval", ) approval_flag["budget_opinion"] = approval_opinion generated_draft = self._create_reimbursement_draft_from_application( application_claim=claim, approval_flag=approval_flag, operator=operator, ) transferred_budget_flag = self._transfer_application_budget_to_reimbursement( application_claim=claim, draft_claim=generated_draft, current_user=current_user, ) if transferred_budget_flag is not None: budget_flags.append(transferred_budget_flag) generated_draft.risk_flags_json = self._append_budget_flags( generated_draft.risk_flags_json, transferred_budget_flag, ) claim.risk_flags_json = self._append_budget_flags( [*list(claim.risk_flags_json or []), approval_flag], budget_flags, ) self.db.commit() self.db.refresh(claim) self.audit_service.log_action( actor=operator, action="expense_claim.approve", resource_type="expense_claim", resource_id=claim.id, before_json=before_json, after_json=self._serialize_claim(claim), ) return claim @staticmethod def _resolve_latest_approval_opinion(claim, *, source: str) -> str: for flag in reversed(list(claim.risk_flags_json or [])): if not isinstance(flag, dict): continue if str(flag.get("source") or "").strip() != source: continue opinion = str(flag.get("opinion") or flag.get("message") or "").strip() if opinion: return opinion return ""