From bea0b2c2fe7f0f3e9aea786edca3eb9327800138 Mon Sep 17 00:00:00 2001 From: "DESKTOP-064TTA1\\Fai LUK" Date: Fri, 27 Mar 2026 15:51:09 +0800 Subject: [PATCH] make m18 syn by code appear in menu for PO/DO/Product --- .../fpsms/m18/service/M18MasterDataService.kt | 39 ++++++++ .../ffii/fpsms/m18/web/M18TestController.kt | 5 + .../scheduler/LaserBag2AutoSendScheduler.kt | 42 +++++++++ .../service/LaserBag2AutoSendService.kt | 93 +++++++++++++++++++ .../web/PlasticBagPrinterController.kt | 21 +++++ .../web/model/LaserBag2AutoSendReport.kt | 18 ++++ src/main/resources/application.yml | 9 ++ 7 files changed, 227 insertions(+) create mode 100644 src/main/java/com/ffii/fpsms/modules/jobOrder/scheduler/LaserBag2AutoSendScheduler.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt index 28ba99c..3b01ec7 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt @@ -303,6 +303,45 @@ open class M18MasterDataService( } } + /** Sync one product/material from M18 by item code (search list, then load line — same idea as PO/DO by code). */ + open fun saveProductByCode(code: String): SyncResult { + val trimmed = code.trim() + if (trimmed.isEmpty()) { + return SyncResult(totalProcessed = 1, totalSuccess = 0, totalFail = 1, query = "empty code") + } + ensureCunitSeededForAllIfEmpty() + val fromLocal = itemsService.findByCode(trimmed)?.m18Id + val m18Id = fromLocal ?: run { + val conds = "(code=equal=$trimmed)" + val listResponse = try { + getList( + stSearch = StSearchType.PRODUCT.value, + params = null, + conds = conds, + request = M18CommonRequest(), + ) + } catch (e: Exception) { + logger.error("(saveProductByCode) M18 search failed: ${e.message}", e) + null + } + listResponse?.values?.firstOrNull()?.id + } + if (m18Id == null) { + return SyncResult( + totalProcessed = 1, + totalSuccess = 0, + totalFail = 1, + query = "code=equal=$trimmed", + ) + } + val result = saveProduct(m18Id) + return if (result != null) { + SyncResult(totalProcessed = 1, totalSuccess = 1, totalFail = 0, query = "code=equal=$trimmed") + } else { + SyncResult(totalProcessed = 1, totalSuccess = 0, totalFail = 1, query = "code=equal=$trimmed") + } + } + open fun saveProducts(request: M18CommonRequest): SyncResult { logger.info("--------------------------------------------Start - Saving M18 Products / Materials--------------------------------------------") ensureCunitSeededForAllIfEmpty() diff --git a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt index 9999a79..44922b4 100644 --- a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt +++ b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt @@ -74,6 +74,11 @@ class M18TestController ( fun testSyncDoByCode(@RequestParam code: String): SyncResult { return m18DeliveryOrderService.saveDeliveryOrderByCode(code) } + + @GetMapping("/test/product-by-code") + fun testSyncProductByCode(@RequestParam code: String): SyncResult { + return m18MasterDataService.saveProductByCode(code) + } // --------------------------------------------- Scheduler --------------------------------------------- /// // @GetMapping("/schedule/po") // fun schedulePo(@RequestParam @Valid newCron: String) { diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/scheduler/LaserBag2AutoSendScheduler.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/scheduler/LaserBag2AutoSendScheduler.kt new file mode 100644 index 0000000..b129781 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/scheduler/LaserBag2AutoSendScheduler.kt @@ -0,0 +1,42 @@ +package com.ffii.fpsms.modules.jobOrder.scheduler + +import com.ffii.fpsms.modules.jobOrder.service.LaserBag2AutoSendService +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import java.time.LocalDate + +/** + * Periodically runs the same laser TCP send as /laserPrint (DB LASER_PRINT.host / port / itemCodes). + * Disabled by default; set laser.bag2.auto-send.enabled=true. + */ +@Component +class LaserBag2AutoSendScheduler( + private val laserBag2AutoSendService: LaserBag2AutoSendService, + @Value("\${laser.bag2.auto-send.enabled:false}") private val enabled: Boolean, + @Value("\${laser.bag2.auto-send.limit-per-run:1}") private val limitPerRun: Int, +) { + private val logger = LoggerFactory.getLogger(javaClass) + + @Scheduled(fixedRateString = "\${laser.bag2.auto-send.interval-ms:60000}") + fun tick() { + if (!enabled) { + return + } + try { + val report = laserBag2AutoSendService.runAutoSend( + planStart = LocalDate.now(), + limitPerRun = limitPerRun, + ) + logger.info( + "Laser Bag2 scheduler: processed {}/{} job orders for {}", + report.jobOrdersProcessed, + report.jobOrdersFound, + report.planStart, + ) + } catch (e: Exception) { + logger.error("Laser Bag2 scheduler failed", e) + } + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt new file mode 100644 index 0000000..002bd9e --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt @@ -0,0 +1,93 @@ +package com.ffii.fpsms.modules.jobOrder.service + +import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2AutoSendReport +import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2JobSendResult +import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendRequest +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate + +/** + * Finds packaging job orders for [planStart] using the same filter as [PlasticBagPrinterService.listLaserPrintJobOrders] + * ([LASER_PRINT.itemCodes]), then sends Bag2-style laser TCP payloads via [PlasticBagPrinterService.sendLaserBag2Job], + * which uses [com.ffii.fpsms.modules.common.SettingNames.LASER_PRINT_HOST] / [LASER_PRINT_PORT] from the database. + * + * Matches /laserPrint row click: [sendsPerJob] rounds with [delayBetweenSendsMs] between rounds (default 3 × 3s like the frontend). + */ +@Service +class LaserBag2AutoSendService( + private val plasticBagPrinterService: PlasticBagPrinterService, +) { + private val logger = LoggerFactory.getLogger(javaClass) + + companion object { + /** Same as LaserPrint page (3 sends per row click). */ + const val DEFAULT_SENDS_PER_JOB = 3 + const val DEFAULT_DELAY_BETWEEN_SENDS_MS = 3000L + } + + fun runAutoSend( + planStart: LocalDate, + limitPerRun: Int = 0, + sendsPerJob: Int = DEFAULT_SENDS_PER_JOB, + delayBetweenSendsMs: Long = DEFAULT_DELAY_BETWEEN_SENDS_MS, + ): LaserBag2AutoSendReport { + val orders = plasticBagPrinterService.listLaserPrintJobOrders(planStart) + val toProcess = if (limitPerRun > 0) orders.take(limitPerRun) else orders + val results = mutableListOf() + + logger.info( + "Laser Bag2 auto-send: planStart={}, found={}, processing={}, sendsPerJob={}", + planStart, + orders.size, + toProcess.size, + sendsPerJob, + ) + + for (jo in toProcess) { + var lastMsg = "" + var overallOk = true + for (attempt in 1..sendsPerJob) { + val resp = plasticBagPrinterService.sendLaserBag2Job( + LaserBag2SendRequest( + itemId = jo.itemId, + stockInLineId = jo.stockInLineId, + itemCode = jo.itemCode, + itemName = jo.itemName, + ), + ) + lastMsg = resp.message + if (!resp.success) { + overallOk = false + logger.warn("Laser send failed jobOrderId={} attempt={}: {}", jo.id, attempt, resp.message) + break + } + if (attempt < sendsPerJob) { + try { + Thread.sleep(delayBetweenSendsMs) + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + overallOk = false + lastMsg = "Interrupted" + break + } + } + } + results.add( + LaserBag2JobSendResult( + jobOrderId = jo.id, + itemCode = jo.itemCode, + success = overallOk, + message = lastMsg, + ), + ) + } + + return LaserBag2AutoSendReport( + planStart = planStart, + jobOrdersFound = orders.size, + jobOrdersProcessed = toProcess.size, + results = results, + ) + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt index 8d99774..101f295 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt @@ -1,9 +1,11 @@ package com.ffii.fpsms.modules.jobOrder.web +import com.ffii.fpsms.modules.jobOrder.service.LaserBag2AutoSendService import com.ffii.fpsms.modules.jobOrder.service.PlasticBagPrinterService import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest import com.ffii.fpsms.modules.jobOrder.web.model.Laser2Request +import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2AutoSendReport import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendRequest import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SettingsResponse @@ -25,6 +27,7 @@ import org.slf4j.LoggerFactory @RequestMapping("/plastic") class PlasticBagPrinterController( private val plasticBagPrinterService: PlasticBagPrinterService, + private val laserBag2AutoSendService: LaserBag2AutoSendService, ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -59,6 +62,24 @@ class PlasticBagPrinterController( } } + /** + * Same as /laserPrint row workflow: list job orders for [planStart] filtered by LASER_PRINT.itemCodes, + * then for each (up to [limitPerRun], 0 = all) send laser TCP commands using LASER_PRINT.host/port (3× with 3s gap per job). + * For manual runs from /testing; scheduler uses [LaserBag2AutoSendScheduler]. + */ + @PostMapping("/laser-bag2-auto-send") + fun runLaserBag2AutoSend( + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) planStart: LocalDate?, + @RequestParam(required = false, defaultValue = "0") limitPerRun: Int, + ): ResponseEntity { + val date = planStart ?: LocalDate.now() + val report = laserBag2AutoSendService.runAutoSend( + planStart = date, + limitPerRun = limitPerRun, + ) + return ResponseEntity.ok(report) + } + @PostMapping("/check-printer") fun checkPrinter(@RequestBody request: PrinterStatusRequest): ResponseEntity { val (connected, message) = plasticBagPrinterService.checkPrinterConnection( diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt new file mode 100644 index 0000000..e27a108 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt @@ -0,0 +1,18 @@ +package com.ffii.fpsms.modules.jobOrder.web.model + +import java.time.LocalDate + +/** Result of [com.ffii.fpsms.modules.jobOrder.service.LaserBag2AutoSendService.runAutoSend]. */ +data class LaserBag2AutoSendReport( + val planStart: LocalDate, + val jobOrdersFound: Int, + val jobOrdersProcessed: Int, + val results: List, +) + +data class LaserBag2JobSendResult( + val jobOrderId: Long, + val itemCode: String?, + val success: Boolean, + val message: String, +) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 56a7a83..f6d66b6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -55,6 +55,15 @@ logging: ngpcl: push-url: ${NGPCL_PUSH_URL:} +# Laser Bag2 (/laserPrint) auto-send: same as listing + TCP send using DB LASER_PRINT.host / port / itemCodes. +# Scheduler is off by default. limit-per-run: max job orders per tick (1 = first matching only); 0 = all matches (heavy). +laser: + bag2: + auto-send: + enabled: false + interval-ms: 60000 + limit-per-run: 1 + bom: import: temp-dir: ${java.io.tmpdir}/fpsms-bom-import