diff --git a/compare.js b/compare.js new file mode 100644 index 0000000..b546c22 --- /dev/null +++ b/compare.js @@ -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(); diff --git a/merge_kaipiao.js b/merge_kaipiao.js new file mode 100644 index 0000000..3f98935 --- /dev/null +++ b/merge_kaipiao.js @@ -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}`); diff --git a/分销-吉利商旅服务费-2026.6月.xlsx b/分销-吉利商旅服务费-2026.6月.xlsx new file mode 100644 index 0000000..5f3df17 Binary files /dev/null and b/分销-吉利商旅服务费-2026.6月.xlsx differ diff --git a/杭州滨江希尔顿欢朋酒店-吉利商旅6月账单.xlsx b/杭州滨江希尔顿欢朋酒店-吉利商旅6月账单.xlsx new file mode 100644 index 0000000..ade04bc Binary files /dev/null and b/杭州滨江希尔顿欢朋酒店-吉利商旅6月账单.xlsx differ diff --git a/杭州碧丽酒店管理有限公司 2026年06月 对账单(0601-0630).xlsx b/杭州碧丽酒店管理有限公司 2026年06月 对账单(0601-0630).xlsx new file mode 100644 index 0000000..4f2605c Binary files /dev/null and b/杭州碧丽酒店管理有限公司 2026年06月 对账单(0601-0630).xlsx differ