FL6 完善财务台账与导出审计
这个提交包含在:
+61
-1
@@ -19,6 +19,14 @@ const {
|
||||
} = require("../services/employeeDirectoryFilters");
|
||||
const { BUDGET_RISK_CODES, toMoneyNumber, toMoneyString } = require("../services/feeControl");
|
||||
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
||||
const {
|
||||
FINANCE_LEDGER_EXPORT_HEADERS,
|
||||
REIMBURSEMENT_LEDGER_INVOICE_STATUS_VALUES,
|
||||
REIMBURSEMENT_LEDGER_PAYMENT_STATUS_VALUES,
|
||||
REIMBURSEMENT_LEDGER_STATUS_VALUES,
|
||||
listFinanceLedgerForExport,
|
||||
mapFinanceLedgerExportRow,
|
||||
} = require("../services/financeLedger");
|
||||
const {
|
||||
HISTORY_STATUS_VALUES,
|
||||
buildPresenceValidationError,
|
||||
@@ -109,6 +117,12 @@ const AP_OPERATIONS_EXPORT_BODY_SCHEMA = z.object({
|
||||
pendingCreatedAtFrom: DATE_ONLY_SCHEMA.optional(),
|
||||
pendingCreatedAtTo: DATE_ONLY_SCHEMA.optional(),
|
||||
riskBucket: z.enum(["all", "exception", "budget_overspend"]).optional(),
|
||||
ledgerStatus: z.enum(REIMBURSEMENT_LEDGER_STATUS_VALUES).optional(),
|
||||
ledgerPaymentStatus: z.enum(REIMBURSEMENT_LEDGER_PAYMENT_STATUS_VALUES).optional(),
|
||||
ledgerInvoiceStatus: z.enum(REIMBURSEMENT_LEDGER_INVOICE_STATUS_VALUES).optional(),
|
||||
ledgerDateFrom: DATE_ONLY_SCHEMA.optional(),
|
||||
ledgerDateTo: DATE_ONLY_SCHEMA.optional(),
|
||||
ledgerSearch: z.string().trim().max(100).optional(),
|
||||
}).strict();
|
||||
|
||||
const PRESENCE_HISTORY_EXPORT_LIMIT = 5000;
|
||||
@@ -573,8 +587,23 @@ async function loadApOperationsExportDataset(prisma, filters) {
|
||||
resolution_status: "pending",
|
||||
match_status: { not: "matched" },
|
||||
};
|
||||
const ledgerFilters = {
|
||||
status: filters.ledgerStatus,
|
||||
paymentStatus: filters.ledgerPaymentStatus,
|
||||
invoiceStatus: filters.ledgerInvoiceStatus,
|
||||
dateFrom: filters.ledgerDateFrom,
|
||||
dateTo: filters.ledgerDateTo,
|
||||
q: filters.ledgerSearch,
|
||||
};
|
||||
|
||||
const [pendingPaymentBatches, failedPaymentBatchItems, riskFlags, reconciliationAnomalies] = await Promise.all([
|
||||
const [
|
||||
reimbursementLedger,
|
||||
pendingPaymentBatches,
|
||||
failedPaymentBatchItems,
|
||||
riskFlags,
|
||||
reconciliationAnomalies,
|
||||
] = await Promise.all([
|
||||
listFinanceLedgerForExport(prisma, ledgerFilters, 5000),
|
||||
prisma.paymentBatch.findMany({
|
||||
where: pendingWhere,
|
||||
orderBy: [{ created_at: "desc" }, { id: "desc" }],
|
||||
@@ -617,6 +646,7 @@ async function loadApOperationsExportDataset(prisma, filters) {
|
||||
]);
|
||||
|
||||
return {
|
||||
reimbursementLedger,
|
||||
pendingWhere,
|
||||
riskWhere,
|
||||
reconciliationWhere,
|
||||
@@ -630,6 +660,13 @@ async function loadApOperationsExportDataset(prisma, filters) {
|
||||
async function* streamApOperationsCsv(dataset) {
|
||||
yield "\ufeff";
|
||||
|
||||
yield csvRow(["财务台账"]);
|
||||
yield csvRow(FINANCE_LEDGER_EXPORT_HEADERS);
|
||||
for (const item of dataset.reimbursementLedger) {
|
||||
yield csvRow(mapFinanceLedgerExportRow(item));
|
||||
}
|
||||
yield csvRow([]);
|
||||
|
||||
yield csvRow(["待付款批次"]);
|
||||
yield csvRow(["批次编号", "实体类型", "批次名称", "状态", "总金额", "高风险", "创建时间"]);
|
||||
for (const item of dataset.pendingPaymentBatches) {
|
||||
@@ -661,6 +698,17 @@ async function* streamApOperationsCsv(dataset) {
|
||||
async function buildApOperationsXlsx(dataset) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
|
||||
const ledgerSheet = workbook.addWorksheet("财务台账");
|
||||
ledgerSheet.columns = FINANCE_LEDGER_EXPORT_HEADERS.map((header, index) => ({
|
||||
header,
|
||||
key: `col${index}`,
|
||||
width: index === 0 ? 24 : index === 14 ? 24 : 16,
|
||||
}));
|
||||
for (const item of dataset.reimbursementLedger) {
|
||||
const row = mapFinanceLedgerExportRow(item);
|
||||
ledgerSheet.addRow(Object.fromEntries(row.map((value, index) => [`col${index}`, value])));
|
||||
}
|
||||
|
||||
const pendingSheet = workbook.addWorksheet("待付款批次");
|
||||
pendingSheet.columns = [
|
||||
{ header: "批次编号", key: "batchNo", width: 24 },
|
||||
@@ -1617,6 +1665,12 @@ async function exportsRoutes(fastify) {
|
||||
pendingCreatedAtFrom: parsedBody.pendingCreatedAtFrom,
|
||||
pendingCreatedAtTo: parsedBody.pendingCreatedAtTo,
|
||||
riskBucket: parsedBody.riskBucket ?? "all",
|
||||
ledgerStatus: parsedBody.ledgerStatus,
|
||||
ledgerPaymentStatus: parsedBody.ledgerPaymentStatus,
|
||||
ledgerInvoiceStatus: parsedBody.ledgerInvoiceStatus,
|
||||
ledgerDateFrom: parsedBody.ledgerDateFrom,
|
||||
ledgerDateTo: parsedBody.ledgerDateTo,
|
||||
ledgerSearch: parsedBody.ledgerSearch,
|
||||
};
|
||||
const dataset = await loadApOperationsExportDataset(fastify.prisma, filters);
|
||||
|
||||
@@ -1635,6 +1689,12 @@ async function exportsRoutes(fastify) {
|
||||
failedPaymentItemTotal: dataset.failedPaymentBatchItems.length,
|
||||
openRiskFlagTotal: dataset.riskFlags.length,
|
||||
reconciliationAnomalyTotal: dataset.reconciliationAnomalies.length,
|
||||
financeLedgerTotal: dataset.reimbursementLedger.length,
|
||||
ledgerStatus: filters.ledgerStatus ?? null,
|
||||
ledgerPaymentStatus: filters.ledgerPaymentStatus ?? null,
|
||||
ledgerInvoiceStatus: filters.ledgerInvoiceStatus ?? null,
|
||||
ledgerDateFrom: filters.ledgerDateFrom ?? null,
|
||||
ledgerDateTo: filters.ledgerDateTo ?? null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户