Files
yunhaifinance/backend/scripts/generate-excel-template.js
T

293 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const ExcelJS = require('exceljs');
const path = require('path');
const db = require('../db');
async function generateTemplate() {
const workbook = new ExcelJS.Workbook();
workbook.creator = '公司财务管理系统';
workbook.created = new Date();
const projectResult = await db.query('SELECT name FROM projects ORDER BY name');
const projectNames = projectResult.rows.map(r => r.name);
const categoryResult = await db.query('SELECT * FROM expense_categories WHERE is_active = true ORDER BY category_level1, sort_order');
const categories = categoryResult.rows;
const incomeCategories = categories.filter(c => c.category_level1 === 'income').map(c => c.label);
const projectCategories = categories.filter(c => c.category_level1 === 'project').map(c => c.label);
const companyCategories = categories.filter(c => c.category_level1 === 'company').map(c => c.label);
// ========== Sheet2: 项目名称列表 ==========
const sheet2 = workbook.addWorksheet('项目列表', {
properties: { tabColor: { argb: '4472C4' } }
});
sheet2.getColumn(1).width = 40;
sheet2.getCell('A1').value = '项目名称';
sheet2.getCell('A1').font = { bold: true, size: 12, color: { argb: 'FFFFFF' } };
sheet2.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '4472C4' } };
sheet2.getCell('A1').alignment = { horizontal: 'center' };
projectNames.forEach((name, i) => {
const cell = sheet2.getCell(`A${i + 2}`);
cell.value = name;
cell.border = { bottom: { style: 'thin', color: { argb: 'D9D9D9' } } };
});
sheet2.getCell('C1').value = '收支类型';
sheet2.getCell('C1').font = { bold: true };
['收入', '支出'].forEach((v, i) => { sheet2.getCell(`C${i + 2}`).value = v; });
sheet2.getCell('D1').value = '一级分类';
sheet2.getCell('D1').font = { bold: true };
['收入', '项目支出', '公司支出'].forEach((v, i) => { sheet2.getCell(`D${i + 2}`).value = v; });
sheet2.getCell('F1').value = '收入分类';
sheet2.getCell('F1').font = { bold: true };
incomeCategories.forEach((v, i) => { sheet2.getCell(`F${i + 2}`).value = v; });
sheet2.getCell('G1').value = '项目支出分类';
sheet2.getCell('G1').font = { bold: true };
projectCategories.forEach((v, i) => { sheet2.getCell(`G${i + 2}`).value = v; });
sheet2.getCell('H1').value = '公司支出分类';
sheet2.getCell('H1').font = { bold: true };
companyCategories.forEach((v, i) => { sheet2.getCell(`H${i + 2}`).value = v; });
sheet2.getCell('J1').value = '币种';
sheet2.getCell('J1').font = { bold: true };
['CNY', 'LAK', 'USD', 'THB'].forEach((v, i) => { sheet2.getCell(`J${i + 2}`).value = v; });
sheet2.getCell('K1').value = '对方类型';
sheet2.getCell('K1').font = { bold: true };
['供应商', '分包商', '客户', '员工', '物流公司', '股东', '其他'].forEach((v, i) => { sheet2.getCell(`K${i + 2}`).value = v; });
const projectListEnd = Math.max(projectNames.length + 1, 2);
const categoryListEnd = Math.max(incomeCategories.length, projectCategories.length, companyCategories.length) + 1;
// ========== Sheet1: 记账主表 ==========
const sheet1 = workbook.addWorksheet('财务记账', {
properties: { tabColor: { argb: '52C41A' } }
});
const headers = [
{ key: 'date', header: '日期', width: 13 },
{ key: 'txn_type', header: '收支类型', width: 11 },
{ key: 'level1', header: '一级分类', width: 11 },
{ key: 'level2', header: '二级分类', width: 14 },
{ key: 'project', header: '项目名称', width: 35 },
{ key: 'amount', header: '金额', width: 14 },
{ key: 'currency', header: '币种', width: 8 },
{ key: 'rate', header: '汇率', width: 10 },
{ key: 'amount_cny', header: '等效人民币', width: 14 },
{ key: 'counterparty', header: '对方名称', width: 18 },
{ key: 'counterparty_type', header: '对方类型', width: 10 },
{ key: 'user', header: '人员姓名', width: 10 },
{ key: 'desc', header: '描述', width: 30 },
{ key: 'voucher', header: '凭证编号', width: 14 },
];
sheet1.columns = headers;
const headerRow = sheet1.getRow(1);
headerRow.height = 28;
headerRow.eachCell((cell) => {
cell.font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '4472C4' } };
cell.alignment = { horizontal: 'center', vertical: 'middle' };
cell.border = {
top: { style: 'thin' }, bottom: { style: 'thin' },
left: { style: 'thin' }, right: { style: 'thin' }
};
});
sheet1.views = [{ state: 'frozen', ySplit: 1 }];
const dataRows = 500;
for (let i = 2; i <= dataRows + 1; i++) {
const row = sheet1.getRow(i);
// I列: 等效人民币 = 金额 × 汇率(自动计算)
const cnyCell = row.getCell(9);
cnyCell.value = { formula: `F${i}*H${i}` };
cnyCell.numFmt = '#,##0.00';
cnyCell.protection = { locked: true };
// 金额列格式
row.getCell(6).numFmt = '#,##0.00';
// 汇率列格式
row.getCell(8).numFmt = '0.000000';
// 日期列格式
row.getCell(1).numFmt = 'YYYY-MM-DD';
// 交替行背景色
if (i % 2 === 0) {
row.eachCell({ includeEmpty: true }, (cell) => {
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'F2F7FB' } };
});
}
// 边框
row.eachCell({ includeEmpty: true }, (cell) => {
cell.border = {
bottom: { style: 'thin', color: { argb: 'D9D9D9' } },
vertical: { style: 'thin', color: { argb: 'D9D9D9' } }
};
});
}
// 数据验证 - 收支类型
sheet1.getDataValidator = function() {};
for (let i = 2; i <= dataRows + 1; i++) {
// B列: 收支类型
sheet1.getCell(`B${i}`).dataValidation = {
type: 'list',
allowBlank: true,
formulae: ['"收入,支出"'],
showErrorMessage: true,
errorTitle: '输入错误',
error: '请从下拉列表中选择:收入 或 支出'
};
// C列: 一级分类
sheet1.getCell(`C${i}`).dataValidation = {
type: 'list',
allowBlank: true,
formulae: ['"收入,项目支出,公司支出"'],
showErrorMessage: true,
errorTitle: '输入错误',
error: '请从下拉列表中选择'
};
// D列: 二级分类(收入+项目+公司 全部列出,用户根据一级分类选择对应的)
const allCategories = [...incomeCategories, ...projectCategories, ...companyCategories];
const catStr = allCategories.join(',');
sheet1.getCell(`D${i}`).dataValidation = {
type: 'list',
allowBlank: true,
formulae: [`"${catStr}"`],
showErrorMessage: true,
errorTitle: '输入错误',
error: '请从下拉列表中选择二级分类'
};
// E列: 项目名称(从Sheet2引用)
sheet1.getCell(`E${i}`).dataValidation = {
type: 'list',
allowBlank: true,
formulae: [`项目列表!$A$2:$A$${projectListEnd}`],
showErrorMessage: true,
errorTitle: '输入错误',
error: '请从下拉列表中选择项目名称'
};
// G列: 币种
sheet1.getCell(`G${i}`).dataValidation = {
type: 'list',
allowBlank: true,
formulae: ['"CNY,LAK,USD,THB"'],
showErrorMessage: true,
errorTitle: '输入错误',
error: '请选择币种:CNY / LAK / USD / THB'
};
// K列: 对方类型
sheet1.getCell(`K${i}`).dataValidation = {
type: 'list',
allowBlank: true,
formulae: ['"供应商,分包商,客户,员工,物流公司,股东,其他"'],
showErrorMessage: true,
errorTitle: '输入错误',
error: '请从下拉列表中选择对方类型'
};
// H列: 汇率 - CNY时自动填1
// A列: 日期验证
sheet1.getCell(`A${i}`).dataValidation = {
type: 'date',
allowBlank: true,
operator: 'greaterThan',
formulae: [new Date(2020, 0, 1)],
showErrorMessage: true,
errorTitle: '日期格式错误',
error: '请输入有效日期,格式:YYYY-MM-DD'
};
}
// 示例数据(第2行)
const exampleRow = sheet1.getRow(2);
exampleRow.getCell(1).value = new Date(2026, 2, 15);
exampleRow.getCell(2).value = '支出';
exampleRow.getCell(3).value = '项目支出';
exampleRow.getCell(4).value = '材料采购';
exampleRow.getCell(5).value = projectNames[0] || '';
exampleRow.getCell(6).value = 150000;
exampleRow.getCell(7).value = 'CNY';
exampleRow.getCell(8).value = 1;
exampleRow.getCell(10).value = '云南XX电力器材有限公司';
exampleRow.getCell(11).value = '供应商';
exampleRow.getCell(13).value = '采购190×12m电杆50根';
// 示例数据(第3行)
const exampleRow2 = sheet1.getRow(3);
exampleRow2.getCell(1).value = new Date(2026, 2, 20);
exampleRow2.getCell(2).value = '支出';
exampleRow2.getCell(3).value = '项目支出';
exampleRow2.getCell(4).value = '食宿费用';
exampleRow2.getCell(5).value = projectNames[0] || '';
exampleRow2.getCell(6).value = 100000;
exampleRow2.getCell(7).value = 'LAK';
exampleRow2.getCell(8).value = 0.0003;
exampleRow2.getCell(10).value = 'XX招待所';
exampleRow2.getCell(11).value = '其他';
exampleRow2.getCell(12).value = '李四';
exampleRow2.getCell(13).value = '3月驻现场住宿费';
// 示例数据(第4行)
const exampleRow3 = sheet1.getRow(4);
exampleRow3.getCell(1).value = new Date(2026, 1, 20);
exampleRow3.getCell(2).value = '收入';
exampleRow3.getCell(3).value = '收入';
exampleRow3.getCell(4).value = '项目合同收款';
exampleRow3.getCell(5).value = projectNames[0] || '';
exampleRow3.getCell(6).value = 300000;
exampleRow3.getCell(7).value = 'CNY';
exampleRow3.getCell(8).value = 1;
exampleRow3.getCell(10).value = '老挝国家电力公司';
exampleRow3.getCell(11).value = '客户';
exampleRow3.getCell(13).value = '合同首付款30%';
// 保护工作表(锁定I列公式)
sheet1.protect('', {
selectLockedCells: true,
selectUnlockedCells: true,
formatCells: true,
formatColumns: true,
formatRows: true,
insertRows: true,
insertColumns: false,
deleteRows: false,
deleteColumns: false,
sort: true,
autoFilter: true,
});
// 设置可编辑列的单元格为未锁定
for (let i = 2; i <= dataRows + 1; i++) {
[1,2,3,4,5,6,7,8,10,11,12,13,14].forEach(col => {
sheet1.getRow(i).getCell(col).protection = { locked: false };
});
}
const outputPath = path.join(__dirname, '..', 'public', 'templates', '财务记账导入模板.xlsx');
const fs = require('fs');
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
await workbook.xlsx.writeFile(outputPath);
console.log('✅ Excel模板已生成:', outputPath);
return outputPath;
}
generateTemplate().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });