diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderLineRepository.kt index 5960349..0b7d533 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderLineRepository.kt @@ -2,8 +2,16 @@ package com.ffii.fpsms.modules.pickOrder.entity import com.ffii.core.support.AbstractRepository import org.springframework.stereotype.Repository - +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param @Repository interface PickOrderLineRepository : AbstractRepository { - + @Query(""" + SELECT DISTINCT pol.pickOrder + FROM PickOrderLine pol + WHERE pol.item.id = :itemId + AND pol.pickOrder.status IN ('PENDING', 'PARTIALLY_COMPLETED', 'RELEASED') + AND pol.pickOrder.deleted = false +""") +fun findAllPickOrdersByItemId(@Param("itemId") itemId: Long): List } \ No newline at end of file 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 aa332a2..ff76255 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 @@ -808,7 +808,7 @@ open class PickOrderService( println("=== Debug: getPickOrderLineLotDetails ===") println("pickOrderLineId: $pickOrderLineId") println("today: $today") - +/* // ✅ 重新添加:首先检查是否需要 resuggest val needsResuggest = checkIfNeedsResuggest(pickOrderLineId) if (needsResuggest) { @@ -828,41 +828,42 @@ open class PickOrderService( } else { println("✅ No resuggest needed for pick order line ID: $pickOrderLineId") } - + */ 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, - 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' - 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 - 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 - ORDER BY il.expiryDate ASC, il.lotNo ASC - """.trimIndent() + 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, + 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) + AND 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 + 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 + ORDER BY il.expiryDate ASC, il.lotNo ASC +""".trimIndent() println("🔍 Executing SQL for lot details: $sql") println("🔍 With parameters: pickOrderLineId = $pickOrderLineId") 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 a3a1e9e..f9c47d9 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 @@ -200,117 +200,230 @@ open class SuggestedPickLotService( open fun saveAll(request: List): List { return suggestedPickLotRepository.saveAllAndFlush(request) } + @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 - val allSuggestions = findAllSuggestionsForPickOrder(pickOrderId) - - if (allSuggestions.isEmpty()) { + open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { + try { + val pickOrder = pickOrderRepository.findById(pickOrderId).orElseThrow() + + // ✅ NEW: Get ALL pick orders for the same items + val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct() + val allCompetingPickOrders = mutableListOf() + + itemIds.forEach { itemId -> + val competingOrders = pickOrderLineRepository.findAllPickOrdersByItemId(itemId) + .filter { it.id != pickOrderId } // Exclude current pick order + allCompetingPickOrders.addAll(competingOrders) + } + + // ✅ NEW: Resuggest ALL competing pick orders together + val allPickOrdersToResuggest = listOf(pickOrder) + allCompetingPickOrders + + // ✅ FIX: Only clear suggestions for competing pick orders, NOT all lots + val allPickOrderIds = allPickOrdersToResuggest.mapNotNull { it.id } + val allSuggestions = findAllSuggestionsForPickOrders(allPickOrderIds) + + // ✅ FIX: Only clear holdQty for lots that are currently suggested + val currentlySuggestedLotIds = allSuggestions.mapNotNull { suggestion -> suggestion.suggestedLotLine?.id }.distinct() + val currentlySuggestedLots = inventoryLotLineRepository.findAllByIdIn(currentlySuggestedLotIds) + + println("=== RESUGGEST DEBUG ===") + println("Currently suggested lot IDs: $currentlySuggestedLotIds") + println("Total competing pick orders: ${allPickOrdersToResuggest.size}") + + // ✅ FIX: Only reset holdQty for currently suggested lots + currentlySuggestedLots.forEach { lotLine -> + println("Clearing holdQty for currently suggested lot line ${lotLine.id}: ${lotLine.holdQty} -> 0") + lotLine.holdQty = BigDecimal.ZERO + } + inventoryLotLineRepository.saveAllAndFlush(currentlySuggestedLots) + + // Delete ALL suggestions for all competing pick orders + suggestedPickLotRepository.deleteAllById(allSuggestions.mapNotNull { suggestion -> suggestion.id }) + + // ✅ NEW: Generate optimal suggestions for ALL pick orders together + val newSuggestions = generateOptimalSuggestionsForAllPickOrders(allPickOrdersToResuggest, emptyMap()) + + // Save new suggestions and update holdQty + val savedSuggestions = suggestedPickLotRepository.saveAllAndFlush(newSuggestions) + + // ✅ FIX: Update holdQty for newly suggested lots + val newlySuggestedLotIds = savedSuggestions.mapNotNull { suggestion -> suggestion.suggestedLotLine?.id }.distinct() + val newlySuggestedLots = inventoryLotLineRepository.findAllByIdIn(newlySuggestedLotIds) + + savedSuggestions.forEach { suggestion -> + val lotLine = newlySuggestedLots.find { it.id == suggestion.suggestedLotLine?.id } + lotLine?.let { + val ratio = BigDecimal.ONE + val suggestionQtyInBaseUnits = (suggestion.qty ?: BigDecimal.ZERO).multiply(ratio) + println("Setting holdQty for newly suggested lot line ${it.id}: ${it.holdQty} -> ${(it.holdQty ?: BigDecimal.ZERO).plus(suggestionQtyInBaseUnits)}") + it.holdQty = (it.holdQty ?: BigDecimal.ZERO).plus(suggestionQtyInBaseUnits) + } + } + inventoryLotLineRepository.saveAllAndFlush(newlySuggestedLots) + + println("=== RESUGGEST COMPLETED ===") + return MessageResponse( id = pickOrderId, - name = "No suggestions found", + name = "Pick order resuggested successfully", code = "SUCCESS", type = "resuggest", - message = "No suggestions to resuggest", + message = "Redistributed suggestions for ${allPickOrdersToResuggest.size} competing pick orders following FEFO order", + errorPosition = null + ) + + } catch (e: Exception) { + println("=== RESUGGEST ERROR ===") + e.printStackTrace() + return MessageResponse( + id = pickOrderId, + name = "Failed to resuggest pick order", + code = "ERROR", + type = "resuggest", + message = "Error: ${e.message}", errorPosition = null ) } + } + +private fun findAllSuggestionsForPickOrders(pickOrderIds: List): List { + val allPickOrderLines = mutableListOf() + + pickOrderIds.forEach { pickOrderId -> + val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) + pickOrder?.let { allPickOrderLines.addAll(it.pickOrderLines) } + } + + val pickOrderLineIds = allPickOrderLines.mapNotNull { it.id } + + return if (pickOrderLineIds.isNotEmpty()) { + suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) + } else { + emptyList() + } +} + + +private fun generateOptimalSuggestionsForAllPickOrders( + pickOrders: List, + originalHoldQtyMap: Map +): List { + val suggestions = mutableListOf() + val zero = BigDecimal.ZERO + val one = BigDecimal.ONE + val today = LocalDate.now() + + // Group pick order lines by item + val pickOrderLinesByItem = pickOrders.flatMap { it.pickOrderLines } + .groupBy { it.item?.id } + .filterKeys { it != null } + + pickOrderLinesByItem.forEach { (itemId, pickOrderLines) -> + if (itemId == null) return@forEach - // Step 2: Calculate total excess quantity from problematic suggestions - val problematicSuggestions = findProblematicSuggestions(pickOrderId) + // Calculate total demand for this item + val totalDemand = pickOrderLines.sumOf { it.qty ?: zero } + + // Get available lots for this item (FEFO order) + val availableLots = inventoryLotLineService + .allInventoryLotLinesByItemIdIn(listOf(itemId)) + .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } + .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } + .sortedBy { it.expiryDate } - // Step 3: Store original holdQty before resetting - val affectedLotLineIds = allSuggestions.mapNotNull { it.suggestedLotLine?.id }.distinct() - val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(affectedLotLineIds) + // ✅ FIX: Get fresh lot data and reset holdQty to 0 for calculation + val lotEntities = inventoryLotLineRepository.findAllByIdIn(availableLots.mapNotNull { it.id }) + lotEntities.forEach { lot -> lot.holdQty = BigDecimal.ZERO } - // ✅ FIX: Store original holdQty values before resetting - val originalHoldQtyMap = inventoryLotLines.associate { it.id to (it.holdQty ?: BigDecimal.ZERO) } + // ✅ FIX: Allocate lots directly to specific pick order lines (FEFO order) + val remainingPickOrderLines = pickOrderLines.toMutableList() + val remainingQtyPerLine = pickOrderLines.associate { it.id to (it.qty ?: zero) }.toMutableMap() - // Reset holdQty to 0 for all affected lots - inventoryLotLines.forEach { lotLine -> - 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 with original holdQty info - val newSuggestions = generateCorrectSuggestionsWithOriginalHolds(pickOrder, originalHoldQtyMap) - - // 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 { - val salesUnit = suggestion.pickOrderLine?.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } - val ratio = BigDecimal.ONE - val suggestionQtyInBaseUnits = (suggestion.qty ?: BigDecimal.ZERO).multiply(ratio) + lotEntities.forEach { lot -> + if (remainingPickOrderLines.isEmpty()) return@forEach + + val totalQty = lot.inQty ?: zero + val outQty = lot.outQty ?: zero + val holdQty = lot.holdQty ?: zero // This should be 0 now + val availableQty = totalQty.minus(outQty).minus(holdQty) + + if (availableQty <= zero) return@forEach + + var lotRemainingQty = availableQty + + // Allocate this lot to pick order lines in order + remainingPickOrderLines.removeAll { pickOrderLine -> + val lineId = pickOrderLine.id + val lineRemainingQty = remainingQtyPerLine[lineId] ?: zero + + if (lineRemainingQty <= zero || lotRemainingQty <= zero) return@removeAll false + + val assignQty = minOf(lotRemainingQty, lineRemainingQty) + lotRemainingQty = lotRemainingQty.minus(assignQty) + remainingQtyPerLine[lineId] = lineRemainingQty.minus(assignQty) + + if (assignQty > zero) { + suggestions.add(SuggestedPickLot().apply { + type = SuggestedPickLotType.PICK_ORDER + suggestedLotLine = lot + this.pickOrderLine = pickOrderLine + qty = assignQty + }) + } - it.holdQty = suggestionQtyInBaseUnits - println("Setting holdQty for lot ${it.id}: suggestion.qty=${suggestion.qty}, ratio=$ratio, holdQty=$suggestionQtyInBaseUnits") + // Remove this line if fully satisfied + remainingQtyPerLine[lineId]!! <= zero } } - inventoryLotLineRepository.saveAll(newInventoryLotLines) - updateInventoryTableAfterResuggest(pickOrder) - - 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 - ) + // ✅ FIX: Create insufficient stock suggestions for remaining quantities + remainingQtyPerLine.forEach { (lineId, remainingQty) -> + if (remainingQty > zero) { + val pickOrderLine = pickOrderLines.find { it.id == lineId } + if (pickOrderLine != null) { + suggestions.add(SuggestedPickLot().apply { + type = SuggestedPickLotType.PICK_ORDER + suggestedLotLine = null + this.pickOrderLine = pickOrderLine + qty = remainingQty + }) + } + } + } } + + return suggestions } - - private fun updateInventoryTableAfterResuggest(pickOrder: PickOrder) { - try { - // Get all item IDs from the pick order - val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct() +private fun updateInventoryTableAfterResuggest(pickOrder: PickOrder) { + try { + // Get all item IDs from the pick order + val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct() + + itemIds.forEach { itemId -> + // ✅ FIX: Use .value to get string representation + val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE.value) + .sumOf { it.holdQty ?: BigDecimal.ZERO } - itemIds.forEach { itemId -> - // Calculate onHoldQty (sum of holdQty from available lots only) - val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, "available") - .sumOf { it.holdQty ?: BigDecimal.ZERO } - - // Calculate unavailableQty (sum of inQty from unavailable lots only) - val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, "unavailable") - .sumOf { it.inQty ?: BigDecimal.ZERO } + // ✅ FIX: Use .value to get string representation + val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE.value) + .sumOf { it.inQty ?: BigDecimal.ZERO } + + // Update the inventory table + val inventory = inventoryRepository.findByItemId(itemId).orElse(null) + if (inventory != null) { + inventory.onHoldQty = onHoldQty + inventory.unavailableQty = unavailableQty + inventoryRepository.save(inventory) - // Update the inventory table - val inventory = inventoryRepository.findByItemId(itemId).orElse(null) - if (inventory != null) { - inventory.onHoldQty = onHoldQty - inventory.unavailableQty = unavailableQty - inventoryRepository.save(inventory) - - println("Updated inventory for item $itemId after resuggest: onHoldQty=$onHoldQty, unavailableQty=$unavailableQty") - } + println("Updated inventory for item $itemId after resuggest: onHoldQty=$onHoldQty, unavailableQty=$unavailableQty") } - } catch (e: Exception) { - println("Error updating inventory table after resuggest: ${e.message}") - e.printStackTrace() } + } catch (e: Exception) { + println("Error updating inventory table after resuggest: ${e.message}") + e.printStackTrace() } +} private fun findAllSuggestionsForPickOrder(pickOrderId: Long): List { val pickOrderLines = pickOrderRepository.findById(pickOrderId) .orElseThrow()