From 797c33da125c4b0e18fa7e025e26ee262d633017 Mon Sep 17 00:00:00 2001 From: Fai Luk Date: Thu, 26 Mar 2026 13:40:08 +0800 Subject: [PATCH] no message --- python/bag2_settings.json | 2 +- .../scheduler/service/SchedulerService.kt | 8 +- .../service/PlasticBagPrinterService.kt | 9 +- .../web/PlasticBagPrinterController.kt | 28 ++++++ .../stock/service/SearchCompletedDnService.kt | 92 +++++++++++++++++++ src/main/resources/application-prod.yml | 2 + 6 files changed, 135 insertions(+), 6 deletions(-) diff --git a/python/bag2_settings.json b/python/bag2_settings.json index 1f6996e..9e790ba 100644 --- a/python/bag2_settings.json +++ b/python/bag2_settings.json @@ -1,5 +1,5 @@ { - "api_ip": "10.10.0.81", + "api_ip": "127.0.0.1", "api_port": "8090", "dabag_ip": "192.168.18.27", "dabag_port": "3008", diff --git a/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt b/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt index aeb5461..3fcc6ce 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt +++ b/src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt @@ -33,6 +33,7 @@ import kotlin.jvm.optionals.getOrNull open class SchedulerService( @Value("\${scheduler.postCompletedDnGrn.enabled:true}") val postCompletedDnGrnEnabled: Boolean, @Value("\${scheduler.postCompletedDnGrn.receiptDate:}") val postCompletedDnGrnReceiptDate: String, + @Value("\${scheduler.postCompletedDnGrn.retryMissingGrnLookbackDays:7}") val postCompletedDnGrnRetryMissingGrnLookbackDays: Int, @Value("\${scheduler.grnCodeSync.enabled:true}") val grnCodeSyncEnabled: Boolean, @Value("\${scheduler.grnCodeSync.syncOffsetDays:0}") val grnCodeSyncSyncOffsetDays: Int, @Value("\${scheduler.m18Units.enabled:true}") val m18UnitsSchedulerEnabled: Boolean, @@ -422,7 +423,12 @@ open class SchedulerService( ) { logger.info("Scheduler - Post completed DN and process GRN") val date = receiptDate ?: java.time.LocalDate.now().minusDays(1) - searchCompletedDnService.postCompletedDnAndProcessGrn(receiptDate = date, skipFirst = skipFirst, limitToFirst = limitToFirst) + searchCompletedDnService.postCompletedDnAndProcessGrnWithMissingRetry( + receiptDate = date, + skipFirst = skipFirst, + limitToFirst = limitToFirst, + retryLookbackDays = postCompletedDnGrnRetryMissingGrnLookbackDays, + ) } open fun getM18MasterData() { diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt index faeae19..970889f 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt @@ -14,7 +14,6 @@ import com.ffii.fpsms.modules.settings.service.SettingsService import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import com.ffii.fpsms.py.PyJobOrderListItem import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional import java.awt.Color import java.awt.Font import java.awt.Graphics2D @@ -46,7 +45,7 @@ import java.time.LocalDate data class BitmapResult(val bytes: ByteArray, val width: Int) @Service -open class PlasticBagPrinterService( +class PlasticBagPrinterService( val jobOrderRepository: JobOrderRepository, private val jdbcDao: JdbcDao, private val stockInLineRepository: StockInLineRepository, @@ -82,9 +81,11 @@ open class PlasticBagPrinterService( /** * Same criteria as [com.ffii.fpsms.py.PyController.listJobOrders], filtered by [SettingNames.LASER_PRINT_ITEM_CODES] * (comma-separated item codes). Blank / whitespace-only setting value means no filter (all packaging job orders). + * + * No Transactional annotation on this class: CGLIB subclassing plus Kotlin breaks constructor-injected deps on the proxy. + * Rely on default OSIV for lazy BOM access within the HTTP request. */ - @Transactional(readOnly = true) - open fun listLaserPrintJobOrders(planStart: LocalDate): List { + fun listLaserPrintJobOrders(planStart: LocalDate): List { val dayStart = planStart.atStartOfDay() val dayEndExclusive = planStart.plusDays(1).atStartOfDay() val orders = jobOrderRepository.findByDeletedFalseAndPlanStartBetweenAndBomProcessNameOrderByIdAsc( 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 97641df..cbe6879 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 @@ -18,12 +18,14 @@ import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.* import java.time.LocalDate import org.springframework.http.ResponseEntity +import org.slf4j.LoggerFactory @RestController @RequestMapping("/plastic") class PlasticBagPrinterController( private val plasticBagPrinterService: PlasticBagPrinterService, ) { + private val logger = LoggerFactory.getLogger(javaClass) /** System defaults + current values from [LASER_PRINT.host] / [LASER_PRINT.port] / [LASER_PRINT.itemCodes] (see Liquibase). */ @GetMapping("/laser-bag2-settings") @@ -97,6 +99,19 @@ class PlasticBagPrinterController( response.contentType = "text/plain;charset=UTF-8" response.writer.write(e.message ?: "Invalid request") response.writer.flush() + } catch (e: Exception) { + logger.error("POST /plastic/download-onpack-qr failed", e) + try { + if (!response.isCommitted) { + response.reset() + } + } catch (_: Exception) { + /* ignore */ + } + response.status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR + response.contentType = "text/plain;charset=UTF-8" + response.writer.write(e.message ?: "Download failed") + response.writer.flush() } } @@ -121,6 +136,19 @@ class PlasticBagPrinterController( response.contentType = "text/plain;charset=UTF-8" response.writer.write(e.message ?: "Invalid request") response.writer.flush() + } catch (e: Exception) { + logger.error("POST /plastic/download-onpack-qr-text failed", e) + try { + if (!response.isCommitted) { + response.reset() + } + } catch (_: Exception) { + /* ignore */ + } + response.status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR + response.contentType = "text/plain;charset=UTF-8" + response.writer.write(e.message ?: "Download failed") + response.writer.flush() } } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt index ece3e97..f705b4e 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt @@ -1,6 +1,7 @@ package com.ffii.fpsms.modules.stock.service import com.ffii.core.support.JdbcDao +import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLogRepository import com.ffii.fpsms.modules.stock.entity.StockInLine import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import com.ffii.fpsms.modules.stock.web.model.SearchCompletedDnResult @@ -20,6 +21,7 @@ open class SearchCompletedDnService( private val jdbcDao: JdbcDao, private val stockInLineRepository: StockInLineRepository, private val stockInLineService: StockInLineService, + private val m18GoodsReceiptNoteLogRepository: M18GoodsReceiptNoteLogRepository, ) { private val logger = LoggerFactory.getLogger(SearchCompletedDnService::class.java) @@ -107,4 +109,94 @@ open class SearchCompletedDnService( } return toProcess.size } + + /** + * Retry GRN creation for completed stock-in lines where the PO does not have a SUCCESS log in + * `m18_goods_receipt_note_log`. + * + * Grouping behavior matches normal scheduler: + * - iterate by `receiptDate` day window + * - for each day, group by `PO (purchaseOrderId)` + * - process one GRN per PO using the first line from that PO group + */ + @Transactional + open fun postCompletedDnAndProcessGrnWithMissingRetry( + receiptDate: LocalDate = LocalDate.now().minusDays(1), + skipFirst: Int = 0, + limitToFirst: Int? = null, + retryLookbackDays: Int = 0, + ): Int { + val lookback = retryLookbackDays.coerceAtLeast(0) + var totalProcessed = 0 + + // Requirement: never process today's completed stock-in lines in retry mode. + val today = LocalDate.now() + var processedDayCount = 0 + + // For each offset, day = receiptDate minus offset. Example: job runs 27/3 00:01, receiptDate=yesterday=26/3, + // lookback=6 => days 26,25,24,23,22,21,20 (skip any day equal to today). + for (offset in 0..lookback) { + val day = receiptDate.minusDays(offset.toLong()) + if (day.isEqual(today)) { + logger.info( + "[postCompletedDnAndProcessGrnWithMissingRetry] Skipping receiptDate=today=$today " + + "(offset=$offset) to avoid creating GRN from today's completed stock-in lines." + ) + continue + } + + val daySkipFirst = if (processedDayCount == 0) skipFirst else 0 + val dayLimitToFirst = if (processedDayCount == 0) limitToFirst else null + + val processedForDay = postCompletedDnAndProcessMissingGrnForReceiptDate( + receiptDate = day, + skipFirst = daySkipFirst, + limitToFirst = dayLimitToFirst, + ) + totalProcessed += processedForDay + processedDayCount++ + logger.info( + "[postCompletedDnAndProcessGrnWithMissingRetry] offset=$offset day=$day processedForDay=$processedForDay totalProcessed=$totalProcessed" + ) + } + + return totalProcessed + } + + private fun postCompletedDnAndProcessMissingGrnForReceiptDate( + receiptDate: LocalDate, + skipFirst: Int, + limitToFirst: Int?, + ): Int { + val lines = searchCompletedDn(receiptDate) + val byPo = lines.groupBy { it.purchaseOrder?.id ?: 0L }.filterKeys { it != 0L } + val entries = byPo.entries.toList() + + // Only retry POs that do NOT have any successful GRN log. + val missingEntries = entries.filter { (poId, _) -> + !m18GoodsReceiptNoteLogRepository.existsByPurchaseOrderIdAndStatusTrue(poId) + } + + val toProcess = missingEntries + .drop(skipFirst) + .let { if (limitToFirst != null) it.take(limitToFirst) else it } + + logger.info( + "[postCompletedDnAndProcessMissingGrnForReceiptDate] receiptDate=$receiptDate foundLines=${lines.size} " + + "poTotal=${byPo.size} poMissingSuccess=${missingEntries.size} processing=${toProcess.size} " + + "(skipFirst=$skipFirst, limitToFirst=$limitToFirst)" + ) + + toProcess.forEach { (poId, silList) -> + silList.firstOrNull()?.let { first -> + try { + stockInLineService.processPurchaseOrderForGrn(first) + } catch (e: Exception) { + logger.error("[postCompletedDnAndProcessMissingGrnForReceiptDate] Failed for PO id=$poId: ${e.message}", e) + } + } + } + + return toProcess.size + } } diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 83f0b60..ed02b61 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -24,6 +24,8 @@ spring: scheduler: postCompletedDnGrn: enabled: true + # Missing-success GRN retry window: receiptDate (default yesterday) back N days inclusive. E.g. run 27/3 with receiptDate=26/3 and N=6 => 20/3–26/3. + retryMissingGrnLookbackDays: 6 grnCodeSync: enabled: true syncOffsetDays: 10 # from (today − 10) 00:00 to now, rows missing grn_code