feat: 优化差旅报销预审流程与个人工作台 UI 体系

- 完善 user_agent_application 申请差旅报销预审槽位与消息组装
- 增强预算助理报告与风险建议卡片交互
- 重构登录页视觉样式与移动端响应式适配
- 优化个人工作台、文档中心、政策中心、员工管理等页面布局
- 拆分 travelRequestDetailPreReviewModel 为 advice/submit 模型
- 补充报销草稿、风险复核、Item Sync 与模板执行器测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-02 14:01:51 +08:00
parent 92444e7eae
commit ca691f3ee0
107 changed files with 5663 additions and 1542 deletions

View File

@@ -307,6 +307,13 @@ class ExpenseClaimDraftFlowMixin:
claim.risk_flags_json = final_risk_flags
self.db.flush()
skip_primary_item = self._should_skip_application_link_placeholder_item(
claim=claim,
context_json=context_json,
document_specs=document_specs,
attachment_count=attachment_count,
amount=amount,
)
if document_specs and (is_new_claim or review_action in DOCUMENT_ASSOCIATION_REVIEW_ACTIONS):
if review_action == "link_to_existing_draft" and claim.items:
self._append_document_items(
@@ -319,6 +326,8 @@ class ExpenseClaimDraftFlowMixin:
item_specs=document_specs,
)
self._sync_claim_from_items(claim)
elif skip_primary_item:
self._sync_application_link_draft_without_items(claim)
else:
self._upsert_primary_item(
claim=claim,
@@ -379,6 +388,66 @@ class ExpenseClaimDraftFlowMixin:
"invoice_count": int(claim.invoice_count or 0),
}
def _sync_application_link_draft_without_items(self, claim: ExpenseClaim) -> None:
claim.amount = Decimal("0.00")
claim.invoice_count = 0
claim.risk_flags_json = self._merge_claim_attachment_risk_flags(claim, [])
claim.risk_flags_json = self._merge_claim_platform_risk_preview_flags(claim, [])
def _should_skip_application_link_placeholder_item(
self,
*,
claim: ExpenseClaim | None,
context_json: dict[str, Any],
document_specs: list[dict[str, Any]],
attachment_count: int,
amount: Decimal | None,
) -> bool:
if document_specs or attachment_count > 0:
return False
if claim is not None and list(claim.items or []):
return False
if self._build_application_link_flag(context_json) is None:
return False
application_amounts = self._resolve_application_amount_candidates(context_json)
review_values = self._normalize_context_object(context_json.get("review_form_values"))
raw_amount = str(review_values.get("amount") or "").strip()
if raw_amount:
parsed_amount = self._parse_context_money_amount(raw_amount)
if parsed_amount is None:
return True
return bool(application_amounts and parsed_amount in application_amounts)
if amount is None or amount <= Decimal("0.00"):
return True
return bool(application_amounts and amount in application_amounts)
@classmethod
def _resolve_application_amount_candidates(cls, context_json: dict[str, Any]) -> set[Decimal]:
review_values = cls._normalize_context_object(context_json.get("review_form_values"))
scene_selection = cls._normalize_context_object(context_json.get("expense_scene_selection"))
candidates: set[Decimal] = set()
for source in (review_values, scene_selection, context_json):
for key in ("application_amount", "application_amount_label", "applicationAmount", "applicationAmountLabel"):
parsed = cls._parse_context_money_amount(source.get(key))
if parsed is not None:
candidates.add(parsed)
return candidates
@staticmethod
def _parse_context_money_amount(value: Any) -> Decimal | None:
raw_value = str(value or "").strip()
if not raw_value:
return None
compact = re.sub(r"[^\d.\-]", "", raw_value.replace(",", ""))
if not compact or compact in {"-", ".", "-."}:
return None
try:
return Decimal(compact).quantize(Decimal("0.01"))
except (InvalidOperation, ValueError):
return None
@staticmethod
def _merge_application_link_flag(
risk_flags: list[Any],