feat(server): 申请单支持草稿保存并统一删除权限口径

- user_agent_application 新增草稿分支:识别'保存草稿/存草稿/先保存'等意图,复用可编辑记录更新或建草稿,提交前单据重叠仍拦截
- 草稿态返回单号与待提交提示,submit 仅在确认提交分支触发,避免草稿进入审批流
- reimbursements 删除接口文案与判定统一为系统管理员可删、申请人删自有草稿/退回单,申请单判定改用 is_application_claim_no
- 更新财务规则表与 reimbursement 端点测试
This commit is contained in:
caoxiaozhu
2026-06-20 21:44:12 +08:00
parent 47c6a4bb73
commit 81e990ab72
9 changed files with 221 additions and 30 deletions

View File

@@ -130,6 +130,12 @@ APPLICATION_SUBMIT_KEYWORDS = (
"确认无误提交",
"直接提交",
)
APPLICATION_SAVE_DRAFT_KEYWORDS = (
"保存草稿",
"保存申请草稿",
"存草稿",
"先保存",
)
APPLICATION_SHORT_CONFIRMATIONS = {"提交", "确认", "", "好的", "可以", "没问题"}
APPLICATION_MISSING_VALUES = {"", "待补充", "待确认", "未知", "暂无", "", "null", "none"}
APPLICATION_DUPLICATE_IGNORED_STATUSES = {
@@ -197,29 +203,45 @@ class UserAgentApplicationMixin:
facts = self._resolve_expense_application_facts(payload)
step = self._resolve_expense_application_step(payload, facts)
application_claim = None
if step == "submitted":
if step in {"draft", "submitted"}:
editable_claim = self._find_editable_expense_application_record(payload)
if editable_claim is not None:
application_claim = self._update_expense_application_record(payload, facts, editable_claim)
application_claim = self._update_expense_application_record(
payload,
facts,
editable_claim,
submit=step == "submitted",
)
facts["application_edit_mode"] = "true"
else:
elif step == "submitted":
application_claim = self._find_duplicate_expense_application_record(payload, facts)
if application_claim is not None:
step = "duplicate"
facts["duplicate_application_stage"] = str(application_claim.approval_stage or "").strip()
else:
application_claim = self._create_expense_application_record(payload, facts)
facts["application_no"] = application_claim.claim_no
facts["application_claim_id"] = application_claim.id
facts["manager_name"] = self._resolve_application_manager_name(payload, application_claim)
application_claim = self._create_expense_application_record(
payload,
facts,
submit=True,
)
else:
application_claim = self._create_expense_application_record(
payload,
facts,
submit=False,
)
if application_claim is not None:
facts["application_no"] = application_claim.claim_no
facts["application_claim_id"] = application_claim.id
facts["manager_name"] = self._resolve_application_manager_name(payload, application_claim)
return UserAgentResponse(
answer=self._build_expense_application_answer(payload, facts=facts, step=step),
citations=[],
suggested_actions=self._build_expense_application_actions(step, facts),
query_payload=None,
draft_payload=(
self._build_submitted_application_payload(application_claim, facts)
if step == "submitted"
self._build_persisted_application_payload(application_claim, facts)
if step in {"draft", "submitted"}
else None
),
review_payload=None,
@@ -251,6 +273,17 @@ class UserAgentApplicationMixin:
]
)
if step == "draft":
application_no = str(facts.get("application_no") or "").strip()
return "\n\n".join(
[
"申请草稿已保存。",
f"草稿单号:{application_no}" if application_no else "草稿单号:待生成",
"当前节点:待提交。",
"后续可进入单据详情继续核对、补充或提交审批。",
]
)
if step == "submitted":
application_no = str(facts.get("application_no") or "").strip() or self._build_application_claim_no(payload, facts)
manager_name = str(facts.get("manager_name") or "").strip() or "直属领导"
@@ -534,6 +567,8 @@ class UserAgentApplicationMixin:
payload: UserAgentRequest,
facts: dict[str, str],
) -> str:
if self._is_application_save_draft_action(payload):
return "draft"
if self._resolve_application_missing_base_fields(facts):
return "ask_missing"
if self._resolve_application_missing_followup_fields(facts):
@@ -1058,6 +1093,8 @@ class UserAgentApplicationMixin:
payload: UserAgentRequest,
facts: dict[str, str],
claim: ExpenseClaim,
*,
submit: bool,
) -> ExpenseClaim:
current_user = self._build_application_current_user(payload)
flags = claim.risk_flags_json
@@ -1080,6 +1117,14 @@ class UserAgentApplicationMixin:
claim.occurred_at = self._parse_application_occurred_at(facts.get("time", ""))
claim.risk_flags_json = [*preserved_flags, self._build_application_detail_flag(facts)]
if not submit:
claim.status = "draft"
claim.approval_stage = "待提交"
claim.submitted_at = None
self.db.commit()
self.db.refresh(claim)
return claim
from app.services.expense_claims import ExpenseClaimService
submitted = ExpenseClaimService(self.db).submit_claim(claim.id, current_user)
@@ -1091,6 +1136,8 @@ class UserAgentApplicationMixin:
self,
payload: UserAgentRequest,
facts: dict[str, str],
*,
submit: bool,
) -> ExpenseClaim:
claim_no = self._build_application_claim_no(payload, facts)
existing = self.db.scalar(
@@ -1130,22 +1177,23 @@ class UserAgentApplicationMixin:
currency="CNY",
invoice_count=0,
occurred_at=self._parse_application_occurred_at(facts.get("time", "")),
submitted_at=datetime.now(UTC),
status="submitted",
approval_stage="直属领导审批",
submitted_at=datetime.now(UTC) if submit else None,
status="submitted" if submit else "draft",
approval_stage="直属领导审批" if submit else "待提交",
risk_flags_json=[self._build_application_detail_flag(facts)],
)
self.db.add(claim)
self.db.flush()
from app.services.expense_claims import ExpenseClaimService
if submit:
from app.services.expense_claims import ExpenseClaimService
platform_review = ExpenseClaimService(self.db).evaluate_platform_risk_rules(
claim,
business_stage="expense_application",
)
platform_flags = list(platform_review.get("flags") or [])
if platform_flags:
claim.risk_flags_json = [*list(claim.risk_flags_json or []), *platform_flags]
platform_review = ExpenseClaimService(self.db).evaluate_platform_risk_rules(
claim,
business_stage="expense_application",
)
platform_flags = list(platform_review.get("flags") or [])
if platform_flags:
claim.risk_flags_json = [*list(claim.risk_flags_json or []), *platform_flags]
self.db.commit()
self.db.refresh(claim)
return claim
@@ -1382,7 +1430,7 @@ class UserAgentApplicationMixin:
return datetime(year, month, day, tzinfo=UTC)
return datetime.now(UTC)
def _build_submitted_application_payload(
def _build_persisted_application_payload(
self,
claim: ExpenseClaim | None,
facts: dict[str, str],
@@ -1400,6 +1448,21 @@ class UserAgentApplicationMixin:
approval_stage=claim.approval_stage,
)
@staticmethod
def _is_application_save_draft_action(payload: UserAgentRequest) -> bool:
context_json = payload.context_json or {}
action = str(
context_json.get("application_action")
or context_json.get("applicationAction")
or ""
).strip().lower()
if action in {"save_draft", "application_save_draft", "draft"}:
return True
if bool(context_json.get("application_save_mode") or context_json.get("applicationSaveMode")):
return True
compact_message = re.sub(r"\s+", "", str(payload.message or ""))
return any(keyword in compact_message for keyword in APPLICATION_SAVE_DRAFT_KEYWORDS)
def _is_application_submit_confirmation(self, payload: UserAgentRequest) -> bool:
compact_message = re.sub(r"\s+", "", str(payload.message or ""))
if any(keyword in compact_message for keyword in APPLICATION_SUBMIT_KEYWORDS):