| @@ -494,12 +494,12 @@ open class DeliveryOrderService( | |||
| @Transactional(rollbackFor = [Exception::class]) | |||
| open fun releaseDeliveryOrder(request: ReleaseDoRequest): MessageResponse { | |||
| println("�� DEBUG: Starting releaseDeliveryOrder for DO ID: ${request.id}, User ID: ${request.userId}") | |||
| 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}") | |||
| println(" DEBUG: Found delivery order - ID: ${deliveryOrder.id}, Shop: ${deliveryOrder.shop?.code}, Status: ${deliveryOrder.status}") | |||
| deliveryOrder.apply { | |||
| status = DeliveryOrderStatus.PENDING | |||
| @@ -530,10 +530,10 @@ open class DeliveryOrderService( | |||
| 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: 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") | |||
| 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}") | |||
| } | |||
| @@ -605,125 +605,98 @@ open class DeliveryOrderService( | |||
| } | |||
| // 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")) | |||
| println(" DEBUG: Target date: $targetDate, Date prefix: $datePrefix") | |||
| // 新逻辑:根据 supplier code 决定楼层 | |||
| // 如果 supplier code 是 "P06B",使用 4F,否则使用 2F | |||
| val supplierCode = deliveryOrder.supplier?.code | |||
| val preferredFloor = if (supplierCode == "P06B") { | |||
| "4F" | |||
| } else { | |||
| "2F" | |||
| } | |||
| println(" DEBUG: Supplier code: $supplierCode, Preferred floor: $preferredFloor") | |||
| // 查找 truck | |||
| 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") | |||
| // 移除提前返回,总是分析 items 分布 | |||
| // 分析 DO order lines 中的 items 分布 | |||
| // 分析 items 来确定 storeId | |||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | |||
| val inventoryQuery = """ | |||
| 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 floorItemCount = mutableMapOf<String, Int>() | |||
| inventoryResults.forEach { row -> | |||
| floorItemCount[row["floor"] as? String ?: "Other"] = (row["item_count"] as? Number)?.toInt() ?: 0 | |||
| val preferredStoreId = when (preferredFloor) { | |||
| "2F" -> "2F" | |||
| "4F" -> "4F" | |||
| "3F" -> "3F" | |||
| else -> "2F" | |||
| } | |||
| println(" DEBUG: Floor item count distribution: $floorItemCount") | |||
| println(" DEBUG: Total items: ${itemIds.size}, Items on 4F: ${floorItemCount["4F"] ?: 0}") | |||
| // 新逻辑:只有所有 items 都在 4F,才算 4F,否则算 2F | |||
| val preferredFloor = if ((floorItemCount["4F"] ?: 0) == itemIds.size && (floorItemCount["2F"] ?: 0) == 0) { | |||
| "4F" // 所有 items 都在 4F | |||
| // 优先选择匹配的 truck | |||
| val selectedTruck = if (trucks.size > 1) { | |||
| trucks.find { it.storeId == preferredStoreId } | |||
| ?: trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| } else { | |||
| "2F" // 只要有任何 item 不在 4F,就算 2F | |||
| trucks.firstOrNull() | |||
| } | |||
| println(" DEBUG: Preferred floor: $preferredFloor (All items on 4F: ${preferredFloor == "4F"})") | |||
| println(" DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}") | |||
| selectedTruck | |||
| } | |||
| // 查找 truck | |||
| 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") | |||
| // 检查 truck 和 preferredFloor 是否匹配 | |||
| val truckStoreId = truck?.storeId | |||
| val expectedStoreId = when (preferredFloor) { | |||
| "2F" -> "2F" | |||
| "4F" -> "4F" | |||
| "3F" -> "3F" | |||
| else -> "2F" | |||
| } | |||
| if (truck == null || truckStoreId != expectedStoreId) { | |||
| val errorMsg = | |||
| "Items preferredFloor ($preferredFloor) does not match available truck (Truck Store: $truckStoreId). Skipping DO ${deliveryOrder.id}." | |||
| println("⚠️ $errorMsg") | |||
| throw IllegalStateException(errorMsg) | |||
| } | |||
| val preferredStoreId = when (preferredFloor) { | |||
| "2F" -> "2F" | |||
| "4F" -> "4F" | |||
| "3F" -> "3F" | |||
| else -> "2F" | |||
| } | |||
| // 优先选择匹配的 truck | |||
| val selectedTruck = if (trucks.size > 1) { | |||
| trucks.find { it.storeId == preferredStoreId } | |||
| ?: trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| } else { | |||
| trucks.firstOrNull() | |||
| } | |||
| println(" DEBUG: Truck matches preferred floor - Truck Store: $truckStoreId, Preferred: $preferredFloor") | |||
| println(" DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}") | |||
| selectedTruck | |||
| } | |||
| // storeId 基于 preferredFloor | |||
| val storeId = "$preferredFloor/F" | |||
| val loadingSequence = truck.loadingSequence ?: 999 | |||
| // 检查 truck 和 preferredFloor 是否匹配 | |||
| val truckStoreId = truck?.storeId | |||
| val expectedStoreId = when (preferredFloor) { | |||
| "2F" -> "2F" | |||
| "4F" -> "4F" | |||
| "3F" -> "3F" | |||
| else -> "2F" | |||
| } | |||
| println(" DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${truck.id}") | |||
| if (truck == null || truckStoreId != expectedStoreId) { | |||
| val errorMsg = | |||
| "Items preferredFloor ($preferredFloor) does not match available truck (Truck Store: $truckStoreId). Skipping DO ${deliveryOrder.id}." | |||
| println("⚠️ $errorMsg") | |||
| throw IllegalStateException(errorMsg) // 或返回错误响应 | |||
| } | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = truck.id, | |||
| pickOrderId = createdPickOrder.id, | |||
| doOrderId = deliveryOrder.id, | |||
| ticketReleaseTime = null, | |||
| shopId = deliveryOrder.shop?.id, | |||
| handlerName = null, | |||
| handledBy = null, | |||
| ticketCompleteDateTime = null, | |||
| truckDepartureTime = truck.departureTime, | |||
| truckLanceCode = truck.truckLanceCode, | |||
| shopCode = deliveryOrder.shop?.code, | |||
| shopName = deliveryOrder.shop?.name, | |||
| requiredDeliveryDate = targetDate, | |||
| createdBy = null, | |||
| modifiedBy = null, | |||
| pickOrderCode = createdPickOrder.code, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| loadingSequence = loadingSequence, | |||
| releaseType = null | |||
| ) | |||
| println(" DEBUG: Truck matches preferred floor - Truck Store: $truckStoreId, Preferred: $preferredFloor") | |||
| // storeId 基于 items 的 preferredFloor | |||
| val storeId = "$preferredFloor/F" | |||
| val loadingSequence = truck.loadingSequence ?: 999 | |||
| println(" DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${truck.id}") | |||
| 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 | |||
| ) | |||
| val savedDoPickOrder = doPickOrderService.save(doPickOrder) | |||
| println(" DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}") | |||
| val savedDoPickOrder = doPickOrderService.save(doPickOrder) | |||
| println(" DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}") | |||
| truck | |||
| } | |||
| return MessageResponse( | |||
| id = deliveryOrder.id, | |||
| code = deliveryOrder.code, | |||
| @@ -1273,63 +1246,20 @@ open class DeliveryOrderService( | |||
| } | |||
| } | |||
| // 分析楼层分布 | |||
| // 新逻辑:根据 supplier code 决定楼层 | |||
| // 如果 supplier code 是 "P06B",使用 4F,否则使用 2F | |||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | |||
| val itemsStoreIdQuery = """ | |||
| SELECT | |||
| i.store_id, | |||
| COUNT(DISTINCT i.id) as item_count | |||
| FROM items i | |||
| WHERE i.id IN (${itemIds.joinToString(",")}) | |||
| AND i.deleted = 0 | |||
| GROUP BY i.store_id | |||
| """.trimIndent() | |||
| val itemsStoreIdResults = jdbcDao.queryForList(itemsStoreIdQuery) | |||
| val storeIdItemCount = mutableMapOf<String, Int>() | |||
| var totalItemsWithStoreId = 0 // 统计有 store_id 的商品总数 | |||
| itemsStoreIdResults.forEach { row -> | |||
| val rawStoreId = row["store_id"] as? String | |||
| if (rawStoreId != null) { // 只统计非 NULL 的 store_id | |||
| val normalizedStoreId = when (rawStoreId) { | |||
| "3F" -> "4F" | |||
| else -> rawStoreId | |||
| } | |||
| val count = (row["item_count"] as? Number)?.toInt() ?: 0 | |||
| storeIdItemCount[normalizedStoreId] = | |||
| (storeIdItemCount[normalizedStoreId] ?: 0) + count | |||
| totalItemsWithStoreId += count | |||
| } | |||
| } | |||
| val count2F = storeIdItemCount["2F"] ?: 0 | |||
| val count4F = storeIdItemCount["4F"] ?: 0 | |||
| val preferredFloor = when { | |||
| totalItemsWithStoreId == 0 -> { | |||
| println("⚠️ WARNING: All items have NULL store_id, defaulting to 2F") | |||
| "2F" | |||
| } | |||
| count4F > count2F -> "4F" | |||
| count2F > count4F -> "2F" | |||
| count4F == totalItemsWithStoreId && count2F == 0 -> "4F" | |||
| else -> "2F" // 默认 2F | |||
| val supplierCode = deliveryOrder.supplier?.code | |||
| val preferredFloor = if (supplierCode == "P06B") { | |||
| "4F" | |||
| } else { | |||
| "2F" | |||
| } | |||
| println(" DEBUG: Floor calculation for DO ${deliveryOrder.id}") | |||
| println(" - Total items: ${itemIds.size}") | |||
| println(" - Items with store_id: $totalItemsWithStoreId") | |||
| println(" - Items without store_id: ${itemIds.size - totalItemsWithStoreId}") | |||
| println(" - 2F items: $count2F") | |||
| println(" - 4F items: $count4F") | |||
| println(" - Supplier code: $supplierCode") | |||
| println(" - Preferred floor: $preferredFloor") | |||
| val truck = deliveryOrder.shop?.id?.let { shopId -> | |||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||
| val preferredStoreId = when (preferredFloor) { | |||
| @@ -1339,19 +1269,16 @@ open class DeliveryOrderService( | |||
| 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 targetDate = estimatedArrivalDate.toLocalDate() | |||
| val dayOfWeek = targetDate.dayOfWeek | |||
| val dayAbbr = when (dayOfWeek) { | |||
| java.time.DayOfWeek.MONDAY -> "Mon" | |||
| java.time.DayOfWeek.TUESDAY -> "Tue" | |||
| @@ -1365,7 +1292,6 @@ open class DeliveryOrderService( | |||
| println(" DEBUG: DO ${deliveryOrder.id} - Target date: $targetDate ($dayAbbr), Shop: $shopId") | |||
| println(" DEBUG: Found ${matchedTrucks.size} matched 4F trucks") | |||
| val dayMatchedTrucks = matchedTrucks.filter { | |||
| it.truckLanceCode?.contains(dayAbbr, ignoreCase = true) == true | |||
| } | |||
| @@ -1376,21 +1302,17 @@ open class DeliveryOrderService( | |||
| } | |||
| if (dayMatchedTrucks.isNotEmpty()) { | |||
| val selected = dayMatchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||
| println("✅ DEBUG: Selected truck matching $dayAbbr - ID=${selected?.id}, Code=${selected?.truckLanceCode}") | |||
| selected | |||
| } else { | |||
| println("⚠️ WARNING: No truck matching $dayAbbr, using first available truck") | |||
| 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) } | |||
| } | |||
| } | |||
| @@ -1419,7 +1341,7 @@ open class DeliveryOrderService( | |||
| truckId = truck.id, | |||
| truckDepartureTime = truck.departureTime, | |||
| truckLanceCode = truck.truckLanceCode, | |||
| loadingSequence = truck.loadingSequence // 直接使用 truck 的值 | |||
| loadingSequence = truck.loadingSequence | |||
| ) | |||
| } | |||
| @@ -77,48 +77,20 @@ class DoReleaseCoordinatorService( | |||
| val updateSql = """ | |||
| UPDATE fpsmsdb.do_pick_order dpo | |||
| INNER JOIN ( | |||
| WITH DoStoreIdCounts AS ( | |||
| SELECT | |||
| dol.deliveryOrderId, | |||
| CASE | |||
| WHEN i.store_id = '3F' THEN '4F' | |||
| ELSE i.store_id | |||
| END AS store_id, | |||
| COUNT(DISTINCT dol.itemId) AS item_count | |||
| FROM fpsmsdb.delivery_order_line dol | |||
| INNER JOIN fpsmsdb.items i ON i.id = dol.itemId | |||
| AND i.deleted = 0 | |||
| WHERE dol.deleted = 0 | |||
| GROUP BY dol.deliveryOrderId, | |||
| CASE | |||
| WHEN i.store_id = '3F' THEN '4F' | |||
| ELSE i.store_id | |||
| END | |||
| ), | |||
| DoStoreIdSummary AS ( | |||
| WITH PreferredFloor AS ( | |||
| SELECT | |||
| do.id AS deliveryOrderId, | |||
| COALESCE(SUM(CASE WHEN dsc.store_id = '2F' THEN dsc.item_count ELSE 0 END), 0) AS count_2f, | |||
| COALESCE(SUM(CASE WHEN dsc.store_id = '4F' THEN dsc.item_count ELSE 0 END), 0) AS count_4f | |||
| FROM fpsmsdb.delivery_order do | |||
| LEFT JOIN DoStoreIdCounts dsc ON dsc.deliveryOrderId = do.id | |||
| WHERE do.deleted = 0 | |||
| GROUP BY do.id | |||
| ), | |||
| PreferredFloor AS ( | |||
| SELECT | |||
| deliveryOrderId, | |||
| CASE | |||
| WHEN count_2f > count_4f THEN '2F' | |||
| WHEN count_4f > count_2f THEN '4F' | |||
| WHEN s.code = 'P06B' THEN '4F' | |||
| ELSE '2F' | |||
| END AS preferred_floor, | |||
| CASE | |||
| WHEN count_2f > count_4f THEN 2 | |||
| WHEN count_4f > count_2f THEN 4 | |||
| WHEN s.code = 'P06B' THEN 4 | |||
| ELSE 2 | |||
| END AS preferred_store_id | |||
| FROM DoStoreIdSummary | |||
| FROM fpsmsdb.delivery_order do | |||
| LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0 | |||
| WHERE do.deleted = 0 | |||
| ), | |||
| TruckSelection AS ( | |||
| SELECT | |||
| @@ -261,68 +233,27 @@ class DoReleaseCoordinatorService( | |||
| e.printStackTrace() | |||
| } | |||
| } | |||
| private fun getOrderedDeliveryOrderIds(ids: List<Long>): List<Long> { | |||
| try { | |||
| println(" DEBUG: Getting ordered IDs for ${ids.size} orders") | |||
| println(" DEBUG: First 5 IDs: ${ids.take(5)}") | |||
| val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate") | |||
| val sql = """ | |||
| WITH DoFloorCounts AS ( | |||
| SELECT | |||
| dol.deliveryOrderId, | |||
| w.store_id, | |||
| COUNT(DISTINCT dol.itemId) AS item_count | |||
| FROM fpsmsdb.delivery_order_line dol | |||
| INNER JOIN fpsmsdb.inventory i ON i.itemId = dol.itemId | |||
| AND i.deleted = 0 | |||
| AND i.onHandQty > 0 | |||
| INNER JOIN fpsmsdb.inventory_lot il ON il.itemId = i.itemId | |||
| AND il.deleted = 0 | |||
| INNER JOIN fpsmsdb.inventory_lot_line ill ON ill.inventoryLotId = il.id | |||
| AND ill.deleted = 0 | |||
| INNER JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||
| AND w.deleted = 0 | |||
| AND w.store_id IN ('2F', '4F') | |||
| WHERE dol.deleted = 0 | |||
| AND dol.deliveryOrderId IN (${ids.joinToString(",")}) | |||
| AND dol.deliveryOrderId NOT IN ( | |||
| SELECT DISTINCT dpol.do_order_id | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| INNER JOIN fpsmsdb.do_pick_order dpo ON dpo.id = dpol.do_pick_order_id | |||
| WHERE dpo.release_type = 'single' | |||
| AND dpo.deleted = 0 | |||
| AND dpol.deleted = 0 | |||
| ) | |||
| GROUP BY dol.deliveryOrderId, w.store_id | |||
| ), | |||
| DoFloorSummary AS ( | |||
| WITH PreferredFloor AS ( | |||
| SELECT | |||
| do.id AS deliveryOrderId, | |||
| COALESCE(SUM(CASE WHEN dfc.store_id = '2F' THEN dfc.item_count ELSE 0 END), 0) AS count_2f, | |||
| COALESCE(SUM(CASE WHEN dfc.store_id = '4F' THEN dfc.item_count ELSE 0 END), 0) AS count_4f | |||
| FROM fpsmsdb.delivery_order do | |||
| LEFT JOIN DoFloorCounts dfc ON dfc.deliveryOrderId = do.id | |||
| WHERE do.id IN (${ids.joinToString(",")}) | |||
| AND do.deleted = 0 | |||
| GROUP BY do.id | |||
| ), | |||
| PreferredFloor AS ( | |||
| SELECT | |||
| deliveryOrderId, | |||
| count_2f, | |||
| count_4f, | |||
| CASE | |||
| WHEN count_2f > count_4f THEN '2F' | |||
| WHEN count_4f > count_2f THEN '4F' | |||
| WHEN s.code = 'P06B' THEN '4F' | |||
| ELSE '2F' | |||
| END AS preferred_floor, | |||
| CASE | |||
| WHEN count_2f > count_4f THEN 2 | |||
| WHEN count_4f > count_2f THEN 4 | |||
| WHEN s.code = 'P06B' THEN 4 | |||
| ELSE 2 | |||
| END AS preferred_store_id | |||
| FROM DoFloorSummary | |||
| FROM fpsmsdb.delivery_order do | |||
| LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0 | |||
| WHERE do.id IN (${ids.joinToString(",")}) | |||
| AND do.deleted = 0 | |||
| ), | |||
| TruckSelection AS ( | |||
| SELECT | |||
| @@ -624,37 +555,45 @@ class DoReleaseCoordinatorService( | |||
| ) | |||
| } | |||
| private fun createMergedDoPickOrder(results: List<ReleaseDoResult>) { | |||
| val first = results.first() | |||
| val storeId = when (first.preferredFloor) { | |||
| "2F" -> "2/F" | |||
| "4F" -> "4/F" | |||
| else -> "2/F" | |||
| } | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = first.truckId, | |||
| truckDepartureTime = first.truckDepartureTime, | |||
| shopId = first.shopId, | |||
| handledBy = null, | |||
| loadingSequence = first.loadingSequence ?: 999, | |||
| ticketReleaseTime = null, | |||
| truckLanceCode = first.truckLanceCode, | |||
| shopCode = first.shopCode, | |||
| shopName = first.shopName, | |||
| requiredDeliveryDate = first.estimatedArrivalDate, | |||
| releaseType = "batch" | |||
| // 替换第 627-653 行 | |||
| private fun createMergedDoPickOrder(results: List<ReleaseDoResult>) { | |||
| val first = results.first() | |||
| val storeId = when (first.preferredFloor) { | |||
| "2F" -> "2/F" | |||
| "4F" -> "4/F" | |||
| else -> "2/F" | |||
| } | |||
| ) | |||
| // 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save() | |||
| val saved = doPickOrderRepository.save(doPickOrder) | |||
| println(" DEBUG: Saved DoPickOrder - ID: ${saved.id}, Ticket: ${saved.ticketNo}") | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = first.truckId, | |||
| pickOrderId = null, | |||
| doOrderId = null, | |||
| ticketReleaseTime = null, | |||
| shopId = first.shopId, | |||
| handlerName = null, | |||
| handledBy = null, | |||
| ticketCompleteDateTime = null, | |||
| truckDepartureTime = first.truckDepartureTime, | |||
| truckLanceCode = first.truckLanceCode, | |||
| shopCode = first.shopCode, | |||
| shopName = first.shopName, | |||
| requiredDeliveryDate = first.estimatedArrivalDate, | |||
| createdBy = null, | |||
| modifiedBy = null, | |||
| pickOrderCode = null, | |||
| deliveryOrderCode = null, | |||
| loadingSequence = first.loadingSequence, | |||
| releaseType = "batch" | |||
| ) | |||
| // 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save() | |||
| val saved = doPickOrderRepository.save(doPickOrder) | |||
| println(" DEBUG: Saved DoPickOrder - ID: ${saved.id}, Ticket: ${saved.ticketNo}") | |||
| // 创建多条 DoPickOrderLine(每个 DO 一条) | |||
| results.forEach { result -> | |||
| @@ -813,25 +752,34 @@ class DoReleaseCoordinatorService( | |||
| doPickOrderLineRepository.save(line) | |||
| println(" DEBUG: Created DoPickOrderLine for existing DoPickOrder ${existingDoPickOrder.id}") | |||
| } | |||
| } else { | |||
| // 如果不存在,创建新的 do_pick_order | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = result.truckId, | |||
| truckDepartureTime = result.truckDepartureTime, | |||
| shopId = result.shopId, | |||
| handledBy = null, | |||
| loadingSequence = result.loadingSequence ?: 999, | |||
| ticketReleaseTime = null, | |||
| truckLanceCode = result.truckLanceCode, | |||
| shopCode = result.shopCode, | |||
| shopName = result.shopName, | |||
| requiredDeliveryDate = result.estimatedArrivalDate, | |||
| releaseType = "single" // 设置为 single | |||
| ) | |||
| val saved = doPickOrderRepository.save(doPickOrder) | |||
| // 替换第 748-764 行 | |||
| } else { | |||
| // 如果不存在,创建新的 do_pick_order | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = result.truckId, | |||
| pickOrderId = null, | |||
| doOrderId = null, | |||
| ticketReleaseTime = null, | |||
| shopId = result.shopId, | |||
| handlerName = null, | |||
| handledBy = null, | |||
| ticketCompleteDateTime = null, | |||
| truckDepartureTime = result.truckDepartureTime, | |||
| truckLanceCode = result.truckLanceCode, | |||
| shopCode = result.shopCode, | |||
| shopName = result.shopName, | |||
| requiredDeliveryDate = result.estimatedArrivalDate, | |||
| createdBy = null, | |||
| modifiedBy = null, | |||
| pickOrderCode = null, | |||
| deliveryOrderCode = null, | |||
| loadingSequence = result.loadingSequence, | |||
| releaseType = "single" | |||
| ) | |||
| val saved = doPickOrderRepository.save(doPickOrder) | |||
| // 创建 do_pick_order_line | |||
| val line = DoPickOrderLine().apply { | |||
| @@ -99,8 +99,8 @@ open class SuggestedPickLotService( | |||
| val suggestedList: MutableList<SuggestedPickLot> = mutableListOf() | |||
| val holdQtyMap: MutableMap<Long?, BigDecimal?> = request.holdQtyMap | |||
| // get current inventory lot line qty & grouped by item id | |||
| val excludedWarehouseCodes = listOf("2F-W202-01-00") | |||
| val availableInventoryLotLines = inventoryLotLineService | |||
| .allInventoryLotLinesByItemIdIn(itemIds) | |||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | |||
| @@ -111,6 +111,7 @@ open class SuggestedPickLotService( | |||
| // loop for suggest pick lot line | |||
| pols.forEach { line -> | |||
| val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | |||
| val lotLines = availableInventoryLotLines[line.item?.id].orEmpty() | |||
| val ratio = one // (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) | |||
| @@ -256,126 +257,97 @@ open class SuggestedPickLotService( | |||
| // loop for suggest pick lot line | |||
| pols.forEach { line -> | |||
| val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | |||
| val ratio = one | |||
| // === 新增:如果是 DO pick order,则按 DO 的 preferredFloor 过滤 lot === | |||
| // preferredFloor 规则:supplier.code == "P06B" -> 4F,否则 2F | |||
| val pickOrder = line.pickOrder | |||
| val isDoPickOrder = pickOrder?.type?.value == "do" || pickOrder?.type?.value == "delivery_order" | |||
| val doPreferredFloor: String? = if (isDoPickOrder) { | |||
| val supplierCode = pickOrder?.deliveryOrder?.supplier?.code | |||
| if (supplierCode == "P06B") "4F" else "2F" | |||
| } else { | |||
| null | |||
| } | |||
| val lotLines = availableInventoryLotLines[line.item?.id].orEmpty() | |||
| val ratio = one // (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) | |||
| // FIX: Calculate remaining quantity needed (not the full required quantity) | |||
| // 计算 remaining qty | |||
| val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(line.id!!) | |||
| val totalPickedQty = stockOutLines | |||
| .filter { | |||
| it.status == "completed" || | |||
| it.status == "partially_completed" || | |||
| (it.status == "rejected" && (it.qty ?: zero) > zero) // 包含已 picked 的 rejected | |||
| } | |||
| .sumOf { it.qty ?: zero } | |||
| .filter { | |||
| it.status == "completed" || | |||
| it.status == "partially_completed" || | |||
| (it.status == "rejected" && (it.qty ?: zero) > zero) | |||
| } | |||
| .sumOf { it.qty ?: zero } | |||
| val requiredQty = line.qty ?: zero | |||
| val remainingQty = requiredQty.minus(totalPickedQty) | |||
| println("=== SUGGESTION DEBUG for Pick Order Line ${line.id} ===") | |||
| println("Required qty: $requiredQty") | |||
| println("Total picked qty: $totalPickedQty") | |||
| println("Remaining qty needed: $remainingQty") | |||
| println("Stock out lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}") | |||
| // FIX: Use remainingQty instead of line.qty | |||
| var remainingQtyToAllocate = remainingQty | |||
| println("remaining1 $remainingQtyToAllocate (sales units)") | |||
| val updatedLotLines = mutableListOf<InventoryLotLineInfo>() | |||
| lotLines.forEachIndexed { index, lotLine -> | |||
| lotLines.forEachIndexed { _, lotLine -> | |||
| if (remainingQtyToAllocate <= zero) return@forEachIndexed | |||
| println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}") | |||
| // 拿 entity 是因为 projection 的 warehouse 没有 store_id 字段 | |||
| val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } | |||
| // 修复:计算可用数量,转换为销售单位 | |||
| ?: return@forEachIndexed | |||
| val warehouseStoreId = inventoryLotLine.warehouse?.store_id | |||
| val warehouseCode = inventoryLotLine.warehouse?.code | |||
| // 规则 1:排除指定 warehouse code | |||
| if (warehouseCode != null && excludedWarehouseCodes.contains(warehouseCode)) { | |||
| return@forEachIndexed | |||
| } | |||
| // 规则 2:原有逻辑:跳过 3F | |||
| if (warehouseStoreId == "3F") { | |||
| return@forEachIndexed | |||
| } | |||
| // 规则 3:新增逻辑:DO 订单必须匹配楼层,否则不建议 | |||
| // 例:doPreferredFloor=2F,但 lot 在 4F => 跳过 | |||
| if (doPreferredFloor != null && warehouseStoreId != doPreferredFloor) { | |||
| return@forEachIndexed | |||
| } | |||
| val availableQtyInBaseUnits = calculateRemainingQtyForInfo(lotLine) | |||
| val holdQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero | |||
| val availableQtyInSalesUnits = availableQtyInBaseUnits | |||
| .minus(holdQtyInBaseUnits) | |||
| .divide(ratio, 2, RoundingMode.HALF_UP) | |||
| println("holdQtyMap[lotLine.id] ?: zero ${holdQtyMap[lotLine.id] ?: zero}") | |||
| if (availableQtyInSalesUnits <= zero) { | |||
| updatedLotLines += lotLine | |||
| return@forEachIndexed | |||
| } | |||
| println("$index : ${lotLine.id}") | |||
| // val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } | |||
| val originalHoldQty = inventoryLotLine?.holdQty | |||
| if (availableQtyInSalesUnits <= zero) return@forEachIndexed | |||
| // 修复:在销售单位中计算分配数量 | |||
| val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQtyToAllocate) | |||
| remainingQtyToAllocate = remainingQtyToAllocate.minus(assignQtyInSalesUnits) | |||
| val newHoldQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero | |||
| // 修复:将销售单位转换为基础单位来更新 holdQty | |||
| val assignQtyInBaseUnits = assignQtyInSalesUnits.multiply(ratio) | |||
| holdQtyMap[lotLine.id] = (holdQtyMap[lotLine.id] ?: zero).plus(assignQtyInBaseUnits) | |||
| suggestedList += SuggestedPickLot().apply { | |||
| type = SuggestedPickLotType.PICK_ORDER | |||
| suggestedLotLine = inventoryLotLine | |||
| pickOrderLine = line | |||
| qty = assignQtyInSalesUnits // 保存销售单位 | |||
| qty = assignQtyInSalesUnits | |||
| } | |||
| } | |||
| // 修复:计算现有 suggestions 中 pending/checked 状态满足的数量 | |||
| var existingSatisfiedQty = BigDecimal.ZERO | |||
| // 查询现有的 suggestions 用于这个 pick order line | |||
| val existingSuggestions = suggestedPickLotRepository.findAllByPickOrderLineId(line.id!!) | |||
| existingSuggestions.forEach { existingSugg -> | |||
| if (existingSugg.suggestedLotLine?.id != null) { | |||
| val stockOutLines = stockOutLIneRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||
| line.id!!, existingSugg.suggestedLotLine?.id!! | |||
| ) | |||
| val canCountAsSatisfied = stockOutLines.isEmpty() || stockOutLines.any { | |||
| it.status == "pending" || it.status == "checked" || it.status == "partially_completed" | |||
| } | |||
| if (canCountAsSatisfied) { | |||
| existingSatisfiedQty = existingSatisfiedQty.plus(existingSugg.qty ?: BigDecimal.ZERO) | |||
| } | |||
| } | |||
| } | |||
| // 调整 remainingQtyToAllocate,减去已经通过现有 suggestions 满足的数量 | |||
| remainingQtyToAllocate = remainingQtyToAllocate.minus(existingSatisfiedQty) | |||
| println("Existing satisfied qty: $existingSatisfiedQty") | |||
| println("Adjusted remaining qty: $remainingQtyToAllocate") | |||
| // if still have remainingQty | |||
| println("remaining2 $remainingQtyToAllocate (sales units)") | |||
| // if still have remainingQty | |||
| println("remaining2 $remainingQtyToAllocate (sales units)") | |||
| // 如果仍有剩余 -> 给 null lot(你举例的场景会走到这里) | |||
| if (remainingQtyToAllocate > zero) { | |||
| suggestedList += SuggestedPickLot().apply { | |||
| type = SuggestedPickLotType.PICK_ORDER | |||
| suggestedLotLine = null | |||
| pickOrderLine = line | |||
| qty = remainingQtyToAllocate // 保存销售单位 | |||
| } | |||
| try { | |||
| /* | |||
| val pickOrder = line.pickOrder | |||
| if (pickOrder != null) { | |||
| createInsufficientStockIssue( | |||
| pickOrder = pickOrder, | |||
| pickOrderLine = line, | |||
| insufficientQty = remainingQtyToAllocate | |||
| ) | |||
| } | |||
| */ | |||
| } catch (e: Exception) { | |||
| println("❌ Error creating insufficient stock issue: ${e.message}") | |||
| e.printStackTrace() | |||
| qty = remainingQtyToAllocate | |||
| } | |||
| } | |||
| } | |||
| return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) | |||
| return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) | |||
| } | |||
| // Convertion | |||