Browse Source

updated

master
CANCERYS\kw093 3 months ago
parent
commit
d1017cc4b2
12 changed files with 882 additions and 231 deletions
  1. +268
    -175
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  2. +3
    -1
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt
  3. +4
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt
  4. +4
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt
  5. +42
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt
  6. +190
    -35
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  7. +332
    -18
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  8. +6
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt
  9. +11
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt
  10. +6
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt
  11. +4
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt
  12. +12
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt

+ 268
- 175
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt View File

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

+ 3
- 1
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt View File

@@ -13,7 +13,8 @@ data class ReleasePickOrderInfoResponse(
)

data class GetPickOrderInfoResponse(
// val consoCode: String,
val consoCode: String?,

val pickOrders: List<GetPickOrderInfo>,
val items: List<CurrentInventoryItemInfo>
)
@@ -60,6 +61,7 @@ data class GetPickOrderLineInfo(
val uomCode: String?,
val uomDesc: String?,
val suggestedList: List<SuggestedPickLot>?,
val pickedQty: BigDecimal?=BigDecimal.ZERO,
)

// Final Response - Conso Pick Order Detail


+ 4
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt View File

@@ -16,4 +16,8 @@ interface StockOutLIneRepository: AbstractRepository<StockOutLine, Long> {
fun findStockOutLineInfoById(id: Long): StockOutLineInfo

fun findAllByStockOutId(stockOutId: Long): List<StockOutLine>
fun findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
pickOrderLineId: Long,
inventoryLotLineId: Long
): List<StockOutLine>
}

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt View File

@@ -7,4 +7,8 @@ import org.springframework.stereotype.Repository
@Repository
interface SuggestPickLotRepository : AbstractRepository<SuggestedPickLot, Long> {
fun findAllByPickOrderLineIn(lines: List<PickOrderLine>): List<SuggestedPickLot>
fun findAllByPickOrderLineIdIn(pickOrderLineIds: List<Long>): List<SuggestedPickLot>
fun findAllByPickOrderLineId(pickOrderLineId: Long): List<SuggestedPickLot>
}

+ 42
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt View File

@@ -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<String, Any> {


+ 190
- 35
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt View File

@@ -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<StockOutLine?> {
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<StockOutLine?> {
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<StockOutLine?> {
/**
@@ -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
}
}
}

+ 332
- 18
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt View File

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

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<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 (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<SuggestedPickLot> {
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<SuggestedPickLot> {
val suggestions = mutableListOf<SuggestedPickLot>()
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<SuggestedPickLot> {
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<SuggestedPickLot> {
val suggestions = mutableListOf<SuggestedPickLot>()
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
}
}



+ 6
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt View File

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

+ 11
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt View File

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

+ 6
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt View File

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

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt View File

@@ -11,3 +11,7 @@ data class LotLineInfo(
val remainingQty: BigDecimal,
val uom: String
)
data class UpdateInventoryLotLineStatusRequest(
val inventoryLotLineId: Long,
val status: String
)

+ 12
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt View File

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

Loading…
Cancel
Save