| @@ -79,4 +79,22 @@ fun findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||
| @Param("requiredDate") requiredDate: LocalDate, | |||
| @Param("statuses") statuses: List<DoPickOrderStatus>, | |||
| ): List<DoPickOrder> | |||
| @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<DoPickOrderStatus> | |||
| ): List<DoPickOrder> | |||
| } | |||
| @@ -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<MutableMap<String, Any>>, | |||
| params: MutableMap<String, Any> | |||
| ): Map<String, Any> { | |||
| 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<Long, List<StockOutLineInfo>> = | |||
| 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<Long, InventoryLotLine> = if (allIllIds.isNotEmpty()) { | |||
| inventoryLotLineRepository.findAllById(allIllIds).associateBy { it.id!! } | |||
| } else { | |||
| emptyMap() | |||
| } | |||
| val itemsMap = itemsById | |||
| sortedLines.forEach { line -> | |||
| val field = mutableMapOf<String, Any>() | |||
| 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<Long?, List<Long>>, | |||
| stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>>, | |||
| illById: Map<Long, InventoryLotLine>, | |||
| ): String { | |||
| val polIds = pickOrderLineIdsByItemId[itemId] ?: return "-" | |||
| val codes = linkedSetOf<String>() | |||
| 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" | |||
| @@ -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 }, | |||
| @@ -3512,10 +3512,10 @@ ORDER BY | |||
| val enrichedResults = filteredResults | |||
| return enrichedResults | |||
| } | |||
| /* | |||
| open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | |||
| 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<String, Any?> { | |||
| 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<Any>()) | |||
| val doPickOrderId = (doPickOrderInfo["do_pick_order_id"] as? Number)?.toLong() | |||
| ?: return mapOf("fgInfo" to null, "pickOrders" to emptyList<Any>()) | |||
| 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<Any>()) | |||
| } | |||
| 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<Int>() | |||
| val allPickOrderLines = mutableListOf<Map<String, Any?>>() | |||
| // 依照 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<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 -> 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<String, Any?> { | |||
| println("=== Debug: getPickOrdersByDateAndStore ===") | |||