feat: 完善审批退回流程与报销申请关联
后端优化报销单访问策略和常量定义,增强退回原因和审批状态 流转,前端完善退回对话框和审批交互组件,新增报销申请关联 模型,优化文档中心行数据和审批收件箱工具函数,增强引导 流程和会话模型,补充单元测试覆盖。
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
GUIDED_ACTION_START_APPLICATION,
|
||||
GUIDED_ACTION_PROCESS_INTERRUPTION,
|
||||
GUIDED_ACTION_SELECT_EXPENSE_TYPE,
|
||||
GUIDED_ACTION_SELECT_REQUIRED_APPLICATION,
|
||||
GUIDED_ACTION_SELECT_QUERY_MODE,
|
||||
GUIDED_ACTION_SELECT_QUERY_STATUS,
|
||||
GUIDED_ACTION_START_REIMBURSEMENT,
|
||||
@@ -42,10 +43,19 @@ import {
|
||||
createGuidedStatusQueryState,
|
||||
isGuidedReimbursementReadyForReview,
|
||||
normalizeGuidedFlowState,
|
||||
selectGuidedRequiredApplication,
|
||||
selectGuidedExpenseType,
|
||||
selectGuidedQueryMode,
|
||||
shouldConfirmGuidedInterruption
|
||||
shouldConfirmGuidedInterruption,
|
||||
waitForGuidedApplicationSelection
|
||||
} from '../src/views/scripts/travelReimbursementGuidedFlowModel.js'
|
||||
import {
|
||||
buildRequiredApplicationActions,
|
||||
buildRequiredApplicationMissingText,
|
||||
buildRequiredApplicationSelectionText,
|
||||
filterRequiredApplicationCandidates,
|
||||
requiresApplicationBeforeReimbursement
|
||||
} from '../src/views/scripts/travelReimbursementApplicationLinkModel.js'
|
||||
import {
|
||||
ASSISTANT_SCOPE_ACTION_SWITCH,
|
||||
resolveAssistantScopeGuard
|
||||
@@ -71,7 +81,7 @@ const submitComposerScript = readFileSync(
|
||||
test('assistant session modes expose independent quick actions', () => {
|
||||
assert.deepEqual(
|
||||
ASSISTANT_SESSION_MODE_OPTIONS.map((item) => item.label),
|
||||
['申请助手', '报销助手', '审核助手', '财务知识助手']
|
||||
['申请助手', '报销助手', '审核助手', '财务知识助手', '预算编制助手']
|
||||
)
|
||||
assert.deepEqual(
|
||||
EXPENSE_WELCOME_QUICK_ACTIONS.map((item) => item.label),
|
||||
@@ -181,6 +191,82 @@ test('guided reimbursement asks type first and walks travel fields in order', ()
|
||||
assert.match(submitOptions.rawText, /出差时间\/天数:2026-05-20 至 2026-05-23,出差 3 天/)
|
||||
})
|
||||
|
||||
test('guided reimbursement requires application selection for travel and entertainment', () => {
|
||||
assert.equal(requiresApplicationBeforeReimbursement('travel'), true)
|
||||
assert.equal(requiresApplicationBeforeReimbursement('meal'), true)
|
||||
assert.equal(requiresApplicationBeforeReimbursement('transport'), false)
|
||||
|
||||
const claimsPayload = {
|
||||
items: [
|
||||
{
|
||||
id: 'app-travel',
|
||||
claim_no: 'AP-202605-001',
|
||||
employee_name: '张小青',
|
||||
expense_type: 'travel_application',
|
||||
reason: '去上海支持项目部署',
|
||||
location: '上海',
|
||||
amount: 1800,
|
||||
status: 'approved',
|
||||
created_at: '2026-05-20T08:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 'app-meal',
|
||||
claim_no: 'AP-202605-002',
|
||||
employee_name: '张小青',
|
||||
expense_type: 'expense_application',
|
||||
reason: '客户招待沟通项目',
|
||||
location: '武汉',
|
||||
amount: 600,
|
||||
status: 'submitted',
|
||||
created_at: '2026-05-21T08:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 'app-draft',
|
||||
claim_no: 'AP-202605-003',
|
||||
employee_name: '张小青',
|
||||
expense_type: 'travel_application',
|
||||
reason: '草稿出差申请',
|
||||
status: 'draft'
|
||||
},
|
||||
{
|
||||
id: 'app-other-user',
|
||||
claim_no: 'AP-202605-004',
|
||||
employee_name: '李四',
|
||||
expense_type: 'travel_application',
|
||||
reason: '其他员工出差申请',
|
||||
status: 'approved'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const currentUser = { name: '张小青', username: 'xiaoqing.zhang' }
|
||||
const travelApplications = filterRequiredApplicationCandidates(claimsPayload, 'travel', currentUser)
|
||||
assert.deepEqual(travelApplications.map((item) => item.claim_no), ['AP-202605-001'])
|
||||
assert.match(buildRequiredApplicationSelectionText('travel', travelApplications), /需要先关联对应的申请单/)
|
||||
assert.match(buildRequiredApplicationMissingText('meal'), /不能继续这类报销流程/)
|
||||
|
||||
const mealApplications = filterRequiredApplicationCandidates(claimsPayload, 'meal', currentUser)
|
||||
assert.deepEqual(mealApplications.map((item) => item.claim_no), ['AP-202605-002'])
|
||||
|
||||
const actions = buildRequiredApplicationActions(travelApplications, GUIDED_ACTION_SELECT_REQUIRED_APPLICATION)
|
||||
assert.equal(actions[0].action_type, GUIDED_ACTION_SELECT_REQUIRED_APPLICATION)
|
||||
assert.equal(actions[0].payload.application_claim_no, 'AP-202605-001')
|
||||
|
||||
let state = waitForGuidedApplicationSelection(createGuidedReimbursementState(), 'travel', travelApplications)
|
||||
assert.equal(state.stepKey, 'application_selection')
|
||||
assert.equal(state.applicationCandidates[0].claim_no, 'AP-202605-001')
|
||||
|
||||
state = selectGuidedRequiredApplication(state, actions[0].payload)
|
||||
assert.equal(state.stepKey, 'reason')
|
||||
assert.equal(state.values.application_claim_no, 'AP-202605-001')
|
||||
assert.match(buildGuidedReimbursementSummaryText(state), /关联申请单:AP-202605-001/)
|
||||
|
||||
const submitOptions = buildGuidedReviewSubmitOptions(state)
|
||||
assert.equal(submitOptions.extraContext.review_form_values.application_claim_no, 'AP-202605-001')
|
||||
assert.equal(submitOptions.extraContext.expense_scene_selection.application_claim_no, 'AP-202605-001')
|
||||
assert.match(submitOptions.rawText, /关联申请单:AP-202605-001/)
|
||||
})
|
||||
|
||||
test('guided reimbursement interrupts suspicious questions before expensive flow', () => {
|
||||
const state = selectGuidedExpenseType(createGuidedReimbursementState(), 'transport')
|
||||
assert.equal(shouldConfirmGuidedInterruption('送客户去机场', state), false)
|
||||
@@ -232,7 +318,8 @@ test('guided flow state is serializable and restored through session state', ()
|
||||
amount: '200',
|
||||
attachment_names: ['a.pdf']
|
||||
},
|
||||
pendingInterruptionText: '查询状态?'
|
||||
pendingInterruptionText: '查询状态?',
|
||||
applicationCandidates: []
|
||||
}
|
||||
)
|
||||
|
||||
@@ -241,7 +328,7 @@ test('guided flow state is serializable and restored through session state', ()
|
||||
assert.match(sessionStateScript, /guidedFlowState,\s*\n\s*insightPanelCollapsed/)
|
||||
assert.match(sessionStateScript, /function refreshWelcomeQuickActions/)
|
||||
assert.match(sessionStateScript, /buildWelcomeQuickActions\(/)
|
||||
assert.match(sessionStateScript, /ASSISTANT_SESSION_TYPES\.reduce/)
|
||||
assert.match(sessionStateScript, /resolveAccessibleSessionTypes\(\)\.reduce/)
|
||||
assert.match(sessionStateScript, /props\.entrySource === 'application' \? SESSION_TYPE_APPLICATION : SESSION_TYPE_EXPENSE/)
|
||||
assert.match(sessionStateScript, /const canRestorePersistedInitialState =[\s\S]*shouldPersistLocalSnapshot/)
|
||||
})
|
||||
@@ -250,6 +337,10 @@ test('guided flow is local until final confirmation or collected query handoff',
|
||||
assert.doesNotMatch(guidedFlowScript, /runOrchestrator/)
|
||||
assert.doesNotMatch(guidedFlowScript, /startExpenseClaimDraftFlowStep/)
|
||||
assert.doesNotMatch(guidedFlowScript, /review_action:\s*['"]save_draft['"]/)
|
||||
assert.match(guidedFlowScript, /fetchExpenseClaims/)
|
||||
assert.match(guidedFlowScript, /GUIDED_ACTION_SELECT_REQUIRED_APPLICATION/)
|
||||
assert.match(guidedFlowScript, /handleSceneSelectionApplicationGate/)
|
||||
assert.match(createViewScript, /handleSceneSelectionApplicationGate/)
|
||||
assert.match(createViewScript, /if \(await handleGuidedComposerSubmit\(options\)\) \{[\s\S]*return null[\s\S]*\}[\s\S]*return submitComposerInternal\(options\)/)
|
||||
assert.match(createViewScript, /ASSISTANT_SCOPE_ACTION_SWITCH/)
|
||||
assert.match(createViewScript, /actionPayload\.carry_text/)
|
||||
|
||||
Reference in New Issue
Block a user