| @@ -0,0 +1,138 @@ | |||||
| package com.ffii.core.utils | |||||
| import org.apache.pdfbox.pdmodel.PDDocument | |||||
| import java.io.File | |||||
| import java.io.OutputStream | |||||
| import java.net.Socket | |||||
| /** | |||||
| * Utility class for generating and sending print jobs to a Canon printer using PDF. | |||||
| * This class requires the 'org.apache.pdfbox:pdfbox' dependency to be included in your project. | |||||
| */ | |||||
| open class CanonPrinterUtil { | |||||
| // Enum to represent valid duplex print modes | |||||
| enum class DuplexMode { | |||||
| SIMPLEX, // Single-sided printing | |||||
| DUPLEX_LONG_EDGE, // Double-sided, flip on long edge (portrait binding) | |||||
| DUPLEX_SHORT_EDGE // Double-sided, flip on short edge (landscape binding) | |||||
| } | |||||
| companion object { | |||||
| /** | |||||
| * Detects if a PDF document is in landscape (horizontal) orientation. | |||||
| * | |||||
| * @param pdfFile The PDF file to check. | |||||
| * @return true if the document is landscape, false if portrait. | |||||
| */ | |||||
| fun isLandscape(pdfFile: File): Boolean { | |||||
| if (!pdfFile.exists() || !pdfFile.canRead()) { | |||||
| return false | |||||
| } | |||||
| try { | |||||
| PDDocument.load(pdfFile).use { document -> | |||||
| if (document.numberOfPages > 0) { | |||||
| val page = document.getPage(0) | |||||
| val mediaBox = page.mediaBox | |||||
| val width = mediaBox.width | |||||
| val height = mediaBox.height | |||||
| // Landscape if width > height | |||||
| return width > height | |||||
| } | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| println("DEBUG: Error detecting PDF orientation: ${e.message}") | |||||
| } | |||||
| return false | |||||
| } | |||||
| /** | |||||
| * Sends a PDF document directly to the specified Canon printer with duplex printing support. | |||||
| * | |||||
| * @param pdfFile The PDF file to be printed. | |||||
| * @param printerIp The IP address of the Canon printer. | |||||
| * @param printerPort The port of the Canon printer, typically 9100. | |||||
| * @param printQty The qty of print, default 1 | |||||
| * @param duplexMode Valid values: SIMPLEX (single-sided), DUPLEX_LONG_EDGE (portrait), DUPLEX_SHORT_EDGE (landscape) | |||||
| * @throws Exception if there is an error during file processing or printing. | |||||
| */ | |||||
| fun printPdfToCanon(pdfFile: File, printerIp: String, printerPort: Int, printQty: Int? = 1, duplexMode: DuplexMode = DuplexMode.DUPLEX_LONG_EDGE) { | |||||
| // Check if the file exists and is readable | |||||
| if (!pdfFile.exists() || !pdfFile.canRead()) { | |||||
| throw IllegalArgumentException("Error: File not found or not readable at path: ${pdfFile.absolutePath}") | |||||
| } | |||||
| try { | |||||
| // 1. Build PJL (Printer Job Language) commands for Canon printers | |||||
| val pjlCommands = StringBuilder() | |||||
| // PJL job start | |||||
| pjlCommands.append("\u001B%-12345X@PJL\n") | |||||
| pjlCommands.append("@PJL JOB NAME=\"PDF Print\"\n") | |||||
| // 2. Set duplex mode | |||||
| when (duplexMode) { | |||||
| DuplexMode.SIMPLEX -> { | |||||
| pjlCommands.append("@PJL SET DUPLEX=OFF\n") | |||||
| } | |||||
| DuplexMode.DUPLEX_LONG_EDGE -> { | |||||
| pjlCommands.append("@PJL SET DUPLEX=ON\n") | |||||
| pjlCommands.append("@PJL SET BINDING=LONGEDGE\n") | |||||
| } | |||||
| DuplexMode.DUPLEX_SHORT_EDGE -> { | |||||
| pjlCommands.append("@PJL SET DUPLEX=ON\n") | |||||
| pjlCommands.append("@PJL SET BINDING=SHORTEDGE\n") | |||||
| } | |||||
| } | |||||
| // 3. Set number of copies | |||||
| if ((printQty ?: 1) > 1) { | |||||
| pjlCommands.append("@PJL SET COPIES=${printQty ?: 1}\n") | |||||
| } | |||||
| // 4. Set paper size to A4 | |||||
| pjlCommands.append("@PJL SET PAPER=A4\n") | |||||
| // 5. Enter PDF language mode | |||||
| pjlCommands.append("@PJL ENTER LANGUAGE=PDF\n") | |||||
| // PJL job end marker (before PDF content) | |||||
| pjlCommands.append("\u001B%-12345X\n") | |||||
| // 6. Read PDF file content | |||||
| val pdfContent = pdfFile.readBytes() | |||||
| println("DEBUG: PDF file size: ${pdfContent.size} bytes") | |||||
| // Print each copy for the specified quantity | |||||
| repeat(printQty ?: 1) { copyIndex -> | |||||
| println("DEBUG: Printing copy ${copyIndex + 1} of ${printQty ?: 1}") | |||||
| // 7. Send to printer via TCP/IP socket | |||||
| Socket(printerIp, printerPort).use { socket -> | |||||
| val os: OutputStream = socket.getOutputStream() | |||||
| // Send PJL commands | |||||
| val printData = pjlCommands.toString().toByteArray(Charsets.US_ASCII) | |||||
| os.write(printData) | |||||
| // Send PDF content | |||||
| os.write(pdfContent) | |||||
| // Send job end marker | |||||
| os.write("\u001B%-12345X".toByteArray(Charsets.US_ASCII)) | |||||
| os.flush() | |||||
| println("DEBUG: Copy ${copyIndex + 1} sent to printer") | |||||
| } | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| // Re-throw the exception with a more descriptive message | |||||
| throw Exception("Error processing print job for PDF: ${e.message}", e) | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -71,6 +71,7 @@ import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository | |||||
| import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService // 添加这行 | import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService // 添加这行 | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.* | import com.ffii.fpsms.modules.deliveryOrder.web.models.* | ||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository // 添加 | import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository // 添加 | ||||
| import com.ffii.core.utils.CanonPrinterUtil | |||||
| @Service | @Service | ||||
| open class DeliveryOrderService( | open class DeliveryOrderService( | ||||
| @@ -928,36 +929,53 @@ open class DeliveryOrderService( | |||||
| ) | ) | ||||
| } | } | ||||
| //Print Delivery Note | |||||
| @Transactional | |||||
| open fun printDeliveryNote(request: PrintDeliveryNoteRequest) { | |||||
| //val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||||
| val pdf = exportDeliveryNote( | |||||
| ExportDeliveryNoteRequest( | |||||
| doPickOrderId = request.doPickOrderId, | |||||
| numOfCarton = request.numOfCarton, | |||||
| isDraft = request.isDraft | |||||
| ) | |||||
| //Print Delivery Note | |||||
| @Transactional | |||||
| open fun printDeliveryNote(request: PrintDeliveryNoteRequest) { | |||||
| val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||||
| val pdf = exportDeliveryNote( | |||||
| ExportDeliveryNoteRequest( | |||||
| doPickOrderId = request.doPickOrderId, | |||||
| numOfCarton = request.numOfCarton, | |||||
| isDraft = request.isDraft | |||||
| ) | ) | ||||
| ) | |||||
| val jasperPrint = pdf["report"] as JasperPrint | |||||
| val jasperPrint = pdf["report"] as JasperPrint | |||||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||||
| try { | |||||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||||
| try { | |||||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||||
| //val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||||
| //printer.ip?.let { ip -> printer.port?.let { port -> | |||||
| // ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED) | |||||
| //}} | |||||
| } finally { | |||||
| //tempPdfFile.delete() | |||||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||||
| val duplexMode = if (CanonPrinterUtil.isLandscape(tempPdfFile)) { | |||||
| CanonPrinterUtil.DuplexMode.DUPLEX_SHORT_EDGE // Landscape: flip on short edge | |||||
| } else { | |||||
| CanonPrinterUtil.DuplexMode.DUPLEX_LONG_EDGE // Portrait: flip on long edge | |||||
| } | } | ||||
| println("DEBUG: PDF orientation detected - Landscape: ${CanonPrinterUtil.isLandscape(tempPdfFile)}, Duplex mode: $duplexMode") | |||||
| printer.ip?.let { ip -> | |||||
| printer.port?.let { port -> | |||||
| CanonPrinterUtil.printPdfToCanon( | |||||
| tempPdfFile, | |||||
| ip, | |||||
| port, | |||||
| printQty, | |||||
| duplexMode | |||||
| ) | |||||
| } | |||||
| } | |||||
| } finally { | |||||
| //tempPdfFile.delete() | |||||
| } | } | ||||
| } | |||||
| //Carton Labels | //Carton Labels | ||||
| open fun exportDNLabels(request: ExportDNLabelsRequest): Map<String, Any> { | open fun exportDNLabels(request: ExportDNLabelsRequest): Map<String, Any> { | ||||
| val DNLABELS_PDF = "DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml" | val DNLABELS_PDF = "DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml" | ||||
| @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.jobOrder.service | |||||
| import com.ffii.core.exception.BadRequestException | import com.ffii.core.exception.BadRequestException | ||||
| import com.ffii.core.response.RecordsRes | import com.ffii.core.response.RecordsRes | ||||
| import com.ffii.core.utils.CanonPrinterUtil | |||||
| import com.ffii.core.utils.GsonUtils | import com.ffii.core.utils.GsonUtils | ||||
| import com.ffii.core.utils.PdfUtils | import com.ffii.core.utils.PdfUtils | ||||
| import com.ffii.core.utils.QrCodeUtil | import com.ffii.core.utils.QrCodeUtil | ||||
| @@ -395,7 +396,7 @@ open class JobOrderService( | |||||
| params["FGCode"] = pickRecordInfo.firstOrNull()?.get("fgCode") as? String ?: "N/A" | params["FGCode"] = pickRecordInfo.firstOrNull()?.get("fgCode") as? String ?: "N/A" | ||||
| params["FGName"] = pickRecordInfo.firstOrNull()?.get("fgName") as? String ?: "N/A" | params["FGName"] = pickRecordInfo.firstOrNull()?.get("fgName") as? String ?: "N/A" | ||||
| /*// Debug UOM information | |||||
| /* Debug UOM information | |||||
| val bomItemUomIdRaw = pickRecordInfo.firstOrNull()?.get("bomItemUomId") | val bomItemUomIdRaw = pickRecordInfo.firstOrNull()?.get("bomItemUomId") | ||||
| val bomItemId = pickRecordInfo.firstOrNull()?.get("bomItemId") | val bomItemId = pickRecordInfo.firstOrNull()?.get("bomItemId") | ||||
| val uomCode = pickRecordInfo.firstOrNull()?.get("uomCode") as? String | val uomCode = pickRecordInfo.firstOrNull()?.get("uomCode") as? String | ||||
| @@ -420,9 +421,10 @@ open class JobOrderService( | |||||
| ) | ) | ||||
| } | } | ||||
| //Print Pick Record | |||||
| @Transactional | @Transactional | ||||
| open fun printPickRecord(request: PrintPickRecordRequest){ | open fun printPickRecord(request: PrintPickRecordRequest){ | ||||
| //val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||||
| val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||||
| val pdf = exportPickRecord( | val pdf = exportPickRecord( | ||||
| ExportPickRecordRequest( | ExportPickRecordRequest( | ||||
| @@ -437,8 +439,30 @@ open class JobOrderService( | |||||
| try{ | try{ | ||||
| JasperExportManager.exportReportToPdfFile(jasperPrint,tempPdfFile.absolutePath) | JasperExportManager.exportReportToPdfFile(jasperPrint,tempPdfFile.absolutePath) | ||||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||||
| // Auto-detect orientation and set duplex mode accordingly | |||||
| val duplexMode = if (CanonPrinterUtil.isLandscape(tempPdfFile)) { | |||||
| CanonPrinterUtil.DuplexMode.DUPLEX_SHORT_EDGE // Landscape: flip on short edge | |||||
| } else { | |||||
| CanonPrinterUtil.DuplexMode.DUPLEX_LONG_EDGE // Portrait: flip on long edge | |||||
| } | |||||
| println("DEBUG: PDF orientation detected - Landscape: ${CanonPrinterUtil.isLandscape(tempPdfFile)}, Duplex mode: $duplexMode") | |||||
| printer.ip?.let { ip -> | |||||
| printer.port?.let { port -> | |||||
| CanonPrinterUtil.printPdfToCanon( | |||||
| tempPdfFile, | |||||
| ip, | |||||
| port, | |||||
| printQty, | |||||
| duplexMode | |||||
| ) | |||||
| } | |||||
| } | |||||
| } finally { | } finally { | ||||
| //tempPdfFile.delete | |||||
| //tempPdfFile.delete() | |||||
| } | } | ||||
| } | } | ||||
| @@ -492,7 +492,7 @@ open class BomService( | |||||
| // val folder = File(folderPath) | // val folder = File(folderPath) | ||||
| val resolver = PathMatchingResourcePatternResolver() | val resolver = PathMatchingResourcePatternResolver() | ||||
| // val excels = resolver.getResources("bomImport/*.xlsx") | // val excels = resolver.getResources("bomImport/*.xlsx") | ||||
| val excels = resolver.getResources("file:C:/Users/ffii_/Downloads/bom/new/*.xlsx") | |||||
| val excels = resolver.getResources("file:C:/Users/Kelvin YAU/Downloads/bom/*.xlsx") | |||||
| // val excels = resolver.getResources("file:C:/Users/2Fi/Desktop/Third Wave of BOM Excel/*.xlsx") | // val excels = resolver.getResources("file:C:/Users/2Fi/Desktop/Third Wave of BOM Excel/*.xlsx") | ||||
| println("size: ${excels.size}") | println("size: ${excels.size}") | ||||
| val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx") | val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx") | ||||