| @@ -11,7 +11,8 @@ interface M18GoodsReceiptNoteLogRepository : AbstractRepository<M18GoodsReceiptN | |||||
| fun existsByPurchaseOrderIdAndStatusTrue(purchaseOrderId: Long): Boolean | fun existsByPurchaseOrderIdAndStatusTrue(purchaseOrderId: Long): Boolean | ||||
| /** | /** | ||||
| * GRN log rows that need M18 AN code backfill: have record id, no grn_code yet, created in [start, end). | |||||
| * GRN log rows that need M18 AN code backfill: have record id, no grn_code yet, | |||||
| * created in [start, end] inclusive (e.g. start = 4 days ago 00:00, end = now). | |||||
| */ | */ | ||||
| @Query( | @Query( | ||||
| """ | """ | ||||
| @@ -19,7 +20,7 @@ interface M18GoodsReceiptNoteLogRepository : AbstractRepository<M18GoodsReceiptN | |||||
| WHERE l.deleted = false | WHERE l.deleted = false | ||||
| AND l.m18RecordId IS NOT NULL | AND l.m18RecordId IS NOT NULL | ||||
| AND (l.grnCode IS NULL OR l.grnCode = '') | AND (l.grnCode IS NULL OR l.grnCode = '') | ||||
| AND l.created >= :start AND l.created < :end | |||||
| AND l.created >= :start AND l.created <= :end | |||||
| """ | """ | ||||
| ) | ) | ||||
| fun findNeedingGrnCode( | fun findNeedingGrnCode( | ||||
| @@ -5,7 +5,9 @@ import org.slf4j.LoggerFactory | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.LocalDateTime | |||||
| import java.time.ZoneId | import java.time.ZoneId | ||||
| import java.time.ZonedDateTime | |||||
| /** | /** | ||||
| * Fills [com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLog.grnCode] by calling M18 GET /root/api/read/an?id=... | * Fills [com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLog.grnCode] by calling M18 GET /root/api/read/an?id=... | ||||
| @@ -18,16 +20,37 @@ open class M18GrnCodeSyncService( | |||||
| private val logger = LoggerFactory.getLogger(M18GrnCodeSyncService::class.java) | private val logger = LoggerFactory.getLogger(M18GrnCodeSyncService::class.java) | ||||
| /** | /** | ||||
| * For today's [created] window, backfill grn_code for rows with m18_record_id but empty grn_code. | |||||
| * Backfill `grn_code` for rows created in **[start of calendar day (today − syncOffsetDays), now]** (inclusive), | |||||
| * with `m18_record_id` set and `grn_code` empty. | |||||
| * | |||||
| * Example: `syncOffsetDays = 4` → `created` from 4 days ago 00:00:00 up to **now**. | |||||
| * Example: `syncOffsetDays = 0` → `created` from **today** 00:00:00 up to **now**. | |||||
| * | |||||
| * One M18 read per distinct [m18_record_id]; all matching log rows get the same code. | * One M18 read per distinct [m18_record_id]; all matching log rows get the same code. | ||||
| */ | */ | ||||
| @Transactional | @Transactional | ||||
| open fun syncGrnCodesForLookback(syncOffsetDays: Int, zoneId: ZoneId = ZoneId.systemDefault()): Int { | |||||
| val days = syncOffsetDays.coerceAtLeast(0) | |||||
| val nowZ = ZonedDateTime.now(zoneId) | |||||
| val startLdt = nowZ.toLocalDate().minusDays(days.toLong()).atStartOfDay(zoneId).toLocalDateTime() | |||||
| val endLdt = nowZ.toLocalDateTime() | |||||
| return syncGrnCodesInWindow(startLdt, endLdt, "lookback syncOffsetDays=$days") | |||||
| } | |||||
| /** | |||||
| * Single calendar day window: [day 00:00, next day 00:00) — useful for manual/testing. | |||||
| */ | |||||
| @Transactional | |||||
| open fun syncGrnCodesForDate(day: LocalDate, zoneId: ZoneId = ZoneId.systemDefault()): Int { | open fun syncGrnCodesForDate(day: LocalDate, zoneId: ZoneId = ZoneId.systemDefault()): Int { | ||||
| val start = day.atStartOfDay(zoneId).toLocalDateTime() | |||||
| val end = day.plusDays(1).atStartOfDay(zoneId).toLocalDateTime() | |||||
| val rows = m18GoodsReceiptNoteLogRepository.findNeedingGrnCode(start, end) | |||||
| val startLdt = day.atStartOfDay(zoneId).toLocalDateTime() | |||||
| val endLdt = day.plusDays(1).atStartOfDay(zoneId).toLocalDateTime().minusNanos(1) | |||||
| return syncGrnCodesInWindow(startLdt, endLdt, "single day=$day") | |||||
| } | |||||
| private fun syncGrnCodesInWindow(startLdt: LocalDateTime, endLdt: LocalDateTime, label: String): Int { | |||||
| val rows = m18GoodsReceiptNoteLogRepository.findNeedingGrnCode(startLdt, endLdt) | |||||
| if (rows.isEmpty()) { | if (rows.isEmpty()) { | ||||
| logger.info("[M18GrnCodeSync] No GRN log rows need grn_code for day=$day") | |||||
| logger.info("[M18GrnCodeSync] No GRN log rows need grn_code ($label, window=[$startLdt, $endLdt])") | |||||
| return 0 | return 0 | ||||
| } | } | ||||
| val byRecord = rows.groupBy { it.m18RecordId } | val byRecord = rows.groupBy { it.m18RecordId } | ||||
| @@ -36,7 +59,7 @@ open class M18GrnCodeSyncService( | |||||
| val rid = recordId ?: continue | val rid = recordId ?: continue | ||||
| val code = m18GoodsReceiptNoteService.readAnCodeByRecordId(rid) | val code = m18GoodsReceiptNoteService.readAnCodeByRecordId(rid) | ||||
| if (code.isNullOrBlank()) { | if (code.isNullOrBlank()) { | ||||
| logger.warn("[M18GrnCodeSync] No code from M18 for m18_record_id=$rid (day=$day)") | |||||
| logger.warn("[M18GrnCodeSync] No code from M18 for m18_record_id=$rid ($label)") | |||||
| continue | continue | ||||
| } | } | ||||
| for (log in list) { | for (log in list) { | ||||
| @@ -49,6 +72,6 @@ open class M18GrnCodeSyncService( | |||||
| return updated | return updated | ||||
| } | } | ||||
| /** Convenience: sync for calendar "today" in the default timezone. */ | |||||
| open fun syncGrnCodesForToday(): Int = syncGrnCodesForDate(LocalDate.now()) | |||||
| /** Convenience: lookback 0 days = from start of today to now. */ | |||||
| open fun syncGrnCodesForToday(): Int = syncGrnCodesForLookback(0) | |||||
| } | } | ||||
| @@ -32,7 +32,10 @@ open class SchedulerService( | |||||
| @Value("\${scheduler.postCompletedDnGrn.enabled:true}") val postCompletedDnGrnEnabled: Boolean, | @Value("\${scheduler.postCompletedDnGrn.enabled:true}") val postCompletedDnGrnEnabled: Boolean, | ||||
| @Value("\${scheduler.postCompletedDnGrn.receiptDate:}") val postCompletedDnGrnReceiptDate: String, | @Value("\${scheduler.postCompletedDnGrn.receiptDate:}") val postCompletedDnGrnReceiptDate: String, | ||||
| @Value("\${scheduler.grnCodeSync.enabled:true}") val grnCodeSyncEnabled: Boolean, | @Value("\${scheduler.grnCodeSync.enabled:true}") val grnCodeSyncEnabled: Boolean, | ||||
| /** 0 = calendar day of run; 1 = previous day (typical for 1:20 AM job to backfill yesterday's GRNs). */ | |||||
| /** | |||||
| * Lookback window for GRN code sync: rows with `created` from **start of (today − N days)** through **now**, | |||||
| * missing `grn_code`. Example: 4 = from 4 days ago 00:00 to now. | |||||
| */ | |||||
| @Value("\${scheduler.grnCodeSync.syncOffsetDays:0}") val grnCodeSyncSyncOffsetDays: Int, | @Value("\${scheduler.grnCodeSync.syncOffsetDays:0}") val grnCodeSyncSyncOffsetDays: Int, | ||||
| val settingsService: SettingsService, | val settingsService: SettingsService, | ||||
| val taskScheduler: TaskScheduler, | val taskScheduler: TaskScheduler, | ||||
| @@ -149,7 +152,7 @@ open class SchedulerService( | |||||
| else try { LocalDate.parse(s) } catch (e: Exception) { LocalDate.now().minusDays(1) } | else try { LocalDate.parse(s) } catch (e: Exception) { LocalDate.now().minusDays(1) } | ||||
| } | } | ||||
| /** Backfill grn_code from M18 read/an for today's logs missing code. Default 1:20 AM daily. Set scheduler.grnCodeSync.enabled=false to disable. */ | |||||
| /** Backfill grn_code from M18 read/an for logs in [today−syncOffsetDays 00:00, now] missing code. Set scheduler.grnCodeSync.enabled=false to disable. */ | |||||
| fun scheduleGrnCodeSync() { | fun scheduleGrnCodeSync() { | ||||
| if (!grnCodeSyncEnabled) { | if (!grnCodeSyncEnabled) { | ||||
| scheduledGrnCodeSync?.cancel(false) | scheduledGrnCodeSync?.cancel(false) | ||||
| @@ -160,16 +163,15 @@ open class SchedulerService( | |||||
| commonSchedule( | commonSchedule( | ||||
| scheduledGrnCodeSync, | scheduledGrnCodeSync, | ||||
| SettingNames.SCHEDULE_GRN_CODE_SYNC, | SettingNames.SCHEDULE_GRN_CODE_SYNC, | ||||
| "0 29 1 * * *", | |||||
| "0 10 1 * * *", | |||||
| { syncGrnCodesFromM18() }, | { syncGrnCodesFromM18() }, | ||||
| ) | ) | ||||
| } | } | ||||
| open fun syncGrnCodesFromM18() { | open fun syncGrnCodesFromM18() { | ||||
| try { | try { | ||||
| val day = java.time.LocalDate.now().minusDays(grnCodeSyncSyncOffsetDays.toLong().coerceAtLeast(0)) | |||||
| val updated = m18GrnCodeSyncService.syncGrnCodesForDate(day) | |||||
| logger.info("Scheduler - M18 GRN code sync done for date=$day, rows updated=$updated") | |||||
| val updated = m18GrnCodeSyncService.syncGrnCodesForLookback(grnCodeSyncSyncOffsetDays) | |||||
| logger.info("Scheduler - M18 GRN code sync done (syncOffsetDays=${grnCodeSyncSyncOffsetDays}), rows updated=$updated") | |||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| logger.error("Scheduler - M18 GRN code sync failed: ${e.message}", e) | logger.error("Scheduler - M18 GRN code sync failed: ${e.message}", e) | ||||
| } | } | ||||
| @@ -186,7 +188,7 @@ open class SchedulerService( | |||||
| commonSchedule( | commonSchedule( | ||||
| scheduledPostCompletedDnGrn, | scheduledPostCompletedDnGrn, | ||||
| SettingNames.SCHEDULE_POST_COMPLETED_DN_GRN, | SettingNames.SCHEDULE_POST_COMPLETED_DN_GRN, | ||||
| "0 57 0 * * *", | |||||
| "0 1 0 * * *", | |||||
| { getPostCompletedDnAndProcessGrn(receiptDate = getPostCompletedDnGrnReceiptDate()) } | { getPostCompletedDnAndProcessGrn(receiptDate = getPostCompletedDnGrnReceiptDate()) } | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -24,7 +24,7 @@ scheduler: | |||||
| enabled: true | enabled: true | ||||
| grnCodeSync: | grnCodeSync: | ||||
| enabled: true | enabled: true | ||||
| syncOffsetDays: 0 # 0 = m18_goods_receipt_note_log rows created today; use 1 for "yesterday" if you prefer night backfill | |||||
| syncOffsetDays: 10 # from (today − 10) 00:00 to now, rows missing grn_code | |||||
| m18: | m18: | ||||
| config: | config: | ||||
| @@ -17,7 +17,8 @@ scheduler: | |||||
| # receiptDate: # leave unset for production (uses yesterday) | # receiptDate: # leave unset for production (uses yesterday) | ||||
| grnCodeSync: | grnCodeSync: | ||||
| enabled: false # set true in prod; backfills grn_code from M18 GET /root/api/read/an | enabled: false # set true in prod; backfills grn_code from M18 GET /root/api/read/an | ||||
| syncOffsetDays: 0 # 0=today, 1=yesterday (use 1 for 1:20 AM job to backfill prior day) | |||||
| # Lookback: created from start of (today − N days) through now, missing grn_code. E.g. 4 = last 4 days + today. | |||||
| syncOffsetDays: 0 | |||||
| spring: | spring: | ||||
| servlet: | servlet: | ||||