| @@ -0,0 +1,3 @@ | |||
| @@ -399,242 +399,216 @@ open class DeliveryOrderService( | |||
| return savedDeliveryOrder | |||
| } | |||
| @Transactional(rollbackFor = [Exception::class]) | |||
| open fun releaseDeliveryOrder(request: ReleaseDoRequest): MessageResponse { | |||
| println("�� DEBUG: Starting releaseDeliveryOrder for DO ID: ${request.id}, User ID: ${request.userId}") | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | |||
| ?: throw NoSuchElementException("Delivery Order not found") | |||
| @Transactional(rollbackFor = [Exception::class]) | |||
| open fun releaseDeliveryOrder(request: ReleaseDoRequest): MessageResponse { | |||
| println("🔍 DEBUG: Starting releaseDeliveryOrder for DO ID: ${request.id}, User ID: ${request.userId}") | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | |||
| ?: throw NoSuchElementException("Delivery Order not found") | |||
| println("🔍 DEBUG: Found delivery order - ID: ${deliveryOrder.id}, Shop: ${deliveryOrder.shop?.code}, Status: ${deliveryOrder.status}") | |||
| deliveryOrder.apply { | |||
| status = DeliveryOrderStatus.PENDING | |||
| } | |||
| deliveryOrderRepository.save(deliveryOrder) | |||
| val pols = deliveryOrder.deliveryOrderLines.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) | |||
| println("🔍 DEBUG: Created pick order - ID: ${createdPickOrder.id}") | |||
| 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) | |||
| println("🔍 DEBUG: Assigned consoCode $consoCode to pick order ${createdPickOrder.id}") | |||
| println("�� DEBUG: Found delivery order - ID: ${deliveryOrder.id}, Shop: ${deliveryOrder.shop?.code}, Status: ${deliveryOrder.status}") | |||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | |||
| println("🔍 DEBUG: Loaded ${lines.size} pick order lines from DB") | |||
| deliveryOrder.apply { | |||
| status = DeliveryOrderStatus.PENDING | |||
| } | |||
| deliveryOrderRepository.save(deliveryOrder) | |||
| val pols = deliveryOrder.deliveryOrderLines.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 suggestions = suggestedPickLotService.suggestionForPickOrderLines( | |||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | |||
| ) | |||
| val createdPickOrder = pickOrderService.create(po) | |||
| println("🔍 DEBUG: Created pick order - ID: ${createdPickOrder.id}") | |||
| println("🔍 DEBUG: Got ${suggestions.suggestedList.size} suggested pick lots") | |||
| val consoCode = pickOrderService.assignConsoCode() | |||
| val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null) | |||
| val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | |||
| println("🔍 DEBUG: Saved ${saveSuggestedPickLots.size} suggested pick lots") | |||
| if (pickOrderEntity != null) { | |||
| pickOrderEntity.consoCode = consoCode | |||
| pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED | |||
| pickOrderRepository.saveAndFlush(pickOrderEntity) | |||
| println("�� DEBUG: Assigned consoCode $consoCode to pick order ${createdPickOrder.id}") | |||
| // ✅ Debug: Check pick order lines | |||
| println("�� DEBUG: Pick order has ${pickOrderEntity.pickOrderLines?.size ?: 0} pick order lines") | |||
| pickOrderEntity.pickOrderLines?.forEach { line -> | |||
| println("🔍 DEBUG: Pick order line - Item ID: ${line.item?.id}, Qty: ${line.qty}") | |||
| } | |||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | |||
| println("🔍 DEBUG: Loaded ${lines.size} pick order lines from DB") | |||
| if (lines.isEmpty()) { | |||
| println("⚠️ No pick order lines found; suggestions will be empty") | |||
| } | |||
| // ✅ Create suggested pick lots and hold inventory (like normal release) | |||
| println("🔍 DEBUG: About to call suggestionForPickOrderLines for pick order ${pickOrderEntity.id}") | |||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLines( | |||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | |||
| ) | |||
| println("🔍 DEBUG: Got ${suggestions.suggestedList.size} suggested pick lots") | |||
| if (suggestions.suggestedList.isEmpty()) { | |||
| println("⚠️ WARNING: No suggested pick lots generated - this might be due to no inventory available") | |||
| } | |||
| val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | |||
| println("🔍 DEBUG: Saved ${saveSuggestedPickLots.size} suggested pick lots") | |||
| // ✅ Hold inventory quantities | |||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | |||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | |||
| ) | |||
| saveSuggestedPickLots.forEach { lot -> | |||
| if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) { | |||
| val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine) | |||
| if (lineIndex >= 0) { | |||
| inventoryLotLines[lineIndex].holdQty = | |||
| (inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO) | |||
| } | |||
| // ✅ Hold inventory quantities | |||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | |||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | |||
| ) | |||
| saveSuggestedPickLots.forEach { lot -> | |||
| if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) { | |||
| val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine) | |||
| if (lineIndex >= 0) { | |||
| inventoryLotLines[lineIndex].holdQty = | |||
| (inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO) | |||
| } | |||
| } | |||
| inventoryLotLineRepository.saveAll(inventoryLotLines) | |||
| // ✅ Create stock out record and pre-create stock out lines | |||
| val stockOut = StockOut().apply { | |||
| this.type = "job" | |||
| this.consoPickOrderCode = consoCode | |||
| this.status = StockOutStatus.PENDING.status | |||
| this.handler = request.userId | |||
| } | |||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||
| // ✅ Pre-create stock out lines for suggested lots | |||
| saveSuggestedPickLots.forEach { lot -> | |||
| val polId = lot.pickOrderLine?.id | |||
| val illId = lot.suggestedLotLine?.id | |||
| if (polId != null && illId != null) { | |||
| val existingLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId) | |||
| if (existingLines.isEmpty()) { | |||
| val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null) | |||
| val inventoryLotLine = inventoryLotLineRepository.findById(illId).orElse(null) | |||
| if (pickOrderLine != null && inventoryLotLine != null) { | |||
| val line = StockOutLine().apply { | |||
| this.stockOut = savedStockOut | |||
| this.pickOrderLine = pickOrderLine | |||
| this.inventoryLotLine = inventoryLotLine | |||
| this.item = pickOrderLine.item | |||
| this.status = StockOutLineStatus.PENDING.status | |||
| this.qty = 0.0 | |||
| } | |||
| stockOutLineRepository.save(line) | |||
| } | |||
| inventoryLotLineRepository.saveAll(inventoryLotLines) | |||
| // ✅ Create stock out record and pre-create stock out lines | |||
| val stockOut = StockOut().apply { | |||
| this.type = "job" | |||
| this.consoPickOrderCode = consoCode | |||
| this.status = StockOutStatus.PENDING.status | |||
| this.handler = request.userId | |||
| } | |||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||
| // ✅ Pre-create stock out lines for suggested lots | |||
| saveSuggestedPickLots.forEach { lot -> | |||
| val polId = lot.pickOrderLine?.id | |||
| val illId = lot.suggestedLotLine?.id | |||
| if (polId != null && illId != null) { | |||
| val existingLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId) | |||
| if (existingLines.isEmpty()) { | |||
| val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null) | |||
| val inventoryLotLine = inventoryLotLineRepository.findById(illId).orElse(null) | |||
| if (pickOrderLine != null && inventoryLotLine != null) { | |||
| val line = StockOutLine().apply { | |||
| this.stockOut = savedStockOut | |||
| this.pickOrderLine = pickOrderLine | |||
| this.inventoryLotLine = inventoryLotLine | |||
| this.item = pickOrderLine.item | |||
| this.status = StockOutLineStatus.PENDING.status | |||
| this.qty = 0.0 | |||
| } | |||
| stockOutLineRepository.save(line) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // ✅ CREATE do_pick_order_record entries | |||
| // 第 471-555 行附近 - 修复创建逻辑 | |||
| // ✅ CREATE do_pick_order_record entries | |||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||
| val datePrefix = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) | |||
| // ✅ CREATE do_pick_order entries | |||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||
| val datePrefix = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) | |||
| println("🔍 DEBUG: Target date: $targetDate, Date prefix: $datePrefix") | |||
| val truck = deliveryOrder.shop?.id?.let { shopId -> | |||
| println("🔍 DEBUG: Looking for truck with shop ID: $shopId") | |||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||
| println("🔍 DEBUG: Found ${trucks.size} trucks for shop $shopId") | |||
| if (trucks.size <= 1) { | |||
| // 如果只有一个或没有 truck,直接返回 | |||
| return@let trucks.firstOrNull() | |||
| } | |||
| println("🔍 DEBUG: Target date: $targetDate, Date prefix: $datePrefix") | |||
| // ✅ 分析 DO order lines 中的 items 分布 | |||
| // ✅ 分析 items 来确定 preferredFloor | |||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | |||
| println("🔍 DEBUG: Analyzing ${itemIds.size} unique items in DO order lines") | |||
| // 使用 SQL 查询统计每个楼层的库存数量 | |||
| val inventoryQuery = """ | |||
| SELECT w.store_id as floor, COUNT(*) as inventory_count | |||
| FROM inventory_lot_line ill | |||
| JOIN inventory_lot il ON il.id = ill.inventoryLotId | |||
| JOIN warehouse w ON w.id = ill.warehouseId | |||
| WHERE il.itemId IN (${itemIds.joinToString(",")}) | |||
| AND ill.deleted = false | |||
| AND il.deleted = false | |||
| AND w.deleted = false | |||
| AND ill.inQty > ill.outQty + COALESCE(ill.holdQty, 0) | |||
| SELECT | |||
| w.store_id as floor, | |||
| COUNT(DISTINCT il.itemId) as item_count | |||
| FROM inventory_lot il | |||
| INNER JOIN inventory i ON i.itemId = il.itemId AND i.deleted = 0 AND i.onHandQty > 0 | |||
| INNER JOIN inventory_lot_line ill ON ill.inventoryLotId = il.id AND ill.deleted = 0 | |||
| INNER JOIN warehouse w ON w.id = ill.warehouseId AND w.deleted = 0 AND w.store_id IN ('2F', '4F') | |||
| WHERE il.itemId IN (${itemIds.joinToString(",")}) AND il.deleted = 0 | |||
| GROUP BY w.store_id | |||
| """.trimIndent() | |||
| val inventoryResults = jdbcDao.queryForList(inventoryQuery) | |||
| val floorInventoryCount = mutableMapOf<String, Int>() | |||
| inventoryResults.forEach { row: Map<String, Any> -> | |||
| val floor = row["floor"] as? String ?: "Other" | |||
| val count = (row["inventory_count"] as? Number)?.toInt() ?: 0 | |||
| floorInventoryCount[floor] = count | |||
| val floorItemCount = mutableMapOf<String, Int>() | |||
| inventoryResults.forEach { row -> | |||
| floorItemCount[row["floor"] as? String ?: "Other"] = (row["item_count"] as? Number)?.toInt() ?: 0 | |||
| } | |||
| println("🔍 DEBUG: Floor inventory distribution: $floorInventoryCount") | |||
| // 决定使用哪个楼层 | |||
| val preferredFloor = when { | |||
| floorInventoryCount["2F"] ?: 0 > floorInventoryCount["4F"] ?: 0 -> "2F" | |||
| floorInventoryCount["4F"] ?: 0 > floorInventoryCount["2F"] ?: 0 -> "4F" | |||
| else -> "2F" // 默认使用 2F | |||
| println("🔍 DEBUG: Floor item count distribution: $floorItemCount") | |||
| // ✅ 新逻辑:只有所有 items 都在 4F,才算 4F,否则算 2F | |||
| val preferredFloor = if ((floorItemCount["4F"] ?: 0) == itemIds.size && (floorItemCount["2F"] ?: 0) == 0) { | |||
| "4F" // 所有 items 都在 4F | |||
| } else { | |||
| "2F" // 只要有任何 item 不在 4F,就算 2F | |||
| } | |||
| println("🔍 DEBUG: Preferred floor based on inventory: $preferredFloor") | |||
| println("🔍 DEBUG: Preferred floor: $preferredFloor") | |||
| val preferredStoreId = when (preferredFloor) { | |||
| "2F" -> 2 | |||
| "4F" -> 4 | |||
| else -> 2 | |||
| // ✅ 查找匹配 preferred floor 的 truck | |||
| val truck = deliveryOrder.shop?.id?.let { shopId -> | |||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||
| val preferredStoreId = when (preferredFloor) { | |||
| "2F" -> 2 | |||
| "4F" -> 4 | |||
| else -> 2 | |||
| } | |||
| // 只选择 store_id 匹配的 truck | |||
| val matchedTrucks = trucks.filter { it.storeId == preferredStoreId } | |||
| if (matchedTrucks.isEmpty()) { | |||
| null // 没有匹配的 truck | |||
| } else { | |||
| matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| } | |||
| } | |||
| val selectedTruck = if (trucks.size > 1) { | |||
| // Multiple trucks: prefer matching preferredStoreId, then earliest departure | |||
| trucks.find { it.storeId == preferredStoreId } | |||
| ?: trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| } else { | |||
| trucks.firstOrNull() | |||
| // ✅ 如果没有匹配的 truck,抛出异常跳过 | |||
| if (truck == null) { | |||
| val errorMsg = "No matching truck found for preferredFloor ($preferredFloor). Skipping DO ${deliveryOrder.id}." | |||
| println("⚠️ $errorMsg") | |||
| throw IllegalStateException(errorMsg) | |||
| } | |||
| println("🔍 DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}, DepartureTime=${selectedTruck?.departureTime}") | |||
| selectedTruck | |||
| } | |||
| println("✅ DEBUG: Truck matches preferred floor - Truck Store: ${truck.storeId}, Preferred: $preferredFloor") | |||
| // ✅ 根据 truck 的 Store_id 字段确定 storeId | |||
| val storeId = when (truck?.storeId) { | |||
| 4 -> "4/F" | |||
| 2 -> "2/F" | |||
| else -> "2/F" // 默认值 | |||
| // ✅ 根据 truck 的 Store_id 字段确定 storeId | |||
| val storeId = when (truck.storeId) { | |||
| 4 -> "4/F" | |||
| 2 -> "2/F" | |||
| else -> "2/F" // 默认值 | |||
| } | |||
| val loadingSequence = truck.loadingSequence ?: 999 | |||
| // ✅ 每个 pick order 只创建一条 DoPickOrder | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = truck.id, | |||
| doOrderId = deliveryOrder.id, | |||
| pickOrderId = createdPickOrder.id, | |||
| truckDepartureTime = truck.departureTime, | |||
| shopId = deliveryOrder.shop?.id, | |||
| handledBy = null, | |||
| pickOrderCode = createdPickOrder.code, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| loadingSequence = loadingSequence, | |||
| ticketReleaseTime = null, | |||
| truckLanceCode = truck.truckLanceCode, | |||
| shopCode = deliveryOrder.shop?.code, | |||
| shopName = deliveryOrder.shop?.name, | |||
| requiredDeliveryDate = targetDate | |||
| ) | |||
| println("🔍 DEBUG: Creating DoPickOrder - Store: $storeId, Truck: ${truck.id}") | |||
| val savedDoPickOrder = doPickOrderService.save(doPickOrder) | |||
| println("🔍 DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}") | |||
| return MessageResponse( | |||
| id = deliveryOrder.id, | |||
| code = deliveryOrder.code, | |||
| name = deliveryOrder.shop?.name, | |||
| type = null, | |||
| message = null, | |||
| errorPosition = null, | |||
| entity = mapOf("status" to deliveryOrder.status?.value) | |||
| ) | |||
| } | |||
| val loadingSequence = truck?.loadingSequence ?: 999 | |||
| // ✅ 每个 pick order 只创建一条 DoPickOrder | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = truck?.id, | |||
| doOrderId = deliveryOrder.id, | |||
| pickOrderId = createdPickOrder.id, | |||
| truckDepartureTime = truck?.departureTime, | |||
| shopId = deliveryOrder.shop?.id, | |||
| handledBy = null, | |||
| pickOrderCode = createdPickOrder.code, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| loadingSequence = loadingSequence, | |||
| ticketReleaseTime = null, | |||
| // ✅ 填充新增字段 | |||
| truckLanceCode = truck?.truckLanceCode, | |||
| shopCode = deliveryOrder.shop?.code, | |||
| shopName = deliveryOrder.shop?.name, | |||
| requiredDeliveryDate = targetDate | |||
| ) | |||
| println("🔍 DEBUG: Creating DoPickOrder - Store: $storeId, Ticket: , Truck: ${truck?.id}") | |||
| val savedDoPickOrder = doPickOrderService.save(doPickOrder) | |||
| println("🔍 DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}") | |||
| return MessageResponse( | |||
| id = deliveryOrder.id, | |||
| code = deliveryOrder.code, | |||
| name = deliveryOrder.shop?.name, | |||
| type = null, | |||
| message = null, | |||
| errorPosition = null, | |||
| entity = mapOf("status" to deliveryOrder.status?.value) | |||
| ) | |||
| // ... existing code ... | |||
| } | |||
| open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String { | |||
| try { | |||
| @@ -214,13 +214,20 @@ class DoPickOrderService( | |||
| println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate") | |||
| val grouped = allRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||
| .mapValues { (_, list) -> | |||
| LaneBtn( | |||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||
| unassigned = list.count { it.handledBy == null }, // 未分配的订单数 | |||
| total = list.size // 总订单数(包括已分配和未分配) | |||
| ) | |||
| } | |||
| .mapValues { (_, list) -> | |||
| // Group by shop_id within this lane | |||
| val shopGroups = list.groupBy { it.shopId } | |||
| LaneBtn( | |||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||
| // Count unique shops where ALL records are unassigned | |||
| unassigned = shopGroups.count { (_, shopRecords) -> | |||
| shopRecords.all { it.handledBy == null } | |||
| }, | |||
| // Count total unique shops in this lane | |||
| total = shopGroups.size | |||
| ) | |||
| } | |||
| val timeGroups = grouped.entries | |||
| .groupBy { it.key.first } | |||
| @@ -243,68 +250,87 @@ class DoPickOrderService( | |||
| } | |||
| // ✅ 修复:把 assignByLane 移到类里面 | |||
| fun assignByLane(request: AssignByLaneRequest): MessageResponse { | |||
| val existingOrders = doPickOrderRepository.findByHandledByAndTicketStatusIn( | |||
| request.userId, | |||
| listOf(DoPickOrderStatus.released, DoPickOrderStatus.pending) | |||
| fun assignByLane(request: AssignByLaneRequest): MessageResponse { | |||
| // Check if user already has active orders | |||
| val existingOrders = doPickOrderRepository.findByHandledByAndTicketStatusIn( | |||
| request.userId, | |||
| listOf(DoPickOrderStatus.released, DoPickOrderStatus.pending) | |||
| ) | |||
| if (existingOrders.isNotEmpty()) { | |||
| return MessageResponse( | |||
| id = null, code = "USER_BUSY", name = null, type = null, | |||
| message = "User already has an active pick order. Please complete it first.", | |||
| errorPosition = null, entity = null | |||
| ) | |||
| } | |||
| if (existingOrders.isNotEmpty()) { | |||
| return MessageResponse( | |||
| id = null, code = "USER_BUSY", name = null, type = null, | |||
| message = "User already has an active pick order. Please complete it first.", | |||
| errorPosition = null, entity = null | |||
| ) | |||
| // Find all orders for this lane | |||
| val allLaneOrders = doPickOrderRepository | |||
| .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | |||
| request.storeId, | |||
| DoPickOrderStatus.pending | |||
| ) | |||
| .filter { | |||
| it.handledBy == null && | |||
| it.truckLanceCode == request.truckLanceCode && | |||
| (request.truckDepartureTime == null || | |||
| it.truckDepartureTime?.toString() == request.truckDepartureTime) | |||
| } | |||
| val candidates = doPickOrderRepository | |||
| .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | |||
| request.storeId, | |||
| DoPickOrderStatus.pending | |||
| ) | |||
| .filter { | |||
| it.handledBy == null && | |||
| it.truckLanceCode == request.truckLanceCode && | |||
| (request.truckDepartureTime == null || | |||
| it.truckDepartureTime?.toString() == request.truckDepartureTime) | |||
| } | |||
| if (allLaneOrders.isEmpty()) { | |||
| return MessageResponse( | |||
| id = null, code = "NO_ORDERS", name = null, type = null, | |||
| message = "No available orders for lane ${request.truckLanceCode}", | |||
| errorPosition = null, entity = null | |||
| ) | |||
| } | |||
| if (candidates.isEmpty()) { | |||
| return MessageResponse( | |||
| id = null, code = "NO_ORDERS", name = null, type = null, | |||
| message = "No available orders for lane ${request.truckLanceCode}", | |||
| errorPosition = null, entity = null | |||
| ) | |||
| } | |||
| // ✅ Group by shop and pick the first shop (with earliest loading sequence) | |||
| val shopGroups = allLaneOrders.groupBy { it.shopId } | |||
| val firstShopOrders = shopGroups.values | |||
| .sortedBy { shopOrders -> shopOrders.minOf { it.loadingSequence ?: Int.MAX_VALUE } } | |||
| .firstOrNull() | |||
| val firstOrder = candidates.first() | |||
| val user = userRepository.findById(request.userId).orElse(null) | |||
| val handlerName = user?.name ?: "Unknown" | |||
| if (firstShopOrders == null || firstShopOrders.isEmpty()) { | |||
| return MessageResponse( | |||
| id = null, code = "NO_ORDERS", name = null, type = null, | |||
| message = "No available orders for lane ${request.truckLanceCode}", | |||
| errorPosition = null, entity = null | |||
| ) | |||
| } | |||
| // ✅ 更新 do_pick_order - 保持原有的卡车信息 | |||
| firstOrder.handledBy = request.userId | |||
| firstOrder.handlerName = handlerName | |||
| firstOrder.ticketStatus = DoPickOrderStatus.released | |||
| firstOrder.ticketReleaseTime = LocalDateTime.now() | |||
| // ✅ 重要:不要修改 truckDepartureTime 和 truckLanceCode | |||
| // 这些信息应该保持用户选择的值 | |||
| doPickOrderRepository.save(firstOrder) | |||
| // ✅ 同步更新 pick_order 表 | |||
| if (firstOrder.pickOrderId != null) { | |||
| val pickOrder = pickOrderRepository.findById(firstOrder.pickOrderId!!).orElse(null) | |||
| if (pickOrder != null) { | |||
| val user = userRepository.findById(request.userId).orElse(null) | |||
| pickOrder.assignTo = user | |||
| pickOrder.status = PickOrderStatus.RELEASED | |||
| pickOrderRepository.save(pickOrder) | |||
| } | |||
| val user = userRepository.findById(request.userId).orElse(null) | |||
| val handlerName = user?.name ?: "Unknown" | |||
| // ✅ Get shop info from first order | |||
| val firstOrder = firstShopOrders.first() | |||
| val shopName = firstOrder.shopName ?: "" | |||
| val shopCode = firstOrder.shopCode ?: "" | |||
| // ✅ Assign ALL records for this shop to the user | |||
| firstShopOrders.forEach { order -> | |||
| order.handledBy = request.userId | |||
| order.handlerName = handlerName | |||
| order.ticketStatus = DoPickOrderStatus.released | |||
| order.ticketReleaseTime = LocalDateTime.now() | |||
| } | |||
| doPickOrderRepository.saveAll(firstShopOrders) | |||
| // ✅ Update ALL pick_order tables for this shop | |||
| val pickOrderIds = firstShopOrders.mapNotNull { it.pickOrderId }.distinct() | |||
| pickOrderIds.forEach { pickOrderId -> | |||
| val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) | |||
| if (pickOrder != null) { | |||
| pickOrder.assignTo = user | |||
| pickOrder.status = PickOrderStatus.RELEASED | |||
| pickOrderRepository.save(pickOrder) | |||
| } | |||
| } | |||
| // 同步更新 record | |||
| val records = doPickOrderRecordRepository.findByPickOrderId(firstOrder.pickOrderId!!) | |||
| // ✅ Update ALL do_pick_order_record entries for this shop | |||
| pickOrderIds.forEach { pickOrderId -> | |||
| val records = doPickOrderRecordRepository.findByPickOrderId(pickOrderId) | |||
| records.forEach { | |||
| it.handledBy = request.userId | |||
| it.handlerName = handlerName | |||
| @@ -312,20 +338,24 @@ class DoPickOrderService( | |||
| it.ticketReleaseTime = LocalDateTime.now() | |||
| } | |||
| doPickOrderRecordRepository.saveAll(records) | |||
| } | |||
| return MessageResponse( | |||
| id = firstOrder.pickOrderId, | |||
| code = "SUCCESS", | |||
| name = null, | |||
| type = null, | |||
| message = "Assigned pick order from lane ${request.truckLanceCode}", | |||
| errorPosition = null, | |||
| entity = mapOf( | |||
| "pickOrderId" to firstOrder.pickOrderId, | |||
| "ticketNo" to firstOrder.ticketNo | |||
| ) | |||
| return MessageResponse( | |||
| id = pickOrderIds.firstOrNull(), | |||
| code = "SUCCESS", | |||
| name = null, | |||
| type = null, | |||
| message = "Assigned ${firstShopOrders.size} order(s) for $shopName from lane ${request.truckLanceCode}", | |||
| errorPosition = null, | |||
| entity = mapOf( | |||
| "pickOrderIds" to pickOrderIds, | |||
| "ticketNos" to firstShopOrders.map { it.ticketNo }, | |||
| "shopCode" to shopCode, | |||
| "shopName" to shopName, | |||
| "orderCount" to firstShopOrders.size | |||
| ) | |||
| } | |||
| ) | |||
| } | |||
| // 在 DoPickOrderService 类中添加这个方法 | |||
| open fun determineStoreId(doOrderId: Long): String { | |||
| @@ -1,6 +1,23 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.web.models | |||
| import java.time.LocalDate; | |||
| import java.time.LocalDateTime; | |||
| import java.time.LocalTime; | |||
| data class ReleaseDoRequest( | |||
| val id: Long, | |||
| val userId: Long | |||
| ) | |||
| data class ReleaseDoResult( | |||
| val deliveryOrderId: Long, | |||
| val deliveryOrderCode: String?, | |||
| val pickOrderId: Long, | |||
| val pickOrderCode: String?, | |||
| val shopId: Long?, | |||
| val shopCode: String?, | |||
| val shopName: String?, | |||
| val estimatedArrivalDate: LocalDate, | |||
| val preferredFloor: String, | |||
| val truckId: Long?, | |||
| val truckDepartureTime: LocalTime?, | |||
| val truckLanceCode: String?, | |||
| val loadingSequence: Int? | |||
| ) | |||
| @@ -1325,7 +1325,7 @@ open fun getCompletedJobOrderPickOrderLotDetails(pickOrderId: Long): List<Map<St | |||
| w.id as routerId, | |||
| w.order as routerIndex, | |||
| w.code as routerRoute, | |||
| w.area as routerArea, | |||
| w.code as routerArea, | |||
| -- Set quantities to NULL for rejected lots | |||
| CASE | |||
| @@ -3361,308 +3361,270 @@ ORDER BY | |||
| } | |||
| // ... existing code ... | |||
| // Fix the method signature and return types | |||
| open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | |||
| println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical ===") | |||
| println("today: ${LocalDate.now()}") | |||
| println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (CURRENT STRUCTURE) ===") | |||
| println("userId filter: $userId") | |||
| // Get all pick order IDs assigned to the user (both RELEASED and PENDING with doId) | |||
| val user = userService.find(userId).orElse(null) | |||
| if (user == null) { | |||
| println("❌ User not found: $userId") | |||
| return emptyMap() | |||
| } | |||
| val statusList = listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED, | |||
| //PickOrderStatus.COMPLETED | |||
| ) | |||
| // Get all pick orders assigned to user with PENDING or RELEASED status that have doId | |||
| val allAssignedPickOrders = pickOrderRepository.findAllByAssignToAndStatusIn( | |||
| user, | |||
| statusList | |||
| ).filter { it.deliveryOrder != null } // Only pick orders with doId | |||
| println("🔍 DEBUG: Found ${allAssignedPickOrders.size} pick orders assigned to user $userId") | |||
| // ✅ NEW LOGIC: Filter based on assignment and status | |||
| val filteredPickOrders = if (allAssignedPickOrders.isNotEmpty()) { | |||
| // Check if there are any RELEASED orders assigned to this user (active work) | |||
| val assignedReleasedOrders = allAssignedPickOrders.filter { | |||
| it.status == PickOrderStatus.RELEASED && it.assignTo?.id == userId | |||
| } | |||
| if (assignedReleasedOrders.isNotEmpty()) { | |||
| // ✅ If there are assigned RELEASED orders, show only those | |||
| println("🔍 DEBUG: Found ${assignedReleasedOrders.size} assigned RELEASED orders, showing only those") | |||
| assignedReleasedOrders | |||
| } else { | |||
| // ✅ If no assigned RELEASED orders, show only the latest COMPLETED order | |||
| val completedOrders = allAssignedPickOrders.filter { it.status == PickOrderStatus.COMPLETED } | |||
| if (completedOrders.isNotEmpty()) { | |||
| val latestCompleted = completedOrders.maxByOrNull { it.completeDate ?: it.modified ?: LocalDateTime.MIN } | |||
| println("🔍 DEBUG: No assigned RELEASED orders, showing latest completed order: ${latestCompleted?.code}") | |||
| listOfNotNull(latestCompleted) | |||
| } else { | |||
| println("🔍 DEBUG: No orders found") | |||
| emptyList() | |||
| } | |||
| } | |||
| } else { | |||
| emptyList() | |||
| } | |||
| val pickOrderIds = filteredPickOrders.map { it.id!! } | |||
| println("🎯 Pick order IDs to fetch: $pickOrderIds") | |||
| // ✅ Step 1: 获取 do_pick_order 基本信息 (直接通过 pickOrderId 关联) | |||
| 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.pick_order_id, | |||
| dpo.shop_id | |||
| FROM fpsmsdb.do_pick_order dpo | |||
| INNER JOIN fpsmsdb.pick_order po ON po.id = dpo.pick_order_id | |||
| WHERE po.assignTo = :userId | |||
| AND po.type = 'do' | |||
| AND po.status IN ('assigned', 'released', 'picking') | |||
| AND po.deleted = false | |||
| AND dpo.deleted = false | |||
| LIMIT 1 | |||
| """.trimIndent() | |||
| if (pickOrderIds.isEmpty()) { | |||
| val doPickOrderInfo = jdbcDao.queryForMap(doPickOrderSql, mapOf("userId" to userId)).orElse(null) | |||
| if (doPickOrderInfo == null) { | |||
| println("❌ No do_pick_order found for user $userId") | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| "fgInfo" to null, | |||
| "pickOrders" to emptyList<Any>() | |||
| ) | |||
| } | |||
| // Use the same SQL query but transform the results into hierarchical structure | |||
| val pickOrderIdsStr = pickOrderIds.joinToString(",") | |||
| val doPickOrderId = (doPickOrderInfo["do_pick_order_id"] as? Number)?.toLong() | |||
| val shopId = (doPickOrderInfo["shop_id"] as? Number)?.toLong() | |||
| println("🔍 Found do_pick_order ID: $doPickOrderId, shop_id: $shopId") | |||
| val sql = """ | |||
| SELECT | |||
| -- Pick Order Information | |||
| 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, | |||
| -- Pick Order Line Information | |||
| pol.id as pickOrderLineId, | |||
| pol.qty as pickOrderLineRequiredQty, | |||
| pol.status as pickOrderLineStatus, | |||
| -- Item Information | |||
| i.id as itemId, | |||
| i.code as itemCode, | |||
| i.name as itemName, | |||
| uc.code as uomCode, | |||
| uc.udfudesc as uomDesc, | |||
| uc.udfShortDesc as uomShortDesc, | |||
| -- Lot Information | |||
| 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, | |||
| -- ✅ 修改:直接使用 warehouse 作为路由信息 | |||
| w.`order` as routerIndex, | |||
| w.code as routerRoute, | |||
| w.code as routerArea, | |||
| -- ✅ FIXED: Set quantities to NULL for rejected lots | |||
| CASE | |||
| WHEN sol.status = 'rejected' THEN NULL | |||
| ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) | |||
| END as availableQty, | |||
| -- Required quantity for this lot | |||
| COALESCE(spl.qty, 0) as requiredQty, | |||
| -- Actual picked quantity | |||
| COALESCE(sol.qty, 0) as actualPickQty, | |||
| -- Suggested pick lot information | |||
| spl.id as suggestedPickLotId, | |||
| ill.status as lotStatus, | |||
| -- Stock out line information | |||
| sol.id as stockOutLineId, | |||
| sol.status as stockOutLineStatus, | |||
| COALESCE(sol.qty, 0) as stockOutLineQty, | |||
| -- Additional detailed fields | |||
| COALESCE(ill.inQty, 0) as inQty, | |||
| COALESCE(ill.outQty, 0) as outQty, | |||
| COALESCE(ill.holdQty, 0) as holdQty, | |||
| COALESCE(spl.suggestedLotLineId, sol.inventoryLotLineId) as debugSuggestedLotLineId, | |||
| ill.inventoryLotId as debugInventoryLotId, | |||
| // ✅ Step 2: 获取该用户分配的所有 pick orders (通过 shop_id 关联) | |||
| val pickOrdersSql = """ | |||
| SELECT DISTINCT | |||
| dpo.pick_order_id, | |||
| po.code as pick_order_code, | |||
| dpo.do_order_id, | |||
| dpo.delivery_order_code, | |||
| po.consoCode, | |||
| po.status, | |||
| DATE_FORMAT(po.targetDate, '%Y-%m-%d') as targetDate, | |||
| dpo.ticket_no, | |||
| dpo.id as do_pick_order_id | |||
| FROM fpsmsdb.do_pick_order dpo | |||
| INNER JOIN fpsmsdb.pick_order po ON po.id = dpo.pick_order_id | |||
| WHERE po.assignTo = :userId | |||
| AND po.type = 'do' | |||
| AND po.status IN ('assigned', 'released', 'picking') | |||
| AND po.deleted = false | |||
| AND dpo.deleted = false | |||
| AND dpo.shop_id = :shopId | |||
| ORDER BY dpo.pick_order_id | |||
| """.trimIndent() | |||
| val pickOrdersInfo = jdbcDao.queryForList(pickOrdersSql, mapOf("userId" to userId, "shopId" to shopId)) | |||
| println("🔍 Found ${pickOrdersInfo.size} pick orders for user $userId in shop $shopId") | |||
| // ✅ Step 3: 为每个 pick order 获取 lines 和 lots | |||
| val pickOrders = pickOrdersInfo.map { poInfo -> | |||
| val currentPickOrderId = (poInfo["pick_order_id"] as? Number)?.toLong() | |||
| -- Calculate total picked quantity by ALL pick orders for this lot | |||
| COALESCE(( | |||
| SELECT SUM(sol_all.qty) | |||
| FROM fpsmsdb.stock_out_line sol_all | |||
| WHERE sol_all.inventoryLotLineId = ill.id | |||
| AND sol_all.deleted = false | |||
| AND sol_all.status IN ('pending', 'checked', 'partially_completed', 'completed') | |||
| ), 0) as totalPickedByAllPickOrders, | |||
| // ✅ 查询该 pick order 的所有 lines 和 lots | |||
| val linesSql = """ | |||
| 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, | |||
| 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, | |||
| 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 来包含没有 lot 的 pick order lines | |||
| 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 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 = :pickOrderId | |||
| AND po.deleted = false | |||
| ORDER BY | |||
| COALESCE(w.`order`, 999999) ASC, | |||
| pol.id ASC, | |||
| il.lotNo ASC | |||
| """.trimIndent() | |||
| -- Calculate remaining available quantity correctly | |||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as remainingAfterAllPickOrders, | |||
| val linesResults = jdbcDao.queryForList(linesSql, mapOf("pickOrderId" to currentPickOrderId)) | |||
| println("🔍 Pick order $currentPickOrderId has ${linesResults.size} line-lot records") | |||
| -- Lot availability status | |||
| 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, | |||
| // ✅ 按 pickOrderLineId 分组 | |||
| val lineGroups = linesResults.groupBy { (it["pickOrderLineId"] as? Number)?.toLong() } | |||
| -- Processing status | |||
| CASE | |||
| WHEN sol.status = 'completed' THEN 'completed' | |||
| WHEN sol.status = 'rejected' THEN 'rejected' | |||
| WHEN sol.status = 'created' THEN 'pending' | |||
| ELSE 'pending' | |||
| END as processingStatus | |||
| val pickOrderLines = lineGroups.map { (lineId, lineRows) -> | |||
| val firstLineRow = lineRows.firstOrNull() | |||
| if (firstLineRow == null) { | |||
| null | |||
| } else { | |||
| // ✅ 构建 lots 列表(如果没有 lot,返回空数组) | |||
| 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"], | |||
| "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() // ✅ 返回空数组而不是 null | |||
| } | |||
| mapOf( | |||
| "id" to lineId, | |||
| "requiredQty" to firstLineRow["pickOrderLineRequiredQty"], | |||
| "status" to firstLineRow["pickOrderLineStatus"], | |||
| "item" to mapOf( | |||
| "id" to firstLineRow["itemId"], | |||
| "code" to firstLineRow["itemCode"], | |||
| "name" to firstLineRow["itemName"], | |||
| "uomCode" to firstLineRow["uomCode"], | |||
| "uomDesc" to firstLineRow["uomDesc"], | |||
| "uomShortDesc" to firstLineRow["uomShortDesc"], | |||
| ), | |||
| "lots" to lots // ✅ 即使是空数组也返回 | |||
| ) | |||
| } | |||
| }.filterNotNull() | |||
| FROM fpsmsdb.pick_order po | |||
| JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id | |||
| JOIN fpsmsdb.items i ON i.id = pol.itemId | |||
| LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId | |||
| -- Base lot links: all lot lines referenced by either suggestions or stock out lines for this POL | |||
| LEFT JOIN ( | |||
| SELECT spl.pickOrderLineId AS 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 | |||
| -- Re-bind spl/sol strictly on the same lot line | |||
| 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 AND sol.deleted = false | |||
| LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = ll.lotLineId | |||
| -- ✅ 删除 router 相关的 JOIN | |||
| -- LEFT JOIN router r ON r.inventoryLotId = ill.inventoryLotId AND r.deleted = false | |||
| -- LEFT JOIN router_order ro ON r.router_id = ro.id | |||
| LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||
| LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId -- ✅ 保留 warehouse JOIN | |||
| WHERE po.deleted = false | |||
| AND po.id IN ($pickOrderIdsStr) | |||
| AND pol.deleted = false | |||
| AND po.status IN ('PENDING', 'RELEASED', 'COMPLETED') | |||
| AND po.assignTo = :userId | |||
| AND ill.deleted = false | |||
| AND il.deleted = false | |||
| AND ll.lotLineId IS NOT NULL | |||
| AND (spl.pickOrderLineId IS NOT NULL OR sol.pickOrderLineId IS NOT NULL) | |||
| ORDER BY | |||
| CASE WHEN sol.status = 'rejected' THEN 0 ELSE 1 END, | |||
| COALESCE(w.`order`, 999999) ASC, -- ✅ 使用 warehouse.order 排序 | |||
| po.code ASC, | |||
| i.code ASC, | |||
| il.expiryDate ASC, | |||
| il.lotNo ASC | |||
| """.trimIndent() | |||
| println("�� Executing SQL for hierarchical structure: $sql") | |||
| println("�� With parameters: userId = $userId, pickOrderIds = $pickOrderIdsStr") | |||
| val results = jdbcDao.queryForList(sql, mapOf("userId" to userId)) | |||
| println("✅ Total result count: ${results.size}") | |||
| // Filter out lots with null availableQty (rejected lots) | |||
| // val filteredResults = results.filter { row -> | |||
| //val availableQty = row["availableQty"] | |||
| // availableQty != null | |||
| // } | |||
| val filteredResults = results | |||
| println("✅ Filtered result count: ${filteredResults.size}") | |||
| // ✅ Transform flat results into hierarchical structure | |||
| if (filteredResults.isEmpty()) { | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| mapOf( | |||
| "pickOrderId" to currentPickOrderId, | |||
| "pickOrderCode" to poInfo["pick_order_code"], | |||
| "doOrderId" to poInfo["do_order_id"], | |||
| "deliveryOrderCode" to poInfo["delivery_order_code"], | |||
| "consoCode" to poInfo["consoCode"], | |||
| "status" to poInfo["status"], | |||
| "targetDate" to poInfo["targetDate"], | |||
| "ticketNo" to poInfo["ticket_no"], | |||
| "doPickOrderId" to poInfo["do_pick_order_id"], | |||
| "pickOrderLines" to pickOrderLines | |||
| ) | |||
| } | |||
| // Get pick order info from first row (all rows have same pick order info) | |||
| val firstRow = filteredResults.first() | |||
| val pickOrderInfo = mapOf( | |||
| "id" to firstRow["pickOrderId"], | |||
| "code" to firstRow["pickOrderCode"], | |||
| "consoCode" to firstRow["pickOrderConsoCode"], | |||
| "targetDate" to firstRow["pickOrderTargetDate"], | |||
| "type" to firstRow["pickOrderType"], | |||
| "status" to firstRow["pickOrderStatus"], | |||
| "assignTo" to firstRow["pickOrderAssignTo"] | |||
| // ✅ 构建 FG 信息 | |||
| val fgInfo = mapOf( | |||
| "doPickOrderId" to doPickOrderId, | |||
| "ticketNo" to pickOrdersInfo.map { it["ticket_no"] }.joinToString(", "), | |||
| "storeId" to doPickOrderInfo["store_id"], | |||
| "shopCode" to doPickOrderInfo["ShopCode"], | |||
| "shopName" to doPickOrderInfo["ShopName"], | |||
| "truckLanceCode" to doPickOrderInfo["TruckLanceCode"], | |||
| "departureTime" to doPickOrderInfo["truck_departure_time"] | |||
| ) | |||
| // Group by pick order line ID to create hierarchical structure | |||
| val pickOrderLinesMap = filteredResults | |||
| .groupBy { it["pickOrderLineId"] as Number } | |||
| .map { (pickOrderLineId, lots) -> | |||
| val firstLot = lots.first() | |||
| // Item information (same for all lots of this line) | |||
| val itemInfo = mapOf( | |||
| "id" to firstLot["itemId"], | |||
| "code" to firstLot["itemCode"], | |||
| "name" to firstLot["itemName"], | |||
| "uomCode" to firstLot["uomCode"], | |||
| "uomDesc" to firstLot["uomDesc"] | |||
| ) | |||
| // Transform lots for this pick order line | |||
| val lotsInfo = lots.map { lot -> | |||
| mapOf( | |||
| "id" to lot["lotId"], | |||
| "lotNo" to lot["lotNo"], | |||
| "expiryDate" to lot["expiryDate"], | |||
| "location" to lot["location"], | |||
| "stockUnit" to lot["stockUnit"], | |||
| "availableQty" to lot["availableQty"], | |||
| "requiredQty" to lot["requiredQty"], | |||
| "actualPickQty" to lot["actualPickQty"], | |||
| "inQty" to lot["inQty"], | |||
| "outQty" to lot["outQty"], | |||
| "holdQty" to lot["holdQty"], | |||
| "lotStatus" to lot["lotStatus"], | |||
| "lotAvailability" to lot["lotAvailability"], | |||
| "processingStatus" to lot["processingStatus"], | |||
| "suggestedPickLotId" to lot["suggestedPickLotId"], | |||
| "stockOutLineId" to lot["stockOutLineId"], | |||
| "stockOutLineStatus" to lot["stockOutLineStatus"], | |||
| "stockOutLineQty" to lot["stockOutLineQty"], | |||
| "router" to mapOf( | |||
| "id" to lot["routerId"], | |||
| "index" to lot["routerIndex"], | |||
| "route" to lot["routerRoute"], | |||
| "area" to lot["routerArea"], | |||
| "itemCode" to firstLot["itemId"], | |||
| "itemName" to firstLot["itemName"], | |||
| "uomId" to firstLot["uomShortDesc"], | |||
| "noofCarton" to lot["requiredQty"] // Use required qty as carton count | |||
| ) | |||
| ) | |||
| } | |||
| // Pick order line with item and lots | |||
| mapOf( | |||
| "id" to pickOrderLineId, | |||
| "requiredQty" to firstLot["pickOrderLineRequiredQty"], | |||
| "status" to firstLot["pickOrderLineStatus"], | |||
| "item" to itemInfo, | |||
| "lots" to lotsInfo | |||
| ) | |||
| } | |||
| return mapOf( | |||
| "pickOrder" to pickOrderInfo as Any?, | |||
| "pickOrderLines" to pickOrderLinesMap as Any? | |||
| "fgInfo" to fgInfo, | |||
| "pickOrders" to pickOrders | |||
| ) | |||
| } | |||
| // Fix the type issues in the getPickOrdersByDateAndStore method | |||
| open fun getPickOrdersByDateAndStore(storeId: String): Map<String, Any?> { | |||
| println("=== Debug: getPickOrdersByDateAndStore ===") | |||