305 行
8.7 KiB
JavaScript
305 行
8.7 KiB
JavaScript
const { toMoneyString } = require("./feeControl");
|
|
|
|
const REIMBURSEMENT_LEDGER_STATUS_VALUES = Object.freeze([
|
|
"draft",
|
|
"reviewing",
|
|
"approved",
|
|
"paid",
|
|
"rejected",
|
|
"cancelled",
|
|
]);
|
|
const REIMBURSEMENT_LEDGER_PAYMENT_STATUS_VALUES = Object.freeze(["unpaid", "paid"]);
|
|
const REIMBURSEMENT_LEDGER_INVOICE_STATUS_VALUES = Object.freeze([
|
|
"none",
|
|
"pending_ocr",
|
|
"ocr_failed",
|
|
"available",
|
|
"bound",
|
|
"archived",
|
|
]);
|
|
|
|
function toDateOnlyString(value) {
|
|
if (!value) return null;
|
|
if (typeof value === "string") return value.slice(0, 10);
|
|
return value.toISOString?.().slice(0, 10) ?? null;
|
|
}
|
|
|
|
function toDateTimeString(value) {
|
|
if (!value) return null;
|
|
if (typeof value === "string") return value;
|
|
return value.toISOString?.() ?? null;
|
|
}
|
|
|
|
function dateRange(dateFrom, dateTo) {
|
|
return {
|
|
...(dateFrom ? { gte: new Date(`${dateFrom}T00:00:00.000Z`) } : {}),
|
|
...(dateTo ? { lte: new Date(`${dateTo}T23:59:59.999Z`) } : {}),
|
|
};
|
|
}
|
|
|
|
function buildFinanceLedgerWhere(filters = {}) {
|
|
const clauses = [];
|
|
const query = filters.q?.trim();
|
|
|
|
if (filters.status) {
|
|
clauses.push({ status: filters.status });
|
|
}
|
|
|
|
if (filters.paymentStatus === "paid") {
|
|
clauses.push({ status: "paid" });
|
|
} else if (filters.paymentStatus === "unpaid") {
|
|
clauses.push({ status: { not: "paid" } });
|
|
}
|
|
|
|
if (filters.invoiceStatus === "none") {
|
|
clauses.push({ invoice_pool_items: { none: {} } });
|
|
} else if (filters.invoiceStatus) {
|
|
clauses.push({ invoice_pool_items: { some: { status: filters.invoiceStatus } } });
|
|
}
|
|
|
|
const reimbursementDateRange = dateRange(filters.dateFrom, filters.dateTo);
|
|
if (Object.keys(reimbursementDateRange).length > 0) {
|
|
clauses.push({ reimbursement_date: reimbursementDateRange });
|
|
}
|
|
|
|
if (query) {
|
|
clauses.push({
|
|
OR: [
|
|
{ form_number: { contains: query, mode: "insensitive" } },
|
|
{ description: { contains: query, mode: "insensitive" } },
|
|
{ project: { is: { project_no: { contains: query, mode: "insensitive" } } } },
|
|
{ project: { is: { name: { contains: query, mode: "insensitive" } } } },
|
|
{ owner: { name: { contains: query, mode: "insensitive" } } },
|
|
{ owner: { employee_no: { contains: query, mode: "insensitive" } } },
|
|
{ submitter: { name: { contains: query, mode: "insensitive" } } },
|
|
{ submitter: { employee_no: { contains: query, mode: "insensitive" } } },
|
|
{ department: { name: { contains: query, mode: "insensitive" } } },
|
|
],
|
|
});
|
|
}
|
|
|
|
return clauses.length ? { AND: clauses } : {};
|
|
}
|
|
|
|
const FINANCE_LEDGER_SELECT = {
|
|
id: true,
|
|
form_number: true,
|
|
reimbursement_date: true,
|
|
status: true,
|
|
total_amount: true,
|
|
description: true,
|
|
payment_voucher_no: true,
|
|
payment_date: true,
|
|
payment_channel: true,
|
|
paid_at: true,
|
|
created_at: true,
|
|
updated_at: true,
|
|
owner: { select: { id: true, employee_no: true, name: true } },
|
|
submitter: { select: { id: true, employee_no: true, name: true } },
|
|
department: { select: { id: true, name: true } },
|
|
project: { select: { id: true, project_no: true, name: true } },
|
|
paid_by_employee: { select: { id: true, employee_no: true, name: true } },
|
|
invoice_pool_items: {
|
|
select: {
|
|
id: true,
|
|
status: true,
|
|
archived_reason: true,
|
|
},
|
|
},
|
|
_count: {
|
|
select: {
|
|
attachments: true,
|
|
invoice_pool_items: true,
|
|
},
|
|
},
|
|
};
|
|
|
|
function summarizeInvoiceStatus(invoices) {
|
|
const counts = {
|
|
total: invoices.length,
|
|
pendingOcr: 0,
|
|
ocrFailed: 0,
|
|
available: 0,
|
|
bound: 0,
|
|
archived: 0,
|
|
usedComplete: 0,
|
|
};
|
|
|
|
for (const invoice of invoices) {
|
|
if (invoice.status === "pending_ocr") counts.pendingOcr += 1;
|
|
if (invoice.status === "ocr_failed") counts.ocrFailed += 1;
|
|
if (invoice.status === "available") counts.available += 1;
|
|
if (invoice.status === "bound") counts.bound += 1;
|
|
if (invoice.status === "archived") counts.archived += 1;
|
|
if (invoice.archived_reason === "used_complete") counts.usedComplete += 1;
|
|
}
|
|
|
|
if (counts.total === 0) return { status: "none", counts };
|
|
if (counts.ocrFailed > 0) return { status: "ocr_failed", counts };
|
|
if (counts.pendingOcr > 0) return { status: "pending_ocr", counts };
|
|
if (counts.total === counts.archived) return { status: "archived", counts };
|
|
if (counts.bound > 0) return { status: "bound", counts };
|
|
if (counts.total === counts.available) return { status: "available", counts };
|
|
return { status: "mixed", counts };
|
|
}
|
|
|
|
function mapEmployee(employee) {
|
|
return employee
|
|
? {
|
|
id: employee.id,
|
|
employeeNo: employee.employee_no,
|
|
name: employee.name,
|
|
}
|
|
: null;
|
|
}
|
|
|
|
function mapFinanceLedgerItem(item) {
|
|
const invoiceSummary = summarizeInvoiceStatus(item.invoice_pool_items ?? []);
|
|
return {
|
|
id: item.id,
|
|
formNumber: item.form_number,
|
|
reimbursementDate: toDateOnlyString(item.reimbursement_date),
|
|
status: item.status,
|
|
totalAmount: toMoneyString(item.total_amount),
|
|
description: item.description ?? null,
|
|
owner: mapEmployee(item.owner),
|
|
submitter: mapEmployee(item.submitter),
|
|
department: item.department ? { id: item.department.id, name: item.department.name } : null,
|
|
project: item.project
|
|
? {
|
|
id: item.project.id,
|
|
projectNo: item.project.project_no,
|
|
name: item.project.name,
|
|
}
|
|
: null,
|
|
payment: {
|
|
status: item.status === "paid" ? "paid" : "unpaid",
|
|
paymentVoucherNo: item.payment_voucher_no ?? null,
|
|
paymentDate: toDateOnlyString(item.payment_date),
|
|
paymentChannel: item.payment_channel ?? null,
|
|
paidAt: toDateTimeString(item.paid_at),
|
|
paidBy: mapEmployee(item.paid_by_employee),
|
|
},
|
|
invoice: invoiceSummary,
|
|
attachmentCount: item._count?.attachments ?? 0,
|
|
createdAt: toDateTimeString(item.created_at),
|
|
updatedAt: toDateTimeString(item.updated_at),
|
|
};
|
|
}
|
|
|
|
async function listFinanceLedger(prisma, filters = {}) {
|
|
const page = Math.max(1, Number(filters.page ?? 1));
|
|
const pageSize = Math.min(100, Math.max(1, Number(filters.pageSize ?? 20)));
|
|
const where = buildFinanceLedgerWhere(filters);
|
|
const [total, rows] = await Promise.all([
|
|
prisma.reimbursementForm.count({ where }),
|
|
prisma.reimbursementForm.findMany({
|
|
where,
|
|
orderBy: [{ updated_at: "desc" }, { id: "desc" }],
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
select: FINANCE_LEDGER_SELECT,
|
|
}),
|
|
]);
|
|
|
|
return {
|
|
page,
|
|
pageSize,
|
|
total,
|
|
items: rows.map(mapFinanceLedgerItem),
|
|
};
|
|
}
|
|
|
|
async function listFinanceLedgerForExport(prisma, filters = {}, limit = 5000) {
|
|
const rows = await prisma.reimbursementForm.findMany({
|
|
where: buildFinanceLedgerWhere(filters),
|
|
orderBy: [{ updated_at: "desc" }, { id: "desc" }],
|
|
take: limit,
|
|
select: FINANCE_LEDGER_SELECT,
|
|
});
|
|
return rows.map(mapFinanceLedgerItem);
|
|
}
|
|
|
|
function paymentStatusLabel(status) {
|
|
return status === "paid" ? "已付款" : "未付款";
|
|
}
|
|
|
|
function reimbursementStatusLabel(status) {
|
|
const labels = {
|
|
draft: "草稿",
|
|
reviewing: "审批中",
|
|
approved: "已通过",
|
|
paid: "已支付",
|
|
rejected: "已驳回",
|
|
cancelled: "已取消",
|
|
};
|
|
return labels[status] ?? status ?? "";
|
|
}
|
|
|
|
function invoiceStatusLabel(status) {
|
|
const labels = {
|
|
none: "无发票",
|
|
pending_ocr: "识别中",
|
|
ocr_failed: "识别失败",
|
|
available: "可添加",
|
|
bound: "已绑定",
|
|
archived: "已归档",
|
|
mixed: "多状态",
|
|
};
|
|
return labels[status] ?? status ?? "";
|
|
}
|
|
|
|
const FINANCE_LEDGER_EXPORT_HEADERS = Object.freeze([
|
|
"报销单号",
|
|
"申请人",
|
|
"部门",
|
|
"项目",
|
|
"金额",
|
|
"审批状态",
|
|
"付款状态",
|
|
"付款日期",
|
|
"付款凭证号",
|
|
"付款渠道",
|
|
"付款操作人",
|
|
"发票状态",
|
|
"发票数",
|
|
"附件数",
|
|
"报销日期",
|
|
"更新时间",
|
|
]);
|
|
|
|
function mapFinanceLedgerExportRow(item) {
|
|
return [
|
|
item.formNumber ?? item.id,
|
|
item.owner?.name ?? item.submitter?.name ?? "",
|
|
item.department?.name ?? "",
|
|
item.project ? [item.project.projectNo, item.project.name].filter(Boolean).join(" ") : "",
|
|
item.totalAmount,
|
|
reimbursementStatusLabel(item.status),
|
|
paymentStatusLabel(item.payment.status),
|
|
item.payment.paymentDate ?? "",
|
|
item.payment.paymentVoucherNo ?? "",
|
|
item.payment.paymentChannel ?? "",
|
|
item.payment.paidBy?.name ?? "",
|
|
invoiceStatusLabel(item.invoice.status),
|
|
item.invoice.counts.total,
|
|
item.attachmentCount,
|
|
item.reimbursementDate ?? "",
|
|
item.updatedAt ?? "",
|
|
];
|
|
}
|
|
|
|
module.exports = {
|
|
FINANCE_LEDGER_EXPORT_HEADERS,
|
|
REIMBURSEMENT_LEDGER_INVOICE_STATUS_VALUES,
|
|
REIMBURSEMENT_LEDGER_PAYMENT_STATUS_VALUES,
|
|
REIMBURSEMENT_LEDGER_STATUS_VALUES,
|
|
buildFinanceLedgerWhere,
|
|
invoiceStatusLabel,
|
|
listFinanceLedger,
|
|
listFinanceLedgerForExport,
|
|
mapFinanceLedgerExportRow,
|
|
paymentStatusLabel,
|
|
reimbursementStatusLabel,
|
|
};
|