|
|
|
@@ -200,117 +200,230 @@ open class SuggestedPickLotService( |
|
|
|
open fun saveAll(request: List<SuggestedPickLot>): List<SuggestedPickLot> { |
|
|
|
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<PickOrder>() |
|
|
|
|
|
|
|
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<Long>): List<SuggestedPickLot> { |
|
|
|
val allPickOrderLines = mutableListOf<PickOrderLine>() |
|
|
|
|
|
|
|
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<PickOrder>, |
|
|
|
originalHoldQtyMap: Map<Long?, BigDecimal> |
|
|
|
): List<SuggestedPickLot> { |
|
|
|
val suggestions = mutableListOf<SuggestedPickLot>() |
|
|
|
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<SuggestedPickLot> { |
|
|
|
val pickOrderLines = pickOrderRepository.findById(pickOrderId) |
|
|
|
.orElseThrow() |
|
|
|
|