| @@ -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 { | |||
| logger.info("--------------------------------------------Start - Saving M18 Products / Materials--------------------------------------------") | |||
| ensureCunitSeededForAllIfEmpty() | |||
| @@ -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) { | |||
| @@ -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 | |||
| 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<LaserBag2AutoSendReport> { | |||
| 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<PrinterStatusResponse> { | |||
| 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: | |||
| 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 | |||