diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt index 3fb8007..83d6ef1 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt @@ -654,6 +654,7 @@ open class PickOrderService( println(pos) val groupsByPickOrderId = pickOrderGroupRepository.findByPickOrderIdInAndDeletedIsFalse(ids) .groupBy { it.pickOrderId } + // Get Inventory Data val requiredItems = pos .flatMap { it.pickOrderLines } @@ -666,13 +667,10 @@ open class PickOrderService( override val uomDesc: String? = value[0].uom?.udfudesc override var availableQty: BigDecimal? = zero override val requiredQty: BigDecimal = value.sumOf { it.qty ?: zero } - } } // itemId - requiredQty - + val itemIds = requiredItems.mapNotNull { it.first } -// val inventories = inventoryLotLineRepository.findCurrentInventoryByItems(itemIds) -// val inventories = inventoryService.allInventoriesByItemIds(itemIds) val inventories = inventoryLotLineService .allInventoryLotLinesByItemIdIn(itemIds) .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } @@ -680,37 +678,71 @@ open class PickOrderService( .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } .sortedBy { it.expiryDate } .groupBy { it.item?.id } - + val suggestions = suggestedPickLotService.suggestionForPickOrders(SuggestedPickLotForPoRequest(pickOrders = pos)) + + // ✅ Get stock out line data for each pick order line + val pickOrderLineIds = pos.flatMap { it.pickOrderLines }.mapNotNull { it.id } + val stockOutLinesByPickOrderLineId = if (pickOrderLineIds.isNotEmpty()) { + pickOrderLineIds.associateWith { pickOrderLineId -> + stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) + } + } else { + emptyMap() + } + // Pick Orders val releasePickOrderInfos = pos .map { po -> val releasePickOrderLineInfos = po.pickOrderLines.map { pol -> -// if (pol.item?.id != null && pol.item!!.id!! > 0) { val inventory = pol.item?.id?.let { inventories[it] } val itemUom = pol.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } -// val inventory = inventories.find { it.itemId == pol.item?.id } - + + // ✅ Fix: Convert availableQty from base units to sales units + val convertedAvailableQty = inventory?.sumOf { i -> + val inQty = i.inQty ?: zero + val outQty = i.outQty ?: zero + val holdQty = i.holdQty ?: zero + val baseQty = inQty.minus(outQty).minus(holdQty) + println("Item ID: ${i.item?.id}, InQty: $inQty, OutQty: $outQty, HoldQty: $holdQty, BaseQty: $baseQty") + // Apply unit conversion if needed + val itemUom = pol.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } + val ratioN = itemUom?.ratioN + val ratioD = itemUom?.ratioD + println("Item ID: ${pol.item?.id}, RatioN: $ratioN, RatioD: $ratioD") + val convertedQty: BigDecimal = if (ratioN != null && ratioD != null && ratioD != zero) { + baseQty.divide(ratioN.divide(ratioD, 10, java.math.RoundingMode.HALF_UP), 2, java.math.RoundingMode.HALF_UP) + } else { + baseQty + } + + // Ensure the lambda returns BigDecimal + convertedQty + } + + // ✅ Calculate total picked quantity from stock out lines + val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() + val totalPickedQty = stockOutLines.sumOf { it.qty ?: zero } + // Return GetPickOrderLineInfo( id = pol.id, itemId = pol.item?.id, itemCode = pol.item?.code, itemName = pol.item?.name, -// availableQty = inventory?.availableQty, - availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) }, -// availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) * (itemUom?.ratioN ?: one) * (itemUom?.ratioD ?: one) }, + availableQty = convertedAvailableQty, // ✅ Use converted quantity requiredQty = pol.qty, uomCode = pol.uom?.code, uomDesc = pol.uom?.udfudesc, - suggestedList = suggestions.suggestedList.filter { it.pickOrderLine?.id == pol.id } + suggestedList = suggestions.suggestedList.filter { it.pickOrderLine?.id == pol.id }, + // ✅ Add picked quantity data + pickedQty = totalPickedQty ) -// } } val groupName = po.id?.let { pickOrderId -> - groupsByPickOrderId[pickOrderId]?.firstOrNull()?.name - } ?: "No Group" + groupsByPickOrderId[pickOrderId]?.firstOrNull()?.name + } ?: "No Group" // Return GetPickOrderInfo( id = po.id, @@ -723,34 +755,37 @@ open class PickOrderService( pickOrderLines = releasePickOrderLineInfos ) } - + // Items val currentInventoryInfos = requiredItems.map { item -> -// val inventory = inventories -// .find { it.itemId == item.first } - val inventory = item.first?.let { inventories[it] } val itemUom = item.first?.let { itemUomService.findSalesUnitByItemId(it) } - + item.second.let { -// it.availableQty = inventory?.availableQty - - it.availableQty = inventory?.sumOf { i -> i.availableQty ?: zero } -// it.availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) * (itemUom?.ratioN ?: one) * (itemUom?.ratioD ?: one) } - - // return + val convertedAvailableQty = inventory?.sumOf { i -> + val baseQty = (i.availableQty ?: zero) + val ratioN = itemUom?.ratioN + val ratioD = itemUom?.ratioD + + if (ratioN != null && ratioD != null && ratioD != zero) { + baseQty.divide(ratioN.divide(ratioD, 10, java.math.RoundingMode.HALF_UP), 2, java.math.RoundingMode.HALF_UP) + } else { + baseQty + } + } + + it.availableQty = convertedAvailableQty it } } - + val consoCode = pos.firstOrNull()?.consoCode return GetPickOrderInfoResponse( -// consoCode = consoCode, + consoCode = consoCode, pickOrders = releasePickOrderInfos, items = currentInventoryInfos, ) } - open fun getAllPickOrdersInfo(): GetPickOrderInfoResponse { // 使用现有的查询方法获取所有 Pick Orders,然后在内存中过滤 val allPickOrders = pickOrderRepository.findAll() @@ -763,6 +798,7 @@ open class PickOrderService( // 如果没有任何已发布的 Pick Orders,返回空结果 if (releasedPickOrderIds.isEmpty()) { return GetPickOrderInfoResponse( + consoCode = null, pickOrders = emptyList(), items = emptyList() ) @@ -779,150 +815,72 @@ open class PickOrderService( println("pickOrderLineId: $pickOrderLineId") println("today: $today") - // 检查具体的数量字段值 - val quantityCheckSql = """ - SELECT - spl.id as suggestedPickLotId, - ill.id as lotLineId, - ill.inQty, - ill.outQty, - ill.holdQty, - COALESCE(ill.inQty, 0) as inQtyCoalesced, - COALESCE(ill.outQty, 0) as outQtyCoalesced, - COALESCE(ill.holdQty, 0) as holdQtyCoalesced, - (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQtyCalculated - FROM fpsmsdb.suggested_pick_lot spl - JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId - WHERE spl.pickOrderLineId = :pickOrderLineId - AND ill.status = 'available' - """.trimIndent() - - val quantityCheckResult = jdbcDao.queryForList(quantityCheckSql, mapOf("pickOrderLineId" to pickOrderLineId)) - println("Quantity check result:") - quantityCheckResult.forEach { row -> - println("Row: $row") + // ✅ 重新添加:首先检查是否需要 resuggest + val needsResuggest = checkIfNeedsResuggest(pickOrderLineId) + if (needsResuggest) { + println("⚠️ NEEDS RESUGGEST: Auto-triggering resuggest for pick order line ID: $pickOrderLineId") + try { + // 获取 pick order ID + val pickOrderId = getPickOrderIdFromPickOrderLineId(pickOrderLineId) + if (pickOrderId != null) { + suggestedPickLotService.resuggestPickOrder(pickOrderId) + println("✅ Resuggest completed successfully for pick order ID: $pickOrderId") + } else { + println("❌ Could not find pick order ID for pick order line ID: $pickOrderLineId") + } + } catch (e: Exception) { + println("❌ Error during auto-resuggest: ${e.message}") + } + } else { + println("✅ No resuggest needed for pick order line ID: $pickOrderLineId") } - // 检查日期条件 - val dateCheckSql = """ + val sql = """ SELECT - spl.id as suggestedPickLotId, - ill.id as lotLineId, + ill.id as lotId, il.lotNo, il.expiryDate, - :today as today, - il.expiryDate >= :today as isNotExpired, - il.expiryDate IS NULL as isNullExpiry - FROM fpsmsdb.suggested_pick_lot spl - JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId - JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId - WHERE spl.pickOrderLineId = :pickOrderLineId - AND ill.status = 'available' - AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 - """.trimIndent() - - val dateCheckResult = - jdbcDao.queryForList(dateCheckSql, mapOf("pickOrderLineId" to pickOrderLineId, "today" to today)) - println("Date check result:") - dateCheckResult.forEach { row -> - println("Row: $row") - } - - // 检查 warehouse JOIN - val warehouseCheckSql = """ - SELECT - spl.id as suggestedPickLotId, - ill.id as lotLineId, - ill.warehouseId, - w.id as warehouseId, - w.name as warehouseName - FROM fpsmsdb.suggested_pick_lot spl - JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId - JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId - JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId - WHERE spl.pickOrderLineId = :pickOrderLineId - AND ill.status = 'available' - AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 - AND (il.expiryDate IS NULL OR il.expiryDate >= :today) - """.trimIndent() - - val warehouseCheckResult = - jdbcDao.queryForList(warehouseCheckSql, mapOf("pickOrderLineId" to pickOrderLineId, "today" to today)) - println("Warehouse check result count: ${warehouseCheckResult.size}") - warehouseCheckResult.forEach { row -> - println("Warehouse Row: $row") - } - - // 检查 uom_conversion JOIN - 使用 LEFT JOIN 并检查 stockItemUomId - val uomCheckSql = """ - SELECT + w.name as location, + COALESCE(uc.udfudesc, 'N/A') as stockUnit, + -- ✅ 修复:availableQty 应该显示实际可用数量(保持小数) + CASE + WHEN sales_iu.ratioN IS NOT NULL AND sales_iu.ratioD IS NOT NULL AND sales_iu.ratioD != 0 THEN + (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) / (sales_iu.ratioN / sales_iu.ratioD) + ELSE + (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) + END as availableQty, + -- ✅ 修复:requiredQty 直接使用 spl.qty,因为它已经是销售单位 + spl.qty as requiredQty, + COALESCE(sol.qty, 0) as actualPickQty, spl.id as suggestedPickLotId, - ill.id as lotLineId, - ill.stockItemUomId, - uc.id as uomId, - uc.code as uomCode + ill.status as lotStatus, + sol.id as stockOutLineId, + sol.status as stockOutLineStatus, + sol.qty as stockOutLineQty, + spl.suggestedLotLineId as debugSuggestedLotLineId, + ill.inventoryLotId as debugInventoryLotId, + CASE + WHEN ill.status != 'available' THEN 'unavailable' + WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' + WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) < (spl.qty * (sales_iu.ratioN / sales_iu.ratioD)) THEN 'insufficient_stock' + ELSE 'available' + END as lotAvailability FROM fpsmsdb.suggested_pick_lot spl JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId - JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId - LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = ill.stockItemUomId + LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId + LEFT JOIN fpsmsdb.item_uom sales_iu ON sales_iu.itemId = il.itemId AND sales_iu.salesUnit = true AND sales_iu.deleted = false + LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = sales_iu.uomId + LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = spl.pickOrderLineId AND sol.inventoryLotLineId = spl.suggestedLotLineId WHERE spl.pickOrderLineId = :pickOrderLineId - AND ill.status = 'available' - AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 - AND (il.expiryDate IS NULL OR il.expiryDate >= :today) + ORDER BY il.expiryDate ASC, il.lotNo ASC """.trimIndent() - val uomCheckResult = - jdbcDao.queryForList(uomCheckSql, mapOf("pickOrderLineId" to pickOrderLineId, "today" to today)) - println("UOM check result count: ${uomCheckResult.size}") - uomCheckResult.forEach { row -> - println("UOM Row: $row") - } + println("🔍 Executing SQL for lot details: $sql") + println("🔍 With parameters: pickOrderLineId = $pickOrderLineId") - // 修改查询,通过 item_uom 表获取 UOM 信息 - val sql = """ - SELECT - ill.id as lotId, - il.lotNo, - il.expiryDate, - w.name as location, - COALESCE(uc.udfudesc, 'N/A') as stockUnit, - (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty, - spl.qty as requiredQty, - COALESCE(sol.qty, 0) as actualPickQty, - spl.id as suggestedPickLotId, - ill.status as lotStatus, - CASE - WHEN ill.status != 'available' THEN 'status_unavailable' - WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < :today) THEN 'expired' - WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) < spl.qty THEN 'insufficient_stock' - ELSE 'available' - END as lotAvailability - FROM fpsmsdb.suggested_pick_lot spl - JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId - JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId - JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId - LEFT JOIN fpsmsdb.item_uom iu ON iu.id = ill.stockItemUomId - LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = iu.uomId - LEFT JOIN fpsmsdb.stock_out_line sol ON sol.inventoryLotLineId = ill.id - AND sol.pickOrderLineId = spl.pickOrderLineId - WHERE spl.pickOrderLineId = :pickOrderLineId - ORDER BY - CASE lotAvailability - WHEN 'available' THEN 1 - WHEN 'insufficient_stock' THEN 2 - WHEN 'expired' THEN 3 - WHEN 'status_unavailable' THEN 4 - END, - il.expiryDate ASC - """.trimIndent() - - val params = mapOf( - "pickOrderLineId" to pickOrderLineId, - "today" to today - ) + val result = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to pickOrderLineId)) - val result = jdbcDao.queryForList(sql, params) println("Final result count: ${result.size}") result.forEach { row -> println("Final Row: $row") @@ -931,7 +889,6 @@ open class PickOrderService( return result } - @Transactional(rollbackFor = [java.lang.Exception::class]) open fun releaseConsoPickOrderAction(request: ReleaseConsoPickOrderRequest): ReleasePickOrderInfoResponse { val zero = BigDecimal.ZERO @@ -993,7 +950,6 @@ open class PickOrderService( val releasedBy = SecurityUtils.getUser().getOrNull() val user = userService.find(assignTo).orElse(null) - val pickOrders = pickOrderRepository.findAllByIdInAndStatus(pickOrderIds, PickOrderStatus.ASSIGNED) if (pickOrders.isEmpty()) { @@ -1007,36 +963,37 @@ open class PickOrderService( ) } + // ← GENERATE CONSOCODE EARLY AND SAVE IMMEDIATELY + val newConsoCode = assignConsoCode() + val currUser = SecurityUtils.getUser().orElseThrow() + // Create and save StockOut immediately to prevent duplicate consoCodes + val stockOut = StockOut().apply { + this.type = "job" + this.consoPickOrderCode = newConsoCode + this.status = StockOutStatus.PENDING.status + this.handler = currUser.id + } + val savedStockOut = stockOutRepository.saveAndFlush(stockOut) // ← FLUSH to commit immediately + + // Update pick orders pickOrders.forEach { pickOrder -> pickOrder.apply { this.releasedBy = releasedBy status = PickOrderStatus.RELEASED this.assignTo = user + this.consoCode = newConsoCode // ← Also assign consoCode to pick orders } } - val suggestions = suggestedPickLotService.suggestionForPickOrders( SuggestedPickLotForPoRequest(pickOrders = pickOrders) ) - - val currUser = SecurityUtils.getUser().orElseThrow() - val stockOut = StockOut().apply { - this.type = "job" - this.consoPickOrderCode = null // 单个 pick orders 没有 consoCode - this.status = StockOutStatus.PENDING.status - this.handler = currUser.id - } - stockOutRepository.save(stockOut) - - val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) pickOrderRepository.saveAll(pickOrders) - val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } ) @@ -1059,7 +1016,8 @@ open class PickOrderService( code = "SUCCESS", type = "pickorder", message = "Pick orders released successfully with inventory management", - errorPosition = null + errorPosition = null, + entity = mapOf("consoCode" to newConsoCode) // ← Return the generated consoCode ) } catch (e: Exception) { @@ -1228,4 +1186,139 @@ open class PickOrderService( errorPosition = "", ) } + + // ✅ 添加缺失的方法:从 pick order line ID 获取 pick order ID + private fun getPickOrderIdFromPickOrderLineId(pickOrderLineId: Long): Long? { + val sql = """ + SELECT po.id as pickOrderId + FROM fpsmsdb.pick_order_line pol + JOIN fpsmsdb.pick_order po ON po.id = pol.poId + WHERE pol.id = :pickOrderLineId + """.trimIndent() + + val result = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to pickOrderLineId)) + + return if (result.isNotEmpty()) { + (result[0]["pickOrderId"] as Number).toLong() + } else { + null + } + } + + // ✅ 添加检查是否需要 resuggest 的方法 + private fun checkIfNeedsResuggest(pickOrderLineId: Long): Boolean { + println("🔍 checkIfNeedsResuggest called with pickOrderLineId: $pickOrderLineId") + + // ✅ 首先执行一个调试查询来查看实际的数值 + val debugSql = """ + SELECT + spl.id as suggestedPickLotId, + spl.qty as suggestedQty, + ill.inQty, + ill.outQty, + ill.holdQty, + (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as baseAvailableQty, + sales_iu.ratioN, + sales_iu.ratioD, + CASE + WHEN sales_iu.ratioN IS NOT NULL AND sales_iu.ratioD IS NOT NULL AND sales_iu.ratioD != 0 + THEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) / (sales_iu.ratioN / sales_iu.ratioD) + ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) + END as convertedAvailableQty + FROM fpsmsdb.suggested_pick_lot spl + JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId + JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId + LEFT JOIN fpsmsdb.item_uom sales_iu ON sales_iu.itemId = il.itemId AND sales_iu.salesUnit = true AND sales_iu.deleted = false + WHERE spl.pickOrderLineId = :pickOrderLineId + """.trimIndent() + + println("🔍 Debug SQL: $debugSql") + val debugResult = jdbcDao.queryForList(debugSql, mapOf("pickOrderLineId" to pickOrderLineId)) + println("🔍 Debug result:") + debugResult.forEach { row -> + println(" SuggestedPickLotId: ${row["suggestedPickLotId"]}") + println(" SuggestedQty: ${row["suggestedQty"]}") + println(" InQty: ${row["inQty"]}") + println(" OutQty: ${row["outQty"]}") + println(" HoldQty: ${row["holdQty"]}") + println(" BaseAvailableQty: ${row["baseAvailableQty"]}") + println(" RatioN: ${row["ratioN"]}") + println(" RatioD: ${row["ratioD"]}") + println(" ConvertedAvailableQty: ${row["convertedAvailableQty"]}") + // ✅ 修复:正确处理 Number 类型转换 + val convertedAvailableQty = (row["convertedAvailableQty"] as Number).toDouble() + val suggestedQty = (row["suggestedQty"] as Number).toDouble() + println(" Is Insufficient: ${convertedAvailableQty < suggestedQty}") + println(" ---") + } + + val checkSql = """ + SELECT + COUNT(*) as totalSuggestions, + COUNT(CASE WHEN ill.status != 'available' THEN 1 END) as unavailableLots, + COUNT(CASE WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 1 END) as expiredLots, + -- ✅ 修复:使用销售单位进行比较 + COUNT(CASE WHEN + CASE + WHEN sales_iu.ratioN IS NOT NULL AND sales_iu.ratioD IS NOT NULL AND sales_iu.ratioD != 0 + THEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) / (sales_iu.ratioN / sales_iu.ratioD) + ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) + END < spl.qty + THEN 1 END) as insufficientStockLots, + COUNT(CASE WHEN sol.status IN ('rejected', 'lot-change', 'determine1') THEN 1 END) as problematicStockOutLines, + -- ✅ 检查单位不一致问题 + COUNT(CASE WHEN spl.qty > pol.qty * 100 THEN 1 END) as unitMismatchLots + FROM fpsmsdb.suggested_pick_lot spl + JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId + JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId + JOIN fpsmsdb.pick_order_line pol ON pol.id = spl.pickOrderLineId + -- ✅ 添加销售单位转换 + LEFT JOIN fpsmsdb.item_uom sales_iu ON sales_iu.itemId = il.itemId AND sales_iu.salesUnit = true AND sales_iu.deleted = false + LEFT JOIN fpsmsdb.stock_out_line sol ON sol.inventoryLotLineId = ill.id + AND sol.pickOrderLineId = spl.pickOrderLineId + WHERE spl.pickOrderLineId = :pickOrderLineId + """.trimIndent() + + println("🔍 Executing SQL: $checkSql") + println("🔍 With parameters: pickOrderLineId = $pickOrderLineId") + + val result = jdbcDao.queryForList(checkSql, mapOf("pickOrderLineId" to pickOrderLineId)) + + println("🔍 SQL result size: ${result.size}") + + if (result.isNotEmpty()) { + val row = result[0] + val totalSuggestions = (row["totalSuggestions"] as Number).toInt() + val unavailableLots = (row["unavailableLots"] as Number).toInt() + val expiredLots = (row["expiredLots"] as Number).toInt() + val insufficientStockLots = (row["insufficientStockLots"] as Number).toInt() + val problematicStockOutLines = (row["problematicStockOutLines"] as Number).toInt() + val unitMismatchLots = (row["unitMismatchLots"] as Number).toInt() + + println("=== Resuggest Check ===") + println("Total suggestions: $totalSuggestions") + println("Unavailable lots: $unavailableLots") + println("Expired lots: $expiredLots") + println("Insufficient stock lots: $insufficientStockLots") + println("Problematic stock out lines: $problematicStockOutLines") + println("Unit mismatch lots: $unitMismatchLots") + + // 检查是否需要 resuggest 的条件 + val needsResuggest = ( + unavailableLots > 0 || + expiredLots > 0 || + insufficientStockLots > 0 || + problematicStockOutLines > 0 || + unitMismatchLots > 0 || // ✅ 单位不一致检查 + totalSuggestions == 0 // 没有建议也需要 resuggest + ) + + println("Needs resuggest: $needsResuggest") + return needsResuggest + } else { + println("🔍 No results returned from SQL query") + } + + return false + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt index 4677edf..dc50156 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt @@ -13,7 +13,8 @@ data class ReleasePickOrderInfoResponse( ) data class GetPickOrderInfoResponse( -// val consoCode: String, + val consoCode: String?, + val pickOrders: List, val items: List ) @@ -60,6 +61,7 @@ data class GetPickOrderLineInfo( val uomCode: String?, val uomDesc: String?, val suggestedList: List?, + val pickedQty: BigDecimal?=BigDecimal.ZERO, ) // Final Response - Conso Pick Order Detail diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt index f359c6d..08d00da 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt @@ -16,4 +16,8 @@ interface StockOutLIneRepository: AbstractRepository { fun findStockOutLineInfoById(id: Long): StockOutLineInfo fun findAllByStockOutId(stockOutId: Long): List + fun findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( + pickOrderLineId: Long, + inventoryLotLineId: Long + ): List } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt index c477824..f9b1e97 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt @@ -7,4 +7,8 @@ import org.springframework.stereotype.Repository @Repository interface SuggestPickLotRepository : AbstractRepository { fun findAllByPickOrderLineIn(lines: List): List + fun findAllByPickOrderLineIdIn(pickOrderLineIds: List): List + + + fun findAllByPickOrderLineId(pickOrderLineId: Long): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt index afd93f1..a4369e1 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt @@ -13,6 +13,8 @@ import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo import com.ffii.fpsms.modules.stock.entity.projection.LotLineToQrcode import com.ffii.fpsms.modules.stock.web.InventoryLotLineController +import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineStatusRequest + import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest import com.ffii.fpsms.modules.stock.web.model.SaveInventoryLotLineRequest import com.ffii.fpsms.modules.stock.web.model.SearchInventoryLotLineInfoRequest @@ -29,7 +31,7 @@ import java.math.BigDecimal import java.time.format.DateTimeFormatter import java.util.Optional import kotlin.jvm.optionals.getOrNull - +import com.ffii.fpsms.modules.master.web.models.MessageResponse @Service open class InventoryLotLineService( private val inventoryLotLineRepository: InventoryLotLineRepository, @@ -87,7 +89,45 @@ open class InventoryLotLineService( return inventoryLotLineRepository.save(inventoryLotLine) } - + @Transactional + open fun updateInventoryLotLineStatus(request: UpdateInventoryLotLineStatusRequest): MessageResponse { + try { + val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() + + // 使用现有的 saveInventoryLotLine 逻辑 + val updateRequest = SaveInventoryLotLineRequest( + id = inventoryLotLine.id, + inventoryLotId = inventoryLotLine.inventoryLot?.id, + warehouseId = inventoryLotLine.warehouse?.id, + stockUomId = inventoryLotLine.stockUom?.id, + inQty = inventoryLotLine.inQty, + outQty = inventoryLotLine.outQty, + holdQty = inventoryLotLine.holdQty, + status = request.status, // 新的状态 + remarks = inventoryLotLine.remarks + ) + + val updatedInventoryLotLine = saveInventoryLotLine(updateRequest) + + return MessageResponse( + id = updatedInventoryLotLine.id, + name = "Inventory lot line status updated", + code = "SUCCESS", + type = "inventory_lot_line", + message = "Status updated to ${request.status}", + errorPosition = null + ) + } catch (e: Exception) { + return MessageResponse( + id = null, + name = "Failed to update inventory lot line status", + code = "ERROR", + type = "inventory_lot_line", + message = "Error: ${e.message}", + errorPosition = null + ) + } + } @Throws(IOException::class) @Transactional open fun exportStockInLineQrcode(request: LotLineToQrcode): Map { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 2a5eaaa..405c147 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt @@ -66,6 +66,23 @@ open class StockOutLineService( open fun create(request: CreateStockOutLineRequest): MessageResponse { // pick flow step 1 // println(request.pickOrderLineId) +val existingStockOutLine = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( + request.pickOrderLineId, + request.inventoryLotLineId + ) + + if (existingStockOutLine.isNotEmpty()) { + // ✅ 如果已存在,返回 null 表示不需要创建 + return MessageResponse( + id = null, + name = "Stock out line already exists", + code = "EXISTS", + type = "stock_out_line", + message = "Stock out line already exists for this pick order line and inventory lot line", + errorPosition = null, + entity = null, + ) + } val stockOut = stockOutRepository.findByConsoPickOrderCode(request.consoCode).orElseThrow() val pickOrderLine = pickOrderLineRepository.saveAndFlush( pickOrderLineRepository.findById(request.pickOrderLineId).orElseThrow() @@ -97,48 +114,146 @@ open class StockOutLineService( entity = mappedSavedStockOutLine, ) } + @Transactional +fun handleQc(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List { + var newStockOutLine: StockOutLine? = null + if (request.qty < stockOutLine.qty!!) { + newStockOutLine = StockOutLine().apply { + this.pickOrderLine = stockOutLine.pickOrderLine + this.stockOut = stockOutLine.stockOut + this.item = stockOutLine.item + this.qty = stockOutLine.qty!! - request.qty + this.status = StockOutLineStatus.DETERMINE1.status // escalated + } + } + val inventoryLotLine = if (request.inventoryLotLineId != null) + inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() + else null + stockOutLine.apply { + this.inventoryLotLine = inventoryLotLine ?: stockOutLine.inventoryLotLine + this.qty = request.qty + this.status = StockOutLineStatus.COMPLETE.status // complete + } + // update inventory lot line + val zero = BigDecimal.ZERO + val one = BigDecimal.ONE + val targetLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId!!).orElseThrow() + val salesUnit = inventoryLotLine?.inventoryLot?.item?.id?.let {_itemId -> itemUomRespository.findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(_itemId) } + val ratio = (salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one).toDouble() + val targetLotLineEntry = targetLotLine.apply { + this.outQty = (this.outQty?: BigDecimal.ZERO) + (request.qty.div(ratio)).toBigDecimal() + this.holdQty = this.holdQty!!.minus((request.qty.div(ratio)).toBigDecimal()) + } + inventoryLotLineRepository.save(targetLotLineEntry) + // update inventory + val inventory = inventoryRepository.findByItemId(request.itemId).orElseThrow() + val inventoryEntry = inventory.apply { + this.onHandQty = this.onHandQty!!.minus((request.qty.div(ratio)).toBigDecimal()) + this.onHoldQty = this.onHoldQty!!.minus((request.qty.div(ratio)).toBigDecimal()) + } + inventoryRepository.save(inventoryEntry) + return listOf(stockOutLine, newStockOutLine) +} @Transactional - fun handleQc(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List { - var newStockOutLine: StockOutLine? = null - if (request.qty < stockOutLine.qty!!) { - newStockOutLine = StockOutLine().apply { - this.pickOrderLine = stockOutLine.pickOrderLine - this.stockOut = stockOutLine.stockOut - this.item = stockOutLine.item - this.qty = stockOutLine.qty!! - request.qty - this.status = StockOutLineStatus.DETERMINE1.status // escalated - } +open fun createWithoutConso(request: CreateStockOutLineWithoutConsoRequest): MessageResponse { + try { + // ✅ Get stockOutId from pickOrderLineId with detailed error + val stockOutId = getStockOutIdFromPickOrderLine(request.pickOrderLineId) + println("Found stockOutId: $stockOutId for pickOrderLineId: ${request.pickOrderLineId}") + + val stockOut = stockOutRepository.findById(stockOutId).orElseThrow { + IllegalArgumentException("StockOut not found with ID: $stockOutId") } - val inventoryLotLine = if (request.inventoryLotLineId != null) - inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() - else null - stockOutLine.apply { - this.inventoryLotLine = inventoryLotLine ?: stockOutLine.inventoryLotLine - this.qty = request.qty - this.status = StockOutLineStatus.COMPLETE.status // complete + println("Found stockOut: ${stockOut.id} with consoCode: ${stockOut.consoPickOrderCode}") + + val pickOrderLine = pickOrderLineRepository.findById(request.pickOrderLineId).orElseThrow { + IllegalArgumentException("PickOrderLine not found with ID: ${request.pickOrderLineId}") } - // update inventory lot line - val zero = BigDecimal.ZERO - val one = BigDecimal.ONE - val targetLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId!!).orElseThrow() - val salesUnit = inventoryLotLine?.inventoryLot?.item?.id?.let {_itemId -> itemUomRespository.findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(_itemId) } - val ratio = (salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one).toDouble() - val targetLotLineEntry = targetLotLine.apply { - - this.outQty = (this.outQty?: BigDecimal.ZERO) + (request.qty.div(ratio)).toBigDecimal() - this.holdQty = this.holdQty!!.minus((request.qty.div(ratio)).toBigDecimal()) + println("Found pickOrderLine: ${pickOrderLine.id}") + + val updatedPickOrderLine = pickOrderLineRepository.saveAndFlush( + pickOrderLine.apply { + this.status = PickOrderLineStatus.PICKING + } + ) + println("Updated pickOrderLine status to: ${updatedPickOrderLine.status}") + + val item = itemRepository.findById(updatedPickOrderLine.item!!.id!!).orElseThrow { + IllegalArgumentException("Item not found with ID: ${updatedPickOrderLine.item!!.id}") } - inventoryLotLineRepository.save(targetLotLineEntry) - // update inventory - val inventory = inventoryRepository.findByItemId(request.itemId).orElseThrow() - val inventoryEntry = inventory.apply { - this.onHandQty = this.onHandQty!!.minus((request.qty.div(ratio)).toBigDecimal()) - this.onHoldQty = this.onHoldQty!!.minus((request.qty.div(ratio)).toBigDecimal()) + println("Found item: ${item.id} - ${item.code}") + + val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow { + IllegalArgumentException("InventoryLotLine not found with ID: ${request.inventoryLotLineId}") } - inventoryRepository.save(inventoryEntry) - return listOf(stockOutLine, newStockOutLine) + println("Found inventoryLotLine: ${inventoryLotLine.id}") + + val stockOutLine = StockOutLine() + .apply { + this.item = item + this.qty = request.qty + this.stockOut = stockOut + this.inventoryLotLine = inventoryLotLine + this.pickOrderLine = updatedPickOrderLine + this.status = StockOutLineStatus.PENDING.status + } + println("Created stockOutLine with qty: ${request.qty}") + + val savedStockOutLine = saveAndFlush(stockOutLine) + println("Saved stockOutLine with ID: ${savedStockOutLine.id}") + + val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) + println("Mapped stockOutLine info: $mappedSavedStockOutLine") + + return MessageResponse( + id = savedStockOutLine.id, + name = savedStockOutLine.inventoryLotLine!!.inventoryLot!!.lotNo, + code = savedStockOutLine.stockOut!!.consoPickOrderCode, + type = savedStockOutLine.status, + message = "success", + errorPosition = null, + entity = mappedSavedStockOutLine, + ) + } catch (e: Exception) { + println("Error in createWithoutConso: ${e.message}") + e.printStackTrace() + throw e + } +} + +// ✅ Update helper method with detailed error messages +private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { + println("Getting stockOutId for pickOrderLineId: $pickOrderLineId") + + // ✅ Fixed: Use poId instead of pick_order_id + val sql = """ + SELECT so.id as stockOutId, so.consoPickOrderCode, po.consoCode + FROM stock_out so + JOIN pick_order po ON po.consoCode = so.consoPickOrderCode + JOIN pick_order_line pol ON pol.poId = po.id -- ✅ Fixed: Use poId + WHERE pol.id = :pickOrderLineId + """.trimIndent() + + val result = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to pickOrderLineId)) + println("SQL result: $result") + + if (result.isEmpty()) { + throw IllegalArgumentException("No StockOut found for pickOrderLineId: $pickOrderLineId. Check if pick order line exists and has associated stock out.") } + + val stockOutId = result[0]["stockOutId"] as? Long + val consoPickOrderCode = result[0]["consoPickOrderCode"] as? String + val consoCode = result[0]["consoCode"] as? String + + println("Found stockOutId: $stockOutId, consoPickOrderCode: $consoPickOrderCode, consoCode: $consoCode") + + if (stockOutId == null) { + throw IllegalArgumentException("StockOut ID is null for pickOrderLineId: $pickOrderLineId. ConsoCode: $consoCode, ConsoPickOrderCode: $consoPickOrderCode") + } + + return stockOutId +} @Transactional fun handleLotChangeApprovalOrReject(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List { /** @@ -244,4 +359,44 @@ open class StockOutLineService( entity = lineInfoList, ) } + + @Transactional + open fun updateStatus(request: UpdateStockOutLineStatusRequest): MessageResponse { + try { + val stockOutLine = stockOutLineRepository.findById(request.id).orElseThrow { + IllegalArgumentException("StockOutLine not found with ID: ${request.id}") + } + + println("Updating StockOutLine ID: ${request.id}") + println("Current status: ${stockOutLine.status}") + println("New status: ${request.status}") + + // Update status + stockOutLine.status = request.status + + // Update quantity if provided + if (request.qty != null) { + stockOutLine.qty = request.qty + } + + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}") + + val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) + + return MessageResponse( + id = savedStockOutLine.id, + name = savedStockOutLine.inventoryLotLine!!.inventoryLot!!.lotNo, + code = savedStockOutLine.stockOut!!.consoPickOrderCode, + type = savedStockOutLine.status, + message = "Stock out line status updated successfully", + errorPosition = null, + entity = mappedSavedStockOutLine, + ) + } catch (e: Exception) { + println("Error updating stock out line status: ${e.message}") + e.printStackTrace() + throw e + } + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index 3c27478..ee98b12 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt @@ -17,7 +17,10 @@ import java.math.BigDecimal import java.time.LocalDate import kotlin.jvm.optionals.getOrDefault import kotlin.jvm.optionals.getOrNull - +import org.springframework.transaction.annotation.Transactional +import com.ffii.fpsms.modules.master.web.models.MessageResponse +import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository +import java.math.RoundingMode @Service open class SuggestedPickLotService( val suggestedPickLotRepository: SuggestPickLotRepository, @@ -26,6 +29,7 @@ open class SuggestedPickLotService( val pickOrderLineRepository: PickOrderLineRepository, val inventoryLotLineService: InventoryLotLineService, val itemUomService: ItemUomService, + val pickOrderRepository: PickOrderRepository ) { // Calculation Available Qty / Remaining Qty open fun calculateRemainingQtyForInfo(inventoryLotLine: InventoryLotLineInfo?): BigDecimal { @@ -84,22 +88,28 @@ open class SuggestedPickLotService( pols.forEach { line -> val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } val lotLines = availableInventoryLotLines[line.item?.id].orEmpty() - val ratio = (salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one) - var remainingQty = (line.qty ?: zero).multiply(ratio) - println("remaining1 $remainingQty") + val ratio = (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) + + // ✅ 修复:remainingQty 应该是销售单位,不需要乘以 ratio + var remainingQty = line.qty ?: zero + println("remaining1 $remainingQty (sales units)") val updatedLotLines = mutableListOf() lotLines.forEachIndexed { index, lotLine -> if (remainingQty <= zero) return@forEachIndexed println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}") - val availableQty = calculateRemainingQtyForInfo(lotLine) - .multiply(ratio) - .minus(holdQtyMap[lotLine.id] ?: zero) + + // ✅ 修复:计算可用数量,转换为销售单位 + val availableQtyInBaseUnits = calculateRemainingQtyForInfo(lotLine) + val holdQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero + val availableQtyInSalesUnits = availableQtyInBaseUnits + .minus(holdQtyInBaseUnits) + .divide(ratio, 2, RoundingMode.HALF_UP) + println("holdQtyMap[lotLine.id] ?: zero ${holdQtyMap[lotLine.id] ?: zero}") - - if (availableQty <= zero) { + if (availableQtyInSalesUnits <= zero) { updatedLotLines += lotLine return@forEachIndexed } @@ -107,27 +117,30 @@ open class SuggestedPickLotService( val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } val originalHoldQty = inventoryLotLine?.holdQty - // Update Qty - val assignQty = minOf(availableQty, remainingQty) - remainingQty = remainingQty.minus(assignQty) - holdQtyMap[lotLine.id] = (holdQtyMap[lotLine.id] ?: zero).plus(assignQty) -// lotLine.holdQty = lotLine.holdQty?.plus(assignQty) + // ✅ 修复:在销售单位中计算分配数量 + val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQty) + remainingQty = remainingQty.minus(assignQtyInSalesUnits) + + // ✅ 修复:将销售单位转换为基础单位来更新 holdQty + val assignQtyInBaseUnits = assignQtyInSalesUnits.multiply(ratio) + holdQtyMap[lotLine.id] = (holdQtyMap[lotLine.id] ?: zero).plus(assignQtyInBaseUnits) + suggestedList += SuggestedPickLot().apply { type = SuggestedPickLotType.PICK_ORDER suggestedLotLine = inventoryLotLine pickOrderLine = line - qty = assignQty + qty = assignQtyInSalesUnits // ✅ 保存销售单位 } } // if still have remainingQty - println("remaining2 $remainingQty") + println("remaining2 $remainingQty (sales units)") if (remainingQty > zero) { suggestedList += SuggestedPickLot().apply { type = SuggestedPickLotType.PICK_ORDER suggestedLotLine = null pickOrderLine = line - qty = remainingQty + qty = remainingQty // ✅ 保存销售单位 } } } @@ -184,4 +197,305 @@ open class SuggestedPickLotService( open fun saveAll(request: List): List { return suggestedPickLotRepository.saveAllAndFlush(request) } -} \ No newline at end of file + @Transactional(rollbackFor = [Exception::class]) + open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { + try { + val pickOrder = pickOrderRepository.findById(pickOrderId).orElseThrow() + + // Step 1: Find ALL suggestions for this pick order (not just problematic ones) + val allSuggestions = findAllSuggestionsForPickOrder(pickOrderId) + + if (allSuggestions.isEmpty()) { + return MessageResponse( + id = pickOrderId, + name = "No suggestions found", + code = "SUCCESS", + type = "resuggest", + message = "No suggestions to resuggest", + errorPosition = null + ) + } + + // Step 2: Calculate total excess quantity from problematic suggestions + val problematicSuggestions = findProblematicSuggestions(pickOrderId) + val totalExcessQty = problematicSuggestions.sumOf { suggestion -> + val pickOrderLine = pickOrder.pickOrderLines.find { it.id == suggestion.pickOrderLine?.id } + val inventoryLotLine = suggestion.suggestedLotLine + + if (pickOrderLine == null || inventoryLotLine == null) { + BigDecimal.ZERO + } else { + val salesUnit = pickOrderLine.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } + val ratio = (salesUnit?.ratioN ?: BigDecimal.ONE).divide(salesUnit?.ratioD ?: BigDecimal.ONE, 10, RoundingMode.HALF_UP) + + val availableQtyInBaseUnits = (inventoryLotLine.inQty ?: BigDecimal.ZERO) + .minus(inventoryLotLine.outQty ?: BigDecimal.ZERO) + .minus(inventoryLotLine.holdQty ?: BigDecimal.ZERO) + + val suggestionQtyInBaseUnits = (suggestion.qty ?: BigDecimal.ZERO).multiply(ratio) + val excessQtyInBaseUnits = suggestionQtyInBaseUnits.minus(availableQtyInBaseUnits) + + if (excessQtyInBaseUnits > BigDecimal.ZERO) { + excessQtyInBaseUnits + } else { + BigDecimal.ZERO + } + } + } + + // Step 3: Remove ALL existing suggestions and reset holdQty + val affectedLotLineIds = allSuggestions.mapNotNull { it.suggestedLotLine?.id }.distinct() + + val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(affectedLotLineIds) + inventoryLotLines.forEach { lotLine -> + // ✅ FIX: Reset holdQty to 0 for lots being resuggested + lotLine.holdQty = BigDecimal.ZERO + } + inventoryLotLineRepository.saveAll(inventoryLotLines) + + // Delete ALL suggestions for this pick order + suggestedPickLotRepository.deleteAllById(allSuggestions.mapNotNull { it.id }) + + // Step 4: Generate completely new suggestions following FEFO + val newSuggestions = generateCorrectSuggestionsWithExistingHolds(pickOrder) + + // Step 5: Save new suggestions and update holdQty + val savedSuggestions = suggestedPickLotRepository.saveAll(newSuggestions) + + val newInventoryLotLines = inventoryLotLineRepository.findAllByIdIn( + savedSuggestions.mapNotNull { it.suggestedLotLine?.id } + ) + /* + savedSuggestions.forEach { suggestion -> + val lotLine = newInventoryLotLines.find { it.id == suggestion.suggestedLotLine?.id } + lotLine?.let { + it.holdQty = (it.holdQty ?: BigDecimal.ZERO).plus(suggestion.qty ?: BigDecimal.ZERO) + } + } + inventoryLotLineRepository.saveAll(newInventoryLotLines) + */ + return MessageResponse( + id = pickOrderId, + name = "Pick order resuggested successfully", + code = "SUCCESS", + type = "resuggest", + message = "Redistributed ${allSuggestions.size} suggestions following FEFO order", + errorPosition = null + ) + + } catch (e: Exception) { + return MessageResponse( + id = pickOrderId, + name = "Failed to resuggest pick order", + code = "ERROR", + type = "resuggest", + message = "Error: ${e.message}", + errorPosition = null + ) + } + } + private fun findAllSuggestionsForPickOrder(pickOrderId: Long): List { + val pickOrderLines = pickOrderRepository.findById(pickOrderId) + .orElseThrow() + .pickOrderLines + + val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } + + return suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) + } + private fun generateSuggestionsForExcessQuantity(pickOrder: PickOrder, excessQtyInBaseUnits: BigDecimal): List { + val suggestions = mutableListOf() + val zero = BigDecimal.ZERO + val one = BigDecimal.ONE + + pickOrder.pickOrderLines.forEach { orderLine -> + val itemId = orderLine.item?.id ?: return@forEach + + // Get sales unit conversion ratio + val salesUnit = itemUomService.findSalesUnitByItemId(itemId) + val ratio = (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) + + // Convert excess quantity to sales units + val excessQtyInSalesUnits = excessQtyInBaseUnits.divide(ratio, 0, RoundingMode.HALF_UP) + + if (excessQtyInSalesUnits <= zero) return@forEach + + // Get available inventory lots for this item (FEFO order) + val availableLots = inventoryLotLineService + .allInventoryLotLinesByItemIdIn(listOf(itemId)) + .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } + .sortedBy { it.expiryDate } + + var remainingQtyInBaseUnits = excessQtyInBaseUnits + + availableLots.forEach { lotInfo -> + if (remainingQtyInBaseUnits <= zero) return@forEach + + val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) } + ?: return@forEach + + val totalQty = lot.inQty ?: zero + val outQty = lot.outQty ?: zero + val holdQty = lot.holdQty ?: zero + val availableQtyInBaseUnits = totalQty.minus(outQty).minus(holdQty) + + if (availableQtyInBaseUnits <= zero) return@forEach + + val assignQtyInBaseUnits = minOf(availableQtyInBaseUnits, remainingQtyInBaseUnits) + remainingQtyInBaseUnits = remainingQtyInBaseUnits.minus(assignQtyInBaseUnits) + + val assignQtyInSalesUnits = assignQtyInBaseUnits.divide(ratio, 0, RoundingMode.DOWN) + + if (assignQtyInSalesUnits > zero) { + suggestions.add(SuggestedPickLot().apply { + type = SuggestedPickLotType.PICK_ORDER + suggestedLotLine = lot + pickOrderLine = orderLine + qty = assignQtyInSalesUnits + }) + } + } + } + + return suggestions + } + + private fun findProblematicSuggestions(pickOrderId: Long): List { + val pickOrderLines = pickOrderRepository.findById(pickOrderId) + .orElseThrow() + .pickOrderLines + + val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } + + println("=== DEBUG: findProblematicSuggestions ===") + println("Pick Order ID: $pickOrderId") + println("Pick Order Line IDs: $pickOrderLineIds") + + val allSuggestions = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) + println("Total suggestions found: ${allSuggestions.size}") + + val problematicSuggestions = allSuggestions.filter { suggestion -> + val pickOrderLine = pickOrderLines.find { it.id == suggestion.pickOrderLine?.id } + val inventoryLotLine = suggestion.suggestedLotLine + + if (pickOrderLine == null || inventoryLotLine == null) { + println("Suggestion ${suggestion.id}: Missing pickOrderLine or inventoryLotLine") + return@filter true + } + + // Get sales unit conversion ratio + val salesUnit = pickOrderLine.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } + val ratio = (salesUnit?.ratioN ?: BigDecimal.ONE).divide(salesUnit?.ratioD ?: BigDecimal.ONE, 10, RoundingMode.HALF_UP) + + val ratioCheck = (suggestion.qty ?: BigDecimal.ZERO).divide(pickOrderLine.qty ?: BigDecimal.ONE, 2, RoundingMode.HALF_UP) + + // Calculate available qty in base units + val availableQtyInBaseUnits = (inventoryLotLine.inQty ?: BigDecimal.ZERO) + .minus(inventoryLotLine.outQty ?: BigDecimal.ZERO) + .minus(inventoryLotLine.holdQty ?: BigDecimal.ZERO) + + // Convert available qty to sales units for comparison + val availableQtyInSalesUnits = availableQtyInBaseUnits.divide(ratio, 2, RoundingMode.HALF_UP) + + // Convert suggestion qty to base units for comparison + val suggestionQtyInBaseUnits = (suggestion.qty ?: BigDecimal.ZERO).multiply(ratio) + + println("Suggestion ${suggestion.id}: qty=${suggestion.qty} (sales), requiredQty=${pickOrderLine.qty} (sales), ratio=$ratioCheck") + println(" - availableQtyInBaseUnits=$availableQtyInBaseUnits, availableQtyInSalesUnits=$availableQtyInSalesUnits") + println(" - suggestionQtyInBaseUnits=$suggestionQtyInBaseUnits") + + // Flag as problematic if: + val exceedsRatio = ratioCheck > BigDecimal("10") + val exceedsAvailable = suggestionQtyInBaseUnits > availableQtyInBaseUnits + + println(" - exceedsRatio=$exceedsRatio, exceedsAvailable=$exceedsAvailable") + + val isProblematic = exceedsRatio || exceedsAvailable + if (isProblematic) { + println("Suggestion ${suggestion.id}: PROBLEMATIC - ratio=$ratioCheck, exceedsAvailable=$exceedsAvailable") + } + isProblematic + } + + println("Problematic suggestions found: ${problematicSuggestions.size}") + return problematicSuggestions +} +private fun generateCorrectSuggestionsWithExistingHolds(pickOrder: PickOrder): List { + val suggestions = mutableListOf() + val zero = BigDecimal.ZERO + val one = BigDecimal.ONE + + pickOrder.pickOrderLines.forEach { orderLine -> + val itemId = orderLine.item?.id ?: return@forEach + val requiredQty = orderLine.qty ?: zero // This is in sales units + + println("=== DEBUG: generateCorrectSuggestionsWithExistingHolds ===") + println("Item ID: $itemId, Required Qty: $requiredQty") + + // Get sales unit conversion ratio + val salesUnit = itemUomService.findSalesUnitByItemId(itemId) + val ratio = (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) + + println("Ratio: $ratio") + + // Get available inventory lots for this item (FEFO order) + val availableLots = inventoryLotLineService + .allInventoryLotLinesByItemIdIn(listOf(itemId)) + .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } + .sortedBy { it.expiryDate } + + var remainingQtyInSalesUnits = requiredQty + + availableLots.forEach { lotInfo -> + if (remainingQtyInSalesUnits <= zero) return@forEach + + // Get the actual InventoryLotLine entity + val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) } + ?: return@forEach + + // ✅ FIX: Calculate available qty directly in sales units + val totalQty = lot.inQty ?: zero + val outQty = lot.outQty ?: zero + val holdQty = lot.holdQty ?: zero + val availableQtyInBaseUnits = totalQty.minus(outQty).minus(holdQty) + + // ✅ FIX: Convert to sales units with proper rounding + val availableQtyInSalesUnits = availableQtyInBaseUnits.divide(ratio, 2, RoundingMode.HALF_UP) + + println("Lot ${lot.id}: availableQtyInBaseUnits=$availableQtyInBaseUnits, availableQtyInSalesUnits=$availableQtyInSalesUnits") + + if (availableQtyInSalesUnits <= zero) return@forEach + + // ✅ FIX: Take what we can from this lot (constrained by available AND remaining) + val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQtyInSalesUnits) + remainingQtyInSalesUnits = remainingQtyInSalesUnits.minus(assignQtyInSalesUnits) + + println("Lot ${lot.id}: assignQtyInSalesUnits=$assignQtyInSalesUnits, remainingQtyInSalesUnits=$remainingQtyInSalesUnits") + + if (assignQtyInSalesUnits > zero) { + suggestions.add(SuggestedPickLot().apply { + type = SuggestedPickLotType.PICK_ORDER + suggestedLotLine = lot + pickOrderLine = orderLine + qty = assignQtyInSalesUnits // Store in sales units + }) + } + } + + // If we still have remaining qty, create a suggestion with null lot (insufficient stock) + if (remainingQtyInSalesUnits > zero) { + println("Remaining Qty in Sales Units: $remainingQtyInSalesUnits") + suggestions.add(SuggestedPickLot().apply { + type = SuggestedPickLotType.PICK_ORDER + suggestedLotLine = null // No lot available + pickOrderLine = orderLine + qty = remainingQtyInSalesUnits + }) + } + } + + return suggestions +} +} + + diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt index faf761b..e263e9d 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt @@ -21,6 +21,8 @@ import java.io.OutputStream import java.io.UnsupportedEncodingException import java.math.BigDecimal import java.text.ParseException +import com.ffii.fpsms.modules.master.web.models.MessageResponse +import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineStatusRequest @RequestMapping("/inventoryLotLine") @RestController @@ -75,4 +77,8 @@ class InventoryLotLineController ( response.addHeader("filename", "${pdf["fileName"]}.pdf") out.write(JasperExportManager.exportReportToPdf(jasperPrint)); } + @PostMapping("/updateStatus") + fun updateInventoryLotLineStatus(@RequestBody request: UpdateInventoryLotLineStatusRequest): MessageResponse { + return inventoryLotLineService.updateInventoryLotLineStatus(request) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt index dc2ceff..622dd60 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt @@ -8,8 +8,10 @@ import com.ffii.fpsms.modules.stock.service.StockInLineService import com.ffii.fpsms.modules.stock.service.StockOutLineService import com.ffii.fpsms.modules.stock.web.model.CreateStockOutLineRequest import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineRequest +import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineStatusRequest import jakarta.validation.Valid import org.springframework.web.bind.annotation.* +import com.ffii.fpsms.modules.stock.web.model.CreateStockOutLineWithoutConsoRequest @RestController @RequestMapping("/stockOutLine") @@ -25,9 +27,18 @@ class StockOutLineController( fun create(@Valid @RequestBody request: CreateStockOutLineRequest): MessageResponse { return stockOutLineService.create(request) } + @PostMapping("/createWithoutConso") + fun createWithoutConso(@Valid @RequestBody request: CreateStockOutLineWithoutConsoRequest): MessageResponse { + return stockOutLineService.createWithoutConso(request) + } @PostMapping("/update") fun update(@Valid @RequestBody request: UpdateStockOutLineRequest): MessageResponse { println("triggering") return stockOutLineService.update(request) } + + @PostMapping("/updateStatus") + fun updateStatus(@Valid @RequestBody request: UpdateStockOutLineStatusRequest): MessageResponse { + return stockOutLineService.updateStatus(request) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt index 68a129d..4cd9141 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import com.ffii.fpsms.modules.master.web.models.MessageResponse @RequestMapping("/suggestedPickLot") @RestController @@ -23,4 +24,9 @@ class SuggestedPickLotController( // val test1 = pickOrderRepository.findAllByConsoCode(conso).flatMap { it.pickOrderLines } // return suggestedPickLotService.suggestionForPickOrderLines(test1) // } + +@PostMapping("/resuggest/{pickOrderId}") +fun resuggestPickOrder(@PathVariable pickOrderId: Long): MessageResponse { + return suggestedPickLotService.resuggestPickOrder(pickOrderId) +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt index 0f6b6fe..32f5d12 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt @@ -11,3 +11,7 @@ data class LotLineInfo( val remainingQty: BigDecimal, val uom: String ) +data class UpdateInventoryLotLineStatusRequest( + val inventoryLotLineId: Long, + val status: String +) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt index d56ade3..b04bc26 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt @@ -46,3 +46,15 @@ data class UpdateStockOutLineRequest( val pickTime: LocalDateTime?, // val pickerId: Long? ) +data class CreateStockOutLineWithoutConsoRequest( + val pickOrderLineId: Long, + val inventoryLotLineId: Long, + val stockOutId: Long, + val qty: Double, +) +data class UpdateStockOutLineStatusRequest( + val id: Long, + val status: String, + val qty: Double? = null, + val remarks: String? = null +) \ No newline at end of file