fix: handle risk explanation standard adjustment
This commit is contained in:
@@ -27,6 +27,7 @@ from app.schemas.ontology import OntologyEntity, OntologyParseResult
|
||||
from app.schemas.reimbursement import (
|
||||
ExpenseClaimItemCreate,
|
||||
ExpenseClaimItemUpdate,
|
||||
ExpenseClaimStandardAdjustmentPayload,
|
||||
ExpenseClaimUpdate,
|
||||
TravelReimbursementCalculatorRequest,
|
||||
)
|
||||
@@ -109,6 +110,7 @@ from app.services.expense_claim_constants import (
|
||||
TRAVEL_POLICY_FLIGHT_CLASS_PATTERNS,
|
||||
TRAVEL_POLICY_TRAIN_CLASS_PATTERNS,
|
||||
TRAVEL_POLICY_HOTEL_NIGHT_PATTERN,
|
||||
STANDARD_ADJUSTMENT_RISK_SOURCE,
|
||||
)
|
||||
from app.services.expense_claim_risk_review import ExpenseClaimRiskReviewMixin
|
||||
from app.services.expense_amounts import (
|
||||
@@ -290,6 +292,126 @@ class ExpenseClaimService(
|
||||
|
||||
return claim
|
||||
|
||||
@staticmethod
|
||||
def _normalize_standard_adjustment_amount(value: Any) -> Decimal | None:
|
||||
try:
|
||||
raw_value = "" if value is None else value
|
||||
amount = Decimal(str(raw_value)).quantize(Decimal("0.01"))
|
||||
except (InvalidOperation, ValueError):
|
||||
return None
|
||||
return amount if amount >= Decimal("0.00") else None
|
||||
|
||||
@staticmethod
|
||||
def _format_adjustment_money(value: Decimal) -> str:
|
||||
normalized = Decimal(value or Decimal("0.00")).quantize(Decimal("0.01"))
|
||||
return f"{normalized:.2f}"
|
||||
|
||||
def accept_standard_adjustment(
|
||||
self,
|
||||
*,
|
||||
claim_id: str,
|
||||
payload: ExpenseClaimStandardAdjustmentPayload,
|
||||
current_user: CurrentUserContext,
|
||||
) -> ExpenseClaim | None:
|
||||
claim = self.get_claim(claim_id, current_user)
|
||||
if claim is None:
|
||||
return None
|
||||
|
||||
self._ensure_draft_claim(claim)
|
||||
if self._is_expense_application_claim(claim):
|
||||
raise ValueError("费用申请单不支持按报销标准重算。")
|
||||
|
||||
risk_entries = list(payload.risks or [])
|
||||
if not risk_entries:
|
||||
raise ValueError("请至少选择一条需要按职级标准重算的风险。")
|
||||
|
||||
before_json = self._serialize_claim(claim)
|
||||
item_map = {str(item.id or "").strip(): item for item in list(claim.items or [])}
|
||||
now_text = datetime.now(UTC).isoformat()
|
||||
adjustment_flags: list[dict[str, Any]] = []
|
||||
|
||||
for index, entry in enumerate(risk_entries, start=1):
|
||||
item_id = str(entry.item_id or "").strip()
|
||||
item = item_map.get(item_id)
|
||||
if item is None:
|
||||
continue
|
||||
|
||||
original_amount = (
|
||||
self._normalize_standard_adjustment_amount(entry.original_amount)
|
||||
or Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01"))
|
||||
)
|
||||
reimbursable_amount = (
|
||||
self._normalize_standard_adjustment_amount(entry.reimbursable_amount)
|
||||
or original_amount
|
||||
)
|
||||
reimbursable_amount = min(max(reimbursable_amount, Decimal("0.00")), original_amount)
|
||||
employee_absorbed_amount = (original_amount - reimbursable_amount).quantize(Decimal("0.01"))
|
||||
item_label = (
|
||||
str(item.item_reason or "").strip()
|
||||
or str(entry.title or "").strip()
|
||||
or f"费用明细第 {index} 条"
|
||||
)
|
||||
source_risk = str(entry.risk or entry.title or "原风险未补充异常说明").strip()
|
||||
message = (
|
||||
f"提交人已选择按职级最高报销标准审核:{item_label} 原票据金额 "
|
||||
f"{self._format_adjustment_money(original_amount)} 元,实际报销金额 "
|
||||
f"{self._format_adjustment_money(reimbursable_amount)} 元,超出 "
|
||||
f"{self._format_adjustment_money(employee_absorbed_amount)} 元由员工自行承担。"
|
||||
)
|
||||
adjustment_flags.append(
|
||||
with_risk_business_stage(
|
||||
{
|
||||
"source": STANDARD_ADJUSTMENT_RISK_SOURCE,
|
||||
"event_type": "standard_adjustment_accepted",
|
||||
"severity": "medium",
|
||||
"label": "接受职级标准审核",
|
||||
"title": "提交人接受职级最高报销标准",
|
||||
"message": message,
|
||||
"summary": "提交人未补充异常说明,已选择按职级最高报销标准重算实际报销金额。",
|
||||
"suggestion": "领导和财务审批时请确认该差额由员工自行承担,并按实际报销金额入账。",
|
||||
"risk_id": str(entry.risk_id or "").strip(),
|
||||
"source_risk": source_risk,
|
||||
"item_id": item_id,
|
||||
"original_amount": self._format_adjustment_money(original_amount),
|
||||
"reimbursable_amount": self._format_adjustment_money(reimbursable_amount),
|
||||
"employee_absorbed_amount": self._format_adjustment_money(employee_absorbed_amount),
|
||||
"risk_domain": "amount",
|
||||
"actionability": "review_decision",
|
||||
"visibility_scope": "leader",
|
||||
"created_at": now_text,
|
||||
},
|
||||
"reimbursement",
|
||||
)
|
||||
)
|
||||
|
||||
if not adjustment_flags:
|
||||
raise ValueError("未找到可按职级标准重算的费用明细。")
|
||||
|
||||
preserved_flags = [
|
||||
flag
|
||||
for flag in list(claim.risk_flags_json or [])
|
||||
if not (
|
||||
isinstance(flag, dict)
|
||||
and str(flag.get("source") or "").strip() == STANDARD_ADJUSTMENT_RISK_SOURCE
|
||||
)
|
||||
]
|
||||
claim.risk_flags_json = [*preserved_flags, *adjustment_flags]
|
||||
self._sync_claim_from_items(claim)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(claim)
|
||||
|
||||
self.audit_service.log_action(
|
||||
actor=current_user.name or current_user.username,
|
||||
action="expense_claim.standard_adjustment_accept",
|
||||
resource_type="expense_claim",
|
||||
resource_id=claim.id,
|
||||
before_json=before_json,
|
||||
after_json=self._serialize_claim(claim),
|
||||
)
|
||||
|
||||
return claim
|
||||
|
||||
def update_claim_item(
|
||||
self,
|
||||
*,
|
||||
@@ -758,6 +880,3 @@ class ExpenseClaimService(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user