实现企业银行账户与建行制单(corporate-bank-account-payment-order)
这个提交包含在:
@@ -0,0 +1,197 @@
|
||||
const XLSX = require("xlsx");
|
||||
|
||||
const { decryptStringOrThrow } = require("./crypto");
|
||||
|
||||
const HEADERS = Object.freeze([
|
||||
"*序号",
|
||||
"*付款方客户账号",
|
||||
"*付款方账户名称",
|
||||
"*收款方行别代码\n(01-本行 02-国内他行)",
|
||||
"*收款方客户账号",
|
||||
"*收款方账户名称",
|
||||
"收款方开户行名称",
|
||||
"收款方联行号",
|
||||
"客户方流水号",
|
||||
"*金额",
|
||||
"*用途",
|
||||
"备注",
|
||||
"是否短信通知收款人(0-不通知,1-通知,默认为0-不通知。)",
|
||||
"收款人手机号码",
|
||||
"短信通知附加信息",
|
||||
]);
|
||||
|
||||
class PaymentOrderXlsError extends Error {
|
||||
constructor(code, details = {}) {
|
||||
super(code);
|
||||
this.name = "PaymentOrderXlsError";
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
function buildCustomerSerial(batchNo, rowNumber) {
|
||||
const serial = `${String(batchNo).trim()}-${String(rowNumber).padStart(3, "0")}`;
|
||||
if (serial.length > 32) {
|
||||
throw new PaymentOrderXlsError("customer_serial_too_long");
|
||||
}
|
||||
if (!/^[A-Z0-9-]+$/.test(serial)) {
|
||||
throw new PaymentOrderXlsError("customer_serial_invalid_characters");
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
|
||||
function requiredText(value, code) {
|
||||
const normalized = typeof value === "string" ? value.trim() : "";
|
||||
if (!normalized) throw new PaymentOrderXlsError(code);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function decryptRequired(config, value, code) {
|
||||
try {
|
||||
return requiredText(value ? decryptStringOrThrow(config, value) : "", code);
|
||||
} catch (error) {
|
||||
if (error instanceof PaymentOrderXlsError) throw error;
|
||||
throw new PaymentOrderXlsError(code);
|
||||
}
|
||||
}
|
||||
|
||||
function stringCell(value = "") {
|
||||
return { t: "s", v: String(value) };
|
||||
}
|
||||
|
||||
function buildPaymentOrderRows(config, batch) {
|
||||
const blockers = [];
|
||||
const addBlocker = (code, entityCode) => {
|
||||
blockers.push({ code, ...(entityCode ? { entityCode: String(entityCode) } : {}) });
|
||||
};
|
||||
const payer = batch?.payment_account_snapshot_json;
|
||||
if (!payer || typeof payer !== "object" || Array.isArray(payer)) {
|
||||
addBlocker("payment_account_snapshot_missing");
|
||||
}
|
||||
if (payer && !payer.bankInstitutionCode) {
|
||||
addBlocker("payer_bank_institution_missing");
|
||||
} else if (payer && payer.bankInstitutionCode !== "CCB") {
|
||||
addBlocker("payer_bank_not_supported");
|
||||
}
|
||||
let payerAccount = "";
|
||||
let payerName = "";
|
||||
if (payer) {
|
||||
try {
|
||||
payerAccount = decryptRequired(config, payer.accountNumberEncrypted, "payer_account_unavailable");
|
||||
} catch (error) {
|
||||
addBlocker(error.code ?? "payer_account_unavailable");
|
||||
}
|
||||
try {
|
||||
payerName = requiredText(payer.accountName, "payer_name_missing");
|
||||
} catch (error) {
|
||||
addBlocker(error.code ?? "payer_name_missing");
|
||||
}
|
||||
}
|
||||
|
||||
const items = [...(batch.items ?? [])].sort((left, right) =>
|
||||
String(left.entity_code).localeCompare(String(right.entity_code), "zh-CN")
|
||||
|| String(left.id).localeCompare(String(right.id)),
|
||||
);
|
||||
if (!items.length) addBlocker("items_missing");
|
||||
|
||||
const serials = new Set();
|
||||
let totalAmount = 0;
|
||||
const preparedRows = items.map((item, index) => {
|
||||
const payee = item.snapshot_json?.payee;
|
||||
const entityCodeHint = typeof item.entity_code === "string" && item.entity_code.trim()
|
||||
? item.entity_code.trim()
|
||||
: undefined;
|
||||
if (!payee?.bankInstitutionCode) {
|
||||
addBlocker("payee_bank_institution_missing", entityCodeHint);
|
||||
} else if (payee.bankInstitutionCode !== "CCB") {
|
||||
addBlocker("payee_bank_not_supported", entityCodeHint);
|
||||
}
|
||||
let payeeAccount = "";
|
||||
let payeeName = "";
|
||||
let entityCode = "";
|
||||
try {
|
||||
payeeAccount = decryptRequired(config, payee?.bankAccountEncrypted, "payee_account_unavailable");
|
||||
} catch (error) {
|
||||
addBlocker(error.code ?? "payee_account_unavailable", entityCodeHint);
|
||||
}
|
||||
try {
|
||||
payeeName = requiredText(payee?.name, "payee_name_missing");
|
||||
} catch (error) {
|
||||
addBlocker(error.code ?? "payee_name_missing", entityCodeHint);
|
||||
}
|
||||
try {
|
||||
entityCode = requiredText(item.entity_code, "entity_code_missing");
|
||||
} catch (error) {
|
||||
addBlocker(error.code ?? "entity_code_missing");
|
||||
}
|
||||
const amount = Number(item.amount);
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
addBlocker("amount_invalid", entityCodeHint);
|
||||
}
|
||||
let serial = "";
|
||||
try {
|
||||
serial = buildCustomerSerial(batch.batch_no, index + 1);
|
||||
if (serials.has(serial)) addBlocker("customer_serial_duplicate", entityCodeHint);
|
||||
serials.add(serial);
|
||||
} catch (error) {
|
||||
addBlocker(error.code ?? "customer_serial_invalid", entityCodeHint);
|
||||
}
|
||||
if (Number.isFinite(amount) && amount > 0) totalAmount += amount;
|
||||
return { payee, payeeAccount, payeeName, entityCode, amount, serial };
|
||||
});
|
||||
|
||||
if (blockers.length) {
|
||||
throw new PaymentOrderXlsError(blockers[0].code, { blockers });
|
||||
}
|
||||
|
||||
const rows = preparedRows.map(({ payee, payeeAccount, payeeName, entityCode, amount, serial }, index) => {
|
||||
return [
|
||||
stringCell(index + 1),
|
||||
stringCell(payerAccount),
|
||||
stringCell(payerName),
|
||||
stringCell("01"),
|
||||
stringCell(payeeAccount),
|
||||
stringCell(payeeName),
|
||||
stringCell(payee.bankName ?? "中国建设银行"),
|
||||
stringCell(""),
|
||||
stringCell(serial),
|
||||
{ t: "n", v: Number(amount.toFixed(2)), z: "0.00" },
|
||||
stringCell("报销款"),
|
||||
stringCell(entityCode),
|
||||
stringCell("0"),
|
||||
stringCell(""),
|
||||
stringCell(""),
|
||||
];
|
||||
});
|
||||
return { rows, totalAmount: totalAmount.toFixed(2), serials: [...serials] };
|
||||
}
|
||||
|
||||
function buildSheet(rows) {
|
||||
const sheet = {};
|
||||
const allRows = [HEADERS.map(stringCell), ...rows];
|
||||
for (let row = 0; row < allRows.length; row += 1) {
|
||||
for (let column = 0; column < HEADERS.length; column += 1) {
|
||||
sheet[XLSX.utils.encode_cell({ r: row, c: column })] = allRows[row][column] ?? stringCell("");
|
||||
}
|
||||
}
|
||||
sheet["!ref"] = XLSX.utils.encode_range({ r: 0, c: 0 }, { r: allRows.length - 1, c: HEADERS.length - 1 });
|
||||
return sheet;
|
||||
}
|
||||
|
||||
function generatePaymentOrderXls(config, batch) {
|
||||
const { rows, totalAmount, serials } = buildPaymentOrderRows(config, batch);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, buildSheet(rows), "Sheet1");
|
||||
XLSX.utils.book_append_sheet(workbook, {}, "Sheet2");
|
||||
XLSX.utils.book_append_sheet(workbook, {}, "Sheet3");
|
||||
const buffer = XLSX.write(workbook, { bookType: "biff8", type: "buffer" });
|
||||
return { buffer, rowCount: rows.length, totalAmount, serials };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HEADERS,
|
||||
PaymentOrderXlsError,
|
||||
buildCustomerSerial,
|
||||
buildPaymentOrderRows,
|
||||
generatePaymentOrderXls,
|
||||
};
|
||||
@@ -86,7 +86,7 @@ const NAVIGATION_REGISTRY = Object.freeze({
|
||||
organization: { key: "organization", label: "组织架构", icon: "account_tree", href: "/organization", anyOf: ["system.users", "report.dashboard.view"], featureFlagKey: FEATURE_FLAG_KEYS.employeeLifecycle },
|
||||
contractsRegistry: { key: "contractsRegistry", label: "合同", icon: "description", href: "/contracts-registry", anyOf: ["project.manage", "project.view_all", "system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.projectLinking },
|
||||
workflowDefinitions: { key: "workflowDefinitions", label: "流程定义", icon: "schema", href: "/system/workflow-definitions", anyOf: ["system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.workflowDefinitionAdmin },
|
||||
systemSettings: { key: "systemSettings", label: "管理", icon: "settings", href: "/system/settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage", "system.audit.view"] },
|
||||
systemSettings: { key: "systemSettings", label: "管理", icon: "settings", href: "/system/settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage", "system.audit.view", "payment.account.read", "payment.account.manage"] },
|
||||
featureFlags: { key: "featureFlags", label: "功能管理", icon: "toggle_on", href: "/system/feature-flags", anyOf: ["system.settings"] },
|
||||
auditLogs: { key: "auditLogs", label: "审计日志", icon: "manage_search", href: "/audit-logs", anyOf: ["system.audit.view"] },
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户