geely-kaipiao/compare_kaipiao.js
2026-07-25 20:53:20 +08:00

166 lines
5.5 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 XLSX = require('xlsx');
const fs = require('fs');
const path = require('path');
/**
* 对比脚本:商旅 vs 吉智 金额核对
*
* 输入:一个包含「商旅」和「吉智」两个 sheet 的 Excel即 merge_kaipiao.js 的合并结果)。
*
* 逻辑:
* 1. 商旅 sheet按「结算账户」聚合「账单总金额」相加。
* 2. 吉智 sheet按「*对账单/项目名称」聚合,「*账单应收总金额」相加。
* (吉智的「*对账单/项目名称」对应商旅的「结算账户」)
* 3. 对比两侧同一 key 的金额,找出不同:
* - 仅商旅有的 key
* - 仅吉智有的 key
* - 两侧都有但金额不一致的 key
*
* 用法:
* node compare_kaipiao.js <合并excel路径> [输出结果路径]
*
* 例如:
* node compare_kaipiao.js "团队开票申请表-合并.xlsx" "对比结果.xlsx"
*/
// ---------- 参数 ----------
const inFile = process.argv[2];
const outFile = process.argv[3] || path.join(process.cwd(), '对比结果.xlsx');
if (!inFile) {
console.error('用法: node compare_kaipiao.js <合并excel路径> [输出结果路径]');
process.exit(1);
}
if (!fs.existsSync(inFile)) {
console.error(`输入文件不存在: ${inFile}`);
process.exit(1);
}
// 金额比较容差(避免浮点误差)
const EPS = 0.005;
// ---------- 工具 ----------
// 按关键字找 sheet 名
function findSheetName(workbook, keyword) {
return workbook.SheetNames.find(n => n.includes(keyword));
}
// 找列下标(精确匹配去空格后的表头)
function findColIndex(header, name) {
return header.findIndex(h => h != null && String(h).trim() === name);
}
// 规范化 key去首尾空格
function normKey(v) {
return v == null ? '' : String(v).trim();
}
// 解析金额为数字(去掉逗号/空格/货币符号)
function toNumber(v) {
if (v == null || v === '') return 0;
if (typeof v === 'number') return v;
const cleaned = String(v).replace(/[,\s\uFFE5\u00A5¥]/g, '');
const n = parseFloat(cleaned);
return isNaN(n) ? 0 : n;
}
// 保留两位小数
function round2(n) {
return Math.round(n * 100) / 100;
}
// 按 keyCol 聚合 amountCol
function aggregate(sheet, keyColName, amountColName, label) {
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null, blankrows: false });
if (rows.length === 0) return new Map();
const header = rows[0];
const keyIdx = findColIndex(header, keyColName);
const amtIdx = findColIndex(header, amountColName);
if (keyIdx === -1) {
console.error(`${label}: 未找到列 "${keyColName}"`);
process.exit(1);
}
if (amtIdx === -1) {
console.error(`${label}: 未找到列 "${amountColName}"`);
process.exit(1);
}
const map = new Map();
for (let i = 1; i < rows.length; i++) {
const key = normKey(rows[i][keyIdx]);
if (key === '') continue;
const amt = toNumber(rows[i][amtIdx]);
const cur = map.get(key) || { sum: 0, count: 0 };
cur.sum += amt;
cur.count += 1;
map.set(key, cur);
}
return map;
}
// ---------- 读取 ----------
const wb = XLSX.readFile(inFile);
const shangluName = findSheetName(wb, '商旅');
const jizhiName = findSheetName(wb, '吉智');
if (!shangluName) { console.error('未找到含"商旅"的 sheet'); process.exit(1); }
if (!jizhiName) { console.error('未找到含"吉智"的 sheet'); process.exit(1); }
const shangluMap = aggregate(wb.Sheets[shangluName], '结算账户', '账单总金额', '商旅');
const jizhiMap = aggregate(wb.Sheets[jizhiName], '*对账单/项目名称', '*账单应收总金额', '吉智');
// ---------- 对比 ----------
const allKeys = new Set([...shangluMap.keys(), ...jizhiMap.keys()]);
const diffRows = [['结算账户/项目名称', '商旅账单总金额', '吉智账单应收总金额', '差额(商旅-吉智)', '差异类型']];
let onlyShanglu = 0, onlyJizhi = 0, mismatch = 0, matched = 0;
for (const key of [...allKeys].sort()) {
const s = shangluMap.get(key);
const j = jizhiMap.get(key);
if (s && !j) {
diffRows.push([key, round2(s.sum), null, round2(s.sum), '仅商旅有']);
onlyShanglu++;
} else if (!s && j) {
diffRows.push([key, null, round2(j.sum), round2(-j.sum), '仅吉智有']);
onlyJizhi++;
} else {
const delta = s.sum - j.sum;
if (Math.abs(delta) > EPS) {
diffRows.push([key, round2(s.sum), round2(j.sum), round2(delta), '金额不一致']);
mismatch++;
} else {
matched++;
}
}
}
// ---------- 输出 ----------
const outWb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(diffRows);
XLSX.utils.book_append_sheet(outWb, ws, '差异');
// 汇总 sheet
const summary = [
['项目', '数量'],
['商旅去重结算账户数', shangluMap.size],
['吉智去重项目名称数', jizhiMap.size],
['仅商旅有', onlyShanglu],
['仅吉智有', onlyJizhi],
['金额不一致', mismatch],
['金额一致', matched],
];
XLSX.utils.book_append_sheet(outWb, XLSX.utils.aoa_to_sheet(summary), '汇总');
XLSX.writeFile(outWb, outFile);
console.log('对比完成:');
console.log(` 商旅去重结算账户数: ${shangluMap.size}`);
console.log(` 吉智去重项目名称数: ${jizhiMap.size}`);
console.log(` 仅商旅有: ${onlyShanglu}`);
console.log(` 仅吉智有: ${onlyJizhi}`);
console.log(` 金额不一致: ${mismatch}`);
console.log(` 金额一致: ${matched}`);
console.log(` 输出文件: ${outFile}`);