From b3f737ce13738bf9ad9f1b69eaf1725713bf5d00 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Fri, 10 Apr 2026 17:24:20 +0800 Subject: [PATCH] efficent improve V1 --- .../entity/DoPickOrderRepository.kt | 18 + .../service/DeliveryOrderService.kt | 284 ++++++++++++--- .../service/DoPickOrderService.kt | 39 +- .../pickOrder/service/PickOrderService.kt | 332 +++++++++++++++++- 4 files changed, 606 insertions(+), 67 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRepository.kt index 560dd0b..0d41964 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRepository.kt @@ -79,4 +79,22 @@ fun findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( @Param("requiredDate") requiredDate: LocalDate, @Param("statuses") statuses: List, ): List + + @Query(""" + SELECT dpo FROM DoPickOrder dpo + WHERE dpo.storeId = :storeId + AND dpo.requiredDeliveryDate = :requiredDeliveryDate + AND dpo.ticketStatus IN :statuses + AND EXISTS ( + SELECT 1 FROM DoPickOrderLine dpol + WHERE dpol.doPickOrderId = dpo.id + AND dpol.deleted = false + AND (dpol.status IS NULL OR dpol.status != 'issue') + ) +""") +fun findByStoreIdAndRequiredDeliveryDateAndTicketStatusInWithNonIssueLines( + storeId: String, + requiredDeliveryDate: LocalDate, + statuses: List +): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index 11966e5..5d2557c 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt @@ -81,6 +81,9 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import com.ffii.fpsms.modules.deliveryOrder.entity.models.DeliveryOrderInfoLite import com.ffii.fpsms.modules.deliveryOrder.entity.models.DeliveryOrderInfoLiteDto +import com.ffii.fpsms.modules.stock.entity.InventoryLotLine +import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo +import java.util.Locale @Service open class DeliveryOrderService( private val deliveryOrderRepository: DeliveryOrderRepository, @@ -1133,107 +1136,133 @@ open class DeliveryOrderService( fields: MutableList>, params: MutableMap ): Map { - + val doPickOrderRecord = doPickOrderRecordRepository.findById(request.doPickOrderId).orElseThrow { NoSuchElementException("DoPickOrderRecord not found with ID: ${request.doPickOrderId}") } - + val doPickOrderLineRecords = doPickOrderLineRecordRepository.findByDoPickOrderId(doPickOrderRecord.recordId) - + val pickOrderIds = doPickOrderLineRecords.mapNotNull { it.pickOrderId }.distinct() if (pickOrderIds.isEmpty()) { throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated pick orders") } - + val deliveryOrderIds = doPickOrderLineRecords.mapNotNull { it.doOrderId }.distinct() if (deliveryOrderIds.isEmpty()) { throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated delivery orders") } - + val deliveryNoteInfo = deliveryOrderIds.flatMap { deliveryOrderId -> deliveryOrderRepository.findDeliveryOrderInfoById(deliveryOrderId) }.toMutableList() - + val pickOrderId = pickOrderIds.first() val truckNo = doPickOrderRecord.truckLanceCode ?: "" val selectedPickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) - + val allLines = deliveryNoteInfo.flatMap { info -> info.deliveryOrderLines.map { line -> line } } - - val pickOrderLines = pickOrderIds.flatMap { pickOrderId -> - pickOrderLineRepository.findAllByPickOrderId(pickOrderId) + + val pickOrderLines = pickOrderIds.flatMap { pid -> + pickOrderLineRepository.findAllByPickOrderId(pid) } - + val pickOrderLineIdsByItemId = pickOrderLines .groupBy { it.item?.id } .mapValues { (_, lines) -> lines.mapNotNull { it.id } } - + val allPickOrderLineIds = pickOrderLines.mapNotNull { it.id } - val stockOutLinesByPickOrderLineId = if (allPickOrderLineIds.isNotEmpty()) { - allPickOrderLineIds.associateWith { pickOrderLineId -> - stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) + val stockOutLinesByPickOrderLineId: Map> = + if (allPickOrderLineIds.isNotEmpty()) { + allPickOrderLineIds.associateWith { polId -> + stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(polId) + } + } else { + emptyMap() } + + val doStoreFloorKey = doPickOrderRecord.storeId?.toString()?.trim()?.uppercase(Locale.ROOT) + ?.replace("/", "") + ?.replace(" ", "") + ?: "" + + val uniqueItemIdsForSort = allLines.mapNotNull { it.itemId }.distinct() + val itemsById = if (uniqueItemIdsForSort.isNotEmpty()) { + itemsRepository.findAllById(uniqueItemIdsForSort).associateBy { it.id!! } } else { emptyMap() } - - val sortedLines = allLines.sortedBy { line -> - line.itemId?.let { itemId -> - getWarehouseOrderByItemId(itemId) - } ?: Int.MAX_VALUE + + val sortedLines = when (doStoreFloorKey) { + "2F" -> allLines.sortedWith( + compareBy( + { line -> itemsById[line.itemId]?.item_Order ?: Int.MAX_VALUE }, + { line -> line.itemNo }, + ), + ) + "4F" -> allLines.sortedBy { line -> + line.itemId?.let { getWarehouseOrderByItemId(it) } ?: Int.MAX_VALUE + } + else -> allLines.sortedBy { line -> + line.itemId?.let { getWarehouseOrderByItemId(it) } ?: Int.MAX_VALUE + } } - - val uniqueItemIds = sortedLines.mapNotNull { it.itemId }.distinct() - val itemsMap = if (uniqueItemIds.isNotEmpty()) { - itemsRepository.findAllById(uniqueItemIds).associateBy { it.id } + + val allIllIds = stockOutLinesByPickOrderLineId.values + .flatMap { lines -> lines.mapNotNull { it.inventoryLotLineId } } + .distinct() + val illById: Map = if (allIllIds.isNotEmpty()) { + inventoryLotLineRepository.findAllById(allIllIds).associateBy { it.id!! } } else { emptyMap() } - + + val itemsMap = itemsById + sortedLines.forEach { line -> val field = mutableMapOf() - + field["sequenceNumber"] = (fields.size + 1).toString() field["itemNo"] = line.itemNo field["itemName"] = line.itemName ?: "" field["uom"] = line.uom ?: "" - + val actualPickQty = if (line.itemId != null) { val pickOrderLineIdsForItem = pickOrderLineIdsByItemId[line.itemId] ?: emptyList() - val totalQty = pickOrderLineIdsForItem.sumOf { pickOrderLineId -> - val stockOutLines = stockOutLinesByPickOrderLineId[pickOrderLineId] ?: emptyList() - stockOutLines.sumOf { it.qty } + val totalQty = pickOrderLineIdsForItem.sumOf { polId -> + stockOutLinesByPickOrderLineId[polId].orEmpty().sumOf { it.qty } } totalQty.toString() } else { line.qty.toString() } field["qty"] = actualPickQty - + field["shortName"] = line.uomShortDesc ?: "" - + val route = line.itemId?.let { itemId -> - getWarehouseCodeByItemId(itemId) + routeFromStockOutsForItem( + itemId, + pickOrderLineIdsByItemId, + stockOutLinesByPickOrderLineId, + illById, + ).takeIf { it != "-" } ?: getWarehouseCodeByItemId(itemId) } ?: "-" field["route"] = route - - //USE STOCK OUT LINE + val lotNo = line.itemId?.let { itemId -> val pickOrderLineIdsForItem = pickOrderLineIdsByItemId[itemId] ?: emptyList() - val lotNumbers = pickOrderLineIdsForItem.flatMap { pickOrderLineId -> - val stockOutLines = stockOutLinesByPickOrderLineId[pickOrderLineId] ?: emptyList() - stockOutLines.mapNotNull { it.lotNo } + val lotNumbers = pickOrderLineIdsForItem.flatMap { polId -> + stockOutLinesByPickOrderLineId[polId].orEmpty().mapNotNull { it.lotNo } }.distinct().joinToString(", ") - - lotNumbers.ifBlank { - "沒有庫存" - } + + lotNumbers.ifBlank { "沒有庫存" } } ?: "沒有庫存" - + field["lotNo"] = lotNo - + val signOff = line.itemId?.let { itemId -> val item = itemsMap[itemId] if (item?.isEgg == true) { @@ -1243,21 +1272,21 @@ open class DeliveryOrderService( } } ?: "" field["signOff"] = signOff - + fields.add(field) } - + params["dnTitle"] = "送貨單" params["colQty"] = "已提數量" params["totalCartonTitle"] = "總箱數:" params["deliveryNoteCodeTitle"] = "送貨單編號:" params["deliveryNoteCode"] = doPickOrderRecord.deliveryNoteCode ?: "" - + params["numOfCarton"] = request.numOfCarton.toString() if (params["numOfCarton"] == "0") { params["numOfCarton"] = "" } - + params["shopName"] = doPickOrderRecord.shopName ?: deliveryNoteInfo[0].shopName ?: "" params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: "" params["deliveryDate"] = @@ -1269,13 +1298,30 @@ open class DeliveryOrderService( params["loadingSequence"] = doPickOrderRecord.loadingSequence?.let { "裝載順序:$it" } ?: "" - + return mapOf( "report" to PdfUtils.fillReport(deliveryNote, fields, params), "filename" to deliveryNoteInfo.joinToString("_") { it.code } ) } - + + private fun routeFromStockOutsForItem( + itemId: Long, + pickOrderLineIdsByItemId: Map>, + stockOutLinesByPickOrderLineId: Map>, + illById: Map, + ): String { + val polIds = pickOrderLineIdsByItemId[itemId] ?: return "-" + val codes = linkedSetOf() + for (polId in polIds) { + for (sol in stockOutLinesByPickOrderLineId[polId].orEmpty()) { + val illId = sol.inventoryLotLineId ?: continue + val ill = illById[illId] ?: continue + ill.warehouse?.code?.takeIf { it.isNotBlank() }?.let { codes.add(it) } + } + } + return if (codes.isEmpty()) "-" else codes.joinToString(", ") + } //Print Delivery Note @Transactional open fun printDeliveryNote(request: PrintDeliveryNoteRequest) { @@ -1674,6 +1720,144 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } ) } + /** + * Workbench NO-HOLD release: + * - Create pick_order + stock_out + * - Do NOT create suggested_pick_lot / stock_out_line + * - Do NOT update inventory_lot_line.holdQty + * + * Downstream should be handled by workbench services (no-hold suggestion + stock_out_line creation). + */ + @Transactional(rollbackFor = [Exception::class]) + open fun releaseDeliveryOrderWithoutTicketNoHold(request: ReleaseDoRequest): ReleaseDoResult { + println(" DEBUG: Starting releaseDeliveryOrderWithoutTicketNoHold for DO ID: ${request.id}") + + val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) + ?: throw NoSuchElementException("Delivery Order not found") + + if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) { + throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release") + } + + // Mark released (same semantics as normal release) + deliveryOrder.status = DeliveryOrderStatus.RECEIVING + deliveryOrderRepository.save(deliveryOrder) + + // Create pick order (same as normal release) + val pols = deliveryOrder.deliveryOrderLines + .filter { it.deleted != true } + .map { + SavePickOrderLineRequest( + itemId = it.item?.id, + qty = it.qty ?: BigDecimal.ZERO, + uomId = it.uom?.id, + ) + } + val po = SavePickOrderRequest( + doId = deliveryOrder.id, + type = PickOrderType.DELIVERY_ORDER, + targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(), + pickOrderLine = pols + ) + + val createdPickOrder = pickOrderService.create(po) + val consoCode = pickOrderService.assignConsoCode() + val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null) + + if (pickOrderEntity != null) { + pickOrderEntity.consoCode = consoCode + pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED + pickOrderRepository.saveAndFlush(pickOrderEntity) + + // Create stock out header only; stock_out_line is created by workbench service + val stockOut = StockOut().apply { + this.type = "do" + this.consoPickOrderCode = consoCode + this.status = StockOutStatus.PENDING.status + this.handler = request.userId + } + stockOutRepository.saveAndFlush(stockOut) + } + + // Truck selection (reuse normal logic) + val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() + val supplierCode = deliveryOrder.supplier?.code + val preferredFloor = when (supplierCode) { + "P06B" -> "4F" + "P07", "P06D" -> "2F" + else -> "2F" + } + + val truck = deliveryOrder.shop?.id?.let { shopId -> + val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) + val preferredStoreId = when (preferredFloor) { + "2F" -> "2F" + "4F" -> "4F" + "3F" -> "3F" + else -> "2F" + } + + val matchedTrucks = trucks.filter { it.storeId == preferredStoreId } + if (matchedTrucks.isEmpty()) { + null + } else { + if (preferredStoreId == "4F" && matchedTrucks.size > 1) { + deliveryOrder.estimatedArrivalDate?.let { estimatedArrivalDate -> + val targetDate2 = estimatedArrivalDate.toLocalDate() + val dayAbbr = when (targetDate2.dayOfWeek) { + java.time.DayOfWeek.MONDAY -> "Mon" + java.time.DayOfWeek.TUESDAY -> "Tue" + java.time.DayOfWeek.WEDNESDAY -> "Wed" + java.time.DayOfWeek.THURSDAY -> "Thu" + java.time.DayOfWeek.FRIDAY -> "Fri" + java.time.DayOfWeek.SATURDAY -> "Sat" + java.time.DayOfWeek.SUNDAY -> "Sun" + } + + val dayMatchedTrucks = matchedTrucks.filter { + it.truckLanceCode?.contains(dayAbbr, ignoreCase = true) == true + } + + if (dayMatchedTrucks.isNotEmpty()) { + dayMatchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } + } else { + matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } + } + } ?: run { + matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } + } + } else { + matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } + } + } + } + + val defaultTruckId = 5577L + val effectiveTruck = truck ?: truckRepository.findById(defaultTruckId).orElse(null) + val usedDefaultTruck = (truck == null) + if (effectiveTruck == null) { + val errorMsg = "No matching truck for preferredFloor ($preferredFloor) and default truck $defaultTruckId not found. Skipping DO ${deliveryOrder.id}." + throw IllegalStateException(errorMsg) + } + + return ReleaseDoResult( + deliveryOrderId = deliveryOrder.id!!, + deliveryOrderCode = deliveryOrder.code, + pickOrderId = createdPickOrder.id!!, + pickOrderCode = pickOrderEntity?.code, + shopId = deliveryOrder.shop?.id, + shopCode = deliveryOrder.shop?.code, + shopName = deliveryOrder.shop?.name, + estimatedArrivalDate = targetDate, + preferredFloor = preferredFloor, + truckId = effectiveTruck.id, + truckDepartureTime = effectiveTruck.departureTime, + truckLanceCode = effectiveTruck.truckLanceCode, + loadingSequence = effectiveTruck.loadingSequence, + usedDefaultTruck = usedDefaultTruck + ) + } + private fun getDayOfWeekAbbr(date: LocalDate): String = when (date.dayOfWeek) { java.time.DayOfWeek.MONDAY -> "Mon" diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt index d6f4191..e1fa914 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt @@ -379,7 +379,7 @@ open class DoPickOrderService( } fun getSummaryByStore(storeId: String, requiredDate: LocalDate?, releaseType: String): StoreLaneSummary { val targetDate = requiredDate ?: LocalDate.now() - println(" DEBUG: Getting summary for store=$storeId, date=$targetDate") + //println(" DEBUG: Getting summary for store=$storeId, date=$targetDate") val actualStoreId = when (storeId) { "2/F" -> "2/F" @@ -387,11 +387,18 @@ open class DoPickOrderService( else -> storeId } + /* val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( actualStoreId, targetDate, listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) ) + */ + val allRecords = doPickOrderRepository + .findByStoreIdAndRequiredDeliveryDateAndTicketStatusInWithNonIssueLines( + actualStoreId, targetDate, + listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) + ) val filteredByReleaseType = when (releaseType.lowercase()) { "batch" -> allRecords.filter { it.releaseType == "batch" } "single" -> allRecords.filter { it.releaseType == "single" } @@ -408,9 +415,9 @@ open class DoPickOrderService( "single" -> finishedRecords.filter { it.releaseType == "single" } else -> finishedRecords // "all" 或其他值,不过滤 } - println(" DEBUG: Found ${allRecords.size} records for date $targetDate") - println(" DEBUG: Found ${finishedRecords.size} finished records for date $targetDate") - + //println(" DEBUG: Found ${allRecords.size} records for date $targetDate") + // println(" DEBUG: Found ${finishedRecords.size} finished records for date $targetDate") + /* val filteredRecords = filteredByReleaseType.filter { doPickOrder -> val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) if (!hasNonIssueLines) { @@ -418,20 +425,24 @@ open class DoPickOrderService( } hasNonIssueLines } + */ + // println(" DEBUG: After filtering, ${filteredRecords.size} records remain") - println(" DEBUG: After filtering, ${filteredRecords.size} records remain") - - val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } + val grouped = filteredByReleaseType.groupBy { it.truckDepartureTime to it.truckLanceCode } .mapValues { (key, list) -> val (truckDepartureTime, truckLanceCode) = key // 计算匹配的 finishedRecords 数量 - val matchingFinishedCount = finishedRecords.count { record -> - (record.truckDepartureTime == truckDepartureTime) && - (record.truckLanceCode == truckLanceCode) - } - println(" DEBUG: Group key - truckDepartureTime: $truckDepartureTime, truckLanceCode: $truckLanceCode") - println(" DEBUG: Found ${list.size} active records in this group") - println(" DEBUG: Found $matchingFinishedCount finished records matching this group") + // val matchingFinishedCount = finishedRecords.count { record -> + //(record.truckDepartureTime == truckDepartureTime) && + // (record.truckLanceCode == truckLanceCode) + // } + val matchingFinishedCount = filteredFinishedRecords.count { record -> + record.truckDepartureTime == truckDepartureTime && + record.truckLanceCode == truckLanceCode + } + // println(" DEBUG: Group key - truckDepartureTime: $truckDepartureTime, truckLanceCode: $truckLanceCode") + //println(" DEBUG: Found ${list.size} active records in this group") + //println(" DEBUG: Found $matchingFinishedCount finished records matching this group") LaneBtn( truckLanceCode = list.first().truckLanceCode ?: "", unassigned = list.count { it.handledBy == null }, diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt index 9ce1158..5127cb3 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt @@ -3512,10 +3512,10 @@ ORDER BY val enrichedResults = filteredResults return enrichedResults } - +/* open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map { - println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===") - println("userId filter: $userId") + //println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===") + // println("userId filter: $userId") val user = userService.find(userId).orElse(null) if (user == null) { @@ -3889,6 +3889,332 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto "pickOrders" to listOfNotNull(mergedPickOrder) ) } +*/ +open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map { + val user = userService.find(userId).orElse(null) + if (user == null) { + println("❌ User not found: $userId") + return emptyMap() + } + // Step 1: 找到該 user 目前處理中的 do_pick_order(你原本就是 LIMIT 1) + val doPickOrderSql = """ + SELECT DISTINCT + dpo.id as do_pick_order_id, + dpo.ticket_no, + dpo.store_id, + dpo.TruckLanceCode, + dpo.truck_departure_time, + dpo.ShopCode, + dpo.ShopName, + dpo.ticket_status as doTicketStatus + FROM fpsmsdb.do_pick_order dpo + INNER JOIN fpsmsdb.do_pick_order_line dpol ON dpol.do_pick_order_id = dpo.id AND dpol.deleted = 0 + INNER JOIN fpsmsdb.pick_order po ON po.id = dpol.pick_order_id + WHERE po.assignTo = :userId + AND po.type = 'do' + AND EXISTS ( + SELECT 1 + FROM fpsmsdb.do_pick_order_line dpol2 + INNER JOIN fpsmsdb.pick_order po2 ON po2.id = dpol2.pick_order_id + WHERE dpol2.do_pick_order_id = dpo.id + AND dpol2.deleted = 0 + 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 + """.trimIndent() + val doPickOrderInfo = jdbcDao.queryForMap(doPickOrderSql, mapOf("userId" to userId)).orElse(null) + ?: return mapOf("fgInfo" to null, "pickOrders" to emptyList()) + val doPickOrderId = (doPickOrderInfo["do_pick_order_id"] as? Number)?.toLong() + ?: return mapOf("fgInfo" to null, "pickOrders" to emptyList()) + val doTicketStatus = doPickOrderInfo["doTicketStatus"] + // Step 2: 找到該 do_pick_order 底下所有 pick orders + val pickOrdersSql = """ + SELECT DISTINCT + dpol.pick_order_id, + dpol.pick_order_code, + dpol.do_order_id, + dpol.delivery_order_code, + po.consoCode, + po.status, + DATE_FORMAT(po.targetDate, '%Y-%m-%d') as targetDate + FROM fpsmsdb.do_pick_order_line dpol + INNER JOIN fpsmsdb.pick_order po ON po.id = dpol.pick_order_id + WHERE dpol.do_pick_order_id = :doPickOrderId + AND dpol.deleted = 0 + AND po.deleted = false + ORDER BY dpol.pick_order_id + """.trimIndent() + val pickOrdersInfo = jdbcDao.queryForList(pickOrdersSql, mapOf("doPickOrderId" to doPickOrderId)) + if (pickOrdersInfo.isEmpty()) { + val fgInfo = mapOf( + "doPickOrderId" to doPickOrderId, + "ticketNo" to doPickOrderInfo["ticket_no"], + "storeId" to doPickOrderInfo["store_id"], + "shopCode" to doPickOrderInfo["ShopCode"], + "shopName" to doPickOrderInfo["ShopName"], + "truckLanceCode" to doPickOrderInfo["TruckLanceCode"], + "departureTime" to doPickOrderInfo["truck_departure_time"], + ) + return mapOf("fgInfo" to fgInfo, "pickOrders" to emptyList()) + } + val pickOrderIds = pickOrdersInfo.mapNotNull { (it["pick_order_id"] as? Number)?.toLong() }.distinct() + val pickOrderCodes = pickOrdersInfo.mapNotNull { it["pick_order_code"] as? String } + val doOrderIds = pickOrdersInfo.mapNotNull { (it["do_order_id"] as? Number)?.toLong() } + val deliveryOrderCodes = pickOrdersInfo.mapNotNull { it["delivery_order_code"] as? String } + val allConsoCodes = pickOrdersInfo.mapNotNull { it["consoCode"] as? String }.distinct() + // Step 3: 一次查完所有 pickOrder 的 line/lot/stockout 明細(避免 N+1) + val linesSqlAll = """ + SELECT + po.id as pickOrderId, + po.code as pickOrderCode, + po.consoCode as pickOrderConsoCode, + DATE_FORMAT(po.targetDate, '%Y-%m-%d') as pickOrderTargetDate, + po.type as pickOrderType, + po.status as pickOrderStatus, + po.assignTo as pickOrderAssignTo, + + pol.id as pickOrderLineId, + pol.qty as pickOrderLineRequiredQty, + pol.status as pickOrderLineStatus, + + i.id as itemId, + i.code as itemCode, + i.name as itemName, + i.item_Order as itemOrder, + uc.code as uomCode, + uc.udfudesc as uomDesc, + uc.udfShortDesc as uomShortDesc, + + ill.id as lotId, + il.lotNo, + DATE_FORMAT(il.expiryDate, '%Y-%m-%d') as expiryDate, + w.name as location, + COALESCE(uc.udfudesc, 'N/A') as stockUnit, + il.stockInLineId as stockInLineId, + w.`order` as routerIndex, + w.code as routerRoute, + + CASE + WHEN sol.status = 'rejected' THEN NULL + ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) + END as availableQty, + + COALESCE(spl.qty, 0) as requiredQty, + COALESCE(sol.qty, 0) as actualPickQty, + spl.id as suggestedPickLotId, + ill.status as lotStatus, + sol.id as stockOutLineId, + sol.status as stockOutLineStatus, + COALESCE(sol.qty, 0) as stockOutLineQty, + COALESCE(ill.inQty, 0) as inQty, + COALESCE(ill.outQty, 0) as outQty, + COALESCE(ill.holdQty, 0) as holdQty, + + CASE + WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' + WHEN sol.status = 'rejected' THEN 'rejected' + WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock' + WHEN ill.status = 'unavailable' THEN 'status_unavailable' + ELSE 'available' + END as lotAvailability, + + CASE + WHEN sol.status = 'completed' THEN 'completed' + WHEN sol.status = 'rejected' THEN 'rejected' + WHEN sol.status = 'created' THEN 'pending' + ELSE 'pending' + END as processingStatus + FROM fpsmsdb.pick_order po + JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false + JOIN fpsmsdb.items i ON i.id = pol.itemId + LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId + LEFT JOIN ( + SELECT spl.pickOrderLineId, spl.suggestedLotLineId AS lotLineId + FROM fpsmsdb.suggested_pick_lot spl + UNION + SELECT sol.pickOrderLineId, sol.inventoryLotLineId + FROM fpsmsdb.stock_out_line sol + WHERE sol.deleted = false + ) ll ON ll.pickOrderLineId = pol.id + LEFT JOIN fpsmsdb.suggested_pick_lot spl + ON spl.pickOrderLineId = pol.id AND spl.suggestedLotLineId = ll.lotLineId + LEFT JOIN fpsmsdb.stock_out_line sol + ON sol.pickOrderLineId = pol.id + AND ( (sol.inventoryLotLineId = ll.lotLineId) + OR (sol.inventoryLotLineId IS NULL AND ll.lotLineId IS NULL) ) + AND sol.deleted = false + LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = ll.lotLineId AND ill.deleted = false + LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId AND il.deleted = false + LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId + WHERE po.id IN (:pickOrderIds) + AND po.deleted = false + ORDER BY + po.id ASC, + COALESCE(w.`order`, 999999) ASC, + pol.id ASC, + il.lotNo ASC + """.trimIndent() + val allLineRows = jdbcDao.queryForList(linesSqlAll, mapOf("pickOrderIds" to pickOrderIds)) + // Kotlin 端組裝:pickOrderId -> lineId -> rows + val byPickOrderId = allLineRows.groupBy { (it["pickOrderId"] as? Number)?.toLong() } + // 用來計算每個 pick order 的 lineCounts(保持你原本回傳欄位) + val lineCountsPerPickOrder = mutableListOf() + val allPickOrderLines = mutableListOf>() + // 依照 Step2 的 pickOrdersInfo 順序處理(避免順序變動) + pickOrdersInfo.forEach { poInfo -> + val pickOrderId = (poInfo["pick_order_id"] as? Number)?.toLong() ?: return@forEach + val rows = byPickOrderId[pickOrderId].orEmpty() + val lineGroups = rows.groupBy { (it["pickOrderLineId"] as? Number)?.toLong() } + val pickOrderLines = lineGroups.mapNotNull { (lineId, lineRows) -> + val first = lineRows.firstOrNull() ?: return@mapNotNull null + val lots = if (lineRows.any { it["lotId"] != null }) { + lineRows + .filter { it["lotId"] != null } + .map { lotRow -> + mapOf( + "id" to lotRow["lotId"], + "lotNo" to lotRow["lotNo"], + "expiryDate" to lotRow["expiryDate"], + "location" to lotRow["location"], + "stockUnit" to lotRow["stockUnit"], + "availableQty" to lotRow["availableQty"], + "requiredQty" to lotRow["requiredQty"], + "actualPickQty" to lotRow["actualPickQty"], + "inQty" to lotRow["inQty"], + "outQty" to lotRow["outQty"], + "holdQty" to lotRow["holdQty"], + "lotStatus" to lotRow["lotStatus"], + "lotAvailability" to lotRow["lotAvailability"], + "processingStatus" to lotRow["processingStatus"], + "suggestedPickLotId" to lotRow["suggestedPickLotId"], + "stockOutLineId" to lotRow["stockOutLineId"], + "stockOutLineStatus" to lotRow["stockOutLineStatus"], + "stockOutLineQty" to lotRow["stockOutLineQty"], + "stockInLineId" to lotRow["stockInLineId"], + "router" to mapOf( + "id" to null, + "index" to lotRow["routerIndex"], + "route" to lotRow["routerRoute"], + "area" to lotRow["routerRoute"], + "itemCode" to lotRow["itemId"], + "itemName" to lotRow["itemName"], + "uomId" to lotRow["uomCode"], + "noofCarton" to lotRow["requiredQty"] + ) + ) + } + } else emptyList() + val stockouts = lineRows + .filter { it["stockOutLineId"] != null } + .distinctBy { it["stockOutLineId"] } + .map { row -> + val lotId = row["lotId"] + val noLot = (lotId == null) + mapOf( + "id" to row["stockOutLineId"], + "status" to row["stockOutLineStatus"], + "qty" to row["stockOutLineQty"], + "lotId" to lotId, + "lotNo" to (row["lotNo"] ?: ""), + "location" to (row["location"] ?: ""), + "availableQty" to row["availableQty"], + "stockInLineId" to row["stockInLineId"], + "noLot" to noLot + ) + } + mapOf( + "id" to lineId, + "requiredQty" to first["pickOrderLineRequiredQty"], + "status" to first["pickOrderLineStatus"], + "itemOrder" to first["itemOrder"], + "item" to mapOf( + "id" to first["itemId"], + "code" to first["itemCode"], + "name" to first["itemName"], + "uomCode" to first["uomCode"], + "uomDesc" to first["uomDesc"], + "uomShortDesc" to first["uomShortDesc"], + ), + "lots" to lots, + "stockouts" to stockouts + ) + } + lineCountsPerPickOrder.add(pickOrderLines.size) + allPickOrderLines.addAll(pickOrderLines) + } + // 保留你原本按 store 樓層的排序規則 + val doStoreFloorKey = doPickOrderInfo["store_id"]?.toString()?.trim()?.uppercase(Locale.ROOT) + ?.replace("/", "") + ?.replace(" ", "") + ?: "" + when (doStoreFloorKey) { + "2F" -> { + allPickOrderLines.sortWith( + compareBy( + { line -> + val v = line["itemOrder"] ?: line["itemorder"] + when (v) { + is Number -> v.toInt() + else -> 999999 + } + }, + { line -> (line["id"] as? Number)?.toLong() ?: Long.MAX_VALUE } + ) + ) + } + "4F" -> { + // 4F:保留 SQL 順序,不做二次排序 + } + else -> { + allPickOrderLines.sortWith( + compareBy { line -> + val lots = line["lots"] as? List> + val firstLot = lots?.firstOrNull() + val router = firstLot?.get("router") as? Map + val indexValue = router?.get("index") + when (indexValue) { + is Number -> indexValue.toInt() + is String -> indexValue.toIntOrNull() ?: 999999 + else -> 999999 + } + } + ) + } + } + val fgInfo = mapOf( + "doPickOrderId" to doPickOrderId, + "ticketNo" to doPickOrderInfo["ticket_no"], + "storeId" to doPickOrderInfo["store_id"], + "shopCode" to doPickOrderInfo["ShopCode"], + "shopName" to doPickOrderInfo["ShopName"], + "truckLanceCode" to doPickOrderInfo["TruckLanceCode"], + "departureTime" to doPickOrderInfo["truck_departure_time"] + ) + val mergedPickOrder = run { + val firstPickOrderInfo = pickOrdersInfo.first() + mapOf( + "pickOrderIds" to pickOrderIds, + "pickOrderCodes" to pickOrderCodes, + "doOrderIds" to doOrderIds, + "deliveryOrderCodes" to deliveryOrderCodes, + "lineCountsPerPickOrder" to lineCountsPerPickOrder, + "consoCodes" to allConsoCodes, + "status" to doTicketStatus, + "targetDate" to firstPickOrderInfo["targetDate"], + "pickOrderLines" to allPickOrderLines + ) + } + return mapOf( + "fgInfo" to fgInfo, + "pickOrders" to listOf(mergedPickOrder) + ) +} + // Fix the type issues in the getPickOrdersByDateAndStore method open fun getPickOrdersByDateAndStore(storeId: String): Map { println("=== Debug: getPickOrdersByDateAndStore ===")