| @@ -11,7 +11,8 @@ interface M18GoodsReceiptNoteLogRepository : AbstractRepository<M18GoodsReceiptN | |||
| 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( | |||
| """ | |||
| @@ -19,7 +20,7 @@ interface M18GoodsReceiptNoteLogRepository : AbstractRepository<M18GoodsReceiptN | |||
| WHERE l.deleted = false | |||
| AND l.m18RecordId IS NOT NULL | |||
| 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( | |||
| @@ -5,7 +5,9 @@ import org.slf4j.LoggerFactory | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.transaction.annotation.Transactional | |||
| import java.time.LocalDate | |||
| import java.time.LocalDateTime | |||
| 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=... | |||
| @@ -18,16 +20,37 @@ open class M18GrnCodeSyncService( | |||
| 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. | |||
| */ | |||
| @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 { | |||
| 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()) { | |||
| 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 | |||
| } | |||
| val byRecord = rows.groupBy { it.m18RecordId } | |||
| @@ -36,7 +59,7 @@ open class M18GrnCodeSyncService( | |||
| val rid = recordId ?: continue | |||
| val code = m18GoodsReceiptNoteService.readAnCodeByRecordId(rid) | |||
| 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 | |||
| } | |||
| for (log in list) { | |||
| @@ -49,6 +72,6 @@ open class M18GrnCodeSyncService( | |||
| 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.receiptDate:}") val postCompletedDnGrnReceiptDate: String, | |||
| @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, | |||
| val settingsService: SettingsService, | |||
| val taskScheduler: TaskScheduler, | |||
| @@ -149,7 +152,7 @@ open class SchedulerService( | |||
| 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() { | |||
| if (!grnCodeSyncEnabled) { | |||
| scheduledGrnCodeSync?.cancel(false) | |||
| @@ -160,16 +163,15 @@ open class SchedulerService( | |||
| commonSchedule( | |||
| scheduledGrnCodeSync, | |||
| SettingNames.SCHEDULE_GRN_CODE_SYNC, | |||
| "0 29 1 * * *", | |||
| "0 10 1 * * *", | |||
| { syncGrnCodesFromM18() }, | |||
| ) | |||
| } | |||
| open fun syncGrnCodesFromM18() { | |||
| 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) { | |||
| logger.error("Scheduler - M18 GRN code sync failed: ${e.message}", e) | |||
| } | |||
| @@ -186,7 +188,7 @@ open class SchedulerService( | |||
| commonSchedule( | |||
| scheduledPostCompletedDnGrn, | |||
| SettingNames.SCHEDULE_POST_COMPLETED_DN_GRN, | |||
| "0 57 0 * * *", | |||
| "0 1 0 * * *", | |||
| { getPostCompletedDnAndProcessGrn(receiptDate = getPostCompletedDnGrnReceiptDate()) } | |||
| ) | |||
| } | |||
| @@ -24,7 +24,7 @@ scheduler: | |||
| enabled: true | |||
| grnCodeSync: | |||
| 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: | |||
| config: | |||
| @@ -17,7 +17,8 @@ scheduler: | |||
| # receiptDate: # leave unset for production (uses yesterday) | |||
| grnCodeSync: | |||
| 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: | |||
| servlet: | |||