feat: 财务看板口径重构与半年模拟数据及报销状态注册表
- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选 - 引入 expense_claim_status_registry 统一报销状态流转 - 完善报销草稿流程、Item Sync 与本体解析器 - 优化总览页趋势图、分页组件与请求进度步骤 - 增强报销申请快速预览、本体工具与详情展示 - 新增半年报销模拟数据种子脚本与状态审计工具 - 补充财务看板、报销状态注册与模拟数据测试覆盖
This commit is contained in:
@@ -158,21 +158,139 @@ class ExpenseClaimItemSyncMixin:
|
||||
end_date = start_date
|
||||
|
||||
days = (end_date - start_date).days + 1
|
||||
application_days = self._resolve_travel_allowance_days_from_application_link(claim)
|
||||
explicit_days = max(
|
||||
(self._extract_travel_day_count(item.item_reason) for item in business_items),
|
||||
default=0,
|
||||
)
|
||||
unique_dates = {value for value in dated_items}
|
||||
if application_days is not None and application_days[0] > days and len(unique_dates) <= 1:
|
||||
return application_days
|
||||
if explicit_days > 0:
|
||||
days = explicit_days
|
||||
end_date = start_date + timedelta(days=days - 1)
|
||||
if application_days is not None and application_days[0] > days and len(unique_dates) <= 1:
|
||||
return application_days
|
||||
return max(1, days), start_date, end_date
|
||||
existing_days = self._extract_travel_allowance_days(existing_allowance)
|
||||
unique_dates = {value for value in dated_items}
|
||||
if existing_days > days and len(unique_dates) <= 1:
|
||||
days = existing_days
|
||||
end_date = start_date + timedelta(days=days - 1)
|
||||
return max(1, days), start_date, end_date
|
||||
|
||||
def _resolve_travel_allowance_days_from_application_link(
|
||||
self,
|
||||
claim: ExpenseClaim,
|
||||
) -> tuple[int, date, date] | None:
|
||||
values = self._collect_application_link_values(claim)
|
||||
if not values:
|
||||
return None
|
||||
|
||||
time_text = str(
|
||||
values.get("application_business_time")
|
||||
or values.get("business_time")
|
||||
or values.get("time_range")
|
||||
or values.get("application_time")
|
||||
or values.get("time")
|
||||
or ""
|
||||
).strip()
|
||||
dates = self._extract_application_link_dates(time_text)
|
||||
if len(dates) >= 2:
|
||||
start_date, end_date = dates[0], dates[-1]
|
||||
if end_date < start_date:
|
||||
start_date, end_date = end_date, start_date
|
||||
return max(1, (end_date - start_date).days + 1), start_date, end_date
|
||||
|
||||
days = self._extract_travel_day_count(
|
||||
str(values.get("application_days") or values.get("days") or "").strip()
|
||||
)
|
||||
if days <= 0:
|
||||
return None
|
||||
start_date = dates[0] if dates else claim.occurred_at.date() if claim.occurred_at is not None else date.today()
|
||||
end_date = start_date + timedelta(days=days - 1)
|
||||
return days, start_date, end_date
|
||||
|
||||
def _collect_application_link_values(self, claim: ExpenseClaim) -> dict[str, Any]:
|
||||
values: dict[str, Any] = {}
|
||||
for flag in list(claim.risk_flags_json or []):
|
||||
if not isinstance(flag, dict):
|
||||
continue
|
||||
if str(flag.get("source") or "").strip() not in {"application_link", "application_handoff"}:
|
||||
continue
|
||||
for source in (
|
||||
flag.get("expense_scene_selection"),
|
||||
flag.get("review_form_values"),
|
||||
flag.get("application_detail"),
|
||||
flag,
|
||||
):
|
||||
if isinstance(source, dict):
|
||||
values.update(source)
|
||||
linked_detail = self._resolve_linked_application_detail_values(values)
|
||||
for key, value in linked_detail.items():
|
||||
values.setdefault(key, value)
|
||||
return values
|
||||
|
||||
def _resolve_linked_application_detail_values(self, values: dict[str, Any]) -> dict[str, Any]:
|
||||
application_claim = self._find_linked_application_claim(values)
|
||||
if application_claim is None:
|
||||
return {}
|
||||
|
||||
detail: dict[str, Any] = {}
|
||||
for flag in list(application_claim.risk_flags_json or []):
|
||||
if not isinstance(flag, dict) or str(flag.get("source") or "").strip() != "application_detail":
|
||||
continue
|
||||
payload = flag.get("application_detail") or flag.get("applicationDetail") or {}
|
||||
if isinstance(payload, dict):
|
||||
detail.update(payload)
|
||||
if detail.get("time"):
|
||||
detail.setdefault("application_time", detail.get("time"))
|
||||
if detail.get("days"):
|
||||
detail.setdefault("application_days", detail.get("days"))
|
||||
if detail.get("transport_mode"):
|
||||
detail.setdefault("application_transport_mode", detail.get("transport_mode"))
|
||||
if detail.get("location"):
|
||||
detail.setdefault("application_location", detail.get("location"))
|
||||
if detail.get("reason"):
|
||||
detail.setdefault("application_reason", detail.get("reason"))
|
||||
if application_claim.occurred_at is not None:
|
||||
detail.setdefault("application_time", application_claim.occurred_at.date().isoformat())
|
||||
detail.setdefault("time", application_claim.occurred_at.date().isoformat())
|
||||
detail.setdefault("application_reason", str(application_claim.reason or "").strip())
|
||||
detail.setdefault("application_location", str(application_claim.location or "").strip())
|
||||
return {str(key): value for key, value in detail.items() if str(value or "").strip()}
|
||||
|
||||
def _find_linked_application_claim(self, values: dict[str, Any]) -> ExpenseClaim | None:
|
||||
application_claim_id = str(
|
||||
values.get("application_claim_id")
|
||||
or values.get("applicationClaimId")
|
||||
or ""
|
||||
).strip()
|
||||
if application_claim_id:
|
||||
linked_claim = self.db.get(ExpenseClaim, application_claim_id)
|
||||
if linked_claim is not None:
|
||||
return linked_claim
|
||||
|
||||
application_claim_no = str(
|
||||
values.get("application_claim_no")
|
||||
or values.get("applicationClaimNo")
|
||||
or ""
|
||||
).strip()
|
||||
if not application_claim_no:
|
||||
return None
|
||||
return self.db.scalar(
|
||||
select(ExpenseClaim).where(ExpenseClaim.claim_no == application_claim_no)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_application_link_dates(value: str) -> list[date]:
|
||||
dates: list[date] = []
|
||||
for matched in re.findall(r"\d{4}-\d{2}-\d{2}", str(value or "")):
|
||||
try:
|
||||
dates.append(date.fromisoformat(matched))
|
||||
except ValueError:
|
||||
continue
|
||||
return dates
|
||||
|
||||
@staticmethod
|
||||
def _extract_travel_allowance_days(item: ExpenseClaimItem | None) -> int:
|
||||
if item is None:
|
||||
|
||||
Reference in New Issue
Block a user