Bläddra i källkod

update

master
CANCERYS\kw093 3 månader sedan
förälder
incheckning
7bc89e8a03
3 ändrade filer med 247 tillägg och 125 borttagningar
  1. +10
    -2
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderLineRepository.kt
  2. +36
    -35
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  3. +201
    -88
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt

+ 10
- 2
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderLineRepository.kt Visa fil

@@ -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<PickOrderLine, Long> {

@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<PickOrder>
}

+ 36
- 35
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt Visa fil

@@ -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")


+ 201
- 88
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Visa fil

@@ -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()


Laddar…
Avbryt
Spara