| @@ -21,15 +21,13 @@ open class DoPickOrderLineRecord: BaseEntity<Long>() { | |||||
| @Column(name = "do_pick_order_id", length = 100) | @Column(name = "do_pick_order_id", length = 100) | ||||
| open var doPickOrderId: Long? = null | open var doPickOrderId: Long? = null | ||||
| @JoinColumn(name = "pick_order_id") | |||||
| @Column(name = "pick_order_id") // ✅ 正确:普通列 | |||||
| open var pickOrderId: Long? = null | open var pickOrderId: Long? = null | ||||
| @JoinColumn(name = "do_order_id") | |||||
| @Column(name = "do_order_id") // ✅ 正确:普通列 | |||||
| open var doOrderId: Long? = null | open var doOrderId: Long? = null | ||||
| @JoinColumn(name = "pick_order_code") | |||||
| @Column(name = "pick_order_code") // ✅ 正确:普通列 | |||||
| open var pickOrderCode: String? = null | open var pickOrderCode: String? = null | ||||
| @Column(name = "delivery_order_code") | @Column(name = "delivery_order_code") | ||||
| @@ -1,6 +1,7 @@ | |||||
| package com.ffii.fpsms.modules.deliveryOrder.service | package com.ffii.fpsms.modules.deliveryOrder.service | ||||
| import com.ffii.core.utils.PdfUtils | import com.ffii.core.utils.PdfUtils | ||||
| import org.springframework.context.annotation.Lazy | |||||
| import com.ffii.core.utils.ZebraPrinterUtil | import com.ffii.core.utils.ZebraPrinterUtil | ||||
| import com.ffii.fpsms.m18.entity.M18DataLogRepository | import com.ffii.fpsms.m18.entity.M18DataLogRepository | ||||
| import com.ffii.fpsms.m18.service.M18DataLogService | 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.purchaseOrder.entity.PurchaseOrderRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository | import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository | ||||
| import com.ffii.fpsms.modules.stock.service.InventoryLotService | import com.ffii.fpsms.modules.stock.service.InventoryLotService | ||||
| import net.sf.jasperreports.engine.JasperPrintManager | import net.sf.jasperreports.engine.JasperPrintManager | ||||
| import net.sf.jasperreports.engine.JRPrintPage | import net.sf.jasperreports.engine.JRPrintPage | ||||
| import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository | import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository | ||||
| import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService // ✅ 添加这行 | 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 | @Service | ||||
| open class DeliveryOrderService( | open class DeliveryOrderService( | ||||
| private val deliveryOrderRepository: DeliveryOrderRepository, | private val deliveryOrderRepository: DeliveryOrderRepository, | ||||
| @@ -80,7 +84,7 @@ open class DeliveryOrderService( | |||||
| private val userService: UserService, | private val userService: UserService, | ||||
| private val userRepository: UserRepository, | private val userRepository: UserRepository, | ||||
| private val pickOrderService: PickOrderService, | private val pickOrderService: PickOrderService, | ||||
| private val doPickOrderService: DoPickOrderService, | |||||
| @Lazy private val doPickOrderService: DoPickOrderService, | |||||
| private val truckRepository: TruckRepository, | private val truckRepository: TruckRepository, | ||||
| private val pickOrderRepository: PickOrderRepository, | private val pickOrderRepository: PickOrderRepository, | ||||
| private val suggestedPickLotService: SuggestedPickLotService, | private val suggestedPickLotService: SuggestedPickLotService, | ||||
| @@ -95,6 +99,7 @@ open class DeliveryOrderService( | |||||
| private val suggestedPickLotRepository: SuggestPickLotRepository, | private val suggestedPickLotRepository: SuggestPickLotRepository, | ||||
| private val inventoryLotRepository: InventoryLotRepository, | private val inventoryLotRepository: InventoryLotRepository, | ||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository, | |||||
| ) { | ) { | ||||
| open fun findByM18DataLogId(m18DataLogId: Long): DeliveryOrder? { | open fun findByM18DataLogId(m18DataLogId: Long): DeliveryOrder? { | ||||
| @@ -274,6 +279,7 @@ open class DeliveryOrderService( | |||||
| open fun searchCodeAndShopName(code: String?, shopName: String?): List<DeliveryOrderInfo> { | open fun searchCodeAndShopName(code: String?, shopName: String?): List<DeliveryOrderInfo> { | ||||
| return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndDeletedIsFalse(code, shopName); | return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndDeletedIsFalse(code, shopName); | ||||
| } | } | ||||
| open fun getWarehouseOrderByItemId(itemId: Long): Int? { | open fun getWarehouseOrderByItemId(itemId: Long): Int? { | ||||
| val inventoryLots = inventoryLotService.findByItemId(itemId) | val inventoryLots = inventoryLotService.findByItemId(itemId) | ||||
| if (inventoryLots.isNotEmpty()) { | if (inventoryLots.isNotEmpty()) { | ||||
| @@ -297,7 +303,7 @@ open class DeliveryOrderService( | |||||
| } | } | ||||
| return null | return null | ||||
| } | } | ||||
| // ✅ 新增方法2:获取 warehouse 的 code 字段(用于显示路由) | // ✅ 新增方法2:获取 warehouse 的 code 字段(用于显示路由) | ||||
| open fun getWarehouseCodeByItemId(itemId: Long): String? { | open fun getWarehouseCodeByItemId(itemId: Long): String? { | ||||
| val inventoryLots = inventoryLotService.findByItemId(itemId) | val inventoryLots = inventoryLotService.findByItemId(itemId) | ||||
| @@ -402,12 +408,12 @@ open class DeliveryOrderService( | |||||
| @Transactional(rollbackFor = [Exception::class]) | @Transactional(rollbackFor = [Exception::class]) | ||||
| open fun releaseDeliveryOrder(request: ReleaseDoRequest): MessageResponse { | open fun releaseDeliveryOrder(request: ReleaseDoRequest): MessageResponse { | ||||
| println("�� DEBUG: Starting releaseDeliveryOrder for DO ID: ${request.id}, User ID: ${request.userId}") | println("�� DEBUG: Starting releaseDeliveryOrder for DO ID: ${request.id}, User ID: ${request.userId}") | ||||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | |||||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | |||||
| ?: throw NoSuchElementException("Delivery Order not found") | ?: throw NoSuchElementException("Delivery Order not found") | ||||
| println("�� DEBUG: Found delivery order - ID: ${deliveryOrder.id}, Shop: ${deliveryOrder.shop?.code}, Status: ${deliveryOrder.status}") | println("�� DEBUG: Found delivery order - ID: ${deliveryOrder.id}, Shop: ${deliveryOrder.shop?.code}, Status: ${deliveryOrder.status}") | ||||
| deliveryOrder.apply { | deliveryOrder.apply { | ||||
| status = DeliveryOrderStatus.PENDING | status = DeliveryOrderStatus.PENDING | ||||
| } | } | ||||
| @@ -429,57 +435,51 @@ open class DeliveryOrderService( | |||||
| val createdPickOrder = pickOrderService.create(po) | val createdPickOrder = pickOrderService.create(po) | ||||
| println("🔍 DEBUG: Created pick order - ID: ${createdPickOrder.id}") | println("🔍 DEBUG: Created pick order - ID: ${createdPickOrder.id}") | ||||
| val consoCode = pickOrderService.assignConsoCode() | val consoCode = pickOrderService.assignConsoCode() | ||||
| val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null) | val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null) | ||||
| if (pickOrderEntity != null) { | if (pickOrderEntity != null) { | ||||
| pickOrderEntity.consoCode = consoCode | pickOrderEntity.consoCode = consoCode | ||||
| pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED | pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED | ||||
| pickOrderRepository.saveAndFlush(pickOrderEntity) | pickOrderRepository.saveAndFlush(pickOrderEntity) | ||||
| println("�� DEBUG: Assigned consoCode $consoCode to pick order ${createdPickOrder.id}") | println("�� DEBUG: Assigned consoCode $consoCode to pick order ${createdPickOrder.id}") | ||||
| // ✅ Debug: Check pick order lines | // ✅ Debug: Check pick order lines | ||||
| println("�� DEBUG: Pick order has ${pickOrderEntity.pickOrderLines?.size ?: 0} pick order lines") | println("�� DEBUG: Pick order has ${pickOrderEntity.pickOrderLines?.size ?: 0} pick order lines") | ||||
| pickOrderEntity.pickOrderLines?.forEach { line -> | pickOrderEntity.pickOrderLines?.forEach { line -> | ||||
| println("🔍 DEBUG: Pick order line - Item ID: ${line.item?.id}, Qty: ${line.qty}") | println("🔍 DEBUG: Pick order line - Item ID: ${line.item?.id}, Qty: ${line.qty}") | ||||
| } | } | ||||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | 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( | val suggestions = suggestedPickLotService.suggestionForPickOrderLines( | ||||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | 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) | val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | ||||
| println("🔍 DEBUG: Saved ${saveSuggestedPickLots.size} suggested pick lots") | 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 | // ✅ Hold inventory quantities | ||||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | ||||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | ||||
| ) | ) | ||||
| saveSuggestedPickLots.forEach { lot -> | saveSuggestedPickLots.forEach { lot -> | ||||
| if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) { | if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) { | ||||
| val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine) | val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine) | ||||
| if (lineIndex >= 0) { | if (lineIndex >= 0) { | ||||
| inventoryLotLines[lineIndex].holdQty = | |||||
| inventoryLotLines[lineIndex].holdQty = | |||||
| (inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO) | (inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO) | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| inventoryLotLineRepository.saveAll(inventoryLotLines) | inventoryLotLineRepository.saveAll(inventoryLotLines) | ||||
| // ✅ Create stock out record and pre-create stock out lines | // ✅ Create stock out record and pre-create stock out lines | ||||
| val stockOut = StockOut().apply { | val stockOut = StockOut().apply { | ||||
| this.type = "job" | this.type = "job" | ||||
| @@ -488,17 +488,18 @@ open class DeliveryOrderService( | |||||
| this.handler = request.userId | this.handler = request.userId | ||||
| } | } | ||||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | ||||
| // ✅ Pre-create stock out lines for suggested lots | // ✅ Pre-create stock out lines for suggested lots | ||||
| saveSuggestedPickLots.forEach { lot -> | saveSuggestedPickLots.forEach { lot -> | ||||
| val polId = lot.pickOrderLine?.id | val polId = lot.pickOrderLine?.id | ||||
| val illId = lot.suggestedLotLine?.id | val illId = lot.suggestedLotLine?.id | ||||
| if (polId != null && illId != null) { | if (polId != null && illId != null) { | ||||
| val existingLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId) | |||||
| val existingLines = | |||||
| stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId) | |||||
| if (existingLines.isEmpty()) { | if (existingLines.isEmpty()) { | ||||
| val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null) | val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null) | ||||
| val inventoryLotLine = inventoryLotLineRepository.findById(illId).orElse(null) | val inventoryLotLine = inventoryLotLineRepository.findById(illId).orElse(null) | ||||
| if (pickOrderLine != null && inventoryLotLine != null) { | if (pickOrderLine != null && inventoryLotLine != null) { | ||||
| val line = StockOutLine().apply { | val line = StockOutLine().apply { | ||||
| this.stockOut = savedStockOut | this.stockOut = savedStockOut | ||||
| @@ -516,332 +517,537 @@ open class DeliveryOrderService( | |||||
| } | } | ||||
| // ✅ CREATE do_pick_order_record entries | // ✅ CREATE do_pick_order_record entries | ||||
| // 第 471-555 行附近 - 修复创建逻辑 | |||||
| // 第 471-555 行附近 - 修复创建逻辑 | |||||
| // ✅ CREATE do_pick_order_record entries | // ✅ CREATE do_pick_order_record entries | ||||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||||
| val datePrefix = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) | |||||
| println("🔍 DEBUG: Target date: $targetDate, Date prefix: $datePrefix") | |||||
| val truck = deliveryOrder.shop?.id?.let { shopId -> | |||||
| println("🔍 DEBUG: Looking for truck with shop ID: $shopId") | |||||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||||
| println("🔍 DEBUG: Found ${trucks.size} trucks for shop $shopId") | |||||
| if (trucks.size <= 1) { | |||||
| // 如果只有一个或没有 truck,直接返回 | |||||
| return@let trucks.firstOrNull() | |||||
| } | |||||
| // ✅ 分析 DO order lines 中的 items 分布 | |||||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | |||||
| println("🔍 DEBUG: Analyzing ${itemIds.size} unique items in DO order lines") | |||||
| // 使用 SQL 查询统计每个楼层的库存数量 | |||||
| val inventoryQuery = """ | |||||
| SELECT w.store_id as floor, COUNT(*) as inventory_count | |||||
| FROM inventory_lot_line ill | |||||
| JOIN inventory_lot il ON il.id = ill.inventoryLotId | |||||
| JOIN warehouse w ON w.id = ill.warehouseId | |||||
| WHERE il.itemId IN (${itemIds.joinToString(",")}) | |||||
| AND ill.deleted = false | |||||
| AND il.deleted = false | |||||
| AND w.deleted = false | |||||
| AND ill.inQty > ill.outQty + COALESCE(ill.holdQty, 0) | |||||
| GROUP BY w.store_id | |||||
| """.trimIndent() | |||||
| val inventoryResults = jdbcDao.queryForList(inventoryQuery) | |||||
| val floorInventoryCount = mutableMapOf<String, Int>() | |||||
| inventoryResults.forEach { row: Map<String, Any> -> | |||||
| val floor = row["floor"] as? String ?: "Other" | |||||
| val count = (row["inventory_count"] as? Number)?.toInt() ?: 0 | |||||
| floorInventoryCount[floor] = count | |||||
| } | |||||
| println("🔍 DEBUG: Floor inventory distribution: $floorInventoryCount") | |||||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||||
| val datePrefix = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) | |||||
| // 决定使用哪个楼层 | |||||
| val preferredFloor = when { | |||||
| floorInventoryCount["2F"] ?: 0 > floorInventoryCount["4F"] ?: 0 -> "2F" | |||||
| floorInventoryCount["4F"] ?: 0 > floorInventoryCount["2F"] ?: 0 -> "4F" | |||||
| else -> "2F" // 默认使用 2F | |||||
| } | |||||
| println("🔍 DEBUG: Preferred floor based on inventory: $preferredFloor") | |||||
| val preferredStoreId = when (preferredFloor) { | |||||
| "2F" -> 2 | |||||
| "4F" -> 4 | |||||
| else -> 2 | |||||
| } | |||||
| val selectedTruck = if (trucks.size > 1) { | |||||
| // Multiple trucks: prefer matching preferredStoreId, then earliest departure | |||||
| trucks.find { it.storeId == preferredStoreId } | |||||
| ?: trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||||
| } else { | |||||
| trucks.firstOrNull() | |||||
| } | |||||
| println("🔍 DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}, DepartureTime=${selectedTruck?.departureTime}") | |||||
| selectedTruck | |||||
| } | |||||
| println("🔍 DEBUG: Target date: $targetDate, Date prefix: $datePrefix") | |||||
| val truck = deliveryOrder.shop?.id?.let { shopId -> | |||||
| println("🔍 DEBUG: Looking for truck with shop ID: $shopId") | |||||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||||
| println("🔍 DEBUG: Found ${trucks.size} trucks for shop $shopId") | |||||
| // ✅ 移除提前返回,总是分析 items 分布 | |||||
| // 分析 DO order lines 中的 items 分布 | |||||
| // ✅ 分析 items 来确定 storeId | |||||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | |||||
| val inventoryQuery = """ | |||||
| SELECT | |||||
| w.store_id as floor, | |||||
| COUNT(DISTINCT il.itemId) as item_count | |||||
| FROM inventory_lot il | |||||
| INNER JOIN inventory i ON i.itemId = il.itemId AND i.deleted = 0 AND i.onHandQty > 0 | |||||
| INNER JOIN inventory_lot_line ill ON ill.inventoryLotId = il.id AND ill.deleted = 0 | |||||
| INNER JOIN warehouse w ON w.id = ill.warehouseId AND w.deleted = 0 AND w.store_id IN ('2F', '4F') | |||||
| WHERE il.itemId IN (${itemIds.joinToString(",")}) AND il.deleted = 0 | |||||
| GROUP BY w.store_id | |||||
| """.trimIndent() | |||||
| val inventoryResults = jdbcDao.queryForList(inventoryQuery) | |||||
| val floorItemCount = mutableMapOf<String, Int>() | |||||
| inventoryResults.forEach { row -> | |||||
| floorItemCount[row["floor"] as? String ?: "Other"] = (row["item_count"] as? Number)?.toInt() ?: 0 | |||||
| } | |||||
| // ✅ 根据 truck 的 Store_id 字段确定 storeId | |||||
| val storeId = when (truck?.storeId) { | |||||
| 4 -> "4/F" | |||||
| 2 -> "2/F" | |||||
| else -> "2/F" // 默认值 | |||||
| } | |||||
| val loadingSequence = truck?.loadingSequence ?: 999 | |||||
| // ✅ 每个 pick order 只创建一条 DoPickOrder | |||||
| val doPickOrder = DoPickOrder( | |||||
| storeId = storeId, | |||||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||||
| ticketStatus = DoPickOrderStatus.pending, | |||||
| truckId = truck?.id, | |||||
| doOrderId = deliveryOrder.id, | |||||
| pickOrderId = createdPickOrder.id, | |||||
| truckDepartureTime = truck?.departureTime, | |||||
| shopId = deliveryOrder.shop?.id, | |||||
| handledBy = null, | |||||
| pickOrderCode = createdPickOrder.code, | |||||
| deliveryOrderCode = deliveryOrder.code, | |||||
| loadingSequence = loadingSequence, | |||||
| ticketReleaseTime = null, | |||||
| // ✅ 填充新增字段 | |||||
| truckLanceCode = truck?.truckLanceCode, | |||||
| shopCode = deliveryOrder.shop?.code, | |||||
| shopName = deliveryOrder.shop?.name, | |||||
| requiredDeliveryDate = targetDate | |||||
| ) | |||||
| println("🔍 DEBUG: Creating DoPickOrder - Store: $storeId, Ticket: , Truck: ${truck?.id}") | |||||
| val savedDoPickOrder = doPickOrderService.save(doPickOrder) | |||||
| println("🔍 DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}") | |||||
| return MessageResponse( | |||||
| id = deliveryOrder.id, | |||||
| code = deliveryOrder.code, | |||||
| name = deliveryOrder.shop?.name, | |||||
| type = null, | |||||
| message = null, | |||||
| errorPosition = null, | |||||
| entity = mapOf("status" to deliveryOrder.status?.value) | |||||
| ) | |||||
| // ... existing code ... | |||||
| println("🔍 DEBUG: Floor item count distribution: $floorItemCount") | |||||
| println("🔍 DEBUG: Total items: ${itemIds.size}, Items on 4F: ${floorItemCount["4F"] ?: 0}") | |||||
| // ✅ 新逻辑:只有所有 items 都在 4F,才算 4F,否则算 2F | |||||
| val preferredFloor = if ((floorItemCount["4F"] ?: 0) == itemIds.size && (floorItemCount["2F"] ?: 0) == 0) { | |||||
| "4F" // 所有 items 都在 4F | |||||
| } else { | |||||
| "2F" // 只要有任何 item 不在 4F,就算 2F | |||||
| } | |||||
| println("🔍 DEBUG: Preferred floor: $preferredFloor (All items on 4F: ${preferredFloor == "4F"})") | |||||
| // ✅ 查找 truck | |||||
| val truck = deliveryOrder.shop?.id?.let { shopId -> | |||||
| println("🔍 DEBUG: Looking for truck with shop ID: $shopId") | |||||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||||
| println("🔍 DEBUG: Found ${trucks.size} trucks for shop $shopId") | |||||
| val preferredStoreId = when (preferredFloor) { | |||||
| "2F" -> 2 | |||||
| "4F" -> 4 | |||||
| else -> 2 | |||||
| } | |||||
| // 优先选择匹配的 truck | |||||
| val selectedTruck = if (trucks.size > 1) { | |||||
| trucks.find { it.storeId == preferredStoreId } | |||||
| ?: trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||||
| } else { | |||||
| trucks.firstOrNull() | |||||
| } | |||||
| println("🔍 DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}") | |||||
| selectedTruck | |||||
| } | |||||
| // ✅ 检查 truck 和 preferredFloor 是否匹配 | |||||
| val truckStoreId = truck?.storeId | |||||
| val expectedStoreId = when (preferredFloor) { | |||||
| "2F" -> 2 | |||||
| "4F" -> 4 | |||||
| else -> 2 | |||||
| } | |||||
| if (truck == null || truckStoreId != expectedStoreId) { | |||||
| val errorMsg = | |||||
| "Items preferredFloor ($preferredFloor) does not match available truck (Truck Store: $truckStoreId). Skipping DO ${deliveryOrder.id}." | |||||
| println("⚠️ $errorMsg") | |||||
| throw IllegalStateException(errorMsg) // 或返回错误响应 | |||||
| } | |||||
| println("✅ DEBUG: Truck matches preferred floor - Truck Store: $truckStoreId, Preferred: $preferredFloor") | |||||
| // ✅ storeId 基于 items 的 preferredFloor | |||||
| val storeId = "$preferredFloor/F" | |||||
| val loadingSequence = truck.loadingSequence ?: 999 | |||||
| println("🔍 DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${truck.id}") | |||||
| val doPickOrder = DoPickOrder( | |||||
| storeId = storeId, | |||||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||||
| ticketStatus = DoPickOrderStatus.pending, | |||||
| truckId = truck.id, | |||||
| doOrderId = deliveryOrder.id, | |||||
| pickOrderId = createdPickOrder.id, | |||||
| truckDepartureTime = truck.departureTime, | |||||
| shopId = deliveryOrder.shop?.id, | |||||
| handledBy = null, | |||||
| pickOrderCode = createdPickOrder.code, | |||||
| deliveryOrderCode = deliveryOrder.code, | |||||
| loadingSequence = loadingSequence, | |||||
| ticketReleaseTime = null, | |||||
| truckLanceCode = truck.truckLanceCode, | |||||
| shopCode = deliveryOrder.shop?.code, | |||||
| shopName = deliveryOrder.shop?.name, | |||||
| requiredDeliveryDate = targetDate | |||||
| ) | |||||
| val savedDoPickOrder = doPickOrderService.save(doPickOrder) | |||||
| println("🔍 DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}") | |||||
| truck | |||||
| } | |||||
| return MessageResponse( | |||||
| id = deliveryOrder.id, | |||||
| code = deliveryOrder.code, | |||||
| name = deliveryOrder.shop?.name, | |||||
| type = null, | |||||
| message = null, | |||||
| errorPosition = null, | |||||
| entity = mapOf("status" to deliveryOrder.status?.value) | |||||
| ) | |||||
| } | } | ||||
| open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String { | |||||
| try { | |||||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId) | |||||
| val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } | |||||
| val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||||
| val lotNumbers = suggestedPickLots | |||||
| .filter { it.pickOrderLine?.item?.id == itemId } | |||||
| .mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo } | |||||
| .distinct() | |||||
| return lotNumbers.joinToString(", ") | |||||
| } catch (e: Exception) { | |||||
| println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}") | |||||
| return "" | |||||
| open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String { | |||||
| try { | |||||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId) | |||||
| val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } | |||||
| val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||||
| val lotNumbers = suggestedPickLots | |||||
| .filter { it.pickOrderLine?.item?.id == itemId } | |||||
| .mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo } | |||||
| .distinct() | |||||
| return lotNumbers.joinToString(", ") | |||||
| } catch (e: Exception) { | |||||
| println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}") | |||||
| return "" | |||||
| } | |||||
| } | } | ||||
| } | |||||
| //Delivery Note | |||||
| @Throws(IOException::class) | |||||
| @Transactional | |||||
| open fun exportDeliveryNote(request: ExportDeliveryNoteRequest): Map<String, Any> { | |||||
| val DELIVERYNOTE_PDF = "DeliveryNote/DeliveryNotePDF.jrxml" | |||||
| val resource = ClassPathResource(DELIVERYNOTE_PDF) | |||||
| if(!resource.exists()){ | |||||
| throw FileNotFoundException("Report file not fount: $DELIVERYNOTE_PDF") | |||||
| } | |||||
| val inputStream = resource.inputStream | |||||
| val deliveryNote = JasperCompileManager.compileReport(inputStream) | |||||
| val deliveryNoteInfo = deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||||
| //Delivery Note | |||||
| @Throws(IOException::class) | |||||
| @Transactional | |||||
| open fun exportDeliveryNote(request: ExportDeliveryNoteRequest): Map<String, Any> { | |||||
| val DELIVERYNOTE_PDF = "DeliveryNote/DeliveryNotePDF.jrxml" | |||||
| val resource = ClassPathResource(DELIVERYNOTE_PDF) | |||||
| if (!resource.exists()) { | |||||
| throw FileNotFoundException("Report file not fount: $DELIVERYNOTE_PDF") | |||||
| } | |||||
| val inputStream = resource.inputStream | |||||
| val deliveryNote = JasperCompileManager.compileReport(inputStream) | |||||
| val deliveryNoteInfo = | |||||
| deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||||
| val params = mutableMapOf<String, Any>() | |||||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||||
| val params = mutableMapOf<String, Any>() | |||||
| val deliveryOrderEntity = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.deliveryOrderIds) | |||||
| val selectedTruckNo = deliveryOrderEntity?.shop?.id?.let { shopId -> | |||||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||||
| trucks.firstOrNull()?.truckLanceCode | |||||
| } ?: "" | |||||
| val deliveryOrderEntity = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.deliveryOrderIds) | |||||
| val selectedTruckNo = deliveryOrderEntity?.shop?.id?.let { shopId -> | |||||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||||
| trucks.firstOrNull()?.truckLanceCode | |||||
| } ?: "" | |||||
| val selectedPickOrder = pickOrderRepository.findById(request.pickOrderIds).orElse(null) | |||||
| val selectedPickOrder = pickOrderRepository.findById(request.pickOrderIds).orElse(null) | |||||
| for (info in deliveryNoteInfo) { | |||||
| val sortedLines = info.deliveryOrderLines.sortedBy { line -> | |||||
| line.itemId?.let { itemId -> | |||||
| getWarehouseOrderByItemId(itemId) // ✅ 改用 warehouse order | |||||
| } ?: Int.MAX_VALUE | |||||
| } | |||||
| for (info in deliveryNoteInfo) { | |||||
| val sortedLines = info.deliveryOrderLines.sortedBy { line -> | |||||
| line.itemId?.let { itemId -> | |||||
| getWarehouseOrderByItemId(itemId) // ✅ 改用 warehouse order | |||||
| } ?: Int.MAX_VALUE | |||||
| } | |||||
| sortedLines.forEachIndexed { index, line -> | |||||
| sortedLines.forEachIndexed { index, line -> | |||||
| val field = mutableMapOf<String, Any>() | |||||
| val field = mutableMapOf<String, Any>() | |||||
| field["sequenceNumber"] = (index + 1).toString() | |||||
| field["itemNo"] = line.itemNo | |||||
| field["itemName"] = line.itemName ?:"" | |||||
| field["uom"] = line.uom ?:"" | |||||
| field["qty"] = line.qty.toString() | |||||
| field["shortName"] = line.uomShortDesc ?:"" | |||||
| field["sequenceNumber"] = (index + 1).toString() | |||||
| field["itemNo"] = line.itemNo | |||||
| field["itemName"] = line.itemName ?: "" | |||||
| field["uom"] = line.uom ?: "" | |||||
| field["qty"] = line.qty.toString() | |||||
| field["shortName"] = line.uomShortDesc ?: "" | |||||
| val route = line.itemId?.let { itemId -> | |||||
| getWarehouseCodeByItemId(itemId) // ✅ 使用新方法 | |||||
| } ?: "" | |||||
| field["route"] = route | |||||
| val route = line.itemId?.let { itemId -> | |||||
| getWarehouseCodeByItemId(itemId) // ✅ 使用新方法 | |||||
| } ?: "" | |||||
| field["route"] = route | |||||
| val lotNo = line.itemId?.let { itemId -> | |||||
| getLotNumbersForPickOrderByItemId(itemId, request.pickOrderIds) | |||||
| } ?: "" | |||||
| field["lotNo"] = lotNo | |||||
| val lotNo = line.itemId?.let { itemId -> | |||||
| getLotNumbersForPickOrderByItemId(itemId, request.pickOrderIds) | |||||
| } ?: "" | |||||
| field["lotNo"] = lotNo | |||||
| fields.add(field) | |||||
| fields.add(field) | |||||
| } | |||||
| } | } | ||||
| } | |||||
| if(request.isDraft){ | |||||
| params["dnTitle"] = "送貨單(初稿)" | |||||
| params["colQty"] = "所需數量" | |||||
| params["totalCartonTitle"] = "" | |||||
| } | |||||
| else{ | |||||
| params["dnTitle"] = "送貨單" | |||||
| params["colQty"] = "數量" | |||||
| params["totalCartonTitle"] = "總箱數:" | |||||
| } | |||||
| if (request.isDraft) { | |||||
| params["dnTitle"] = "送貨單(初稿)" | |||||
| params["colQty"] = "所需數量" | |||||
| params["totalCartonTitle"] = "" | |||||
| } else { | |||||
| params["dnTitle"] = "送貨單" | |||||
| params["colQty"] = "數量" | |||||
| params["totalCartonTitle"] = "總箱數:" | |||||
| } | |||||
| params["numOfCarton"] = request.numOfCarton.toString() | |||||
| if(params["numOfCarton"] == "0"){ | |||||
| params["numOfCarton"] = "" | |||||
| } | |||||
| params["numOfCarton"] = request.numOfCarton.toString() | |||||
| if (params["numOfCarton"] == "0") { | |||||
| params["numOfCarton"] = "" | |||||
| } | |||||
| params["deliveryOrderCode"] = deliveryNoteInfo[0].code | |||||
| params["shopName"] = deliveryNoteInfo[0].shopName ?: "" | |||||
| params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: "" | |||||
| params["deliveryDate"] = deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||||
| params["truckNo"] = selectedTruckNo | |||||
| params["ShopPurchaseOrderNo"] = deliveryNoteInfo[0].code | |||||
| params["FGPickOrderNo"] = selectedPickOrder?.code ?: "" | |||||
| params["deliveryOrderCode"] = deliveryNoteInfo[0].code | |||||
| params["shopName"] = deliveryNoteInfo[0].shopName ?: "" | |||||
| params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: "" | |||||
| params["deliveryDate"] = | |||||
| deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||||
| params["truckNo"] = selectedTruckNo | |||||
| params["ShopPurchaseOrderNo"] = deliveryNoteInfo[0].code | |||||
| params["FGPickOrderNo"] = selectedPickOrder?.code ?: "" | |||||
| return mapOf( | return mapOf( | ||||
| "report" to PdfUtils.fillReport(deliveryNote, fields, params), | |||||
| "filename" to deliveryNoteInfo[0].code | |||||
| ) | |||||
| } | |||||
| "report" to PdfUtils.fillReport(deliveryNote, fields, params), | |||||
| "filename" to deliveryNoteInfo[0].code | |||||
| ) | |||||
| } | |||||
| //Print Delivery Note | |||||
| @Transactional | |||||
| open fun printDeliveryNote(request: PrintDeliveryNoteRequest){ | |||||
| //val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||||
| val pdf = exportDeliveryNote( | |||||
| ExportDeliveryNoteRequest( | |||||
| deliveryOrderIds = request.deliveryOrderId, | |||||
| numOfCarton = request.numOfCarton, | |||||
| isDraft = request.isDraft, | |||||
| pickOrderIds = request.pickOrderId | |||||
| //Print Delivery Note | |||||
| @Transactional | |||||
| open fun printDeliveryNote(request: PrintDeliveryNoteRequest) { | |||||
| //val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||||
| val pdf = exportDeliveryNote( | |||||
| ExportDeliveryNoteRequest( | |||||
| deliveryOrderIds = request.deliveryOrderId, | |||||
| numOfCarton = request.numOfCarton, | |||||
| isDraft = request.isDraft, | |||||
| pickOrderIds = request.pickOrderId | |||||
| ) | |||||
| ) | ) | ||||
| ) | |||||
| val jasperPrint = pdf["report"] as JasperPrint | |||||
| val jasperPrint = pdf["report"] as JasperPrint | |||||
| val tempPdfFile = File.createTempFile("print_job_",".pdf") | |||||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||||
| try{ | |||||
| JasperExportManager.exportReportToPdfFile(jasperPrint,tempPdfFile.absolutePath) | |||||
| try { | |||||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||||
| //val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||||
| //printer.ip?.let { ip -> printer.port?.let { port -> | |||||
| // ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED) | |||||
| //}} | |||||
| } finally { | |||||
| //tempPdfFile.delete() | |||||
| } | |||||
| //val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||||
| //printer.ip?.let { ip -> printer.port?.let { port -> | |||||
| // ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED) | |||||
| //}} | |||||
| } finally { | |||||
| //tempPdfFile.delete() | |||||
| } | } | ||||
| } | |||||
| //Carton Labels | |||||
| open fun exportDNLabels(request: ExportDNLabelsRequest): Map<String, Any> { | |||||
| val DNLABELS_PDF = "DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml" | |||||
| val resource = ClassPathResource(DNLABELS_PDF) | |||||
| if (!resource.exists()) { | |||||
| throw FileNotFoundException("Label file not found: $DNLABELS_PDF") | |||||
| } | |||||
| val inputStream = resource.inputStream | |||||
| val cartonLabel = JasperCompileManager.compileReport(inputStream) | |||||
| val cartonLabelInfo = | |||||
| deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||||
| val params = mutableMapOf<String, Any>() | |||||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||||
| for (info in cartonLabelInfo) { | |||||
| val field = mutableMapOf<String, Any>() | |||||
| } | |||||
| //Carton Labels | |||||
| open fun exportDNLabels(request: ExportDNLabelsRequest): Map<String, Any>{ | |||||
| val DNLABELS_PDF = "DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml" | |||||
| val resource = ClassPathResource(DNLABELS_PDF) | |||||
| if(!resource.exists()){ | |||||
| throw FileNotFoundException("Label file not found: $DNLABELS_PDF") | |||||
| } | |||||
| val inputStream = resource.inputStream | |||||
| val cartonLabel = JasperCompileManager.compileReport(inputStream) | |||||
| val cartonLabelInfo = deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||||
| val params = mutableMapOf<String, Any>() | |||||
| val fields = mutableListOf<MutableMap<String ,Any>>() | |||||
| for (info in cartonLabelInfo) { | |||||
| val field = mutableMapOf<String, Any>() | |||||
| } | |||||
| params["shopPurchaseOrderNo"] = cartonLabelInfo[0].code | |||||
| params["deliveryOrderCode"] = cartonLabelInfo[0].code | |||||
| params["shopAddress"] = cartonLabelInfo[0].shopAddress ?: "" | |||||
| params["shopName"] = cartonLabelInfo[0].shopName ?: "" | |||||
| params["shopPurchaseOrderNo"] = cartonLabelInfo[0].code | |||||
| params["deliveryOrderCode"] = cartonLabelInfo[0].code | |||||
| params["shopAddress"] = cartonLabelInfo[0].shopAddress?: "" | |||||
| params["shopName"] = cartonLabelInfo[0].shopName?: "" | |||||
| for (cartonNumber in 1..request.numOfCarton) { | |||||
| val field = mutableMapOf<String, Any>() | |||||
| fields.add(field) | |||||
| } | |||||
| return mapOf( | |||||
| "report" to PdfUtils.fillReport(cartonLabel, fields, params), | |||||
| "filename" to "${cartonLabelInfo[0].code}_carton_labels" | |||||
| ) | |||||
| for(cartonNumber in 1..request.numOfCarton){ | |||||
| val field = mutableMapOf<String, Any>() | |||||
| fields.add(field) | |||||
| } | } | ||||
| return mapOf( | |||||
| "report" to PdfUtils.fillReport(cartonLabel, fields, params), | |||||
| "filename" to "${cartonLabelInfo[0].code}_carton_labels" | |||||
| ) | |||||
| } | |||||
| //Print Carton Labels | |||||
| @Transactional | |||||
| open fun printDNLabels(request: PrintDNLabelsRequest) { | |||||
| val printer = | |||||
| printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||||
| val pdf = exportDNLabels( | |||||
| ExportDNLabelsRequest( | |||||
| deliveryOrderIds = request.deliveryOrderId, | |||||
| numOfCarton = request.numOfCarton | |||||
| ) | |||||
| ) | |||||
| val jasperPrint = pdf["report"] as JasperPrint | |||||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||||
| try { | |||||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||||
| //Print Carton Labels | |||||
| @Transactional | |||||
| open fun printDNLabels(request: PrintDNLabelsRequest){ | |||||
| val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer") | |||||
| val pdf = exportDNLabels( | |||||
| ExportDNLabelsRequest( | |||||
| deliveryOrderIds = request.deliveryOrderId, | |||||
| numOfCarton = request.numOfCarton | |||||
| ) | |||||
| ) | |||||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||||
| printer.ip?.let { ip -> | |||||
| printer.port?.let { port -> | |||||
| ZebraPrinterUtil.printPdfToZebra( | |||||
| tempPdfFile, | |||||
| ip, | |||||
| port, | |||||
| printQty, | |||||
| ZebraPrinterUtil.PrintDirection.ROTATED | |||||
| ) | |||||
| } | |||||
| } | |||||
| val jasperPrint = pdf["report"] as JasperPrint | |||||
| println("Test PDF saved to: ${tempPdfFile.absolutePath}") | |||||
| val tempPdfFile = File.createTempFile("print_job_",".pdf") | |||||
| } finally { | |||||
| //tempPdfFile.delete() | |||||
| } | |||||
| try{ | |||||
| JasperExportManager.exportReportToPdfFile(jasperPrint,tempPdfFile.absolutePath) | |||||
| } | |||||
| @Transactional(rollbackFor = [Exception::class]) | |||||
| open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult { | |||||
| println("🔍 DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}") | |||||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | |||||
| ?: throw NoSuchElementException("Delivery Order not found") | |||||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||||
| printer.ip?.let { ip -> printer.port?.let { port -> | |||||
| ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED) | |||||
| }} | |||||
| // ✅ 检查状态,跳过已完成或已发布的DO | |||||
| if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) { | |||||
| throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release") | |||||
| } | |||||
| println("Test PDF saved to: ${tempPdfFile.absolutePath}") | |||||
| // ✅ 更新状态为released (使用RECEIVING表示已发布) | |||||
| deliveryOrder.apply { | |||||
| status = DeliveryOrderStatus.RECEIVING // 使用RECEIVING表示已发布状态 | |||||
| } | |||||
| deliveryOrderRepository.save(deliveryOrder) | |||||
| // 创建 pick order | |||||
| val pols = deliveryOrder.deliveryOrderLines.map { | |||||
| SavePickOrderLineRequest( | |||||
| itemId = it.item?.id, | |||||
| qty = it.qty ?: BigDecimal.ZERO, | |||||
| uomId = it.uom?.id, | |||||
| ) | |||||
| } | |||||
| val po = SavePickOrderRequest( | |||||
| doId = deliveryOrder.id, | |||||
| type = PickOrderType.DELIVERY_ORDER, | |||||
| targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(), | |||||
| pickOrderLine = pols | |||||
| ) | |||||
| val createdPickOrder = pickOrderService.create(po) | |||||
| val consoCode = pickOrderService.assignConsoCode() | |||||
| val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null) | |||||
| if (pickOrderEntity != null) { | |||||
| pickOrderEntity.consoCode = consoCode | |||||
| pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED | |||||
| pickOrderRepository.saveAndFlush(pickOrderEntity) | |||||
| // 创建 suggestions 和 hold inventory | |||||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | |||||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLines( | |||||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | |||||
| ) | |||||
| val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | |||||
| val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null } | |||||
| if (insufficientCount > 0) { | |||||
| println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)") | |||||
| } | |||||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | |||||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | |||||
| ) | |||||
| } finally { | |||||
| //tempPdfFile.delete() | |||||
| saveSuggestedPickLots.forEach { lot -> | |||||
| if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) { | |||||
| val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine) | |||||
| if (lineIndex >= 0) { | |||||
| inventoryLotLines[lineIndex].holdQty = | |||||
| (inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO) | |||||
| } | |||||
| } | |||||
| } | |||||
| inventoryLotLineRepository.saveAll(inventoryLotLines) | |||||
| // 创建 stock out | |||||
| val stockOut = StockOut().apply { | |||||
| this.type = "job" | |||||
| this.consoPickOrderCode = consoCode | |||||
| this.status = StockOutStatus.PENDING.status | |||||
| this.handler = request.userId | |||||
| } | } | ||||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||||
| saveSuggestedPickLots.forEach { lot -> | |||||
| val polId = lot.pickOrderLine?.id | |||||
| val illId = lot.suggestedLotLine?.id | |||||
| if (polId != null && illId != null) { | |||||
| val existingLines = | |||||
| stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId) | |||||
| if (existingLines.isEmpty()) { | |||||
| val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null) | |||||
| val inventoryLotLine = inventoryLotLineRepository.findById(illId).orElse(null) | |||||
| if (pickOrderLine != null && inventoryLotLine != null) { | |||||
| val line = StockOutLine().apply { | |||||
| this.stockOut = savedStockOut | |||||
| this.pickOrderLine = pickOrderLine | |||||
| this.inventoryLotLine = inventoryLotLine | |||||
| this.item = pickOrderLine.item | |||||
| this.status = StockOutLineStatus.PENDING.status | |||||
| this.qty = 0.0 | |||||
| } | |||||
| stockOutLineRepository.save(line) | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| // 分析楼层分布 | |||||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | |||||
| val inventoryQuery = """ | |||||
| SELECT | |||||
| w.store_id as floor, | |||||
| COUNT(DISTINCT il.itemId) as item_count | |||||
| FROM inventory_lot il | |||||
| INNER JOIN inventory i ON i.itemId = il.itemId AND i.deleted = 0 AND i.onHandQty > 0 | |||||
| INNER JOIN inventory_lot_line ill ON ill.inventoryLotId = il.id AND ill.deleted = 0 | |||||
| INNER JOIN warehouse w ON w.id = ill.warehouseId AND w.deleted = 0 AND w.store_id IN ('2F', '4F') | |||||
| WHERE il.itemId IN (${itemIds.joinToString(",")}) AND il.deleted = 0 | |||||
| GROUP BY w.store_id | |||||
| """.trimIndent() | |||||
| val inventoryResults = jdbcDao.queryForList(inventoryQuery) | |||||
| val floorItemCount = mutableMapOf<String, Int>() | |||||
| inventoryResults.forEach { row -> | |||||
| floorItemCount[row["floor"] as? String ?: "Other"] = (row["item_count"] as? Number)?.toInt() ?: 0 | |||||
| } | |||||
| val preferredFloor = if ((floorItemCount["4F"] ?: 0) == itemIds.size && (floorItemCount["2F"] ?: 0) == 0) { | |||||
| "4F" | |||||
| } else { | |||||
| "2F" | |||||
| } | |||||
| // ✅ 查找匹配 preferred floor 的 truck | |||||
| val truck = deliveryOrder.shop?.id?.let { shopId -> | |||||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||||
| val preferredStoreId = when (preferredFloor) { | |||||
| "2F" -> 2 | |||||
| "4F" -> 4 | |||||
| else -> 2 | |||||
| } | |||||
| // 只选择 store_id 匹配的 truck | |||||
| val matchedTrucks = trucks.filter { it.storeId == preferredStoreId } | |||||
| if (matchedTrucks.isEmpty()) { | |||||
| null // 没有匹配的 truck | |||||
| } else { | |||||
| matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } | |||||
| } | |||||
| } | |||||
| // ✅ 如果没有匹配的 truck,抛出异常跳过 | |||||
| if (truck == null) { | |||||
| val errorMsg = "No matching truck found for preferredFloor ($preferredFloor). Skipping DO ${deliveryOrder.id}." | |||||
| println("⚠️ $errorMsg") | |||||
| throw IllegalStateException(errorMsg) | |||||
| } | |||||
| println("✅ DEBUG: Matched truck - ID=${truck.id}, Store=${truck.storeId}, Floor=$preferredFloor") | |||||
| return ReleaseDoResult( | |||||
| deliveryOrderId = deliveryOrder.id!!, | |||||
| deliveryOrderCode = deliveryOrder.code, | |||||
| pickOrderId = createdPickOrder.id!!, | |||||
| pickOrderCode = pickOrderEntity?.code, | |||||
| shopId = deliveryOrder.shop?.id, | |||||
| shopCode = deliveryOrder.shop?.code, | |||||
| shopName = deliveryOrder.shop?.name, | |||||
| estimatedArrivalDate = targetDate, | |||||
| preferredFloor = preferredFloor, | |||||
| truckId = truck.id, | |||||
| truckDepartureTime = truck.departureTime, | |||||
| truckLanceCode = truck.truckLanceCode, | |||||
| loadingSequence = truck.loadingSequence // ✅ 直接使用 truck 的值 | |||||
| ) | |||||
| } | |||||
| @Transactional | |||||
| open fun syncDeliveryOrderStatusFromDoPickOrder(): Int { | |||||
| val sql = """ | |||||
| UPDATE fpsmsdb.delivery_order do | |||||
| INNER JOIN fpsmsdb.do_pick_order dpo ON dpo.do_order_id = do.id | |||||
| SET do.status = 'completed' | |||||
| WHERE dpo.ticket_status = 'completed' | |||||
| AND do.status != 'completed' | |||||
| AND do.deleted = 0 | |||||
| AND dpo.deleted = 0 | |||||
| """.trimIndent() | |||||
| return jdbcDao.executeUpdate(sql, emptyMap<String, Any>()) | |||||
| } | |||||
| } | } | ||||
| } | |||||
| @@ -0,0 +1,169 @@ | |||||
| package com.ffii.fpsms.modules.deliveryOrder.service | |||||
| import com.ffii.core.support.JdbcDao | |||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | |||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | |||||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByLaneRequest | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | |||||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||||
| import com.ffii.fpsms.modules.user.entity.UserRepository | |||||
| import org.springframework.stereotype.Service | |||||
| import java.time.LocalDateTime | |||||
| @Service | |||||
| class DoPickOrderAssignmentService( | |||||
| private val doPickOrderRepository: DoPickOrderRepository, | |||||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||||
| private val pickOrderRepository: PickOrderRepository, | |||||
| private val userRepository: UserRepository, | |||||
| private val jdbcDao: JdbcDao // ✅ 添加 JdbcDao | |||||
| ) { | |||||
| fun assignByLane(request: AssignByLaneRequest): MessageResponse { | |||||
| val user = userRepository.findById(request.userId).orElse(null) | |||||
| ?: return MessageResponse( | |||||
| id = null, code = "USER_NOT_FOUND", name = null, type = null, | |||||
| message = "User not found", errorPosition = null, entity = null | |||||
| ) | |||||
| // ✅ 转换 storeId 格式 | |||||
| val actualStoreId = when (request.storeId) { | |||||
| "2/F" -> "2/F" | |||||
| "4/F" -> "4/F" | |||||
| else -> request.storeId | |||||
| } | |||||
| println("🔍 DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'") | |||||
| // ✅ 获取所有候选记录 | |||||
| val allCandidates = doPickOrderRepository | |||||
| .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | |||||
| actualStoreId, | |||||
| DoPickOrderStatus.pending | |||||
| ) | |||||
| .filter { it.truckLanceCode == request.truckLanceCode } | |||||
| println("🔍 DEBUG: Found ${allCandidates.size} candidate do_pick_orders for lane ${request.truckLanceCode}") | |||||
| // ✅ 过滤掉所有 pick orders 都是 issue 的记录 | |||||
| val filteredCandidates = allCandidates.filter { doPickOrder -> | |||||
| val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | |||||
| if (!hasNonIssueLines) { | |||||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | |||||
| } | |||||
| hasNonIssueLines | |||||
| } | |||||
| println("🔍 DEBUG: After filtering, ${filteredCandidates.size} do_pick_orders remain") | |||||
| if (filteredCandidates.isEmpty()) { | |||||
| return MessageResponse( | |||||
| id = null, code = "NO_ORDERS", name = null, type = null, | |||||
| message = "No available pick order(s) for this lane (all have stock issues).", | |||||
| errorPosition = null, entity = null | |||||
| ) | |||||
| } | |||||
| val firstOrder = filteredCandidates.first() | |||||
| // ✅ 更新 do_pick_order | |||||
| firstOrder.handledBy = request.userId | |||||
| firstOrder.handlerName = user.name | |||||
| firstOrder.ticketStatus = DoPickOrderStatus.released | |||||
| firstOrder.ticketReleaseTime = LocalDateTime.now() | |||||
| doPickOrderRepository.save(firstOrder) | |||||
| // ✅ 获取这个 do_pick_order 下的所有 pick orders 并分配给用户 | |||||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(firstOrder.id!!) | |||||
| println("🔍 DEBUG: Found ${doPickOrderLines.size} pick orders in do_pick_order ${firstOrder.id}") | |||||
| doPickOrderLines.forEach { line -> | |||||
| if (line.pickOrderId != null) { | |||||
| val pickOrder = pickOrderRepository.findById(line.pickOrderId!!).orElse(null) | |||||
| if (pickOrder != null) { | |||||
| pickOrder.assignTo = user | |||||
| pickOrder.status = PickOrderStatus.RELEASED | |||||
| pickOrderRepository.save(pickOrder) | |||||
| println("🔍 DEBUG: Assigned pick order ${line.pickOrderId} to user ${request.userId}") | |||||
| } else { | |||||
| println("⚠️ WARNING: Pick order ${line.pickOrderId} not found") | |||||
| } | |||||
| } | |||||
| } | |||||
| // ✅ 同步更新 do_pick_order_record(如果有的话) | |||||
| doPickOrderLines.forEach { line -> | |||||
| if (line.pickOrderId != null) { | |||||
| val records = doPickOrderRecordRepository.findByPickOrderId(line.pickOrderId!!) | |||||
| records.forEach { record -> | |||||
| record.handledBy = request.userId | |||||
| record.handlerName = user.name | |||||
| record.ticketStatus = DoPickOrderStatus.released | |||||
| record.ticketReleaseTime = LocalDateTime.now() | |||||
| } | |||||
| if (records.isNotEmpty()) { | |||||
| doPickOrderRecordRepository.saveAll(records) | |||||
| println("🔍 DEBUG: Updated ${records.size} do_pick_order_record for pick order ${line.pickOrderId}") | |||||
| } | |||||
| } | |||||
| } | |||||
| return MessageResponse( | |||||
| id = firstOrder.id, | |||||
| code = "SUCCESS", | |||||
| name = null, | |||||
| type = null, | |||||
| message = "Assigned ${doPickOrderLines.size} pick order(s) from lane ${request.truckLanceCode}", | |||||
| errorPosition = null, | |||||
| entity = mapOf( | |||||
| "doPickOrderId" to firstOrder.id, | |||||
| "ticketNo" to firstOrder.ticketNo, | |||||
| "numberOfPickOrders" to doPickOrderLines.size, | |||||
| "pickOrderIds" to doPickOrderLines.mapNotNull { it.pickOrderId } | |||||
| ) | |||||
| ) | |||||
| } | |||||
| // ✅ 添加过滤方法(和 DoPickOrderQueryService 中相同的逻辑) | |||||
| private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean { | |||||
| return try { | |||||
| val totalLinesSql = """ | |||||
| SELECT COUNT(*) as total_lines | |||||
| FROM fpsmsdb.do_pick_order_line dpol | |||||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||||
| AND dpol.deleted = 0 | |||||
| """.trimIndent() | |||||
| val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||||
| val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0 | |||||
| if (totalLines == 0) { | |||||
| return true // 没有 lines,不算过滤 | |||||
| } | |||||
| val nonIssueLinesSql = """ | |||||
| SELECT COUNT(*) as non_issue_lines | |||||
| FROM fpsmsdb.do_pick_order_line dpol | |||||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||||
| AND dpol.deleted = 0 | |||||
| AND (dpol.status IS NULL OR dpol.status != 'issue') | |||||
| """.trimIndent() | |||||
| val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||||
| val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0 | |||||
| val hasNonIssueLines = nonIssueLines > 0 | |||||
| println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total lines: $totalLines, Non-issue lines: $nonIssueLines, Has non-issue lines: $hasNonIssueLines") | |||||
| hasNonIssueLines | |||||
| } catch (e: Exception) { | |||||
| println("❌ Error checking non-issue lines for do_pick_order $doPickOrderId: ${e.message}") | |||||
| true // 出错时不过滤 | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,102 @@ | |||||
| package com.ffii.fpsms.modules.deliveryOrder.service | |||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.* | |||||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | |||||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||||
| import org.springframework.stereotype.Service | |||||
| import org.springframework.transaction.annotation.Transactional | |||||
| import java.time.LocalDateTime | |||||
| @Service | |||||
| open class DoPickOrderCompletionService( | |||||
| private val doPickOrderRepository: DoPickOrderRepository, | |||||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | |||||
| private val deliveryOrderRepository: DeliveryOrderRepository | |||||
| ) { | |||||
| @Transactional | |||||
| fun completeDoPickOrdersForPickOrder(pickOrderId: Long): List<DoPickOrder> { | |||||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||||
| doPickOrders.forEach { | |||||
| it.ticketStatus = DoPickOrderStatus.completed | |||||
| it.ticketCompleteDateTime = LocalDateTime.now() | |||||
| } | |||||
| val savedDoPickOrders = doPickOrderRepository.saveAll(doPickOrders) | |||||
| // ✅ 同步更新相关的delivery_order状态 | |||||
| savedDoPickOrders.forEach { doPickOrder -> | |||||
| if (doPickOrder.doOrderId != null) { | |||||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doPickOrder.doOrderId!!) | |||||
| if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) { | |||||
| deliveryOrder.status = DeliveryOrderStatus.COMPLETED | |||||
| deliveryOrderRepository.save(deliveryOrder) | |||||
| println("✅ Updated delivery order ${doPickOrder.doOrderId} status to completed") | |||||
| } | |||||
| } | |||||
| } | |||||
| return savedDoPickOrders | |||||
| } | |||||
| @Transactional | |||||
| fun removeDoPickOrdersForPickOrder(pickOrderId: Long): Int { | |||||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||||
| var deletedCount = 0 | |||||
| doPickOrders.forEach { doPickOrder -> | |||||
| // ✅ 第一步:复制 do_pick_order 到 do_pick_order_record | |||||
| val doPickOrderRecord = DoPickOrderRecord( | |||||
| storeId = doPickOrder.storeId ?: "", | |||||
| ticketNo = doPickOrder.ticketNo ?: "", | |||||
| ticketStatus = DoPickOrderStatus.completed, | |||||
| truckId = doPickOrder.truckId, | |||||
| pickOrderId = doPickOrder.pickOrderId, | |||||
| truckDepartureTime = doPickOrder.truckDepartureTime, | |||||
| shopId = doPickOrder.shopId, | |||||
| handledBy = doPickOrder.handledBy, | |||||
| handlerName = doPickOrder.handlerName, | |||||
| doOrderId = doPickOrder.doOrderId, | |||||
| pickOrderCode = doPickOrder.pickOrderCode, | |||||
| deliveryOrderCode = doPickOrder.deliveryOrderCode, | |||||
| loadingSequence = doPickOrder.loadingSequence, | |||||
| ticketReleaseTime = doPickOrder.ticketReleaseTime, | |||||
| ticketCompleteDateTime = LocalDateTime.now(), | |||||
| truckLanceCode = doPickOrder.truckLanceCode, | |||||
| shopCode = doPickOrder.shopCode, | |||||
| shopName = doPickOrder.shopName, | |||||
| requiredDeliveryDate = doPickOrder.requiredDeliveryDate | |||||
| ) | |||||
| val savedRecord = doPickOrderRecordRepository.save(doPickOrderRecord) | |||||
| println("✅ Copied do_pick_order ${doPickOrder.id} to do_pick_order_record") | |||||
| // ✅ 第二步:复制 do_pick_order_line 到 do_pick_order_line_record | |||||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!) | |||||
| doPickOrderLines.forEach { line -> | |||||
| val doPickOrderLineRecord = DoPickOrderLineRecord() | |||||
| doPickOrderLineRecord.doPickOrderId = savedRecord.id | |||||
| doPickOrderLineRecord.pickOrderId = line.pickOrderId | |||||
| doPickOrderLineRecord.doOrderId = line.doOrderId | |||||
| doPickOrderLineRecord.pickOrderCode = line.pickOrderCode | |||||
| doPickOrderLineRecord.deliveryOrderCode = line.deliveryOrderCode | |||||
| doPickOrderLineRecord.status = line.status | |||||
| doPickOrderLineRecordRepository.save(doPickOrderLineRecord) | |||||
| println("✅ Copied do_pick_order_line ${line.id} to do_pick_order_line_record") | |||||
| } | |||||
| // ✅ 第三步:删除原始的 do_pick_order_line 记录 | |||||
| if (doPickOrderLines.isNotEmpty()) { | |||||
| doPickOrderLineRepository.deleteAll(doPickOrderLines) | |||||
| println("✅ Deleted ${doPickOrderLines.size} do_pick_order_line records") | |||||
| } | |||||
| // ✅ 第四步:删除原始的 do_pick_order 记录 | |||||
| doPickOrderRepository.delete(doPickOrder) | |||||
| deletedCount++ | |||||
| println("✅ Deleted do_pick_order ${doPickOrder.id}") | |||||
| } | |||||
| return deletedCount | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,108 @@ | |||||
| package com.ffii.fpsms.modules.deliveryOrder.service | |||||
| import com.ffii.core.support.JdbcDao | |||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneBtn | |||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneRow | |||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.StoreLaneSummary | |||||
| import org.springframework.stereotype.Service | |||||
| import java.time.LocalDate | |||||
| @Service | |||||
| class DoPickOrderQueryService( | |||||
| private val doPickOrderRepository: DoPickOrderRepository, | |||||
| private val jdbcDao: JdbcDao | |||||
| ) { | |||||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | |||||
| val targetDate = requiredDate ?: LocalDate.now() | |||||
| println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate") | |||||
| val actualStoreId = when (storeId) { | |||||
| "2/F" -> "2/F" | |||||
| "4/F" -> "4/F" | |||||
| else -> storeId | |||||
| } | |||||
| val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||||
| actualStoreId, | |||||
| targetDate, | |||||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | |||||
| ) | |||||
| println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate") | |||||
| val filteredRecords = allRecords.filter { doPickOrder -> | |||||
| val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | |||||
| if (!hasNonIssueLines) { | |||||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | |||||
| } | |||||
| hasNonIssueLines | |||||
| } | |||||
| println("🔍 DEBUG: After filtering, ${filteredRecords.size} records remain") | |||||
| val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||||
| .mapValues { (_, list) -> | |||||
| LaneBtn( | |||||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||||
| unassigned = list.count { it.handledBy == null }, | |||||
| total = list.size | |||||
| ) | |||||
| } | |||||
| val timeGroups = grouped.entries | |||||
| .groupBy { it.key.first } | |||||
| .mapValues { (_, entries) -> | |||||
| entries.map { it.value } | |||||
| .sortedByDescending { it.unassigned } | |||||
| .take(3) | |||||
| } | |||||
| .filterValues { lanes -> lanes.any { it.unassigned > 0 } } | |||||
| .toSortedMap(compareBy { it }) | |||||
| .entries.take(4) | |||||
| .map { (time, lanes) -> | |||||
| LaneRow( | |||||
| truckDepartureTime = time?.toString() ?: "", | |||||
| lanes = lanes | |||||
| ) | |||||
| } | |||||
| return StoreLaneSummary(storeId = storeId, rows = timeGroups) | |||||
| } | |||||
| private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean { | |||||
| return try { | |||||
| val totalLinesSql = """ | |||||
| SELECT COUNT(*) as total_lines | |||||
| FROM fpsmsdb.do_pick_order_line dpol | |||||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||||
| AND dpol.deleted = 0 | |||||
| """.trimIndent() | |||||
| val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||||
| val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0 | |||||
| if (totalLines == 0) { | |||||
| return true | |||||
| } | |||||
| val nonIssueLinesSql = """ | |||||
| SELECT COUNT(*) as non_issue_lines | |||||
| FROM fpsmsdb.do_pick_order_line dpol | |||||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||||
| AND dpol.deleted = 0 | |||||
| AND (dpol.status IS NULL OR dpol.status != 'issue') | |||||
| """.trimIndent() | |||||
| val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId)) | |||||
| val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0 | |||||
| nonIssueLines > 0 | |||||
| } catch (e: Exception) { | |||||
| println("❌ Error checking non-issue lines: ${e.message}") | |||||
| true | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -3,7 +3,6 @@ package com.ffii.fpsms.modules.deliveryOrder.service | |||||
| import com.ffii.fpsms.m18.entity.M18DataLogRepository | import com.ffii.fpsms.m18.entity.M18DataLogRepository | ||||
| import com.ffii.fpsms.m18.service.M18DataLogService | import com.ffii.fpsms.m18.service.M18DataLogService | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrder | 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.entity.models.DeliveryOrderInfo | ||||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.SaveDeliveryOrderRequest | import com.ffii.fpsms.modules.deliveryOrder.web.models.SaveDeliveryOrderRequest | ||||
| @@ -42,15 +41,22 @@ import java.time.LocalDate | |||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| import com.ffii.core.support.JdbcDao | import com.ffii.core.support.JdbcDao | ||||
| import com.ffii.fpsms.modules.pickOrder.entity.Truck | 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 | @Service | ||||
| class DoPickOrderService( | |||||
| open class DoPickOrderService( | |||||
| private val doPickOrderRepository: DoPickOrderRepository, | private val doPickOrderRepository: DoPickOrderRepository, | ||||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | ||||
| private val userRepository: UserRepository, | private val userRepository: UserRepository, | ||||
| private val pickOrderRepository: PickOrderRepository, | private val pickOrderRepository: PickOrderRepository, | ||||
| private val jdbcDao: JdbcDao, // ✅ 添加这行 | private val jdbcDao: JdbcDao, // ✅ 添加这行 | ||||
| private val truckRepository: TruckRepository | |||||
| private val truckRepository: TruckRepository, | |||||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||||
| @Lazy private val deliveryOrderRepository: DeliveryOrderRepository, | |||||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository | |||||
| ) { | ) { | ||||
| fun findReleasedDoPickOrders(): List<DoPickOrder> { | fun findReleasedDoPickOrders(): List<DoPickOrder> { | ||||
| return doPickOrderRepository.findByTicketStatusIn( | return doPickOrderRepository.findByTicketStatusIn( | ||||
| @@ -141,20 +147,86 @@ class DoPickOrderService( | |||||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | ||||
| doPickOrders.forEach { | doPickOrders.forEach { | ||||
| it.ticketStatus = DoPickOrderStatus.completed | 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 | // ✅ New method to remove do_pick_order records when auto-assigning by store | ||||
| // ✅ 修改方法:先复制记录到record表,再删除原记录 | |||||
| @Transactional | |||||
| fun removeDoPickOrdersForPickOrder(pickOrderId: Long): Int { | fun removeDoPickOrdersForPickOrder(pickOrderId: Long): Int { | ||||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | 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 { | fun saveRecord(record: DoPickOrderRecord): DoPickOrderRecord { | ||||
| @@ -197,31 +269,48 @@ class DoPickOrderService( | |||||
| } | } | ||||
| return doPickOrderRepository.saveAll(doPickOrders) | return doPickOrderRepository.saveAll(doPickOrders) | ||||
| } | } | ||||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | ||||
| // ✅ 使用传入的日期,如果没有传入则使用今天 | |||||
| val targetDate = requiredDate ?: LocalDate.now() | val targetDate = requiredDate ?: LocalDate.now() | ||||
| println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate") | println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate") | ||||
| // ✅ 修改:按日期查询订单 | |||||
| // ✅ 修复格式转换:保持原始格式 | |||||
| val actualStoreId = when (storeId) { | |||||
| "2/F" -> "2/F" // ✅ 保持原格式 | |||||
| "4/F" -> "4/F" // ✅ 保持原格式 | |||||
| else -> storeId | |||||
| } | |||||
| println("🔍 DEBUG: Using storeId: '$actualStoreId'") | |||||
| // ✅ 直接查询 do_pick_order 表 | |||||
| val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | ||||
| storeId, | |||||
| actualStoreId, | |||||
| targetDate, | targetDate, | ||||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | ||||
| ) | ) | ||||
| println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate") | 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) -> | .mapValues { (_, list) -> | ||||
| LaneBtn( | LaneBtn( | ||||
| truckLanceCode = list.first().truckLanceCode ?: "", | 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 | val timeGroups = grouped.entries | ||||
| .groupBy { it.key.first } | .groupBy { it.key.first } | ||||
| .mapValues { (_, entries) -> | .mapValues { (_, entries) -> | ||||
| @@ -238,91 +327,136 @@ class DoPickOrderService( | |||||
| lanes = lanes | lanes = lanes | ||||
| ) | ) | ||||
| } | } | ||||
| return StoreLaneSummary(storeId = storeId, rows = timeGroups) | 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 移到类里面 | // ✅ 修复:把 assignByLane 移到类里面 | ||||
| fun assignByLane(request: AssignByLaneRequest): MessageResponse { | 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 | val candidates = doPickOrderRepository | ||||
| .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | ||||
| request.storeId, | |||||
| actualStoreId, | |||||
| DoPickOrderStatus.pending | 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()) { | if (candidates.isEmpty()) { | ||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, code = "NO_ORDERS", name = null, type = null, | 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 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.handledBy = request.userId | ||||
| firstOrder.handlerName = handlerName | |||||
| firstOrder.handlerName = user.name | |||||
| firstOrder.ticketStatus = DoPickOrderStatus.released | firstOrder.ticketStatus = DoPickOrderStatus.released | ||||
| firstOrder.ticketReleaseTime = LocalDateTime.now() | firstOrder.ticketReleaseTime = LocalDateTime.now() | ||||
| // ✅ 重要:不要修改 truckDepartureTime 和 truckLanceCode | |||||
| // 这些信息应该保持用户选择的值 | |||||
| doPickOrderRepository.save(firstOrder) | 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( | return MessageResponse( | ||||
| id = firstOrder.pickOrderId, | |||||
| id = firstOrder.id, | |||||
| code = "SUCCESS", | code = "SUCCESS", | ||||
| name = null, | name = null, | ||||
| type = null, | type = null, | ||||
| message = "Assigned pick order from lane ${request.truckLanceCode}", | |||||
| message = "Assigned ${doPickOrderLines.size} pick order(s) from lane ${request.truckLanceCode}", | |||||
| errorPosition = null, | errorPosition = null, | ||||
| entity = mapOf( | entity = mapOf( | ||||
| "pickOrderId" to firstOrder.pickOrderId, | |||||
| "ticketNo" to firstOrder.ticketNo | |||||
| "doPickOrderId" to firstOrder.id, | |||||
| "ticketNo" to firstOrder.ticketNo, | |||||
| "numberOfPickOrders" to doPickOrderLines.size, | |||||
| "pickOrderIds" to doPickOrderLines.mapNotNull { it.pickOrderId } | |||||
| ) | ) | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -10,7 +10,14 @@ import java.util.concurrent.Executors | |||||
| import java.util.concurrent.atomic.AtomicInteger | import java.util.concurrent.atomic.AtomicInteger | ||||
| import kotlin.math.min | import kotlin.math.min | ||||
| import com.ffii.core.support.JdbcDao | 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( | data class BatchReleaseJobStatus( | ||||
| val jobId: String, | val jobId: String, | ||||
| val total: Int, | val total: Int, | ||||
| @@ -24,7 +31,10 @@ data class BatchReleaseJobStatus( | |||||
| @Service | @Service | ||||
| class DoReleaseCoordinatorService( | class DoReleaseCoordinatorService( | ||||
| private val deliveryOrderService: DeliveryOrderService, | 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 poolSize = Runtime.getRuntime().availableProcessors() | ||||
| private val executor = Executors.newFixedThreadPool(min(poolSize, 4)) | private val executor = Executors.newFixedThreadPool(min(poolSize, 4)) | ||||
| @@ -135,7 +145,13 @@ class DoReleaseCoordinatorService( | |||||
| ) AS loading_sequence | ) AS loading_sequence | ||||
| FROM fpsmsdb.delivery_order do | FROM fpsmsdb.delivery_order do | ||||
| LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id | 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 | SELECT | ||||
| dpo2.id, | dpo2.id, | ||||
| @@ -224,7 +240,7 @@ class DoReleaseCoordinatorService( | |||||
| FROM DoFloorSummary | FROM DoFloorSummary | ||||
| ), | ), | ||||
| TruckSelection AS ( | TruckSelection AS ( | ||||
| SELECT | |||||
| SELECT | |||||
| do.id AS delivery_order_id, | do.id AS delivery_order_id, | ||||
| do.shopId, | do.shopId, | ||||
| do.estimatedArrivalDate, | do.estimatedArrivalDate, | ||||
| @@ -296,7 +312,7 @@ class DoReleaseCoordinatorService( | |||||
| ) AS loading_sequence | ) AS loading_sequence | ||||
| FROM fpsmsdb.delivery_order do | FROM fpsmsdb.delivery_order do | ||||
| LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id | 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 | AND do.deleted = 0 | ||||
| ) | ) | ||||
| SELECT delivery_order_id AS id | SELECT delivery_order_id AS id | ||||
| @@ -314,8 +330,8 @@ class DoReleaseCoordinatorService( | |||||
| println("🔍 DEBUG: SQL length: ${sql.length} characters") // ✅ 添加这行 | println("🔍 DEBUG: SQL length: ${sql.length} characters") // ✅ 添加这行 | ||||
| println("🔍 DEBUG: SQL first 500 chars: ${sql.take(500)}") // ✅ 添加这行 | 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 type: ${results.javaClass.name}") // ✅ 添加这行 | ||||
| println("🔍 DEBUG: Results size: ${results.size}") // ✅ 添加这行 | println("🔍 DEBUG: Results size: ${results.size}") // ✅ 添加这行 | ||||
| @@ -337,13 +353,13 @@ class DoReleaseCoordinatorService( | |||||
| } else { | } else { | ||||
| sortedIds | sortedIds | ||||
| } | } | ||||
| } catch (e: Exception) { | |||||
| } catch (e: Exception) { | |||||
| println("❌ ERROR: ${e.message}") | println("❌ ERROR: ${e.message}") | ||||
| println("❌ ERROR Stack Trace:") // ✅ 添加这行 | println("❌ ERROR Stack Trace:") // ✅ 添加这行 | ||||
| e.printStackTrace() | |||||
| return ids | |||||
| e.printStackTrace() | |||||
| return ids | |||||
| } | |||||
| } | } | ||||
| } | |||||
| fun startBatchReleaseAsync(ids: List<Long>, userId: Long): MessageResponse { | fun startBatchReleaseAsync(ids: List<Long>, userId: Long): MessageResponse { | ||||
| if (ids.isEmpty()) { | if (ids.isEmpty()) { | ||||
| return MessageResponse(id = null, code = "NO_IDS", name = null, type = null, | return MessageResponse(id = null, code = "NO_IDS", name = null, type = null, | ||||
| @@ -356,55 +372,72 @@ class DoReleaseCoordinatorService( | |||||
| executor.submit { | executor.submit { | ||||
| try { | 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") | println("🔍 DEBUG: Got ${sortedIds.size} sorted orders") | ||||
| sortedIds.forEachIndexed { index, id -> | |||||
| val releaseResults = mutableListOf<ReleaseDoResult>() | |||||
| // 第一步:发布所有 DO(创建 pick orders,但不创建 DoPickOrder) | |||||
| sortedIds.forEach { id -> | |||||
| try { | 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) | 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) { | } catch (e: Exception) { | ||||
| synchronized(status.failed) { | synchronized(status.failed) { | ||||
| status.failed.add(id to (e.message ?: "Exception")) | 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() | e.printStackTrace() | ||||
| } | } | ||||
| } | } | ||||
| // 第四步:更新 ticket numbers | |||||
| if (status.success.get() > 0) { | if (status.success.get() > 0) { | ||||
| println("🎫 Updating ticket numbers...") | println("🎫 Updating ticket numbers...") | ||||
| updateBatchTicketNumbers() | updateBatchTicketNumbers() | ||||
| } | } | ||||
| println("✅ Batch completed: ${status.success.get()} success, ${status.failed.size} failed") | 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) { | } catch (e: Exception) { | ||||
| println("❌ Batch release exception: ${e.javaClass.simpleName} - ${e.message}") | println("❌ Batch release exception: ${e.javaClass.simpleName} - ${e.message}") | ||||
| e.printStackTrace() | e.printStackTrace() | ||||
| @@ -416,10 +449,118 @@ class DoReleaseCoordinatorService( | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, code = "STARTED", name = null, type = null, | 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) | entity = mapOf("jobId" to jobId, "total" to ids.size) | ||||
| ) | ) | ||||
| } | } | ||||
| private fun createMergedDoPickOrder(results: List<ReleaseDoResult>) { | |||||
| val first = results.first() | |||||
| val storeId = when (first.preferredFloor) { | |||||
| "2F" -> "2/F" | |||||
| "4F" -> "4/F" | |||||
| else -> "2/F" | |||||
| } | |||||
| val doPickOrder = DoPickOrder( | |||||
| storeId = storeId, | |||||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||||
| ticketStatus = DoPickOrderStatus.pending, | |||||
| truckId = first.truckId, | |||||
| truckDepartureTime = first.truckDepartureTime, | |||||
| shopId = first.shopId, | |||||
| handledBy = null, | |||||
| loadingSequence = first.loadingSequence ?: 999, | |||||
| ticketReleaseTime = null, | |||||
| truckLanceCode = first.truckLanceCode, | |||||
| shopCode = first.shopCode, | |||||
| shopName = first.shopName, | |||||
| requiredDeliveryDate = first.estimatedArrivalDate | |||||
| ) | |||||
| // ✅ 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save() | |||||
| val saved = doPickOrderRepository.save(doPickOrder) | |||||
| println("🔍 DEBUG: Saved DoPickOrder - ID: ${saved.id}, Ticket: ${saved.ticketNo}") | |||||
| // 创建多条 DoPickOrderLine(每个 DO 一条) | |||||
| results.forEach { result -> | |||||
| val existingLines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(result.pickOrderId) | |||||
| if (existingLines.isNotEmpty()) { | |||||
| println("⚠️ WARNING: pick_order ${result.pickOrderId} already in do_pick_order_line, skipping") | |||||
| return@forEach // 跳过这个 | |||||
| } | |||||
| // ✅ 先创建 DoPickOrderLine,然后检查库存问题 | |||||
| val line = DoPickOrderLine().apply { | |||||
| doPickOrderId = saved.id | |||||
| pickOrderId = result.pickOrderId | |||||
| doOrderId = result.deliveryOrderId | |||||
| pickOrderCode = result.pickOrderCode | |||||
| deliveryOrderCode = result.deliveryOrderCode | |||||
| status = "pending" // 初始状态 | |||||
| } | |||||
| doPickOrderLineRepository.save(line) | |||||
| println("🔍 DEBUG: Created DoPickOrderLine for pick order ${result.pickOrderId}") | |||||
| } | |||||
| // ✅ 现在检查整个 DoPickOrder 是否有库存问题 | |||||
| val hasStockIssues = checkPickOrderHasStockIssues(saved.id!!) | |||||
| if (hasStockIssues) { | |||||
| // 更新所有相关的 DoPickOrderLine 状态为 "issue" | |||||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(saved.id!!) | |||||
| doPickOrderLines.forEach { line -> | |||||
| line.status = "issue" | |||||
| } | |||||
| doPickOrderLineRepository.saveAll(doPickOrderLines) | |||||
| println("🔍 DEBUG: Updated ${doPickOrderLines.size} DoPickOrderLine records to 'issue' status") | |||||
| } | |||||
| println("🔍 DEBUG: Created ${results.size} DoPickOrderLine records") | |||||
| } | |||||
| private fun checkPickOrderHasStockIssues(doPickOrderId: Long): Boolean { | |||||
| try { | |||||
| // 1. 获取 do_pick_order 的所有 pick orders 数量 | |||||
| val totalPickOrdersSql = """ | |||||
| SELECT COUNT(*) as total_pick_orders | |||||
| FROM fpsmsdb.do_pick_order_line dpol | |||||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||||
| AND dpol.deleted = 0 | |||||
| """.trimIndent() | |||||
| val totalPickOrdersResult = jdbcDao.queryForList(totalPickOrdersSql, mapOf("doPickOrderId" to doPickOrderId)) | |||||
| val totalPickOrders = (totalPickOrdersResult.firstOrNull()?.get("total_pick_orders") as? Number)?.toInt() ?: 0 | |||||
| if (totalPickOrders == 0) { | |||||
| return false // 没有 pick orders,不算 issue | |||||
| } | |||||
| // 2. 获取有库存问题的 pick orders 数量(通过检查 pick_execution_issue 表) | |||||
| val issuePickOrdersSql = """ | |||||
| SELECT COUNT(DISTINCT dpol.pick_order_id) as issue_pick_orders | |||||
| FROM fpsmsdb.do_pick_order_line dpol | |||||
| INNER JOIN fpsmsdb.pick_execution_issue pei ON pei.pick_order_id = dpol.pick_order_id | |||||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||||
| AND pei.deleted = 0 | |||||
| AND dpol.deleted = 0 | |||||
| """.trimIndent() | |||||
| val issuePickOrdersResult = jdbcDao.queryForList(issuePickOrdersSql, mapOf("doPickOrderId" to doPickOrderId)) | |||||
| val issuePickOrders = (issuePickOrdersResult.firstOrNull()?.get("issue_pick_orders") as? Number)?.toInt() ?: 0 | |||||
| // 3. 只有当所有 pick orders 都有问题时才算 issue | |||||
| val hasAllPickOrdersIssues = (totalPickOrders > 0) && (issuePickOrders == totalPickOrders) | |||||
| println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total pick orders: $totalPickOrders, Issue pick orders: $issuePickOrders, All pick orders have issues: $hasAllPickOrdersIssues") | |||||
| return hasAllPickOrdersIssues | |||||
| } catch (e: Exception) { | |||||
| println("❌ Error checking stock issues for do pick order $doPickOrderId: ${e.message}") | |||||
| return false | |||||
| } | |||||
| } | |||||
| fun getBatchReleaseProgress(jobId: String): MessageResponse { | fun getBatchReleaseProgress(jobId: String): MessageResponse { | ||||
| val s = jobs[jobId] ?: return MessageResponse( | val s = jobs[jobId] ?: return MessageResponse( | ||||
| id = null, code = "NOT_FOUND", name = null, type = null, | id = null, code = "NOT_FOUND", name = null, type = null, | ||||
| @@ -39,12 +39,18 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByStoreRequest | |||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.* | import com.ffii.fpsms.modules.deliveryOrder.web.models.* | ||||
| import org.springframework.format.annotation.DateTimeFormat | import org.springframework.format.annotation.DateTimeFormat | ||||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoReleaseCoordinatorService | 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 | @RestController | ||||
| @RequestMapping("/doPickOrder") | @RequestMapping("/doPickOrder") | ||||
| class DoPickOrderController( | class DoPickOrderController( | ||||
| private val doPickOrderService: DoPickOrderService, | private val doPickOrderService: DoPickOrderService, | ||||
| private val doPickOrderRepository: DoPickOrderRepository, | 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") | @PostMapping("/assign-by-store") | ||||
| fun assignPickOrderByStore(@RequestBody request: AssignByStoreRequest): MessageResponse { | fun assignPickOrderByStore(@RequestBody request: AssignByStoreRequest): MessageResponse { | ||||
| @@ -65,12 +71,13 @@ class DoPickOrderController( | |||||
| @RequestParam storeId: String, | @RequestParam storeId: String, | ||||
| @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate? | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate? | ||||
| ): StoreLaneSummary { | ): StoreLaneSummary { | ||||
| return doPickOrderService.getSummaryByStore(storeId, requiredDate) | |||||
| return doPickOrderQueryService.getSummaryByStore(storeId, requiredDate) | |||||
| } | } | ||||
| @PostMapping("/assign-by-lane") | @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") | @PostMapping("/batch-release/async") | ||||
| fun startBatchReleaseAsync( | fun startBatchReleaseAsync( | ||||
| @RequestBody ids: List<Long>, | @RequestBody ids: List<Long>, | ||||
| @@ -1,6 +1,23 @@ | |||||
| package com.ffii.fpsms.modules.deliveryOrder.web.models | package com.ffii.fpsms.modules.deliveryOrder.web.models | ||||
| import java.time.LocalDate | |||||
| import java.time.LocalTime | |||||
| data class ReleaseDoRequest( | data class ReleaseDoRequest( | ||||
| val id: Long, | val id: Long, | ||||
| val userId: Long | val userId: Long | ||||
| ) | |||||
| data class ReleaseDoResult( | |||||
| val deliveryOrderId: Long, | |||||
| val deliveryOrderCode: String?, | |||||
| val pickOrderId: Long, | |||||
| val pickOrderCode: String?, | |||||
| val shopId: Long?, | |||||
| val shopCode: String?, | |||||
| val shopName: String?, | |||||
| val estimatedArrivalDate: LocalDate?, | |||||
| val preferredFloor: String, | |||||
| val truckId: Long?, | |||||
| val truckDepartureTime: LocalTime?, | |||||
| val truckLanceCode: String?, | |||||
| val loadingSequence: Int? | |||||
| ) | ) | ||||
| @@ -60,7 +60,11 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository | import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository | import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository | ||||
| import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus | 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 | @Service | ||||
| open class PickOrderService( | open class PickOrderService( | ||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| @@ -81,12 +85,13 @@ open class PickOrderService( | |||||
| private val deliveryOrderRepository: DeliveryOrderRepository, | private val deliveryOrderRepository: DeliveryOrderRepository, | ||||
| private val truckRepository: TruckRepository, | private val truckRepository: TruckRepository, | ||||
| private val doPickOrderService: DoPickOrderService, | private val doPickOrderService: DoPickOrderService, | ||||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | |||||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | ||||
| private val doPickOrderRepository: DoPickOrderRepository, | private val doPickOrderRepository: DoPickOrderRepository, | ||||
| private val userRepository: UserRepository, | private val userRepository: UserRepository, | ||||
| private val joPickOrderRepository: JoPickOrderRepository, // ✅ 添加这行 | private val joPickOrderRepository: JoPickOrderRepository, // ✅ 添加这行 | ||||
| private val joPickOrderRecordRepository: JoPickOrderRecordRepository, | private val joPickOrderRecordRepository: JoPickOrderRecordRepository, | ||||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||||
| ) : AbstractBaseEntityService<PickOrder, Long, PickOrderRepository>(jdbcDao, pickOrderRepository) { | ) : AbstractBaseEntityService<PickOrder, Long, PickOrderRepository>(jdbcDao, pickOrderRepository) { | ||||
| @@ -1220,13 +1225,87 @@ logger.info("Precreated $precreated stock out lines for suggested lots on releas | |||||
| pickOrderRepository.save(pickOrder) | pickOrderRepository.save(pickOrder) | ||||
| println("✅ Updated pick order ${pickOrder.code} to COMPLETED status") | 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(避免循环依赖) | // ✅ 添加:直接使用 Repository 处理 JO pick order(避免循环依赖) | ||||
| if (pickOrder.jobOrder != null) { | if (pickOrder.jobOrder != null) { | ||||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrderId) | val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrderId) | ||||
| @@ -2771,38 +2850,52 @@ open fun autoAssignAndReleasePickOrderByStoreAndTicket(storeId: String, ticketNo | |||||
| try { | try { | ||||
| println("🔍 Starting getFgPickOrdersByUserId with userId: $userId") | println("🔍 Starting getFgPickOrdersByUserId with userId: $userId") | ||||
| // ✅ 完全修复:使用 shop 表的正确字段 | |||||
| // ✅ 修复:从 do_pick_order_line 获取 pick order 信息 | |||||
| val sql = """ | val sql = """ | ||||
| SELECT | SELECT | ||||
| dpo.id as doPickOrderId, | dpo.id as doPickOrderId, | ||||
| dpo.pick_order_id as pickOrderId, | |||||
| dpo.do_order_id as deliveryOrderId, | |||||
| dpo.store_id as storeId, | dpo.store_id as storeId, | ||||
| dpo.ticket_no as ticketNo, | dpo.ticket_no as ticketNo, | ||||
| dpo.TruckLanceCode as truckLanceCode, | dpo.TruckLanceCode as truckLanceCode, | ||||
| dpo.truck_departure_time as DepartureTime, | dpo.truck_departure_time as DepartureTime, | ||||
| dpo.ShopCode as shopCode, | dpo.ShopCode as shopCode, | ||||
| dpo.ShopName as shopName, | 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.id as shopId, | ||||
| s.name as shopNameFromShop, | s.name as shopNameFromShop, | ||||
| CONCAT_WS(', ', s.addr1, s.addr2, s.addr3, s.addr4, s.district) as shopAddress, | 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 | 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 | LEFT JOIN shop s ON s.id = dpo.shop_id | ||||
| WHERE po.assignTo = :userId | WHERE po.assignTo = :userId | ||||
| AND po.type = 'do' | AND po.type = 'do' | ||||
| AND po.status IN ('assigned', 'released', 'picking') | AND po.status IN ('assigned', 'released', 'picking') | ||||
| AND po.deleted = false | AND po.deleted = false | ||||
| AND dpo.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() | """.trimIndent() | ||||
| println("🔍 Executing SQL for FG pick orders by userId: $sql") | 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") | 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 -> | 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( | 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"] ?: ""), | "pickOrderConsoCode" to (row["pickOrderConsoCode"] ?: ""), | ||||
| "pickOrderTargetDate" to (row["pickOrderTargetDate"]?.toString() ?: ""), | "pickOrderTargetDate" to (row["pickOrderTargetDate"]?.toString() ?: ""), | ||||
| "pickOrderStatus" to (row["pickOrderStatus"] ?: ""), | "pickOrderStatus" to (row["pickOrderStatus"] ?: ""), | ||||
| "deliveryOrderId" to (row["deliveryOrderId"] ?: 0L), | |||||
| "deliveryNo" to (row["deliveryNo"] ?: ""), | |||||
| "deliveryDate" to (row["deliveryDate"]?.toString() ?: ""), | "deliveryDate" to (row["deliveryDate"]?.toString() ?: ""), | ||||
| "shopId" to (row["shopId"] ?: 0L), | "shopId" to (row["shopId"] ?: 0L), | ||||
| "shopCode" to (row["shopCode"] ?: ""), | "shopCode" to (row["shopCode"] ?: ""), | ||||
| "shopName" to (row["shopName"] ?: row["shopNameFromShop"] ?: ""), | "shopName" to (row["shopName"] ?: row["shopNameFromShop"] ?: ""), | ||||
| "shopAddress" to (row["shopAddress"] ?: ""), | "shopAddress" to (row["shopAddress"] ?: ""), | ||||
| "shopPoNo" to "", | "shopPoNo" to "", | ||||
| "numberOfPickOrders" to (row["numberOfPickOrders"] ?: 0), // ✅ 新增:pick order 数量 | |||||
| "numberOfCartons" to (row["numberOfCartons"] ?: 0), | "numberOfCartons" to (row["numberOfCartons"] ?: 0), | ||||
| "truckLanceCode" to (row["truckLanceCode"] ?: ""), | "truckLanceCode" to (row["truckLanceCode"] ?: ""), | ||||
| "DepartureTime" to (row["DepartureTime"]?.toString() ?: ""), | "DepartureTime" to (row["DepartureTime"]?.toString() ?: ""), | ||||
| "ticketNo" to (row["ticketNo"] ?: ""), | "ticketNo" to (row["ticketNo"] ?: ""), | ||||
| "storeId" to (row["storeId"] ?: ""), | "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 | // Fix the method signature and return types | ||||
| open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | ||||
| println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical ===") | |||||
| println("today: ${LocalDate.now()}") | |||||
| println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===") | |||||
| println("userId filter: $userId") | 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) | val user = userService.find(userId).orElse(null) | ||||
| if (user == null) { | if (user == null) { | ||||
| println("❌ User not found: $userId") | println("❌ User not found: $userId") | ||||
| return emptyMap() | 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( | return mapOf( | ||||
| "pickOrder" to null as Any?, | |||||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||||
| "fgInfo" to null, | |||||
| "pickOrders" to emptyList<Any>() | |||||
| ) | ) | ||||
| } | } | ||||
| // Use the same SQL query but transform the results into hierarchical structure | |||||
| val pickOrderIdsStr = pickOrderIds.joinToString(",") | |||||
| val doPickOrderId = (doPickOrderInfo["do_pick_order_id"] as? Number)?.toLong() | |||||
| println("🔍 Found do_pick_order ID: $doPickOrderId") | |||||
| val sql = """ | |||||
| SELECT | |||||
| -- Pick Order Information | |||||
| po.id as pickOrderId, | |||||
| po.code as pickOrderCode, | |||||
| po.consoCode as pickOrderConsoCode, | |||||
| DATE_FORMAT(po.targetDate, '%Y-%m-%d') as pickOrderTargetDate, | |||||
| po.type as pickOrderType, | |||||
| po.status as pickOrderStatus, | |||||
| po.assignTo as pickOrderAssignTo, | |||||
| -- Pick Order Line Information | |||||
| pol.id as pickOrderLineId, | |||||
| pol.qty as pickOrderLineRequiredQty, | |||||
| pol.status as pickOrderLineStatus, | |||||
| -- Item Information | |||||
| i.id as itemId, | |||||
| i.code as itemCode, | |||||
| i.name as itemName, | |||||
| uc.code as uomCode, | |||||
| uc.udfudesc as uomDesc, | |||||
| uc.udfShortDesc as uomShortDesc, | |||||
| -- Lot Information | |||||
| ill.id as lotId, | |||||
| il.lotNo, | |||||
| DATE_FORMAT(il.expiryDate, '%Y-%m-%d') as expiryDate, | |||||
| w.name as location, | |||||
| COALESCE(uc.udfudesc, 'N/A') as stockUnit, | |||||
| -- ✅ 修改:直接使用 warehouse 作为路由信息 | |||||
| w.`order` as routerIndex, | |||||
| w.code as routerRoute, | |||||
| w.code as routerArea, | |||||
| -- ✅ FIXED: Set quantities to NULL for rejected lots | |||||
| CASE | |||||
| WHEN sol.status = 'rejected' THEN NULL | |||||
| ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) | |||||
| END as availableQty, | |||||
| -- Required quantity for this lot | |||||
| COALESCE(spl.qty, 0) as requiredQty, | |||||
| -- Actual picked quantity | |||||
| COALESCE(sol.qty, 0) as actualPickQty, | |||||
| -- Suggested pick lot information | |||||
| spl.id as suggestedPickLotId, | |||||
| ill.status as lotStatus, | |||||
| -- Stock out line information | |||||
| sol.id as stockOutLineId, | |||||
| sol.status as stockOutLineStatus, | |||||
| COALESCE(sol.qty, 0) as stockOutLineQty, | |||||
| -- Additional detailed fields | |||||
| COALESCE(ill.inQty, 0) as inQty, | |||||
| COALESCE(ill.outQty, 0) as outQty, | |||||
| COALESCE(ill.holdQty, 0) as holdQty, | |||||
| COALESCE(spl.suggestedLotLineId, sol.inventoryLotLineId) as debugSuggestedLotLineId, | |||||
| ill.inventoryLotId as debugInventoryLotId, | |||||
| // ✅ Step 2: 获取该 do_pick_order 下的所有 pick orders | |||||
| val pickOrdersSql = """ | |||||
| SELECT DISTINCT | |||||
| dpol.pick_order_id, | |||||
| dpol.pick_order_code, | |||||
| dpol.do_order_id, | |||||
| dpol.delivery_order_code, | |||||
| po.consoCode, | |||||
| po.status, | |||||
| DATE_FORMAT(po.targetDate, '%Y-%m-%d') as targetDate | |||||
| FROM fpsmsdb.do_pick_order_line dpol | |||||
| INNER JOIN fpsmsdb.pick_order po ON po.id = dpol.pick_order_id | |||||
| WHERE dpol.do_pick_order_id = :doPickOrderId | |||||
| AND dpol.deleted = 0 | |||||
| AND po.deleted = false | |||||
| ORDER BY dpol.pick_order_id | |||||
| """.trimIndent() | |||||
| val pickOrdersInfo = jdbcDao.queryForList(pickOrdersSql, mapOf("doPickOrderId" to doPickOrderId)) | |||||
| println("🔍 Found ${pickOrdersInfo.size} pick orders") | |||||
| // ✅ Step 3: 为每个 pick order 获取 lines 和 lots(包括 null stock 的) | |||||
| val pickOrders = pickOrdersInfo.map { poInfo -> | |||||
| val pickOrderId = (poInfo["pick_order_id"] as? Number)?.toLong() | |||||
| -- Calculate total picked quantity by ALL pick orders for this lot | |||||
| COALESCE(( | |||||
| SELECT SUM(sol_all.qty) | |||||
| FROM fpsmsdb.stock_out_line sol_all | |||||
| WHERE sol_all.inventoryLotLineId = ill.id | |||||
| AND sol_all.deleted = false | |||||
| AND sol_all.status IN ('pending', 'checked', 'partially_completed', 'completed') | |||||
| ), 0) as totalPickedByAllPickOrders, | |||||
| // ✅ 查询该 pick order 的所有 lines 和 lots | |||||
| val linesSql = """ | |||||
| SELECT | |||||
| po.id as pickOrderId, | |||||
| po.code as pickOrderCode, | |||||
| po.consoCode as pickOrderConsoCode, | |||||
| DATE_FORMAT(po.targetDate, '%Y-%m-%d') as pickOrderTargetDate, | |||||
| po.type as pickOrderType, | |||||
| po.status as pickOrderStatus, | |||||
| po.assignTo as pickOrderAssignTo, | |||||
| pol.id as pickOrderLineId, | |||||
| pol.qty as pickOrderLineRequiredQty, | |||||
| pol.status as pickOrderLineStatus, | |||||
| i.id as itemId, | |||||
| i.code as itemCode, | |||||
| i.name as itemName, | |||||
| uc.code as uomCode, | |||||
| uc.udfudesc as uomDesc, | |||||
| uc.udfShortDesc as uomShortDesc, | |||||
| ill.id as lotId, | |||||
| il.lotNo, | |||||
| DATE_FORMAT(il.expiryDate, '%Y-%m-%d') as expiryDate, | |||||
| w.name as location, | |||||
| COALESCE(uc.udfudesc, 'N/A') as stockUnit, | |||||
| w.`order` as routerIndex, | |||||
| w.code as routerRoute, | |||||
| CASE | |||||
| WHEN sol.status = 'rejected' THEN NULL | |||||
| ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) | |||||
| END as availableQty, | |||||
| COALESCE(spl.qty, 0) as requiredQty, | |||||
| COALESCE(sol.qty, 0) as actualPickQty, | |||||
| spl.id as suggestedPickLotId, | |||||
| ill.status as lotStatus, | |||||
| sol.id as stockOutLineId, | |||||
| sol.status as stockOutLineStatus, | |||||
| COALESCE(sol.qty, 0) as stockOutLineQty, | |||||
| COALESCE(ill.inQty, 0) as inQty, | |||||
| COALESCE(ill.outQty, 0) as outQty, | |||||
| COALESCE(ill.holdQty, 0) as holdQty, | |||||
| CASE | |||||
| WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' | |||||
| WHEN sol.status = 'rejected' THEN 'rejected' | |||||
| WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock' | |||||
| WHEN ill.status = 'unavailable' THEN 'status_unavailable' | |||||
| ELSE 'available' | |||||
| END as lotAvailability, | |||||
| CASE | |||||
| WHEN sol.status = 'completed' THEN 'completed' | |||||
| WHEN sol.status = 'rejected' THEN 'rejected' | |||||
| WHEN sol.status = 'created' THEN 'pending' | |||||
| ELSE 'pending' | |||||
| END as processingStatus | |||||
| FROM fpsmsdb.pick_order po | |||||
| JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false | |||||
| JOIN fpsmsdb.items i ON i.id = pol.itemId | |||||
| LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId | |||||
| -- ✅ 关键修改:使用 LEFT JOIN 来包含没有 lot 的 pick order lines | |||||
| LEFT JOIN ( | |||||
| SELECT spl.pickOrderLineId, spl.suggestedLotLineId AS lotLineId | |||||
| FROM fpsmsdb.suggested_pick_lot spl | |||||
| UNION | |||||
| SELECT sol.pickOrderLineId, sol.inventoryLotLineId | |||||
| FROM fpsmsdb.stock_out_line sol | |||||
| WHERE sol.deleted = false | |||||
| ) ll ON ll.pickOrderLineId = pol.id | |||||
| LEFT JOIN fpsmsdb.suggested_pick_lot spl | |||||
| ON spl.pickOrderLineId = pol.id AND spl.suggestedLotLineId = ll.lotLineId | |||||
| LEFT JOIN fpsmsdb.stock_out_line sol | |||||
| ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ll.lotLineId AND sol.deleted = false | |||||
| LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = ll.lotLineId AND ill.deleted = false | |||||
| LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId AND il.deleted = false | |||||
| LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||||
| WHERE po.id = :pickOrderId | |||||
| AND po.deleted = false | |||||
| ORDER BY | |||||
| COALESCE(w.`order`, 999999) ASC, | |||||
| pol.id ASC, | |||||
| il.lotNo ASC | |||||
| """.trimIndent() | |||||
| -- Calculate remaining available quantity correctly | |||||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as remainingAfterAllPickOrders, | |||||
| val linesResults = jdbcDao.queryForList(linesSql, mapOf("pickOrderId" to pickOrderId)) | |||||
| println("🔍 Pick order $pickOrderId has ${linesResults.size} line-lot records") | |||||
| -- Lot availability status | |||||
| CASE | |||||
| WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' | |||||
| WHEN sol.status = 'rejected' THEN 'rejected' | |||||
| WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock' | |||||
| WHEN ill.status = 'unavailable' THEN 'status_unavailable' | |||||
| ELSE 'available' | |||||
| END as lotAvailability, | |||||
| // ✅ 按 pickOrderLineId 分组 | |||||
| val lineGroups = linesResults.groupBy { (it["pickOrderLineId"] as? Number)?.toLong() } | |||||
| -- Processing status | |||||
| CASE | |||||
| WHEN sol.status = 'completed' THEN 'completed' | |||||
| WHEN sol.status = 'rejected' THEN 'rejected' | |||||
| WHEN sol.status = 'created' THEN 'pending' | |||||
| ELSE 'pending' | |||||
| END as processingStatus | |||||
| val pickOrderLines = lineGroups.map { (lineId, lineRows) -> | |||||
| val firstLineRow = lineRows.firstOrNull() | |||||
| if (firstLineRow == null) { | |||||
| null | |||||
| } else { | |||||
| // ✅ 构建 lots 列表(如果没有 lot,返回空数组) | |||||
| val lots = if (lineRows.any { it["lotId"] != null }) { | |||||
| lineRows.filter { it["lotId"] != null }.map { lotRow -> | |||||
| mapOf( | |||||
| "id" to lotRow["lotId"], | |||||
| "lotNo" to lotRow["lotNo"], | |||||
| "expiryDate" to lotRow["expiryDate"], | |||||
| "location" to lotRow["location"], | |||||
| "stockUnit" to lotRow["stockUnit"], | |||||
| "availableQty" to lotRow["availableQty"], | |||||
| "requiredQty" to lotRow["requiredQty"], | |||||
| "actualPickQty" to lotRow["actualPickQty"], | |||||
| "inQty" to lotRow["inQty"], | |||||
| "outQty" to lotRow["outQty"], | |||||
| "holdQty" to lotRow["holdQty"], | |||||
| "lotStatus" to lotRow["lotStatus"], | |||||
| "lotAvailability" to lotRow["lotAvailability"], | |||||
| "processingStatus" to lotRow["processingStatus"], | |||||
| "suggestedPickLotId" to lotRow["suggestedPickLotId"], | |||||
| "stockOutLineId" to lotRow["stockOutLineId"], | |||||
| "stockOutLineStatus" to lotRow["stockOutLineStatus"], | |||||
| "stockOutLineQty" to lotRow["stockOutLineQty"], | |||||
| "router" to mapOf( | |||||
| "id" to null, | |||||
| "index" to lotRow["routerIndex"], | |||||
| "route" to lotRow["routerRoute"], | |||||
| "area" to lotRow["routerRoute"], | |||||
| "itemCode" to lotRow["itemId"], | |||||
| "itemName" to lotRow["itemName"], | |||||
| "uomId" to lotRow["uomCode"], | |||||
| "noofCarton" to lotRow["requiredQty"] | |||||
| ) | |||||
| ) | |||||
| } | |||||
| } else { | |||||
| emptyList() // ✅ 返回空数组而不是 null | |||||
| } | |||||
| mapOf( | |||||
| "id" to lineId, | |||||
| "requiredQty" to firstLineRow["pickOrderLineRequiredQty"], | |||||
| "status" to firstLineRow["pickOrderLineStatus"], | |||||
| "item" to mapOf( | |||||
| "id" to firstLineRow["itemId"], | |||||
| "code" to firstLineRow["itemCode"], | |||||
| "name" to firstLineRow["itemName"], | |||||
| "uomCode" to firstLineRow["uomCode"], | |||||
| "uomDesc" to firstLineRow["uomDesc"], | |||||
| "uomShortDesc" to firstLineRow["uomShortDesc"], | |||||
| ), | |||||
| "lots" to lots // ✅ 即使是空数组也返回 | |||||
| ) | |||||
| } | |||||
| }.filterNotNull() | |||||
| FROM fpsmsdb.pick_order po | |||||
| JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id | |||||
| JOIN fpsmsdb.items i ON i.id = pol.itemId | |||||
| LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId | |||||
| -- Base lot links: all lot lines referenced by either suggestions or stock out lines for this POL | |||||
| LEFT JOIN ( | |||||
| SELECT spl.pickOrderLineId AS pickOrderLineId, spl.suggestedLotLineId AS lotLineId | |||||
| FROM fpsmsdb.suggested_pick_lot spl | |||||
| UNION | |||||
| SELECT sol.pickOrderLineId, sol.inventoryLotLineId | |||||
| FROM fpsmsdb.stock_out_line sol | |||||
| WHERE sol.deleted = false | |||||
| ) ll ON ll.pickOrderLineId = pol.id | |||||
| -- Re-bind spl/sol strictly on the same lot line | |||||
| LEFT JOIN fpsmsdb.suggested_pick_lot spl | |||||
| ON spl.pickOrderLineId = pol.id AND spl.suggestedLotLineId = ll.lotLineId | |||||
| LEFT JOIN fpsmsdb.stock_out_line sol | |||||
| ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ll.lotLineId AND sol.deleted = false | |||||
| LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = ll.lotLineId | |||||
| -- ✅ 删除 router 相关的 JOIN | |||||
| -- LEFT JOIN router r ON r.inventoryLotId = ill.inventoryLotId AND r.deleted = false | |||||
| -- LEFT JOIN router_order ro ON r.router_id = ro.id | |||||
| LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||||
| LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId -- ✅ 保留 warehouse JOIN | |||||
| WHERE po.deleted = false | |||||
| AND po.id IN ($pickOrderIdsStr) | |||||
| AND pol.deleted = false | |||||
| AND po.status IN ('PENDING', 'RELEASED', 'COMPLETED') | |||||
| AND po.assignTo = :userId | |||||
| AND ill.deleted = false | |||||
| AND il.deleted = false | |||||
| AND ll.lotLineId IS NOT NULL | |||||
| AND (spl.pickOrderLineId IS NOT NULL OR sol.pickOrderLineId IS NOT NULL) | |||||
| ORDER BY | |||||
| CASE WHEN sol.status = 'rejected' THEN 0 ELSE 1 END, | |||||
| COALESCE(w.`order`, 999999) ASC, -- ✅ 使用 warehouse.order 排序 | |||||
| po.code ASC, | |||||
| i.code ASC, | |||||
| il.expiryDate ASC, | |||||
| il.lotNo ASC | |||||
| """.trimIndent() | |||||
| println("�� Executing SQL for hierarchical structure: $sql") | |||||
| println("�� With parameters: userId = $userId, pickOrderIds = $pickOrderIdsStr") | |||||
| val results = jdbcDao.queryForList(sql, mapOf("userId" to userId)) | |||||
| println("✅ Total result count: ${results.size}") | |||||
| // Filter out lots with null availableQty (rejected lots) | |||||
| // val filteredResults = results.filter { row -> | |||||
| //val availableQty = row["availableQty"] | |||||
| // availableQty != null | |||||
| // } | |||||
| val filteredResults = results | |||||
| println("✅ Filtered result count: ${filteredResults.size}") | |||||
| // ✅ Transform flat results into hierarchical structure | |||||
| if (filteredResults.isEmpty()) { | |||||
| return mapOf( | |||||
| "pickOrder" to null as Any?, | |||||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||||
| mapOf( | |||||
| "pickOrderId" to pickOrderId, | |||||
| "pickOrderCode" to poInfo["pick_order_code"], | |||||
| "doOrderId" to poInfo["do_order_id"], | |||||
| "deliveryOrderCode" to poInfo["delivery_order_code"], | |||||
| "consoCode" to poInfo["consoCode"], | |||||
| "status" to poInfo["status"], | |||||
| "targetDate" to poInfo["targetDate"], | |||||
| "pickOrderLines" to pickOrderLines | |||||
| ) | ) | ||||
| } | } | ||||
| // Get pick order info from first row (all rows have same pick order info) | |||||
| val firstRow = filteredResults.first() | |||||
| val pickOrderInfo = mapOf( | |||||
| "id" to firstRow["pickOrderId"], | |||||
| "code" to firstRow["pickOrderCode"], | |||||
| "consoCode" to firstRow["pickOrderConsoCode"], | |||||
| "targetDate" to firstRow["pickOrderTargetDate"], | |||||
| "type" to firstRow["pickOrderType"], | |||||
| "status" to firstRow["pickOrderStatus"], | |||||
| "assignTo" to firstRow["pickOrderAssignTo"] | |||||
| // ✅ 构建 FG 信息 | |||||
| val fgInfo = mapOf( | |||||
| "doPickOrderId" to doPickOrderId, | |||||
| "ticketNo" to doPickOrderInfo["ticket_no"], | |||||
| "storeId" to doPickOrderInfo["store_id"], | |||||
| "shopCode" to doPickOrderInfo["ShopCode"], | |||||
| "shopName" to doPickOrderInfo["ShopName"], | |||||
| "truckLanceCode" to doPickOrderInfo["TruckLanceCode"], | |||||
| "departureTime" to doPickOrderInfo["truck_departure_time"] | |||||
| ) | ) | ||||
| // Group by pick order line ID to create hierarchical structure | |||||
| val pickOrderLinesMap = filteredResults | |||||
| .groupBy { it["pickOrderLineId"] as Number } | |||||
| .map { (pickOrderLineId, lots) -> | |||||
| val firstLot = lots.first() | |||||
| // Item information (same for all lots of this line) | |||||
| val itemInfo = mapOf( | |||||
| "id" to firstLot["itemId"], | |||||
| "code" to firstLot["itemCode"], | |||||
| "name" to firstLot["itemName"], | |||||
| "uomCode" to firstLot["uomCode"], | |||||
| "uomDesc" to firstLot["uomDesc"] | |||||
| ) | |||||
| // Transform lots for this pick order line | |||||
| val lotsInfo = lots.map { lot -> | |||||
| mapOf( | |||||
| "id" to lot["lotId"], | |||||
| "lotNo" to lot["lotNo"], | |||||
| "expiryDate" to lot["expiryDate"], | |||||
| "location" to lot["location"], | |||||
| "stockUnit" to lot["stockUnit"], | |||||
| "availableQty" to lot["availableQty"], | |||||
| "requiredQty" to lot["requiredQty"], | |||||
| "actualPickQty" to lot["actualPickQty"], | |||||
| "inQty" to lot["inQty"], | |||||
| "outQty" to lot["outQty"], | |||||
| "holdQty" to lot["holdQty"], | |||||
| "lotStatus" to lot["lotStatus"], | |||||
| "lotAvailability" to lot["lotAvailability"], | |||||
| "processingStatus" to lot["processingStatus"], | |||||
| "suggestedPickLotId" to lot["suggestedPickLotId"], | |||||
| "stockOutLineId" to lot["stockOutLineId"], | |||||
| "stockOutLineStatus" to lot["stockOutLineStatus"], | |||||
| "stockOutLineQty" to lot["stockOutLineQty"], | |||||
| "router" to mapOf( | |||||
| "id" to lot["routerId"], | |||||
| "index" to lot["routerIndex"], | |||||
| "route" to lot["routerRoute"], | |||||
| "area" to lot["routerArea"], | |||||
| "itemCode" to firstLot["itemId"], | |||||
| "itemName" to firstLot["itemName"], | |||||
| "uomId" to firstLot["uomShortDesc"], | |||||
| "noofCarton" to lot["requiredQty"] // Use required qty as carton count | |||||
| ) | |||||
| ) | |||||
| } | |||||
| // Pick order line with item and lots | |||||
| mapOf( | |||||
| "id" to pickOrderLineId, | |||||
| "requiredQty" to firstLot["pickOrderLineRequiredQty"], | |||||
| "status" to firstLot["pickOrderLineStatus"], | |||||
| "item" to itemInfo, | |||||
| "lots" to lotsInfo | |||||
| ) | |||||
| } | |||||
| return mapOf( | return mapOf( | ||||
| "pickOrder" to pickOrderInfo as Any?, | |||||
| "pickOrderLines" to pickOrderLinesMap as Any? | |||||
| "fgInfo" to fgInfo, | |||||
| "pickOrders" to pickOrders | |||||
| ) | ) | ||||
| } | } | ||||
| @@ -301,4 +301,5 @@ fun getCompletedDoPickOrders( | |||||
| fun getLotDetailsByPickOrderId(@PathVariable pickOrderId: Long): List<Map<String, Any>> { | fun getLotDetailsByPickOrderId(@PathVariable pickOrderId: Long): List<Map<String, Any>> { | ||||
| return pickOrderService.getLotDetailsByPickOrderId(pickOrderId); | return pickOrderService.getLotDetailsByPickOrderId(pickOrderId); | ||||
| } | } | ||||
| } | } | ||||
| @@ -209,6 +209,19 @@ existingSuggestions.forEach { existingSugg -> | |||||
| pickOrderLine = line | pickOrderLine = line | ||||
| qty = remainingQtyToAllocate // ✅ 保存销售单位 | 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) | 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() | |||||
| } | |||||
| } | |||||
| } | } | ||||