From 1c1eadc71d31f4005baa94d40f41b8f70a41f5d1 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Thu, 11 Jun 2026 22:16:43 +0800 Subject: [PATCH] alter for the BOM sync finding PP purchase order supplier and also others for supplier id by code --- .../m18/model/M18BomForShopSaveRequest.kt | 2 +- .../fpsms/m18/service/M18BomForShopService.kt | 87 +++++++++++++++++-- .../fpsms/m18/service/M18MasterDataService.kt | 19 ++++ .../modules/master/entity/ShopRepository.kt | 2 + .../modules/master/service/ShopService.kt | 6 ++ .../entity/PurchaseOrderLineRepository.kt | 22 +---- 6 files changed, 108 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt b/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt index 6dfe3bc..ecadc84 100644 --- a/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt +++ b/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt @@ -79,7 +79,7 @@ data class M18UdfProductSaveValue( val udfIngredients: String? = null, /** Line UOM: [com.ffii.fpsms.modules.master.entity.UomConversion.code] (same unit as [udfqty]). */ val udfBaseUnit: String? = null, - /** PO supplier [com.ffii.fpsms.modules.master.entity.Shop.m18Id] (via `purchase_order.supplierId`) for latest PO line matching material [com.ffii.fpsms.modules.master.entity.Items.code]. */ + /** M18 vendor id from latest synced PO for the material: local [com.ffii.fpsms.modules.master.entity.Shop.m18Id] or M18 `ven` search (PF BE when BOM is PF). */ val udfSupplier: Long? = null, /** * M18 UOM id for price/purchase unit on the **M18-linked** PO line (`m18DataLog` present): diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt index 4bd2af0..855e07b 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt @@ -16,6 +16,8 @@ import com.ffii.fpsms.m18.model.M18UdfProductWrapper import com.ffii.fpsms.modules.master.entity.Bom import com.ffii.fpsms.modules.master.entity.BomMaterial import com.ffii.fpsms.modules.master.service.ItemUomService +import com.ffii.fpsms.modules.master.service.ShopService +import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -40,6 +42,8 @@ open class M18BomForShopService( private val itemUomService: ItemUomService, private val purchaseOrderLineRepository: PurchaseOrderLineRepository, private val m18BomShopSyncLogRepository: M18BomShopSyncLogRepository, + private val shopService: ShopService, + private val m18MasterDataService: M18MasterDataService, ) { private val logger: Logger = LoggerFactory.getLogger(M18BomForShopService::class.java) @@ -120,10 +124,14 @@ open class M18BomForShopService( val (udfHarvest, udfHarvestUnit) = resolveUdfHarvestFields(bom, outputQty) val udfEffectiveDate = bom.created?.atZone(m18Tz)?.toInstant()?.toEpochMilli() + val targetBeId = resolveTargetBeId(flowTypeId) + val supplierCache = mutableMapOf() val lines = bom.bomMaterials .filter { it.deleted != true } .sortedBy { it.id ?: 0L } - .mapIndexedNotNull { idx, mat -> toProductLine(mat, idx + 1) } + .mapIndexedNotNull { idx, mat -> + toProductLine(mat, idx + 1, flowTypeId, targetBeId, supplierCache) + } if (lines.isEmpty()) { logger.warn("[M18 BOM] BOM id=$bomId code=$routingCode has no materials; skipping M18 save") @@ -320,7 +328,13 @@ open class M18BomForShopService( return harvestQty.toPlainString() to unitSuffix } - private fun toProductLine(mat: BomMaterial, lineNo: Int): M18UdfProductSaveValue? { + private fun toProductLine( + mat: BomMaterial, + lineNo: Int, + flowTypeId: Int, + targetBeId: Long?, + supplierCache: MutableMap, + ): M18UdfProductSaveValue? { val proId = mat.item?.m18Id?.takeIf { it > 0 } ?: run { logger.warn("[M18 BOM] material item m18Id missing bomMaterialId=${mat.id} itemId=${mat.item?.id}") return null @@ -331,14 +345,12 @@ open class M18BomForShopService( } val itemId = mat.item?.id val latestPoLine = itemId?.let { id -> - purchaseOrderLineRepository.findLatestLinesForBomM18ByItemId(id, PageRequest.of(0, 1)).firstOrNull() - } - val itemCode = mat.item?.code?.trim()?.takeIf { it.isNotEmpty() } - val supplierM18Id = itemCode?.let { code -> - purchaseOrderLineRepository.findLatestPoSupplierM18IdByItemCodeNative(code) - .firstOrNull() - ?.takeIf { it > 0L } + pickPreferredPoLine( + purchaseOrderLineRepository.findLatestLinesForBomM18ByItemId(id, PageRequest.of(0, 10)), + targetBeId, + ) } + val supplierM18Id = resolveSupplierM18Id(latestPoLine, flowTypeId, supplierCache) /** * M18 line price unit id ([M18PurchaseOrderPot.unitId]): prefer [PurchaseOrderLine.uomM18] from M18 PO sync, * else [PurchaseOrderLine.uom] when uomM18 is missing. @@ -362,6 +374,63 @@ open class M18BomForShopService( ) } + /** Prefer a PO line whose header [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder.m18BeId] matches the BOM BE. */ + private fun pickPreferredPoLine(lines: List, preferredBeId: Long?): PurchaseOrderLine? { + if (lines.isEmpty()) return null + if (preferredBeId == null) return lines.first() + return lines.firstOrNull { it.purchaseOrder?.m18BeId == preferredBeId } ?: lines.first() + } + + private fun resolveTargetBeId(flowTypeId: Int): Long? = when (flowTypeId) { + 2 -> m18Config.BEID_PF.toLongOrNull() + 3 -> m18Config.BEID_PP.toLongOrNull() + else -> null + } + + /** + * Resolves M18 vendor id for BOM material line supplier: + * - PF BOMs: M18 search by supplier code + [M18Config.BEID_PF] + * - PP / other: local [Shop.m18Id] by code, then M18 search with [M18Config.BEID_PP] when needed + */ + private fun resolveSupplierM18Id( + latestPoLine: PurchaseOrderLine?, + flowTypeId: Int, + cache: MutableMap, + ): Long? { + val po = latestPoLine?.purchaseOrder + val supplier = po?.supplier + val directM18Id = supplier?.m18Id?.takeIf { it > 0L } + val supplierCode = supplier?.code?.trim()?.takeIf { it.isNotEmpty() } + val targetBeId = resolveTargetBeId(flowTypeId) + val poBeId = po?.m18BeId + + if (supplierCode == null) { + return directM18Id + } + if (directM18Id != null && (targetBeId == null || poBeId == targetBeId)) { + return directM18Id + } + + val cacheKey = "$supplierCode|$flowTypeId" + cache[cacheKey]?.let { return it } + + val resolved = when (flowTypeId) { + 2 -> m18MasterDataService.findVendorM18IdByCode(supplierCode, m18Config.BEID_PF) + ?: directM18Id.also { + if (it == null) { + logger.warn("[M18 BOM] PF vendor M18 id not found for supplierCode=$supplierCode") + } + } + 3 -> shopService.findVendorByCode(supplierCode)?.m18Id?.takeIf { it > 0L } + ?: m18MasterDataService.findVendorM18IdByCode(supplierCode, m18Config.BEID_PP) + ?: directM18Id + else -> shopService.findVendorByCode(supplierCode)?.m18Id?.takeIf { it > 0L } + ?: directM18Id + } + cache[cacheKey] = resolved + return resolved + } + private fun resolveFlowTypeId(code: String): Int = when { code.startsWith("TOA") -> 1 code.startsWith("BOMPP") || code.startsWith("PP") -> 3 diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt index 3b01ec7..9aeb8bb 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt @@ -557,6 +557,25 @@ open class M18MasterDataService( ) } + /** M18 vendor id for [code] scoped to [beId] (e.g. [M18Config.BEID_PF] vs [M18Config.BEID_PP]). */ + open fun findVendorM18IdByCode(code: String, beId: String): Long? { + val trimmed = code.trim() + if (trimmed.isEmpty() || beId.isBlank()) return null + val conds = "(code=equal=$trimmed)=and=(beId=equal=$beId)" + val listResponse = try { + getList( + stSearch = StSearchType.VENDOR.value, + params = null, + conds = conds, + request = M18CommonRequest(), + ) + } catch (e: Exception) { + logger.warn("(findVendorM18IdByCode) M18 search failed code=$trimmed beId=$beId: ${e.message}") + null + } + return listResponse?.values?.firstOrNull()?.id?.takeIf { it > 0L } + } + open fun saveVendors(request: M18CommonRequest) : SyncResult{ logger.info("--------------------------------------------Start - Saving M18 Vendors--------------------------------------------") val vendors = getVendors(request) diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ShopRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ShopRepository.kt index 5fae1eb..3816d03 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ShopRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ShopRepository.kt @@ -31,6 +31,8 @@ interface ShopRepository : AbstractRepository { fun findByCode(code: String): Shop? + fun findByCodeAndTypeAndDeletedIsFalse(code: String, type: ShopType): Shop? + @Query( """ SELECT s FROM Shop s diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ShopService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ShopService.kt index d430d4b..4d8dd94 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ShopService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ShopService.kt @@ -26,6 +26,12 @@ open class ShopService( return shopRepository.findByM18IdAndTypeAndDeletedIsFalse(m18Id, ShopType.SUPPLIER) } + open fun findVendorByCode(code: String): Shop? { + val trimmed = code.trim() + if (trimmed.isEmpty()) return null + return shopRepository.findByCodeAndTypeAndDeletedIsFalse(trimmed, ShopType.SUPPLIER) + } + open fun findShopByM18Id(m18Id: Long): Shop? { return shopRepository.findByM18IdAndTypeAndDeletedIsFalse(m18Id, ShopType.SHOP) } diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLineRepository.kt index c1e8bef..2f60f95 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLineRepository.kt @@ -12,7 +12,7 @@ import java.io.Serializable @Repository interface PurchaseOrderLineRepository : AbstractRepository { /** - * Latest POL for BOM/M18 pushes: PO code must start with **`PP`**, row has M18 log, newest `pol.created` first. + * Latest M18-linked POL for BOM shop pushes (any PO type: PF, PP, etc.), newest `pol.created` first. */ @Query( "SELECT pol FROM PurchaseOrderLine pol " + @@ -21,7 +21,7 @@ interface PurchaseOrderLineRepository : AbstractRepository - /** - * Latest **PP** PO (code starts with `PP`, by header `purchase_order.created`) for a material item code: - * supplier `shop.m18Id` from `purchase_order.supplierId`. - */ - @Query( - value = - "SELECT sh.m18Id FROM purchase_order_line pol " + - "LEFT JOIN items it ON pol.itemId = it.id " + - "LEFT JOIN purchase_order po ON pol.purchaseOrderId = po.id " + - "LEFT JOIN shop sh ON po.supplierId = sh.id " + - "LEFT JOIN uom_conversion um ON pol.uomIdM18 = um.id " + - "WHERE pol.deleted = false AND po.deleted = false AND it.deleted = false AND it.code = :itemCode " + - "AND po.code LIKE 'PP%' " + - "ORDER BY po.created DESC LIMIT 1", - nativeQuery = true, - ) - fun findLatestPoSupplierM18IdByItemCodeNative(@Param("itemCode") itemCode: String): List - fun findByM18DataLogIdAndDeletedIsFalse(m18datalogId: Serializable): PurchaseOrderLine? fun findAllPurchaseOrderLineInfoByPurchaseOrderIdAndDeletedIsFalse(purchaseOrderId: Long): List fun findAllByPurchaseOrderIdAndDeletedIsFalse(purchaseOrderId: Long): List