feat: 财务看板口径重构与半年模拟数据及报销状态注册表
- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选 - 引入 expense_claim_status_registry 统一报销状态流转 - 完善报销草稿流程、Item Sync 与本体解析器 - 优化总览页趋势图、分页组件与请求进度步骤 - 增强报销申请快速预览、本体工具与详情展示 - 新增半年报销模拟数据种子脚本与状态审计工具 - 补充财务看板、报销状态注册与模拟数据测试覆盖
This commit is contained in:
@@ -14,8 +14,10 @@ from app.schemas.user_agent import (
|
||||
UserAgentResponse,
|
||||
UserAgentSuggestedAction,
|
||||
)
|
||||
from app.schemas.reimbursement import TravelReimbursementCalculatorRequest
|
||||
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
|
||||
from app.services.expense_claim_risk_stage import with_risk_business_stage
|
||||
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
|
||||
from app.services.document_numbering import (
|
||||
build_document_number,
|
||||
generate_unique_expense_claim_no,
|
||||
@@ -25,6 +27,11 @@ from app.services.user_agent_application_dates import (
|
||||
resolve_application_days_from_time_range,
|
||||
)
|
||||
from app.services.user_agent_application_locations import normalize_application_location
|
||||
from app.services.user_agent_application_summary import (
|
||||
build_application_summary,
|
||||
build_application_summary_table,
|
||||
resolve_application_time_label,
|
||||
)
|
||||
from app.services.application_system_estimate import apply_application_system_estimate_to_facts
|
||||
|
||||
APPLICATION_CONTEXT_VALUES = {
|
||||
@@ -35,7 +42,7 @@ APPLICATION_CONTEXT_VALUES = {
|
||||
"preapproval",
|
||||
}
|
||||
APPLICATION_BASE_FIELDS = ("time", "location", "reason")
|
||||
APPLICATION_TIME_LABELS = ("行程时间", "招待时间", "申请时间", "发生时间", "业务发生时间", "时间")
|
||||
APPLICATION_TIME_LABELS = ("行程时间", "出发时间", "返回时间", "招待时间", "申请时间", "发生时间", "业务发生时间", "时间")
|
||||
APPLICATION_FIELD_LABELS = (
|
||||
"申请类型",
|
||||
"费用类型",
|
||||
@@ -202,7 +209,7 @@ class UserAgentApplicationMixin:
|
||||
facts: dict[str, str],
|
||||
step: str,
|
||||
) -> str:
|
||||
recognized_table = self._build_application_summary_table(facts, include_empty=False)
|
||||
recognized_table = build_application_summary_table(facts, include_empty=False)
|
||||
|
||||
if step == "ask_missing":
|
||||
missing_fields = self._resolve_application_missing_fields(facts)
|
||||
@@ -234,7 +241,7 @@ class UserAgentApplicationMixin:
|
||||
if step == "duplicate":
|
||||
application_no = str(facts.get("application_no") or "").strip()
|
||||
stage = str(facts.get("duplicate_application_stage") or "").strip() or "处理中"
|
||||
time_label = self._resolve_application_time_label(facts)
|
||||
time_label = resolve_application_time_label(facts)
|
||||
return "\n\n".join(
|
||||
[
|
||||
f"检测到同一申请人、同一申请类型、同一{time_label}已存在申请单,系统没有重复创建。",
|
||||
@@ -247,7 +254,7 @@ class UserAgentApplicationMixin:
|
||||
return "\n\n".join(
|
||||
[
|
||||
"这是费用申请核对结果,请核对:",
|
||||
self._build_application_summary_table(facts),
|
||||
build_application_summary_table(facts),
|
||||
"请核对上述信息无误,确认无误后 [确认](#application-submit) 提交至审批流程。",
|
||||
]
|
||||
)
|
||||
@@ -375,9 +382,71 @@ class UserAgentApplicationMixin:
|
||||
range_days = resolve_application_days_from_time_range(facts.get("time", ""))
|
||||
if range_days:
|
||||
facts["days"] = f"{range_days}天"
|
||||
self._apply_rule_center_travel_policy_to_application_facts(payload, facts)
|
||||
apply_application_system_estimate_to_facts(facts)
|
||||
return facts
|
||||
|
||||
def _apply_rule_center_travel_policy_to_application_facts(
|
||||
self,
|
||||
payload: UserAgentRequest,
|
||||
facts: dict[str, str],
|
||||
) -> None:
|
||||
if "差旅" not in str(facts.get("application_type") or "") and "出差" not in str(facts.get("application_type") or ""):
|
||||
return
|
||||
|
||||
location = str(facts.get("location") or "").strip()
|
||||
grade = str(facts.get("grade") or "").strip()
|
||||
if not location or not grade:
|
||||
return
|
||||
|
||||
days = self._parse_application_days_count(facts.get("days", "")) or 1
|
||||
try:
|
||||
result = TravelReimbursementCalculatorService(self.db).calculate(
|
||||
TravelReimbursementCalculatorRequest(days=days, location=location, grade=grade),
|
||||
self._build_application_current_user(payload),
|
||||
)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
hotel_rate = self._format_application_policy_money(result.hotel_rate)
|
||||
hotel_amount = self._format_application_policy_money(result.hotel_amount)
|
||||
allowance_rate = self._format_application_policy_money(result.total_allowance_rate)
|
||||
allowance_amount = self._format_application_policy_money(result.allowance_amount)
|
||||
if hotel_rate:
|
||||
facts["lodging_daily_cap"] = f"{hotel_rate}元/天"
|
||||
if hotel_amount:
|
||||
facts["hotel_amount"] = f"{hotel_amount}元"
|
||||
if allowance_rate:
|
||||
facts["subsidy_daily_cap"] = f"{allowance_rate}元/天"
|
||||
if allowance_amount:
|
||||
facts["allowance_amount"] = f"{allowance_amount}元"
|
||||
if str(result.matched_city or "").strip():
|
||||
facts["matched_city"] = str(result.matched_city).strip()
|
||||
if str(result.rule_name or "").strip():
|
||||
facts["rule_name"] = str(result.rule_name).strip()
|
||||
if str(result.rule_version or "").strip():
|
||||
facts["rule_version"] = str(result.rule_version).strip()
|
||||
|
||||
@staticmethod
|
||||
def _format_application_policy_money(value: object) -> str:
|
||||
try:
|
||||
amount = Decimal(str(value or "0")).quantize(Decimal("0.01"))
|
||||
except (InvalidOperation, ValueError):
|
||||
return ""
|
||||
if amount == amount.to_integral():
|
||||
return f"{int(amount):,}"
|
||||
return f"{amount:,.2f}".rstrip("0").rstrip(".")
|
||||
|
||||
@staticmethod
|
||||
def _parse_application_days_count(value: object) -> int:
|
||||
match = re.search(r"\d+", str(value or ""))
|
||||
if not match:
|
||||
return 0
|
||||
try:
|
||||
return max(0, int(match.group(0)))
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _resolve_application_preview_facts(context_json: dict[str, object]) -> dict[str, str]:
|
||||
preview = context_json.get("application_preview")
|
||||
@@ -496,6 +565,17 @@ class UserAgentApplicationMixin:
|
||||
|
||||
@staticmethod
|
||||
def _resolve_application_time_from_text(message: str) -> str:
|
||||
departure_time = UserAgentApplicationMixin._resolve_application_labeled_value(
|
||||
message,
|
||||
("出发时间", "出发日期"),
|
||||
)
|
||||
return_time = UserAgentApplicationMixin._resolve_application_labeled_value(
|
||||
message,
|
||||
("返回时间", "返回日期"),
|
||||
)
|
||||
if departure_time and return_time:
|
||||
return departure_time if departure_time == return_time else f"{departure_time} 至 {return_time}"
|
||||
|
||||
labeled = UserAgentApplicationMixin._resolve_application_labeled_value(
|
||||
message,
|
||||
APPLICATION_TIME_LABELS,
|
||||
@@ -543,6 +623,13 @@ class UserAgentApplicationMixin:
|
||||
@staticmethod
|
||||
def _resolve_application_labeled_value(message: str, labels: tuple[str, ...]) -> str:
|
||||
label_pattern = "|".join(re.escape(label) for label in labels)
|
||||
table_match = re.search(
|
||||
rf"\|\s*(?:{label_pattern})\s*\|\s*(?P<value>[^|\n]+?)\s*\|",
|
||||
str(message or ""),
|
||||
)
|
||||
if table_match:
|
||||
return table_match.group("value").strip()
|
||||
|
||||
next_label_pattern = "|".join(re.escape(label) for label in APPLICATION_FIELD_LABELS)
|
||||
match = re.search(
|
||||
rf"(?:{label_pattern})[::]\s*(?P<value>[\s\S]*?)(?=\s*(?:{next_label_pattern})[::]|[\n,。;;]|$)",
|
||||
@@ -644,7 +731,7 @@ class UserAgentApplicationMixin:
|
||||
return ""
|
||||
|
||||
text = re.sub(
|
||||
r"^(?:行程时间|招待时间|发生时间|业务发生时间|申请时间|时间|地点|业务地点|发生地点|目的地|天数|出差天数|申请天数|出行方式|交通方式|交通工具|出行工具|用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额|费用)[::]\s*",
|
||||
r"^(?:行程时间|出发时间|返回时间|招待时间|发生时间|业务发生时间|申请时间|时间|地点|业务地点|发生地点|目的地|天数|出差天数|申请天数|出行方式|交通方式|交通工具|出行工具|用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额|费用)[::]\s*",
|
||||
"",
|
||||
text,
|
||||
)
|
||||
@@ -843,73 +930,6 @@ class UserAgentApplicationMixin:
|
||||
return "会务费用申请"
|
||||
return "差旅费用申请"
|
||||
|
||||
@staticmethod
|
||||
def _resolve_application_time_label(facts: dict[str, str]) -> str:
|
||||
application_type = str(facts.get("application_type") or "").strip()
|
||||
if "差旅" in application_type or "出差" in application_type:
|
||||
return "行程时间"
|
||||
if "招待" in application_type or "宴请" in application_type or "餐饮" in application_type:
|
||||
return "招待时间"
|
||||
return "申请时间"
|
||||
|
||||
@classmethod
|
||||
def _build_application_summary(cls, facts: dict[str, str]) -> str:
|
||||
time_label = cls._resolve_application_time_label(facts)
|
||||
return "\n".join(
|
||||
f"{label}:{value or '待补充'}"
|
||||
for label, value in (
|
||||
("申请类型", facts.get("application_type", "")),
|
||||
("姓名", facts.get("applicant", "")),
|
||||
("部门", facts.get("department", "")),
|
||||
("岗位", facts.get("position", "")),
|
||||
("职级", facts.get("grade", "")),
|
||||
("直属领导", facts.get("manager_name", "")),
|
||||
(time_label, facts.get("time", "")),
|
||||
("地点", facts.get("location", "")),
|
||||
("事由", facts.get("reason", "")),
|
||||
("天数", facts.get("days", "")),
|
||||
("出行方式", facts.get("transport_mode", "")),
|
||||
("住宿上限/天", facts.get("lodging_daily_cap", "")),
|
||||
("补贴标准/天", facts.get("subsidy_daily_cap", "")),
|
||||
("交通费用口径", facts.get("transport_policy", "")),
|
||||
("规则测算参考", facts.get("policy_estimate", "")),
|
||||
("系统预估费用", facts.get("amount", "")),
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _build_application_summary_table(
|
||||
cls,
|
||||
facts: dict[str, str],
|
||||
*,
|
||||
include_empty: bool = True,
|
||||
) -> str:
|
||||
time_label = cls._resolve_application_time_label(facts)
|
||||
rows = [
|
||||
("申请类型", facts.get("application_type", "")),
|
||||
("姓名", facts.get("applicant", "")),
|
||||
("部门", facts.get("department", "")),
|
||||
("岗位", facts.get("position", "")),
|
||||
("职级", facts.get("grade", "")),
|
||||
("直属领导", facts.get("manager_name", "")),
|
||||
(time_label, facts.get("time", "")),
|
||||
("地点", facts.get("location", "")),
|
||||
("事由", facts.get("reason", "")),
|
||||
("天数", facts.get("days", "")),
|
||||
("出行方式", facts.get("transport_mode", "")),
|
||||
("住宿上限/天", facts.get("lodging_daily_cap", "")),
|
||||
("补贴标准/天", facts.get("subsidy_daily_cap", "")),
|
||||
("交通费用口径", facts.get("transport_policy", "")),
|
||||
("规则测算参考", facts.get("policy_estimate", "")),
|
||||
("系统预估费用", facts.get("amount", "")),
|
||||
]
|
||||
visible_rows = rows if include_empty else [(label, value) for label, value in rows if str(value or "").strip()]
|
||||
if not visible_rows:
|
||||
visible_rows = [("申请描述", "已收到,正在按费用申请上下文继续整理")]
|
||||
lines = ["| 字段 | 内容 |", "| --- | --- |"]
|
||||
lines.extend(f"| {label} | {value or '待补充'} |" for label, value in visible_rows)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _create_expense_application_record(
|
||||
self,
|
||||
payload: UserAgentRequest,
|
||||
@@ -1204,7 +1224,7 @@ class UserAgentApplicationMixin:
|
||||
return UserAgentDraftPayload(
|
||||
draft_type="expense_application",
|
||||
title=str(facts.get("application_type") or "费用申请").strip() or "费用申请",
|
||||
body=self._build_application_summary(facts),
|
||||
body=build_application_summary(facts),
|
||||
confirmation_required=False,
|
||||
claim_id=claim.id,
|
||||
claim_no=claim.claim_no,
|
||||
|
||||
Reference in New Issue
Block a user