合并
This commit is contained in:
parent
c1e378a631
commit
11451aac40
283
compare.js
Normal file
283
compare.js
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
const XLSX = require('xlsx');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能:读取Excel文件并解析为JSON
|
||||||
|
* 参数:filePath - 文件路径
|
||||||
|
* 返回值:解析后的工作簿对象
|
||||||
|
*/
|
||||||
|
function readExcel(filePath) {
|
||||||
|
return XLSX.readFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能:判断是否为有效订单号(FR开头的订单号)
|
||||||
|
* 参数:orderNo - 订单号
|
||||||
|
* 返回值:是否有效
|
||||||
|
*/
|
||||||
|
function isValidOrderNo(orderNo) {
|
||||||
|
if (!orderNo) return false;
|
||||||
|
const str = String(orderNo).trim();
|
||||||
|
if (!str) return false;
|
||||||
|
if (str === '总计' || str === '合计' || str === '汇总') return false;
|
||||||
|
if (str.startsWith('FR')) return true;
|
||||||
|
return /^\d+$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能:从酒店账单提取订单号和金额
|
||||||
|
* 参数:workbook - Excel工作簿对象
|
||||||
|
* 返回值:订单Map {订单号: {金额, 入住人, 入住时间, 离店时间}}
|
||||||
|
*/
|
||||||
|
function extractHotelOrders(workbook) {
|
||||||
|
const sheet = workbook.Sheets['.'];
|
||||||
|
const data = XLSX.utils.sheet_to_json(sheet);
|
||||||
|
const orders = new Map();
|
||||||
|
|
||||||
|
data.forEach(row => {
|
||||||
|
const orderNo = row['吉利订单号'];
|
||||||
|
if (isValidOrderNo(orderNo)) {
|
||||||
|
orders.set(String(orderNo).trim(), {
|
||||||
|
结算金额: parseFloat(row['结算金额']) || 0,
|
||||||
|
系统金额: parseFloat(row['系统金额']) || 0,
|
||||||
|
差异: parseFloat(row['差异']) || 0,
|
||||||
|
入住人: row['入住人姓名'] || '',
|
||||||
|
入住时间: row['入住时间'] || '',
|
||||||
|
离店时间: row['离店时间'] || '',
|
||||||
|
房型: row['房型'] || '',
|
||||||
|
房号: row['房号'] || '',
|
||||||
|
房间数: row['房间数'] || 0,
|
||||||
|
间夜数: row['间夜数'] || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能:从商旅账单Export表提取订单号和金额(合并同一订单的正负金额)
|
||||||
|
* 参数:workbook - Excel工作簿对象
|
||||||
|
* 返回值:订单Map {订单号: 金额对象}
|
||||||
|
*/
|
||||||
|
function extractGeelyExportOrders(workbook) {
|
||||||
|
const sheet = workbook.Sheets['Export'];
|
||||||
|
const data = XLSX.utils.sheet_to_json(sheet, { header: 1 }); // 使用数组索引访问
|
||||||
|
const orders = new Map();
|
||||||
|
|
||||||
|
// 从第2行开始(第0行是标题,第1行是表头)
|
||||||
|
for (let i = 2; i < data.length; i++) {
|
||||||
|
const row = data[i];
|
||||||
|
const orderNo = row[5]; // 供应商订单号在第5列
|
||||||
|
|
||||||
|
if (isValidOrderNo(orderNo)) {
|
||||||
|
// 金额在第11列,纯数字,已包含正负值
|
||||||
|
const amount = parseFloat(row[11]) || 0;
|
||||||
|
const key = String(orderNo).trim();
|
||||||
|
|
||||||
|
// 合并同一订单号的金额
|
||||||
|
if (orders.has(key)) {
|
||||||
|
const existing = orders.get(key);
|
||||||
|
existing.金额 += amount;
|
||||||
|
// 保留最后一条记录的其他信息
|
||||||
|
existing.对账明细名称 = row[6] || existing.对账明细名称;
|
||||||
|
existing.结算状态 = row[9] || existing.结算状态;
|
||||||
|
existing.对账单号 = row[0] || existing.对账单号;
|
||||||
|
} else {
|
||||||
|
orders.set(key, {
|
||||||
|
金额: amount,
|
||||||
|
对账明细名称: row[6] || '',
|
||||||
|
结算状态: row[9] || '',
|
||||||
|
对账单号: row[0] || '',
|
||||||
|
订单号: row[4] || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能:对比两个订单集合的差异
|
||||||
|
* 参数:hotelOrders - 酒店方订单Map, geelyOrders - 商旅方订单Map
|
||||||
|
* 返回值:差异结果对象
|
||||||
|
*/
|
||||||
|
function compareOrders(hotelOrders, geelyOrders) {
|
||||||
|
const hotelOnly = [];
|
||||||
|
const geelyOnly = [];
|
||||||
|
const amountDiff = [];
|
||||||
|
const matched = [];
|
||||||
|
|
||||||
|
// 检查酒店有但商旅没有的订单
|
||||||
|
for (const [orderNo, hotelInfo] of hotelOrders) {
|
||||||
|
if (!geelyOrders.has(orderNo)) {
|
||||||
|
hotelOnly.push({ 订单号: orderNo, ...hotelInfo });
|
||||||
|
} else {
|
||||||
|
const geelyInfo = geelyOrders.get(orderNo);
|
||||||
|
const hotelAmount = hotelInfo.结算金额;
|
||||||
|
const geelyAmount = geelyInfo.金额;
|
||||||
|
|
||||||
|
if (Math.abs(hotelAmount - geelyAmount) > 0.01) {
|
||||||
|
amountDiff.push({
|
||||||
|
订单号: orderNo,
|
||||||
|
酒店金额: hotelAmount,
|
||||||
|
商旅金额: geelyAmount,
|
||||||
|
差额: hotelAmount - geelyAmount,
|
||||||
|
入住人: hotelInfo.入住人,
|
||||||
|
酒店明细: hotelInfo,
|
||||||
|
商旅明细: geelyInfo
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
matched.push({
|
||||||
|
订单号: orderNo,
|
||||||
|
金额: hotelAmount,
|
||||||
|
入住人: hotelInfo.入住人
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商旅有但酒店没有的订单
|
||||||
|
for (const [orderNo, geelyInfo] of geelyOrders) {
|
||||||
|
if (!hotelOrders.has(orderNo)) {
|
||||||
|
geelyOnly.push({ 订单号: orderNo, ...geelyInfo });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hotelOnly, geelyOnly, amountDiff, matched };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能:生成差异报告并输出
|
||||||
|
* 参数:result - 对比结果对象
|
||||||
|
*/
|
||||||
|
function generateReport(result, hotelOrdersCount, geelyOrdersCount) {
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
console.log(' 吉利商旅 vs 酒店账单 差异报告');
|
||||||
|
console.log(' (仅使用Export表,已合并同一订单的正负金额)');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
console.log(`\n【统计概览】`);
|
||||||
|
console.log(` 酒店账单有效订单数: ${hotelOrdersCount}`);
|
||||||
|
console.log(` 商旅账单有效订单数: ${geelyOrdersCount}`);
|
||||||
|
console.log(` ✓ 匹配成功订单数: ${result.matched.length}`);
|
||||||
|
console.log(` ⚠ 金额差异订单数: ${result.amountDiff.length}`);
|
||||||
|
console.log(` ✗ 酒店有-商旅无: ${result.hotelOnly.length}`);
|
||||||
|
console.log(` ? 商旅有-酒店无: ${result.geelyOnly.length}`);
|
||||||
|
|
||||||
|
if (result.hotelOnly.length > 0) {
|
||||||
|
console.log(`\n【酒店有但商旅没有的订单 - 共${result.hotelOnly.length}条】`);
|
||||||
|
console.log('-'.repeat(70));
|
||||||
|
result.hotelOnly.forEach((item, i) => {
|
||||||
|
console.log(`${i+1}. ${item.订单号} | ${item.入住人.padEnd(6)} | ¥${String(item.结算金额).padStart(6)} | ${item.房型}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.geelyOnly.length > 0) {
|
||||||
|
console.log(`\n【商旅有但酒店没有的订单 - 共${result.geelyOnly.length}条】`);
|
||||||
|
console.log('-'.repeat(70));
|
||||||
|
result.geelyOnly.forEach((item, i) => {
|
||||||
|
console.log(`${i+1}. ${item.订单号} | ¥${String(item.金额).padStart(6)} | ${item.对账明细名称 || ''}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.amountDiff.length > 0) {
|
||||||
|
console.log(`\n【金额存在差异的订单 - 共${result.amountDiff.length}条】`);
|
||||||
|
console.log('-'.repeat(70));
|
||||||
|
result.amountDiff.forEach((item, i) => {
|
||||||
|
console.log(`${i+1}. ${item.订单号} | ${item.入住人}`);
|
||||||
|
console.log(` 酒店: ¥${item.酒店金额} | 商旅: ¥${item.商旅金额} | 差额: ¥${item.差额}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
|
||||||
|
// 计算总金额
|
||||||
|
let hotelTotal = 0;
|
||||||
|
let geelyTotal = 0;
|
||||||
|
|
||||||
|
for (const item of result.matched) {
|
||||||
|
hotelTotal += item.金额;
|
||||||
|
geelyTotal += item.金额;
|
||||||
|
}
|
||||||
|
for (const item of result.amountDiff) {
|
||||||
|
hotelTotal += item.酒店金额;
|
||||||
|
geelyTotal += item.商旅金额;
|
||||||
|
}
|
||||||
|
for (const item of result.hotelOnly) {
|
||||||
|
hotelTotal += item.结算金额;
|
||||||
|
}
|
||||||
|
for (const item of result.geelyOnly) {
|
||||||
|
geelyTotal += item.金额;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n【金额汇总】`);
|
||||||
|
console.log(` 酒店账单总金额: ¥${hotelTotal}`);
|
||||||
|
console.log(` 商旅账单总金额: ¥${geelyTotal}`);
|
||||||
|
console.log(` 总差额: ¥${hotelTotal - geelyTotal}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
return {
|
||||||
|
统计: {
|
||||||
|
酒店订单总数: hotelOrdersCount,
|
||||||
|
商旅订单总数: geelyOrdersCount,
|
||||||
|
匹配成功: result.matched.length,
|
||||||
|
金额差异: result.amountDiff.length,
|
||||||
|
酒店有商旅无: result.hotelOnly.length,
|
||||||
|
商旅有酒店无: result.geelyOnly.length,
|
||||||
|
酒店总金额: hotelTotal,
|
||||||
|
商旅总金额: geelyTotal,
|
||||||
|
总差额: hotelTotal - geelyTotal
|
||||||
|
},
|
||||||
|
酒店有商旅无: result.hotelOnly,
|
||||||
|
商旅有酒店无: result.geelyOnly,
|
||||||
|
金额差异: result.amountDiff,
|
||||||
|
匹配成功: result.matched
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能:将结果保存为JSON文件
|
||||||
|
* 参数:report - 报告对象, filename - 文件名
|
||||||
|
*/
|
||||||
|
function saveReportToJson(report, filename) {
|
||||||
|
const jsonReport = JSON.parse(JSON.stringify(report, (key, value) => {
|
||||||
|
if (typeof value === 'bigint') return value.toString();
|
||||||
|
return value;
|
||||||
|
}));
|
||||||
|
|
||||||
|
fs.writeFileSync(filename, JSON.stringify(jsonReport, null, 2), 'utf8');
|
||||||
|
console.log(`详细结果已保存到: ${filename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
function main() {
|
||||||
|
const geelyFile = '杭州碧丽酒店管理有限公司 2026年06月 对账单(0601-0630).xlsx';
|
||||||
|
const hotelFile = '杭州滨江希尔顿欢朋酒店-吉利商旅6月账单.xlsx';
|
||||||
|
|
||||||
|
console.log('正在读取Excel文件...');
|
||||||
|
|
||||||
|
const geelyWorkbook = readExcel(geelyFile);
|
||||||
|
const hotelWorkbook = readExcel(hotelFile);
|
||||||
|
|
||||||
|
console.log('正在提取订单数据...');
|
||||||
|
|
||||||
|
const hotelOrders = extractHotelOrders(hotelWorkbook);
|
||||||
|
const geelyOrders = extractGeelyExportOrders(geelyWorkbook);
|
||||||
|
|
||||||
|
console.log(`酒店有效订单数: ${hotelOrders.size}`);
|
||||||
|
console.log(`商旅Export表有效订单数(合并后): ${geelyOrders.size}`);
|
||||||
|
|
||||||
|
console.log('\n正在对比订单...');
|
||||||
|
|
||||||
|
const result = compareOrders(hotelOrders, geelyOrders);
|
||||||
|
|
||||||
|
const report = generateReport(result, hotelOrders.size, geelyOrders.size);
|
||||||
|
|
||||||
|
saveReportToJson(report, '账单差异报告.json');
|
||||||
|
|
||||||
|
console.log('\n对比完成!');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
159
merge_kaipiao.js
Normal file
159
merge_kaipiao.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
const XLSX = require('xlsx');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并「团队开票申请表」脚本
|
||||||
|
*
|
||||||
|
* 目录结构假设:
|
||||||
|
* <根目录>/
|
||||||
|
* 7.xxx/
|
||||||
|
* *团队开票申请表*.xlsx (含4个sheet,其中一个含"商旅",一个含"吉智")
|
||||||
|
* 其它两个表格...
|
||||||
|
* 7.yyy/
|
||||||
|
* ...
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* node merge_kaipiao.js <根目录> [输出文件路径]
|
||||||
|
*
|
||||||
|
* 例如:
|
||||||
|
* node merge_kaipiao.js "D:\\某个目录" "D:\\合并结果.xlsx"
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------- 参数 ----------
|
||||||
|
const rootDir = process.argv[2];
|
||||||
|
const outFile = process.argv[3] || path.join(process.cwd(), '团队开票申请表-合并.xlsx');
|
||||||
|
|
||||||
|
if (!rootDir) {
|
||||||
|
console.error('用法: node merge_kaipiao.js <根目录> [输出文件路径]');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
|
||||||
|
console.error(`根目录不存在或不是文件夹: ${rootDir}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 工具函数 ----------
|
||||||
|
|
||||||
|
// 判断是否为 Excel 文件(排除临时文件 ~$ 开头)
|
||||||
|
function isExcelFile(name) {
|
||||||
|
return /\.(xlsx|xls)$/i.test(name) && !name.startsWith('~$');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在一个工作簿里按关键字查找 sheet 名
|
||||||
|
function findSheetName(workbook, keyword) {
|
||||||
|
return workbook.SheetNames.find(n => n.includes(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把一个 sheet 读成二维数组(含表头行)
|
||||||
|
function sheetToRows(sheet) {
|
||||||
|
return XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null, blankrows: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 收集子文件夹 ----------
|
||||||
|
const subDirs = fs.readdirSync(rootDir, { withFileTypes: true })
|
||||||
|
.filter(d => d.isDirectory() && d.name.startsWith('7.'))
|
||||||
|
.map(d => d.name)
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
if (subDirs.length === 0) {
|
||||||
|
console.error('未找到以 "7." 开头的子文件夹');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`找到 ${subDirs.length} 个 "7." 子文件夹`);
|
||||||
|
|
||||||
|
// 合并结果:分别累积 商旅 / 吉智 两组数据
|
||||||
|
let shangluHeader = null;
|
||||||
|
const shangluRows = [];
|
||||||
|
let jizhiHeader = null;
|
||||||
|
const jizhiRows = [];
|
||||||
|
|
||||||
|
let processedCount = 0;
|
||||||
|
|
||||||
|
for (const sub of subDirs) {
|
||||||
|
const subPath = path.join(rootDir, sub);
|
||||||
|
|
||||||
|
// 找到名字含「团队开票申请表」的表格
|
||||||
|
const files = fs.readdirSync(subPath).filter(isExcelFile);
|
||||||
|
const targetFile = files.find(f => f.includes('团队开票申请表'));
|
||||||
|
|
||||||
|
if (!targetFile) {
|
||||||
|
console.warn(` [跳过] ${sub}: 未找到含"团队开票申请表"的表格`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(subPath, targetFile);
|
||||||
|
let workbook;
|
||||||
|
try {
|
||||||
|
workbook = XLSX.readFile(filePath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(` [跳过] ${sub}: 读取失败 - ${e.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shangluName = findSheetName(workbook, '商旅');
|
||||||
|
const jizhiName = findSheetName(workbook, '吉智');
|
||||||
|
|
||||||
|
if (!shangluName) console.warn(` [警告] ${sub}: 未找到含"商旅"的sheet`);
|
||||||
|
if (!jizhiName) console.warn(` [警告] ${sub}: 未找到含"吉智"的sheet`);
|
||||||
|
|
||||||
|
// 处理 商旅
|
||||||
|
if (shangluName) {
|
||||||
|
const rows = sheetToRows(workbook.Sheets[shangluName]);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
const [header, ...body] = rows;
|
||||||
|
if (!shangluHeader) shangluHeader = header;
|
||||||
|
shangluRows.push(...body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 吉智
|
||||||
|
if (jizhiName) {
|
||||||
|
const rows = sheetToRows(workbook.Sheets[jizhiName]);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
const [header, ...body] = rows;
|
||||||
|
if (!jizhiHeader) jizhiHeader = header;
|
||||||
|
jizhiRows.push(...body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processedCount++;
|
||||||
|
console.log(` [完成] ${sub}: ${targetFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedCount === 0) {
|
||||||
|
console.error('没有可合并的数据');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 生成输出工作簿 ----------
|
||||||
|
const outWb = XLSX.utils.book_new();
|
||||||
|
|
||||||
|
if (shangluHeader) {
|
||||||
|
const data = [shangluHeader, ...shangluRows];
|
||||||
|
const ws = XLSX.utils.aoa_to_sheet(data);
|
||||||
|
XLSX.utils.book_append_sheet(outWb, ws, '商旅');
|
||||||
|
} else {
|
||||||
|
console.warn('未收集到任何"商旅"数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jizhiHeader) {
|
||||||
|
const data = [jizhiHeader, ...jizhiRows];
|
||||||
|
const ws = XLSX.utils.aoa_to_sheet(data);
|
||||||
|
XLSX.utils.book_append_sheet(outWb, ws, '吉智');
|
||||||
|
} else {
|
||||||
|
console.warn('未收集到任何"吉智"数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outWb.SheetNames.length === 0) {
|
||||||
|
console.error('没有生成任何 sheet,未写出文件');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
XLSX.writeFile(outWb, outFile);
|
||||||
|
|
||||||
|
console.log('\n合并完成:');
|
||||||
|
console.log(` 商旅: ${shangluRows.length} 行数据`);
|
||||||
|
console.log(` 吉智: ${jizhiRows.length} 行数据`);
|
||||||
|
console.log(` 输出文件: ${outFile}`);
|
||||||
BIN
分销-吉利商旅服务费-2026.6月.xlsx
Normal file
BIN
分销-吉利商旅服务费-2026.6月.xlsx
Normal file
Binary file not shown.
BIN
杭州滨江希尔顿欢朋酒店-吉利商旅6月账单.xlsx
Normal file
BIN
杭州滨江希尔顿欢朋酒店-吉利商旅6月账单.xlsx
Normal file
Binary file not shown.
BIN
杭州碧丽酒店管理有限公司 2026年06月 对账单(0601-0630).xlsx
Normal file
BIN
杭州碧丽酒店管理有限公司 2026年06月 对账单(0601-0630).xlsx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user