重构财务台账工具与异常优先页面(05-30-finance-ledger-tools-redesign)

这个提交包含在:
2026-05-30 12:22:06 +08:00
父节点 c4edb12bb8
当前提交 51a1347827
共修改 17 个文件,包含 305 行新增和 117 行删除
+1
查看文件
@@ -46,6 +46,7 @@ export type PaymentBatch = {
reviewComment: string | null;
secondaryReviewComment: string | null;
executionSummaryJson: Record<string, unknown> | null;
receiptAttachmentCount?: number;
createdAt: string | null;
createdBy: string | null;
reviewedAt: string | null;
+40 -13
查看文件
@@ -284,6 +284,7 @@ export function ApOperationsPage() {
const [ledgerDateTo, setLedgerDateTo] = useState("");
const [ledgerSearch, setLedgerSearch] = useState("");
const [ledgerPage, setLedgerPage] = useState(1);
const [showLedgerTools, setShowLedgerTools] = useState(false);
const [reviewTarget, setReviewTarget] = useState<PaymentBatch | null>(null);
const [reviewComment, setReviewComment] = useState("");
@@ -617,8 +618,8 @@ export function ApOperationsPage() {
return (
<ListWorkspace>
<WorkspaceHeader
title="财务付款与对账"
subtitle="待付款、异常付款、风险、对账和台账集中处理。"
title="应付账款"
subtitle="优先处理待付款、异常付款、风险和对账差异;导出和代账交接进入台账工具。"
actions={
<>
<Button
@@ -632,24 +633,16 @@ export function ApOperationsPage() {
<Button
type="button"
variant="secondary"
onClick={() => exportMutation.mutate("csv")}
disabled={exportMutation.isPending}
onClick={() => setShowLedgerTools((current) => !current)}
>
{exportMutation.isPending ? "导出中..." : "导出 CSV"}
</Button>
<Button
type="button"
onClick={() => exportMutation.mutate("xlsx")}
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出 Excel"}
台账工具
</Button>
</>
}
/>
{summaryQuery.isError ? (
<SectionError message={resolveErrorMessage(summaryQuery.error, "AP 摘要加载失败")} />
<SectionError message={resolveErrorMessage(summaryQuery.error, "应付摘要加载失败")} />
) : null}
{paymentBatchesQuery.isError ? (
<SectionError message={resolveErrorMessage(paymentBatchesQuery.error, "付款批次数据加载失败")} />
@@ -677,6 +670,40 @@ export function ApOperationsPage() {
/>
</div>
{showLedgerTools ? (
<div className="finance-command-panel space-y-3" data-testid="ap-ledger-tools">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<h3 className="text-base font-semibold text-zinc-900">台账工具</h3>
<p className="mt-1 text-sm leading-6 text-zinc-500">
导出只用于财务复核和代账交接,范围按当前筛选条件生成;文件可能包含付款、票据和员工摘要信息。
</p>
</div>
<div className="flex shrink-0 flex-wrap gap-2">
<Button
type="button"
variant="secondary"
onClick={() => exportMutation.mutate("csv")}
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出 CSV"}
</Button>
<Button
type="button"
variant="secondary"
onClick={() => exportMutation.mutate("xlsx")}
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出 Excel"}
</Button>
</div>
</div>
<div className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600">
当前工具不会执行真实付款,也不会生成正式总账凭证;付款仍由线下银行或既有财务流程完成。
</div>
</div>
) : null}
<div className="finance-command-panel">
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as OperationsTab)}>
<TabsList className="ops-command-bar__tabs !w-full !justify-start">
+41 -21
查看文件
@@ -85,6 +85,7 @@ export function InvoicePoolPage() {
const [editTotalAmount, setEditTotalAmount] = useState("");
const [error, setError] = useState("");
const [confirmAction, setConfirmAction] = useState<{ type: string; invoiceId: string } | null>(null);
const [showLedgerTools, setShowLedgerTools] = useState(false);
const previousDetailStatusRef = useRef<InvoicePoolStatus | null>(null);
// --- Queries ---
@@ -459,28 +460,11 @@ export function InvoicePoolPage() {
<ListWorkspace>
<WorkspaceHeader
title="票据中心"
subtitle="上传、识别、绑定和异常复核集中处理。"
subtitle="优先看识别状态、失败票据和待绑定票据;导出、全范围和财务交接进入台账工具。"
actions={
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="invoice-export"
className="btn-secondary"
type="button"
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem data-testid="invoice-export-csv" onSelect={() => exportMutation.mutate("csv")}>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem data-testid="invoice-export-xlsx" onSelect={() => exportMutation.mutate("xlsx")}>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button type="button" variant={showLedgerTools ? "default" : "secondary"} onClick={() => setShowLedgerTools((current) => !current)}>
台账工具
</Button>
}
/>
@@ -490,6 +474,42 @@ export function InvoicePoolPage() {
</p>
) : null}
{showLedgerTools ? (
<div className="finance-command-panel space-y-3" data-testid="invoice-ledger-tools">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<h3 className="text-base font-semibold text-zinc-900">台账工具</h3>
<p className="mt-1 text-sm leading-6 text-zinc-500">
导出按当前状态和范围生成,只用于财务复核、OCR 排障和代账交接;普通员工日常补票据仍从报销详情或移动上传进入。
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="invoice-export"
type="button"
variant="secondary"
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem data-testid="invoice-export-csv" onSelect={() => exportMutation.mutate("csv")}>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem data-testid="invoice-export-xlsx" onSelect={() => exportMutation.mutate("xlsx")}>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600">
票据原文、预览和下载仍按详情页权限与审计控制处理,列表导出不作为敏感材料分发入口。
</div>
</div>
) : null}
<div className="finance-status-strip" data-testid="invoice-pool-summary">
<div className="finance-metric-card">
<div className="finance-metric-card__label">票据总数</div>
+89 -59
查看文件
@@ -113,6 +113,7 @@ export function PaymentBatchesPage() {
const receiptInputRef = useRef<HTMLInputElement | null>(null);
const [selectedReceiptFiles, setSelectedReceiptFiles] = useState<File[]>([]);
const [error, setError] = useState("");
const [showBatchTools, setShowBatchTools] = useState(false);
const [confirmPaymentAction, setConfirmPaymentAction] = useState<{
kind: "financeReview" | "financePayment" | "cancel";
batchId: string;
@@ -132,9 +133,17 @@ export function PaymentBatchesPage() {
const batchStats = useMemo(() => {
return {
total: batches.length,
actionable: batches.filter((item) => ["draft", "reviewed", "failed"].includes(item.status)).length,
highRisk: batches.filter((item) => item.highRisk).length,
failed: batches.filter((item) => item.status === "failed").length,
needsReview: batches.filter((item) => item.reviewRequirement.nextStep !== "ready_for_execute" && ["draft", "reviewed"].includes(item.status)).length,
readyToPay: batches.filter((item) => item.reviewRequirement.nextStep === "ready_for_execute" && ["reviewed", "failed"].includes(item.status)).length,
needsReceipt: batches.filter((item) =>
["reviewed", "failed"].includes(item.status) &&
(item.receiptAttachmentCount ?? 0) === 0 &&
item.items.some((entry) =>
["pending", "failed"].includes(entry.status) &&
(entry.paymentChannelOverride ?? item.defaultPaymentChannel) === "bank",
),
).length,
exceptions: batches.filter((item) => item.highRisk || item.status === "failed").length,
};
}, [batches]);
@@ -143,7 +152,6 @@ export function PaymentBatchesPage() {
enabled: Boolean(selectedBatch?.id),
queryFn: () => fetchPaymentBatchReceiptAttachments(selectedBatch!.id),
});
useEffect(() => {
if (!selectedBatch) {
return;
@@ -353,68 +361,88 @@ export function PaymentBatchesPage() {
return (
<ListWorkspace>
{downloadReasonDialog}
<WorkspaceHeader title="付款批次" subtitle="复核、回单和已付款标记集中处理。" />
<WorkspaceHeader
title="付款"
subtitle="先核对待复核、待付款和异常批次;真实付款在线下完成,OA 只记录复核、回单和付款状态。"
actions={
<>
<Button type="button" variant="secondary" onClick={() => void refresh()}>
刷新
</Button>
<Button type="button" variant={showBatchTools ? "default" : "secondary"} onClick={() => setShowBatchTools((current) => !current)}>
台账工具
</Button>
</>
}
/>
{error ? <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</div> : null}
<div className="finance-status-strip" data-testid="payment-batches-summary">
<div className="finance-metric-card">
<div className="finance-metric-card__label">待复核</div>
<div className="finance-metric-card__value">{batchStats.needsReview}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">待付款标记</div>
<div className="finance-metric-card__value">{batchStats.readyToPay}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">待补回单</div>
<div className="finance-metric-card__value">{batchStats.needsReceipt}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">异常批次</div>
<div className="finance-metric-card__value">{batchStats.exceptions}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">批次数</div>
<div className="finance-metric-card__value">{batchStats.total}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">可处理</div>
<div className="finance-metric-card__value">{batchStats.actionable}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">高风险</div>
<div className="finance-metric-card__value">{batchStats.highRisk}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">失败批次</div>
<div className="finance-metric-card__value">{batchStats.failed}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">当前批次</div>
<div className="finance-metric-card__value text-base">{selectedBatch?.batchNo ?? "-"}</div>
</div>
</div>
<div className="finance-workspace-grid">
{showBatchTools ? (
<div className="finance-command-panel space-y-4" data-testid="payment-batch-create-panel">
<h3 className="text-base font-semibold text-zinc-900">新建批次</h3>
<div>
<h3 className="text-base font-semibold text-zinc-900">台账工具 · 新建批次</h3>
<p className="mt-1 text-sm text-zinc-500">
仅把已确认待付款的单据归入批次。真实付款仍在银行或既有财务流程完成,OA 记录复核、回单和状态。
</p>
</div>
{!canManageBatch ? (
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
当前账号仅可查看批次并执行复核,创建与编辑入口仅对付款批次管理员开放。
</div>
) : null}
<div className="space-y-2">
<Label>实体类型</Label>
<select
className="select-std"
data-testid="payment-batch-create-entity-type"
value={entityType}
disabled={!canManageBatch}
onChange={(event) => setEntityType(event.target.value as PaymentBatchEntityType)}
>
<option value="reimbursement">报销付款</option>
<option value="travel_order">差旅订单付款</option>
</select>
</div>
<div className="space-y-2">
<Label>批次名称</Label>
<Input
data-testid="payment-batch-create-name"
value={name}
disabled={!canManageBatch}
onChange={(event) => setName(event.target.value)}
placeholder="例如:2026Q2-财务付款批次-01"
/>
<div className="grid gap-3 lg:grid-cols-[180px_minmax(0,1fr)]">
<div className="space-y-2">
<Label>实体类型</Label>
<select
className="select-std"
data-testid="payment-batch-create-entity-type"
value={entityType}
disabled={!canManageBatch}
onChange={(event) => setEntityType(event.target.value as PaymentBatchEntityType)}
>
<option value="reimbursement">报销付款</option>
<option value="travel_order">差旅订单付款</option>
</select>
</div>
<div className="space-y-2">
<Label>批次名称</Label>
<Input
data-testid="payment-batch-create-name"
value={name}
disabled={!canManageBatch}
onChange={(event) => setName(event.target.value)}
placeholder="例如:2026Q2-财务付款批次-01"
/>
</div>
</div>
<div className="space-y-2">
<Label>待付款单据 ID</Label>
<textarea
className="min-h-28 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm disabled:bg-zinc-50"
className="min-h-24 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm disabled:bg-zinc-50"
data-testid="payment-batch-create-entity-ids"
value={entityIds}
disabled={!canManageBatch}
@@ -422,7 +450,7 @@ export function PaymentBatchesPage() {
placeholder="输入报销或差旅待付款单据 ID,多个用逗号或换行分隔"
/>
</div>
<div className="grid gap-3 md:grid-cols-2">
<div className="grid gap-3 md:grid-cols-3">
<div className="space-y-2">
<Label>默认付款日期</Label>
<DateTextInput
@@ -439,29 +467,31 @@ export function PaymentBatchesPage() {
data-testid="payment-batch-create-payment-channel"
value={defaultPaymentChannel}
disabled={!canManageBatch}
onChange={(event) => setDefaultPaymentChannel((event.target.value as PaymentChannel | "") || "") }
onChange={(event) => setDefaultPaymentChannel((event.target.value as PaymentChannel | "") || "")}
>
<option value="">不设置</option>
{PAYMENT_CHANNEL_OPTIONS.map((item) => <option key={item} value={item}>{paymentChannelLabel(item)}</option>)}
</select>
</div>
</div>
<div className="space-y-2">
<Label>凭证前缀</Label>
<Input
data-testid="payment-batch-create-voucher-prefix"
value={voucherPrefix}
disabled={!canManageBatch}
onChange={(event) => setVoucherPrefix(event.target.value)}
placeholder="例如 PBX"
/>
<div className="space-y-2">
<Label>凭证前缀</Label>
<Input
data-testid="payment-batch-create-voucher-prefix"
value={voucherPrefix}
disabled={!canManageBatch}
onChange={(event) => setVoucherPrefix(event.target.value)}
placeholder="例如 PBX"
/>
</div>
</div>
<Button type="button" disabled={createMutation.isPending || !canManageBatch} onClick={() => createMutation.mutate()}>
{createMutation.isPending ? "创建中..." : "创建批次"}
</Button>
</div>
) : null}
<div className="space-y-5">
<div className="finance-workspace-grid">
<div className="grid gap-5 xl:col-span-2 xl:grid-cols-[340px,minmax(0,1fr)]">
<div className="finance-command-panel">
<h3 className="mb-3 text-base font-semibold text-zinc-900">批次列表</h3>
<div className="space-y-2">
+4 -1
查看文件
@@ -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 });