diff --git a/src/main/java/com/ffii/core/utils/BrotherPrinterUtil.kt b/src/main/java/com/ffii/core/utils/BrotherPrinterUtil.kt index 7908bb7..70af932 100644 --- a/src/main/java/com/ffii/core/utils/BrotherPrinterUtil.kt +++ b/src/main/java/com/ffii/core/utils/BrotherPrinterUtil.kt @@ -5,58 +5,51 @@ import org.apache.pdfbox.rendering.PDFRenderer import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream import java.io.File -import java.io.OutputStream import java.net.Socket -import javax.imageio.ImageIO +import org.slf4j.LoggerFactory class BrotherPrinterUtil { companion object { + private const val SOCKET_TIMEOUT_MS = 120_000 // 2 min for large jobs; Brother may be slow to accept + private const val BROTHER_DPI = 200 // Lower DPI = smaller job; some Brother models buffer better at 200 + + private val log = LoggerFactory.getLogger(BrotherPrinterUtil::class.java) + /** - * //Usage - * BrotherPrinterUtil.printToBrother( - pdfFile = File("document.pdf"), - printerIp = "192.168.1.50", - printQty = 1 - ) - - * Sends a PDF to a Brother DCP-1610W by rendering it to a PCL-compatible format. + * Sends a PDF to a Brother (e.g. DCP-1610W) by rendering to PCL raster over port 9100. + * Uses socket timeout and flush-per-page so the printer has time to accept data. */ fun printToBrother(pdfFile: File, printerIp: String, printerPort: Int = 9100, printQty: Int = 1) { - if (!pdfFile.exists()) throw IllegalArgumentException("File not found.") + if (!pdfFile.exists()) throw IllegalArgumentException("File not found: ${pdfFile.absolutePath}") - println("DEBUG: PDF file size: ${pdfFile.length()} bytes") + log.info("Brother print: file=${pdfFile.name} size=${pdfFile.length()} ip=$printerIp port=$printerPort copies=$printQty") PDDocument.load(pdfFile).use { document -> val renderer = PDFRenderer(document) - val totalPages = document.numberOfPages - repeat(printQty) { copyIndex -> - println("DEBUG: Printing copy ${copyIndex + 1} of $printQty") - } Socket(printerIp, printerPort).use { socket -> + socket.soTimeout = SOCKET_TIMEOUT_MS val os = socket.getOutputStream() - - // 1. Start PJL Job + + // 1. PJL header (optional; some Brother prefer minimal PJL) os.write(getPjlHeader(printQty)) + os.flush() - // 2. Render each page as a 300 DPI image - for (pageIndex in 0 until document.numberOfPages) { - val image = renderer.renderImageWithDPI(pageIndex, 300f, org.apache.pdfbox.rendering.ImageType.BINARY) - - // 3. Convert Image to PCL Raster Data - val pclData = convertImageToPcl(image) - os.write(pclData) - - // Form Feed (Move to next page) - os.write("\u000C".toByteArray()) + repeat(printQty) { copyIndex -> + for (pageIndex in 0 until totalPages) { + val image = renderer.renderImageWithDPI(pageIndex, BROTHER_DPI.toFloat(), org.apache.pdfbox.rendering.ImageType.BINARY) + val pclData = convertImageToPcl(image, BROTHER_DPI) + os.write(pclData) + os.write("\u000C".toByteArray(Charsets.US_ASCII)) // Form feed + os.flush() // Flush per page so Brother receives incrementally + } } - // 4. End Job os.write("\u001B%-12345X".toByteArray(Charsets.US_ASCII)) os.flush() - println("DEBUG: Print job sent to printer") + log.info("Brother print job sent successfully") } } } @@ -64,24 +57,25 @@ class BrotherPrinterUtil { private fun getPjlHeader(qty: Int): ByteArray { val pjl = StringBuilder() pjl.append("\u001B%-12345X@PJL\r\n") - pjl.append("@PJL SET COPIES=$qty\r\n") - pjl.append("@PJL ENTER LANGUAGE=PCL\r\n") // Tell printer to expect PCL graphics, not PDF + pjl.append("@PJL SET COPIES=1\r\n") // We send copies in app; some Brother ignore COPIES or mis-handle it + pjl.append("@PJL ENTER LANGUAGE=PCL\r\n") return pjl.toString().toByteArray(Charsets.US_ASCII) } /** - * Converts a BufferedImage into PCL Level 3/5 Raster Graphics. - * This is the "magic" that allows a budget printer to print a PDF. + * Converts a BufferedImage into PCL raster graphics for Brother. + * Uses given DPI (e.g. 200) to keep job size smaller for printers with limited buffer. */ - private fun convertImageToPcl(image: BufferedImage): ByteArray { + private fun convertImageToPcl(image: BufferedImage, dpi: Int): ByteArray { val out = ByteArrayOutputStream() val width = image.width val height = image.height - // PCL: Start Graphics at 300 DPI - out.write("\u001B*t300R".toByteArray()) - // PCL: Start Raster Graphics - out.write("\u001B*r1A".toByteArray()) + // PCL reset (Brother often needs clean state) + out.write("\u001BE".toByteArray(Charsets.US_ASCII)) // Esc E = reset + // PCL: Set resolution and start raster + out.write("\u001B*t${dpi}R".toByteArray(Charsets.US_ASCII)) + out.write("\u001B*r1A".toByteArray(Charsets.US_ASCII)) for (y in 0 until height) { val rowBytes = (width + 7) / 8 diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt index dd9f89a..8c78b73 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt @@ -46,6 +46,7 @@ interface JobOrderRepository : AbstractRepository { b.code as itemCode, b.name, jo.reqQty, + jo.bomId, b.outputQtyUom as unit, uc2.udfudesc as uom, COALESCE(uc2.udfShortDesc, uc2.udfudesc) as shortUom, diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt index 5f67a5f..4e7a77c 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt @@ -76,6 +76,7 @@ interface JobOrderDetailWithJsonString { val code: String?; val itemCode: String?; val name: String?; + val bomId: Long?; val itemId: Long?; val reqQty: BigDecimal?; val uom: String?; @@ -115,6 +116,7 @@ data class JobOrderDetail( val code: String?, val itemCode: String?, val name: String?, + val bomId: Long?, val reqQty: BigDecimal?, val uom: String?, val shortUom: String?, diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 2c70071..60f90de 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -334,6 +334,7 @@ open class JobOrderService( name = sqlResult.name, reqQty = sqlResult.reqQty, uom = sqlResult.uom, + bomId = sqlResult.bomId, pickLines = jsonResult, status = sqlResult.status, shortUom = sqlResult.shortUom @@ -352,6 +353,7 @@ open class JobOrderService( name = sqlResult.name, reqQty = sqlResult.reqQty, uom = sqlResult.uom, + bomId = sqlResult.bomId, pickLines = jsonResult, status = sqlResult.status, shortUom = sqlResult.shortUom @@ -371,6 +373,7 @@ open class JobOrderService( code = sqlResult.code, itemCode = sqlResult.itemCode, name = sqlResult.name, + bomId = sqlResult.bomId, reqQty = sqlResult.reqQty, uom = sqlResult.uom, pickLines = jsonResult, @@ -390,6 +393,7 @@ open class JobOrderService( code = sqlResult.code, itemCode = sqlResult.itemCode, name = sqlResult.name, + bomId = sqlResult.bomId, reqQty = sqlResult.reqQty, uom = sqlResult.uom, pickLines = jsonResult, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index d4c1201..d723695 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -298,7 +298,7 @@ open class StockInLineService( ) val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId) // PO-origin: frontend sends qty in stock; non-PO: treat as purchase and convert to stock - val convertedBaseQty = if (stockInLine.purchaseOrderLine != null) { + val convertedBaseQty = if (stockInLine.purchaseOrderLine != null || stockInLine.jobOrder != null) { line.qty } else if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) { (line.qty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!) @@ -680,6 +680,17 @@ open class StockInLineService( } else { requestQty ?: this.acceptedQty } + } else if (request.qcAccept == true && this.status == StockInLineStatus.ESCALATED.status) { + // Case: line was escalated (QC decision 3), handler resolves with decision 1 (accept). + // Use 揀收數量 (acceptQty) for put away instead of keeping original received qty. + val requestQty = request.acceptQty ?: request.acceptedQty + if (requestQty != null) { + this.acceptedQty = if (this.purchaseOrderLine != null && this.item?.id != null) { + itemUomService.convertPurchaseQtyToStockQty(this.item!!.id!!, requestQty) + } else { + requestQty + } + } } // Set demandQty based on source if (this.jobOrder != null && this.jobOrder?.bom != null) { @@ -710,8 +721,8 @@ open class StockInLineService( ) val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId) - val convertedBaseQty = if (stockInLine.purchaseOrderLine != null) { - // PO-origin: qty is already stock qty + val convertedBaseQty = if (stockInLine.purchaseOrderLine != null || stockInLine.jobOrder != null) { + // PO and Job Order: qty is already stock qty line.qty } else if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) { // Legacy: treat as purchase qty, convert to stock qty