feat: 数字员工财务报告体系与定时提醒及看板快照调度

- 新增数字员工财务报告生成、邮件投递与渲染调度器
- 引入员工画像扫描调度与定时提醒任务
- 完善财务看板快照、排行口径与部门人员占比计算
- 优化数字员工工作看板仪表盘与技能目录
- 增强前端总览页图表、工作台摘要与顶部导航栏交互
- 新增差旅申请规划推动提醒与报销创建会话状态管理
- 补充财务报告、看板调度、数字员工工作记录测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-03 09:25:23 +08:00
parent 0c74b4ab4a
commit 15006a05a7
114 changed files with 7356 additions and 650 deletions

View File

@@ -178,12 +178,17 @@ class UserAgentApplicationMixin:
step = self._resolve_expense_application_step(payload, facts)
application_claim = None
if step == "submitted":
application_claim = self._find_duplicate_expense_application_record(payload, facts)
if application_claim is not None:
step = "duplicate"
facts["duplicate_application_stage"] = str(application_claim.approval_stage or "").strip()
editable_claim = self._find_editable_expense_application_record(payload)
if editable_claim is not None:
application_claim = self._update_expense_application_record(payload, facts, editable_claim)
facts["application_edit_mode"] = "true"
else:
application_claim = self._create_expense_application_record(payload, facts)
application_claim = self._find_duplicate_expense_application_record(payload, facts)
if application_claim is not None:
step = "duplicate"
facts["duplicate_application_stage"] = str(application_claim.approval_stage or "").strip()
else:
application_claim = self._create_expense_application_record(payload, facts)
facts["application_no"] = application_claim.claim_no
facts["application_claim_id"] = application_claim.id
facts["manager_name"] = self._resolve_application_manager_name(payload, application_claim)
@@ -229,9 +234,14 @@ class UserAgentApplicationMixin:
if step == "submitted":
application_no = str(facts.get("application_no") or "").strip() or self._build_application_claim_no(payload, facts)
manager_name = str(facts.get("manager_name") or "").strip() or "直属领导"
submitted_title = (
"申请单据已修改并重新提交,已进入审批流程。"
if str(facts.get("application_edit_mode") or "").strip().lower() == "true"
else "申请单据已生成,并已进入审批流程。"
)
return "\n\n".join(
[
"申请单据已生成,并已进入审批流程。",
submitted_title,
f"系统已推送给 {manager_name} 审核,当前节点:{manager_name}审核中。",
f"申请单号:{application_no}",
"下方是简要单据信息。需要查看完整详情时,请点击快捷方式进入单据详情。",
@@ -930,6 +940,101 @@ class UserAgentApplicationMixin:
return "会务费用申请"
return "差旅费用申请"
@staticmethod
def _resolve_application_edit_claim_id(context_json: dict[str, object]) -> str:
if not isinstance(context_json, dict):
return ""
is_edit_mode = bool(context_json.get("application_edit_mode") or context_json.get("applicationEditMode"))
claim_id = str(
context_json.get("application_edit_claim_id")
or context_json.get("applicationEditClaimId")
or ""
).strip()
return claim_id if is_edit_mode and claim_id else ""
@staticmethod
def _is_expense_application_claim_like(claim: ExpenseClaim) -> bool:
expense_type = str(claim.expense_type or "").strip().lower()
claim_no = str(claim.claim_no or "").strip().upper()
flags = claim.risk_flags_json
if isinstance(flags, dict):
flags = [flags]
if not isinstance(flags, list):
flags = []
has_application_detail = any(
isinstance(flag, dict)
and (
str(flag.get("business_stage") or "").strip() == "expense_application"
or isinstance(flag.get("application_detail"), dict)
)
for flag in flags
)
return (
expense_type in {"application", "expense_application"}
or expense_type.endswith("_application")
or claim_no.startswith("AP-")
or claim_no.startswith("APP-")
or has_application_detail
)
def _find_editable_expense_application_record(
self,
payload: UserAgentRequest,
) -> ExpenseClaim | None:
claim_id = self._resolve_application_edit_claim_id(payload.context_json or {})
if not claim_id:
return None
claim = self.db.get(ExpenseClaim, claim_id)
if claim is None:
raise ValueError("未找到要修改的申请单。")
if not self._is_expense_application_claim_like(claim):
raise ValueError("只能修改申请单。")
current_user = self._build_application_current_user(payload)
access_policy = ExpenseClaimAccessPolicy(self.db)
if not (current_user.is_admin or access_policy.is_claim_owned_by_current_user(claim, current_user)):
raise ValueError("只能修改本人被退回的申请单。")
status = str(claim.status or "").strip().lower()
if status not in {"returned", "draft", "supplement"}:
raise ValueError("当前申请单状态不支持修改。")
return claim
def _update_expense_application_record(
self,
payload: UserAgentRequest,
facts: dict[str, str],
claim: ExpenseClaim,
) -> ExpenseClaim:
current_user = self._build_application_current_user(payload)
flags = claim.risk_flags_json
if isinstance(flags, dict):
flags = [flags]
if not isinstance(flags, list):
flags = []
preserved_flags = [
flag
for flag in flags
if not (
isinstance(flag, dict)
and str(flag.get("source") or "").strip() == "application_detail"
)
]
claim.expense_type = self._resolve_application_expense_type_code(facts)
claim.reason = str(facts.get("reason") or "费用申请").strip() or "费用申请"
claim.location = str(facts.get("location") or "待补充").strip() or "待补充"
claim.amount = self._parse_application_amount_to_decimal(facts.get("amount", ""))
claim.occurred_at = self._parse_application_occurred_at(facts.get("time", ""))
claim.risk_flags_json = [*preserved_flags, self._build_application_detail_flag(facts)]
from app.services.expense_claims import ExpenseClaimService
submitted = ExpenseClaimService(self.db).submit_claim(claim.id, current_user)
if submitted is None:
raise ValueError("未找到可修改的申请单。")
return submitted
def _create_expense_application_record(
self,
payload: UserAgentRequest,