From 344041d703ea1a2b92e06b9b8a00966953d1eda5 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 14 Jan 2026 11:24:15 +0800 Subject: [PATCH] update --- .../master/entity/ItemUomRespository.kt | 4 + .../master/entity/UomConversionRepository.kt | 1 + .../modules/master/service/ItemUomService.kt | 58 ++++- .../modules/master/web/ItemsController.kt | 2 +- .../master/web/models/NewItemRequest.kt | 39 ++- .../entity/projections/ProductProcessInfo.kt | 13 +- .../service/ProductProcessService.kt | 235 ++++++++++++++---- 7 files changed, 303 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt index be4d0a8..bbe2126 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt @@ -20,6 +20,10 @@ interface ItemUomRespository : AbstractRepository { fun findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(itemId: Serializable): ItemUom? fun findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(itemId: Serializable): ItemUom? + fun findByItemIdAndBaseUnitIsTrueAndDeletedIsFalse(itemId: Serializable): ItemUom? + fun findByItemIdAndPickingUnitIsTrueAndDeletedIsFalse(itemId: Serializable): ItemUom? + fun findByItemM18IdAndPurchaseUnitIsTrueAndDeletedIsFalse(itemM18Id: Long): ItemUom? fun findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(itemId: Long): ItemUom? + fun findByItemIdAndUomIdAndDeletedIsFalse(itemId: Long, uomId: Long): ItemUom? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/UomConversionRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/UomConversionRepository.kt index d9518c6..7bc7f6e 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/UomConversionRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/UomConversionRepository.kt @@ -15,4 +15,5 @@ interface UomConversionRepository : AbstractRepository { fun findByM18IdAndDeletedFalse(m18Id: Long): UomConversion?; fun findByLastModifyDateAndM18IdAndDeletedFalse(lastModifyDate: LocalDateTime, m18Id: Long): UomConversion?; + fun findByUdfudescAndDeletedFalse(udfudesc: String): UomConversion?; } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt index c77c7c6..5a5e1b7 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt @@ -2,7 +2,7 @@ package com.ffii.fpsms.modules.master.service import com.ffii.fpsms.modules.master.entity.ItemUom import com.ffii.fpsms.modules.master.entity.ItemUomRespository -import com.ffii.fpsms.modules.master.web.models.ItemUomRequest +import com.ffii.fpsms.modules.master.web.models.* import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.io.IOException @@ -44,6 +44,25 @@ open class ItemUomService( return itemUomRespository.findByItemM18IdAndPurchaseUnitIsTrueAndDeletedIsFalse(m18ItemId) } + open fun findBaseUnitByItemId(itemId: Long): ItemUom? { + return itemUomRespository.findByItemIdAndBaseUnitIsTrueAndDeletedIsFalse(itemId) + } + + open fun findPickingUnitByItemId(itemId: Long): ItemUom? { + return itemUomRespository.findByItemIdAndPickingUnitIsTrueAndDeletedIsFalse(itemId) + } + + private fun findTargetItemUom(itemId: Long, targetUnit: String): ItemUom? { + return when (targetUnit.lowercase()) { + "baseunit", "base_unit" -> findBaseUnitByItemId(itemId) + "stockunit", "stock_unit" -> findStockUnitByItemId(itemId) + "purchaseunit", "purchase_unit" -> findPurchaseUnitByItemId(itemId) + "salesunit", "sales_unit" -> findSalesUnitByItemId(itemId) + "pickingunit", "picking_unit" -> findPickingUnitByItemId(itemId) + else -> null + } + } + open fun convertPurchaseQtyToStockQty(itemId: Long, purchaseQty: BigDecimal): BigDecimal { val purchaseUnit = findPurchaseUnitByItemId(itemId) ?: return purchaseQty; val stockUnit = findStockUnitByItemId(itemId) ?: return purchaseQty; @@ -96,4 +115,41 @@ open class ItemUomService( itemUomRespository.deleteAllByIdIn(deleteIds) } } + + + open fun convertUomByItem(request: ConvertUomByItemRequest): ConvertUomByItemResponse { + // Find source ItemUom by itemId and uomId + val sourceItemUom = itemUomRespository.findByItemIdAndUomIdAndDeletedIsFalse(request.itemId, request.uomId) + ?: throw IllegalArgumentException("Source ItemUom not found for itemId=${request.itemId}, uomId=${request.uomId}") + + // Find target ItemUom by itemId and targetUnit + val targetItemUom = findTargetItemUom(request.itemId, request.targetUnit) + ?: throw IllegalArgumentException("Target ItemUom not found for itemId=${request.itemId}, targetUnit=${request.targetUnit}") + + // Convert quantity using ratioN/ratioD via base unit + val one = BigDecimal.ONE + val sourceRatioN = sourceItemUom.ratioN ?: one + val sourceRatioD = sourceItemUom.ratioD ?: one + val targetRatioN = targetItemUom.ratioN ?: one + val targetRatioD = targetItemUom.ratioD ?: one + + // Convert source qty to base: qty * ratioN / ratioD + val baseQty = request.qty.multiply(sourceRatioN).divide(sourceRatioD, 2, RoundingMode.HALF_UP) + + // Convert base to target: baseQty * ratioD / ratioN + val newQty = baseQty.multiply(targetRatioD).divide(targetRatioN, 2, RoundingMode.HALF_UP) + + // Get UomConversion from target ItemUom + val uomConversion = targetItemUom.uom + ?: throw IllegalArgumentException("Target UomConversion not found for target ItemUom") + + return ConvertUomByItemResponse( + newQty = newQty, + udfudesc = uomConversion.udfudesc, + udfShortDesc = uomConversion.udfShortDesc + ) + } + + + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt index 4ffeb2c..d89e80f 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt @@ -132,5 +132,5 @@ fun getItemsWithDetailsByPage(request: HttpServletRequest): RecordsRes?, // val weightUnit: List?, ) +data class UomConversionResponse( + val uomId: Long?, + val uomCode: String?, + val udfudesc: String?, + val udfShortDesc: String? +) +data class ConversionUomRequest( + val itemid: Long?, + val itemcode: String?, + val qty: String?, + + val currentUomCode: String?, + val currentUomid: Long?, + val currentUomUdfudesc: String?, + + val newUomCode: String?, + val newUomid: Long?, + val newUomUdfudesc: String? + +) +data class ConversionUomResponse( + val qty: BigDecimal, + val UomCode: String, + val UomUdfudesc: String, + val udfShortDesc: String +) +data class ConvertUomByItemRequest( + val itemId: Long, + val qty: BigDecimal, + val uomId: Long, + val targetUnit: String +) +data class ConvertUomByItemResponse( + val newQty: BigDecimal, + val udfudesc: String?, + val udfShortDesc: String? +) diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt index 9c846b2..aa6f331 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt @@ -93,10 +93,19 @@ data class jobOrderLineInfo( val itemCode: String?, val itemName: String?, val type: String?, + val reqQty: Int?, + val baseReqQty: Int?, + val stockQty: Int?, - val uom: String?, - val shortUom: String?, + val baseStockQty: Int?, + + val reqUom: String?, + val reqBaseUom: String?, + + val stockUom: String?, + val stockBaseUom: String?, + val availableStatus: String?, val bomProcessId: Long?, val bomProcessSeqNo: Long? diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt index 7d5a424..b71cbc0 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt @@ -28,7 +28,7 @@ import com.ffii.fpsms.modules.master.entity.Process import com.ffii.fpsms.modules.master.entity.Equipment import com.ffii.fpsms.modules.master.entity.BomProcess import com.ffii.fpsms.modules.master.entity.Bom - +import com.ffii.fpsms.modules.master.service.ItemUomService import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.jobOrder.entity.JobOrderBomMaterialRepository import com.ffii.fpsms.modules.master.service.ProductionScheduleService @@ -48,6 +48,8 @@ import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus import com.ffii.fpsms.modules.master.entity.UomConversionRepository import com.ffii.fpsms.modules.master.entity.ItemUomRespository +import com.ffii.fpsms.modules.master.web.models.* +import java.math.RoundingMode @Service @Transactional open class ProductProcessService( @@ -71,8 +73,9 @@ open class ProductProcessService( private val jobTypeRepository: JobTypeRepository, private val pickOrderRepository: PickOrderRepository, private val joPickOrderRepository: JoPickOrderRepository, - private val uomConversionRepository: UomConversionRepository, private val itemUomRepository: ItemUomRespository, + private val uomConversionRepository: UomConversionRepository, + private val itemUomService: ItemUomService ) { open fun findAll(pageable: Pageable): Page { @@ -507,32 +510,44 @@ open class ProductProcessService( .minus(lot.outQty ?: BigDecimal.ZERO) .minus(lot.holdQty ?: BigDecimal.ZERO) } - itemId to stockQty - }.toMap() - - // calculate statistics - val totalStockQty = stockQtyMap.values.sumOf { it.toInt() } - val insufficientStockQty = bomMaterials - .filter { material -> - val itemId = material.item?.id ?: 0L - val stockQty = stockQtyMap[itemId] ?: BigDecimal.ZERO - stockQty < (material.reqQty ?: BigDecimal.ZERO) - } - .sumOf { material -> - val itemId = material.item?.id ?: 0L - stockQtyMap[itemId]?.toInt() ?: 0 - } - val sufficientStockQty = bomMaterials - .filter { material -> - val itemId = material.item?.id ?: 0L - val stockQty = stockQtyMap[itemId] ?: BigDecimal.ZERO - stockQty >= (material.reqQty ?: BigDecimal.ZERO) + // Get the stockUom from the first available lot line + // stockItemUomId is the ItemUom id, we need to get the UomConversion id from it + val stockItemUom = availableLots.firstOrNull()?.stockUom + if (stockItemUom == null) { + println("WARNING: No stockUom found for itemId=$itemId in available lots") } - .sumOf { material -> - val itemId = material.item?.id ?: 0L - stockQtyMap[itemId]?.toInt() ?: 0 + val stockUomId = stockItemUom?.uom?.id + if (stockUomId == null && stockItemUom != null) { + println("WARNING: stockItemUom.id=${stockItemUom.id} exists but has no uom (UomConversion)") } + println("DEBUG itemId=$itemId: stockItemUom.id=${stockItemUom?.id}, stockUom.uom.id=$stockUomId, availableLots count=${availableLots.size}") + itemId to Pair(stockQty, stockUomId) + }.toMap() + // calculate statistics + val totalStockQty = stockQtyMap.values.sumOf { (qty, _) -> qty.toInt() } +val insufficientStockQty = bomMaterials + .filter { material -> + val itemId = material.item?.id ?: 0L + val (stockQty, _) = stockQtyMap[itemId] ?: (BigDecimal.ZERO to null) + stockQty < (material.reqQty ?: BigDecimal.ZERO) + } + .sumOf { material -> + val itemId = material.item?.id ?: 0L + val (stockQty, _) = stockQtyMap[itemId] ?: (BigDecimal.ZERO to null) + stockQty.toInt() + } +val sufficientStockQty = bomMaterials + .filter { material -> + val itemId = material.item?.id ?: 0L + val (stockQty, _) = stockQtyMap[itemId] ?: (BigDecimal.ZERO to null) + stockQty >= (material.reqQty ?: BigDecimal.ZERO) + } + .sumOf { material -> + val itemId = material.item?.id ?: 0L + val (stockQty, _) = stockQtyMap[itemId] ?: (BigDecimal.ZERO to null) + stockQty.toInt() + } // 获取 productionPriority val itemId = jobOrder?.bom?.item?.id val planEndDate = jobOrder?.planEnd?.toLocalDate() @@ -645,44 +660,176 @@ open class ProductProcessService( }.sortedBy { it.seqNo }, jobOrderLines = bomMaterials.map { line -> val itemId = line.item?.id ?: 0L - val stockQty = stockQtyMap[itemId]?.toInt() ?: 0 - // 首先通过 bomId 和 itemId 找到 BomMaterial + val (stockQtyValue, stockUomId) = stockQtyMap[itemId] ?: (BigDecimal.ZERO to null) + val stockQty = stockQtyValue.toInt() + + // Find BomMaterial val bomMaterial = bom?.id?.let { bomId -> bomMaterialRepository.findByBomIdAndItemId(bomId, itemId) } + + // Get req UOM from bomMaterial (or fallback to line.uom from JobOrderBomMaterial) + val reqUomId = bomMaterial?.uom?.id ?: line.uom?.id ?: 0L + val reqUom = reqUomId.takeIf { it > 0 }?.let { uomConversionRepository.findByIdAndDeletedFalse(it) } + val uomName = reqUom?.udfudesc + val shortUom = reqUom?.udfShortDesc + val stockUom = stockUomId?.takeIf { it > 0 }?.let { uomConversionRepository.findByIdAndDeletedFalse(it) } + val stockUomName = stockUom?.udfudesc + + println("=== Quantity Calculation for Item: ${line.item?.code} (id=$itemId) ===") + println("Original reqQty: ${line.reqQty} in UOM: $uomName (id=$reqUomId)") + println("Original stockQty: $stockQtyValue in UOM: $stockUomName (id=$stockUomId)") + + // Convert reqQty to base unit + val baseReqQtyResult = if (reqUomId > 0) { + try { + // First check if ItemUom exists for this item and UomConversion + val sourceItemUom = itemUomRepository.findByItemIdAndUomIdAndDeletedIsFalse(itemId, reqUomId) + val targetItemUom = itemUomService.findBaseUnitByItemId(itemId) + + println("Converting reqQty: ${line.reqQty} from UOM id=$reqUomId to baseUnit") + println(" Source ItemUom - ratioN: ${sourceItemUom?.ratioN}, ratioD: ${sourceItemUom?.ratioD}") + println(" Target ItemUom (baseUnit) - ratioN: ${targetItemUom?.ratioN}, ratioD: ${targetItemUom?.ratioD}") + + if (sourceItemUom == null) { + println("WARNING: No ItemUom found for itemId=$itemId, uomId=$reqUomId (UomConversion id). Cannot convert reqQty.") + println(" This means the item doesn't have this UOM configured in item_uom table.") + println(" Calculation: Cannot calculate - missing Source ItemUom with ratioN and ratioD") + null + } else if (targetItemUom == null) { + println("WARNING: No baseUnit ItemUom found for itemId=$itemId. Cannot convert reqQty.") + println(" Calculation: Cannot calculate - missing Target ItemUom (baseUnit)") + null + } else { + val sourceRatioN = sourceItemUom.ratioN ?: BigDecimal.ONE + val sourceRatioD = sourceItemUom.ratioD ?: BigDecimal.ONE + val targetRatioN = targetItemUom.ratioN ?: BigDecimal.ONE + val targetRatioD = targetItemUom.ratioD ?: BigDecimal.ONE + + val baseQty = (line.reqQty ?: BigDecimal.ZERO).multiply(sourceRatioN).divide(sourceRatioD, 2, RoundingMode.HALF_UP) + val finalQty = baseQty.multiply(targetRatioD).divide(targetRatioN, 2, RoundingMode.HALF_UP) + + println(" Calculation: baseQty = ${line.reqQty} * $sourceRatioN / $sourceRatioD = $baseQty") + println(" Calculation: finalQty = $baseQty * $targetRatioD / $targetRatioN = $finalQty") + + val result = itemUomService.convertUomByItem( + ConvertUomByItemRequest( + itemId = itemId, + qty = line.reqQty ?: BigDecimal.ZERO, + uomId = reqUomId, + targetUnit = "baseUnit" + ) + ) + println("Converted reqQty result: ${result.newQty} in base UOM: ${result.udfudesc}") + result + } + } catch (e: Exception) { + println("Error converting reqQty: ${e.message}") + e.printStackTrace() + null + } + } else { + println("reqUomId is 0 or invalid, skipping reqQty conversion") + null + } + + val baseReqQty = baseReqQtyResult?.newQty?.toInt() ?: 0 + // Get base unit UOM from the base unit ItemUom (same for both req and stock) + val baseUnitItemUom = itemUomService.findBaseUnitByItemId(itemId) + val baseUomName = baseUnitItemUom?.uom?.udfudesc ?: baseReqQtyResult?.udfudesc + val baseShortUom = baseUnitItemUom?.uom?.udfShortDesc ?: baseReqQtyResult?.udfShortDesc + + // Convert stockQty to base unit (using stockUomId from inventory lot lines) + val baseStockQtyResult = if (stockUomId != null && stockUomId > 0) { + try { + // Get source and target ItemUom to show ratioN/ratioD + val sourceItemUom = itemUomRepository.findByItemIdAndUomIdAndDeletedIsFalse(itemId, stockUomId) + val targetItemUom = itemUomService.findBaseUnitByItemId(itemId) + + println("Converting stockQty: $stockQtyValue from UOM id=$stockUomId to baseUnit") + println(" Source ItemUom - ratioN: ${sourceItemUom?.ratioN}, ratioD: ${sourceItemUom?.ratioD}") + println(" Target ItemUom (baseUnit) - ratioN: ${targetItemUom?.ratioN}, ratioD: ${targetItemUom?.ratioD}") + + if (sourceItemUom != null && targetItemUom != null) { + val sourceRatioN = sourceItemUom.ratioN ?: BigDecimal.ONE + val sourceRatioD = sourceItemUom.ratioD ?: BigDecimal.ONE + val targetRatioN = targetItemUom.ratioN ?: BigDecimal.ONE + val targetRatioD = targetItemUom.ratioD ?: BigDecimal.ONE + + val baseQty = stockQtyValue.multiply(sourceRatioN).divide(sourceRatioD, 2, RoundingMode.HALF_UP) + val finalQty = baseQty.multiply(targetRatioD).divide(targetRatioN, 2, RoundingMode.HALF_UP) + + println(" Calculation: baseQty = $stockQtyValue * $sourceRatioN / $sourceRatioD = $baseQty") + println(" Calculation: finalQty = $baseQty * $targetRatioD / $targetRatioN = $finalQty") + } + + val result = itemUomService.convertUomByItem( + ConvertUomByItemRequest( + itemId = itemId, + qty = stockQtyValue, + uomId = stockUomId, + targetUnit = "baseUnit" + ) + ) + println("Converted stockQty result: ${result.newQty} in base UOM: ${result.udfudesc}") + result + } catch (e: Exception) { + println("Error converting stockQty: ${e.message}") + e.printStackTrace() + null + } + } else { + println("stockUomId is null or invalid, skipping stockQty conversion") + null + } + + val baseStockQty = baseStockQtyResult?.newQty?.toInt() ?: 0 + // Use the same baseUomName for stockBaseUom since it's the same base unit + val baseStockUomName = baseUomName + val baseStockShortUom = baseShortUom + + println("Final values - reqQty: ${line.reqQty?.toInt()}, baseReqQty: $baseReqQty, stockQty: $stockQty, baseStockQty: $baseStockQty") + - // 使用 bom_material.uom_name 而不是关联的 uom 对象 - val uom = uomConversionRepository.findByCodeAndDeletedFalse(bomMaterial?.uomName?:"") - val uomName = uom?.udfudesc - val shortUom = uom?.udfudesc - // 然后遍历 bomProcessIds 找到第一个匹配的 BomProcessMaterial + // Find BomProcessMaterial val bomProcessMaterial = bomMaterial?.id?.let { bomMaterialId -> bomProcessIds.firstNotNullOfOrNull { bomProcessId -> bomProcessMaterialRepository.findByBomProcessIdAndBomMaterialId(bomProcessId, bomMaterialId) } } - // 计算 availableStatus - val availableStatus = if (stockQty >= (line.reqQty?.toInt() ?: 0)) { + + // Calculate availableStatus using base quantities + val availableStatus = if (baseStockQty >= baseReqQty) { "available" } else { "insufficient" } - + + println("Available status: $availableStatus (baseStockQty=$baseStockQty >= baseReqQty=$baseReqQty: ${baseStockQty >= baseReqQty})") + println("=== End Quantity Calculation ===\n") + jobOrderLineInfo( - id = line.id?:0, + id = line.id ?: 0, itemId = itemId, - itemCode = line.item?.code?:"", - itemName = line.item?.name?:"", - reqQty = line.reqQty?.toInt() ?: 0, + itemCode = line.item?.code ?: "", + itemName = line.item?.name ?: "", + + reqQty = line.reqQty?.toInt() ?: 0, + baseReqQty = baseReqQty, + // Add this field if not exists stockQty = stockQty, + baseStockQty = baseStockQty, // Add this field if not exists + reqUom = uomName ?: "", + reqBaseUom = baseUomName ?: "", + stockUom = stockUomName ?: "", + stockBaseUom = baseUomName ?: "", + - uom = uomName?:"", - shortUom = shortUom?:"", - type = line.item?.type?: "", + type = line.item?.type ?: "", availableStatus = availableStatus, - bomProcessId = bomProcessMaterial?.bomProcess?.id?:0, - bomProcessSeqNo = bomProcessMaterial?.bomProcess?.seqNo?:0 + bomProcessId = bomProcessMaterial?.bomProcess?.id ?: 0, + bomProcessSeqNo = bomProcessMaterial?.bomProcess?.seqNo ?: 0 ) } )