A3 收口报销发票 OCR 主链路
这个提交包含在:
@@ -102,7 +102,7 @@ function buildActionErrorMessage(error: unknown, fallbackMessage: string) {
|
||||
}
|
||||
|
||||
function reimbursementStatusLabel(status: ReimbursementStatus) {
|
||||
return REIMBURSEMENT_STATUS_LABEL[status] ?? status;
|
||||
return REIMBURSEMENT_STATUS_LABEL[status] ?? "未知状态";
|
||||
}
|
||||
|
||||
function invoiceStatusLabel(status: string) {
|
||||
@@ -118,21 +118,30 @@ function resolveErpSyncStatus(
|
||||
| null,
|
||||
) {
|
||||
if (!exportState) {
|
||||
return "未同步";
|
||||
return "未导出";
|
||||
}
|
||||
if (exportState.lastOutboxStatus === "pending") {
|
||||
return "已入队";
|
||||
return "待导出";
|
||||
}
|
||||
if (exportState.lastOutboxStatus === "processing") {
|
||||
return "处理中";
|
||||
return "导出中";
|
||||
}
|
||||
if (exportState.lastOutboxStatus === "failed" || exportState.lastOutboxStatus === "dead_letter") {
|
||||
return "已失败";
|
||||
return "导出失败";
|
||||
}
|
||||
if (exportState.lastOutboxStatus === "succeeded" || exportState.exportedAt) {
|
||||
return "已成功";
|
||||
return "已导出";
|
||||
}
|
||||
return "未同步";
|
||||
return "未导出";
|
||||
}
|
||||
|
||||
function budgetControlModeLabel(controlMode: string | null | undefined) {
|
||||
const labels: Record<string, string> = {
|
||||
block: "超额阻断",
|
||||
warn: "超额提醒",
|
||||
manual_review: "人工复核",
|
||||
};
|
||||
return controlMode ? labels[controlMode] ?? "按制度处理" : "按制度处理";
|
||||
}
|
||||
|
||||
export function ReimbursementDetailPage() {
|
||||
@@ -533,7 +542,7 @@ export function ReimbursementDetailPage() {
|
||||
|
||||
const handleTrackedInvoicesUnavailable = useCallback((items: Array<{ id: string; originalName: string | null; message: string; availabilityMessage?: string | null; ocrLastError?: string | null }>) => {
|
||||
const message = items
|
||||
.map((item) => `${item.originalName ?? item.id.slice(0, 8)}: ${item.availabilityMessage ?? item.message ?? formatInvoiceOcrErrorMessage(item.ocrLastError)}`)
|
||||
.map((item) => `${item.originalName ?? "未命名发票"}: ${item.availabilityMessage ?? item.message ?? formatInvoiceOcrErrorMessage(item.ocrLastError)}`)
|
||||
.join(";");
|
||||
setInvoiceActionError(message);
|
||||
toast.error("部分发票需要处理");
|
||||
@@ -1356,10 +1365,10 @@ export function ReimbursementDetailPage() {
|
||||
本次提交后预计占用:{budgetHint.nextUsedAmount} / {budgetHint.allocatedAmount}
|
||||
({budgetHint.nextUsagePercent.toFixed(2)}%)
|
||||
</div>
|
||||
<div>控制模式:{budgetHint.controlMode}</div>
|
||||
<div>控制方式:{budgetControlModeLabel(budgetHint.controlMode)}</div>
|
||||
{budgetHint.exceedsBudget && budgetHint.controlMode === "block" ? (
|
||||
<p className="mt-3 rounded-md border border-red-200 bg-red-50 px-2 py-1 text-red-600">
|
||||
当前金额将超出预算上限,提交时会被阻断(block 模式)。
|
||||
当前金额将超出预算上限,提交时会被阻断。
|
||||
</p>
|
||||
) : budgetHint.exceedsThreshold ? (
|
||||
<p className="mt-3 rounded-md border border-amber-200 bg-amber-50 px-2 py-1 text-amber-700">
|
||||
@@ -1376,8 +1385,8 @@ export function ReimbursementDetailPage() {
|
||||
|
||||
{canManageIntegration ? (
|
||||
<SectionPanel
|
||||
title="ERP 同步状态"
|
||||
description="付款后同步到 ERP。"
|
||||
title="财务导出状态"
|
||||
description="付款后可生成财务导出记录。"
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -1389,18 +1398,18 @@ export function ReimbursementDetailPage() {
|
||||
预览凭证
|
||||
</Button>
|
||||
<Button type="button" disabled={!isPaidReimbursement || erpExportMutation.isPending} onClick={() => erpExportMutation.mutate()}>
|
||||
{erpExportMutation.isPending ? "导出中..." : "导出到 ERP"}
|
||||
{erpExportMutation.isPending ? "导出中..." : "生成导出"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{!isPaidReimbursement ? (
|
||||
<p className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-700">
|
||||
未满足导出条件,仅已付款报销可导出到 ERP。
|
||||
未满足导出条件,仅已付款报销可生成财务导出记录。
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{erpPreviewQuery.isLoading ? <p className="text-sm text-zinc-500">正在加载 ERP 凭证预览...</p> : null}
|
||||
{erpPreviewQuery.isLoading ? <p className="text-sm text-zinc-500">正在加载财务凭证预览...</p> : null}
|
||||
{erpPreviewQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(erpPreviewQuery.error)
|
||||
@@ -1409,25 +1418,25 @@ export function ReimbursementDetailPage() {
|
||||
erpPreviewQuery.error.details,
|
||||
erpPreviewQuery.error.message,
|
||||
)}`
|
||||
: "加载 ERP 凭证预览失败"}
|
||||
: "加载财务凭证预览失败"}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-3 text-sm md:grid-cols-2 xl:grid-cols-4" data-testid="reimbursement-erp-export">
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3">
|
||||
<div className="text-xs text-zinc-500">同步状态</div>
|
||||
<div className="text-xs text-zinc-500">导出状态</div>
|
||||
<div className="mt-1 font-medium text-zinc-900">{erpSyncStatus}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3">
|
||||
<div className="text-xs text-zinc-500">最后同步</div>
|
||||
<div className="text-xs text-zinc-500">最近导出</div>
|
||||
<div className="mt-1 font-medium text-zinc-900">{formatDateTime(erpPreviewQuery.data?.exportState.exportedAt ?? null)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3">
|
||||
<div className="text-xs text-zinc-500">同步队列</div>
|
||||
<div className="text-xs text-zinc-500">导出任务</div>
|
||||
<div className="mt-1 break-all font-medium text-zinc-900">{erpPreviewQuery.data?.exportState.lastOutboxId ?? "-"}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3">
|
||||
<div className="text-xs text-zinc-500">外部 ID</div>
|
||||
<div className="text-xs text-zinc-500">外部凭证号</div>
|
||||
<div className="mt-1 break-all font-medium text-zinc-900">{erpPreviewQuery.data?.exportState.externalId ?? "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1656,8 +1665,8 @@ export function ReimbursementDetailPage() {
|
||||
<Dialog open={isErpPreviewOpen} onOpenChange={setIsErpPreviewOpen}>
|
||||
<DialogContent className="max-h-[85vh] max-w-3xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>ERP 凭证预览</DialogTitle>
|
||||
<DialogDescription>查看凭证内容和最近同步状态。</DialogDescription>
|
||||
<DialogTitle>财务凭证预览</DialogTitle>
|
||||
<DialogDescription>查看凭证内容和最近导出状态。</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{erpPreviewQuery.data ? (
|
||||
@@ -1674,16 +1683,16 @@ export function ReimbursementDetailPage() {
|
||||
</div>
|
||||
<div className="space-y-2 rounded-lg border border-zinc-200 p-4">
|
||||
<h4 className="font-medium text-zinc-900">导出状态</h4>
|
||||
<div>同步状态:{erpSyncStatus}</div>
|
||||
<div>外部 ID:{erpPreviewQuery.data.exportState.externalId ?? "-"}</div>
|
||||
<div>队列 ID:{erpPreviewQuery.data.exportState.lastOutboxId ?? "-"}</div>
|
||||
<div>队列状态:{erpPreviewQuery.data.exportState.lastOutboxStatus ?? "-"}</div>
|
||||
<div>导出状态:{erpSyncStatus}</div>
|
||||
<div>外部凭证号:{erpPreviewQuery.data.exportState.externalId ?? "-"}</div>
|
||||
<div>任务编号:{erpPreviewQuery.data.exportState.lastOutboxId ?? "-"}</div>
|
||||
<div>任务状态:{erpSyncStatus}</div>
|
||||
<div>最近导出:{formatDateTime(erpPreviewQuery.data.exportState.exportedAt)}</div>
|
||||
<div className="break-all">最近错误:{erpPreviewQuery.data.exportState.lastError ?? "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-zinc-500">暂无可预览的 ERP 凭证数据。</p>
|
||||
<p className="text-sm text-zinc-500">暂无可预览的财务凭证数据。</p>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
在新工单中引用
屏蔽一个用户