| @@ -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<M18ProductListResponse>( | |||||
| 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 { | open fun saveProducts(request: M18CommonRequest): SyncResult { | ||||
| logger.info("--------------------------------------------Start - Saving M18 Products / Materials--------------------------------------------") | logger.info("--------------------------------------------Start - Saving M18 Products / Materials--------------------------------------------") | ||||
| ensureCunitSeededForAllIfEmpty() | ensureCunitSeededForAllIfEmpty() | ||||
| @@ -74,6 +74,11 @@ class M18TestController ( | |||||
| fun testSyncDoByCode(@RequestParam code: String): SyncResult { | fun testSyncDoByCode(@RequestParam code: String): SyncResult { | ||||
| return m18DeliveryOrderService.saveDeliveryOrderByCode(code) | return m18DeliveryOrderService.saveDeliveryOrderByCode(code) | ||||
| } | } | ||||
| @GetMapping("/test/product-by-code") | |||||
| fun testSyncProductByCode(@RequestParam code: String): SyncResult { | |||||
| return m18MasterDataService.saveProductByCode(code) | |||||
| } | |||||
| // --------------------------------------------- Scheduler --------------------------------------------- /// | // --------------------------------------------- Scheduler --------------------------------------------- /// | ||||
| // @GetMapping("/schedule/po") | // @GetMapping("/schedule/po") | ||||
| // fun schedulePo(@RequestParam @Valid newCron: String) { | // fun schedulePo(@RequestParam @Valid newCron: String) { | ||||
| @@ -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) | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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<LaserBag2JobSendResult>() | |||||
| 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, | |||||
| ) | |||||
| } | |||||
| } | |||||
| @@ -1,9 +1,11 @@ | |||||
| package com.ffii.fpsms.modules.jobOrder.web | 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.service.PlasticBagPrinterService | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest | 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.LaserRequest | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.Laser2Request | 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.LaserBag2SendRequest | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse | import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SettingsResponse | import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SettingsResponse | ||||
| @@ -25,6 +27,7 @@ import org.slf4j.LoggerFactory | |||||
| @RequestMapping("/plastic") | @RequestMapping("/plastic") | ||||
| class PlasticBagPrinterController( | class PlasticBagPrinterController( | ||||
| private val plasticBagPrinterService: PlasticBagPrinterService, | private val plasticBagPrinterService: PlasticBagPrinterService, | ||||
| private val laserBag2AutoSendService: LaserBag2AutoSendService, | |||||
| ) { | ) { | ||||
| private val logger = LoggerFactory.getLogger(javaClass) | 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<LaserBag2AutoSendReport> { | |||||
| val date = planStart ?: LocalDate.now() | |||||
| val report = laserBag2AutoSendService.runAutoSend( | |||||
| planStart = date, | |||||
| limitPerRun = limitPerRun, | |||||
| ) | |||||
| return ResponseEntity.ok(report) | |||||
| } | |||||
| @PostMapping("/check-printer") | @PostMapping("/check-printer") | ||||
| fun checkPrinter(@RequestBody request: PrinterStatusRequest): ResponseEntity<PrinterStatusResponse> { | fun checkPrinter(@RequestBody request: PrinterStatusRequest): ResponseEntity<PrinterStatusResponse> { | ||||
| val (connected, message) = plasticBagPrinterService.checkPrinterConnection( | val (connected, message) = plasticBagPrinterService.checkPrinterConnection( | ||||
| @@ -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<LaserBag2JobSendResult>, | |||||
| ) | |||||
| data class LaserBag2JobSendResult( | |||||
| val jobOrderId: Long, | |||||
| val itemCode: String?, | |||||
| val success: Boolean, | |||||
| val message: String, | |||||
| ) | |||||
| @@ -55,6 +55,15 @@ logging: | |||||
| ngpcl: | ngpcl: | ||||
| push-url: ${NGPCL_PUSH_URL:} | 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: | bom: | ||||
| import: | import: | ||||
| temp-dir: ${java.io.tmpdir}/fpsms-bom-import | temp-dir: ${java.io.tmpdir}/fpsms-bom-import | ||||