| @@ -57,16 +57,33 @@ open class M18DeliveryOrderService( | |||
| open fun getDeliveryOrdersWithType(request: M18CommonRequest): M18PurchaseOrderListResponseWithType? { | |||
| val deliveryOrders = M18PurchaseOrderListResponseWithType(mutableListOf()) | |||
| val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") | |||
| val dateFrom = request.modifiedDateFrom?.let { | |||
| LocalDateTime.parse(it, formatter).toLocalDate().toString() | |||
| val lastDateConds = if (request.modifiedDateFrom != null && request.modifiedDateTo != null) { | |||
| val dateFrom = LocalDateTime.parse(request.modifiedDateFrom, formatter).toString() | |||
| val dateTo = LocalDateTime.parse(request.modifiedDateTo, formatter).toString() | |||
| "lastModifyDate=largerOrEqual=$dateFrom=and=lastModifyDate=lessOrEqual=$dateTo" | |||
| } else { | |||
| "" | |||
| } | |||
| val dateTo = request.modifiedDateTo?.let { | |||
| LocalDateTime.parse(it, formatter).toLocalDate().toString() | |||
| val dDateConds = if (request.dDateFrom != null && request.dDateTo != null) { | |||
| val dateFrom = LocalDateTime.parse(request.dDateFrom, formatter).toLocalDate().toString() | |||
| val dateTo = LocalDateTime.parse(request.dDateTo, formatter).toLocalDate().toString() | |||
| "dDate=largerOrEqual=$dateFrom=and=dDate=lessOrEqual=$dateTo" | |||
| } else { | |||
| "" | |||
| } | |||
| val lastDateConds = | |||
| //"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}" | |||
| "dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}" | |||
| val dDateEqualConds = if (request.dDateEqual != null) { | |||
| val dDateEqual = LocalDateTime.parse(request.dDateEqual, formatter).toLocalDate().toString() | |||
| "dDate=equal=$dDateEqual" | |||
| } else { | |||
| "" | |||
| } | |||
| // Shop PO | |||
| val shopPoBuyers = commonUtils.listToString(listOf(m18Config.BEID_TOA), "beId=equal=", "=or=") | |||
| val shopPoSupplier = commonUtils.listToString( | |||
| @@ -74,8 +91,20 @@ open class M18DeliveryOrderService( | |||
| "venId=equal=", | |||
| "=or=" | |||
| ) | |||
| val shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})=and=(${lastDateConds})" | |||
| var shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})" | |||
| if (request.modifiedDateFrom != null && request.modifiedDateTo != null) { | |||
| shopPoConds += "=and=(${lastDateConds})" | |||
| } | |||
| if (request.dDateFrom != null && request.dDateTo != null) { | |||
| shopPoConds += "=and=(${dDateConds})" | |||
| } | |||
| if (request.dDateEqual != null) { | |||
| shopPoConds += "=and=(${dDateEqualConds})" | |||
| } | |||
| println("shopPoConds: ${shopPoConds}") | |||
| val shopPoParams = M18PurchaseOrderListRequest( | |||
| params = null, | |||
| conds = shopPoConds | |||
| @@ -59,15 +59,32 @@ open class M18PurchaseOrderService( | |||
| open fun getPurchaseOrdersWithType(request: M18CommonRequest): M18PurchaseOrderListResponseWithType? { | |||
| val purchaseOrders = M18PurchaseOrderListResponseWithType(mutableListOf()) | |||
| val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") | |||
| val dateFrom = request.modifiedDateFrom?.let { | |||
| LocalDateTime.parse(it, formatter).toLocalDate().toString() | |||
| val lastDateConds = if (request.modifiedDateFrom != null && request.modifiedDateTo != null) { | |||
| val dateFrom = LocalDateTime.parse(request.modifiedDateFrom, formatter).toLocalDate().toString() | |||
| val dateTo = LocalDateTime.parse(request.modifiedDateTo, formatter).toLocalDate().toString() | |||
| "lastModifyDate=largerOrEqual=$dateFrom=and=lastModifyDate=lessOrEqual=$dateTo" | |||
| } else { | |||
| "" | |||
| } | |||
| val dateTo = request.modifiedDateTo?.let { | |||
| LocalDateTime.parse(it, formatter).toLocalDate().toString() | |||
| val dDateConds = if (request.dDateFrom != null && request.dDateTo != null) { | |||
| val dateFrom = LocalDateTime.parse(request.dDateFrom, formatter).toLocalDate().toString() | |||
| val dateTo = LocalDateTime.parse(request.dDateTo, formatter).toLocalDate().toString() | |||
| "dDate=largerOrEqual=$dateFrom=and=dDate=lessOrEqual=$dateTo" | |||
| } else { | |||
| "" | |||
| } | |||
| val lastDateConds = | |||
| //"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}" | |||
| "dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}" | |||
| val dDateEqualConds = if (request.dDateEqual != null) { | |||
| val dDateEqual = LocalDateTime.parse(request.dDateEqual, formatter).toLocalDate().toString() | |||
| "dDate=equal=$dDateEqual" | |||
| } else { | |||
| "" | |||
| } | |||
| // Material PO | |||
| val materialPoBuyers = | |||
| commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=") | |||
| @@ -76,8 +93,21 @@ open class M18PurchaseOrderService( | |||
| "venId=unequal=", | |||
| "=or=" | |||
| ) | |||
| val materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot})=and=(${lastDateConds})" | |||
| var materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot}))" | |||
| if (request.modifiedDateFrom != null && request.modifiedDateTo != null) { | |||
| materialPoConds += "=and=(${lastDateConds})" | |||
| } | |||
| if (request.dDateFrom != null && request.dDateTo != null) { | |||
| materialPoConds += "=and=(${dDateConds})" | |||
| } | |||
| if (request.dDateEqual != null) { | |||
| materialPoConds += "=and=(${dDateEqualConds})" | |||
| } | |||
| println("materialPoConds: ${materialPoConds}") | |||
| val materialPoParams = M18PurchaseOrderListRequest( | |||
| params = null, | |||
| conds = materialPoConds | |||
| @@ -3,4 +3,7 @@ package com.ffii.fpsms.m18.web.models | |||
| data class M18CommonRequest( | |||
| val modifiedDateFrom: String? = null, | |||
| val modifiedDateTo: String? = null, | |||
| val dDateFrom: String? = null, | |||
| val dDateTo: String? = null, | |||
| val dDateEqual: String? = null, | |||
| ) | |||
| @@ -25,6 +25,9 @@ public abstract class SettingNames { | |||
| */ | |||
| public static final String SCHEDULE_M18_PO = "SCHEDULE.m18.po"; | |||
| public static final String SCHEDULE_M18_DO1 = "SCHEDULE.m18.do1"; | |||
| public static final String SCHEDULE_M18_DO2 = "SCHEDULE.m18.do2"; | |||
| public static final String SCHEDULE_M18_MASTER = "SCHEDULE.m18.master"; | |||
| public static final String SCHEDULE_PROD_ROUGH = "SCHEDULE.prod.rough"; | |||
| @@ -31,19 +31,23 @@ open class SchedulerService( | |||
| ) { | |||
| var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) | |||
| val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd") | |||
| val dateTimeStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") | |||
| val defaultCronExpression = "0 0 2 * * *"; | |||
| @Volatile | |||
| //@Volatile | |||
| var scheduledM18Po: ScheduledFuture<*>? = null | |||
| var scheduledM18Do1: ScheduledFuture<*>? = null | |||
| var scheduledM18Do2: ScheduledFuture<*>? = null | |||
| @Volatile | |||
| var scheduledM18Master: ScheduledFuture<*>? = null | |||
| @Volatile | |||
| var scheduledRoughProd: ScheduledFuture<*>? = null | |||
| //@Volatile | |||
| //var scheduledRoughProd: ScheduledFuture<*>? = null | |||
| @Volatile | |||
| var scheduledDetailedProd: ScheduledFuture<*>? = null | |||
| //@Volatile | |||
| //var scheduledDetailedProd: ScheduledFuture<*>? = null | |||
| // Common Function | |||
| fun isValidCronExpression(cronExpression: String): Boolean { | |||
| @@ -55,52 +59,63 @@ open class SchedulerService( | |||
| } | |||
| } | |||
| fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit) { | |||
| fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit): ScheduledFuture<*>? { | |||
| scheduled?.cancel(false) | |||
| var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCronExpression; | |||
| var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCronExpression | |||
| if (!isValidCronExpression(cron)) { | |||
| cron = defaultCronExpression | |||
| } | |||
| scheduledM18Po = taskScheduler.schedule( | |||
| { | |||
| scheduleFunc() | |||
| }, | |||
| // Now Kotlin is happy because both the method and the function allow nulls | |||
| return taskScheduler.schedule( | |||
| { scheduleFunc() }, | |||
| CronTrigger(cron) | |||
| ) | |||
| } | |||
| // Init Scheduler | |||
| // @PostConstruct | |||
| @PostConstruct | |||
| fun init() { | |||
| // scheduleM18PoTask(); | |||
| //scheduleM18PoTask(); | |||
| scheduleM18Do1(); | |||
| scheduleM18Do2(); | |||
| scheduleM18MasterData(); | |||
| scheduleRoughProd(); | |||
| scheduleDetailedProd(); | |||
| //scheduleRoughProd(); | |||
| //scheduleDetailedProd(); | |||
| } | |||
| // Scheduler | |||
| // --------------------------- FP-MTMS --------------------------- // | |||
| fun scheduleRoughProd() { | |||
| commonSchedule(scheduledRoughProd, SettingNames.SCHEDULE_PROD_ROUGH, ::getRoughProdSchedule) | |||
| } | |||
| //fun scheduleRoughProd() { | |||
| // commonSchedule(scheduledRoughProd, SettingNames.SCHEDULE_PROD_ROUGH, ::getRoughProdSchedule) | |||
| //} | |||
| fun scheduleDetailedProd() { | |||
| commonSchedule(scheduledDetailedProd, SettingNames.SCHEDULE_PROD_DETAILED, ::getDetailedProdSchedule) | |||
| } | |||
| //fun scheduleDetailedProd() { | |||
| // commonSchedule(scheduledDetailedProd, SettingNames.SCHEDULE_PROD_DETAILED, ::getDetailedProdSchedule) | |||
| //} | |||
| // --------------------------- M18 --------------------------- // | |||
| fun scheduleM18PoTask() { | |||
| commonSchedule(scheduledM18Po, SettingNames.SCHEDULE_M18_PO, ::getM18Pos) | |||
| } | |||
| fun scheduleM18Do1() { | |||
| commonSchedule(scheduledM18Do1, SettingNames.SCHEDULE_M18_DO1, ::getM18Dos1) | |||
| } | |||
| fun scheduleM18Do2() { | |||
| commonSchedule(scheduledM18Do2, SettingNames.SCHEDULE_M18_DO2, ::getM18Dos2) | |||
| } | |||
| fun scheduleM18MasterData() { | |||
| commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData) | |||
| } | |||
| // Function for schedule | |||
| // --------------------------- FP-MTMS --------------------------- // | |||
| open fun getRoughProdSchedule() { | |||
| try { | |||
| logger.info("Daily Scheduler - Rough Prod") | |||
| @@ -123,7 +138,7 @@ open class SchedulerService( | |||
| throw RuntimeException("Error generate schedule: ${e.message}", e) | |||
| } | |||
| } | |||
| open fun getDetailedProdSchedule() { | |||
| try { | |||
| logger.info("Daily Scheduler - Detailed Prod") | |||
| @@ -145,7 +160,6 @@ open class SchedulerService( | |||
| throw RuntimeException("Error generate schedule: ${e.message}", e) | |||
| } | |||
| } | |||
| // --------------------------- M18 --------------------------- // | |||
| // @Async | |||
| // @Scheduled(cron = "0 0 2 * * *") // (SS/MM/HH/DD/MM/YY) | |||
| @@ -175,6 +189,46 @@ open class SchedulerService( | |||
| // logger.info("yesterday: ${yesterday.format(dataStringFormat)}") | |||
| } | |||
| open fun getM18Dos1() { | |||
| logger.info("DO Scheduler 1 - DO") | |||
| val currentTime = LocalDateTime.now() | |||
| val today = currentTime.toLocalDate().atStartOfDay() | |||
| val twoDaysLater = today.plusDays(2L) | |||
| var requestDO = M18CommonRequest( | |||
| dDateTo = twoDaysLater.format(dateTimeStringFormat), | |||
| dDateFrom = twoDaysLater.format(dateTimeStringFormat) | |||
| ) | |||
| m18DeliveryOrderService.saveDeliveryOrders(requestDO); | |||
| } | |||
| open fun getM18Dos2() { | |||
| logger.info("DO Scheduler 2 - DO") | |||
| val currentTime = LocalDateTime.now() | |||
| // .atStartOfDay() results in 00:00:00 | |||
| val today = currentTime.toLocalDate().atStartOfDay() | |||
| val ysd = today.minusDays(1L) | |||
| val tmr = today.plusDays(1L) | |||
| // Set to 19:00:00 of yesterday | |||
| val ysdNight = ysd.withHour(19).withMinute(0).withSecond(0) | |||
| // Set to 11:00:00 of today | |||
| val todayEleven = today.withHour(11).withMinute(0).withSecond(0) | |||
| val requestDO = M18CommonRequest( | |||
| // These will now produce "yyyy-MM-dd HH:mm:ss" | |||
| dDateTo = tmr.format(dateTimeStringFormat), // e.g. 2026-01-19 00:00:00 | |||
| dDateFrom = tmr.format(dateTimeStringFormat), // e.g. 2026-01-19 00:00:00 | |||
| modifiedDateTo = todayEleven.format(dateTimeStringFormat), // 2026-01-18 11:00:00 | |||
| modifiedDateFrom = ysdNight.format(dateTimeStringFormat) // 2026-01-17 19:00:00 | |||
| ) | |||
| m18DeliveryOrderService.saveDeliveryOrders(requestDO) | |||
| } | |||
| open fun getM18MasterData() { | |||
| logger.info("Daily Scheduler - Master Data") | |||
| val currentTime = LocalDateTime.now() | |||
| @@ -4,7 +4,6 @@ import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest | |||
| import org.springframework.stereotype.Service | |||
| import java.awt.Color | |||
| import java.awt.Font | |||
| import java.awt.Graphics2D | |||
| @@ -24,112 +23,117 @@ import java.nio.charset.Charset | |||
| import java.io.BufferedReader | |||
| import java.io.InputStreamReader | |||
| import java.net.ConnectException | |||
| import java.net.SocketTimeoutException | |||
| // 1. Data class to store both the raw bytes and the width for XML injection | |||
| // Data class to store bitmap bytes + width (for XML) | |||
| data class BitmapResult(val bytes: ByteArray, val width: Int) | |||
| @Service | |||
| open class PlasticBagPrinterService( | |||
| val jobOrderRepository: JobOrderRepository, | |||
| ) { | |||
| fun generatePrintJobBundle(itemCode: String, lotNo: String, expiryDate: String, productName: String): ByteArray { | |||
| fun generatePrintJobBundle( | |||
| itemCode: String, | |||
| lotNo: String, | |||
| expiryDate: String, | |||
| productName: String | |||
| ): ByteArray { | |||
| val baos = ByteArrayOutputStream() | |||
| ZipOutputStream(baos).use { zos -> | |||
| // Generate Bitmaps and capture their widths | |||
| val productBmp = createMonochromeBitmap(productName, 704) | |||
| val codeBmp = createMonochromeBitmap(itemCode, 1173) | |||
| val expBmp = createMonochromeBitmap(expiryDate, 1173) | |||
| val qrBmp = createQrCodeBitmap("$itemCode|$lotNo|$expiryDate", 1000) | |||
| // Reduced sizes to make files smaller while keeping readability/scanability | |||
| val productBmp = createMonochromeBitmap(productName, 520) // was 704 | |||
| val codeBmp = createMonochromeBitmap(itemCode, 900) // was 1173 | |||
| val expBmp = createMonochromeBitmap(expiryDate, 900) // was 1173 | |||
| val qrBmp = createQrCodeBitmap("$itemCode|$lotNo|$expiryDate", 450) // was 1000 | |||
| val nameFile = "${lotNo}_product.bmp" | |||
| val codeFile = "${lotNo}_code.bmp" | |||
| val expFile = "${lotNo}_expiry.bmp" | |||
| val qrFile = "${lotNo}_qr.bmp" | |||
| val expFile = "${lotNo}_expiry.bmp" | |||
| val qrFile = "${lotNo}_qr.bmp" | |||
| // Now "bytes" property is resolved | |||
| addToZip(zos, nameFile, productBmp.bytes) | |||
| addToZip(zos, codeFile, codeBmp.bytes) | |||
| addToZip(zos, expFile, expBmp.bytes) | |||
| addToZip(zos, qrFile, qrBmp.bytes) | |||
| // 2. Generate the .image file with XML headers and mandatory X40 tags | |||
| val imageXml = """ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <Legend type="Image.V1" PaperColor="White" BackgroundColor="DarkGray"> | |||
| <DeviceInfo DeviceURI="NGEDriver[SDX40c.cadb94e3-677c-4bcb-9c80-88bf9526d6fe]/"><DeviceName /></DeviceInfo> | |||
| <Mirrored>false</Mirrored> | |||
| <Orientation>DEG_0</Orientation> | |||
| <Width>5300</Width><Height>7100</Height> | |||
| <FieldList> | |||
| <Logo type="Logo.V1"> | |||
| <Name>LOGO</Name> | |||
| <Geometry><X>0</X><Y>250</Y></Geometry> | |||
| <FieldColor>BLACK</FieldColor> | |||
| <FileName>$nameFile</FileName> | |||
| <Width>${productBmp.width}</Width> | |||
| <Height>704</Height> | |||
| </Logo> | |||
| <Logo type="Logo.V1"> | |||
| <Name>LOGO_2</Name> | |||
| <Geometry><X>1000</X><Y>1250</Y></Geometry> | |||
| <FieldColor>BLACK</FieldColor> | |||
| <FileName>$codeFile</FileName> | |||
| <Width>${codeBmp.width}</Width> | |||
| <Height>1173</Height> | |||
| </Logo> | |||
| <Logo type="Logo.V1"> | |||
| <Name>LOGO_3</Name> | |||
| <Geometry><X>500</X><Y>2500</Y></Geometry> | |||
| <FieldColor>BLACK</FieldColor> | |||
| <FileName>$expFile</FileName> | |||
| <Width>${expBmp.width}</Width> | |||
| <Height>1173</Height> | |||
| </Logo> | |||
| <Logo type="Logo.V1"> | |||
| <Name>LOGO_4</Name> | |||
| <Geometry><X>750</X><Y>3750</Y></Geometry> | |||
| <FieldColor>BLACK</FieldColor> | |||
| <FileName>$qrFile</FileName> | |||
| <Width>${qrBmp.width}</Width> | |||
| <Height>1000</Height> | |||
| </Logo> | |||
| </FieldList> | |||
| </Legend> | |||
| """.trimIndent() | |||
| addToZip(zos, expFile, expBmp.bytes) | |||
| addToZip(zos, qrFile, qrBmp.bytes) | |||
| // Minified XML - no indentation/newlines between tags | |||
| val imageXml = """<?xml version="1.0" encoding="utf-8"?><Legend type="Image.V1" PaperColor="White" BackgroundColor="DarkGray"><DeviceInfo DeviceURI="NGEDriver[SDX40c.cadb94e3-677c-4bcb-9c80-88bf9526d6fe]/"><DeviceName/></DeviceInfo><Mirrored>false</Mirrored><Orientation>DEG_0</Orientation><Width>5300</Width><Height>7100</Height><FieldList><Logo type="Logo.V1"><Name>LOGO</Name><Geometry><X>0</X><Y>250</Y></Geometry><FieldColor>BLACK</FieldColor><FileName>$nameFile</FileName><Width>${productBmp.width}</Width><Height>520</Height></Logo><Logo type="Logo.V1"><Name>LOGO_2</Name><Geometry><X>1000</X><Y>1250</Y></Geometry><FieldColor>BLACK</FieldColor><FileName>$codeFile</FileName><Width>${codeBmp.width}</Width><Height>900</Height></Logo><Logo type="Logo.V1"><Name>LOGO_3</Name><Geometry><X>500</X><Y>2500</Y></Geometry><FieldColor>BLACK</FieldColor><FileName>$expFile</FileName><Width>${expBmp.width}</Width><Height>900</Height></Logo><Logo type="Logo.V1"><Name>LOGO_4</Name><Geometry><X>750</X><Y>3750</Y></Geometry><FieldColor>BLACK</FieldColor><FileName>$qrFile</FileName><Width>${qrBmp.width}</Width><Height>450</Height></Logo></FieldList></Legend>""".trimIndent() | |||
| addToZip(zos, "$lotNo.image", imageXml.toByteArray(Charsets.UTF_8)) | |||
| val jobXml = """<?xml version="1.0" encoding="utf-8"?><Job><ImageFileName>$lotNo.image</ImageFileName></Job>""" | |||
| addToZip(zos, "$lotNo.job", jobXml.toByteArray(Charsets.UTF_8)) | |||
| } | |||
| return baos.toByteArray() | |||
| } | |||
| private fun createMonochromeBitmap(text: String, maxHeight: Int): BitmapResult { | |||
| private fun createMonochromeBitmap(text: String, targetHeight: Int): BitmapResult { | |||
| // Step 1: Measure text width with temporary image | |||
| val tempImg = BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY) | |||
| val g2dTemp = tempImg.createGraphics() | |||
| val font = Font("SimSun", Font.BOLD, (maxHeight * 0.8).toInt()) | |||
| val font = Font("SimSun", Font.BOLD, (targetHeight * 0.92).toInt()) | |||
| g2dTemp.font = font | |||
| val metrics = g2dTemp.getFontMetrics(font) | |||
| val width = metrics.stringWidth(text).let { if (it < 1) 1 else it } | |||
| val height = maxHeight | |||
| val textWidth = metrics.stringWidth(text).coerceAtLeast(1) | |||
| g2dTemp.dispose() | |||
| val finalImg = BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY) | |||
| val g2d = finalImg.createGraphics() | |||
| g2d.color = Color.WHITE | |||
| g2d.fillRect(0, 0, width, height) | |||
| g2d.color = Color.BLACK | |||
| g2d.font = font | |||
| g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF) | |||
| g2d.drawString(text, 0, metrics.ascent) | |||
| g2d.dispose() | |||
| // Step 2: Create actual image | |||
| // CHANGE 1: Changed 'val' to 'var' so we can reassign it on line 84 | |||
| var img = BufferedImage(textWidth, targetHeight, BufferedImage.TYPE_BYTE_BINARY) | |||
| // CHANGE 2: Removed 'val g2d =' because it was unused | |||
| img.createGraphics().apply { | |||
| color = Color.WHITE | |||
| fillRect(0, 0, textWidth, targetHeight) | |||
| color = Color.BLACK | |||
| this.font = font // use 'this.font' to be explicit inside apply | |||
| setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF) | |||
| drawString(text, 0, metrics.ascent) | |||
| dispose() | |||
| } | |||
| // Step 3: Crop excess white space | |||
| // This line (84) will now work because 'img' is a 'var' | |||
| img = cropToContent(img) | |||
| val baos = ByteArrayOutputStream() | |||
| ImageIO.write(finalImg, "bmp", baos) | |||
| return BitmapResult(baos.toByteArray(), width) | |||
| ImageIO.write(img, "bmp", baos) | |||
| return BitmapResult(baos.toByteArray(), img.width) | |||
| } | |||
| fun testSmartDateX40Tcp(printerIp: String, port: Int = 3001, timeoutMs: Int = 3000): Pair<Boolean, String> { | |||
| return try { | |||
| Socket().use { socket -> | |||
| socket.connect(InetSocketAddress(printerIp, port), timeoutMs) | |||
| socket.soTimeout = 2000 // read timeout | |||
| val output = PrintWriter(socket.getOutputStream(), true) | |||
| output.println("~VR") // or any simple command your printer understands | |||
| output.flush() | |||
| val input = BufferedReader(InputStreamReader(socket.getInputStream())) | |||
| val response = try { | |||
| input.readLine() ?: "No response line" | |||
| } catch (e: java.net.SocketTimeoutException) { | |||
| "No response within timeout" | |||
| } | |||
| if (response.isNotBlank() && response != "No response within timeout") { | |||
| true to "Connected successfully. Response: $response" | |||
| } else { | |||
| true to "Connected (port open), but no meaningful response" | |||
| } | |||
| } | |||
| } catch (e: SocketTimeoutException) { | |||
| false to "Connection timed out (port may be closed or printer unreachable)" | |||
| } catch (e: ConnectException) { | |||
| false to "Connection refused (port closed or wrong IP)" | |||
| } catch (e: Exception) { | |||
| false to "Test failed: ${e.javaClass.simpleName} - ${e.message}" | |||
| } | |||
| } | |||
| private fun createQrCodeBitmap(content: String, size: Int): BitmapResult { | |||
| @@ -141,10 +145,43 @@ open class PlasticBagPrinterService( | |||
| image.setRGB(x, y, if (bitMatrix.get(x, y)) Color.BLACK.rgb else Color.WHITE.rgb) | |||
| } | |||
| } | |||
| // Crop QR code (removes quiet zone excess if any) | |||
| val cropped = cropToContent(image) | |||
| val baos = ByteArrayOutputStream() | |||
| ImageIO.write(image, "bmp", baos) | |||
| // For QR code, width and height are the same (size) | |||
| return BitmapResult(baos.toByteArray(), size) | |||
| ImageIO.write(cropped, "bmp", baos) | |||
| return BitmapResult(baos.toByteArray(), cropped.width) | |||
| } | |||
| private fun cropToContent(img: BufferedImage): BufferedImage { | |||
| val width = img.width | |||
| val height = img.height | |||
| var minX = width | |||
| var minY = height | |||
| var maxX = 0 | |||
| var maxY = 0 | |||
| for (x in 0 until width) { | |||
| for (y in 0 until height) { | |||
| if (img.getRGB(x, y) == Color.BLACK.rgb) { | |||
| minX = minOf(minX, x) | |||
| minY = minOf(minY, y) | |||
| maxX = maxOf(maxX, x) | |||
| maxY = maxOf(maxY, y) | |||
| } | |||
| } | |||
| } | |||
| if (minX > maxX || minY > maxY) return img // empty image fallback | |||
| val padding = 4 | |||
| val cropX = (minX - padding).coerceAtLeast(0) | |||
| val cropY = (minY - padding).coerceAtLeast(0) | |||
| val cropW = (maxX - minX + padding * 2).coerceAtMost(width - cropX) | |||
| val cropH = (maxY - minY + padding * 2).coerceAtMost(height - cropY) | |||
| return img.getSubimage(cropX, cropY, cropW, cropH) | |||
| } | |||
| private fun addToZip(zos: ZipOutputStream, entryName: String, content: ByteArray) { | |||
| @@ -153,30 +190,35 @@ open class PlasticBagPrinterService( | |||
| zos.write(content) | |||
| zos.closeEntry() | |||
| } | |||
| // ──────────────────────────────────────────────── | |||
| // The rest of your printer communication methods remain unchanged | |||
| // ──────────────────────────────────────────────── | |||
| fun sendDataFlexJob(request: PrintRequest) { | |||
| Socket().use { socket -> | |||
| try { | |||
| socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 3000) | |||
| val writer = PrintWriter(socket.getOutputStream(), true) | |||
| // Videojet/DataFlex ASCII protocol | |||
| // We assign the Lot number to both the text variable and the barcode variable | |||
| val command = """ | |||
| ^SVAR1|${request.itemCode} | |||
| ^SVAR2|${request.itemName} | |||
| ^SVAR3|${request.lotNo} | |||
| ^SVAR4|${request.expiryDate} | |||
| ^SVAR_QR|${request.lotNo} | |||
| ^PRNT1 | |||
| """.trimIndent() | |||
| writer.println(command) | |||
| socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 5000) | |||
| val output = socket.getOutputStream() | |||
| val writer = PrintWriter(output, true) | |||
| val command = buildString { | |||
| append("\u0002") // <STX> | |||
| append("<ESC>JI;YOUR_JOB_NAME\r\n") | |||
| append("<ESC>VB;ItemCode;${request.itemCode}\r\n") | |||
| append("<ESC>VB;ItemName;${request.itemName}\r\n") | |||
| append("<ESC>VB;LotNo;${request.lotNo}\r\n") | |||
| append("<ESC>VB;ExpiryDate;${request.expiryDate}\r\n") | |||
| append("<ESC>VB;QRContent;lot=${request.lotNo}&item=${request.itemCode}\r\n") | |||
| append("<ESC>P;0\r\n") | |||
| append("\u0003") // <ETX> | |||
| } | |||
| writer.print(command) | |||
| writer.flush() | |||
| println("DataFlex: Print job with QR data sent to ${request.printerIp}") | |||
| println("DataFlex: Print job sent to ${request.printerIp}:${request.printerPort}") | |||
| } catch (e: Exception) { | |||
| throw RuntimeException("DataFlex Error: ${e.message}") | |||
| throw RuntimeException("DataFlex communication failed: ${e.message}", e) | |||
| } | |||
| } | |||
| } | |||
| @@ -185,46 +227,20 @@ open class PlasticBagPrinterService( | |||
| Socket().use { socket -> | |||
| socket.connect(InetSocketAddress(request.printerIp, request.printerPort.toInt()), 3000) | |||
| val writer = PrintWriter(socket.getOutputStream(), true) | |||
| // Standard HANS/General Laser Command Format: | |||
| // [STX]Command|Variable1|Variable2[ETX] | |||
| // Note: Exact protocol depends on your HANS controller software (usually JCZ or similar) | |||
| val command = "STRSET|${request.templateId}|LOT=${request.lotNo}|EXP=${request.expiryDate}\n" | |||
| writer.print(command) | |||
| writer.flush() | |||
| // Optional: Trigger the marking immediately | |||
| // writer.print("STRMARK\n") | |||
| // writer.flush() | |||
| } | |||
| } | |||
| /* | |||
| fun previewLaser(request: LaserRequest) { | |||
| Socket().use { socket -> | |||
| socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 2000) | |||
| val writer = PrintWriter(socket.getOutputStream(), true) | |||
| // Typical HANS command for Red Light Preview | |||
| writer.println("JOBLOAD|${request.templateId}") | |||
| writer.println("REDLIGHT|1") // 1 to turn on, 0 to turn off | |||
| writer.flush() | |||
| } | |||
| } */ | |||
| fun sendLaserPreview(request: LaserRequest) { | |||
| Socket().use { socket -> | |||
| socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 3000) | |||
| val writer = PrintWriter(socket.getOutputStream(), true) | |||
| // HANS Protocol for Red Light | |||
| // Often requires loading the job first so it knows the boundary | |||
| val command = """ | |||
| JOBLOAD|${request.templateId} | |||
| REDLIGHT|1 | |||
| """.trimIndent() | |||
| writer.println(command) | |||
| writer.flush() | |||
| } | |||
| @@ -236,38 +252,25 @@ open class PlasticBagPrinterService( | |||
| socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 3000) | |||
| val out = DataOutputStream(socket.getOutputStream()) | |||
| // 203 DPI: 8 dots = 1mm | |||
| val tspl = StringBuilder() | |||
| .append("SIZE 100 mm, 50 mm\n") | |||
| .append("GAP 3 mm, 0 mm\n") | |||
| .append("DIRECTION 1\n") | |||
| .append("CLS\n") | |||
| // --- Text Content --- | |||
| .append("TEXT 50,40,\"ROMAN.TTF\",0,1,1,\"ITEM: ${request.itemCode}\"\n") | |||
| .append("TEXT 50,80,\"ROMAN.TTF\",0,1,1,\"NAME: ${request.itemName}\"\n") | |||
| .append("TEXT 50,120,\"ROMAN.TTF\",0,1,1,\"EXP: ${request.expiryDate}\"\n") | |||
| .append("TEXT 50,160,\"ROMAN.TTF\",0,1,1,\"LOT: ${request.lotNo}\"\n") | |||
| // --- QR Code for Lot Number --- | |||
| // Syntax: QRCODE x, y, ECC, cell_width, mode, rotation, "content" | |||
| // ECC Level M (Standard), Cell width 4 (size), Auto mode, 0 rotation | |||
| .append("QRCODE 450,40,M,4,A,0,\"${request.lotNo}\"\n") | |||
| // Optional: Label for the QR Code | |||
| .append("TEXT 450,180,\"ROMAN.TTF\",0,1,1,\"SCAN LOT\"\n") | |||
| .append("PRINT 1,1\n") | |||
| .toString() | |||
| // Use TIS-620 for Thai support or Windows-1252 for English | |||
| val bytes = tspl.toByteArray(Charset.forName("TIS-620")) | |||
| out.write(bytes) | |||
| out.flush() | |||
| println("TSC: Print job with QR code sent to ${request.printerIp}") | |||
| println("TSC: Print job sent to ${request.printerIp}") | |||
| } catch (e: Exception) { | |||
| println("TSC Error: ${e.message}") | |||
| throw RuntimeException("TSC Printer Error: ${e.message}") | |||
| } | |||
| } | |||
| @@ -335,5 +338,4 @@ open class PlasticBagPrinterService( | |||
| println("Han's Laser TCP 連接已關閉") | |||
| } | |||
| } | |||
| } | |||
| @@ -88,6 +88,19 @@ class PlasticBagPrinterController( | |||
| } | |||
| } | |||
| @GetMapping("/test-x40") | |||
| fun testX40( | |||
| @RequestParam printerIp: String, | |||
| @RequestParam(defaultValue = "3001") port: Int | |||
| ): ResponseEntity<String> { | |||
| val (success, message) = plasticBagPrinterService.testSmartDateX40Tcp(printerIp, port) | |||
| return if (success) { | |||
| ResponseEntity.ok("OK - $message") | |||
| } else { | |||
| ResponseEntity.status(503).body("FAIL - $message") | |||
| } | |||
| } | |||
| @PostMapping("/print-laser-tcp") | |||
| fun printLaserTcp(@RequestBody request: Laser2Request): ResponseEntity<String> { | |||
| return try { | |||
| @@ -628,19 +628,20 @@ open class ProductionScheduleService( | |||
| i.outputQty, | |||
| i.avgQtyLastMonth, | |||
| (i.onHandQty -500), | |||
| (i.onHandQty -500) * 1.0 / i.avgQtyLastMonth as daysLeft, | |||
| -- (i.onHandQty -500) * 1.0 / i.avgQtyLastMonth as daysLeft, | |||
| i.avgQtyLastMonth * 2.6 - stockQty as needQty, | |||
| i.stockQty, | |||
| CASE | |||
| WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN | |||
| CEIL((i.avgQtyLastMonth * 2.6 - stockQty) / i.outputQty) | |||
| ELSE 0 | |||
| END AS needNoOfJobOrder, | |||
| CASE | |||
| WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN | |||
| CEIL((i.avgQtyLastMonth * 2.6 - stockQty) / i.outputQty) | |||
| ELSE 0 | |||
| END AS batchNeed, | |||
| -- CASE | |||
| -- WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN | |||
| -- CEIL((i.avgQtyLastMonth * 2.6 - stockQty) / i.outputQty) | |||
| -- ELSE 0 | |||
| -- END AS needNoOfJobOrder, | |||
| -- The first date only consider how many batch pending for job order | |||
| 0 AS needNoOfJobOrder, | |||
| 0 AS batchNeed, | |||
| i.pendingJobQty, | |||
| ((i.stockQty * 1.0) + (i.needNoOfJobOrder * i.outputQty) ) / i.avgQtyLastMonth as daysLeft, | |||
| 25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority, | |||
| i.* | |||
| FROM | |||
| @@ -658,6 +659,9 @@ open class ProductionScheduleService( | |||
| -- AND MONTH(do.estimatedArrivalDate) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH)) | |||
| AND do.estimatedArrivalDate >= '2025-12-01' AND do.estimatedArrivalDate < '2025-12-11' | |||
| GROUP BY do.estimatedArrivalDate) AS d) AS avgQtyLastMonth, | |||
| (select sum(reqQty) from job_order where bomId = bom.id and status != 'completed') AS pendingJobQty, | |||
| inventory.onHandQty - 500 AS stockQty, | |||
| bom.outputQty, | |||
| bom.outputQtyUom, | |||
| @@ -0,0 +1,16 @@ | |||
| --liquibase formatted sql | |||
| --changeset author:add_scheduler | |||
| INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`) | |||
| SELECT 'SCHEDULE.m18.do1', '0 0 19 * * *', 'SCHEDULE', 'string' | |||
| FROM DUAL | |||
| WHERE NOT EXISTS ( | |||
| SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'SCHEDULE.m18.do1' | |||
| ); | |||
| INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`) | |||
| SELECT 'SCHEDULE.m18.do2', '0 0 11 * * *', 'SCHEDULE', 'string' | |||
| FROM DUAL | |||
| WHERE NOT EXISTS ( | |||
| SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'SCHEDULE.m18.do2' | |||
| ); | |||
| @@ -1,6 +1,6 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <!-- Created with Jaspersoft Studio version 6.17.0.final using JasperReports Library version 6.17.0-6d93193241dd8cc42629e188b94f9e0bc5722efd --> | |||
| <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="FGStockOutTraceabilityReport" pageWidth="842" pageHeight="595" orientation="Landscape" columnWidth="802" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="c40e235b-365d-48a0-a1b8-a8e0ece2cdd3"> | |||
| <!-- Created with Jaspersoft Studio version 6.21.3.final using JasperReports Library version 5.5.2 --> | |||
| <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="FGStockOutTraceabilityReport" pageWidth="842" pageHeight="595" orientation="Landscape" whenNoDataType="AllSectionsNoDetail" columnWidth="802" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="c40e235b-365d-48a0-a1b8-a8e0ece2cdd3"> | |||
| <property name="com.jaspersoft.studio.unit." value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/> | |||
| @@ -310,7 +310,7 @@ | |||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement textAlignment="Center" verticalAlignment="Justified"> | |||
| <textElement textAlignment="Center" verticalAlignment="Top"> | |||
| <font fontName="微軟正黑體" size="12"/> | |||
| </textElement> | |||
| <text><![CDATA[/]]></text> | |||