| @@ -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", | |||
| @@ -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() { | |||
| @@ -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<PyJobOrderListItem> { | |||
| fun listLaserPrintJobOrders(planStart: LocalDate): List<PyJobOrderListItem> { | |||
| val dayStart = planStart.atStartOfDay() | |||
| val dayEndExclusive = planStart.plusDays(1).atStartOfDay() | |||
| val orders = jobOrderRepository.findByDeletedFalseAndPlanStartBetweenAndBomProcessNameOrderByIdAsc( | |||
| @@ -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() | |||
| } | |||
| } | |||
| @@ -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 | |||
| } | |||
| } | |||
| @@ -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 | |||