整理
This commit is contained in:
parent
477914858b
commit
e51c262673
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
node_modules/*
|
||||
~$*
|
||||
.reasonix/*
|
||||
255
jizhi_fill.js
Normal file
255
jizhi_fill.js
Normal file
@ -0,0 +1,255 @@
|
||||
const XLSX = require('xlsx');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 开票清单 -> 吉智对账单 填列脚本
|
||||
*
|
||||
* 输入 excel1: sheet="发票清单"
|
||||
* col "资源类型" = 大类(火车票/国际机票/国内机票/国际酒店/酒店/附加项)
|
||||
* col "开票明细" = 小类(包含字符: 机票/机票服务费/住宿费/住宿服务费/火车票/火车票服务费)
|
||||
* col "开票金额" = 金额
|
||||
* col "结算账户" = 归属 TEAM 编号(TEAM-XXXX 或 XXXXTEAM-XXXX, 结尾 : 去掉)
|
||||
*
|
||||
* 输出 excel2: sheet 名含"吉智"(只有一个)
|
||||
* 有一行数据的 col "*对账单/项目名称" = TEAM 编号
|
||||
* 填写以下列(每个项名对应一个 col):
|
||||
* 吉智服务费 = 酒店服务费 + 机票服务费 + 火车票服务费
|
||||
* 酒店房费 = 国际酒店房费 + 酒店房费
|
||||
* 酒店服务费 = 国际酒店服务费 + 酒店服务费
|
||||
* 机票服务费 = 国际机票服务费 + 国内机票服务费
|
||||
* 火车票服务费 = 火车票服务费
|
||||
*
|
||||
* 中断条件: excel2 中找不到对应 TEAM -> 提示并中断
|
||||
* 找到多个对应 TEAM -> 提示并中断
|
||||
*
|
||||
* 用法: node jizhi_fill.js [excel1路径] [excel2路径]
|
||||
* 不带参数则使用下方配置常量
|
||||
*/
|
||||
|
||||
// ======================== 配置 ========================
|
||||
const EXCEL1 = path.join(__dirname, '开票清单.xlsx'); // excel1 路径可修改
|
||||
const EXCEL2 = path.join(__dirname, '吉智对账单.xlsx'); // excel2 路径可修改
|
||||
|
||||
const SHEET1_NAME = '发票清单';
|
||||
|
||||
// 需要填入 excel2 的字段: [列名(兼容*与空格), 取值函数]
|
||||
const FIELDS = [
|
||||
{ name: '吉智服务费', get: s => s.totalService },
|
||||
{ name: '酒店房费', get: s => s.hotelRoom },
|
||||
{ name: '酒店服务费', get: s => s.hotelService },
|
||||
{ name: '机票服务费', get: s => s.flightService },
|
||||
{ name: '火车票服务费', get: s => s.trainService },
|
||||
];
|
||||
|
||||
// ======================== 工具 ========================
|
||||
function round2(n) {
|
||||
return Math.round((Number(n) + Number.EPSILON) * 100) / 100;
|
||||
}
|
||||
|
||||
// 去掉全角/半角冒号(仅结尾), 以及空格
|
||||
function normalizeTeam(v) {
|
||||
let s = String(v == null ? '' : v).trim();
|
||||
s = s.replace(/[::]\s*$/, '').trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
// 在 sheet 名中查找包含关键字的 sheet(恰好一个)
|
||||
function findSheet(workbook, keyword, label) {
|
||||
const names = workbook.SheetNames.filter(n => n.includes(keyword));
|
||||
if (names.length === 0) {
|
||||
console.error(`[中断] ${label}: 未找到名称包含"${keyword}"的 sheet`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (names.length > 1) {
|
||||
console.error(`[中断] ${label}: 找到多个名称包含"${keyword}"的 sheet: ${names.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return names[0];
|
||||
}
|
||||
|
||||
// 精确(去星号/空格)匹配列名 -> 列下标; 找不到返回 -1
|
||||
function findColIndex(rows, targetName) {
|
||||
const want = targetName.replace(/[*\s]/g, '');
|
||||
const header = rows[0];
|
||||
if (!header) return -1;
|
||||
for (let c = 0; c < header.length; c++) {
|
||||
const h = header[c];
|
||||
if (h == null) continue;
|
||||
if (String(h).replace(/[*\s]/g, '') === want) return c;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 读取一个 sheet 成二维数组(含表头)
|
||||
function sheetToRows(sheet) {
|
||||
return XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null, blankrows: false });
|
||||
}
|
||||
|
||||
// ======================== 1. 读取 excel1 ========================
|
||||
function readInvoice(excel1Path) {
|
||||
const wb = XLSX.readFile(excel1Path);
|
||||
const sheet = wb.Sheets[SHEET1_NAME];
|
||||
if (!sheet) {
|
||||
console.error(`[中断] excel1: 未找到 sheet "${SHEET1_NAME}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
const rows = sheetToRows(sheet);
|
||||
const header = rows[0];
|
||||
|
||||
const col = name => findColIndex(rows, name);
|
||||
const idxResource = col('资源类型');
|
||||
const idxDetail = col('开票明细');
|
||||
const idxAmount = col('开票金额');
|
||||
const idxAccount = col('结算账户');
|
||||
|
||||
if ([idxResource, idxDetail, idxAmount, idxAccount].some(i => i === -1)) {
|
||||
console.error(`[中断] excel1: 缺少必要列(资源类型/开票明细/开票金额/结算账户), 现有表头: ${JSON.stringify(header)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 每个 TEAM 的累加结果
|
||||
const sums = new Map(); // key: teamId -> {totalService, hotelRoom, hotelService, flightService, trainService}
|
||||
const newSum = () => ({ totalService: 0, hotelRoom: 0, hotelService: 0, flightService: 0, trainService: 0 });
|
||||
|
||||
for (let r = 1; r < rows.length; r++) {
|
||||
const row = rows[r];
|
||||
const resource = row[idxResource] == null ? '' : String(row[idxResource]).trim();
|
||||
const detail = row[idxDetail] == null ? '' : String(row[idxDetail]).trim();
|
||||
const amount = Number(row[idxAmount]);
|
||||
const accountRaw = row[idxAccount];
|
||||
|
||||
if (resource === '' && detail === '') continue; // 空行
|
||||
|
||||
// 归属 TEAM
|
||||
const match = normalizeTeam(accountRaw).match(/TEAM-\d+/);
|
||||
if (!match) {
|
||||
console.warn(` [警告] 第${r + 1}行: 无法从结算账户提取 TEAM (值: "${accountRaw}"),跳过`);
|
||||
continue;
|
||||
}
|
||||
const teamId = match[0];
|
||||
const s = sums.get(teamId) || newSum();
|
||||
sums.set(teamId, s);
|
||||
|
||||
if (Number.isNaN(amount)) {
|
||||
console.warn(` [警告] 第${r + 1}行: 开票金额无法解析 (值: "${row[idxAmount]}"),跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 按"开票明细"小类包含字符归类(注意先匹配更具体的"服务费")
|
||||
if (detail.includes('住宿服务费')) {
|
||||
s.hotelService += amount;
|
||||
} else if (detail.includes('住宿费')) {
|
||||
s.hotelRoom += amount;
|
||||
} else if (detail.includes('机票服务费')) {
|
||||
s.flightService += amount;
|
||||
} else if (detail.includes('火车票服务费')) {
|
||||
s.trainService += amount;
|
||||
} else {
|
||||
console.warn(` [警告] 第${r + 1}行: 开票明细"${detail}"未识别到服务费/住宿费类别,跳过 (${teamId}, ${amount})`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算吉智服务费 = 各服务费之和
|
||||
for (const s of sums.values()) {
|
||||
s.totalService = round2(s.hotelService + s.flightService + s.trainService);
|
||||
s.hotelRoom = round2(s.hotelRoom);
|
||||
s.hotelService = round2(s.hotelService);
|
||||
s.flightService = round2(s.flightService);
|
||||
s.trainService = round2(s.trainService);
|
||||
}
|
||||
|
||||
if (sums.size === 0) {
|
||||
console.error('[中断] excel1: 未提取到任何 TEAM 数据');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[excel1] 解析完成, 共 ${sums.size} 个 TEAM:`);
|
||||
for (const [tid, s] of sums) {
|
||||
console.log(` ${tid}: 吉智服务费=${s.totalService}, 酒店房费=${s.hotelRoom}, 酒店服务费=${s.hotelService}, 机票服务费=${s.flightService}, 火车票服务费=${s.trainService}`);
|
||||
}
|
||||
return sums;
|
||||
}
|
||||
|
||||
// ======================== 2. 写入 excel2 ========================
|
||||
function writeJizhi(excel2Path, sums) {
|
||||
const wb = XLSX.readFile(excel2Path);
|
||||
const sheetName = findSheet(wb, '吉智', 'excel2');
|
||||
const sheet = wb.Sheets[sheetName];
|
||||
const rows = sheetToRows(sheet);
|
||||
const header = rows[0];
|
||||
|
||||
const nameIdx = findColIndex(rows, '对账单/项目名称');
|
||||
if (nameIdx === -1) {
|
||||
console.error(`[中断] excel2: 未找到列"对账单/项目名称", 现有表头: ${JSON.stringify(header)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 收集每个 TEAM 匹配到的行
|
||||
const matched = new Map(); // teamId -> [rowIdx...]
|
||||
for (let r = 1; r < rows.length; r++) {
|
||||
const val = rows[r][nameIdx];
|
||||
if (val == null || String(val).trim() === '') continue;
|
||||
const teamId = normalizeTeam(val).match(/TEAM-\d+/);
|
||||
if (!teamId) continue;
|
||||
const tid = teamId[0];
|
||||
if (!matched.has(tid)) matched.set(tid, []);
|
||||
matched.get(tid).push(r);
|
||||
}
|
||||
|
||||
// 校验: 每个需要填写的 TEAM 在 excel2 中必须恰好匹配一行
|
||||
for (const [tid] of sums) {
|
||||
const rowsFound = matched.get(tid) || [];
|
||||
if (rowsFound.length === 0) {
|
||||
console.error(`[中断] excel2: 找不到 TEAM "${tid}" 对应的行, 请检查"对账单/项目名称"列`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (rowsFound.length > 1) {
|
||||
console.error(`[中断] excel2: TEAM "${tid}" 在 excel2 中找到 ${rowsFound.length} 行(第 ${rowsFound.map(x => x + 1).join(', ')} 行), 存在重复, 无法确定唯一目标`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 写入各字段
|
||||
for (const field of FIELDS) {
|
||||
const colIdx = findColIndex(rows, field.name);
|
||||
if (colIdx === -1) {
|
||||
console.error(`[中断] excel2: 未找到列"${field.name}", 现有表头: ${JSON.stringify(header)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
for (const [tid, s] of sums) {
|
||||
const rowIdx = matched.get(tid)[0];
|
||||
XLSX.utils.sheet_add_aoa(sheet, [[field.get(s)]], { origin: { r: rowIdx, c: colIdx } });
|
||||
}
|
||||
}
|
||||
|
||||
XLSX.writeFile(wb, excel2Path);
|
||||
console.log(`\n[excel2] 已写入: ${excel2Path} (sheet: ${sheetName})`);
|
||||
for (const [tid] of sums) {
|
||||
console.log(` 填写完成: ${tid} (第 ${matched.get(tid)[0] + 1} 行)`);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 主流程 ========================
|
||||
function main() {
|
||||
const excel1Path = process.argv[2] || EXCEL1;
|
||||
const excel2Path = process.argv[3] || EXCEL2;
|
||||
|
||||
if (!require('fs').existsSync(excel1Path)) {
|
||||
console.error(`[中断] excel1 文件不存在: ${excel1Path}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!require('fs').existsSync(excel2Path)) {
|
||||
console.error(`[中断] excel2 文件不存在: ${excel2Path}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`excel1: ${excel1Path}`);
|
||||
console.log(`excel2: ${excel2Path}\n`);
|
||||
|
||||
const sums = readInvoice(excel1Path);
|
||||
writeJizhi(excel2Path, sums);
|
||||
console.log('\n========== 完成 ==========');
|
||||
}
|
||||
|
||||
main();
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"server": {
|
||||
"url": "ws://localhost:18888/websocket_path",
|
||||
"password": "helloworld"
|
||||
},
|
||||
"client_name": "cheney"
|
||||
{
|
||||
"server": {
|
||||
"url": "ws://bh.vps.honor3.com:18888/websocket_path",
|
||||
"password": "helloworld"
|
||||
},
|
||||
"client_name": "cheney"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user