feat(server): 申请单支持草稿保存并统一删除权限口径
- user_agent_application 新增草稿分支:识别'保存草稿/存草稿/先保存'等意图,复用可编辑记录更新或建草稿,提交前单据重叠仍拦截 - 草稿态返回单号与待提交提示,submit 仅在确认提交分支触发,避免草稿进入审批流 - reimbursements 删除接口文案与判定统一为系统管理员可删、申请人删自有草稿/退回单,申请单判定改用 is_application_claim_no - 更新财务规则表与 reimbursement 端点测试
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,16 +10,17 @@ from app.api.deps import CurrentUserContext, get_current_user, get_db
|
|||||||
from app.api.pagination import PageNumber, PageSize, page_payload, wants_page
|
from app.api.pagination import PageNumber, PageSize, page_payload, wants_page
|
||||||
from app.schemas.budget import BudgetClaimAnalysisRead
|
from app.schemas.budget import BudgetClaimAnalysisRead
|
||||||
from app.schemas.common import ErrorResponse, PaginatedResponse
|
from app.schemas.common import ErrorResponse, PaginatedResponse
|
||||||
|
from app.schemas.ontology import OntologyParseResult, OntologyPermission
|
||||||
from app.schemas.reimbursement import (
|
from app.schemas.reimbursement import (
|
||||||
ExpenseApplicationPreviewActionPayload,
|
ExpenseApplicationPreviewActionPayload,
|
||||||
ExpenseApplicationPreviewActionResponse,
|
ExpenseApplicationPreviewActionResponse,
|
||||||
ExpenseApplicationPreviewActionResult,
|
ExpenseApplicationPreviewActionResult,
|
||||||
ExpenseClaimAttachmentActionResponse,
|
|
||||||
ExpenseClaimActionResponse,
|
ExpenseClaimActionResponse,
|
||||||
ExpenseClaimAttachmentRead,
|
|
||||||
ExpenseClaimApprovalPayload,
|
ExpenseClaimApprovalPayload,
|
||||||
ExpenseClaimItemCreate,
|
ExpenseClaimAttachmentActionResponse,
|
||||||
|
ExpenseClaimAttachmentRead,
|
||||||
ExpenseClaimItemActionResponse,
|
ExpenseClaimItemActionResponse,
|
||||||
|
ExpenseClaimItemCreate,
|
||||||
ExpenseClaimItemUpdate,
|
ExpenseClaimItemUpdate,
|
||||||
ExpenseClaimRead,
|
ExpenseClaimRead,
|
||||||
ExpenseClaimReturnPayload,
|
ExpenseClaimReturnPayload,
|
||||||
@@ -30,9 +31,9 @@ from app.schemas.reimbursement import (
|
|||||||
TravelReimbursementCalculatorRequest,
|
TravelReimbursementCalculatorRequest,
|
||||||
TravelReimbursementCalculatorResponse,
|
TravelReimbursementCalculatorResponse,
|
||||||
)
|
)
|
||||||
from app.schemas.ontology import OntologyParseResult, OntologyPermission
|
|
||||||
from app.schemas.user_agent import UserAgentRequest
|
from app.schemas.user_agent import UserAgentRequest
|
||||||
from app.services.budget import BudgetService
|
from app.services.budget import BudgetService
|
||||||
|
from app.services.document_numbering import is_application_claim_no
|
||||||
from app.services.expense_claims import ExpenseClaimService
|
from app.services.expense_claims import ExpenseClaimService
|
||||||
from app.services.reimbursement import ReimbursementService
|
from app.services.reimbursement import ReimbursementService
|
||||||
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
|
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
|
||||||
@@ -119,7 +120,10 @@ def _build_application_preview_action_context(
|
|||||||
"/application-preview-action",
|
"/application-preview-action",
|
||||||
response_model=ExpenseApplicationPreviewActionResponse,
|
response_model=ExpenseApplicationPreviewActionResponse,
|
||||||
summary="按申请核对预览快速保存或提交申请单",
|
summary="按申请核对预览快速保存或提交申请单",
|
||||||
description="用于 AI 工作台已完成表格核对后的轻量建单/提交流程,避免重复进入通用 Orchestrator 编排。",
|
description=(
|
||||||
|
"用于 AI 工作台已完成表格核对后的轻量建单/提交流程,"
|
||||||
|
"避免重复进入通用 Orchestrator 编排。"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
def run_application_preview_action(
|
def run_application_preview_action(
|
||||||
payload: ExpenseApplicationPreviewActionPayload,
|
payload: ExpenseApplicationPreviewActionPayload,
|
||||||
@@ -831,7 +835,7 @@ def pay_expense_claim(
|
|||||||
"/claims/{claim_id}",
|
"/claims/{claim_id}",
|
||||||
response_model=ExpenseClaimActionResponse,
|
response_model=ExpenseClaimActionResponse,
|
||||||
summary="删除报销单",
|
summary="删除报销单",
|
||||||
description="申请人可删除自己的草稿、待补充或退回单据(含申请单和报销单);高级财务人员可删除可见的非归档报销单;已归档单据仅高级管理员可删除,财务人员没有删除权限。",
|
description="申请人可删除自己的草稿、待补充或退回单据(含申请单和报销单);系统管理员可删除单据;已归档单据仅系统管理员可删除。",
|
||||||
responses={
|
responses={
|
||||||
status.HTTP_404_NOT_FOUND: {
|
status.HTTP_404_NOT_FOUND: {
|
||||||
"model": ErrorResponse,
|
"model": ErrorResponse,
|
||||||
@@ -855,7 +859,11 @@ def delete_expense_claim(claim_id: str, db: DbSession, current_user: CurrentUser
|
|||||||
|
|
||||||
claim_no = str(claim.claim_no or "").strip()
|
claim_no = str(claim.claim_no or "").strip()
|
||||||
expense_type = str(claim.expense_type or "").strip().lower()
|
expense_type = str(claim.expense_type or "").strip().lower()
|
||||||
document_label = "申请单" if claim_no.upper().startswith(("AP-", "APP-")) or expense_type.endswith("_application") else "报销单"
|
document_label = (
|
||||||
|
"申请单"
|
||||||
|
if is_application_claim_no(claim_no) or expense_type.endswith("_application")
|
||||||
|
else "报销单"
|
||||||
|
)
|
||||||
return ExpenseClaimActionResponse(
|
return ExpenseClaimActionResponse(
|
||||||
message=f"{claim.claim_no} {document_label}已删除。",
|
message=f"{claim.claim_no} {document_label}已删除。",
|
||||||
claim_id=claim.id,
|
claim_id=claim.id,
|
||||||
|
|||||||
@@ -130,6 +130,12 @@ APPLICATION_SUBMIT_KEYWORDS = (
|
|||||||
"确认无误提交",
|
"确认无误提交",
|
||||||
"直接提交",
|
"直接提交",
|
||||||
)
|
)
|
||||||
|
APPLICATION_SAVE_DRAFT_KEYWORDS = (
|
||||||
|
"保存草稿",
|
||||||
|
"保存申请草稿",
|
||||||
|
"存草稿",
|
||||||
|
"先保存",
|
||||||
|
)
|
||||||
APPLICATION_SHORT_CONFIRMATIONS = {"提交", "确认", "是", "好的", "可以", "没问题"}
|
APPLICATION_SHORT_CONFIRMATIONS = {"提交", "确认", "是", "好的", "可以", "没问题"}
|
||||||
APPLICATION_MISSING_VALUES = {"", "待补充", "待确认", "未知", "暂无", "无", "null", "none"}
|
APPLICATION_MISSING_VALUES = {"", "待补充", "待确认", "未知", "暂无", "无", "null", "none"}
|
||||||
APPLICATION_DUPLICATE_IGNORED_STATUSES = {
|
APPLICATION_DUPLICATE_IGNORED_STATUSES = {
|
||||||
@@ -197,18 +203,34 @@ class UserAgentApplicationMixin:
|
|||||||
facts = self._resolve_expense_application_facts(payload)
|
facts = self._resolve_expense_application_facts(payload)
|
||||||
step = self._resolve_expense_application_step(payload, facts)
|
step = self._resolve_expense_application_step(payload, facts)
|
||||||
application_claim = None
|
application_claim = None
|
||||||
if step == "submitted":
|
if step in {"draft", "submitted"}:
|
||||||
editable_claim = self._find_editable_expense_application_record(payload)
|
editable_claim = self._find_editable_expense_application_record(payload)
|
||||||
if editable_claim is not None:
|
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"
|
facts["application_edit_mode"] = "true"
|
||||||
else:
|
elif step == "submitted":
|
||||||
application_claim = self._find_duplicate_expense_application_record(payload, facts)
|
application_claim = self._find_duplicate_expense_application_record(payload, facts)
|
||||||
if application_claim is not None:
|
if application_claim is not None:
|
||||||
step = "duplicate"
|
step = "duplicate"
|
||||||
facts["duplicate_application_stage"] = str(application_claim.approval_stage or "").strip()
|
facts["duplicate_application_stage"] = str(application_claim.approval_stage or "").strip()
|
||||||
else:
|
else:
|
||||||
application_claim = self._create_expense_application_record(payload, facts)
|
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_no"] = application_claim.claim_no
|
||||||
facts["application_claim_id"] = application_claim.id
|
facts["application_claim_id"] = application_claim.id
|
||||||
facts["manager_name"] = self._resolve_application_manager_name(payload, application_claim)
|
facts["manager_name"] = self._resolve_application_manager_name(payload, application_claim)
|
||||||
@@ -218,8 +240,8 @@ class UserAgentApplicationMixin:
|
|||||||
suggested_actions=self._build_expense_application_actions(step, facts),
|
suggested_actions=self._build_expense_application_actions(step, facts),
|
||||||
query_payload=None,
|
query_payload=None,
|
||||||
draft_payload=(
|
draft_payload=(
|
||||||
self._build_submitted_application_payload(application_claim, facts)
|
self._build_persisted_application_payload(application_claim, facts)
|
||||||
if step == "submitted"
|
if step in {"draft", "submitted"}
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
review_payload=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":
|
if step == "submitted":
|
||||||
application_no = str(facts.get("application_no") or "").strip() or self._build_application_claim_no(payload, facts)
|
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 "直属领导"
|
manager_name = str(facts.get("manager_name") or "").strip() or "直属领导"
|
||||||
@@ -534,6 +567,8 @@ class UserAgentApplicationMixin:
|
|||||||
payload: UserAgentRequest,
|
payload: UserAgentRequest,
|
||||||
facts: dict[str, str],
|
facts: dict[str, str],
|
||||||
) -> str:
|
) -> str:
|
||||||
|
if self._is_application_save_draft_action(payload):
|
||||||
|
return "draft"
|
||||||
if self._resolve_application_missing_base_fields(facts):
|
if self._resolve_application_missing_base_fields(facts):
|
||||||
return "ask_missing"
|
return "ask_missing"
|
||||||
if self._resolve_application_missing_followup_fields(facts):
|
if self._resolve_application_missing_followup_fields(facts):
|
||||||
@@ -1058,6 +1093,8 @@ class UserAgentApplicationMixin:
|
|||||||
payload: UserAgentRequest,
|
payload: UserAgentRequest,
|
||||||
facts: dict[str, str],
|
facts: dict[str, str],
|
||||||
claim: ExpenseClaim,
|
claim: ExpenseClaim,
|
||||||
|
*,
|
||||||
|
submit: bool,
|
||||||
) -> ExpenseClaim:
|
) -> ExpenseClaim:
|
||||||
current_user = self._build_application_current_user(payload)
|
current_user = self._build_application_current_user(payload)
|
||||||
flags = claim.risk_flags_json
|
flags = claim.risk_flags_json
|
||||||
@@ -1080,6 +1117,14 @@ class UserAgentApplicationMixin:
|
|||||||
claim.occurred_at = self._parse_application_occurred_at(facts.get("time", ""))
|
claim.occurred_at = self._parse_application_occurred_at(facts.get("time", ""))
|
||||||
claim.risk_flags_json = [*preserved_flags, self._build_application_detail_flag(facts)]
|
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
|
from app.services.expense_claims import ExpenseClaimService
|
||||||
|
|
||||||
submitted = ExpenseClaimService(self.db).submit_claim(claim.id, current_user)
|
submitted = ExpenseClaimService(self.db).submit_claim(claim.id, current_user)
|
||||||
@@ -1091,6 +1136,8 @@ class UserAgentApplicationMixin:
|
|||||||
self,
|
self,
|
||||||
payload: UserAgentRequest,
|
payload: UserAgentRequest,
|
||||||
facts: dict[str, str],
|
facts: dict[str, str],
|
||||||
|
*,
|
||||||
|
submit: bool,
|
||||||
) -> ExpenseClaim:
|
) -> ExpenseClaim:
|
||||||
claim_no = self._build_application_claim_no(payload, facts)
|
claim_no = self._build_application_claim_no(payload, facts)
|
||||||
existing = self.db.scalar(
|
existing = self.db.scalar(
|
||||||
@@ -1130,13 +1177,14 @@ class UserAgentApplicationMixin:
|
|||||||
currency="CNY",
|
currency="CNY",
|
||||||
invoice_count=0,
|
invoice_count=0,
|
||||||
occurred_at=self._parse_application_occurred_at(facts.get("time", "")),
|
occurred_at=self._parse_application_occurred_at(facts.get("time", "")),
|
||||||
submitted_at=datetime.now(UTC),
|
submitted_at=datetime.now(UTC) if submit else None,
|
||||||
status="submitted",
|
status="submitted" if submit else "draft",
|
||||||
approval_stage="直属领导审批",
|
approval_stage="直属领导审批" if submit else "待提交",
|
||||||
risk_flags_json=[self._build_application_detail_flag(facts)],
|
risk_flags_json=[self._build_application_detail_flag(facts)],
|
||||||
)
|
)
|
||||||
self.db.add(claim)
|
self.db.add(claim)
|
||||||
self.db.flush()
|
self.db.flush()
|
||||||
|
if submit:
|
||||||
from app.services.expense_claims import ExpenseClaimService
|
from app.services.expense_claims import ExpenseClaimService
|
||||||
|
|
||||||
platform_review = ExpenseClaimService(self.db).evaluate_platform_risk_rules(
|
platform_review = ExpenseClaimService(self.db).evaluate_platform_risk_rules(
|
||||||
@@ -1382,7 +1430,7 @@ class UserAgentApplicationMixin:
|
|||||||
return datetime(year, month, day, tzinfo=UTC)
|
return datetime(year, month, day, tzinfo=UTC)
|
||||||
return datetime.now(UTC)
|
return datetime.now(UTC)
|
||||||
|
|
||||||
def _build_submitted_application_payload(
|
def _build_persisted_application_payload(
|
||||||
self,
|
self,
|
||||||
claim: ExpenseClaim | None,
|
claim: ExpenseClaim | None,
|
||||||
facts: dict[str, str],
|
facts: dict[str, str],
|
||||||
@@ -1400,6 +1448,21 @@ class UserAgentApplicationMixin:
|
|||||||
approval_stage=claim.approval_stage,
|
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:
|
def _is_application_submit_confirmation(self, payload: UserAgentRequest) -> bool:
|
||||||
compact_message = re.sub(r"\s+", "", str(payload.message or ""))
|
compact_message = re.sub(r"\s+", "", str(payload.message or ""))
|
||||||
if any(keyword in compact_message for keyword in APPLICATION_SUBMIT_KEYWORDS):
|
if any(keyword in compact_message for keyword in APPLICATION_SUBMIT_KEYWORDS):
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from app.models.risk_observation import RiskObservation, RiskObservationFeedback
|
|||||||
from app.models.role import Role
|
from app.models.role import Role
|
||||||
from app.schemas.ocr import OcrRecognizeBatchRead, OcrRecognizeDocumentRead
|
from app.schemas.ocr import OcrRecognizeBatchRead, OcrRecognizeDocumentRead
|
||||||
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
|
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
|
||||||
from app.services.expense_claims import ExpenseClaimService
|
|
||||||
from app.services.ocr import OcrService
|
from app.services.ocr import OcrService
|
||||||
|
|
||||||
|
|
||||||
@@ -814,6 +813,37 @@ def test_claim_delete_allows_admin_and_cleans_risk_observations(monkeypatch, tmp
|
|||||||
assert db.get(RiskObservationFeedback, "risk-observation-feedback-delete-1") is None
|
assert db.get(RiskObservationFeedback, "risk-observation-feedback-delete-1") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_claim_delete_allows_applicant_to_delete_own_draft(monkeypatch, tmp_path) -> None:
|
||||||
|
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||||
|
|
||||||
|
client, session_factory = build_client()
|
||||||
|
with session_factory() as db:
|
||||||
|
claim, _ = seed_claim(db)
|
||||||
|
claim.claim_no = "AP-20260620-DRAFT"
|
||||||
|
claim.expense_type = "travel_application"
|
||||||
|
claim_id = claim.id
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1/reimbursements/claims/{claim_id}",
|
||||||
|
headers={
|
||||||
|
"x-auth-username": "zhangsan@example.com",
|
||||||
|
"x-auth-name": "张三",
|
||||||
|
"x-auth-employee-no": "E10001",
|
||||||
|
"x-auth-role-codes": "user",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["claim_id"] == claim_id
|
||||||
|
assert payload["status"] == "deleted"
|
||||||
|
assert "申请单已删除" in payload["message"]
|
||||||
|
|
||||||
|
with session_factory() as db:
|
||||||
|
assert db.get(ExpenseClaim, claim_id) is None
|
||||||
|
|
||||||
|
|
||||||
def test_claim_delete_allows_legacy_superadmin_without_is_admin_header(monkeypatch, tmp_path) -> None:
|
def test_claim_delete_allows_legacy_superadmin_without_is_admin_header(monkeypatch, tmp_path) -> None:
|
||||||
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||||
|
|
||||||
@@ -859,7 +889,19 @@ def test_application_preview_action_submits_without_orchestrator_run(monkeypatch
|
|||||||
"source": "user_message",
|
"source": "user_message",
|
||||||
"user_id": "zhangsan@example.com",
|
"user_id": "zhangsan@example.com",
|
||||||
"conversation_id": "conversation-fast-submit",
|
"conversation_id": "conversation-fast-submit",
|
||||||
"message": "差旅费用申请提交审批\n申请类型:差旅费用申请\n申请时间:2026-07-01 至 2026-07-03\n地点:北京\n事由:项目实施\n天数:3天\n出行方式:火车\n申请金额:1000元\n直接提交",
|
"message": "\n".join(
|
||||||
|
[
|
||||||
|
"差旅费用申请提交审批",
|
||||||
|
"申请类型:差旅费用申请",
|
||||||
|
"申请时间:2026-07-01 至 2026-07-03",
|
||||||
|
"地点:北京",
|
||||||
|
"事由:项目实施",
|
||||||
|
"天数:3天",
|
||||||
|
"出行方式:火车",
|
||||||
|
"申请金额:1000元",
|
||||||
|
"直接提交",
|
||||||
|
]
|
||||||
|
),
|
||||||
"context_json": {
|
"context_json": {
|
||||||
"session_type": "application",
|
"session_type": "application",
|
||||||
"entry_source": "workbench_ai_inline",
|
"entry_source": "workbench_ai_inline",
|
||||||
@@ -899,3 +941,81 @@ def test_application_preview_action_submits_without_orchestrator_run(monkeypatch
|
|||||||
assert claim is not None
|
assert claim is not None
|
||||||
assert claim.status == "submitted"
|
assert claim.status == "submitted"
|
||||||
assert claim.employee_name == "张三"
|
assert claim.employee_name == "张三"
|
||||||
|
|
||||||
|
|
||||||
|
def test_application_preview_action_saves_draft_with_detail_reference(monkeypatch, tmp_path) -> None:
|
||||||
|
monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)
|
||||||
|
|
||||||
|
client, session_factory = build_client()
|
||||||
|
with session_factory() as db:
|
||||||
|
seed_claim(db)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/reimbursements/application-preview-action",
|
||||||
|
headers={
|
||||||
|
"x-auth-username": "zhangsan@example.com",
|
||||||
|
"x-auth-name": "Zhang San",
|
||||||
|
"x-auth-employee-no": "E10001",
|
||||||
|
"x-auth-role-codes": "user",
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"source": "user_message",
|
||||||
|
"user_id": "zhangsan@example.com",
|
||||||
|
"conversation_id": "conversation-fast-save",
|
||||||
|
"message": "\n".join(
|
||||||
|
[
|
||||||
|
"费用申请保存草稿",
|
||||||
|
"申请类型:差旅费用申请",
|
||||||
|
"申请时间:2026-07-04 至 2026-07-05",
|
||||||
|
"地点:上海",
|
||||||
|
"事由:项目验收",
|
||||||
|
"天数:2天",
|
||||||
|
"出行方式:火车",
|
||||||
|
"申请金额:800元",
|
||||||
|
"保存草稿",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"context_json": {
|
||||||
|
"session_type": "application",
|
||||||
|
"entry_source": "workbench_ai_inline",
|
||||||
|
"document_type": "expense_application",
|
||||||
|
"application_stage": "expense_application",
|
||||||
|
"application_action": "save_draft",
|
||||||
|
"application_save_mode": True,
|
||||||
|
"application_preview": {
|
||||||
|
"fields": {
|
||||||
|
"applicationType": "差旅费用申请",
|
||||||
|
"time": "2026-07-04 至 2026-07-05",
|
||||||
|
"location": "上海",
|
||||||
|
"reason": "项目验收",
|
||||||
|
"days": "2天",
|
||||||
|
"transportMode": "火车",
|
||||||
|
"amount": "800元",
|
||||||
|
"applicant": "张三",
|
||||||
|
"department": "市场部",
|
||||||
|
"position": "招商主管",
|
||||||
|
"grade": "P4",
|
||||||
|
"managerName": "李总",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["status"] == "succeeded"
|
||||||
|
draft_payload = payload["result"]["draft_payload"]
|
||||||
|
assert draft_payload["draft_type"] == "expense_application"
|
||||||
|
assert draft_payload["status"] == "draft"
|
||||||
|
assert draft_payload["approval_stage"] == "待提交"
|
||||||
|
assert draft_payload["claim_id"]
|
||||||
|
assert draft_payload["claim_no"].startswith("AP-")
|
||||||
|
|
||||||
|
with session_factory() as db:
|
||||||
|
claim = db.get(ExpenseClaim, draft_payload["claim_id"])
|
||||||
|
assert claim is not None
|
||||||
|
assert claim.status == "draft"
|
||||||
|
assert claim.approval_stage == "待提交"
|
||||||
|
assert claim.submitted_at is None
|
||||||
|
assert claim.employee_name == "张三"
|
||||||
|
|||||||
Reference in New Issue
Block a user