feat: 优化差旅报销预审流程与个人工作台 UI 体系
- 完善 user_agent_application 申请差旅报销预审槽位与消息组装 - 增强预算助理报告与风险建议卡片交互 - 重构登录页视觉样式与移动端响应式适配 - 优化个人工作台、文档中心、政策中心、员工管理等页面布局 - 拆分 travelRequestDetailPreReviewModel 为 advice/submit 模型 - 补充报销草稿、风险复核、Item Sync 与模板执行器测试覆盖
This commit is contained in:
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user