feat: 完善文档中心与报销申请交互及侧边栏重构

后端优化编排器报销查询和本体检测精度,增强报销单草稿保
存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导
航,完善文档中心状态筛选和详情提示,报销创建和审批详情
页优化会话管理和费用明细交互,新增助手应用服务和预设动
作工具函数,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-25 13:35:39 +08:00
parent 50b1c3f9a9
commit d0e946cf47
59 changed files with 5117 additions and 416 deletions

View File

@@ -143,6 +143,40 @@ class ExpenseClaimService(
self._attachment_storage = ExpenseClaimAttachmentStorage()
self._attachment_presentation = ExpenseClaimAttachmentPresentation(self._attachment_storage)
@staticmethod
def _is_expense_application_claim(claim: ExpenseClaim) -> bool:
claim_no = str(getattr(claim, "claim_no", "") or "").strip().upper()
expense_type = str(getattr(claim, "expense_type", "") or "").strip().lower()
document_type = str(
getattr(claim, "document_type_code", "")
or getattr(claim, "document_type", "")
or ""
).strip().lower()
return (
claim_no.startswith("APP-")
or expense_type == "application"
or expense_type.endswith("_application")
or document_type in {"application", "expense_application"}
)
def _validate_application_claim_for_submission(self, claim: ExpenseClaim) -> list[str]:
issues: list[str] = []
if self._is_missing_value(claim.employee_name):
issues.append("申请人未完善")
if self._is_missing_value(claim.department_name):
issues.append("所属部门未完善")
if self._is_missing_value(claim.expense_type):
issues.append("申请类型未完善")
if self._is_missing_value(claim.reason):
issues.append("申请事由未完善")
if self._is_missing_value(claim.location):
issues.append("业务地点未完善")
if claim.amount is None or claim.amount <= Decimal("0.00"):
issues.append("预计总费用未完善")
if claim.occurred_at is None:
issues.append("申请时间未完善")
return issues
def list_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]:
stmt = (
select(ExpenseClaim)
@@ -389,18 +423,51 @@ class ExpenseClaimService(
self._ensure_draft_claim(claim)
self._access_policy.backfill_claim_identity_from_current_user(claim, current_user)
self._sync_claim_from_items(claim)
missing_fields = self._validate_claim_for_submission(claim)
is_application_claim = self._is_expense_application_claim(claim)
if not is_application_claim:
self._sync_claim_from_items(claim)
missing_fields = (
self._validate_application_claim_for_submission(claim)
if is_application_claim
else self._validate_claim_for_submission(claim)
)
if missing_fields:
raise ExpenseClaimSubmissionBlockedError(missing_fields)
before_json = self._serialize_claim(claim)
review_result = self._run_ai_submission_review(claim)
if is_application_claim:
submitted_at = datetime.now(UTC)
preserved_flags = [
flag
for flag in list(claim.risk_flags_json or [])
if not (
isinstance(flag, dict)
and str(flag.get("source") or "").strip() in {"submission_review", "attachment_analysis"}
)
]
submit_flag = {
"source": "application_submission",
"event_type": "expense_application_submission",
"severity": "info",
"label": "申请提交",
"message": "费用申请已提交至直属领导审批,并同步纳入预算管理口径。",
"previous_status": str(claim.status or "").strip(),
"previous_approval_stage": str(claim.approval_stage or "").strip(),
"next_status": "submitted",
"next_approval_stage": "直属领导审批",
"created_at": submitted_at.isoformat(),
}
claim.status = "submitted"
claim.approval_stage = "直属领导审批"
claim.risk_flags_json = [*preserved_flags, submit_flag]
claim.submitted_at = submitted_at
else:
review_result = self._run_ai_submission_review(claim)
claim.status = str(review_result.get("status") or "supplement")
claim.approval_stage = str(review_result.get("approval_stage") or "待补充")
claim.risk_flags_json = list(review_result.get("risk_flags") or [])
claim.submitted_at = datetime.now(UTC) if claim.status == "submitted" else None
claim.status = str(review_result.get("status") or "supplement")
claim.approval_stage = str(review_result.get("approval_stage") or "待补充")
claim.risk_flags_json = list(review_result.get("risk_flags") or [])
claim.submitted_at = datetime.now(UTC) if claim.status == "submitted" else None
self.db.commit()
self.db.refresh(claim)
@@ -562,19 +629,29 @@ class ExpenseClaimService(
normalized_status = str(claim.status or "").strip().lower()
if normalized_status != "submitted":
raise ValueError("只有审批中的报销单可以审批通过。")
raise ValueError("只有审批中的单可以审批通过。")
previous_stage = str(claim.approval_stage or "").strip()
is_application_claim = self._is_expense_application_claim(claim)
if previous_stage == "直属领导审批":
if not self._access_policy.can_approve_claim(current_user, claim):
raise ValueError("只有当前直属领导审批人可以审批通过该报销单。")
raise ValueError("只有当前直属领导审批人可以审批通过该单")
approval_source = "manual_approval"
event_type = "expense_claim_approval"
label = "领导审批通过"
next_status = "submitted"
next_stage = "财务审批"
default_message = "{operator} 已审批通过,流转至{next_stage}"
if is_application_claim:
event_type = "expense_application_approval"
label = "领导审批通过"
next_status = "approved"
next_stage = "审批完成"
default_message = "{operator} 已审批通过,申请流程完成。"
else:
event_type = "expense_claim_approval"
label = "领导审批通过"
next_status = "submitted"
next_stage = "财务审批"
default_message = "{operator} 已审批通过,流转至{next_stage}"
elif previous_stage == "财务审批":
if is_application_claim:
raise ValueError("费用申请无需财务审批,直属领导审批通过后即完成。")
if not self._access_policy.can_approve_claim(current_user, claim):
raise ValueError("只有财务人员可以完成财务终审。")
approval_source = "finance_approval"
@@ -606,7 +683,7 @@ class ExpenseClaimService(
],
"previous_status": str(claim.status or "").strip(),
"previous_approval_stage": previous_stage,
"next_status": "submitted",
"next_status": next_status,
"next_approval_stage": next_stage,
"created_at": datetime.now(UTC).isoformat(),
}