| @@ -21,15 +21,13 @@ open class DoPickOrderLineRecord: BaseEntity<Long>() { | |||
| @Column(name = "do_pick_order_id", length = 100) | |||
| open var doPickOrderId: Long? = null | |||
| @JoinColumn(name = "pick_order_id") | |||
| @Column(name = "pick_order_id") // ✅ 正确:普通列 | |||
| open var pickOrderId: Long? = null | |||
| @JoinColumn(name = "do_order_id") | |||
| @Column(name = "do_order_id") // ✅ 正确:普通列 | |||
| open var doOrderId: Long? = null | |||
| @JoinColumn(name = "pick_order_code") | |||
| @Column(name = "pick_order_code") // ✅ 正确:普通列 | |||
| open var pickOrderCode: String? = null | |||
| @Column(name = "delivery_order_code") | |||
| @@ -1,6 +1,7 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.service | |||
| import com.ffii.core.utils.PdfUtils | |||
| import org.springframework.context.annotation.Lazy | |||
| import com.ffii.core.utils.ZebraPrinterUtil | |||
| import com.ffii.fpsms.m18.entity.M18DataLogRepository | |||
| import com.ffii.fpsms.m18.service.M18DataLogService | |||
| @@ -64,12 +65,15 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintDNLabelsRequest | |||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository | |||
| import com.ffii.fpsms.modules.stock.service.InventoryLotService | |||
| import net.sf.jasperreports.engine.JasperPrintManager | |||
| import net.sf.jasperreports.engine.JRPrintPage | |||
| import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository | |||
| import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService // ✅ 添加这行 | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.* | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssue // ✅ 添加 | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository // ✅ 添加 | |||
| import com.ffii.fpsms.modules.pickOrder.entity.IssueCategory // ✅ 添加 | |||
| import com.ffii.fpsms.modules.pickOrder.entity.HandleStatus | |||
| @Service | |||
| open class DeliveryOrderService( | |||
| private val deliveryOrderRepository: DeliveryOrderRepository, | |||
| @@ -80,7 +84,7 @@ open class DeliveryOrderService( | |||
| private val userService: UserService, | |||
| private val userRepository: UserRepository, | |||
| private val pickOrderService: PickOrderService, | |||
| private val doPickOrderService: DoPickOrderService, | |||
| @Lazy private val doPickOrderService: DoPickOrderService, | |||
| private val truckRepository: TruckRepository, | |||
| private val pickOrderRepository: PickOrderRepository, | |||
| private val suggestedPickLotService: SuggestedPickLotService, | |||
| @@ -95,6 +99,7 @@ open class DeliveryOrderService( | |||
| private val suggestedPickLotRepository: SuggestPickLotRepository, | |||
| private val inventoryLotRepository: InventoryLotRepository, | |||
| private val jdbcDao: JdbcDao, | |||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository, | |||
| ) { | |||
| open fun findByM18DataLogId(m18DataLogId: Long): DeliveryOrder? { | |||
| @@ -274,6 +279,7 @@ open class DeliveryOrderService( | |||
| open fun searchCodeAndShopName(code: String?, shopName: String?): List<DeliveryOrderInfo> { | |||
| return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndDeletedIsFalse(code, shopName); | |||
| } | |||
| open fun getWarehouseOrderByItemId(itemId: Long): Int? { | |||
| val inventoryLots = inventoryLotService.findByItemId(itemId) | |||
| if (inventoryLots.isNotEmpty()) { | |||
| @@ -297,7 +303,7 @@ open class DeliveryOrderService( | |||
| } | |||
| return null | |||
| } | |||
| // ✅ 新增方法2:获取 warehouse 的 code 字段(用于显示路由) | |||
| open fun getWarehouseCodeByItemId(itemId: Long): String? { | |||
| val inventoryLots = inventoryLotService.findByItemId(itemId) | |||
| @@ -402,12 +408,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}") | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | |||
| 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 | |||
| } | |||
| @@ -429,57 +435,51 @@ open class DeliveryOrderService( | |||
| 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}") | |||
| // ✅ 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") | |||
| val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null } | |||
| if (insufficientCount > 0) { | |||
| println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)") | |||
| } | |||
| // ✅ 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 = | |||
| (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" | |||
| @@ -488,17 +488,18 @@ open class DeliveryOrderService( | |||
| 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) | |||
| 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 | |||
| @@ -516,332 +517,537 @@ open class DeliveryOrderService( | |||
| } | |||
| // ✅ CREATE do_pick_order_record entries | |||
| // 第 471-555 行附近 - 修复创建逻辑 | |||
| // 第 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") | |||
| 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() | |||
| } | |||
| // ✅ 分析 DO order lines 中的 items 分布 | |||
| 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) | |||
| 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 | |||
| } | |||
| println("🔍 DEBUG: Floor inventory distribution: $floorInventoryCount") | |||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||
| val datePrefix = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) | |||
| // 决定使用哪个楼层 | |||
| val preferredFloor = when { | |||
| floorInventoryCount["2F"] ?: 0 > floorInventoryCount["4F"] ?: 0 -> "2F" | |||
| floorInventoryCount["4F"] ?: 0 > floorInventoryCount["2F"] ?: 0 -> "4F" | |||
| else -> "2F" // 默认使用 2F | |||
| } | |||
| println("🔍 DEBUG: Preferred floor based on inventory: $preferredFloor") | |||
| val preferredStoreId = when (preferredFloor) { | |||
| "2F" -> 2 | |||
| "4F" -> 4 | |||
| else -> 2 | |||
| } | |||
| 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() | |||
| } | |||
| println("🔍 DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}, DepartureTime=${selectedTruck?.departureTime}") | |||
| selectedTruck | |||
| } | |||
| 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") | |||
| // ✅ 移除提前返回,总是分析 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 | |||
| } | |||
| // ✅ 根据 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, 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 ... | |||
| 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 | |||
| } else { | |||
| "2F" // 只要有任何 item 不在 4F,就算 2F | |||
| } | |||
| println("🔍 DEBUG: Preferred floor: $preferredFloor (All items on 4F: ${preferredFloor == "4F"})") | |||
| // ✅ 查找 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") | |||
| val preferredStoreId = when (preferredFloor) { | |||
| "2F" -> 2 | |||
| "4F" -> 4 | |||
| else -> 2 | |||
| } | |||
| // 优先选择匹配的 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: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}") | |||
| selectedTruck | |||
| } | |||
| // ✅ 检查 truck 和 preferredFloor 是否匹配 | |||
| val truckStoreId = truck?.storeId | |||
| val expectedStoreId = when (preferredFloor) { | |||
| "2F" -> 2 | |||
| "4F" -> 4 | |||
| else -> 2 | |||
| } | |||
| 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) // 或返回错误响应 | |||
| } | |||
| 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}") | |||
| truck | |||
| } | |||
| 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) | |||
| ) | |||
| } | |||
| open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String { | |||
| try { | |||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId) | |||
| val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } | |||
| val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||
| val lotNumbers = suggestedPickLots | |||
| .filter { it.pickOrderLine?.item?.id == itemId } | |||
| .mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo } | |||
| .distinct() | |||
| return lotNumbers.joinToString(", ") | |||
| } catch (e: Exception) { | |||
| println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}") | |||
| return "" | |||
| open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String { | |||
| try { | |||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId) | |||
| val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } | |||
| val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||
| val lotNumbers = suggestedPickLots | |||
| .filter { it.pickOrderLine?.item?.id == itemId } | |||
| .mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo } | |||
| .distinct() | |||
| return lotNumbers.joinToString(", ") | |||
| } catch (e: Exception) { | |||
| println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}") | |||
| return "" | |||
| } | |||
| } | |||
| } | |||
| //Delivery Note | |||
| @Throws(IOException::class) | |||
| @Transactional | |||
| open fun exportDeliveryNote(request: ExportDeliveryNoteRequest): Map<String, Any> { | |||
| val DELIVERYNOTE_PDF = "DeliveryNote/DeliveryNotePDF.jrxml" | |||
| val resource = ClassPathResource(DELIVERYNOTE_PDF) | |||
| if(!resource.exists()){ | |||
| throw FileNotFoundException("Report file not fount: $DELIVERYNOTE_PDF") | |||
| } | |||
| val inputStream = resource.inputStream | |||
| val deliveryNote = JasperCompileManager.compileReport(inputStream) | |||
| val deliveryNoteInfo = deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||
| //Delivery Note | |||
| @Throws(IOException::class) | |||
| @Transactional | |||
| open fun exportDeliveryNote(request: ExportDeliveryNoteRequest): Map<String, Any> { | |||
| val DELIVERYNOTE_PDF = "DeliveryNote/DeliveryNotePDF.jrxml" | |||
| val resource = ClassPathResource(DELIVERYNOTE_PDF) | |||
| if (!resource.exists()) { | |||
| throw FileNotFoundException("Report file not fount: $DELIVERYNOTE_PDF") | |||
| } | |||
| val inputStream = resource.inputStream | |||
| val deliveryNote = JasperCompileManager.compileReport(inputStream) | |||
| val deliveryNoteInfo = | |||
| deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||
| val params = mutableMapOf<String, Any>() | |||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||
| val params = mutableMapOf<String, Any>() | |||
| val deliveryOrderEntity = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.deliveryOrderIds) | |||
| val selectedTruckNo = deliveryOrderEntity?.shop?.id?.let { shopId -> | |||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||
| trucks.firstOrNull()?.truckLanceCode | |||
| } ?: "" | |||
| val deliveryOrderEntity = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.deliveryOrderIds) | |||
| val selectedTruckNo = deliveryOrderEntity?.shop?.id?.let { shopId -> | |||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||
| trucks.firstOrNull()?.truckLanceCode | |||
| } ?: "" | |||
| val selectedPickOrder = pickOrderRepository.findById(request.pickOrderIds).orElse(null) | |||
| val selectedPickOrder = pickOrderRepository.findById(request.pickOrderIds).orElse(null) | |||
| for (info in deliveryNoteInfo) { | |||
| val sortedLines = info.deliveryOrderLines.sortedBy { line -> | |||
| line.itemId?.let { itemId -> | |||
| getWarehouseOrderByItemId(itemId) // ✅ 改用 warehouse order | |||
| } ?: Int.MAX_VALUE | |||
| } | |||
| for (info in deliveryNoteInfo) { | |||
| val sortedLines = info.deliveryOrderLines.sortedBy { line -> | |||
| line.itemId?.let { itemId -> | |||
| getWarehouseOrderByItemId(itemId) // ✅ 改用 warehouse order | |||
| } ?: Int.MAX_VALUE | |||
| } | |||
| sortedLines.forEachIndexed { index, line -> | |||
| sortedLines.forEachIndexed { index, line -> | |||
| val field = mutableMapOf<String, Any>() | |||
| val field = mutableMapOf<String, Any>() | |||
| field["sequenceNumber"] = (index + 1).toString() | |||
| field["itemNo"] = line.itemNo | |||
| field["itemName"] = line.itemName ?:"" | |||
| field["uom"] = line.uom ?:"" | |||
| field["qty"] = line.qty.toString() | |||
| field["shortName"] = line.uomShortDesc ?:"" | |||
| field["sequenceNumber"] = (index + 1).toString() | |||
| field["itemNo"] = line.itemNo | |||
| field["itemName"] = line.itemName ?: "" | |||
| field["uom"] = line.uom ?: "" | |||
| field["qty"] = line.qty.toString() | |||
| field["shortName"] = line.uomShortDesc ?: "" | |||
| val route = line.itemId?.let { itemId -> | |||
| getWarehouseCodeByItemId(itemId) // ✅ 使用新方法 | |||
| } ?: "" | |||
| field["route"] = route | |||
| val route = line.itemId?.let { itemId -> | |||
| getWarehouseCodeByItemId(itemId) // ✅ 使用新方法 | |||
| } ?: "" | |||
| field["route"] = route | |||
| val lotNo = line.itemId?.let { itemId -> | |||
| getLotNumbersForPickOrderByItemId(itemId, request.pickOrderIds) | |||
| } ?: "" | |||
| field["lotNo"] = lotNo | |||
| val lotNo = line.itemId?.let { itemId -> | |||
| getLotNumbersForPickOrderByItemId(itemId, request.pickOrderIds) | |||
| } ?: "" | |||
| field["lotNo"] = lotNo | |||
| fields.add(field) | |||
| fields.add(field) | |||
| } | |||
| } | |||
| } | |||
| if(request.isDraft){ | |||
| params["dnTitle"] = "送貨單(初稿)" | |||
| params["colQty"] = "所需數量" | |||
| params["totalCartonTitle"] = "" | |||
| } | |||
| else{ | |||
| params["dnTitle"] = "送貨單" | |||
| params["colQty"] = "數量" | |||
| params["totalCartonTitle"] = "總箱數:" | |||
| } | |||
| if (request.isDraft) { | |||
| params["dnTitle"] = "送貨單(初稿)" | |||
| params["colQty"] = "所需數量" | |||
| params["totalCartonTitle"] = "" | |||
| } else { | |||
| params["dnTitle"] = "送貨單" | |||
| params["colQty"] = "數量" | |||
| params["totalCartonTitle"] = "總箱數:" | |||
| } | |||
| params["numOfCarton"] = request.numOfCarton.toString() | |||
| if(params["numOfCarton"] == "0"){ | |||
| params["numOfCarton"] = "" | |||
| } | |||
| params["numOfCarton"] = request.numOfCarton.toString() | |||
| if (params["numOfCarton"] == "0") { | |||
| params["numOfCarton"] = "" | |||
| } | |||
| params["deliveryOrderCode"] = deliveryNoteInfo[0].code | |||
| params["shopName"] = deliveryNoteInfo[0].shopName ?: "" | |||
| params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: "" | |||
| params["deliveryDate"] = deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| params["truckNo"] = selectedTruckNo | |||
| params["ShopPurchaseOrderNo"] = deliveryNoteInfo[0].code | |||
| params["FGPickOrderNo"] = selectedPickOrder?.code ?: "" | |||
| params["deliveryOrderCode"] = deliveryNoteInfo[0].code | |||
| params["shopName"] = deliveryNoteInfo[0].shopName ?: "" | |||
| params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: "" | |||
| params["deliveryDate"] = | |||
| deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| params["truckNo"] = selectedTruckNo | |||
| params["ShopPurchaseOrderNo"] = deliveryNoteInfo[0].code | |||
| params["FGPickOrderNo"] = selectedPickOrder?.code ?: "" | |||
| return mapOf( | |||
| "report" to PdfUtils.fillReport(deliveryNote, fields, params), | |||
| "filename" to deliveryNoteInfo[0].code | |||
| ) | |||
| } | |||
| "report" to PdfUtils.fillReport(deliveryNote, fields, params), | |||
| "filename" to deliveryNoteInfo[0].code | |||
| ) | |||
| } | |||
| //Print Delivery Note | |||
| @Transactional | |||
| open fun printDeliveryNote(request: PrintDeliveryNoteRequest){ | |||
| //val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||
| val pdf = exportDeliveryNote( | |||
| ExportDeliveryNoteRequest( | |||
| deliveryOrderIds = request.deliveryOrderId, | |||
| numOfCarton = request.numOfCarton, | |||
| isDraft = request.isDraft, | |||
| pickOrderIds = request.pickOrderId | |||
| //Print Delivery Note | |||
| @Transactional | |||
| open fun printDeliveryNote(request: PrintDeliveryNoteRequest) { | |||
| //val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||
| val pdf = exportDeliveryNote( | |||
| ExportDeliveryNoteRequest( | |||
| deliveryOrderIds = request.deliveryOrderId, | |||
| numOfCarton = request.numOfCarton, | |||
| isDraft = request.isDraft, | |||
| pickOrderIds = request.pickOrderId | |||
| ) | |||
| ) | |||
| ) | |||
| val jasperPrint = pdf["report"] as JasperPrint | |||
| val jasperPrint = pdf["report"] as JasperPrint | |||
| val tempPdfFile = File.createTempFile("print_job_",".pdf") | |||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||
| try{ | |||
| JasperExportManager.exportReportToPdfFile(jasperPrint,tempPdfFile.absolutePath) | |||
| try { | |||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||
| //val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||
| //printer.ip?.let { ip -> printer.port?.let { port -> | |||
| // ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED) | |||
| //}} | |||
| } finally { | |||
| //tempPdfFile.delete() | |||
| } | |||
| //val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||
| //printer.ip?.let { ip -> printer.port?.let { port -> | |||
| // ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED) | |||
| //}} | |||
| } finally { | |||
| //tempPdfFile.delete() | |||
| } | |||
| } | |||
| //Carton Labels | |||
| open fun exportDNLabels(request: ExportDNLabelsRequest): Map<String, Any> { | |||
| val DNLABELS_PDF = "DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml" | |||
| val resource = ClassPathResource(DNLABELS_PDF) | |||
| if (!resource.exists()) { | |||
| throw FileNotFoundException("Label file not found: $DNLABELS_PDF") | |||
| } | |||
| val inputStream = resource.inputStream | |||
| val cartonLabel = JasperCompileManager.compileReport(inputStream) | |||
| val cartonLabelInfo = | |||
| deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||
| val params = mutableMapOf<String, Any>() | |||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||
| for (info in cartonLabelInfo) { | |||
| val field = mutableMapOf<String, Any>() | |||
| } | |||
| //Carton Labels | |||
| open fun exportDNLabels(request: ExportDNLabelsRequest): Map<String, Any>{ | |||
| val DNLABELS_PDF = "DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml" | |||
| val resource = ClassPathResource(DNLABELS_PDF) | |||
| if(!resource.exists()){ | |||
| throw FileNotFoundException("Label file not found: $DNLABELS_PDF") | |||
| } | |||
| val inputStream = resource.inputStream | |||
| val cartonLabel = JasperCompileManager.compileReport(inputStream) | |||
| val cartonLabelInfo = deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||
| val params = mutableMapOf<String, Any>() | |||
| val fields = mutableListOf<MutableMap<String ,Any>>() | |||
| for (info in cartonLabelInfo) { | |||
| val field = mutableMapOf<String, Any>() | |||
| } | |||
| params["shopPurchaseOrderNo"] = cartonLabelInfo[0].code | |||
| params["deliveryOrderCode"] = cartonLabelInfo[0].code | |||
| params["shopAddress"] = cartonLabelInfo[0].shopAddress ?: "" | |||
| params["shopName"] = cartonLabelInfo[0].shopName ?: "" | |||
| params["shopPurchaseOrderNo"] = cartonLabelInfo[0].code | |||
| params["deliveryOrderCode"] = cartonLabelInfo[0].code | |||
| params["shopAddress"] = cartonLabelInfo[0].shopAddress?: "" | |||
| params["shopName"] = cartonLabelInfo[0].shopName?: "" | |||
| for (cartonNumber in 1..request.numOfCarton) { | |||
| val field = mutableMapOf<String, Any>() | |||
| fields.add(field) | |||
| } | |||
| return mapOf( | |||
| "report" to PdfUtils.fillReport(cartonLabel, fields, params), | |||
| "filename" to "${cartonLabelInfo[0].code}_carton_labels" | |||
| ) | |||
| for(cartonNumber in 1..request.numOfCarton){ | |||
| val field = mutableMapOf<String, Any>() | |||
| fields.add(field) | |||
| } | |||
| return mapOf( | |||
| "report" to PdfUtils.fillReport(cartonLabel, fields, params), | |||
| "filename" to "${cartonLabelInfo[0].code}_carton_labels" | |||
| ) | |||
| } | |||
| //Print Carton Labels | |||
| @Transactional | |||
| open fun printDNLabels(request: PrintDNLabelsRequest) { | |||
| val printer = | |||
| printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||
| val pdf = exportDNLabels( | |||
| ExportDNLabelsRequest( | |||
| deliveryOrderIds = request.deliveryOrderId, | |||
| numOfCarton = request.numOfCarton | |||
| ) | |||
| ) | |||
| val jasperPrint = pdf["report"] as JasperPrint | |||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||
| try { | |||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||
| //Print Carton Labels | |||
| @Transactional | |||
| open fun printDNLabels(request: PrintDNLabelsRequest){ | |||
| val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||
| val pdf = exportDNLabels( | |||
| ExportDNLabelsRequest( | |||
| deliveryOrderIds = request.deliveryOrderId, | |||
| numOfCarton = request.numOfCarton | |||
| ) | |||
| ) | |||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||
| printer.ip?.let { ip -> | |||
| printer.port?.let { port -> | |||
| ZebraPrinterUtil.printPdfToZebra( | |||
| tempPdfFile, | |||
| ip, | |||
| port, | |||
| printQty, | |||
| ZebraPrinterUtil.PrintDirection.ROTATED | |||
| ) | |||
| } | |||
| } | |||
| val jasperPrint = pdf["report"] as JasperPrint | |||
| println("Test PDF saved to: ${tempPdfFile.absolutePath}") | |||
| val tempPdfFile = File.createTempFile("print_job_",".pdf") | |||
| } finally { | |||
| //tempPdfFile.delete() | |||
| } | |||
| try{ | |||
| JasperExportManager.exportReportToPdfFile(jasperPrint,tempPdfFile.absolutePath) | |||
| } | |||
| @Transactional(rollbackFor = [Exception::class]) | |||
| open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult { | |||
| println("🔍 DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}") | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | |||
| ?: throw NoSuchElementException("Delivery Order not found") | |||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||
| printer.ip?.let { ip -> printer.port?.let { port -> | |||
| ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED) | |||
| }} | |||
| // ✅ 检查状态,跳过已完成或已发布的DO | |||
| if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) { | |||
| throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release") | |||
| } | |||
| println("Test PDF saved to: ${tempPdfFile.absolutePath}") | |||
| // ✅ 更新状态为released (使用RECEIVING表示已发布) | |||
| deliveryOrder.apply { | |||
| status = DeliveryOrderStatus.RECEIVING // 使用RECEIVING表示已发布状态 | |||
| } | |||
| deliveryOrderRepository.save(deliveryOrder) | |||
| // 创建 pick order | |||
| 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) | |||
| 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) | |||
| // 创建 suggestions 和 hold inventory | |||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | |||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLines( | |||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | |||
| ) | |||
| val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | |||
| val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null } | |||
| if (insufficientCount > 0) { | |||
| println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)") | |||
| } | |||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | |||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | |||
| ) | |||
| } finally { | |||
| //tempPdfFile.delete() | |||
| 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) | |||
| // 创建 stock out | |||
| val stockOut = StockOut().apply { | |||
| this.type = "job" | |||
| this.consoPickOrderCode = consoCode | |||
| this.status = StockOutStatus.PENDING.status | |||
| this.handler = request.userId | |||
| } | |||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||
| 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) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // 分析楼层分布 | |||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||
| 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 preferredFloor = if ((floorItemCount["4F"] ?: 0) == itemIds.size && (floorItemCount["2F"] ?: 0) == 0) { | |||
| "4F" | |||
| } else { | |||
| "2F" | |||
| } | |||
| // ✅ 查找匹配 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) } | |||
| } | |||
| } | |||
| // ✅ 如果没有匹配的 truck,抛出异常跳过 | |||
| if (truck == null) { | |||
| val errorMsg = "No matching truck found for preferredFloor ($preferredFloor). Skipping DO ${deliveryOrder.id}." | |||
| println("⚠️ $errorMsg") | |||
| throw IllegalStateException(errorMsg) | |||
| } | |||
| println("✅ DEBUG: Matched truck - ID=${truck.id}, Store=${truck.storeId}, Floor=$preferredFloor") | |||
| 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 = truck.id, | |||
| truckDepartureTime = truck.departureTime, | |||
| truckLanceCode = truck.truckLanceCode, | |||
| loadingSequence = truck.loadingSequence // ✅ 直接使用 truck 的值 | |||
| ) | |||
| } | |||
| @Transactional | |||
| open fun syncDeliveryOrderStatusFromDoPickOrder(): Int { | |||
| val sql = """ | |||
| UPDATE fpsmsdb.delivery_order do | |||
| INNER JOIN fpsmsdb.do_pick_order dpo ON dpo.do_order_id = do.id | |||
| SET do.status = 'completed' | |||
| WHERE dpo.ticket_status = 'completed' | |||
| AND do.status != 'completed' | |||
| AND do.deleted = 0 | |||
| AND dpo.deleted = 0 | |||
| """.trimIndent() | |||
| return jdbcDao.executeUpdate(sql, emptyMap<String, Any>()) | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,169 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.service | |||
| import com.ffii.core.support.JdbcDao | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByLaneRequest | |||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | |||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||
| import com.ffii.fpsms.modules.user.entity.UserRepository | |||
| import org.springframework.stereotype.Service | |||
| import java.time.LocalDateTime | |||
| @Service | |||
| class DoPickOrderAssignmentService( | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||
| private val pickOrderRepository: PickOrderRepository, | |||
| private val userRepository: UserRepository, | |||
| private val jdbcDao: JdbcDao // ✅ 添加 JdbcDao | |||
| ) { | |||
| fun assignByLane(request: AssignByLaneRequest): MessageResponse { | |||
| val user = userRepository.findById(request.userId).orElse(null) | |||
| ?: return MessageResponse( | |||
| id = null, code = "USER_NOT_FOUND", name = null, type = null, | |||
| message = "User not found", errorPosition = null, entity = null | |||
| ) | |||
| // ✅ 转换 storeId 格式 | |||
| val actualStoreId = when (request.storeId) { | |||
| "2/F" -> "2/F" | |||
| "4/F" -> "4/F" | |||
| else -> request.storeId | |||
| } | |||
| println("🔍 DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'") | |||
| // ✅ 获取所有候选记录 | |||
| val allCandidates = doPickOrderRepository | |||
| .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | |||
| actualStoreId, | |||
| DoPickOrderStatus.pending | |||
| ) | |||
| .filter { it.truckLanceCode == request.truckLanceCode } | |||
| println("🔍 DEBUG: Found ${allCandidates.size} candidate do_pick_orders for lane ${request.truckLanceCode}") | |||
| // ✅ 过滤掉所有 pick orders 都是 issue 的记录 | |||
| val filteredCandidates = allCandidates.filter { doPickOrder -> | |||
| val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | |||
| if (!hasNonIssueLines) { | |||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | |||
| } | |||
| hasNonIssueLines | |||
| } | |||
| println("🔍 DEBUG: After filtering, ${filteredCandidates.size} do_pick_orders remain") | |||
| if (filteredCandidates.isEmpty()) { | |||
| return MessageResponse( | |||
| id = null, code = "NO_ORDERS", name = null, type = null, | |||
| message = "No available pick order(s) for this lane (all have stock issues).", | |||
| errorPosition = null, entity = null | |||
| ) | |||
| } | |||
| val firstOrder = filteredCandidates.first() | |||
| // ✅ 更新 do_pick_order | |||
| firstOrder.handledBy = request.userId | |||
| firstOrder.handlerName = user.name | |||
| firstOrder.ticketStatus = DoPickOrderStatus.released | |||
| firstOrder.ticketReleaseTime = LocalDateTime.now() | |||
| doPickOrderRepository.save(firstOrder) | |||
| // ✅ 获取这个 do_pick_order 下的所有 pick orders 并分配给用户 | |||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(firstOrder.id!!) | |||
| println("🔍 DEBUG: Found ${doPickOrderLines.size} pick orders in do_pick_order ${firstOrder.id}") | |||
| doPickOrderLines.forEach { line -> | |||
| if (line.pickOrderId != null) { | |||
| val pickOrder = pickOrderRepository.findById(line.pickOrderId!!).orElse(null) | |||
| if (pickOrder != null) { | |||
| pickOrder.assignTo = user | |||
| pickOrder.status = PickOrderStatus.RELEASED | |||
| pickOrderRepository.save(pickOrder) | |||
| println("🔍 DEBUG: Assigned pick order ${line.pickOrderId} to user ${request.userId}") | |||
| } else { | |||
| println("⚠️ WARNING: Pick order ${line.pickOrderId} not found") | |||
| } | |||
| } | |||
| } | |||
| // ✅ 同步更新 do_pick_order_record(如果有的话) | |||
| doPickOrderLines.forEach { line -> | |||
| if (line.pickOrderId != null) { | |||
| val records = doPickOrderRecordRepository.findByPickOrderId(line.pickOrderId!!) | |||
| records.forEach { record -> | |||
| record.handledBy = request.userId | |||
| record.handlerName = user.name | |||
| record.ticketStatus = DoPickOrderStatus.released | |||
| record.ticketReleaseTime = LocalDateTime.now() | |||
| } | |||
| if (records.isNotEmpty()) { | |||
| doPickOrderRecordRepository.saveAll(records) | |||
| println("🔍 DEBUG: Updated ${records.size} do_pick_order_record for pick order ${line.pickOrderId}") | |||
| } | |||
| } | |||
| } | |||
| return MessageResponse( | |||
| id = firstOrder.id, | |||
| code = "SUCCESS", | |||
| name = null, | |||
| type = null, | |||
| message = "Assigned ${doPickOrderLines.size} pick order(s) from lane ${request.truckLanceCode}", | |||
| errorPosition = null, | |||
| entity = mapOf( | |||
| "doPickOrderId" to firstOrder.id, | |||
| "ticketNo" to firstOrder.ticketNo, | |||
| "numberOfPickOrders" to doPickOrderLines.size, | |||
| "pickOrderIds" to doPickOrderLines.mapNotNull { it.pickOrderId } | |||
| ) | |||
| ) | |||
| } | |||
| // ✅ 添加过滤方法(和 DoPickOrderQueryService 中相同的逻辑) | |||
| private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean { | |||
| return try { | |||
| val totalLinesSql = """ | |||
| SELECT COUNT(*) as total_lines | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||
| AND dpol.deleted = 0 | |||
| """.trimIndent() | |||
| val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||
| val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0 | |||
| if (totalLines == 0) { | |||
| return true // 没有 lines,不算过滤 | |||
| } | |||
| val nonIssueLinesSql = """ | |||
| SELECT COUNT(*) as non_issue_lines | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||
| AND dpol.deleted = 0 | |||
| AND (dpol.status IS NULL OR dpol.status != 'issue') | |||
| """.trimIndent() | |||
| val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||
| val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0 | |||
| val hasNonIssueLines = nonIssueLines > 0 | |||
| println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total lines: $totalLines, Non-issue lines: $nonIssueLines, Has non-issue lines: $hasNonIssueLines") | |||
| hasNonIssueLines | |||
| } catch (e: Exception) { | |||
| println("❌ Error checking non-issue lines for do_pick_order $doPickOrderId: ${e.message}") | |||
| true // 出错时不过滤 | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,102 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.service | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.* | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.transaction.annotation.Transactional | |||
| import java.time.LocalDateTime | |||
| @Service | |||
| open class DoPickOrderCompletionService( | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | |||
| private val deliveryOrderRepository: DeliveryOrderRepository | |||
| ) { | |||
| @Transactional | |||
| fun completeDoPickOrdersForPickOrder(pickOrderId: Long): List<DoPickOrder> { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| doPickOrders.forEach { | |||
| it.ticketStatus = DoPickOrderStatus.completed | |||
| it.ticketCompleteDateTime = LocalDateTime.now() | |||
| } | |||
| val savedDoPickOrders = doPickOrderRepository.saveAll(doPickOrders) | |||
| // ✅ 同步更新相关的delivery_order状态 | |||
| savedDoPickOrders.forEach { doPickOrder -> | |||
| if (doPickOrder.doOrderId != null) { | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doPickOrder.doOrderId!!) | |||
| if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) { | |||
| deliveryOrder.status = DeliveryOrderStatus.COMPLETED | |||
| deliveryOrderRepository.save(deliveryOrder) | |||
| println("✅ Updated delivery order ${doPickOrder.doOrderId} status to completed") | |||
| } | |||
| } | |||
| } | |||
| return savedDoPickOrders | |||
| } | |||
| @Transactional | |||
| fun removeDoPickOrdersForPickOrder(pickOrderId: Long): Int { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| var deletedCount = 0 | |||
| doPickOrders.forEach { doPickOrder -> | |||
| // ✅ 第一步:复制 do_pick_order 到 do_pick_order_record | |||
| val doPickOrderRecord = DoPickOrderRecord( | |||
| storeId = doPickOrder.storeId ?: "", | |||
| ticketNo = doPickOrder.ticketNo ?: "", | |||
| ticketStatus = DoPickOrderStatus.completed, | |||
| truckId = doPickOrder.truckId, | |||
| pickOrderId = doPickOrder.pickOrderId, | |||
| truckDepartureTime = doPickOrder.truckDepartureTime, | |||
| shopId = doPickOrder.shopId, | |||
| handledBy = doPickOrder.handledBy, | |||
| handlerName = doPickOrder.handlerName, | |||
| doOrderId = doPickOrder.doOrderId, | |||
| pickOrderCode = doPickOrder.pickOrderCode, | |||
| deliveryOrderCode = doPickOrder.deliveryOrderCode, | |||
| loadingSequence = doPickOrder.loadingSequence, | |||
| ticketReleaseTime = doPickOrder.ticketReleaseTime, | |||
| ticketCompleteDateTime = LocalDateTime.now(), | |||
| truckLanceCode = doPickOrder.truckLanceCode, | |||
| shopCode = doPickOrder.shopCode, | |||
| shopName = doPickOrder.shopName, | |||
| requiredDeliveryDate = doPickOrder.requiredDeliveryDate | |||
| ) | |||
| val savedRecord = doPickOrderRecordRepository.save(doPickOrderRecord) | |||
| println("✅ Copied do_pick_order ${doPickOrder.id} to do_pick_order_record") | |||
| // ✅ 第二步:复制 do_pick_order_line 到 do_pick_order_line_record | |||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!) | |||
| doPickOrderLines.forEach { line -> | |||
| val doPickOrderLineRecord = DoPickOrderLineRecord() | |||
| doPickOrderLineRecord.doPickOrderId = savedRecord.id | |||
| doPickOrderLineRecord.pickOrderId = line.pickOrderId | |||
| doPickOrderLineRecord.doOrderId = line.doOrderId | |||
| doPickOrderLineRecord.pickOrderCode = line.pickOrderCode | |||
| doPickOrderLineRecord.deliveryOrderCode = line.deliveryOrderCode | |||
| doPickOrderLineRecord.status = line.status | |||
| doPickOrderLineRecordRepository.save(doPickOrderLineRecord) | |||
| println("✅ Copied do_pick_order_line ${line.id} to do_pick_order_line_record") | |||
| } | |||
| // ✅ 第三步:删除原始的 do_pick_order_line 记录 | |||
| if (doPickOrderLines.isNotEmpty()) { | |||
| doPickOrderLineRepository.deleteAll(doPickOrderLines) | |||
| println("✅ Deleted ${doPickOrderLines.size} do_pick_order_line records") | |||
| } | |||
| // ✅ 第四步:删除原始的 do_pick_order 记录 | |||
| doPickOrderRepository.delete(doPickOrder) | |||
| deletedCount++ | |||
| println("✅ Deleted do_pick_order ${doPickOrder.id}") | |||
| } | |||
| return deletedCount | |||
| } | |||
| } | |||
| @@ -0,0 +1,108 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.service | |||
| import com.ffii.core.support.JdbcDao | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneBtn | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneRow | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.StoreLaneSummary | |||
| import org.springframework.stereotype.Service | |||
| import java.time.LocalDate | |||
| @Service | |||
| class DoPickOrderQueryService( | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val jdbcDao: JdbcDao | |||
| ) { | |||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | |||
| val targetDate = requiredDate ?: LocalDate.now() | |||
| println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate") | |||
| val actualStoreId = when (storeId) { | |||
| "2/F" -> "2/F" | |||
| "4/F" -> "4/F" | |||
| else -> storeId | |||
| } | |||
| val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||
| actualStoreId, | |||
| targetDate, | |||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | |||
| ) | |||
| println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate") | |||
| val filteredRecords = allRecords.filter { doPickOrder -> | |||
| val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | |||
| if (!hasNonIssueLines) { | |||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | |||
| } | |||
| hasNonIssueLines | |||
| } | |||
| println("🔍 DEBUG: After filtering, ${filteredRecords.size} records remain") | |||
| val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||
| .mapValues { (_, list) -> | |||
| LaneBtn( | |||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||
| unassigned = list.count { it.handledBy == null }, | |||
| total = list.size | |||
| ) | |||
| } | |||
| val timeGroups = grouped.entries | |||
| .groupBy { it.key.first } | |||
| .mapValues { (_, entries) -> | |||
| entries.map { it.value } | |||
| .sortedByDescending { it.unassigned } | |||
| .take(3) | |||
| } | |||
| .filterValues { lanes -> lanes.any { it.unassigned > 0 } } | |||
| .toSortedMap(compareBy { it }) | |||
| .entries.take(4) | |||
| .map { (time, lanes) -> | |||
| LaneRow( | |||
| truckDepartureTime = time?.toString() ?: "", | |||
| lanes = lanes | |||
| ) | |||
| } | |||
| return StoreLaneSummary(storeId = storeId, rows = timeGroups) | |||
| } | |||
| private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean { | |||
| return try { | |||
| val totalLinesSql = """ | |||
| SELECT COUNT(*) as total_lines | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||
| AND dpol.deleted = 0 | |||
| """.trimIndent() | |||
| val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||
| val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0 | |||
| if (totalLines == 0) { | |||
| return true | |||
| } | |||
| val nonIssueLinesSql = """ | |||
| SELECT COUNT(*) as non_issue_lines | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||
| AND dpol.deleted = 0 | |||
| AND (dpol.status IS NULL OR dpol.status != 'issue') | |||
| """.trimIndent() | |||
| val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||
| val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0 | |||
| nonIssueLines > 0 | |||
| } catch (e: Exception) { | |||
| println("❌ Error checking non-issue lines: ${e.message}") | |||
| true | |||
| } | |||
| } | |||
| } | |||
| @@ -3,7 +3,6 @@ package com.ffii.fpsms.modules.deliveryOrder.service | |||
| import com.ffii.fpsms.m18.entity.M18DataLogRepository | |||
| import com.ffii.fpsms.m18.service.M18DataLogService | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrder | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.models.DeliveryOrderInfo | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.SaveDeliveryOrderRequest | |||
| @@ -42,15 +41,22 @@ import java.time.LocalDate | |||
| import java.time.format.DateTimeFormatter | |||
| import com.ffii.core.support.JdbcDao | |||
| import com.ffii.fpsms.modules.pickOrder.entity.Truck | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord | |||
| import org.springframework.context.annotation.Lazy | |||
| @Service | |||
| class DoPickOrderService( | |||
| open class DoPickOrderService( | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||
| private val userRepository: UserRepository, | |||
| private val pickOrderRepository: PickOrderRepository, | |||
| private val jdbcDao: JdbcDao, // ✅ 添加这行 | |||
| private val truckRepository: TruckRepository | |||
| private val truckRepository: TruckRepository, | |||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||
| @Lazy private val deliveryOrderRepository: DeliveryOrderRepository, | |||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository | |||
| ) { | |||
| fun findReleasedDoPickOrders(): List<DoPickOrder> { | |||
| return doPickOrderRepository.findByTicketStatusIn( | |||
| @@ -141,20 +147,86 @@ class DoPickOrderService( | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| doPickOrders.forEach { | |||
| it.ticketStatus = DoPickOrderStatus.completed | |||
| it.ticketCompleteDateTime = LocalDateTime.now() // ✅ 设置完成时间 | |||
| it.ticketCompleteDateTime = LocalDateTime.now() | |||
| } | |||
| return doPickOrderRepository.saveAll(doPickOrders) | |||
| val savedDoPickOrders = doPickOrderRepository.saveAll(doPickOrders) | |||
| // ✅ 同步更新相关的delivery_order状态 | |||
| savedDoPickOrders.forEach { doPickOrder -> | |||
| if (doPickOrder.doOrderId != null) { | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doPickOrder.doOrderId!!) | |||
| if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) { | |||
| deliveryOrder.status = DeliveryOrderStatus.COMPLETED | |||
| deliveryOrderRepository.save(deliveryOrder) | |||
| println("✅ Updated delivery order ${doPickOrder.doOrderId} status to completed") | |||
| } | |||
| } | |||
| } | |||
| return savedDoPickOrders | |||
| } | |||
| // ✅ New method to remove do_pick_order records when auto-assigning by store | |||
| // ✅ 修改方法:先复制记录到record表,再删除原记录 | |||
| @Transactional | |||
| fun removeDoPickOrdersForPickOrder(pickOrderId: Long): Int { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| if (doPickOrders.isNotEmpty()) { | |||
| // ✅ 物理删除记录 | |||
| doPickOrderRepository.deleteAll(doPickOrders) | |||
| return doPickOrders.size | |||
| var deletedCount = 0 | |||
| doPickOrders.forEach { doPickOrder -> | |||
| // ✅ 第一步:复制 do_pick_order 到 do_pick_order_record | |||
| val doPickOrderRecord = DoPickOrderRecord( | |||
| storeId = doPickOrder.storeId?: "", | |||
| ticketNo = doPickOrder.ticketNo?: "", | |||
| ticketStatus = DoPickOrderStatus.completed, // 设置为completed状态 | |||
| truckId = doPickOrder.truckId, | |||
| pickOrderId = doPickOrder.pickOrderId, | |||
| truckDepartureTime = doPickOrder.truckDepartureTime, | |||
| shopId = doPickOrder.shopId, | |||
| handledBy = doPickOrder.handledBy, | |||
| handlerName = doPickOrder.handlerName, | |||
| doOrderId = doPickOrder.doOrderId, | |||
| pickOrderCode = doPickOrder.pickOrderCode, | |||
| deliveryOrderCode = doPickOrder.deliveryOrderCode, | |||
| loadingSequence = doPickOrder.loadingSequence, | |||
| ticketReleaseTime = doPickOrder.ticketReleaseTime, | |||
| ticketCompleteDateTime = LocalDateTime.now(), // 设置完成时间 | |||
| truckLanceCode = doPickOrder.truckLanceCode, | |||
| shopCode = doPickOrder.shopCode, | |||
| shopName = doPickOrder.shopName, | |||
| requiredDeliveryDate = doPickOrder.requiredDeliveryDate | |||
| ) | |||
| val savedRecord = doPickOrderRecordRepository.save(doPickOrderRecord) | |||
| println("✅ Copied do_pick_order ${doPickOrder.id} to do_pick_order_record") | |||
| // ✅ 第二步:复制 do_pick_order_line 到 do_pick_order_line_record | |||
| // ✅ 第二步:复制 do_pick_order_line 到 do_pick_order_line_record | |||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!) | |||
| doPickOrderLines.forEach { line -> | |||
| val doPickOrderLineRecord = DoPickOrderLineRecord() // ✅ 使用默认构造函数 | |||
| doPickOrderLineRecord.doPickOrderId = savedRecord.id // ✅ 设置属性 | |||
| doPickOrderLineRecord.pickOrderId = line.pickOrderId | |||
| doPickOrderLineRecord.doOrderId = line.doOrderId | |||
| doPickOrderLineRecord.pickOrderCode = line.pickOrderCode | |||
| doPickOrderLineRecord.deliveryOrderCode = line.deliveryOrderCode | |||
| doPickOrderLineRecord.status = line.status | |||
| doPickOrderLineRecordRepository.save(doPickOrderLineRecord) | |||
| println("✅ Copied do_pick_order_line ${line.id} to do_pick_order_line_record") | |||
| } | |||
| // ✅ 第三步:删除原始的 do_pick_order_line 记录 | |||
| if (doPickOrderLines.isNotEmpty()) { | |||
| doPickOrderLineRepository.deleteAll(doPickOrderLines) | |||
| println("✅ Deleted ${doPickOrderLines.size} do_pick_order_line records") | |||
| } | |||
| // ✅ 第四步:删除原始的 do_pick_order 记录 | |||
| doPickOrderRepository.delete(doPickOrder) | |||
| deletedCount++ | |||
| println("✅ Deleted do_pick_order ${doPickOrder.id}") | |||
| } | |||
| return 0 | |||
| return deletedCount | |||
| } | |||
| fun saveRecord(record: DoPickOrderRecord): DoPickOrderRecord { | |||
| @@ -197,31 +269,48 @@ class DoPickOrderService( | |||
| } | |||
| return doPickOrderRepository.saveAll(doPickOrders) | |||
| } | |||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | |||
| // ✅ 使用传入的日期,如果没有传入则使用今天 | |||
| val targetDate = requiredDate ?: LocalDate.now() | |||
| println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate") | |||
| // ✅ 修改:按日期查询订单 | |||
| // ✅ 修复格式转换:保持原始格式 | |||
| val actualStoreId = when (storeId) { | |||
| "2/F" -> "2/F" // ✅ 保持原格式 | |||
| "4/F" -> "4/F" // ✅ 保持原格式 | |||
| else -> storeId | |||
| } | |||
| println("🔍 DEBUG: Using storeId: '$actualStoreId'") | |||
| // ✅ 直接查询 do_pick_order 表 | |||
| val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||
| storeId, | |||
| actualStoreId, | |||
| targetDate, | |||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | |||
| ) | |||
| println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate") | |||
| val grouped = allRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||
| // ✅ 过滤掉所有 do_pick_order_line 都是 "issue" 状态的 shop | |||
| val filteredRecords = allRecords.filter { doPickOrder -> | |||
| val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | |||
| if (!hasNonIssueLines) { | |||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | |||
| } | |||
| hasNonIssueLines | |||
| } | |||
| println("🔍 DEBUG: After filtering, ${filteredRecords.size} records remain") | |||
| // ✅ 使用过滤后的记录 | |||
| val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||
| .mapValues { (_, list) -> | |||
| LaneBtn( | |||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||
| unassigned = list.count { it.handledBy == null }, // 未分配的订单数 | |||
| total = list.size // 总订单数(包括已分配和未分配) | |||
| unassigned = list.count { it.handledBy == null }, | |||
| total = list.size | |||
| ) | |||
| } | |||
| val timeGroups = grouped.entries | |||
| .groupBy { it.key.first } | |||
| .mapValues { (_, entries) -> | |||
| @@ -238,91 +327,136 @@ class DoPickOrderService( | |||
| lanes = lanes | |||
| ) | |||
| } | |||
| return StoreLaneSummary(storeId = storeId, rows = timeGroups) | |||
| } | |||
| private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean { | |||
| try { | |||
| // 1. 获取该 do_pick_order 的所有 do_pick_order_line 数量 | |||
| val totalLinesSql = """ | |||
| SELECT COUNT(*) as total_lines | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||
| AND dpol.deleted = 0 | |||
| """.trimIndent() | |||
| val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||
| val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0 | |||
| if (totalLines == 0) { | |||
| return true // 没有 lines,不算过滤 | |||
| } | |||
| // 2. 获取非 "issue" 状态的 lines 数量 | |||
| val nonIssueLinesSql = """ | |||
| SELECT COUNT(*) as non_issue_lines | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||
| AND dpol.deleted = 0 | |||
| AND (dpol.status IS NULL OR dpol.status != 'issue') | |||
| """.trimIndent() | |||
| val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||
| val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0 | |||
| // 3. 只有当所有 lines 都是 "issue" 状态时才过滤掉 | |||
| val hasNonIssueLines = nonIssueLines > 0 | |||
| println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total lines: $totalLines, Non-issue lines: $nonIssueLines, Has non-issue lines: $hasNonIssueLines") | |||
| return hasNonIssueLines | |||
| } catch (e: Exception) { | |||
| println("❌ Error checking non-issue lines for do_pick_order $doPickOrderId: ${e.message}") | |||
| return true // 出错时不过滤 | |||
| } | |||
| } | |||
| // ✅ 修复:把 assignByLane 移到类里面 | |||
| fun assignByLane(request: AssignByLaneRequest): MessageResponse { | |||
| 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 | |||
| val user = userRepository.findById(request.userId).orElse(null) | |||
| ?: return MessageResponse( | |||
| id = null, code = "USER_NOT_FOUND", name = null, type = null, | |||
| message = "User not found", errorPosition = null, entity = null | |||
| ) | |||
| // ✅ 转换 storeId 格式:'2/F' -> '2F/F', '4/F' -> '4F/F' | |||
| val actualStoreId = when (request.storeId) { | |||
| "2/F" -> "2/F" // ✅ 保持原格式 | |||
| "4/F" -> "4/F" // ✅ 保持原格式 | |||
| else -> request.storeId | |||
| } | |||
| println("🔍 DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'") | |||
| val candidates = doPickOrderRepository | |||
| .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | |||
| request.storeId, | |||
| actualStoreId, | |||
| DoPickOrderStatus.pending | |||
| ) | |||
| .filter { | |||
| it.handledBy == null && | |||
| it.truckLanceCode == request.truckLanceCode && | |||
| (request.truckDepartureTime == null || | |||
| it.truckDepartureTime?.toString() == request.truckDepartureTime) | |||
| } | |||
| .filter { it.truckLanceCode == request.truckLanceCode } | |||
| 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 | |||
| message = "No available pick order(s) for this lane.", errorPosition = null, entity = null | |||
| ) | |||
| } | |||
| val firstOrder = candidates.first() | |||
| val user = userRepository.findById(request.userId).orElse(null) | |||
| val handlerName = user?.name ?: "Unknown" | |||
| // ✅ 更新 do_pick_order - 保持原有的卡车信息 | |||
| // ✅ 更新 do_pick_order | |||
| firstOrder.handledBy = request.userId | |||
| firstOrder.handlerName = handlerName | |||
| firstOrder.handlerName = user.name | |||
| 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) | |||
| // ✅ 关键修改:获取这个 do_pick_order 下的所有 pick orders 并分配给用户 | |||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(firstOrder.id!!) | |||
| println("🔍 DEBUG: Found ${doPickOrderLines.size} pick orders in do_pick_order ${firstOrder.id}") | |||
| doPickOrderLines.forEach { line -> | |||
| if (line.pickOrderId != null) { | |||
| val pickOrder = pickOrderRepository.findById(line.pickOrderId!!).orElse(null) | |||
| if (pickOrder != null) { | |||
| pickOrder.assignTo = user | |||
| pickOrder.status = PickOrderStatus.RELEASED | |||
| pickOrderRepository.save(pickOrder) | |||
| println("🔍 DEBUG: Assigned pick order ${line.pickOrderId} to user ${request.userId}") | |||
| } else { | |||
| println("⚠️ WARNING: Pick order ${line.pickOrderId} not found") | |||
| } | |||
| } | |||
| } | |||
| // 同步更新 record | |||
| val records = doPickOrderRecordRepository.findByPickOrderId(firstOrder.pickOrderId!!) | |||
| records.forEach { | |||
| it.handledBy = request.userId | |||
| it.handlerName = handlerName | |||
| it.ticketStatus = DoPickOrderStatus.released | |||
| it.ticketReleaseTime = LocalDateTime.now() | |||
| // ✅ 同步更新 do_pick_order_record(如果有的话) | |||
| doPickOrderLines.forEach { line -> | |||
| if (line.pickOrderId != null) { | |||
| val records = doPickOrderRecordRepository.findByPickOrderId(line.pickOrderId!!) | |||
| records.forEach { record -> | |||
| record.handledBy = request.userId | |||
| record.handlerName = user.name | |||
| record.ticketStatus = DoPickOrderStatus.released | |||
| record.ticketReleaseTime = LocalDateTime.now() | |||
| } | |||
| if (records.isNotEmpty()) { | |||
| doPickOrderRecordRepository.saveAll(records) | |||
| println("🔍 DEBUG: Updated ${records.size} do_pick_order_record for pick order ${line.pickOrderId}") | |||
| } | |||
| } | |||
| } | |||
| doPickOrderRecordRepository.saveAll(records) | |||
| return MessageResponse( | |||
| id = firstOrder.pickOrderId, | |||
| id = firstOrder.id, | |||
| code = "SUCCESS", | |||
| name = null, | |||
| type = null, | |||
| message = "Assigned pick order from lane ${request.truckLanceCode}", | |||
| message = "Assigned ${doPickOrderLines.size} pick order(s) from lane ${request.truckLanceCode}", | |||
| errorPosition = null, | |||
| entity = mapOf( | |||
| "pickOrderId" to firstOrder.pickOrderId, | |||
| "ticketNo" to firstOrder.ticketNo | |||
| "doPickOrderId" to firstOrder.id, | |||
| "ticketNo" to firstOrder.ticketNo, | |||
| "numberOfPickOrders" to doPickOrderLines.size, | |||
| "pickOrderIds" to doPickOrderLines.mapNotNull { it.pickOrderId } | |||
| ) | |||
| ) | |||
| } | |||
| @@ -10,7 +10,14 @@ import java.util.concurrent.Executors | |||
| import java.util.concurrent.atomic.AtomicInteger | |||
| import kotlin.math.min | |||
| import com.ffii.core.support.JdbcDao | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoResult | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrder | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLine | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||
| data class BatchReleaseJobStatus( | |||
| val jobId: String, | |||
| val total: Int, | |||
| @@ -24,7 +31,10 @@ data class BatchReleaseJobStatus( | |||
| @Service | |||
| class DoReleaseCoordinatorService( | |||
| private val deliveryOrderService: DeliveryOrderService, | |||
| private val jdbcDao: JdbcDao | |||
| private val jdbcDao: JdbcDao, | |||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||
| private val deliveryOrderRepository: DeliveryOrderRepository, | |||
| private val doPickOrderRepository: DoPickOrderRepository | |||
| ) { | |||
| private val poolSize = Runtime.getRuntime().availableProcessors() | |||
| private val executor = Executors.newFixedThreadPool(min(poolSize, 4)) | |||
| @@ -135,7 +145,13 @@ class DoReleaseCoordinatorService( | |||
| ) AS loading_sequence | |||
| FROM fpsmsdb.delivery_order do | |||
| LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id | |||
| WHERE do.deleted = 0 | |||
| WHERE do.id IN ( | |||
| SELECT DISTINCT do_order_id | |||
| FROM fpsmsdb.do_pick_order | |||
| WHERE ticket_no LIKE 'TEMP-%' | |||
| AND deleted = 0 | |||
| ) | |||
| AND do.deleted = 0 | |||
| ) | |||
| SELECT | |||
| dpo2.id, | |||
| @@ -224,7 +240,7 @@ class DoReleaseCoordinatorService( | |||
| FROM DoFloorSummary | |||
| ), | |||
| TruckSelection AS ( | |||
| SELECT | |||
| SELECT | |||
| do.id AS delivery_order_id, | |||
| do.shopId, | |||
| do.estimatedArrivalDate, | |||
| @@ -296,7 +312,7 @@ class DoReleaseCoordinatorService( | |||
| ) AS loading_sequence | |||
| FROM fpsmsdb.delivery_order do | |||
| LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id | |||
| WHERE do.id IN (${ids.joinToString(",")}) | |||
| WHERE do.id IN (${ids.joinToString(",")}) | |||
| AND do.deleted = 0 | |||
| ) | |||
| SELECT delivery_order_id AS id | |||
| @@ -314,8 +330,8 @@ class DoReleaseCoordinatorService( | |||
| println("🔍 DEBUG: SQL length: ${sql.length} characters") // ✅ 添加这行 | |||
| println("🔍 DEBUG: SQL first 500 chars: ${sql.take(500)}") // ✅ 添加这行 | |||
| val results = jdbcDao.queryForList(sql) | |||
| val results = jdbcDao.queryForList(sql) | |||
| println("🔍 DEBUG: Results type: ${results.javaClass.name}") // ✅ 添加这行 | |||
| println("🔍 DEBUG: Results size: ${results.size}") // ✅ 添加这行 | |||
| @@ -337,13 +353,13 @@ class DoReleaseCoordinatorService( | |||
| } else { | |||
| sortedIds | |||
| } | |||
| } catch (e: Exception) { | |||
| } catch (e: Exception) { | |||
| println("❌ ERROR: ${e.message}") | |||
| println("❌ ERROR Stack Trace:") // ✅ 添加这行 | |||
| e.printStackTrace() | |||
| return ids | |||
| e.printStackTrace() | |||
| return ids | |||
| } | |||
| } | |||
| } | |||
| fun startBatchReleaseAsync(ids: List<Long>, userId: Long): MessageResponse { | |||
| if (ids.isEmpty()) { | |||
| return MessageResponse(id = null, code = "NO_IDS", name = null, type = null, | |||
| @@ -356,55 +372,72 @@ class DoReleaseCoordinatorService( | |||
| executor.submit { | |||
| try { | |||
| println("📦 Starting serial batch release for ${ids.size} orders") | |||
| val sortedIds = getOrderedDeliveryOrderIds(ids) // ✅ 使用本地方法 | |||
| println("📦 Starting batch release for ${ids.size} orders") | |||
| val sortedIds = getOrderedDeliveryOrderIds(ids) | |||
| println("🔍 DEBUG: Got ${sortedIds.size} sorted orders") | |||
| sortedIds.forEachIndexed { index, id -> | |||
| val releaseResults = mutableListOf<ReleaseDoResult>() | |||
| // 第一步:发布所有 DO(创建 pick orders,但不创建 DoPickOrder) | |||
| sortedIds.forEach { id -> | |||
| try { | |||
| val res = deliveryOrderService.releaseDeliveryOrder( | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(id) | |||
| if (deliveryOrder?.status == DeliveryOrderStatus.COMPLETED || | |||
| deliveryOrder?.status == DeliveryOrderStatus.RECEIVING) { | |||
| println("⏭️ DO $id is already ${deliveryOrder.status?.value}, skipping") | |||
| return@forEach | |||
| } | |||
| val result = deliveryOrderService.releaseDeliveryOrderWithoutTicket( | |||
| ReleaseDoRequest(id = id, userId = userId) | |||
| ) | |||
| val code = res.code ?: "OK" | |||
| println("🔍 DO $id -> code='$code', msg='${res.message}'") | |||
| val isSuccess = code in setOf("SUCCESS", "OK", "PARTIAL_SUCCESS") || | |||
| code.matches(Regex("TO[A-Z]{2}\\d{2}PO\\d+")) || | |||
| (res.message == null && code.isNotEmpty()) | |||
| if (isSuccess) { | |||
| status.success.incrementAndGet() | |||
| } else { | |||
| synchronized(status.failed) { | |||
| status.failed.add(id to "Code: $code, Msg: ${res.message}") | |||
| } | |||
| println("⚠️ DO $id marked as failed: code='$code'") | |||
| } | |||
| if ((index + 1) % 50 == 0) { | |||
| println("📊 Progress: ${index + 1}/${sortedIds.size} (Success: ${status.success.get()}, Failed: ${status.failed.size})") | |||
| } | |||
| releaseResults.add(result) | |||
| status.success.incrementAndGet() | |||
| println("🔍 DO $id -> Success") | |||
| } catch (e: Exception) { | |||
| synchronized(status.failed) { | |||
| status.failed.add(id to (e.message ?: "Exception")) | |||
| } | |||
| println("❌ DO $id exception: ${e.javaClass.simpleName} - ${e.message}") | |||
| println("❌ DO $id skipped: ${e.message}") | |||
| } | |||
| } | |||
| // 第二步:按日期、楼层、店铺分组(与 SQL 逻辑一致) | |||
| val sortedResults = releaseResults.sortedWith(compareBy( | |||
| { it.estimatedArrivalDate }, | |||
| { it.preferredFloor }, | |||
| { it.truckDepartureTime }, | |||
| { it.truckLanceCode }, | |||
| { it.loadingSequence }, | |||
| { it.shopId } | |||
| )) | |||
| // ✅ 然后按正确的顺序分组(保持排序后的顺序) | |||
| val grouped = sortedResults.groupBy { | |||
| Triple(it.estimatedArrivalDate, it.preferredFloor, it.shopId) | |||
| } | |||
| println("🔍 DEBUG: Grouped into ${grouped.size} DoPickOrders") | |||
| // 第三步:为每组创建一个 DoPickOrder 和多条 DoPickOrderLine | |||
| grouped.forEach { (key, group) -> | |||
| try { | |||
| createMergedDoPickOrder(group) | |||
| println("🔍 DEBUG: Created DoPickOrder for ${group.size} DOs") | |||
| } catch (e: Exception) { | |||
| println("❌ Error creating DoPickOrder: ${e.message}") | |||
| e.printStackTrace() | |||
| } | |||
| } | |||
| // 第四步:更新 ticket numbers | |||
| if (status.success.get() > 0) { | |||
| println("🎫 Updating ticket numbers...") | |||
| updateBatchTicketNumbers() | |||
| } | |||
| println("✅ Batch completed: ${status.success.get()} success, ${status.failed.size} failed") | |||
| if (status.failed.isNotEmpty()) { | |||
| println("🔴 Failed examples:") | |||
| status.failed.take(10).forEach { (id, msg) -> | |||
| println(" DO $id: $msg") | |||
| } | |||
| } | |||
| } catch (e: Exception) { | |||
| println("❌ Batch release exception: ${e.javaClass.simpleName} - ${e.message}") | |||
| e.printStackTrace() | |||
| @@ -416,10 +449,118 @@ class DoReleaseCoordinatorService( | |||
| return MessageResponse( | |||
| id = null, code = "STARTED", name = null, type = null, | |||
| message = "Batch release started (serial mode)", errorPosition = null, | |||
| message = "Batch release started", errorPosition = null, | |||
| entity = mapOf("jobId" to jobId, "total" to ids.size) | |||
| ) | |||
| } | |||
| 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 | |||
| ) | |||
| // ✅ 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save() | |||
| val saved = doPickOrderRepository.save(doPickOrder) | |||
| println("🔍 DEBUG: Saved DoPickOrder - ID: ${saved.id}, Ticket: ${saved.ticketNo}") | |||
| // 创建多条 DoPickOrderLine(每个 DO 一条) | |||
| results.forEach { result -> | |||
| val existingLines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(result.pickOrderId) | |||
| if (existingLines.isNotEmpty()) { | |||
| println("⚠️ WARNING: pick_order ${result.pickOrderId} already in do_pick_order_line, skipping") | |||
| return@forEach // 跳过这个 | |||
| } | |||
| // ✅ 先创建 DoPickOrderLine,然后检查库存问题 | |||
| val line = DoPickOrderLine().apply { | |||
| doPickOrderId = saved.id | |||
| pickOrderId = result.pickOrderId | |||
| doOrderId = result.deliveryOrderId | |||
| pickOrderCode = result.pickOrderCode | |||
| deliveryOrderCode = result.deliveryOrderCode | |||
| status = "pending" // 初始状态 | |||
| } | |||
| doPickOrderLineRepository.save(line) | |||
| println("🔍 DEBUG: Created DoPickOrderLine for pick order ${result.pickOrderId}") | |||
| } | |||
| // ✅ 现在检查整个 DoPickOrder 是否有库存问题 | |||
| val hasStockIssues = checkPickOrderHasStockIssues(saved.id!!) | |||
| if (hasStockIssues) { | |||
| // 更新所有相关的 DoPickOrderLine 状态为 "issue" | |||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(saved.id!!) | |||
| doPickOrderLines.forEach { line -> | |||
| line.status = "issue" | |||
| } | |||
| doPickOrderLineRepository.saveAll(doPickOrderLines) | |||
| println("🔍 DEBUG: Updated ${doPickOrderLines.size} DoPickOrderLine records to 'issue' status") | |||
| } | |||
| println("🔍 DEBUG: Created ${results.size} DoPickOrderLine records") | |||
| } | |||
| private fun checkPickOrderHasStockIssues(doPickOrderId: Long): Boolean { | |||
| try { | |||
| // 1. 获取 do_pick_order 的所有 pick orders 数量 | |||
| val totalPickOrdersSql = """ | |||
| SELECT COUNT(*) as total_pick_orders | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||
| AND dpol.deleted = 0 | |||
| """.trimIndent() | |||
| val totalPickOrdersResult = jdbcDao.queryForList(totalPickOrdersSql, mapOf("doPickOrderId" to doPickOrderId)) | |||
| val totalPickOrders = (totalPickOrdersResult.firstOrNull()?.get("total_pick_orders") as? Number)?.toInt() ?: 0 | |||
| if (totalPickOrders == 0) { | |||
| return false // 没有 pick orders,不算 issue | |||
| } | |||
| // 2. 获取有库存问题的 pick orders 数量(通过检查 pick_execution_issue 表) | |||
| val issuePickOrdersSql = """ | |||
| SELECT COUNT(DISTINCT dpol.pick_order_id) as issue_pick_orders | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| INNER JOIN fpsmsdb.pick_execution_issue pei ON pei.pick_order_id = dpol.pick_order_id | |||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||
| AND pei.deleted = 0 | |||
| AND dpol.deleted = 0 | |||
| """.trimIndent() | |||
| val issuePickOrdersResult = jdbcDao.queryForList(issuePickOrdersSql, mapOf("doPickOrderId" to doPickOrderId)) | |||
| val issuePickOrders = (issuePickOrdersResult.firstOrNull()?.get("issue_pick_orders") as? Number)?.toInt() ?: 0 | |||
| // 3. 只有当所有 pick orders 都有问题时才算 issue | |||
| val hasAllPickOrdersIssues = (totalPickOrders > 0) && (issuePickOrders == totalPickOrders) | |||
| println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total pick orders: $totalPickOrders, Issue pick orders: $issuePickOrders, All pick orders have issues: $hasAllPickOrdersIssues") | |||
| return hasAllPickOrdersIssues | |||
| } catch (e: Exception) { | |||
| println("❌ Error checking stock issues for do pick order $doPickOrderId: ${e.message}") | |||
| return false | |||
| } | |||
| } | |||
| fun getBatchReleaseProgress(jobId: String): MessageResponse { | |||
| val s = jobs[jobId] ?: return MessageResponse( | |||
| id = null, code = "NOT_FOUND", name = null, type = null, | |||
| @@ -39,12 +39,18 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByStoreRequest | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.* | |||
| import org.springframework.format.annotation.DateTimeFormat | |||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoReleaseCoordinatorService | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderQueryService | |||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderAssignmentService | |||
| @RestController | |||
| @RequestMapping("/doPickOrder") | |||
| class DoPickOrderController( | |||
| private val doPickOrderService: DoPickOrderService, | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val doReleaseCoordinatorService: DoReleaseCoordinatorService | |||
| private val doReleaseCoordinatorService: DoReleaseCoordinatorService, | |||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||
| private val doPickOrderQueryService: DoPickOrderQueryService, | |||
| private val doPickOrderAssignmentService: DoPickOrderAssignmentService | |||
| ) { | |||
| @PostMapping("/assign-by-store") | |||
| fun assignPickOrderByStore(@RequestBody request: AssignByStoreRequest): MessageResponse { | |||
| @@ -65,12 +71,13 @@ class DoPickOrderController( | |||
| @RequestParam storeId: String, | |||
| @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate? | |||
| ): StoreLaneSummary { | |||
| return doPickOrderService.getSummaryByStore(storeId, requiredDate) | |||
| return doPickOrderQueryService.getSummaryByStore(storeId, requiredDate) | |||
| } | |||
| @PostMapping("/assign-by-lane") | |||
| fun assignByLane(@RequestBody request: AssignByLaneRequest): MessageResponse { | |||
| return doPickOrderService.assignByLane(request) | |||
| } | |||
| fun assignByLane(@RequestBody request: AssignByLaneRequest): MessageResponse { | |||
| return doPickOrderAssignmentService.assignByLane(request) // ✅ 使用新的 Service | |||
| } | |||
| @PostMapping("/batch-release/async") | |||
| fun startBatchReleaseAsync( | |||
| @RequestBody ids: List<Long>, | |||
| @@ -1,6 +1,23 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.web.models | |||
| import java.time.LocalDate | |||
| 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? | |||
| ) | |||
| @@ -60,7 +60,11 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository | |||
| import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLine | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | |||
| @Service | |||
| open class PickOrderService( | |||
| private val jdbcDao: JdbcDao, | |||
| @@ -81,12 +85,13 @@ open class PickOrderService( | |||
| private val deliveryOrderRepository: DeliveryOrderRepository, | |||
| private val truckRepository: TruckRepository, | |||
| private val doPickOrderService: DoPickOrderService, | |||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val userRepository: UserRepository, | |||
| private val joPickOrderRepository: JoPickOrderRepository, // ✅ 添加这行 | |||
| private val joPickOrderRecordRepository: JoPickOrderRecordRepository, | |||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||
| ) : AbstractBaseEntityService<PickOrder, Long, PickOrderRepository>(jdbcDao, pickOrderRepository) { | |||
| @@ -1220,13 +1225,87 @@ logger.info("Precreated $precreated stock out lines for suggested lots on releas | |||
| pickOrderRepository.save(pickOrder) | |||
| println("✅ Updated pick order ${pickOrder.code} to COMPLETED status") | |||
| val removedCount = doPickOrderService.removeDoPickOrdersForPickOrder(pickOrderId) | |||
| println("✅ Removed $removedCount do_pick_order records for completed pick order ${pickOrderId}") | |||
| // ✅ Update do_pick_order_record status to completed (don't remove) | |||
| doPickOrderService.completeDoPickOrderRecordsForPickOrder(pickOrderId) | |||
| println("✅ Updated do_pick_order_record status to COMPLETED for pick order ${pickOrderId}") | |||
| // ✅ 修改:通过 do_pick_order_line 查询(因为 do_pick_order.pick_order_id 可能为 null) | |||
| val doPickOrderLines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(pickOrderId) | |||
| val doPickOrderIds = doPickOrderLines.mapNotNull { it.doPickOrderId }.distinct() | |||
| println("🔍 DEBUG: Found ${doPickOrderLines.size} do_pick_order_line records for pick order $pickOrderId") | |||
| println("🔍 DEBUG: Unique do_pick_order IDs: $doPickOrderIds") | |||
| if (doPickOrderIds.isEmpty()) { | |||
| println("ℹ️ INFO: No do_pick_order records found - skipping record copying") | |||
| } else { | |||
| var copied = 0 | |||
| var deleted = 0 | |||
| doPickOrderIds.forEach { doPickOrderId -> | |||
| val dpo = doPickOrderRepository.findById(doPickOrderId).orElse(null) | |||
| if (dpo == null) { | |||
| println("⚠️ WARNING: do_pick_order $doPickOrderId not found, skipping") | |||
| return@forEach | |||
| } | |||
| println("🔍 Processing do_pick_order ID: ${dpo.id}, ticket: ${dpo.ticketNo}") | |||
| // 2) 先复制 do_pick_order -> do_pick_order_record | |||
| val dpoRecord = DoPickOrderRecord( | |||
| storeId = dpo.storeId ?: "", | |||
| ticketNo = dpo.ticketNo ?: "", | |||
| ticketStatus = DoPickOrderStatus.completed, | |||
| truckId = dpo.truckId, | |||
| truckDepartureTime = dpo.truckDepartureTime, | |||
| shopId = dpo.shopId, | |||
| handledBy = dpo.handledBy, | |||
| handlerName = dpo.handlerName, | |||
| doOrderId = dpo.doOrderId, | |||
| pickOrderCode = dpo.pickOrderCode, | |||
| deliveryOrderCode = dpo.deliveryOrderCode, | |||
| loadingSequence = dpo.loadingSequence, | |||
| ticketReleaseTime = dpo.ticketReleaseTime, | |||
| ticketCompleteDateTime = java.time.LocalDateTime.now(), | |||
| truckLanceCode = dpo.truckLanceCode, | |||
| shopCode = dpo.shopCode, | |||
| shopName = dpo.shopName, | |||
| requiredDeliveryDate = dpo.requiredDeliveryDate | |||
| ) | |||
| val savedHeader = doPickOrderRecordRepository.save(dpoRecord) | |||
| // 3) 复制行 do_pick_order_line -> do_pick_order_line_record | |||
| val lines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(dpo.id!!) | |||
| val lineRecords = lines.map { l: DoPickOrderLine -> | |||
| DoPickOrderLineRecord().apply { | |||
| this.doPickOrderId = savedHeader.id | |||
| this.pickOrderId = l.pickOrderId | |||
| this.doOrderId = l.doOrderId | |||
| this.pickOrderCode = l.pickOrderCode | |||
| this.deliveryOrderCode = l.deliveryOrderCode | |||
| this.status = l.status | |||
| } | |||
| } | |||
| if (lineRecords.isNotEmpty()) { | |||
| doPickOrderLineRecordRepository.saveAll(lineRecords) | |||
| } | |||
| copied++ | |||
| // 4) 删除原行、原表 | |||
| if (lines.isNotEmpty()) doPickOrderLineRepository.deleteAll(lines) | |||
| doPickOrderRepository.delete(dpo) | |||
| deleted++ | |||
| // 5) 同步更新 delivery_order 状态为 completed | |||
| dpo.doOrderId?.let { doId -> | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId) | |||
| if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) { | |||
| deliveryOrder.status = DeliveryOrderStatus.COMPLETED | |||
| deliveryOrderRepository.save(deliveryOrder) | |||
| println("✅ Updated delivery order $doId to COMPLETED") | |||
| } | |||
| } | |||
| } | |||
| println("✅ Copied $copied do_pick_order to record and deleted $deleted original(s)") | |||
| } | |||
| // ✅ 添加:直接使用 Repository 处理 JO pick order(避免循环依赖) | |||
| if (pickOrder.jobOrder != null) { | |||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| @@ -2771,38 +2850,52 @@ open fun autoAssignAndReleasePickOrderByStoreAndTicket(storeId: String, ticketNo | |||
| try { | |||
| println("🔍 Starting getFgPickOrdersByUserId with userId: $userId") | |||
| // ✅ 完全修复:使用 shop 表的正确字段 | |||
| // ✅ 修复:从 do_pick_order_line 获取 pick order 信息 | |||
| val sql = """ | |||
| SELECT | |||
| dpo.id as doPickOrderId, | |||
| dpo.pick_order_id as pickOrderId, | |||
| dpo.do_order_id as deliveryOrderId, | |||
| dpo.store_id as storeId, | |||
| dpo.ticket_no as ticketNo, | |||
| dpo.TruckLanceCode as truckLanceCode, | |||
| dpo.truck_departure_time as DepartureTime, | |||
| dpo.ShopCode as shopCode, | |||
| dpo.ShopName as shopName, | |||
| dpo.delivery_order_code as deliveryNo, | |||
| dpo.pick_order_code as pickOrderCode, | |||
| po.consoCode as pickOrderConsoCode, | |||
| po.targetDate as pickOrderTargetDate, | |||
| po.status as pickOrderStatus, | |||
| do.orderDate as deliveryDate, | |||
| s.id as shopId, | |||
| s.name as shopNameFromShop, | |||
| CONCAT_WS(', ', s.addr1, s.addr2, s.addr3, s.addr4, s.district) as shopAddress, | |||
| (SELECT COUNT(*) FROM pick_order_line pol WHERE pol.poId = po.id AND pol.deleted = false) as numberOfCartons | |||
| -- ✅ 从 do_pick_order_line 获取所有关联的 pick orders 和 delivery orders | |||
| GROUP_CONCAT(DISTINCT dpol.pick_order_id ORDER BY dpol.pick_order_id) as pickOrderIds, | |||
| GROUP_CONCAT(DISTINCT dpol.pick_order_code ORDER BY dpol.pick_order_id SEPARATOR ', ') as pickOrderCodes, | |||
| GROUP_CONCAT(DISTINCT dpol.do_order_id ORDER BY dpol.do_order_id) as deliveryOrderIds, | |||
| GROUP_CONCAT(DISTINCT dpol.delivery_order_code ORDER BY dpol.do_order_id SEPARATOR ', ') as deliveryNos, | |||
| -- ✅ 获取第一个 pick order 的详细信息(用于兼容性) | |||
| (SELECT po2.consoCode FROM pick_order po2 WHERE po2.id = MIN(dpol.pick_order_id) LIMIT 1) as pickOrderConsoCode, | |||
| (SELECT po2.targetDate FROM pick_order po2 WHERE po2.id = MIN(dpol.pick_order_id) LIMIT 1) as pickOrderTargetDate, | |||
| (SELECT po2.status FROM pick_order po2 WHERE po2.id = MIN(dpol.pick_order_id) LIMIT 1) as pickOrderStatus, | |||
| (SELECT do2.orderDate FROM delivery_order do2 WHERE do2.id = MIN(dpol.do_order_id) LIMIT 1) as deliveryDate, | |||
| COUNT(DISTINCT dpol.pick_order_id) as numberOfPickOrders, | |||
| (SELECT SUM(pol_count.line_count) | |||
| FROM ( | |||
| SELECT po3.id, COUNT(*) as line_count | |||
| FROM pick_order po3 | |||
| JOIN pick_order_line pol3 ON pol3.poId = po3.id AND pol3.deleted = false | |||
| WHERE po3.id IN (SELECT dpol2.pick_order_id FROM do_pick_order_line dpol2 WHERE dpol2.do_pick_order_id = dpo.id AND dpol2.deleted = 0) | |||
| GROUP BY po3.id | |||
| ) pol_count | |||
| ) as numberOfCartons | |||
| FROM do_pick_order dpo | |||
| JOIN pick_order po ON po.id = dpo.pick_order_id | |||
| LEFT JOIN delivery_order do ON do.id = dpo.do_order_id | |||
| INNER JOIN do_pick_order_line dpol ON dpol.do_pick_order_id = dpo.id AND dpol.deleted = 0 | |||
| -- ✅ JOIN pick_order 以检查用户分配 | |||
| INNER JOIN pick_order po ON po.id = dpol.pick_order_id | |||
| LEFT JOIN shop s ON s.id = dpo.shop_id | |||
| WHERE po.assignTo = :userId | |||
| AND po.type = 'do' | |||
| AND po.status IN ('assigned', 'released', 'picking') | |||
| AND po.deleted = false | |||
| AND dpo.deleted = false | |||
| ORDER BY po.targetDate DESC, po.code ASC | |||
| GROUP BY dpo.id, dpo.store_id, dpo.ticket_no, dpo.TruckLanceCode, dpo.truck_departure_time, | |||
| dpo.ShopCode, dpo.ShopName, s.id, s.name, s.addr1, s.addr2, s.addr3, s.addr4, s.district | |||
| ORDER BY MIN(po.targetDate) DESC, MIN(po.code) ASC | |||
| """.trimIndent() | |||
| println("🔍 Executing SQL for FG pick orders by userId: $sql") | |||
| @@ -2817,27 +2910,48 @@ open fun autoAssignAndReleasePickOrderByStoreAndTicket(storeId: String, ticketNo | |||
| println("🔍 Found ${results.size} active FG pick orders for user: $userId") | |||
| // ✅ 添加调试信息 | |||
| results.forEachIndexed { index, row -> | |||
| println("🔍 DEBUG: Result $index:") | |||
| println(" - doPickOrderId: ${row["doPickOrderId"]}") | |||
| println(" - pickOrderIds: ${row["pickOrderIds"]}") | |||
| println(" - numberOfPickOrders: ${row["numberOfPickOrders"]}") | |||
| println(" - ticketNo: ${row["ticketNo"]}") | |||
| } | |||
| val formattedResults = results.map { row -> | |||
| // ✅ 解析 pick order IDs 列表 | |||
| val pickOrderIdsStr = row["pickOrderIds"] as? String ?: "" | |||
| val pickOrderIds = if (pickOrderIdsStr.isNotEmpty()) { | |||
| pickOrderIdsStr.split(",").mapNotNull { it.toLongOrNull() } | |||
| } else { | |||
| emptyList() | |||
| } | |||
| mapOf( | |||
| "pickOrderId" to (row["pickOrderId"] ?: 0L), | |||
| "pickOrderCode" to (row["pickOrderCode"] ?: ""), | |||
| "doPickOrderId" to (row["doPickOrderId"] ?: 0L), | |||
| "pickOrderIds" to pickOrderIds, // ✅ 返回所有 pick order IDs | |||
| "pickOrderId" to (pickOrderIds.firstOrNull() ?: 0L), // ✅ 兼容:返回第一个 | |||
| "pickOrderCodes" to (row["pickOrderCodes"] ?: ""), | |||
| "pickOrderCode" to ((row["pickOrderCodes"] as? String)?.split(", ")?.firstOrNull() ?: ""), // ✅ 兼容 | |||
| "deliveryOrderIds" to (row["deliveryOrderIds"] as? String ?: "").split(",").mapNotNull { it.toLongOrNull() }, | |||
| "deliveryNos" to (row["deliveryNos"] ?: ""), | |||
| "pickOrderConsoCode" to (row["pickOrderConsoCode"] ?: ""), | |||
| "pickOrderTargetDate" to (row["pickOrderTargetDate"]?.toString() ?: ""), | |||
| "pickOrderStatus" to (row["pickOrderStatus"] ?: ""), | |||
| "deliveryOrderId" to (row["deliveryOrderId"] ?: 0L), | |||
| "deliveryNo" to (row["deliveryNo"] ?: ""), | |||
| "deliveryDate" to (row["deliveryDate"]?.toString() ?: ""), | |||
| "shopId" to (row["shopId"] ?: 0L), | |||
| "shopCode" to (row["shopCode"] ?: ""), | |||
| "shopName" to (row["shopName"] ?: row["shopNameFromShop"] ?: ""), | |||
| "shopAddress" to (row["shopAddress"] ?: ""), | |||
| "shopPoNo" to "", | |||
| "numberOfPickOrders" to (row["numberOfPickOrders"] ?: 0), // ✅ 新增:pick order 数量 | |||
| "numberOfCartons" to (row["numberOfCartons"] ?: 0), | |||
| "truckLanceCode" to (row["truckLanceCode"] ?: ""), | |||
| "DepartureTime" to (row["DepartureTime"]?.toString() ?: ""), | |||
| "ticketNo" to (row["ticketNo"] ?: ""), | |||
| "storeId" to (row["storeId"] ?: ""), | |||
| "qrCodeData" to (row["pickOrderId"] ?: 0L) | |||
| "qrCodeData" to (row["doPickOrderId"] ?: 0L) // ✅ 改为 doPickOrderId | |||
| ) | |||
| } | |||
| @@ -3363,303 +3477,258 @@ ORDER BY | |||
| // 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 (NEW 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 基本信息 | |||
| 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 | |||
| 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 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() | |||
| println("🔍 Found do_pick_order ID: $doPickOrderId") | |||
| 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: 获取该 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)) | |||
| println("🔍 Found ${pickOrdersInfo.size} pick orders") | |||
| // ✅ Step 3: 为每个 pick order 获取 lines 和 lots(包括 null stock 的) | |||
| val pickOrders = pickOrdersInfo.map { poInfo -> | |||
| val pickOrderId = (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 pickOrderId)) | |||
| println("🔍 Pick order $pickOrderId 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 pickOrderId, | |||
| "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"], | |||
| "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 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"] | |||
| ) | |||
| // 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 | |||
| ) | |||
| } | |||
| @@ -301,4 +301,5 @@ fun getCompletedDoPickOrders( | |||
| fun getLotDetailsByPickOrderId(@PathVariable pickOrderId: Long): List<Map<String, Any>> { | |||
| return pickOrderService.getLotDetailsByPickOrderId(pickOrderId); | |||
| } | |||
| } | |||
| @@ -209,6 +209,19 @@ existingSuggestions.forEach { existingSugg -> | |||
| 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() | |||
| } | |||
| } | |||
| } | |||
| return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) | |||
| @@ -1480,7 +1493,67 @@ open fun updateSuggestedLotLineId(suggestedPickLotId: Long, newLotLineId: Long): | |||
| } | |||
| } | |||
| // ... existing code ... | |||
| private fun createInsufficientStockIssue( | |||
| pickOrder: PickOrder, | |||
| pickOrderLine: PickOrderLine, | |||
| insufficientQty: BigDecimal | |||
| ) { | |||
| try { | |||
| // ✅ 检查是否已存在相同的 issue(避免重复创建) | |||
| val existingIssues = pickExecutionIssueRepository | |||
| .findByPickOrderLineIdAndDeletedFalse(pickOrderLine.id ?: 0L) | |||
| .filter { | |||
| it.issueCategory == IssueCategory.resuggest_issue && | |||
| it.handleStatus == HandleStatus.pending | |||
| } | |||
| if (existingIssues.isNotEmpty()) { | |||
| println("⏭️ Issue already exists for pick order line ${pickOrderLine.id}, skipping") | |||
| return | |||
| } | |||
| val issue = PickExecutionIssue( | |||
| id = null, | |||
| pickOrderId = pickOrder.id!!, | |||
| pickOrderCode = pickOrder.code ?: "", | |||
| pickOrderCreateDate = pickOrder.created?.toLocalDate(), | |||
| pickExecutionDate = LocalDate.now(), | |||
| doPickOrderId = if (pickOrder.type?.value == "do") pickOrder.deliveryOrder?.id else null, | |||
| joPickOrderId = pickOrder.jobOrder?.id, | |||
| pickOrderLineId = pickOrderLine.id!!, | |||
| issueNo = generateIssueNo(), | |||
| issueCategory = IssueCategory.resuggest_issue, | |||
| itemId = pickOrderLine.item?.id ?: 0L, | |||
| itemCode = pickOrderLine.item?.code, | |||
| itemDescription = pickOrderLine.item?.name, | |||
| lotId = null, | |||
| lotNo = null, | |||
| storeLocation = null, | |||
| requiredQty = pickOrderLine.qty, | |||
| actualPickQty = BigDecimal.ZERO, | |||
| missQty = insufficientQty, | |||
| badItemQty = BigDecimal.ZERO, | |||
| issueRemark = "No inventory available for suggestion (auto-created by system)", | |||
| pickerName = null, | |||
| handleStatus = HandleStatus.pending, | |||
| handleDate = null, | |||
| handledBy = null, | |||
| created = LocalDateTime.now(), | |||
| createdBy = "SYSTEM", | |||
| version = 0, | |||
| modified = LocalDateTime.now(), | |||
| modifiedBy = "SYSTEM", | |||
| deleted = false | |||
| ) | |||
| pickExecutionIssueRepository.save(issue) | |||
| println("✅ Auto-created issue ${issue.issueNo} for insufficient stock (line ${pickOrderLine.id}, qty ${insufficientQty})") | |||
| } catch (e: Exception) { | |||
| println("❌ Error creating insufficient stock issue: ${e.message}") | |||
| e.printStackTrace() | |||
| } | |||
| } | |||
| } | |||