Sfoglia il codice sorgente

improve do pick order and show not yet finish do pick order and jo floor count

reset-do-picking-order
CANCERYS\kw093 2 settimane fa
parent
commit
98a2fa939c
10 ha cambiato i file con 436 aggiunte e 363 eliminazioni
  1. +5
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRepository.kt
  2. +48
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt
  3. +96
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
  4. +16
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt
  5. +18
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt
  6. +43
    -4
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  7. +6
    -2
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt
  8. +198
    -350
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  9. +4
    -3
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  10. +2
    -1
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt

+ 5
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRepository.kt Vedi File

@@ -45,4 +45,9 @@ fun findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn(
releaseType: String,
ticketStatus: DoPickOrderStatus
): List<DoPickOrder>

fun findFirstByHandledByAndDeletedFalseAndTicketStatusIn(
handledBy: Long,
ticketStatus: List<String>
): DoPickOrder?
}

+ 48
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt Vedi File

@@ -926,5 +926,53 @@ open class DoPickOrderService(

return allPickOrderLines.size
}
open fun findReleasedDoPickOrdersForSelection(
shopName: String?,
storeId: String?,
truck: String?
): List<ReleasedDoPickOrderListItem> {
val doPickOrders = doPickOrderRepository.findByTicketStatusIn(
listOf(DoPickOrderStatus.released, DoPickOrderStatus.pending)
)
var filtered = doPickOrders
if (!storeId.isNullOrBlank()) {
filtered = filtered.filter { it.storeId == storeId }
}
if (!shopName.isNullOrBlank()) {
filtered = filtered.filter {
it.shopName?.contains(shopName, ignoreCase = true) == true ||
it.shopCode?.contains(shopName, ignoreCase = true) == true
}
}
if (!truck.isNullOrBlank()) {
filtered = filtered.filter { it.truckLanceCode == truck }
}
return filtered.map { dpo ->
val lines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(dpo.id!!)
val deliveryOrderCodes = lines
.mapNotNull { it.deliveryOrderCode }
.distinct()
ReleasedDoPickOrderListItem(
id = dpo.id!!,
requiredDeliveryDate = dpo.requiredDeliveryDate,
shopCode = dpo.shopCode,
shopName = dpo.shopName,
storeId = dpo.storeId,
truckLanceCode = dpo.truckLanceCode,
truckDepartureTime = dpo.truckDepartureTime,
deliveryOrderCodes = deliveryOrderCodes.ifEmpty {
listOfNotNull(dpo.deliveryOrderCode)
}
)
}.sortedWith(
compareBy<ReleasedDoPickOrderListItem> { it.requiredDeliveryDate }
.thenBy { it.truckDepartureTime }
.thenBy { it.truckLanceCode }
)
}

}// 类结束

+ 96
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt Vedi File

@@ -20,7 +20,13 @@ import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.pickOrder.service.PickExecutionIssueService
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.DayOfWeek
import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByDoPickOrderIdRequest
import com.ffii.fpsms.modules.user.entity.UserRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
data class BatchReleaseJobStatus(
val jobId: String,
val total: Int,
@@ -39,6 +45,9 @@ class DoReleaseCoordinatorService(
private val deliveryOrderRepository: DeliveryOrderRepository,
private val doPickOrderRepository: DoPickOrderRepository,
private val pickExecutionIssueService: PickExecutionIssueService,
private val userRepository: UserRepository,
private val pickOrderRepository: PickOrderRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
) {
private val poolSize = Runtime.getRuntime().availableProcessors()
private val executor = Executors.newFixedThreadPool(min(poolSize, 4))
@@ -911,4 +920,90 @@ class DoReleaseCoordinatorService(
e.printStackTrace()
}
}
}

fun assignByDoPickOrderId(request: AssignByDoPickOrderIdRequest): MessageResponse {
val user = userRepository.findById(request.userId).orElse(null)
?: return MessageResponse(id = null, code = "USER_NOT_FOUND", name = null, type = null, message = null, errorPosition = null, entity = null)
val doPickOrder = doPickOrderRepository.findById(request.doPickOrderId).orElse(null)
?: return MessageResponse(id = null, code = "NOT_FOUND", name = null, type = null, message = null, errorPosition = null, entity = null)
// 檢查狀態是否可 assign
if (doPickOrder.ticketStatus !in listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released)) {
return MessageResponse(id = null, code = "INVALID_STATUS", name = null, type = null, message = null, errorPosition = null, entity = null)
}
// 檢查是否有 non-issue lines(可重用 checkDoPickOrderHasNonIssueLines)
if (!checkDoPickOrderHasNonIssueLines(doPickOrder.id!!)) {
return MessageResponse(id = null, code = "NO_ORDERS", name = null, type = null, message = "All lines are issues", errorPosition = null, entity = null)
}
// 更新 do_pick_order
doPickOrder.handledBy = request.userId
doPickOrder.handlerName = user.name
doPickOrder.ticketStatus = DoPickOrderStatus.released
doPickOrder.ticketReleaseTime = LocalDateTime.now()
doPickOrderRepository.save(doPickOrder)
// 更新關聯的 pick orders
val lines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!)
lines.forEach { line ->
line.pickOrderId?.let { pickOrderId ->
pickOrderRepository.findById(pickOrderId).ifPresent { po ->
po.assignTo = user
po.status = PickOrderStatus.RELEASED
pickOrderRepository.save(po)
}
}
}
// 更新 do_pick_order_record(若有)
lines.forEach { line ->
line.pickOrderId?.let { pickOrderId ->
val records = doPickOrderRecordRepository.findByPickOrderId(pickOrderId)
records.forEach { record ->
record.handledBy = request.userId
record.handlerName = user.name
record.ticketStatus = DoPickOrderStatus.released
record.ticketReleaseTime = LocalDateTime.now()
}
if (records.isNotEmpty()) doPickOrderRecordRepository.saveAll(records)
}
}
return MessageResponse(
id = doPickOrder.id,
code = "SUCCESS",
name = null,
type = null,
message = null,
errorPosition = null,
entity = null
)
}
private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean {
return try {
val totalLinesSql = """
SELECT COUNT(*) as total_lines
FROM fpsmsdb.do_pick_order_line dpol
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
""".trimIndent()
val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId))
val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0
if (totalLines == 0) return true
val nonIssueLinesSql = """
SELECT COUNT(*) as non_issue_lines
FROM fpsmsdb.do_pick_order_line dpol
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
AND (dpol.status IS NULL OR dpol.status != 'issue')
""".trimIndent()
val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId))
val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0
nonIssueLines > 0
} catch (e: Exception) {
println("❌ Error checking non-issue lines for do_pick_order $doPickOrderId: ${e.message}")
true
}
}}

+ 16
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt Vedi File

@@ -61,11 +61,26 @@ class DoPickOrderController(
fun releaseAssignedPickOrderByStore(@RequestBody request: AssignByStoreRequest): MessageResponse {
return doPickOrderService.releaseAssignedByStore(request)
}
/*
@GetMapping("/released")
fun getReleasedDoPickOrders(): List<DoPickOrder> {
return doPickOrderService.findReleasedDoPickOrders()
}
*/

@GetMapping("/released")
fun getReleasedDoPickOrders(
@RequestParam(required = false) shopName: String?,
@RequestParam(required = false) storeId: String?,
@RequestParam(required = false) truck: String?
): List<ReleasedDoPickOrderListItem> {
return doPickOrderService.findReleasedDoPickOrdersForSelection(shopName, storeId, truck)
}
@PostMapping("/assign-by-id")
fun assignByDoPickOrderId(@RequestBody request: AssignByDoPickOrderIdRequest): MessageResponse {
return doReleaseCoordinatorService.assignByDoPickOrderId(request) // 改用 doReleaseCoordinatorService
}
@GetMapping("/summary-by-store")
fun getSummaryByStore(
@RequestParam storeId: String,


+ 18
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt Vedi File

@@ -3,6 +3,7 @@ package com.ffii.fpsms.modules.deliveryOrder.web.models
import java.time.LocalDateTime
import java.time.LocalDate
import com.fasterxml.jackson.annotation.JsonFormat
import java.time.LocalTime
data class DoDetailResponse(
val id: Long,
val code: String,
@@ -89,4 +90,20 @@ interface DoSearchRowProjection {
val estimatedArrivalDate: LocalDateTime?
val status: String?
val deliveryOrderLineCount: Long
}
}
data class ReleasedDoPickOrderListItem(
val id: Long, // doPickOrderId,用於 assign
val requiredDeliveryDate: LocalDate?, // Date 欄
val shopCode: String?, // Shop
val shopName: String?, // Shop
val storeId: String?, // 2/F or 4/F
val truckLanceCode: String?, // Truck (Lane)
val truckDepartureTime: LocalTime?, // Truck 時間
val deliveryOrderCodes: List<String> // 多個 DO code,前端換行顯示
)

// Assign 用的 request
data class AssignByDoPickOrderIdRequest(
val userId: Long,
val doPickOrderId: Long
)

+ 43
- 4
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt Vedi File

@@ -12,7 +12,7 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import com.ffii.fpsms.modules.jobOrder.web.model.FloorPickCountDto
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus
import java.time.LocalDateTime
@@ -1751,7 +1751,12 @@ open fun getCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId: Lo
}
}


private fun normalizeFloor(raw: String): String {
if (raw.isBlank()) return raw
val cleaned = raw.trim().uppercase()
val num = cleaned.replace(Regex("[^0-9]"), "")
return if (num.isNotEmpty()) "${num}F" else cleaned
}
open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> {
println("=== getAllJoPickOrders ===")
@@ -1763,6 +1768,33 @@ open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> {
}

println("Found ${releasedPickOrders.size} released job order pick orders")
val pickOrderIds = releasedPickOrders.mapNotNull { it.id }

// 2. 批量查询每个 pick order 的按楼层统计(若没有则跳过)
val floorCountsByPickOrderId: Map<Long, List<Map<String, Any?>>> = if (pickOrderIds.isNotEmpty()) {
val sql = """
SELECT
po.id AS pickOrderId,
COALESCE(NULLIF(TRIM(w.store_id), ''), SUBSTRING_INDEX(w.code, '-', 1)) AS floorKey,
COUNT(DISTINCT pol.id) AS totalCount,
COUNT(DISTINCT CASE WHEN pol.status = 'COMPLETED' THEN pol.id END) AS finishedCount
FROM fpsmsdb.pick_order po
JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false
LEFT JOIN fpsmsdb.suggested_pick_lot spl ON pol.id = spl.pickOrderLineId AND spl.deleted = false
LEFT JOIN fpsmsdb.inventory_lot_line ill ON spl.suggestedLotLineId = ill.id AND ill.deleted = false
LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
WHERE po.deleted = false
AND po.status = 'RELEASED'
AND po.joId IS NOT NULL
AND po.id IN (${pickOrderIds.joinToString(",")})
AND w.id IS NOT NULL
GROUP BY po.id, floorKey
""".trimIndent()
jdbcDao.queryForList(sql, emptyMap<String, Any?>()).groupBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L }
} else {
emptyMap()
}

val jobOrderPickOrders = releasedPickOrders.mapNotNull { pickOrder ->
println("Processing pick order: ${pickOrder.id}, code: ${pickOrder.code}")
@@ -1800,7 +1832,13 @@ open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> {
val jobOrderType = jobOrder.jobTypeId?.let { jobTypeRepository.findById(it).orElse(null) }

println("✅ Building response for pick order ${pickOrder.id}")

val floorPickCounts = floorCountsByPickOrderId[pickOrder.id]?.map { row ->
com.ffii.fpsms.modules.jobOrder.web.model.FloorPickCountDto(
floor = normalizeFloor((row["floorKey"] as? String).orEmpty()),
finishedCount = (row["finishedCount"] as? Number)?.toInt() ?: 0,
totalCount = (row["totalCount"] as? Number)?.toInt() ?: 0
)
}.orEmpty()
AllJoPickOrderResponse(
id = pickOrder.id ?: 0L,
pickOrderId = pickOrder.id,
@@ -1816,7 +1854,8 @@ open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> {
uomId = 0,
uomName = bom?.outputQtyUom?: "",
jobOrderStatus = jobOrder.status?.value ?: "",
finishedPickOLineCount = finishedLines
finishedPickOLineCount = finishedLines,
floorPickCounts = floorPickCounts
)
}



+ 6
- 2
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt Vedi File

@@ -34,7 +34,11 @@ data class CreateJobOrderProcessRequest (
val status: String = "pending",
)


data class FloorPickCountDto(
val floor: String, // e.g. "2F", "4F"
val finishedCount: Int,
val totalCount: Int
)
data class AllJoPickOrderResponse(
val id: Long,
val pickOrderId: Long?,
@@ -50,7 +54,7 @@ data class AllJoPickOrderResponse(
val uomName: String,
val jobOrderStatus: String,
val finishedPickOLineCount: Int,
val floorPickCounts: List<FloorPickCountDto> = emptyList()
)




+ 198
- 350
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt Vedi File

@@ -3328,406 +3328,252 @@ ORDER BY
return enrichedResults
}
// 修改后的逻辑
open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> {
/*
open fun getAllPickOrderLotsWithDetailsHierarchicalold(userId: Long): Map<String, Any?> {
println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (Repository-based) ===")
println("userId filter: $userId")
val user = userService.find(userId).orElse(null)
if (user == null) {
println("❌ User not found: $userId")
return emptyMap()
}
// Step 1: 先找到用户有活跃任务的 do_pick_order(通过 do_pick_order_line)
// 这样可以确保只获取同一个 ticket 的 pick orders
val userPickOrdersForDo = pickOrderRepository.findAll()
.filter {
it.deleted == false &&
it.assignTo?.id == userId &&
it.type?.value == "do" &&
(it.status == PickOrderStatus.RELEASED ||
it.status == PickOrderStatus.PENDING ||
it.status == PickOrderStatus.PICKING ||
it.status == PickOrderStatus.ASSIGNED)
}
if (userPickOrdersForDo.isEmpty()) {
println("❌ No active pick orders found for user $userId")
return mapOf(
"fgInfo" to null,
"pickOrders" to emptyList<Any>()
)
}
val activePickOrderIds = userPickOrdersForDo.mapNotNull { it.id }
println(" Found ${activePickOrderIds.size} active pick orders assigned to user $userId")
// Step 2: 通过 do_pick_order_line 找到相关的 do_pick_order
val doPickOrderLineRecords = activePickOrderIds.flatMap { poId ->
doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(poId)
}
val doPickOrderIds = doPickOrderLineRecords.mapNotNull { it.doPickOrderId }.distinct()
if (doPickOrderIds.isEmpty()) {
println("❌ No do_pick_order found for pick orders: $activePickOrderIds")
return mapOf(
"fgInfo" to null,
"pickOrders" to emptyList<Any>()
)
}
println(" Found ${doPickOrderIds.size} do_pick_order records")
// Step 3: 获取第一个 do_pick_order 的详细信息(用于构建 fgInfo)
val doPickOrder = doPickOrderRepository.findById(doPickOrderIds.first()).orElse(null)

// Step 1:直接按 handledBy 查当前用户的活动 do_pick_order(一个 ticket)
val activeTicketStatuses = listOf("released", "picking") // 如果你用的是 DoPickOrderStatus 枚举,也可以改成 List<DoPickOrderStatus>
val doPickOrder = doPickOrderRepository
.findFirstByHandledByAndDeletedFalseAndTicketStatusIn(user.id!!, activeTicketStatuses)

if (doPickOrder == null) {
println("❌ do_pick_order not found: ${doPickOrderIds.first()}")
println("❌ No active do_pick_order found for handledBy user $userId")
return mapOf(
"fgInfo" to null,
"pickOrders" to emptyList<Any>()
)
}

val doPickOrderId = doPickOrder.id!!
println(" Using do_pick_order ID: $doPickOrderId")
// Step 4: 关键修改 - 只获取这个 do_pick_order 下的所有 pick orders(无论状态)
println(" Using do_pick_order ID (by handledBy): $doPickOrderId")

// Step 2:用这个 do_pick_orderId 查对应的 do_pick_order_line / pick_order
val allDoPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrderId)
val allPickOrderIdsForThisTicket = allDoPickOrderLines.mapNotNull { it.pickOrderId }.distinct()

println(" Found ${allPickOrderIdsForThisTicket.size} pick orders in this do_pick_order (including completed)")
// Step 5: 加载这些 pick orders(包括 COMPLETED 状态

// Step 3:加载这些 pick orders(包括 COMPLETED)
val pickOrders = pickOrderRepository.findAllById(allPickOrderIdsForThisTicket)
.filter {
it.deleted == false &&
it.assignTo?.id == userId && // 确保是分配给该用户的
.filter {
it.deleted == false &&
it.assignTo?.id == userId &&
it.type?.value == "do"
// 不限制状态,包括 COMPLETED
}

println(" Loaded ${pickOrders.size} pick orders (including completed)")
// 收集所有需要的数据
// 收集所有需要的数据
val allPickOrderLineIds = pickOrders.flatMap { it.pickOrderLines }.mapNotNull { it.id }
// 使用 Repository 批量加载所有 suggestions 和 stock out lines
// 使用 Repository 批量加载所有 suggestions 和 stock out lines
val allSuggestions = suggestPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIds) // 修复:使用 suggestPickLotRepository

// Step 4:原来你从 3413 行开始的收集所有 line / lots 的逻辑,全部保留
val allPickOrderLineIds = pickOrders.flatMap { it.pickOrderLines }.mapNotNull { it.id }

val allSuggestions = suggestPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIds)
val allStockOutLines = allPickOrderLineIds.flatMap { lineId ->
stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(lineId)
}
// 按 pickOrderLineId 分组
val suggestionsByLineId = allSuggestions.groupBy { spl: SuggestedPickLot -> // 修复:明确类型

val suggestionsByLineId = allSuggestions.groupBy { spl: SuggestedPickLot ->
spl.pickOrderLine?.id
}
val stockOutLinesByLineId = allStockOutLines.groupBy { sol: StockOutLineInfo -> // 修复:明确类型
val stockOutLinesByLineId = allStockOutLines.groupBy { sol: StockOutLineInfo ->
sol.pickOrderLineId
}
// 构建层级数据结构

val allPickOrderLines = mutableListOf<Map<String, Any?>>()
val lineCountsPerPickOrder = mutableListOf<Int>()
val pickOrderIdsList = mutableListOf<Long>()
val pickOrderCodesList = mutableListOf<String>()
val doOrderIdsList = mutableListOf<Long>()
val deliveryOrderCodesList = mutableListOf<String>()
pickOrders.forEach { po ->
pickOrderIdsList.add(po.id!!)
pickOrderCodesList.add(po.code ?: "")
val doOrderId = po.deliveryOrder?.id
if (doOrderId != null) doOrderIdsList.add(doOrderId)
deliveryOrderCodesList.add(po.deliveryOrder?.code ?: "")
val lines = po.pickOrderLines.filter { !it.deleted }
val lineDtos = lines.map { pol ->
val lineId = pol.id!!

val lineDtos = po.pickOrderLines
.filter { !it.deleted }
.map { pol ->
val lineId = pol.id
val item = pol.item
val uom = pol.uom
val zero = BigDecimal.ZERO
val today = LocalDate.now()
// 使用 Repository 获取该 line 的 suggestions 和 stock out lines
val suggestions = suggestionsByLineId[lineId] ?: emptyList<SuggestedPickLot>() // 修复:明确类型
val stockOutLines = stockOutLinesByLineId[lineId] ?: emptyList<StockOutLineInfo>() // 修复:明确类型
// 合并 suggestions 和 stock out lines,按 (lotId, stockOutLineId) 去重
// 合并 suggestions 和 stock out lines,按 lotId 去重(合并相同 lot 的多个条目)
val lotMap = mutableMapOf<Long?, MutableMap<String, Any?>>()
// 第一步:处理 suggestions,构建基础数据
suggestions.forEach { spl: SuggestedPickLot ->
val ill = spl.suggestedLotLine
if (ill != null) {
val lotId = ill.id
val il = ill.inventoryLot
val w = ill.warehouse
val isExpired = il?.expiryDate?.let { exp: LocalDate -> exp.isBefore(today) } == true
val availableQty = (ill.inQty ?: zero)
.minus(ill.outQty ?: zero)
.minus(ill.holdQty ?: zero)
// 查找对应的 stock out line(通过 lotId 查找,而不是通过 suggestion.stockOutLine)
val stockOutLine = stockOutLines.find { sol: StockOutLineInfo ->
sol.inventoryLotLineId == lotId
}
if (lotMap.containsKey(lotId)) {
// 如果已存在,合并 requiredQty(累加)
val existing = lotMap[lotId]!!
val existingRequiredQty = (existing["requiredQty"] as? BigDecimal) ?: zero
val newRequiredQty = spl.qty ?: zero
existing["requiredQty"] = existingRequiredQty.plus(newRequiredQty)
// 如果当前有 stockOutLine 但 existing 没有,更新 stockOutLine 信息
if (stockOutLine != null && existing["stockOutLineId"] == null) {
existing["stockOutLineId"] = stockOutLine.id
existing["stockOutLineStatus"] = stockOutLine.status
existing["stockOutLineQty"] = numToBigDecimal(stockOutLine.qty as? Number)
existing["actualPickQty"] = numToBigDecimal(stockOutLine.qty as? Number)
existing["processingStatus"] = when {
stockOutLine.status == "completed" -> "completed"
stockOutLine.status == "rejected" -> "rejected"
else -> "pending"
}
existing["lotAvailability"] = when {
isExpired -> "expired"
stockOutLine.status == "rejected" -> "rejected"
availableQty <= zero -> "insufficient_stock"
ill.status?.value == "unavailable" -> "status_unavailable"
else -> "available"
}
}
} else {
// 首次遇到,创建新条目
lotMap[lotId] = mutableMapOf(
"id" to lotId,
"lotNo" to il?.lotNo,
"expiryDate" to il?.expiryDate?.toString(),
"location" to w?.code,
"stockUnit" to (ill.stockUom?.uom?.udfudesc ?: uom?.udfudesc ?: "N/A"),
"availableQty" to availableQty,
"requiredQty" to (spl.qty ?: zero),
"actualPickQty" to numToBigDecimal(stockOutLine?.qty as? Number),
"inQty" to ill.inQty,
"outQty" to ill.outQty,
"holdQty" to ill.holdQty,
"lotStatus" to ill.status?.value,
"stockInLineId" to il?.stockInLine?.id,
"lotAvailability" to when {
isExpired -> "expired"
stockOutLine?.status == "rejected" -> "rejected"
availableQty <= zero -> "insufficient_stock"
ill.status?.value == "unavailable" -> "status_unavailable"
else -> "available"
},
"processingStatus" to when {
stockOutLine?.status == "completed" -> "completed"
stockOutLine?.status == "rejected" -> "rejected"
else -> "pending"
},
"suggestedPickLotId" to spl.id,
"stockOutLineId" to stockOutLine?.id,
"stockOutLineStatus" to stockOutLine?.status,
"stockOutLineQty" to numToBigDecimal(stockOutLine?.qty as? Number),
"router" to mapOf(
"id" to null,
"index" to w?.order,
"route" to w?.code,
"area" to w?.code,
"itemCode" to item?.id,
"itemName" to item?.name,
"uomId" to uom?.code,
"noofCarton" to (spl.qty ?: zero)
)
)
}
}
}
// 第二步:处理 stock out lines(包括没有 suggestion 的,或更新已存在的)
stockOutLines.forEach { sol: StockOutLineInfo ->
val inventoryLotLineId = sol.inventoryLotLineId
val stockOutLineId = sol.id
if (inventoryLotLineId != null) {
// 有 lot 的 stock out line
if (lotMap.containsKey(inventoryLotLineId)) {
// 如果 lot 已存在,更新 stockOutLine 信息(如果还没有)
val existing = lotMap[inventoryLotLineId]!!
if (existing["stockOutLineId"] == null) {
existing["stockOutLineId"] = stockOutLineId
existing["stockOutLineStatus"] = sol.status
existing["stockOutLineQty"] = numToBigDecimal(sol.qty as? Number)
existing["actualPickQty"] = numToBigDecimal(sol.qty as? Number)
existing["processingStatus"] = when {
sol.status == "completed" -> "completed"
sol.status == "rejected" -> "rejected"
else -> "pending"
}
// 更新 lotAvailability(如果 stockOutLine 是 rejected)
if (sol.status == "rejected") {
existing["lotAvailability"] = "rejected"
}
}
} else {
// lot 不存在,创建新条目(没有 suggestion 的情况)
val ill = inventoryLotLineRepository.findById(inventoryLotLineId).orElse(null)
if (ill != null) {
val il = ill.inventoryLot
val w = ill.warehouse
val isExpired = il?.expiryDate?.let { exp: LocalDate -> exp.isBefore(today) } == true
val availableQty = (ill.inQty ?: zero)
.minus(ill.outQty ?: zero)
.minus(ill.holdQty ?: zero)
// 查找对应的 suggestion(如果有)
val suggestion = suggestions.find { spl: SuggestedPickLot ->
spl.suggestedLotLine?.id == inventoryLotLineId
}
lotMap[inventoryLotLineId] = mutableMapOf(
"id" to inventoryLotLineId,
"lotNo" to il?.lotNo,
"expiryDate" to il?.expiryDate?.toString(),
"location" to w?.code,
"stockUnit" to (ill.stockUom?.uom?.udfudesc ?: uom?.udfudesc ?: "N/A"),
"availableQty" to availableQty,
"requiredQty" to (suggestion?.qty ?: zero),
"actualPickQty" to numToBigDecimal(sol.qty as? Number),
"inQty" to ill.inQty,
"outQty" to ill.outQty,
"holdQty" to ill.holdQty,
"lotStatus" to ill.status?.value,
"stockInLineId" to il?.stockInLine?.id,
"lotAvailability" to when {
isExpired -> "expired"
sol.status == "rejected" -> "rejected"
availableQty <= zero -> "insufficient_stock"
ill.status?.value == "unavailable" -> "status_unavailable"
else -> "available"
},
"processingStatus" to when {
sol.status == "completed" -> "completed"
sol.status == "rejected" -> "rejected"
else -> "pending"
},
"suggestedPickLotId" to suggestion?.id,
"stockOutLineId" to stockOutLineId,
"stockOutLineStatus" to sol.status,
"stockOutLineQty" to numToBigDecimal(sol.qty as? Number),
"router" to mapOf(
"id" to null,
"index" to w?.order,
"route" to w?.code,
"area" to w?.code,
"itemCode" to item?.id,
"itemName" to item?.name,
"uomId" to uom?.code,
"noofCarton" to (suggestion?.qty ?: zero)
)
)
}
}
}
}
val lots = lotMap.values.toList()
// 处理没有 lot 的 stock out lines(noLot = true)
val stockouts = stockOutLines
.filter { sol: StockOutLineInfo -> sol.inventoryLotLineId == null }
.map { sol: StockOutLineInfo ->
mapOf(
"id" to sol.id,
"status" to sol.status,
"qty" to numToBigDecimal(sol.qty as? Number),
"lotId" to null,
"lotNo" to "",
"location" to "",
"availableQty" to null,
"noLot" to true
)
}
// 获取该 line 的 suggestions 和 stock out lines
val suggestions = lineId?.let { suggestionsByLineId[it] } ?: emptyList()
val stockOutLines = lineId?.let { stockOutLinesByLineId[it] } ?: emptyList()
// 构建 lots(合并相同 lot 的多个 suggestions)
val lotMap = mutableMapOf<Long?, LotDetailResponse>()
suggestions.forEach { spl ->
val ill = spl.suggestedLotLine
if (ill != null && ill.id != null) {
val illId = ill.id!!
val illEntity = inventoryLotLinesMap[illId] ?: ill
val il = illEntity.inventoryLot
val w = illEntity.warehouse
val isExpired = il?.expiryDate?.let { exp -> exp.isBefore(today) } == true
val availableQty = (illEntity.inQty ?: zero)
.minus(illEntity.outQty ?: zero)
.minus(illEntity.holdQty ?: zero)
// 查找对应的 stock out line
val stockOutLine = stockOutLines.find { sol ->
sol.inventoryLotLineId == illId
}
// 计算 actualPickQty
val actualPickQty = stockOutLine?.qty?.let { numToBigDecimal(it as? Number) }
if (lotMap.containsKey(illId)) {
// 合并 requiredQty
val existing = lotMap[illId]!!
val newRequiredQty = (existing.requiredQty ?: zero).plus(spl.qty ?: zero)
lotMap[illId] = existing.copy(requiredQty = newRequiredQty)
} else {
lotMap[illId] = LotDetailResponse(
id = illId,
lotNo = il?.lotNo,
expiryDate = il?.expiryDate,
location = w?.code,
stockUnit = illEntity.stockUom?.uom?.udfudesc ?: uom?.udfudesc ?: "N/A",
availableQty = availableQty,
requiredQty = spl.qty,
actualPickQty = actualPickQty,
inQty = illEntity.inQty,
outQty = illEntity.outQty,
holdQty = illEntity.holdQty,
lotStatus = illEntity.status?.value,
lotAvailability = when {
isExpired -> "expired"
stockOutLine?.status == "rejected" -> "rejected"
availableQty <= zero -> "insufficient_stock"
illEntity.status?.value == "unavailable" -> "status_unavailable"
else -> "available"
},
processingStatus = when {
stockOutLine?.status == "completed" -> "completed"
stockOutLine?.status == "rejected" -> "rejected"
else -> "pending"
},
suggestedPickLotId = spl.id,
stockOutLineId = stockOutLine?.id,
stockOutLineStatus = stockOutLine?.status,
stockOutLineQty = stockOutLine?.qty?.let { numToBigDecimal(it as? Number) },
router = RouterInfoResponse(
id = null,
index = w?.order.toString(),
route = w?.code,
area = w?.code
)
)
}
}
}
val lots = lotMap.values.toList()
// 构建 stockouts(包括没有 lot 的)
val stockouts = stockOutLines.map { sol ->
val illId = sol.inventoryLotLineId
val ill = illId?.let { inventoryLotLinesMap[it] }
val il = ill?.inventoryLot
val w = ill?.warehouse
val available = if (ill == null) null else
(ill.inQty ?: zero)
.minus(ill.outQty ?: zero)
.minus(ill.holdQty ?: zero)
mapOf(
"id" to lineId,
"pickOrderLineId" to lineId,
"pickOrderId" to po.id,
"requiredQty" to pol.qty,
"status" to pol.status?.value,
"item" to mapOf(
"id" to item?.id,
"code" to item?.code,
"name" to item?.name,
"uomCode" to uom?.code,
"uomDesc" to uom?.udfudesc,
"uomShortDesc" to uom?.udfShortDesc
),
"lots" to lots,
"stockouts" to stockouts
StockOutDetailResponse(
id = sol.id,
status = sol.status,
qty = sol.qty?.let { numToBigDecimal(it as? Number) },
lotId = ill?.id,
lotNo = il?.lotNo ?: "",
location = w?.code ?: "",
availableQty = available,
noLot = (ill == null)
)
}
lineCountsPerPickOrder.add(lineDtos.size)
allPickOrderLines.addAll(lineDtos)
PickOrderLineDetailResponse(
id = lineId,
requiredQty = pol.qty,
status = pol.status?.value,
item = ItemInfoResponse(
id = item?.id,
code = item?.code,
name = item?.name,
uomCode = uom?.code,
uomDesc = uom?.udfudesc,
uomShortDesc = uom?.udfShortDesc
),
lots = lots,
stockouts = stockouts
)
}
// 按 router index 排序


lineCountsPerPickOrder.add(lineDtos.size)
allPickOrderLines.addAll(lineDtos)
}

// 排序、fgInfo、mergedPickOrder 这些也全部沿用你当前代码,只要用上面定义好的 doPickOrder/doPickOrderId 即可:
allPickOrderLines.sortWith(compareBy(
{ line ->
val lots = line["lots"] as? List<Map<String, Any?>>
val firstLot = lots?.firstOrNull()
val router = firstLot?.get("router") as? Map<String, Any?>
val indexValue = router?.get("index")
// 提取楼层排序值:1F = 1, 2F/4F = 2, 其他 = 3
val floorSortValue = when (indexValue) {
is String -> {
val parts = indexValue.split("-")
if (parts.isNotEmpty()) {
val floorPart = parts[0].uppercase() // "1F", "2F", "4F"
when (floorPart) {
"1F" -> 1
"2F", "4F" -> 2
else -> 3
}
} else {
3
{ line ->
val lots = line["lots"] as? List<Map<String, Any?>>
val firstLot = lots?.firstOrNull()
val router = firstLot?.get("router") as? Map<String, Any?>
val indexValue = router?.get("index")
val floorSortValue = when (indexValue) {
is String -> {
val parts = indexValue.split("-")
if (parts.isNotEmpty()) {
val floorPart = parts[0].uppercase()
when (floorPart) {
"1F" -> 1
"2F", "4F" -> 2
else -> 3
}
} else 3
}
else -> 3
}
else -> 3
}
floorSortValue
},
{ line ->
val lots = line["lots"] as? List<Map<String, Any?>>
val firstLot = lots?.firstOrNull()
val router = firstLot?.get("router") as? Map<String, Any?>
val indexValue = router?.get("index")
// 提取数字部分:格式为 "store_id-number" (如 "2F-004")
when (indexValue) {
is Number -> indexValue.toInt()
is String -> {
val parts = indexValue.split("-")
if (parts.size > 1) {
parts.last().toIntOrNull() ?: 999999
} else {
indexValue.toIntOrNull() ?: 999999
floorSortValue
},
{ line ->
val lots = line["lots"] as? List<Map<String, Any?>>
val firstLot = lots?.firstOrNull()
val router = firstLot?.get("router") as? Map<String, Any?>
val indexValue = router?.get("index")
when (indexValue) {
is Number -> indexValue.toInt()
is String -> {
val parts = indexValue.split("-")
if (parts.size > 1) {
parts.last().toIntOrNull() ?: 999999
} else {
indexValue.toIntOrNull() ?: 999999
}
}
else -> 999999
}
else -> 999999
}
}
))
// 构建 FG 信息
))

val fgInfo = mapOf(
"doPickOrderId" to doPickOrderId,
"ticketNo" to doPickOrder.ticketNo,
@@ -3737,8 +3583,7 @@ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, A
"truckLanceCode" to doPickOrder.truckLanceCode,
"departureTime" to doPickOrder.truckDepartureTime?.toString()
)
// 构建合并后的 pick order 对象

val mergedPickOrder = if (pickOrders.isNotEmpty()) {
val firstPickOrder = pickOrders.first()
mapOf(
@@ -3755,13 +3600,14 @@ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, A
} else {
null
}
return mapOf(
"fgInfo" to fgInfo,
"pickOrders" to listOfNotNull(mergedPickOrder)
)
}
open fun getAllPickOrderLotsWithDetailsHierarchicalold(userId: Long): Map<String, Any?> {
*/
open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> {
println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===")
println("userId filter: $userId")
@@ -3796,6 +3642,8 @@ open fun getAllPickOrderLotsWithDetailsHierarchicalold(userId: Long): Map<String
AND po2.status IN ('assigned', 'released', 'picking')
AND po2.deleted = false
)
AND dpo.handled_by = :userId
AND dpo.ticket_status IN ('released','picking')
AND po.deleted = false
AND dpo.deleted = false
LIMIT 1
@@ -3812,7 +3660,7 @@ open fun getAllPickOrderLotsWithDetailsHierarchicalold(userId: Long): Map<String
val doPickOrderId = (doPickOrderInfo["do_pick_order_id"] as? Number)?.toLong()
println(" Found do_pick_order ID: $doPickOrderId")
val doTicketStatus = doPickOrderInfo["ticket_status"]
val doTicketStatus = doPickOrderInfo["doTicketStatus"]
// Step 2: 获取该 do_pick_order 下的所有 pick orders
val pickOrdersSql = """
SELECT DISTINCT


+ 4
- 3
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Vedi File

@@ -184,7 +184,8 @@ class StockTakeRecordService(
approverName = approverName,
startTime = latestStockTake?.actualStart,
endTime = latestStockTake?.actualEnd,
ReStockTakeTrueFalse = reStockTakeTrueFalse
ReStockTakeTrueFalse = reStockTakeTrueFalse,
planStartDate = latestStockTake?.planStart?.toLocalDate()

)
)
@@ -322,8 +323,8 @@ class StockTakeRecordService(
TotalItemNumber = totalItemNumber,
startTime = latestStockTake?.actualStart,
endTime = latestStockTake?.actualEnd,
ReStockTakeTrueFalse = reStockTakeTrueFalse
ReStockTakeTrueFalse = reStockTakeTrueFalse,
planStartDate = latestStockTake?.planStart?.toLocalDate()
)
)
}


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt Vedi File

@@ -23,7 +23,8 @@ data class AllPickedStockTakeListReponse(
val approverName: String?,
val TotalItemNumber: Int,
val ReStockTakeTrueFalse: Boolean,

@JsonFormat(pattern = "yyyy-MM-dd")
val planStartDate: LocalDate?,
)
data class InventoryLotDetailResponse(
val id: Long,


Caricamento…
Annulla
Salva