This commit is contained in:
@@ -0,0 +1,571 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
import type { Page, Route } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const eventId = '01M00000000000000000000160';
|
||||
const voucherId = '01M00000000000000000000161';
|
||||
const companyId = '01M00000000000000000000162';
|
||||
const projectId = '01M00000000000000000000163';
|
||||
const counterpartyId = '01M00000000000000000000164';
|
||||
const debitAccountId = '01M00000000000000000000165';
|
||||
const creditAccountId = '01M00000000000000000000166';
|
||||
|
||||
interface AccountingFixtureOptions {
|
||||
initialTab?: 'pending' | 'draft';
|
||||
initialVoucher?: ReturnType<typeof voucher>;
|
||||
initialEvidence?: Array<Record<string, unknown>>;
|
||||
generateRequests?: Array<Record<string, unknown>>;
|
||||
updateRequests?: Array<Record<string, unknown>>;
|
||||
recordRequests?: Array<Record<string, unknown>>;
|
||||
verifyRequests?: Array<Record<string, unknown>>;
|
||||
reverseRequests?: Array<Record<string, unknown>>;
|
||||
reopenRequests?: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
function accountingEvent() {
|
||||
return {
|
||||
publicId: eventId,
|
||||
businessNo: 'EVT-E2E-001',
|
||||
eventType: 'PAYABLE',
|
||||
sourceType: 'PAYABLE',
|
||||
sourcePublicId: '01M00000000000000000000167',
|
||||
sourceVersion: '1',
|
||||
eventSequence: 1,
|
||||
sourceEventKey: 'PAYABLE:E2E:1',
|
||||
company: { publicId: companyId, code: 'GS-E2E-001', name: '凯迪测试公司' },
|
||||
project: { publicId: projectId, code: 'XM-E2E-001', name: '记账验收项目' },
|
||||
counterparty: { publicId: counterpartyId, code: 'GY-E2E-001', name: '验收供应商' },
|
||||
businessDate: '2026-08-15',
|
||||
period: '2026-08',
|
||||
amount: '1000.00',
|
||||
currency: 'CNY',
|
||||
status: 'PENDING',
|
||||
voucherPublicId: null,
|
||||
version: 0,
|
||||
createdAt: '2026-08-15T08:00:00Z',
|
||||
allowedActions: ['GENERATE_DRAFT'],
|
||||
};
|
||||
}
|
||||
|
||||
function voucher(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
publicId: voucherId,
|
||||
businessNo: 'VCH-202608-E2E001',
|
||||
eventPublicId: eventId,
|
||||
eventBusinessNo: 'EVT-E2E-001',
|
||||
company: { publicId: companyId, code: 'GS-E2E-001', name: '凯迪测试公司' },
|
||||
project: { publicId: projectId, code: 'XM-E2E-001', name: '记账验收项目' },
|
||||
period: '2026-08',
|
||||
businessDate: '2026-08-15',
|
||||
summary: '应付确认 EVT-E2E-001',
|
||||
debitTotal: '1000.00',
|
||||
creditTotal: '1000.00',
|
||||
balanceStatus: 'BALANCED',
|
||||
status: 'DRAFT',
|
||||
submittedByName: null,
|
||||
reviewedByName: null,
|
||||
exportedAt: null,
|
||||
exportBatchNo: null,
|
||||
exportSha256: null,
|
||||
exportFileId: null,
|
||||
ruleVersion: 'ACCOUNTING-RULE-V1',
|
||||
externalVoucherNo: null,
|
||||
resultAt: null,
|
||||
resultCycleNo: 0,
|
||||
evidenceFileIds: [],
|
||||
resultRecordedByName: null,
|
||||
resultRecordedAt: null,
|
||||
resultRecordRemark: null,
|
||||
resultVerifiedByName: null,
|
||||
resultVerifiedAt: null,
|
||||
resultVerifyOpinion: null,
|
||||
voidReason: null,
|
||||
version: 1,
|
||||
createdAt: '2026-08-15T08:01:00Z',
|
||||
updatedAt: '2026-08-15T08:01:00Z',
|
||||
allowedActions: ['EDIT', 'SUBMIT', 'VOID'],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function entries() {
|
||||
const auxiliaryJson = JSON.stringify({ projectId, counterpartyId });
|
||||
return [
|
||||
{
|
||||
publicId: '01M00000000000000000000168',
|
||||
lineNo: 1,
|
||||
direction: 'DEBIT',
|
||||
account: { publicId: debitAccountId, code: '6601', name: '项目成本' },
|
||||
amount: '1000.00',
|
||||
summary: '应付确认 EVT-E2E-001',
|
||||
auxiliaryJson,
|
||||
},
|
||||
{
|
||||
publicId: '01M00000000000000000000169',
|
||||
lineNo: 2,
|
||||
direction: 'CREDIT',
|
||||
account: { publicId: creditAccountId, code: '2202', name: '应付账款' },
|
||||
amount: '1000.00',
|
||||
summary: '应付确认 EVT-E2E-001',
|
||||
auxiliaryJson,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function voucherDetail(
|
||||
currentVoucher = voucher(),
|
||||
evidenceFiles: Array<Record<string, unknown>> = [],
|
||||
resultActions: Array<Record<string, unknown>> = [],
|
||||
) {
|
||||
return { voucher: currentVoucher, event: accountingEvent(), entries: entries(), evidenceFiles, resultActions };
|
||||
}
|
||||
|
||||
function userSession(reviewer = false) {
|
||||
return {
|
||||
authenticated: true,
|
||||
user: {
|
||||
publicId: reviewer ? '01M00000000000000000000183' : '01M00000000000000000000170',
|
||||
username: reviewer ? 'accounting-reviewer-e2e' : 'accounting-recorder-e2e',
|
||||
displayName: reviewer ? '记账复核员' : '记账登记员',
|
||||
departmentName: '财务部',
|
||||
mustChangePassword: false,
|
||||
},
|
||||
roles: [{ code: 'FINANCE_MANAGER', name: '财务管理人员', workbenchRoute: '/workbench/finance' }],
|
||||
activeRole: 'FINANCE_MANAGER',
|
||||
permissions: [
|
||||
'accounting:event:view',
|
||||
'accounting:event:generate',
|
||||
'accounting:voucher:view',
|
||||
'accounting:voucher:edit',
|
||||
'accounting:voucher:reverse-result',
|
||||
'accounting:voucher:reopen-result',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function installAccountingFixture(page: Page, options: AccountingFixtureOptions = {}) {
|
||||
let reviewerSession = false;
|
||||
let currentVoucher = options.initialVoucher || voucher();
|
||||
let currentEvidence: Array<Record<string, unknown>> = options.initialEvidence || [];
|
||||
const resultActions: Array<Record<string, unknown>> = [];
|
||||
|
||||
await page.route('**/api/v1/**', async (route: Route) => {
|
||||
const request = route.request();
|
||||
const pathname = new URL(request.url()).pathname;
|
||||
const session = userSession(reviewerSession);
|
||||
|
||||
if (pathname === '/api/v1/auth/session') {
|
||||
await route.fulfill({ json: { data: session, requestId: '01M00000000000000000000171' } });
|
||||
return;
|
||||
}
|
||||
if (pathname === '/api/v1/auth/profile') {
|
||||
await route.fulfill({ json: { data: session.user, requestId: '01M00000000000000000000172' } });
|
||||
return;
|
||||
}
|
||||
if (pathname === '/api/v1/auth/roles') {
|
||||
await route.fulfill({ json: { data: session.roles, requestId: '01M00000000000000000000173' } });
|
||||
return;
|
||||
}
|
||||
if (pathname === '/api/v1/auth/csrf') {
|
||||
await route.fulfill({
|
||||
headers: { 'set-cookie': 'XSRF-TOKEN=accounting-e2e-csrf; Path=/' },
|
||||
json: { data: { token: 'accounting-e2e-csrf' }, requestId: '01M00000000000000000000174' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === '/api/v1/accounting/accounts') {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: [
|
||||
{
|
||||
publicId: debitAccountId,
|
||||
code: '6601',
|
||||
name: '项目成本',
|
||||
accountType: 'EXPENSE',
|
||||
auxiliaryRequired: true,
|
||||
},
|
||||
{
|
||||
publicId: creditAccountId,
|
||||
code: '2202',
|
||||
name: '应付账款',
|
||||
accountType: 'LIABILITY',
|
||||
auxiliaryRequired: true,
|
||||
},
|
||||
],
|
||||
requestId: '01M00000000000000000000175',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === '/api/v1/masterdata/references') {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: {
|
||||
companies: [accountingEvent().company],
|
||||
projects: [accountingEvent().project],
|
||||
counterparties: [accountingEvent().counterparty],
|
||||
costCategories: [],
|
||||
},
|
||||
requestId: '01M00000000000000000000176',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === '/api/v1/accounting/events') {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: [accountingEvent()],
|
||||
meta: { page: 1, size: 20, totalElements: 1, totalPages: 1 },
|
||||
requestId: '01M00000000000000000000177',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === '/api/v1/accounting/vouchers') {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: [currentVoucher],
|
||||
meta: { page: 1, size: 20, totalElements: 1, totalPages: 1 },
|
||||
requestId: '01M00000000000000000000178',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'GET' && pathname === `/api/v1/accounting/vouchers/${voucherId}`) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: voucherDetail(currentVoucher, currentEvidence, resultActions),
|
||||
requestId: '01M00000000000000000000179',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/accounting/events/${eventId}/generate-draft`) {
|
||||
options.generateRequests?.push(request.postDataJSON() as Record<string, unknown>);
|
||||
await route.fulfill({
|
||||
json: { data: voucherDetail(currentVoucher), requestId: '01M00000000000000000000180' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'PATCH' && pathname === `/api/v1/accounting/vouchers/${voucherId}`) {
|
||||
const payload = request.postDataJSON() as Record<string, unknown>;
|
||||
options.updateRequests?.push(payload);
|
||||
currentVoucher = voucher({ summary: payload.summary, version: 2, updatedAt: '2026-08-15T08:02:00Z' });
|
||||
await route.fulfill({
|
||||
json: { data: voucherDetail(currentVoucher), requestId: '01M00000000000000000000181' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === '/api/v1/files') {
|
||||
currentEvidence = [
|
||||
{
|
||||
publicId: '01M00000000000000000000184',
|
||||
originalName: 'accounting-result.pdf',
|
||||
mediaType: 'application/pdf',
|
||||
sizeBytes: 20,
|
||||
sha256: 'c'.repeat(64),
|
||||
scanStatus: 'AVAILABLE',
|
||||
},
|
||||
];
|
||||
await route.fulfill({
|
||||
json: { data: currentEvidence[0], requestId: '01M00000000000000000000185' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/accounting/vouchers/${voucherId}/record-result`) {
|
||||
const payload = request.postDataJSON() as Record<string, unknown>;
|
||||
options.recordRequests?.push(payload);
|
||||
currentVoucher = voucher({
|
||||
status: 'RESULT_RECORDED',
|
||||
version: 4,
|
||||
allowedActions: ['VERIFY_RESULT'],
|
||||
externalVoucherNo: payload.externalVoucherNo,
|
||||
resultAt: payload.resultAt,
|
||||
evidenceFileIds: payload.evidenceFileIds,
|
||||
resultRecordedByName: '记账登记员',
|
||||
resultRecordedAt: '2026-08-15T08:10:00Z',
|
||||
resultRecordRemark: payload.remark,
|
||||
});
|
||||
reviewerSession = true;
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: voucherDetail(currentVoucher, currentEvidence, resultActions),
|
||||
requestId: '01M00000000000000000000186',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/accounting/vouchers/${voucherId}/verify-result`) {
|
||||
const payload = request.postDataJSON() as Record<string, unknown>;
|
||||
options.verifyRequests?.push(payload);
|
||||
currentVoucher = voucher({
|
||||
...currentVoucher,
|
||||
status: 'COMPLETED',
|
||||
version: 5,
|
||||
allowedActions: [],
|
||||
resultVerifiedByName: '记账复核员',
|
||||
resultVerifiedAt: '2026-08-15T08:15:00Z',
|
||||
resultVerifyOpinion: payload.opinion,
|
||||
});
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: voucherDetail(currentVoucher, currentEvidence, resultActions),
|
||||
requestId: '01M00000000000000000000187',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/accounting/vouchers/${voucherId}/reverse-result`) {
|
||||
const payload = request.postDataJSON() as Record<string, unknown>;
|
||||
options.reverseRequests?.push(payload);
|
||||
resultActions.unshift({
|
||||
publicId: '01M00000000000000000000188',
|
||||
sequenceNo: 1,
|
||||
resultCycleNo: 0,
|
||||
actionType: 'REVERSE',
|
||||
reason: payload.reason,
|
||||
externalVoucherNo: currentVoucher.externalVoucherNo,
|
||||
resultAt: currentVoucher.resultAt,
|
||||
resultRecordedByName: currentVoucher.resultRecordedByName,
|
||||
resultRecordedAt: currentVoucher.resultRecordedAt,
|
||||
resultRecordRemark: currentVoucher.resultRecordRemark,
|
||||
resultVerifiedByName: currentVoucher.resultVerifiedByName,
|
||||
resultVerifiedAt: currentVoucher.resultVerifiedAt,
|
||||
resultVerifyOpinion: currentVoucher.resultVerifyOpinion,
|
||||
evidenceFiles: currentEvidence,
|
||||
operatedByName: '记账复核员',
|
||||
operatedAt: '2026-08-15T08:20:00Z',
|
||||
versionBefore: 5,
|
||||
versionAfter: 6,
|
||||
});
|
||||
currentVoucher = voucher({
|
||||
...currentVoucher,
|
||||
status: 'RESULT_REVERSED',
|
||||
version: 6,
|
||||
allowedActions: ['REOPEN_RESULT'],
|
||||
});
|
||||
reviewerSession = false;
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: voucherDetail(currentVoucher, currentEvidence, resultActions),
|
||||
requestId: '01M00000000000000000000189',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'POST' && pathname === `/api/v1/accounting/vouchers/${voucherId}/reopen-result`) {
|
||||
const payload = request.postDataJSON() as Record<string, unknown>;
|
||||
options.reopenRequests?.push(payload);
|
||||
resultActions.unshift({
|
||||
...resultActions[0],
|
||||
publicId: '01M00000000000000000000190',
|
||||
sequenceNo: 2,
|
||||
actionType: 'REOPEN',
|
||||
reason: payload.reason,
|
||||
operatedByName: '记账登记员',
|
||||
operatedAt: '2026-08-15T08:25:00Z',
|
||||
versionBefore: 6,
|
||||
versionAfter: 7,
|
||||
});
|
||||
currentVoucher = voucher({
|
||||
...currentVoucher,
|
||||
status: 'EXPORTED',
|
||||
version: 7,
|
||||
resultCycleNo: 1,
|
||||
allowedActions: ['RECORD_RESULT'],
|
||||
externalVoucherNo: null,
|
||||
resultAt: null,
|
||||
evidenceFileIds: [],
|
||||
resultRecordedByName: null,
|
||||
resultRecordedAt: null,
|
||||
resultRecordRemark: null,
|
||||
resultVerifiedByName: null,
|
||||
resultVerifiedAt: null,
|
||||
resultVerifyOpinion: null,
|
||||
});
|
||||
currentEvidence = [];
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: voucherDetail(currentVoucher, currentEvidence, resultActions),
|
||||
requestId: '01M00000000000000000000191',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() === 'GET') {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: [],
|
||||
meta: { page: 1, size: 20, totalElements: 0, totalPages: 0 },
|
||||
requestId: '01M00000000000000000000182',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
await route.fulfill({ status: 204 });
|
||||
});
|
||||
}
|
||||
|
||||
test('PAGE-16 generates one voucher draft with the fixed rule version', async ({ page }) => {
|
||||
const generateRequests: Array<Record<string, unknown>> = [];
|
||||
await installAccountingFixture(page, { generateRequests });
|
||||
|
||||
await page.goto('/finance/accounting?tab=pending');
|
||||
await expect(page.getByRole('heading', { name: '记账准备', exact: true })).toBeVisible();
|
||||
await expect(page.getByText('EVT-E2E-001', { exact: true }).first()).toBeVisible();
|
||||
await page.getByRole('button', { name: '生成草稿', exact: true }).click();
|
||||
|
||||
await expect.poll(() => generateRequests.length).toBe(1);
|
||||
expect(generateRequests[0]).toEqual({ version: 0, ruleVersion: 'ACCOUNTING-RULE-V1' });
|
||||
await expect(page.getByText('凭证草稿已生成', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('借贷平衡', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('PAGE-16 edits an auxiliary voucher directly from the list without raw JSON input', async ({ page }) => {
|
||||
const updateRequests: Array<Record<string, unknown>> = [];
|
||||
await installAccountingFixture(page, { initialTab: 'draft', updateRequests });
|
||||
|
||||
await page.goto('/finance/accounting?tab=draft');
|
||||
await expect(page.getByText('VCH-202608-E2E001', { exact: true }).first()).toBeVisible();
|
||||
await page.getByRole('button', { name: '更多操作', exact: true }).click();
|
||||
await page.getByText('编辑草稿', { exact: true }).click();
|
||||
|
||||
const dialog = page.locator('.t-dialog').filter({ hasText: '编辑凭证草稿' });
|
||||
await expect(dialog.getByText('项目:XM-E2E-001 记账验收项目', { exact: true }).first()).toBeVisible();
|
||||
await expect(dialog.getByText('往来单位:GY-E2E-001 验收供应商', { exact: true }).first()).toBeVisible();
|
||||
await expect(dialog.getByText('辅助核算 JSON', { exact: true })).toHaveCount(0);
|
||||
await dialog.locator('textarea').fill('核对来源维度后保存');
|
||||
await dialog.getByRole('button', { name: '保存草稿', exact: true }).click();
|
||||
|
||||
await expect.poll(() => updateRequests.length).toBe(1);
|
||||
const payload = updateRequests[0];
|
||||
const payloadEntries = payload.entries as Array<Record<string, unknown>>;
|
||||
expect(payloadEntries).toHaveLength(2);
|
||||
for (const entry of payloadEntries) {
|
||||
expect(JSON.parse(String(entry.auxiliaryJson))).toEqual({ projectId, counterpartyId });
|
||||
}
|
||||
expect(payload.changeReason).toBe('核对来源维度后保存');
|
||||
await expect(dialog).toBeHidden();
|
||||
await expect(page.getByText('凭证草稿已保存', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('PAGE-16 records evidence and preserves separate record and verification notes', async ({ page }) => {
|
||||
const recordRequests: Array<Record<string, unknown>> = [];
|
||||
const verifyRequests: Array<Record<string, unknown>> = [];
|
||||
await installAccountingFixture(page, {
|
||||
initialVoucher: voucher({ status: 'EXPORTED', version: 3, allowedActions: ['RECORD_RESULT'] }),
|
||||
recordRequests,
|
||||
verifyRequests,
|
||||
});
|
||||
|
||||
await page.goto('/finance/accounting?tab=result');
|
||||
await expect(page.getByText('VCH-202608-E2E001', { exact: true }).first()).toBeVisible();
|
||||
await page.getByRole('button', { name: '更多操作', exact: true }).click();
|
||||
await page.getByText('登记记账结果', { exact: true }).click();
|
||||
|
||||
const recordDialog = page.locator('.t-dialog').filter({ hasText: '登记记账结果' });
|
||||
const resultDate = await page.evaluate(() => {
|
||||
const now = new Date();
|
||||
return new Date(now.getTime() - now.getTimezoneOffset() * 60_000).toISOString().slice(0, 10);
|
||||
});
|
||||
await expect(recordDialog.locator('.t-date-picker input')).toHaveValue(resultDate);
|
||||
await recordDialog.getByPlaceholder('请输入', { exact: true }).fill('TPLUS-E2E-001');
|
||||
await recordDialog.locator('textarea').fill('人工导入成功');
|
||||
await recordDialog.locator('input[type="file"]').setInputFiles({
|
||||
name: 'accounting-result.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
buffer: Buffer.from('accounting-result-evidence'),
|
||||
});
|
||||
await recordDialog.getByRole('button', { name: '上传已选文件', exact: true }).click();
|
||||
await expect(recordDialog.getByText('可用', { exact: true })).toBeVisible();
|
||||
await recordDialog.getByRole('button', { name: '确认登记记账结果', exact: true }).click();
|
||||
|
||||
await expect.poll(() => recordRequests.length).toBe(1);
|
||||
expect(recordRequests[0]).toEqual({
|
||||
version: 3,
|
||||
externalVoucherNo: 'TPLUS-E2E-001',
|
||||
resultAt: resultDate,
|
||||
evidenceFileIds: ['01M00000000000000000000184'],
|
||||
remark: '人工导入成功',
|
||||
});
|
||||
await expect(recordDialog).toBeHidden();
|
||||
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: '更多操作', exact: true }).click();
|
||||
await page.getByText('复核记账结果', { exact: true }).click();
|
||||
const verifyDialog = page.locator('.t-dialog').filter({ hasText: '复核记账结果' });
|
||||
await verifyDialog.locator('textarea').fill('凭证号和导入结果一致');
|
||||
await verifyDialog.getByRole('button', { name: '确认复核记账结果', exact: true }).click();
|
||||
|
||||
await expect.poll(() => verifyRequests.length).toBe(1);
|
||||
expect(verifyRequests[0]).toEqual({ version: 4, opinion: '凭证号和导入结果一致' });
|
||||
await expect(verifyDialog).toBeHidden();
|
||||
|
||||
await page.getByText('VCH-202608-E2E001', { exact: true }).first().click();
|
||||
await expect(page.getByText('人工导入成功', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('凭证号和导入结果一致', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('PAGE-16 reverses and reopens a completed result from server allowed actions', async ({ page }) => {
|
||||
const reverseRequests: Array<Record<string, unknown>> = [];
|
||||
const reopenRequests: Array<Record<string, unknown>> = [];
|
||||
const evidence = {
|
||||
publicId: '01M00000000000000000000184',
|
||||
originalName: 'accounting-result.pdf',
|
||||
mediaType: 'application/pdf',
|
||||
sizeBytes: 20,
|
||||
sha256: 'c'.repeat(64),
|
||||
scanStatus: 'AVAILABLE',
|
||||
};
|
||||
await installAccountingFixture(page, {
|
||||
initialVoucher: voucher({
|
||||
status: 'COMPLETED',
|
||||
version: 5,
|
||||
allowedActions: ['REVERSE_RESULT'],
|
||||
externalVoucherNo: 'TPLUS-E2E-REVERSED',
|
||||
resultAt: '2026-08-15',
|
||||
evidenceFileIds: [evidence.publicId],
|
||||
resultRecordedByName: '记账登记员',
|
||||
resultRecordedAt: '2026-08-15T08:10:00Z',
|
||||
resultRecordRemark: '原记账结果',
|
||||
resultVerifiedByName: '记账复核员',
|
||||
resultVerifiedAt: '2026-08-15T08:15:00Z',
|
||||
resultVerifyOpinion: '原结果复核通过',
|
||||
}),
|
||||
initialEvidence: [evidence],
|
||||
reverseRequests,
|
||||
reopenRequests,
|
||||
});
|
||||
|
||||
await page.goto('/finance/accounting?tab=completed');
|
||||
await page.getByRole('button', { name: '更多操作', exact: true }).click();
|
||||
await page.getByText('冲销记账结果', { exact: true }).click();
|
||||
const reverseDialog = page.locator('.t-dialog').filter({ hasText: '冲销记账结果' });
|
||||
await reverseDialog.getByRole('button', { name: '确认冲销记账结果', exact: true }).click();
|
||||
await expect(reverseDialog.getByText('处理意见或原因至少填写 2 个字符。', { exact: true })).toBeVisible();
|
||||
await reverseDialog.locator('textarea').fill('外部账套凭证有误');
|
||||
await reverseDialog.getByRole('button', { name: '确认冲销记账结果', exact: true }).click();
|
||||
|
||||
await expect.poll(() => reverseRequests.length).toBe(1);
|
||||
expect(reverseRequests[0]).toEqual({ version: 5, reason: '外部账套凭证有误' });
|
||||
await expect(reverseDialog).toBeHidden();
|
||||
|
||||
await page.getByRole('button', { name: '更多操作', exact: true }).click();
|
||||
await page.getByText('重开记账结果', { exact: true }).click();
|
||||
const reopenDialog = page.locator('.t-dialog').filter({ hasText: '重开记账结果' });
|
||||
await reopenDialog.locator('textarea').fill('更正后重新登记');
|
||||
await reopenDialog.getByRole('button', { name: '确认重开记账结果', exact: true }).click();
|
||||
|
||||
await expect.poll(() => reopenRequests.length).toBe(1);
|
||||
expect(reopenRequests[0]).toEqual({ version: 6, reason: '更正后重新登记' });
|
||||
await expect(reopenDialog).toBeHidden();
|
||||
|
||||
await page.getByText('VCH-202608-E2E001', { exact: true }).first().click();
|
||||
await expect(page.getByRole('region', { name: '记账结果冲销与重开记录' })).toBeVisible();
|
||||
await expect(page.getByText('TPLUS-E2E-REVERSED', { exact: true })).toHaveCount(2);
|
||||
await expect(page.getByText('外部账套凭证有误', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('更正后重新登记', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('1 份证据', { exact: true })).toHaveCount(2);
|
||||
await expect(page.getByRole('region', { name: '记账结果证据' })).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user