@@ -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.Bom
import com.ffii.fpsms.modules.master.entity.BomMaterial
import com.ffii.fpsms.modules.master.entity.BomMaterial
import com.ffii.fpsms.modules.master.service.ItemUomService
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 com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository
import org.slf4j.Logger
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.LoggerFactory
@@ -40,6 +42,8 @@ open class M18BomForShopService(
private val itemUomService: ItemUomService,
private val itemUomService: ItemUomService,
private val purchaseOrderLineRepository: PurchaseOrderLineRepository,
private val purchaseOrderLineRepository: PurchaseOrderLineRepository,
private val m18BomShopSyncLogRepository: M18BomShopSyncLogRepository,
private val m18BomShopSyncLogRepository: M18BomShopSyncLogRepository,
private val shopService: ShopService,
private val m18MasterDataService: M18MasterDataService,
) {
) {
private val logger: Logger = LoggerFactory.getLogger(M18BomForShopService::class.java)
private val logger: Logger = LoggerFactory.getLogger(M18BomForShopService::class.java)
@@ -120,10 +124,14 @@ open class M18BomForShopService(
val (udfHarvest, udfHarvestUnit) = resolveUdfHarvestFields(bom, outputQty)
val (udfHarvest, udfHarvestUnit) = resolveUdfHarvestFields(bom, outputQty)
val udfEffectiveDate = bom.created?.atZone(m18Tz)?.toInstant()?.toEpochMilli()
val udfEffectiveDate = bom.created?.atZone(m18Tz)?.toInstant()?.toEpochMilli()
val targetBeId = resolveTargetBeId(flowTypeId)
val supplierCache = mutableMapOf<String, Long?>()
val lines = bom.bomMaterials
val lines = bom.bomMaterials
.filter { it.deleted != true }
.filter { it.deleted != true }
.sortedBy { it.id ?: 0L }
.sortedBy { it.id ?: 0L }
.mapIndexedNotNull { idx, mat -> toProductLine(mat, idx + 1) }
.mapIndexedNotNull { idx, mat ->
toProductLine(mat, idx + 1, flowTypeId, targetBeId, supplierCache)
}
if (lines.isEmpty()) {
if (lines.isEmpty()) {
logger.warn("[M18 BOM] BOM id=$bomId code=$routingCode has no materials; skipping M18 save")
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
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<String, Long?>,
): M18UdfProductSaveValue? {
val proId = mat.item?.m18Id?.takeIf { it > 0 } ?: run {
val proId = mat.item?.m18Id?.takeIf { it > 0 } ?: run {
logger.warn("[M18 BOM] material item m18Id missing bomMaterialId=${mat.id} itemId=${mat.item?.id}")
logger.warn("[M18 BOM] material item m18Id missing bomMaterialId=${mat.id} itemId=${mat.item?.id}")
return null
return null
@@ -331,14 +345,12 @@ open class M18BomForShopService(
}
}
val itemId = mat.item?.id
val itemId = mat.item?.id
val latestPoLine = itemId?.let { 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,
* M18 line price unit id ([M18PurchaseOrderPot.unitId]): prefer [PurchaseOrderLine.uomM18] from M18 PO sync,
* else [PurchaseOrderLine.uom] when uomM18 is missing.
* 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<PurchaseOrderLine>, 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<String, Long?>,
): 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 {
private fun resolveFlowTypeId(code: String): Int = when {
code.startsWith("TOA") -> 1
code.startsWith("TOA") -> 1
code.startsWith("BOMPP") || code.startsWith("PP") -> 3
code.startsWith("BOMPP") || code.startsWith("PP") -> 3