From a81c02be3f6e57ffe7cc2d7cc22151b3fc6522a5 Mon Sep 17 00:00:00 2001 From: Fai Luk Date: Fri, 18 Sep 2026 16:16:05 +0800 Subject: [PATCH] added a sync for stock in PO, when entered poNum and status = pending, and also the database settings M18.po.stockIn.refreshIfNewer.enabled = true (liquidbase default is false), then ask m18 to sync with lastModified after MTMS PO modified datetime --- .../java/com/ffii/fpsms/config/WebConfig.java | 2 +- .../m18/service/M18PurchaseOrderService.kt | 135 +++++++++--------- .../fpsms/modules/common/SettingNames.java | 4 +- .../service/PurchaseOrderService.kt | 14 ++ .../web/PurchaseOrderController.kt | 22 ++- .../settings/service/SettingsService.java | 12 ++ 6 files changed, 114 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/config/WebConfig.java b/src/main/java/com/ffii/fpsms/config/WebConfig.java index 5ef870a..9a29940 100644 --- a/src/main/java/com/ffii/fpsms/config/WebConfig.java +++ b/src/main/java/com/ffii/fpsms/config/WebConfig.java @@ -16,7 +16,7 @@ public class WebConfig implements WebMvcConfigurer { registry.addMapping("/**") .allowedHeaders("*") .allowedOrigins("*") - .exposedHeaders("filename", "Content-Disposition", "X-OnPack-Skipped-Expiry") + .exposedHeaders("filename", "Content-Disposition", "X-OnPack-Skipped-Expiry", "X-M18-Po-Refresh") .allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"); } diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt index a0c4eed..dd488c9 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt @@ -20,7 +20,6 @@ import com.ffii.fpsms.modules.purchaseOrder.service.PurchaseOrderLineService import com.ffii.fpsms.modules.purchaseOrder.service.PurchaseOrderService import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderLineRequest import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderRequest -import com.ffii.fpsms.modules.settings.entity.Settings import com.ffii.fpsms.modules.settings.service.SettingsService import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -45,12 +44,18 @@ open class M18PurchaseOrderService( val m18MasterDataService: M18MasterDataService, val settingsService: SettingsService, ) { + companion object { + fun isStockInLookupPoCode(code: String?): Boolean { + val trimmed = code?.trim().orEmpty() + return trimmed.length > 14 && (trimmed.startsWith("PP") || trimmed.startsWith("PF")) + } + } + val commonUtils = CommonUtils() val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) val lastModifyDateStart = "2025-05-14 14:00:00" val lastModifyDateEnd = "2025-05-14 14:30:00" - private val M18_LAST_MODIFY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") // val lastModifyDateConds = // "lastModifyDate=largerOrEqual=${lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${lastModifyDateEnd}" // val lastModifyDate = LocalDateTime.now().minusMinutes(30) @@ -237,67 +242,65 @@ open class M18PurchaseOrderService( } /** - * Stock-in `/po` lookup: create if missing; for a local **pending** PO, pull from M18 only when - * M18 list `lastModifyDate` is after [PurchaseOrder.modified] (last MTMS header write, usually last sync). - * Receiving / completed POs are not refreshed. + * Stock-in `/po` lookup. + * - Missing local PO: always sync from M18 (same as [savePurchaseOrderByCode]). + * - Local pending + setting [SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED] true: + * sync from M18 (overwrite pending). Receiving / completed are not refreshed. + * - Local PO + setting not true: do not call M18. */ - open fun savePurchaseOrderByCodeIfNewer(code: String): SyncResult { + open fun savePurchaseOrderByCodeIfNewer(code: String): SyncResult = + refreshForStockInLookup(code).syncResult + + open fun refreshForStockInLookup(code: String): StockInPoRefreshOutcome { val trimmed = code.trim() - val localPo = purchaseOrderService.findByCode(trimmed) - if (localPo != null && !isStockInRefreshIfNewerEnabled()) { - logger.info( - "Purchase Order: Skipping if-newer M18 sync — setting ${SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED} " + - "is not true. code=$trimmed" - ) - return SyncResult( - totalProcessed = 0, - totalSuccess = 0, - totalFail = 0, - query = "skipped (ifNewer): setting ${SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED} is not true" - ) - } - if (localPo != null && localPo.status != PurchaseOrderStatus.PENDING) { + val enabled = isStockInRefreshIfNewerEnabled() + val localStatus = purchaseOrderService.findActiveStatusByCode(trimmed) + logger.info( + "Purchase Order: stock-in ifNewer code={} settingEnabled={} localStatus={}", + trimmed, + enabled, + localStatus ?: "", + ) + if (localStatus != null && !enabled) { logger.info( - "Purchase Order: Skipping if-newer M18 sync — local PO id=${localPo.id} code=${localPo.code} " + - "status=${localPo.status?.value} (only pending may be refreshed)." + "Purchase Order: Skipping M18 sync — setting ${SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED} " + + "is not true. code=$trimmed status=$localStatus" ) - return SyncResult( - totalProcessed = 0, - totalSuccess = 0, - totalFail = 0, - query = "skipped (ifNewer): local PO not pending: $trimmed status=${localPo.status?.value}" + return StockInPoRefreshOutcome( + action = StockInPoRefreshAction.SKIPPED_DISABLED, + syncResult = SyncResult( + totalProcessed = 0, + totalSuccess = 0, + totalFail = 0, + query = "skipped (ifNewer): setting ${SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED} is not true" + ), ) } - if (localPo == null) { - return savePurchaseOrderByCode(trimmed) - } - - val conds = "(code=equal=$trimmed)" - val poListResponse = searchPurchaseOrdersByConds(conds) - val m18Row = poListResponse?.values?.firstOrNull() - val localModified = localPo.modified ?: localPo.created - val m18LastModify = parseM18LastModifyDate(m18Row?.lastModifyDate) - if (m18Row == null || m18LastModify == null || localModified == null || !m18LastModify.isAfter(localModified)) { + if (localStatus != null && + !PurchaseOrderStatus.PENDING.value.equals(localStatus, ignoreCase = true) + ) { logger.info( - "Purchase Order: Skipping if-newer M18 sync — M18 lastModifyDate not after " + - "purchase_order.modified. code=$trimmed localModified=$localModified " + - "m18LastModifyRaw=${m18Row?.lastModifyDate} m18LastModify=$m18LastModify" + "Purchase Order: Skipping M18 sync — local PO code=$trimmed status=$localStatus (only pending may be refreshed)." ) - return SyncResult( - totalProcessed = 0, - totalSuccess = 0, - totalFail = 0, - query = "skipped (ifNewer): M18 lastModifyDate not after purchase_order.modified: " + - "code=$trimmed localModified=$localModified m18LastModify=${m18Row?.lastModifyDate}" + return StockInPoRefreshOutcome( + action = StockInPoRefreshAction.SKIPPED_NOT_PENDING, + syncResult = SyncResult( + totalProcessed = 0, + totalSuccess = 0, + totalFail = 0, + query = "skipped (ifNewer): local PO not pending: $trimmed status=$localStatus" + ), ) } - logger.info( - "Purchase Order: M18 lastModifyDate after purchase_order.modified — syncing. " + - "code=$trimmed localModified=$localModified m18LastModify=$m18LastModify" + return StockInPoRefreshOutcome( + action = StockInPoRefreshAction.SYNCED, + syncResult = savePurchaseOrderByCode(trimmed), ) - return saveFromPoListSearch(conds, poListResponse) } + private fun isStockInRefreshIfNewerEnabled(): Boolean = + settingsService.isBooleanTrueFromDb(SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED) + private fun saveFromPoListSearch( query: String, poListResponse: M18PurchaseOrderListResponse?, @@ -318,25 +321,6 @@ open class M18PurchaseOrderService( return savePurchaseOrdersWithPreparedList(purchaseOrdersWithType = request) } - private fun isStockInRefreshIfNewerEnabled(): Boolean = - settingsService.findByName(SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED) - .map { Settings.VALUE_BOOLEAN_TRUE.equals(it.value?.trim(), ignoreCase = true) } - .orElse(false) - - private fun parseM18LastModifyDate(raw: String?): LocalDateTime? { - val value = raw?.trim().orEmpty() - if (value.isEmpty()) return null - return try { - LocalDateTime.parse(value, M18_LAST_MODIFY_FORMATTER) - } catch (_: Exception) { - try { - LocalDateTime.parse(value) - } catch (_: Exception) { - value.toLongOrNull()?.let { commonUtils.timestampToLocalDateTime(it) } - } - } - } - private fun searchPurchaseOrdersByConds(conds: String): M18PurchaseOrderListResponse? { val searchRequest = M18PurchaseOrderListRequest( stSearch = "po", @@ -752,4 +736,15 @@ open class M18PurchaseOrderService( query = purchaseOrdersWithType?.query ?: "" ) } -} \ No newline at end of file +} + +enum class StockInPoRefreshAction(val headerValue: String) { + SKIPPED_DISABLED("skipped_disabled"), + SKIPPED_NOT_PENDING("skipped_not_pending"), + SYNCED("synced"), +} + +data class StockInPoRefreshOutcome( + val action: StockInPoRefreshAction, + val syncResult: SyncResult, +) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java index 00a13e2..4f747a0 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java +++ b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java @@ -53,9 +53,9 @@ public abstract class SettingNames { public static final String M18_BOM_SHOP_SYNC_ENABLED = "M18.bom.shop.sync.enabled"; /** - * When "true", stock-in {@code /po} lookup for an existing pending PO asks M18 whether - * {@code lastModifyDate} is after {@code purchase_order.modified} and re-syncs if newer. + * When "true", stock-in {@code /po} lookup for an existing pending PO re-syncs from M18. * Read from DB on each request (no server restart). Missing POs still auto-sync. + * Receiving / completed POs are never overwritten. */ public static final String M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED = "M18.po.stockIn.refreshIfNewer.enabled"; diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt index 9474f07..80a3e83 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt @@ -364,6 +364,20 @@ open fun getPoSummariesByIds(ids: List): List { return purchaseOrderRepository.findTopByCodeAndDeletedIsFalseOrderByModifiedDesc(code) } + /** JDBC lookup so stock-in skip does not depend on JPA finding the entity. */ + open fun findActiveStatusByCode(code: String): String? { + val status = jdbcDao.queryForString( + """ + SELECT status FROM purchase_order + WHERE deleted = 0 AND code = :code + ORDER BY modified DESC + LIMIT 1 + """.trimIndent(), + mapOf("code" to code), + ) + return status.takeIf { it.isNotBlank() } + } + @Throws(IOException::class) @Transactional open fun savePurchaseOrder(request: SavePurchaseOrderRequest): SavePurchaseOrderResponse { diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt index f2ccc1d..6e47530 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt @@ -10,12 +10,14 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder import com.ffii.fpsms.modules.purchaseOrder.entity.projections.PurchaseOrderDataClass import com.ffii.fpsms.modules.purchaseOrder.entity.projections.PurchaseOrderInfo +import com.ffii.fpsms.m18.service.M18PurchaseOrderService import com.ffii.fpsms.modules.purchaseOrder.service.PurchaseOrderService import com.ffii.fpsms.modules.purchaseOrder.web.model.PagingRequest import com.ffii.fpsms.modules.stock.service.StockInLineService import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse import net.sf.jasperreports.engine.JasperExportManager import net.sf.jasperreports.engine.JasperPrint import org.springframework.data.domain.Page @@ -23,15 +25,20 @@ import org.springframework.data.domain.PageRequest import org.springframework.web.bind.annotation.* import java.io.File import com.ffii.fpsms.modules.purchaseOrder.web.model.PurchaseOrderSummary +import org.slf4j.LoggerFactory + @RestController @RequestMapping("/po") class PurchaseOrderController( private val purchaseOrderService: PurchaseOrderService, - private val stockInLineService: StockInLineService + private val stockInLineService: StockInLineService, + private val m18PurchaseOrderService: M18PurchaseOrderService, ) { + private val logger = LoggerFactory.getLogger(PurchaseOrderController::class.java) @GetMapping("/list") fun getPoList( - request: HttpServletRequest + request: HttpServletRequest, + response: HttpServletResponse, // @RequestParam(required = false) pageNum: Int, // @RequestParam(required = false) pageSize: Int ): RecordsRes { @@ -50,6 +57,17 @@ class PurchaseOrderController( // println(criteriaArgs) val pageSize = request.getParameter("pageSize")?.toIntOrNull()?.coerceAtLeast(1) ?: 10 val pageNum = request.getParameter("pageNum")?.toIntOrNull()?.coerceAtLeast(1) ?: 1 + val rawCode = request.getParameter("code")?.trim() + val refreshFromM18 = request.getParameter("refreshFromM18")?.equals("true", ignoreCase = true) == true + if (refreshFromM18 && M18PurchaseOrderService.isStockInLookupPoCode(rawCode)) { + try { + logger.info("Stock-in /po/list: M18 refresh for code={}", rawCode) + val outcome = m18PurchaseOrderService.refreshForStockInLookup(rawCode!!) + response.setHeader("X-M18-Po-Refresh", outcome.action.headerValue) + } catch (e: Exception) { + logger.error("Stock-in /po/list: M18 refresh failed for code={}", rawCode, e) + } + } val total = purchaseOrderService.getPoListTotalCount(criteriaArgs) if (total == 0) { diff --git a/src/main/java/com/ffii/fpsms/modules/settings/service/SettingsService.java b/src/main/java/com/ffii/fpsms/modules/settings/service/SettingsService.java index 6e43c34..bacadcd 100644 --- a/src/main/java/com/ffii/fpsms/modules/settings/service/SettingsService.java +++ b/src/main/java/com/ffii/fpsms/modules/settings/service/SettingsService.java @@ -30,6 +30,18 @@ public class SettingsService extends AbstractIdEntityService findAllByCategory(String category) { return this.repository.findAllByCategory(category); }