@@ -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.service.PurchaseOrderService
import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderLineRequest
import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderLineRequest
import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderRequest
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 com.ffii.fpsms.modules.settings.service.SettingsService
import org.slf4j.Logger
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.LoggerFactory
@@ -45,12 +44,18 @@ open class M18PurchaseOrderService(
val m18MasterDataService: M18MasterDataService,
val m18MasterDataService: M18MasterDataService,
val settingsService: SettingsService,
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 commonUtils = CommonUtils()
val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)
val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)
val lastModifyDateStart = "2025-05-14 14:00:00"
val lastModifyDateStart = "2025-05-14 14:00:00"
val lastModifyDateEnd = "2025-05-14 14:30: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 =
// val lastModifyDateConds =
// "lastModifyDate=largerOrEqual=${lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${lastModifyDateEnd}"
// "lastModifyDate=largerOrEqual=${lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${lastModifyDateEnd}"
// val lastModifyDate = LocalDateTime.now().minusMinutes(30)
// 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 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 ?: "<none>",
)
if (localStatus != null && !enabled) {
logger.info(
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(
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(
private fun saveFromPoListSearch(
query: String,
query: String,
poListResponse: M18PurchaseOrderListResponse?,
poListResponse: M18PurchaseOrderListResponse?,
@@ -318,25 +321,6 @@ open class M18PurchaseOrderService(
return savePurchaseOrdersWithPreparedList(purchaseOrdersWithType = request)
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? {
private fun searchPurchaseOrdersByConds(conds: String): M18PurchaseOrderListResponse? {
val searchRequest = M18PurchaseOrderListRequest(
val searchRequest = M18PurchaseOrderListRequest(
stSearch = "po",
stSearch = "po",
@@ -752,4 +736,15 @@ open class M18PurchaseOrderService(
query = purchaseOrdersWithType?.query ?: ""
query = purchaseOrdersWithType?.query ?: ""
)
)
}
}
}
}
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,
)