重构财务台账工具与异常优先页面(05-30-finance-ledger-tools-redesign)
这个提交包含在:
@@ -109,7 +109,10 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
|
||||
const paymentDate = new Date().toISOString().slice(0, 10);
|
||||
const batchName = `E2E-报销付款批次-${Date.now()}`;
|
||||
await page.goto("/payment-batches");
|
||||
await expect(page.getByTestId("payment-batch-create-panel")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "台账工具" }).click();
|
||||
const createCard = page.getByTestId("payment-batch-create-panel");
|
||||
await expect(createCard).toBeVisible();
|
||||
const createBatchResponsePromise = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/payment-batches" && response.ok();
|
||||
@@ -124,7 +127,7 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
|
||||
const createBatchPayload = (await (await createBatchResponsePromise).json()) as { data?: { item?: { id?: string } } };
|
||||
const batchId = createBatchPayload.data?.item?.id;
|
||||
if (!batchId) {
|
||||
throw new Error(`创建支付批次后未返回 id: ${JSON.stringify(createBatchPayload)}`);
|
||||
throw new Error(`创建付款批次后未返回 id: ${JSON.stringify(createBatchPayload)}`);
|
||||
}
|
||||
|
||||
await logout(page, FINANCE.name);
|
||||
|
||||
@@ -1129,7 +1129,7 @@ test(`${trialrunIssueId} 浏览器业务先行试运行:报销、审批、请
|
||||
|
||||
const finance = await loginRole(page, "finance");
|
||||
await page.goto("/ap-operations");
|
||||
await expect(page.getByRole("heading", { name: /AP 运营|财务付款与对账/ }).first()).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "应付账款", exact: true })).toBeVisible();
|
||||
if (await page.getByText(/当前功能未启用/).first().isVisible().catch(() => false)) {
|
||||
return {
|
||||
status: "SKIP",
|
||||
@@ -1158,17 +1158,18 @@ test(`${trialrunIssueId} 浏览器业务先行试运行:报销、审批、请
|
||||
const batchName = `${trialrunMarkerPrefix} 浏览器付款批次 ${marker}`;
|
||||
state.paymentBatchName = batchName;
|
||||
await page.goto("/payment-batches");
|
||||
await expect(page.getByRole("heading", { name: "支付批次", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "付款", exact: true })).toBeVisible();
|
||||
if (await page.getByText(/当前功能未启用/).first().isVisible().catch(() => false)) {
|
||||
return {
|
||||
status: "SKIP",
|
||||
businessId: state.reimbursementCode ?? state.reimbursementId,
|
||||
pcResult: "SKIP: 支付批次功能开关未启用",
|
||||
pcResult: "SKIP: 付款批次功能开关未启用",
|
||||
conclusion: "暂停",
|
||||
detail: "报销审批已完成;/payment-batches 返回当前功能未启用,未创建付款批次或执行付款。",
|
||||
};
|
||||
}
|
||||
const createCard = page.locator(".card-std").filter({ hasText: "新建批次" }).first();
|
||||
await page.getByRole("button", { name: "台账工具" }).click();
|
||||
const createCard = page.getByTestId("payment-batch-create-panel");
|
||||
await expect(createCard.getByRole("button", { name: "创建批次" })).toBeEnabled();
|
||||
const createBatchResponsePromise = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
@@ -1176,19 +1177,19 @@ test(`${trialrunIssueId} 浏览器业务先行试运行:报销、审批、请
|
||||
});
|
||||
await createCard.locator("select").nth(0).selectOption("reimbursement");
|
||||
await createCard.getByPlaceholder("例如:2026Q2-财务付款批次-01").fill(batchName);
|
||||
await createCard.getByPlaceholder("用逗号或换行分隔 reimbursement / travel_order ID").fill(state.reimbursementId);
|
||||
await createCard.getByPlaceholder("输入报销或差旅待付款单据 ID,多个用逗号或换行分隔").fill(state.reimbursementId);
|
||||
await createCard.locator('input[type="date"]').fill(new Date().toISOString().slice(0, 10));
|
||||
await createCard.locator("select").nth(1).selectOption("bank");
|
||||
await createCard.getByPlaceholder("例如 PBX").fill(`${trialrunSafePrefix}-${marker.slice(-6)}`);
|
||||
await createCard.getByRole("button", { name: "创建批次" }).click();
|
||||
const createBatchResponse = await createBatchResponsePromise;
|
||||
if (!createBatchResponse.ok()) {
|
||||
throw new Error(`创建支付批次失败:HTTP ${createBatchResponse.status()} ${await createBatchResponse.text()}`);
|
||||
throw new Error(`创建付款批次失败:HTTP ${createBatchResponse.status()} ${await createBatchResponse.text()}`);
|
||||
}
|
||||
const createBatchPayload = (await createBatchResponse.json()) as { data?: { item?: PaymentBatch } };
|
||||
const batch = createBatchPayload.data?.item;
|
||||
if (!batch?.id) {
|
||||
throw new Error(`创建支付批次后未返回 id: ${JSON.stringify(createBatchPayload)}`);
|
||||
throw new Error(`创建付款批次后未返回 id: ${JSON.stringify(createBatchPayload)}`);
|
||||
}
|
||||
state.paymentBatchId = batch.id;
|
||||
await logoutLiveUser(page, { name: finance.name }).catch(() => undefined);
|
||||
|
||||
@@ -93,13 +93,13 @@ const ROUTE_PLANS: RoutePlan[] = [
|
||||
{ role: "finance", route: "/portal", label: "财务工作台", layer: "A", expectation: "accessible" },
|
||||
{ role: "finance", route: "/invoice-pool", label: "财务票据中心", layer: "B", expectation: "accessible" },
|
||||
{ role: "finance", route: "/payment-batches", label: "财务付款批次", layer: "B", expectation: "accessible" },
|
||||
{ role: "finance", route: "/ap-operations", label: "财务 AP 运营", layer: "B", expectation: "accessible" },
|
||||
{ role: "finance", route: "/ap-operations", label: "财务应付账款", layer: "B", expectation: "accessible" },
|
||||
{ role: "finance", route: "/reimbursements", label: "财务报销列表", layer: "A", expectation: "accessible" },
|
||||
|
||||
{ role: "finance_mgr", route: "/portal", label: "财务负责人工作台", layer: "A", expectation: "accessible" },
|
||||
{ role: "finance_mgr", route: "/workflow/tasks", label: "财务负责人审批待办", layer: "A", expectation: "accessible" },
|
||||
{ role: "finance_mgr", route: "/payment-batches", label: "财务负责人付款批次", layer: "B", expectation: "accessible" },
|
||||
{ role: "finance_mgr", route: "/ap-operations", label: "财务负责人 AP 运营", layer: "B", expectation: "accessible" },
|
||||
{ role: "finance_mgr", route: "/ap-operations", label: "财务负责人应付账款", layer: "B", expectation: "accessible" },
|
||||
{ role: "finance_mgr", route: "/audit-logs", label: "财务负责人审计日志", layer: "C", expectation: "accessible" },
|
||||
|
||||
{ role: "admin", route: "/portal", label: "管理员工作台", layer: "A", expectation: "accessible" },
|
||||
|
||||
@@ -1030,6 +1030,31 @@ test("PC 财务与管理页 Wave4-5 视觉冒烟", async ({ page }) => {
|
||||
}
|
||||
});
|
||||
|
||||
test("PC 财务台账工具默认收起并可按需打开", async ({ page }) => {
|
||||
await mockWave4Apis(page);
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await login(page);
|
||||
|
||||
await page.goto("/payment-batches");
|
||||
await expect(page.getByTestId("payment-batch-create-panel")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "台账工具" }).click();
|
||||
await expect(page.getByTestId("payment-batch-create-panel")).toBeVisible();
|
||||
await expect(page.getByTestId("payment-batch-create-panel")).toContainText("台账工具 · 新建批次");
|
||||
|
||||
await page.goto("/ap-operations");
|
||||
await expect(page.getByRole("button", { name: "导出 CSV" })).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "台账工具" }).click();
|
||||
await expect(page.getByTestId("ap-ledger-tools")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "导出 CSV" })).toBeVisible();
|
||||
|
||||
await page.goto("/invoice-pool");
|
||||
await expect(page.getByTestId("invoice-export")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "台账工具" }).click();
|
||||
await expect(page.getByTestId("invoice-ledger-tools")).toBeVisible();
|
||||
await page.getByTestId("invoice-export").click();
|
||||
await expect(page.getByTestId("invoice-export-csv")).toBeVisible();
|
||||
});
|
||||
|
||||
test("PC 票据中心辅助核验动作可提交并重试", async ({ page }) => {
|
||||
const failedVerification: MockVerificationRecord = {
|
||||
...(invoiceListItem.verification as MockVerificationRecord),
|
||||
|
||||
@@ -521,7 +521,7 @@ test.describe("R3 权限直连审计", () => {
|
||||
await expectApiStatus({ role: "employee", api: employee.api, path: "/api/invoice-pool?ownerScope=all", statuses: [403], note: "员工不得查看全量票据" });
|
||||
await expectApiStatus({ role: "employee", api: employee.api, path: "/api/audit-logs", statuses: [403], note: "员工不得查看审计日志" });
|
||||
await expectApiStatus({ role: "employee", api: employee.api, path: "/api/notifications/deliveries", statuses: [403], note: "员工不得查看通知投递审计" });
|
||||
await expectApiMutationStatus({ role: "employee", api: employee.api, path: "/api/exports/ap-operations", method: "post", data: { format: "csv" }, statuses: [403, 409], note: "员工不得导出 AP 运营数据;409 表示功能门禁关闭且未泄露数据" });
|
||||
await expectApiMutationStatus({ role: "employee", api: employee.api, path: "/api/exports/ap-operations", method: "post", data: { format: "csv" }, statuses: [403, 409], note: "员工不得导出应付账款数据;409 表示功能门禁关闭且未泄露数据" });
|
||||
await expectApiStatus({ role: "employee", api: employee.api, path: "/api/contracts-registry", statuses: [200, 409], note: "员工合同台账只能走范围过滤或功能门禁" });
|
||||
await expectApiStatus({ role: "employee", api: employee.api, path: "/api/travel-requests?mine=0&pageSize=50", statuses: [403], note: "差旅冻结期员工不得直连差旅列表" });
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ensureFeatureFlagOn } from "./helpers/featureFlags";
|
||||
import { openNavLink } from "./helpers/navigation";
|
||||
import { TEST_IDS } from "./helpers/testIds";
|
||||
|
||||
test("E2E 报销支付批次主链路:提交 -> 审批 -> 创建批次 -> 复核 -> 执行 -> 下游收口", async ({
|
||||
test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复核 -> 执行 -> 下游收口", async ({
|
||||
page,
|
||||
reimbursementDetail,
|
||||
workflowDetail,
|
||||
@@ -104,7 +104,10 @@ test("E2E 报销支付批次主链路:提交 -> 审批 -> 创建批次 -> 复
|
||||
await page.goto("/payment-batches");
|
||||
await expect(page).toHaveURL(/\/payment-batches/);
|
||||
|
||||
await expect(page.getByTestId("payment-batch-create-panel")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "台账工具" }).click();
|
||||
const createCard = page.getByTestId("payment-batch-create-panel");
|
||||
await expect(createCard).toBeVisible();
|
||||
const createBatchResponsePromise = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/payment-batches" && response.ok();
|
||||
@@ -122,7 +125,7 @@ test("E2E 报销支付批次主链路:提交 -> 审批 -> 创建批次 -> 复
|
||||
};
|
||||
const batchId = createBatchPayload.data?.item?.id;
|
||||
if (!batchId) {
|
||||
throw new Error(`创建支付批次后未返回 id: ${JSON.stringify(createBatchPayload)}`);
|
||||
throw new Error(`创建付款批次后未返回 id: ${JSON.stringify(createBatchPayload)}`);
|
||||
}
|
||||
await page.goto(`/payment-batches?id=${batchId}`);
|
||||
const batchDetailCard = page.getByTestId("payment-batch-detail-panel").filter({ hasText: batchName });
|
||||
|
||||
在新工单中引用
屏蔽一个用户