diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderLineRecord.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderLineRecord.kt index c9bc76a..b54bdcb 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderLineRecord.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderLineRecord.kt @@ -21,15 +21,13 @@ open class DoPickOrderLineRecord: BaseEntity() { @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") diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index 37abfe1..48e554f 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt @@ -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 { 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() - - inventoryResults.forEach { row: Map -> - 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() + 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 { - 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 { + 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>() - val params = mutableMapOf() + val fields = mutableListOf>() + val params = mutableMapOf() - 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() + val field = mutableMapOf() - 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 { + 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() + val fields = mutableListOf>() + for (info in cartonLabelInfo) { + val field = mutableMapOf() + } - //Carton Labels - open fun exportDNLabels(request: ExportDNLabelsRequest): Map{ - 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() - val fields = mutableListOf>() - for (info in cartonLabelInfo) { - val field = mutableMapOf() - } + 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() + 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() - 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() + 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()) + } } - -} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderAssignmentService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderAssignmentService.kt new file mode 100644 index 0000000..44c2141 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderAssignmentService.kt @@ -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 // 出错时不过滤 + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderCompletionService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderCompletionService.kt new file mode 100644 index 0000000..12a5d99 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderCompletionService.kt @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt new file mode 100644 index 0000000..7f3c7cd --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt index 0181121..5210974 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt @@ -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 { 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 } ) ) } diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt index 4185a06..8bac6ad 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt @@ -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, 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() + + // 第一步:发布所有 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) { + 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, diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt index f92f6be..0b800dc 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt @@ -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, diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt index 31e498a..042a643 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt @@ -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? ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt index c4495d6..f418736 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt @@ -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(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 { - 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>() as Any? + "fgInfo" to null, + "pickOrders" to emptyList() ) } - // 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>() 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 ) } diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt index 3078bba..6334370 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt @@ -301,4 +301,5 @@ fun getCompletedDoPickOrders( fun getLotDetailsByPickOrderId(@PathVariable pickOrderId: Long): List> { return pickOrderService.getLotDetailsByPickOrderId(pickOrderId); } + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index 1c5bc45..c147254 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt @@ -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() + } +} }