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

@@ -0,0 +1,176 @@
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.db.base import Base
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.models.role import Role
from app.services.digital_employee_dashboard import DigitalEmployeeDashboardService
from app.services.digital_employee_reminder_task import DigitalEmployeeReminderTaskService
def build_session() -> Session:
engine = create_engine(
"sqlite+pysqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(bind=engine)
session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
return session_factory()
def test_digital_employee_reminder_task_generates_actionable_report() -> None:
now = datetime(2026, 6, 2, 2, 0, tzinfo=UTC)
with build_session() as db:
_seed_reminder_data(db, now)
result = DigitalEmployeeReminderTaskService(db).refresh_reminders(now=now)
summary = result["summary"]
report = result["report"]
assert result["task_type"] == "digital_employee_reminder_scan"
assert summary["recipient_count"] >= 3
assert summary["reminder_count"] >= 4
assert summary["approval_pending_count"] == 1
assert summary["budget_reminder_count"] == 1
assert summary["travel_application_reminder_count"] == 1
assert summary["reimbursement_overdue_count"] == 1
reminder_types = {
reminder["type"]
for recipient in report["recipients"]
for reminder in recipient["reminders"]
}
assert {
"approval_pending",
"budget_compilation",
"travel_application_expiry",
"reimbursement_overdue",
}.issubset(reminder_types)
dashboard = DigitalEmployeeDashboardService(db).build_dashboard(days=7)
assert dashboard.totals["reminders"] >= 4
assert dashboard.totals["businessOutputs"] >= 4
assert dashboard.task_distribution[0]["taskType"] == "digital_employee_reminder_scan"
def _seed_reminder_data(db: Session, now: datetime) -> None:
budget_role = Role(
id="role-budget",
role_code="budget_monitor",
name="预算管理员",
description="预算编制提醒接收人",
)
manager = Employee(
id="emp-manager",
employee_no="M001",
name="审批领导",
email="manager@example.com",
position="部门负责人",
grade="M2",
)
employee = Employee(
id="emp-user",
employee_no="E001",
name="出差员工",
email="employee@example.com",
position="客户经理",
grade="P5",
manager=manager,
finance_owner_name="财务BP",
)
budget_admin = Employee(
id="emp-budget",
employee_no="B001",
name="预算管理员甲",
email="budget@example.com",
position="预算管理员",
grade="P6",
roles=[budget_role],
)
db.add_all([budget_role, manager, employee, budget_admin])
db.add_all(
[
_claim(
"claim-approval",
"EXP-APPROVAL-001",
employee,
"travel",
"12000.00",
now - timedelta(days=3),
"submitted",
"直属领导审批",
),
_claim(
"claim-travel-app",
"APP-TRAVEL-001",
employee,
"travel_application",
"8000.00",
now - timedelta(days=1),
"approved",
"已审批",
risk_flags=[
{
"source": "application_detail",
"application_detail": {
"application_type": "差旅申请",
"time": "2026-06-01",
},
}
],
),
_claim(
"claim-supplement",
"EXP-SUPPLEMENT-001",
employee,
"meal",
"600.00",
now - timedelta(days=2),
"returned",
"材料待补",
),
]
)
db.commit()
def _claim(
claim_id: str,
claim_no: str,
employee: Employee,
expense_type: str,
amount: str,
happened_at: datetime,
status: str,
approval_stage: str,
*,
risk_flags: list[dict] | None = None,
) -> ExpenseClaim:
return ExpenseClaim(
id=claim_id,
claim_no=claim_no,
employee_id=employee.id,
employee_name=employee.name,
department_name="市场部",
expense_type=expense_type,
reason="客户拜访",
location="上海",
amount=Decimal(amount),
invoice_count=1,
occurred_at=happened_at,
submitted_at=happened_at,
status=status,
approval_stage=approval_stage,
risk_flags_json=risk_flags or [],
created_at=happened_at,
updated_at=happened_at,
)