From e13bb15969fa39012220c62832863b497629fd87 Mon Sep 17 00:00:00 2001 From: Misaka Date: Tue, 3 Mar 2026 21:32:11 +0800 Subject: [PATCH] feat: implement Excel merge functionality for data extraction - Add mergeFiles() method in ExtractorService to combine downloaded batch files - Add saveMergedOrders() method to output full 31-column Excel format - Update recordCount to return actual material record count - Add missing field mappings in OrderHeader type and ExcelParser: - factory, materialStatus, planNumber, materialType - department, remark, createDate, approveDate - Output file named with timestamp: merged_YYYYMMDDHHMMSS.xlsx Co-Authored-By: Claude (glm-5) --- src/main/services/erp/extractor.ts | 162 +++++++++++++++++++++++- src/main/services/excel/excel-parser.ts | 11 ++ src/main/types/excel.types.ts | 27 ++++ 3 files changed, 198 insertions(+), 2 deletions(-) diff --git a/src/main/services/erp/extractor.ts b/src/main/services/erp/extractor.ts index 200dc31..5275137 100644 --- a/src/main/services/erp/extractor.ts +++ b/src/main/services/erp/extractor.ts @@ -2,8 +2,10 @@ import path from 'path' import fs from 'fs/promises' import { ERP_LOCATORS } from './locators' import { ErpAuthService } from './erp-auth' +import { ExcelParser } from '../excel/excel-parser' import type { ExtractorInput, ExtractorResult } from '../../types/extractor.types' import type { ErpSession } from '../../types/erp.types' +import type { DiscreteMaterialPlan } from '../../types/excel.types' /** * ERP Data Extractor Service @@ -66,8 +68,13 @@ export class ExtractorService { } } - // TODO: Merge files (implement in separate task) - // result.mergedFile = await this.mergeFiles(result.downloadedFiles); + // Merge downloaded files into a single Excel file + if (result.downloadedFiles.length > 0) { + input.onProgress?.('正在合并文件...', 95) + const mergeResult = await this.mergeFiles(result.downloadedFiles) + result.mergedFile = mergeResult.mergedFile + result.recordCount = mergeResult.recordCount + } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' result.errors.push(`Extraction failed: ${message}`) @@ -222,4 +229,155 @@ export class ExtractorService { } return batches } + + /** + * Merge downloaded Excel files into a single file + * Uses ExcelParser to parse and combine all material plans + * + * @param filePaths - Array of downloaded Excel file paths + * @returns Merged file path and total record count + */ + private async mergeFiles( + filePaths: string[] + ): Promise<{ mergedFile: string | null; recordCount: number }> { + if (filePaths.length === 0) { + return { mergedFile: null, recordCount: 0 } + } + + const parser = new ExcelParser({ verbose: false }) + + // Collect all orders with full order info and materials + // Each order has: { orderInfo: OrderHeader, materials: MaterialRow[] } + const allOrders: Array<{ orderInfo: any; materials: any[] }> = [] + + // Parse each downloaded file and collect orders + for (const filePath of filePaths) { + try { + await parser.parse(filePath) + // After parse(), the parser stores orders internally as lastOrders + const orders = (parser as any).lastOrders + if (orders && Array.isArray(orders)) { + allOrders.push(...orders) + } + } catch (error) { + console.error(`Failed to parse file ${filePath}:`, error) + } + } + + // Calculate total record count (total material rows) + let recordCount = 0 + for (const order of allOrders) { + recordCount += order.materials.length + } + + if (recordCount === 0) { + return { mergedFile: null, recordCount: 0 } + } + + // Generate output filename with timestamp + const timestamp = new Date() + .toISOString() + .replace(/[-:T]/g, '') + .replace(/\..+/, '') + .slice(0, 14) + const outputPath = path.join(this.downloadDir, `merged_${timestamp}.xlsx`) + + // Save with full 31 columns matching ExcelParser.saveAsExcel format + await this.saveMergedOrders(allOrders, outputPath) + + return { mergedFile: outputPath, recordCount } + } + + /** + * Save merged orders to a new Excel file with full 31 columns + * Matches the output format of ExcelParser.saveAsExcel() + */ + private async saveMergedOrders( + orders: Array<{ orderInfo: any; materials: any[] }>, + outputPath: string + ): Promise { + const ExcelJS = await import('exceljs') + const workbook = new ExcelJS.Workbook() + const worksheet = workbook.addWorksheet('Data') + + // Define all 31 columns matching ExcelParser.saveAsExcel output format + worksheet.columns = [ + { header: '工厂', key: 'factory', width: 25 }, + { header: '备料状态', key: 'materialStatus', width: 15 }, + { header: '备料计划单号', key: 'planNumber', width: 25 }, + { header: '来源单号', key: 'productionOrder', width: 20 }, + { header: '备料类型', key: 'materialType', width: 15 }, + { header: '产品编码', key: 'productCode', width: 15 }, + { header: '产品名称', key: 'productName', width: 30 }, + { header: '产品计划数量', key: 'productPlannedQuantity', width: 15 }, + { header: '单位', key: 'productUnit', width: 10 }, + { header: '用料部门', key: 'department', width: 15 }, + { header: '备注', key: 'remark', width: 20 }, + { header: '制单人', key: 'creator', width: 15 }, + { header: '制单日期', key: 'createDate', width: 15 }, + { header: '审批人', key: 'approver', width: 15 }, + { header: '审批日期', key: 'approveDate', width: 15 }, + { header: '序号', key: 'sequence', width: 10 }, + { header: '材料编码', key: 'materialCode', width: 15 }, + { header: '材料名称', key: 'materialName', width: 30 }, + { header: '规格', key: 'specification', width: 30 }, + { header: '型号', key: 'model', width: 20 }, + { header: '图号', key: 'drawingNumber', width: 20 }, + { header: '物料材质', key: 'material', width: 15 }, + { header: '计划数量', key: 'quantity', width: 12 }, + { header: '单位', key: 'unit', width: 10 }, + { header: '需用日期', key: 'requiredDate', width: 15 }, + { header: '发料仓库', key: 'warehouse', width: 15 }, + { header: '单位用量', key: 'unitUsage', width: 12 }, + { header: '累计出库数量', key: 'cumulativeOutboundQty', width: 15 }, + { header: '打印人', key: 'printer', width: 15 }, + { header: '打印日期', key: 'printDate', width: 20 } + ] + + // Add data rows - merge orderInfo with each material + for (const order of orders) { + const { orderInfo, materials } = order + + for (const material of materials) { + worksheet.addRow({ + // Order info (first 15 columns) + factory: orderInfo.factory || '', + materialStatus: orderInfo.materialStatus || '', + planNumber: orderInfo.planNumber || '', + productionOrder: orderInfo.productionOrder || '', + materialType: orderInfo.materialType || '', + productCode: orderInfo.productCode || '', + productName: orderInfo.productName || '', + productPlannedQuantity: orderInfo.plannedQuantity || '', + productUnit: orderInfo.unit || '', + department: orderInfo.department || '', + remark: orderInfo.remark || '', + creator: orderInfo.creator || '', + createDate: orderInfo.createDate || '', + approver: orderInfo.approver || '', + approveDate: orderInfo.approveDate || '', + // Material data (columns 16-28) + sequence: material.sequence || '', + materialCode: material.materialCode || '', + materialName: material.materialName || '', + specification: material.specification || '', + model: material.model || '', + drawingNumber: material.drawingNumber || '', + material: material.material || '', + quantity: material.quantity || 0, + unit: material.unit || '', + requiredDate: material.requiredDate || '', + warehouse: material.warehouse || '', + unitUsage: material.unitUsage || 0, + cumulativeOutboundQty: material.cumulativeOutboundQty || 0, + // Footer info (last 2 columns) + printer: orderInfo.printer || '', + printDate: orderInfo.printDate || '' + }) + } + } + + await workbook.xlsx.writeFile(outputPath) + console.log(`Merged ${orders.length} orders to ${outputPath}`) + } } diff --git a/src/main/services/excel/excel-parser.ts b/src/main/services/excel/excel-parser.ts index 98ea7cf..a72c2af 100644 --- a/src/main/services/excel/excel-parser.ts +++ b/src/main/services/excel/excel-parser.ts @@ -22,6 +22,11 @@ export class ExcelParser { // Mapping from Chinese field names to English property names private CHINESE_TO_ENGLISH_MAPPING: Record = { + // Header fields (row 2-4) + 工厂: 'factory', + 备料状态: 'materialStatus', + 备料计划单号: 'planNumber', + 备料类型: 'materialType', 生产部门: 'productionDepartment', 生产订单: 'productionOrder', 来源单号: 'productionOrder', // This is the order number we need! @@ -30,8 +35,14 @@ export class ExcelParser { 产品规格: 'productSpecification', 计划数量: 'plannedQuantity', 单位: 'unit', + 用料部门: 'department', + 备注: 'remark', 需用日期: 'requiredDate', + // Footer fields (row 14-15) 制单人: 'creator', + 制单日期: 'createDate', + 审批人: 'approver', + 审批日期: 'approveDate', 打印人: 'printer', 打印日期: 'printDate', // Mapped fields (after FIELD_NAME_MAPPING) diff --git a/src/main/types/excel.types.ts b/src/main/types/excel.types.ts index 7afb64d..75242ca 100644 --- a/src/main/types/excel.types.ts +++ b/src/main/types/excel.types.ts @@ -81,6 +81,18 @@ export interface OrderHeader { /** Order title (离散备料计划) */ title?: string + /** Factory (工厂) */ + factory?: string + + /** Material status (备料状态) */ + materialStatus?: string + + /** Plan number (备料计划单号) */ + planNumber?: string + + /** Material type (备料类型) */ + materialType?: string + /** Production department (生产部门) */ productionDepartment?: string @@ -102,12 +114,27 @@ export interface OrderHeader { /** Unit (单位) */ unit?: string + /** Department (用料部门) */ + department?: string + + /** Remark (备注) */ + remark?: string + /** Required date (需用日期) */ requiredDate?: string /** Creator (制单人) */ creator?: string + /** Create date (制单日期) */ + createDate?: string + + /** Approver (审批人) */ + approver?: string + + /** Approve date (审批日期) */ + approveDate?: string + /** Printer (打印人) */ printer?: string