|
|
|
@@ -36,6 +36,7 @@ import com.ffii.fpsms.modules.stock.entity.FailInventoryLotLineRepository |
|
|
|
import com.ffii.fpsms.modules.stock.entity.StockOutRepository |
|
|
|
import com.ffii.fpsms.modules.master.entity.ItemsRepository |
|
|
|
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus |
|
|
|
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus |
|
|
|
import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo |
|
|
|
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository |
|
|
|
import com.ffii.fpsms.modules.stock.web.model.StockOutStatus |
|
|
|
@@ -533,24 +534,28 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { |
|
|
|
println("Pick Order Code: ${pickOrder.code}") |
|
|
|
println("Pick Order Status: ${pickOrder.status}") |
|
|
|
|
|
|
|
// NEW: Get ALL pick orders for the same items |
|
|
|
val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct() |
|
|
|
println("Item IDs in current pick order: $itemIds") |
|
|
|
|
|
|
|
val allCompetingPickOrders = mutableListOf<PickOrder>() |
|
|
|
|
|
|
|
itemIds.forEach { itemId -> |
|
|
|
val competingOrders = pickOrderLineRepository.findAllPickOrdersByItemId(itemId) |
|
|
|
.filter { it.id != pickOrderId } // Exclude current pick order |
|
|
|
println("Found ${competingOrders.size} competing pick orders for item $itemId") |
|
|
|
competingOrders.forEach { order -> |
|
|
|
println(" - Competing Order: ${order.code} (ID: ${order.id}, Status: ${order.status})") |
|
|
|
} |
|
|
|
allCompetingPickOrders.addAll(competingOrders) |
|
|
|
// NEW: Get ALL pick orders for the same items |
|
|
|
val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct() |
|
|
|
println("Item IDs in current pick order: $itemIds") |
|
|
|
|
|
|
|
// 收集所有競爭 pick order,並按 id 去重 |
|
|
|
// ✅ 优化:只考虑活跃的 pick orders(排除已完成和已取消的) |
|
|
|
val allCompetingPickOrders = itemIds.flatMap { itemId -> |
|
|
|
val competingOrders = pickOrderLineRepository.findAllPickOrdersByItemId(itemId) |
|
|
|
.filter { |
|
|
|
it.id != pickOrderId && // Exclude current pick order |
|
|
|
it.status != PickOrderStatus.COMPLETED |
|
|
|
} |
|
|
|
|
|
|
|
// OPTIMIZATION 3: 批量收集所有需要检查的 pick order line IDs |
|
|
|
val allPickOrderLineIdsToCheck = (listOf(pickOrder) + allCompetingPickOrders) |
|
|
|
if (competingOrders.isNotEmpty()) { |
|
|
|
println("Found ${competingOrders.size} active competing pick orders for item $itemId") |
|
|
|
} |
|
|
|
competingOrders |
|
|
|
}.distinctBy { it.id } |
|
|
|
|
|
|
|
// basePickOrders 也做一次去重,避免後面 filter 產生大量重複記錄 |
|
|
|
val basePickOrders = (listOf(pickOrder) + allCompetingPickOrders).distinctBy { it.id } |
|
|
|
|
|
|
|
val allPickOrderLineIdsToCheck = basePickOrders |
|
|
|
.flatMap { it.pickOrderLines } |
|
|
|
.mapNotNull { it.id } |
|
|
|
.distinct() |
|
|
|
@@ -558,7 +563,6 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { |
|
|
|
println("Checking ${allPickOrderLineIdsToCheck.size} pick order lines for rejected stock out lines") |
|
|
|
|
|
|
|
// OPTIMIZATION 3: 批量查询所有 stock out lines(一次查询代替 N*M 次查询) |
|
|
|
// 注意:findAllByPickOrderLineIdInAndDeletedFalse 返回 List<StockOutLine>(实体类) |
|
|
|
val allStockOutLines = if (allPickOrderLineIdsToCheck.isNotEmpty()) { |
|
|
|
stockOutLIneRepository.findAllByPickOrderLineIdInAndDeletedFalse(allPickOrderLineIdsToCheck) |
|
|
|
} else { |
|
|
|
@@ -568,32 +572,52 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { |
|
|
|
println("Found ${allStockOutLines.size} stock out lines in batch query (1 query instead of ${allPickOrderLineIdsToCheck.size} queries)") |
|
|
|
|
|
|
|
// OPTIMIZATION 3: 按 pickOrderLineId 分组,便于后续查找 |
|
|
|
// 注意:StockOutLine 实体使用 pickOrderLine?.id,不是 pickOrderLineId |
|
|
|
val stockOutLinesByPickOrderLineId = allStockOutLines |
|
|
|
.groupBy { it.pickOrderLine?.id } |
|
|
|
|
|
|
|
// FIX: Only resuggest pick orders that have rejected stock out lines |
|
|
|
val allPickOrdersToResuggest = (listOf(pickOrder) + allCompetingPickOrders) |
|
|
|
.filter { pickOrderToCheck -> |
|
|
|
// Only resuggest if the pick order has rejected stock out lines |
|
|
|
pickOrderToCheck.pickOrderLines.any { pol -> |
|
|
|
// OPTIMIZATION 3: 从预加载的 Map 中获取,而不是查询数据库 |
|
|
|
val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() |
|
|
|
val hasRejectedStockOutLine = stockOutLines.any { |
|
|
|
it.status?.equals("rejected", ignoreCase = true) == true |
|
|
|
} |
|
|
|
|
|
|
|
if (hasRejectedStockOutLine) { |
|
|
|
println("Pick Order ${pickOrderToCheck.code} has rejected stock out lines - will resuggest") |
|
|
|
stockOutLines.filter { it.status?.equals("rejected", ignoreCase = true) == true }.forEach { sol -> |
|
|
|
// NEW: 預先查出 basePickOrders 所有行對應的 suggestions,用來判斷是否用了 UNAVAILABLE lot |
|
|
|
val allSuggestionsForBaseOrders = if (allPickOrderLineIdsToCheck.isNotEmpty()) { |
|
|
|
suggestedPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIdsToCheck) |
|
|
|
} else { |
|
|
|
emptyList() |
|
|
|
} |
|
|
|
|
|
|
|
val suggestionsByPickOrderLineId = allSuggestionsForBaseOrders.groupBy { it.pickOrderLine?.id } |
|
|
|
val pickOrderIdsWithChanges = mutableSetOf<Long>() |
|
|
|
// FIX: 重算條件 = 有 rejected 行,或者 有使用 UNAVAILABLE lot 的 suggestion |
|
|
|
val allPickOrdersToResuggest = basePickOrders.filter { po -> |
|
|
|
val hasRejected = po.pickOrderLines.any { pol -> |
|
|
|
val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() |
|
|
|
val hasRejectedStockOutLine = stockOutLines.any { |
|
|
|
it.status?.equals("rejected", ignoreCase = true) == true |
|
|
|
} |
|
|
|
|
|
|
|
if (hasRejectedStockOutLine) { |
|
|
|
println("Pick Order ${po.code} has rejected stock out lines - will resuggest") |
|
|
|
stockOutLines |
|
|
|
.filter { it.status?.equals("rejected", ignoreCase = true) == true } |
|
|
|
.forEach { sol -> |
|
|
|
println(" - Rejected stock out line: ${sol.id} (lot: ${sol.inventoryLotLine?.id}, qty: ${sol.qty})") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
hasRejectedStockOutLine |
|
|
|
} |
|
|
|
|
|
|
|
hasRejectedStockOutLine |
|
|
|
} |
|
|
|
|
|
|
|
val hasUnavailableLot = po.pickOrderLines.any { pol -> |
|
|
|
val suggestions = suggestionsByPickOrderLineId[pol.id] ?: emptyList() |
|
|
|
val usedUnavailable = suggestions.any { |
|
|
|
it.suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE |
|
|
|
} |
|
|
|
if (usedUnavailable) { |
|
|
|
println("Pick Order ${po.code} has suggestions using UNAVAILABLE lots - will resuggest") |
|
|
|
} |
|
|
|
usedUnavailable |
|
|
|
} |
|
|
|
|
|
|
|
hasRejected || hasUnavailableLot |
|
|
|
}.distinctBy { it.id } |
|
|
|
|
|
|
|
println("=== RESUGGEST DEBUG ===") |
|
|
|
println("Original pick orders: ${(listOf(pickOrder) + allCompetingPickOrders).size}") |
|
|
|
println("Filtered pick orders to resuggest: ${allPickOrdersToResuggest.size}") |
|
|
|
@@ -656,35 +680,88 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { |
|
|
|
// FIX: Separate suggestions to keep (those WITHOUT rejected stock out lines) and delete |
|
|
|
val suggestionsToKeep = allSuggestions.filter { suggestion -> |
|
|
|
val pickOrderLineId = suggestion.pickOrderLine?.id |
|
|
|
val suggestedLotLineId = suggestion.suggestedLotLine?.id |
|
|
|
val suggestedLotLine = suggestion.suggestedLotLine |
|
|
|
val suggestedLotLineId = suggestedLotLine?.id |
|
|
|
|
|
|
|
|
|
|
|
val isLotUnavailable = suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE |
|
|
|
if (isLotUnavailable) { |
|
|
|
println("⏭️ Suggestion ${suggestion.id} uses UNAVAILABLE lot ${suggestedLotLine?.id}, will be deleted") |
|
|
|
return@filter false |
|
|
|
} |
|
|
|
|
|
|
|
if (pickOrderLineId != null && suggestedLotLineId != null) { |
|
|
|
// OPTIMIZATION 3: 从预加载的 Map 中获取 |
|
|
|
val stockOutLines = stockOutLinesByLineAndLot[pickOrderLineId to suggestedLotLineId] ?: emptyList() |
|
|
|
// 保留没有 rejected stock out lines 的 suggestions |
|
|
|
!stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } |
|
|
|
} else { |
|
|
|
true // 保留有问题的 suggestions 用于调试 |
|
|
|
true |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 只删除有 rejected stock out lines 的 suggestions |
|
|
|
val suggestionsToDelete = allSuggestions.filter { suggestion -> |
|
|
|
val pickOrderLineId = suggestion.pickOrderLine?.id |
|
|
|
val suggestedLotLineId = suggestion.suggestedLotLine?.id |
|
|
|
|
|
|
|
val suggestedLotLine = suggestion.suggestedLotLine |
|
|
|
val suggestedLotLineId = suggestedLotLine?.id |
|
|
|
|
|
|
|
val isLotUnavailable = suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE |
|
|
|
|
|
|
|
if (pickOrderLineId != null && suggestedLotLineId != null) { |
|
|
|
// OPTIMIZATION 3: 从预加载的 Map 中获取 |
|
|
|
val stockOutLines = stockOutLinesByLineAndLot[pickOrderLineId to suggestedLotLineId] ?: emptyList() |
|
|
|
stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } // 只删除 rejected 的 |
|
|
|
val hasRejected = stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } |
|
|
|
|
|
|
|
// ✅ 逻辑:只要 (lot UNAVAILABLE) 或 (有 rejected 的 stock_out_line),就删掉这条 suggestion |
|
|
|
if (isLotUnavailable || hasRejected) { |
|
|
|
println("🗑 Deleting suggestion ${suggestion.id} for lot $suggestedLotLineId, " + |
|
|
|
"unavailable=$isLotUnavailable, hasRejected=$hasRejected") |
|
|
|
true |
|
|
|
} else { |
|
|
|
false |
|
|
|
} |
|
|
|
} else { |
|
|
|
suggestedLotLineId == null |
|
|
|
// lotId == null 的老逻辑保留 |
|
|
|
suggestedLotLineId == null |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
println("Suggestions to keep: ${suggestionsToKeep.size}") |
|
|
|
println("Suggestions to delete: ${suggestionsToDelete.size}") |
|
|
|
|
|
|
|
// ✅ FIX: 更新使用 UNAVAILABLE lot 的 stockOutLine 状态为 rejected |
|
|
|
val unavailableLotIds = suggestionsToDelete |
|
|
|
.filter { it.suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE } |
|
|
|
.mapNotNull { it.suggestedLotLine?.id } |
|
|
|
.distinct() |
|
|
|
|
|
|
|
if (unavailableLotIds.isNotEmpty()) { |
|
|
|
println("=== Updating stock out lines for UNAVAILABLE lots ===") |
|
|
|
println("UNAVAILABLE lot IDs: $unavailableLotIds") |
|
|
|
|
|
|
|
// ✅ FIX: Find ALL stockOutLines using unavailable lots (not just those with suggestions) |
|
|
|
val stockOutLinesToReject = stockOutLIneRepository |
|
|
|
.findAllByInventoryLotLineIdInAndNotCompletedOrRejected(unavailableLotIds) |
|
|
|
.filter { sol -> |
|
|
|
sol.status != StockOutLineStatus.REJECTED.status && |
|
|
|
sol.status != StockOutLineStatus.COMPLETE.status |
|
|
|
} |
|
|
|
|
|
|
|
println("Found ${stockOutLinesToReject.size} stock out lines to reject for UNAVAILABLE lots (across all pick orders)") |
|
|
|
|
|
|
|
stockOutLinesToReject.forEach { sol -> |
|
|
|
val oldStatus = sol.status |
|
|
|
sol.status = StockOutLineStatus.REJECTED.status |
|
|
|
sol.modified = LocalDateTime.now() |
|
|
|
sol.modifiedBy = "system" |
|
|
|
println(" - Updated stock out line ${sol.id} (lot: ${sol.inventoryLotLine?.id}, pickOrderLine: ${sol.pickOrderLine?.id}) status: $oldStatus -> rejected") |
|
|
|
} |
|
|
|
|
|
|
|
if (stockOutLinesToReject.isNotEmpty()) { |
|
|
|
stockOutLIneRepository.saveAll(stockOutLinesToReject) |
|
|
|
stockOutLIneRepository.flush() |
|
|
|
println("✅ Updated ${stockOutLinesToReject.size} stock out lines to REJECTED status") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// FIX: Clear holdQty ONLY for lots that have rejected stock out lines |
|
|
|
val rejectedLotIds = suggestionsToDelete.mapNotNull { it.suggestedLotLine?.id }.distinct() |
|
|
|
println("Rejected lot IDs (clearing holdQty only): $rejectedLotIds") |
|
|
|
@@ -737,17 +814,27 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { |
|
|
|
|
|
|
|
println("Final existing holdQtyMap: $existingHoldQtyMap") |
|
|
|
|
|
|
|
// FIX: Create new suggestions for all pick orders to resuggest |
|
|
|
allPickOrdersToResuggest.forEach { pickOrderToResuggest -> |
|
|
|
// 只获取有 rejected stock out lines 的 pick order lines |
|
|
|
// OPTIMIZATION 3: 使用预加载的 Map |
|
|
|
// 获取需要重新建议的 pick order lines: |
|
|
|
// 1. 有 rejected stock out lines 的行 |
|
|
|
// 2. 有使用 UNAVAILABLE lot 的 suggestion 的行 |
|
|
|
val problematicPickOrderLines = pickOrderToResuggest.pickOrderLines.filter { pol -> |
|
|
|
// 情况1: 有 rejected stock out line |
|
|
|
val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() |
|
|
|
stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } |
|
|
|
val hasRejected = stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } |
|
|
|
|
|
|
|
// 情况2: 有使用 UNAVAILABLE lot 的 suggestion |
|
|
|
val suggestions = suggestionsByPickOrderLineId[pol.id] ?: emptyList() |
|
|
|
val hasUnavailableLot = suggestions.any { |
|
|
|
it.suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE |
|
|
|
} |
|
|
|
|
|
|
|
hasRejected || hasUnavailableLot |
|
|
|
} |
|
|
|
|
|
|
|
if (problematicPickOrderLines.isNotEmpty()) { |
|
|
|
println("=== Creating new suggestions for pick order: ${pickOrderToResuggest.code} ===") |
|
|
|
println(" - Problematic lines: ${problematicPickOrderLines.map { it.id }}") |
|
|
|
|
|
|
|
// 调用 suggestionForPickOrderLines 生成新的 suggestions |
|
|
|
val request = SuggestedPickLotForPolRequest( |
|
|
|
@@ -760,7 +847,7 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { |
|
|
|
|
|
|
|
if (response.suggestedList.isNotEmpty()) { |
|
|
|
println("Saving ${response.suggestedList.size} new suggestions") |
|
|
|
|
|
|
|
pickOrderToResuggest.id?.let { pickOrderIdsWithChanges.add(it) } |
|
|
|
// 获取现有的 pending/checked 状态的 suggestions(可以更新的) |
|
|
|
val existingUpdatableSuggestions = suggestionsToKeep |
|
|
|
.filter { it.suggestedLotLine?.id != null } |
|
|
|
@@ -879,7 +966,8 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 更新 holdQty |
|
|
|
// ✅ 优化:批量更新 holdQty(避免逐个保存) |
|
|
|
val lotsToUpdate = mutableListOf<InventoryLotLine>() |
|
|
|
response.holdQtyMap.forEach { (lotId, newHoldQty) -> |
|
|
|
if (lotId != null && newHoldQty != null && newHoldQty > BigDecimal.ZERO) { |
|
|
|
val lot = inventoryLotLineRepository.findById(lotId).orElse(null) |
|
|
|
@@ -889,12 +977,16 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { |
|
|
|
val additionalHoldQty = newHoldQty.minus(existingHoldQty) |
|
|
|
val finalHoldQty = currentHoldQty.plus(additionalHoldQty) |
|
|
|
it.holdQty = finalHoldQty |
|
|
|
inventoryLotLineRepository.save(it) |
|
|
|
lotsToUpdate.add(it) |
|
|
|
existingHoldQtyMap[lotId] = newHoldQty |
|
|
|
println("Updated holdQty for lot $lotId: $currentHoldQty + $additionalHoldQty = $finalHoldQty") |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// 批量保存所有更新的 lots |
|
|
|
if (lotsToUpdate.isNotEmpty()) { |
|
|
|
inventoryLotLineRepository.saveAll(lotsToUpdate) |
|
|
|
println("Batch updated holdQty for ${lotsToUpdate.size} lots") |
|
|
|
} |
|
|
|
} else { |
|
|
|
// 如果完全没有生成任何 suggestions |
|
|
|
println("No suggestions generated at all for pick order: ${pickOrderToResuggest.code}") |
|
|
|
@@ -918,12 +1010,38 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// FIX: Update inventory table for each pick order |
|
|
|
allPickOrdersToResuggest.forEach { pickOrderToUpdate -> |
|
|
|
println("=== Updating inventory table for pick order: ${pickOrderToUpdate.code} ===") |
|
|
|
updateInventoryTableAfterResuggest(pickOrderToUpdate) |
|
|
|
} |
|
|
|
// ✅ 优化:批量更新 inventory table(收集所有 item IDs,只更新一次) |
|
|
|
val allItemIdsToUpdate = allPickOrdersToResuggest |
|
|
|
.flatMap { it.pickOrderLines } |
|
|
|
.mapNotNull { it.item?.id } |
|
|
|
.distinct() |
|
|
|
|
|
|
|
if (allItemIdsToUpdate.isNotEmpty()) { |
|
|
|
println("=== Batch updating inventory table for ${allItemIdsToUpdate.size} items ===") |
|
|
|
allItemIdsToUpdate.forEach { itemId -> |
|
|
|
// Calculate onHoldQty for ALL pick orders that use this item |
|
|
|
// ✅ FIX: Use enum directly, not .value |
|
|
|
val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) |
|
|
|
.sumOf { it.holdQty ?: BigDecimal.ZERO } |
|
|
|
|
|
|
|
// ✅ FIX: Use enum directly, not .value |
|
|
|
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE) |
|
|
|
.sumOf { |
|
|
|
val inQty = it.inQty ?: BigDecimal.ZERO |
|
|
|
val outQty = it.outQty ?: BigDecimal.ZERO |
|
|
|
val remainingQty = inQty.minus(outQty) |
|
|
|
remainingQty |
|
|
|
} |
|
|
|
|
|
|
|
val inventory = inventoryRepository.findByItemId(itemId).orElse(null) |
|
|
|
if (inventory != null) { |
|
|
|
inventory.onHoldQty = onHoldQty |
|
|
|
inventory.unavailableQty = unavailableQty |
|
|
|
inventoryRepository.save(inventory) |
|
|
|
} |
|
|
|
} |
|
|
|
println("✅ Batch updated inventory table for ${allItemIdsToUpdate.size} items") |
|
|
|
} |
|
|
|
println("=== RESUGGEST DEBUG END ===") |
|
|
|
|
|
|
|
return MessageResponse( |
|
|
|
|