diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt index 2ca6359..695a2cc 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt @@ -12,6 +12,7 @@ import com.ffii.fpsms.modules.stock.entity.InventoryLotLine import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository import com.ffii.fpsms.modules.stock.entity.InventoryRepository import com.ffii.fpsms.modules.stock.service.StockOutLineService +import org.springframework.context.annotation.Lazy import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService @@ -32,7 +33,7 @@ import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus import com.ffii.fpsms.modules.stock.web.model.StockOutStatus - +import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus import com.ffii.fpsms.modules.pickOrder.web.models.* import com.ffii.fpsms.modules.common.SecurityUtils @@ -43,7 +44,10 @@ import com.ffii.fpsms.modules.common.CodeGenerator import com.ffii.fpsms.modules.stock.entity.StockLedger import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository - +import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository +import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus +import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository +import org.springframework.beans.factory.annotation.Value @Service open class PickExecutionIssueService( private val pickExecutionIssueRepository: PickExecutionIssueRepository, @@ -54,15 +58,21 @@ open class PickExecutionIssueService( private val pickOrderRepository: PickOrderRepository, private val jdbcDao: JdbcDao, private val stockOutRepository: StockOutRepository, - private val stockOutLineService: StockOutLineService, + @Lazy private val stockOutLineService: StockOutLineService, private val pickOrderLineRepository: PickOrderLineRepository, private val doPickOrderService: DoPickOrderService, private val joPickOrderRepository: JoPickOrderRepository, private val joPickOrderRecordRepository: JoPickOrderRecordRepository, private val itemsRepository: ItemsRepository, - private val stockLedgerRepository: StockLedgerRepository + private val stockLedgerRepository: StockLedgerRepository, + private val deliveryOrderRepository: DeliveryOrderRepository, + private val suggestedPickLotRepository: SuggestPickLotRepository, + private val doPickOrderRepository: DoPickOrderRepository ) { + // 配置属性:是否在批次拒绝时自动重新建议批次(默认为 false,不自动重新建议) + @Value("\${pick.execution.auto-resuggest-on-rejection:false}") + private val autoResuggestOnLotRejection: Boolean = false @Transactional(rollbackFor = [Exception::class]) open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse { @@ -106,24 +116,23 @@ open class PickExecutionIssueService( val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO val missQty = request.missQty ?: BigDecimal.ZERO val badItemQty = request.badItemQty ?: BigDecimal.ZERO - + val badReason = request.badReason ?: "quantity_problem" // 4. 计算 issueQty(实际的问题数量) val issueQty = when { // 情况1: 已拣完但有坏品 actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { badItemQty // issueQty = badItemQty } - - // 情况2: 未拣完(有缺失或坏品) + badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> { + badItemQty + } + actualPickQty < requiredQty -> { - // issueQty = bookQty - actualPickQty val calculatedIssueQty = bookQty.subtract(actualPickQty) - if (missQty > BigDecimal.ZERO && missQty > calculatedIssueQty) { println("⚠️ Warning: User reported missQty (${missQty}) exceeds calculated issueQty (${calculatedIssueQty})") println(" BookQty: ${bookQty}, ActualPickQty: ${actualPickQty}") } - calculatedIssueQty } @@ -273,12 +282,22 @@ open class PickExecutionIssueService( val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null) val consoCode = pickOrderForCompletion?.consoCode - if (consoCode != null) { + if (!consoCode.isNullOrBlank()) { + // 优先走原来的 consoCode 逻辑(兼容已有 DO 流程) println("🔍 Checking if pick order $consoCode should be completed after lot rejection...") try { checkAndCompletePickOrder(consoCode) } catch (e: Exception) { - println("⚠️ Error checking pick order completion: ${e.message}") + println("⚠️ Error checking pick order completion by consoCode: ${e.message}") + } + } else if (pickOrderForCompletion != null) { + // 🔁 没有 consoCode 的情况:改用 pickOrderId 去检查是否可以完结 + println("🔍 Checking if pick order ${pickOrderForCompletion.code} (ID=${pickOrderForCompletion.id}) " + + "should be completed after lot rejection (no consoCode)...") + try { + checkAndCompletePickOrderByPickOrderId(pickOrderForCompletion.id!!) + } catch (e: Exception) { + println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}") } } @@ -460,8 +479,143 @@ private fun checkAndCompletePickOrder(consoCode: String) { println("⏳ Still have ${unfinishedLines.size} unfinished lines, pick order not completed yet") } } -// FPSMS-backend/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt -// 修复:处理有部分拣货但有 miss item 的情况 +@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class]) +private fun checkAndCompletePickOrderByPickOrderId(pickOrderId: Long) { + println("=== DEBUG: checkAndCompletePickOrderByPickOrderId ===") + println("pickOrderId: $pickOrderId") + + val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) + if (pickOrder == null) { + println("❌ No pick_order found for id: $pickOrderId") + return + } + + // 1. 找出这个 pick order 下所有的 pick order line + val pickOrderLineIds = pickOrder.pickOrderLines.mapNotNull { it.id } + if (pickOrderLineIds.isEmpty()) { + println("⚠️ Pick order ${pickOrder.code} has no lines, skip completion check") + return + } + + // 2. 找出这些行对应的所有 stock_out_line + val stockOutLines = stockOutLineRepository.findAllByPickOrderLineIdInAndDeletedFalse(pickOrderLineIds) + println("Total stock out lines for pickOrder ${pickOrder.code}: ${stockOutLines.size}") + + // 3. 计算未完成的行(既不是 COMPLETE 也不是 REJECTED) + val unfinishedLines = stockOutLines.filter { + it.status != StockOutLineStatus.COMPLETE.status && + it.status != StockOutLineStatus.REJECTED.status + } + println("📊 Stock out lines: ${stockOutLines.size}, Unfinished: ${unfinishedLines.size}") + + // 4. 如果还有未完成的,不做完结 + if (unfinishedLines.isNotEmpty()) { + println("⏳ Still have ${unfinishedLines.size} unfinished lines for pickOrder ${pickOrder.code}, not completed yet") + return + } + + // 5. 所有行都 COMPLETE/REJECTED → 完成相关 stock_out + pick_order_line + pick_order + println("✅ All stock out lines completed or rejected for pickOrder ${pickOrder.code}, completing pick order...") + + // 5.1 完成相关的 stock_out(可能有多个) + val stockOuts = stockOutLines.mapNotNull { it.stockOut }.distinct() + stockOuts.forEach { so -> + so.status = StockOutStatus.COMPLETE.status + stockOutRepository.saveAndFlush(so) + println(" - Updated stock_out ${so.id} to COMPLETE") + } + + // 5.2 更新所有 pick_order_line 状态为 COMPLETED + pickOrder.pickOrderLines.forEach { line -> + line.status = PickOrderLineStatus.COMPLETED + println(" - Updated pick_order_line ${line.id} to COMPLETED") + } + pickOrderLineRepository.saveAll(pickOrder.pickOrderLines) + + // 5.3 更新 pick_order 本身状态 + pickOrder.status = PickOrderStatus.COMPLETED + pickOrder.completeDate = LocalDateTime.now() + pickOrderRepository.save(pickOrder) + println(" - Updated pick_order ${pickOrder.code} to COMPLETED") + + // 5.4 DO 相关处理 + try { + // 先获取 doOrderIds,因为 removeDoPickOrdersForPickOrder 会删除记录 + val doPickOrders = doPickOrderService.findByPickOrderId(pickOrderId) + val doOrderIds = doPickOrders.mapNotNull { it.doOrderId }.distinct() + + val removedCount = doPickOrderService.removeDoPickOrdersForPickOrder(pickOrderId) + println(" - Removed $removedCount do_pick_order records for completed pick order $pickOrderId") + + doPickOrderService.completeDoPickOrderRecordsForPickOrder(pickOrderId) + println(" - Updated do_pick_order_record status to COMPLETED for pick order $pickOrderId") + + // ✅ 5.4.1 检查并更新 DeliveryOrder 状态 + // 如果该 DO 的所有 pick orders 都已完成,则更新 DeliveryOrder 状态 + doOrderIds.forEach { doOrderId -> + try { + // 使用 SQL 查询检查该 DO 的所有 pick orders 是否都已完成 + val sql = """ + SELECT COUNT(DISTINCT po.id) as total_pick_orders, + COUNT(DISTINCT CASE WHEN po.status = 'COMPLETED' THEN po.id END) as completed_pick_orders + FROM pick_order po + JOIN do_pick_order_record dpor ON dpor.pickOrderId = po.id + WHERE dpor.doOrderId = :doOrderId + AND po.deleted = false + """.trimIndent() + + val result = jdbcDao.queryForList(sql, mapOf("doOrderId" to doOrderId)) + if (result.isNotEmpty()) { + val row = result[0] + val totalPickOrders = (row["total_pick_orders"] as? Number)?.toInt() ?: 0 + val completedPickOrders = (row["completed_pick_orders"] as? Number)?.toInt() ?: 0 + + println(" - DO $doOrderId: $completedPickOrders/$totalPickOrders pick orders completed") + + if (totalPickOrders > 0 && totalPickOrders == completedPickOrders) { + val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doOrderId) + if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) { + deliveryOrder.status = DeliveryOrderStatus.COMPLETED + deliveryOrderRepository.save(deliveryOrder) + println(" - ✅ Updated delivery order $doOrderId to COMPLETED") + } + } + } + } catch (e: Exception) { + println("⚠️ Error checking DeliveryOrder completion for doOrderId $doOrderId: ${e.message}") + e.printStackTrace() + } + } + } catch (e: Exception) { + println("⚠️ Error updating DO pick order records: ${e.message}") + e.printStackTrace() + } + + // 5.5 JO 相关处理(复用 consoCode 版本里的逻辑) + if (pickOrder.jobOrder != null) { + try { + val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrderId) + joPickOrders.forEach { + it.ticketCompleteTime = LocalDateTime.now() + } + joPickOrderRepository.saveAll(joPickOrders) + + val joPickOrderRecords = joPickOrderRecordRepository.findByPickOrderId(pickOrderId) + joPickOrderRecords.forEach { + it.ticketCompleteTime = LocalDateTime.now() + } + joPickOrderRecordRepository.saveAll(joPickOrderRecords) + + println(" - Set jo_pick_order ticketCompleteTime for pick order $pickOrderId") + } catch (e: Exception) { + println("⚠️ Error updating JO pick order records: ${e.message}") + e.printStackTrace() + } + } + + println("🎉 Pick order ${pickOrder.code} completed successfully (by pickOrderId, with DO/JO)!") +} + @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class]) private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, actualPickQty: BigDecimal, missQty: BigDecimal) { println("=== HANDLING MISS ITEM WITH PARTIAL PICK (NEW LOGIC: DON'T REJECT LOT) ===") @@ -472,6 +626,10 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac val itemId = request.itemId ?: return val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) + // ✅ 修复:在更新 lot line 之前获取 inventory 的 onHandQty + val inventoryBeforeUpdate = inventoryRepository.findByItemId(itemId).orElse(null) + val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + if (inventoryLotLine != null) { // ✅ 修改:更新 outQty 为 actualPickQty val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO @@ -484,11 +642,6 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac val newHoldQty = (currentHoldQty - requiredQty).coerceAtLeast(BigDecimal.ZERO) inventoryLotLine.holdQty = newHoldQty - // ✅ 修改:不设置 status 为 UNAVAILABLE - // if (missQty > BigDecimal.ZERO) { - // inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE - // } - inventoryLotLine.modified = LocalDateTime.now() inventoryLotLine.modifiedBy = "system" inventoryLotLineRepository.saveAndFlush(inventoryLotLine) @@ -500,7 +653,6 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac } // ✅ 修改:不更新 unavailableQty(因为不 reject lot) - // updateInventoryUnavailableQty(itemId, missQty) // 删除这行 // ✅ 修改:不 reject stock_out_line,根据 actualPickQty 设置状态 val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( @@ -527,12 +679,22 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac println("Updated stock out line ${stockOutLine.id} status to: ${newStatus} (NOT rejected)") - // 创建 stock_ledger 记录 - createStockLedgerForStockOut(savedStockOutLine, "Nor") + // ✅ 修复:使用更新前的 onHandQty 计算 balance + val balance = onHandQtyBeforeUpdate - actualPickQtyDouble + createStockLedgerForStockOut(savedStockOutLine, "Nor", balance) + } + + // ✅ 修复:检查 pick order line 是否应该标记为完成 + if (request.pickOrderLineId != null) { + try { + checkPickOrderLineCompletion(request.pickOrderLineId) + println("Miss item with partial pick: Checked pick order line ${request.pickOrderLineId} completion status") + } catch (e: Exception) { + println("⚠️ Error checking pick order line completion: ${e.message}") + } } // ✅ 修改:不重新建议拣货批次(因为 lot 仍然可用) - // resuggestPickOrder(request.pickOrderId) // 删除这行 println("Miss item with partial pick: Did NOT resuggest pick order (lot remains available)") } @@ -546,8 +708,11 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD val itemId = request.itemId ?: return val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) + // ✅ 修复:在更新 lot line 之前获取 inventory 的 onHandQty + val inventoryBeforeUpdate = inventoryRepository.findByItemId(itemId).orElse(null) + val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + if (inventoryLotLine != null) { - // ✅ 修改:不设置 status 为 UNAVAILABLE // ✅ 修改:释放 holdQty(减少 holdQty) val currentHoldQty = inventoryLotLine.holdQty ?: BigDecimal.ZERO val requiredQty = request.requiredQty ?: BigDecimal.ZERO @@ -574,7 +739,6 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD } // ✅ 修改:不更新 unavailableQty(因为不 reject lot) - // updateInventoryUnavailableQty(itemId, remainingQty) // 删除这行 // ✅ 修改:不 reject stock_out_line,根据 actualPickQty 设置状态 val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( @@ -586,7 +750,7 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD val requiredQty = request.requiredQty?.toDouble() ?: 0.0 val actualPickQtyDouble = request.actualPickQty?.toDouble() ?: 0.0 - // 设置状态:如果 actualPickQty >= requiredQty,则为 completed,否则为 partially_completed + // 设置状态:如果 actualPickQty >= requiredQty,则为 completed,否则为 completed val newStatus = if (actualPickQtyDouble >= requiredQty) { "completed" } else { @@ -601,50 +765,25 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD println("Updated stock out line ${stockOutLine.id} status to: ${newStatus} (NOT rejected)") - // 创建 stock_ledger 记录(如果有 actualPickQty) + // ✅ 修复:创建 stock_ledger 记录(如果有 actualPickQty),使用更新前的 onHandQty 计算 balance if (actualPickQtyDouble > 0) { - createStockLedgerForStockOut(savedStockOutLine, "Nor") + val balance = onHandQtyBeforeUpdate - actualPickQtyDouble + createStockLedgerForStockOut(savedStockOutLine, "Nor", balance) } } - // ✅ NEW: If all stock_out_line under this pick order line are finished (completed/rejected), - // mark pick_order_line as COMPLETED. This is required for "miss all required qty" flows - // where qty can be 0 but picking decision is finalized via issue form. - try { - stockOutLineRepository.flush() - - val allStockOutLines = stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(request.pickOrderLineId) - val unfinishedLines = allStockOutLines.filter { - it.status != StockOutLineStatus.COMPLETE.status && - it.status != StockOutLineStatus.REJECTED.status - } - - println("Miss item: Checking pick order line ${request.pickOrderLineId}") - println(" Total stock out lines: ${allStockOutLines.size}") - println(" Unfinished lines: ${unfinishedLines.size}") - unfinishedLines.forEach { line -> - println(" - Line ${line.id}: status=${line.status}") - } - - if (unfinishedLines.isEmpty() && allStockOutLines.isNotEmpty()) { - val pickOrderLine = pickOrderLineRepository.findById(request.pickOrderLineId).orElse(null) - if (pickOrderLine != null) { - pickOrderLine.status = PickOrderLineStatus.COMPLETED - pickOrderLineRepository.saveAndFlush(pickOrderLine) - println("✅ Miss item: Updated pick order line ${request.pickOrderLineId} status to COMPLETED") - } else { - println("⚠️ Miss item: Pick order line ${request.pickOrderLineId} not found") - } - } else { - println("⚠️ Miss item: Pick order line ${request.pickOrderLineId} still has ${unfinishedLines.size} unfinished lines") + // ✅ 修复:检查 pick order line 是否应该标记为完成 + if (request.pickOrderLineId != null) { + try { + checkPickOrderLineCompletion(request.pickOrderLineId) + println("Miss item only: Checked pick order line ${request.pickOrderLineId} completion status") + } catch (e: Exception) { + println("⚠️ Error checking pick order line completion: ${e.message}") + e.printStackTrace() } - } catch (e: Exception) { - println("⚠️ Miss item: Error checking pick order line completion: ${e.message}") - e.printStackTrace() } // ✅ 修改:不重新建议拣货批次(因为 lot 仍然可用) - // resuggestPickOrder(request.pickOrderId) // 删除这行 println("Miss item: Did NOT resuggest pick order (lot remains available)") } @@ -667,6 +806,22 @@ private fun handleBadItemOnly(request: PickExecutionIssueRequest, badItemQty: Bi inventoryLotLineRepository.save(inventoryLotLine) println("Bad item only: Set lot ${lotId} status to UNAVAILABLE") + + // ✅ NEW: Reject ALL stockOutLines using this lot across ALL pick orders + val allStockOutLinesForLot = stockOutLineRepository + .findAllByInventoryLotLineIdInAndNotCompletedOrRejected(listOf(lotId)) + + allStockOutLinesForLot.forEach { sol -> + sol.status = StockOutLineStatus.REJECTED.status + sol.modified = LocalDateTime.now() + sol.modifiedBy = "system" + println(" - Rejecting stock out line ${sol.id} (pickOrderLine: ${sol.pickOrderLine?.id}) for unavailable lot $lotId") + } + + if (allStockOutLinesForLot.isNotEmpty()) { + stockOutLineRepository.saveAllAndFlush(allStockOutLinesForLot) + println("✅ Rejected ${allStockOutLinesForLot.size} stockOutLines for unavailable lot $lotId (across all pick orders)") + } } // 修复:更新 inventory 表的 unavailableQty @@ -675,12 +830,16 @@ private fun handleBadItemOnly(request: PickExecutionIssueRequest, badItemQty: Bi // 修复:更新 stock_out_line 状态为 rejected updateStockOutLineStatus(request, "rejected") - // 重新建议拣货批次 - try { - resuggestPickOrder(request.pickOrderId) - println("Resuggested pick order for bad item qty: ${badItemQty}") - } catch (e: Exception) { - println("Error during resuggest in handleBadItemOnly: ${e.message}") + // ✅ 优化:根据配置决定是否重新建议拣货批次(默认不自动重新建议) + if (autoResuggestOnLotRejection) { + try { + resuggestPickOrder(request.pickOrderId) + println("Resuggested pick order for bad item qty: ${badItemQty}") + } catch (e: Exception) { + println("Error during resuggest in handleBadItemOnly: ${e.message}") + } + } else { + println("Skipped resuggest for bad item (autoResuggestOnLotRejection=false). User can scan another lot to switch.") } } @@ -706,6 +865,22 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty println(" - Miss Qty: ${missQty}") println(" - Bad Item Qty: ${badItemQty}") println(" - Total Unavailable Qty: ${totalUnavailableQty}") + + // ✅ NEW: Reject ALL stockOutLines using this lot across ALL pick orders + val allStockOutLinesForLot = stockOutLineRepository + .findAllByInventoryLotLineIdInAndNotCompletedOrRejected(listOf(lotId)) + + allStockOutLinesForLot.forEach { sol -> + sol.status = StockOutLineStatus.REJECTED.status + sol.modified = LocalDateTime.now() + sol.modifiedBy = "system" + println(" - Rejecting stock out line ${sol.id} (pickOrderLine: ${sol.pickOrderLine?.id}) for unavailable lot $lotId") + } + + if (allStockOutLinesForLot.isNotEmpty()) { + stockOutLineRepository.saveAllAndFlush(allStockOutLinesForLot) + println("✅ Rejected ${allStockOutLinesForLot.size} stockOutLines for unavailable lot $lotId (across all pick orders)") + } } // 修复:更新 inventory 表的 unavailableQty @@ -714,12 +889,16 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty // 修复:更新 stock_out_line 状态为 rejected updateStockOutLineStatus(request, "rejected") - // 重新建议拣货批次 - try { - resuggestPickOrder(request.pickOrderId) - println("Resuggested pick order for both miss qty: ${missQty} and bad item qty: ${badItemQty}") - } catch (e: Exception) { - println("Error during resuggest in handleBothMissAndBadItem: ${e.message}") + // ✅ 优化:根据配置决定是否重新建议拣货批次(默认不自动重新建议) + if (autoResuggestOnLotRejection) { + try { + resuggestPickOrder(request.pickOrderId) + println("Resuggested pick order for both miss qty: ${missQty} and bad item qty: ${badItemQty}") + } catch (e: Exception) { + println("Error during resuggest in handleBothMissAndBadItem: ${e.message}") + } + } else { + println("Skipped resuggest for both miss and bad item (autoResuggestOnLotRejection=false). User can scan another lot to switch.") } } @@ -728,14 +907,21 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty private fun handleNormalPick(request: PickExecutionIssueRequest, actualPickQty: BigDecimal) { println("=== HANDLING NORMAL PICK ===") - // 修复:更新 stock_out_line,但不要累积 qty + val lotId = request.lotId ?: return + val itemId = request.itemId ?: return + + // ✅ 修复:在更新 lot line 之前获取 inventory 的 onHandQty + val inventoryBeforeUpdate = inventoryRepository.findByItemId(itemId).orElse(null) + val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + + // ✅ 修复:更新 stock_out_line,但不要累积 qty val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( request.pickOrderLineId, request.lotId ?: 0L ) stockOutLines.forEach { stockOutLine -> - // 修复:直接设置 qty 为 actualPickQty,不要累积 + // ✅ 修复:直接设置 qty 为 actualPickQty,不要累积 val requiredQty = request.requiredQty?.toDouble() ?: 0.0 val actualPickQtyDouble = actualPickQty.toDouble() val newStatus = if (actualPickQtyDouble >= requiredQty) { @@ -745,22 +931,22 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty } stockOutLine.status = newStatus - stockOutLine.qty = actualPickQtyDouble // 直接设置,不累积 + stockOutLine.qty = actualPickQtyDouble // ✅ 直接设置,不累积 stockOutLine.modified = LocalDateTime.now() stockOutLine.modifiedBy = "system" val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) // 使用 saveAndFlush println("Updated stock out line ${stockOutLine.id}: status=${newStatus}, qty=${actualPickQtyDouble}") - // 修复:为正常拣货创建 stock_ledger 记录 - createStockLedgerForStockOut(savedStockOutLine, "Nor") + // ✅ 修复:为正常拣货创建 stock_ledger 记录,使用更新前的 onHandQty 计算 balance + val balance = onHandQtyBeforeUpdate - actualPickQtyDouble + createStockLedgerForStockOut(savedStockOutLine, "Nor", balance) } - // 修复:更新 inventory_lot_line 的 outQty - val lotId = request.lotId ?: return + // ✅ 修复:更新 inventory_lot_line 的 outQty val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) if (inventoryLotLine != null) { - // 修复:计算新的 outQty,考虑之前的 outQty + // ✅ 修复:计算新的 outQty,考虑之前的 outQty val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO val previousPickedQty = currentOutQty.minus(actualPickQty) // 计算之前已拣的数量 val newOutQty = previousPickedQty.add(actualPickQty) // 更新为新的总拣货数量 @@ -787,7 +973,6 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty } } - // 修复:更新 stock_out_line 状态 private fun updateStockOutLineStatus(request: PickExecutionIssueRequest, status: String) { val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( request.pickOrderLineId, @@ -810,12 +995,27 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty println("Updated stock out line ${stockOutLine.id} status to: ${status}") } try { - stockOutLineRepository.flush() + // ✅ FIX: 直接在这里实现检查逻辑,避免调用 stockOutLineService 导致的 NullPointerException + val unfinishedLines = stockOutLineRepository + .findAllByPickOrderLineIdAndDeletedFalse(request.pickOrderLineId) + .filter { + it.status != StockOutLineStatus.COMPLETE.status && + it.status != StockOutLineStatus.REJECTED.status + } - stockOutLineService.checkIsStockOutLineCompleted(request.pickOrderLineId) - println("✅ Checked pick order line ${request.pickOrderLineId} completion status after updating stock out line") + if (unfinishedLines.isEmpty()) { + // 所有 stock out lines 都已完成或被拒绝,更新 pick order line 状态为 COMPLETED + val pickOrderLine = pickOrderLineRepository.findById(request.pickOrderLineId).orElse(null) + if (pickOrderLine != null) { + pickOrderLine.status = PickOrderLineStatus.COMPLETED + pickOrderLineRepository.save(pickOrderLine) + println("✅ Updated pick order line ${request.pickOrderLineId} to COMPLETED status") + } + } else { + println("⏳ Pick order line ${request.pickOrderLineId} still has ${unfinishedLines.size} unfinished stock out lines") + } } catch (e: Exception) { println("⚠️ Error checking pick order line completion: ${e.message}") e.printStackTrace() @@ -823,21 +1023,302 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty } } // 修复:使用 REQUIRES_NEW 传播级别,避免事务冲突 - @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class]) - private fun resuggestPickOrder(pickOrderId: Long?) { - if (pickOrderId != null) { - try { - val resuggestResult = suggestedPickLotService.resuggestPickOrder(pickOrderId) - println("Resuggest result: ${resuggestResult.code} - ${resuggestResult.message}") + open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { + try { + val pickOrder = pickOrderRepository.findById(pickOrderId).orElseThrow() + + println("=== RESUGGEST DEBUG START ===") + println("Pick Order ID: $pickOrderId") + println("Pick Order Code: ${pickOrder.code}") + println("Pick Order Status: ${pickOrder.status}") + + // NEW: Get ALL pick orders for the same items + val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct() + println("Item IDs in current pick order: $itemIds") + + // 收集所有競爭 pick order,並按 id 去重 + // ✅ 优化:只考虑活跃的 pick orders(排除已完成和已取消的) + val allCompetingPickOrders = itemIds.flatMap { itemId -> + val competingOrders = pickOrderLineRepository.findAllPickOrdersByItemId(itemId) + .filter { + it.id != pickOrderId && // Exclude current pick order + it.status != PickOrderStatus.COMPLETED + } + // ✅ 优化:只在有 rejected 或 unavailable lots 时才打印详细信息 + if (competingOrders.isNotEmpty()) { + println("Found ${competingOrders.size} active competing pick orders for item $itemId") + } + competingOrders + }.distinctBy { it.id } + + // basePickOrders 也做一次去重,避免後面 filter 產生大量重複記錄 + val basePickOrders = (listOf(pickOrder) + allCompetingPickOrders).distinctBy { it.id } + + val allPickOrderLineIdsToCheck = basePickOrders + .flatMap { it.pickOrderLines } + .mapNotNull { it.id } + .distinct() + + println("Checking ${allPickOrderLineIdsToCheck.size} pick order lines for rejected stock out lines") + + // OPTIMIZATION 3: 批量查询所有 stock out lines(一次查询代替 N*M 次查询) + val allStockOutLines = if (allPickOrderLineIdsToCheck.isNotEmpty()) { + stockOutLineRepository.findAllByPickOrderLineIdInAndDeletedFalse(allPickOrderLineIdsToCheck) + } else { + emptyList() + } + + println("Found ${allStockOutLines.size} stock out lines in batch query") + + // OPTIMIZATION 3: 按 pickOrderLineId 分组,便于后续查找 + val stockOutLinesByPickOrderLineId = allStockOutLines + .groupBy { it.pickOrderLine?.id } + + // NEW: 預先查出 basePickOrders 所有行對應的 suggestions,用來判斷是否用了 UNAVAILABLE lot + val allSuggestionsForBaseOrders = if (allPickOrderLineIdsToCheck.isNotEmpty()) { + suggestedPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIdsToCheck) + } else { + emptyList() + } + + val suggestionsByPickOrderLineId = allSuggestionsForBaseOrders.groupBy { it.pickOrderLine?.id } + val pickOrderIdsWithChanges = mutableSetOf() + + // FIX: 重算條件 = 有 rejected 行,或者 有使用 UNAVAILABLE lot 的 suggestion + val allPickOrdersToResuggest = basePickOrders.filter { po -> + val hasRejected = po.pickOrderLines.any { pol -> + val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() + val hasRejectedStockOutLine = stockOutLines.any { + it.status?.equals("rejected", ignoreCase = true) == true + } + + if (hasRejectedStockOutLine) { + println("Pick Order ${po.code} has rejected stock out lines - will resuggest") + stockOutLines + .filter { it.status?.equals("rejected", ignoreCase = true) == true } + .forEach { sol -> + println(" - Rejected stock out line: ${sol.id} (lot: ${sol.inventoryLotLine?.id}, qty: ${sol.qty})") + } + } + + hasRejectedStockOutLine + } + + val hasUnavailableLot = po.pickOrderLines.any { pol -> + val suggestions = suggestionsByPickOrderLineId[pol.id] ?: emptyList() + val usedUnavailable = suggestions.any { + it.suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE + } + if (usedUnavailable) { + println("Pick Order ${po.code} has suggestions using UNAVAILABLE lots - will resuggest") + } + usedUnavailable + } + + hasRejected || hasUnavailableLot + }.distinctBy { it.id } + + println("=== RESUGGEST DEBUG ===") + println("Original pick orders: ${basePickOrders.size}") + println("Filtered pick orders to resuggest: ${allPickOrdersToResuggest.size}") + println("Pick orders being resuggested: ${allPickOrdersToResuggest.map { "${it.code}(${it.status})" }}") + + // FIX: Only resuggest if there are actually pick orders with rejected lots + if (allPickOrdersToResuggest.isEmpty()) { + println("No pick orders need resuggesting - no rejected lots found") + return MessageResponse( + id = pickOrderId, + name = "No resuggest needed", + code = "SUCCESS", + type = "resuggest", + message = "No pick orders have rejected lots, no resuggest needed", + errorPosition = null + ) + } + + // FIX: Get all pick order line IDs for the orders to resuggest + val allPickOrderLineIds = allPickOrdersToResuggest + .flatMap { it.pickOrderLines } + .mapNotNull { it.id } + + println("All pick order line IDs to resuggest: ${allPickOrderLineIds.size} lines") + + // FIX: Get all existing suggestions for these pick order lines + val allSuggestions = suggestedPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIds) + println("Found ${allSuggestions.size} existing suggestions") + + // OPTIMIZATION 3: 批量查询所有相关的 stock out lines(用于检查 rejected 状态) + val allSuggestionStockOutLines = if (allSuggestions.isNotEmpty()) { + val suggestionLineIds = allSuggestions.mapNotNull { it.pickOrderLine?.id }.distinct() + val suggestionLotIds = allSuggestions.mapNotNull { it.suggestedLotLine?.id }.distinct() - if (resuggestResult.code != "SUCCESS") { - println("Warning: Resuggest failed: ${resuggestResult.message}") + // OPTIMIZATION 3: 使用批量查询方法(一次查询代替 N*M 次查询) + if (suggestionLineIds.isNotEmpty() && suggestionLotIds.isNotEmpty()) { + stockOutLineRepository.findAllByPickOrderLineIdInAndInventoryLotLineIdInAndDeletedFalse( + suggestionLineIds, + suggestionLotIds + ) + } else { + emptyList() + } + } else { + emptyList() + } + + println("Found ${allSuggestionStockOutLines.size} stock out lines for suggestions") + + // OPTIMIZATION 3: 按 (pickOrderLineId, inventoryLotLineId) 分组 + val stockOutLinesByLineAndLot = allSuggestionStockOutLines + .groupBy { + val lineId = it.pickOrderLine?.id + val lotId = it.inventoryLotLine?.id + lineId to lotId + } + + // FIX: Separate suggestions to keep (those WITHOUT rejected stock out lines) and delete + val suggestionsToKeep = allSuggestions.filter { suggestion -> + val pickOrderLineId = suggestion.pickOrderLine?.id + val suggestedLotLine = suggestion.suggestedLotLine + val suggestedLotLineId = suggestedLotLine?.id + + // ✅ 如果 lot 已经是 UNAVAILABLE,强制不保留(一定要重新建议) + val isLotUnavailable = suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE + if (isLotUnavailable) { + println("⏭️ Suggestion ${suggestion.id} uses UNAVAILABLE lot ${suggestedLotLine?.id}, will be deleted") + return@filter false + } + + if (pickOrderLineId != null && suggestedLotLineId != null) { + val stockOutLines = stockOutLinesByLineAndLot[pickOrderLineId to suggestedLotLineId] ?: emptyList() + !stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } + } else { + true } + } + + // 只删除有 rejected stock out lines 的 suggestions,或者使用 UNAVAILABLE lot 的 suggestions + val suggestionsToDelete = allSuggestions.filter { suggestion -> + val pickOrderLineId = suggestion.pickOrderLine?.id + val suggestedLotLine = suggestion.suggestedLotLine + val suggestedLotLineId = suggestedLotLine?.id + + val isLotUnavailable = suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE + + if (pickOrderLineId != null && suggestedLotLineId != null) { + val stockOutLines = stockOutLinesByLineAndLot[pickOrderLineId to suggestedLotLineId] ?: emptyList() + val hasRejected = stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } + + // ✅ 逻辑:只要 (lot UNAVAILABLE) 或 (有 rejected 的 stock_out_line),就删掉这条 suggestion + if (isLotUnavailable || hasRejected) { + println("🗑 Deleting suggestion ${suggestion.id} for lot $suggestedLotLineId, " + + "unavailable=$isLotUnavailable, hasRejected=$hasRejected") + true + } else { + false + } + } else { + // lotId == null 的老逻辑保留 + suggestedLotLineId == null + } + } + + println("Suggestions to keep: ${suggestionsToKeep.size}") + println("Suggestions to delete: ${suggestionsToDelete.size}") + + // ✅ FIX: 更新使用 UNAVAILABLE lot 的 stockOutLine 状态为 rejected + val unavailableLotIds = suggestionsToDelete + .filter { it.suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE } + .mapNotNull { it.suggestedLotLine?.id } + .distinct() + + if (unavailableLotIds.isNotEmpty()) { + println("=== Updating stock out lines for UNAVAILABLE lots ===") + println("UNAVAILABLE lot IDs: $unavailableLotIds") + + // ✅ FIX: Find ALL stockOutLines using unavailable lots (not just those with suggestions) + val stockOutLinesToReject = stockOutLineRepository + .findAllByInventoryLotLineIdInAndNotCompletedOrRejected(unavailableLotIds) + .filter { sol -> + sol.status != StockOutLineStatus.REJECTED.status && + sol.status != StockOutLineStatus.COMPLETE.status + } + + println("Found ${stockOutLinesToReject.size} stock out lines to reject for UNAVAILABLE lots (across all pick orders)") + + stockOutLinesToReject.forEach { sol -> + val oldStatus = sol.status + sol.status = StockOutLineStatus.REJECTED.status + sol.modified = LocalDateTime.now() + sol.modifiedBy = "system" + println(" - Updated stock out line ${sol.id} (lot: ${sol.inventoryLotLine?.id}, pickOrderLine: ${sol.pickOrderLine?.id}) status: $oldStatus -> rejected") + } + + if (stockOutLinesToReject.isNotEmpty()) { + stockOutLineRepository.saveAll(stockOutLinesToReject) + stockOutLineRepository.flush() + println("✅ Updated ${stockOutLinesToReject.size} stock out lines to REJECTED status") + } + } + + // FIX: Clear holdQty ONLY for lots that have rejected stock out lines + val rejectedLotIds = suggestionsToDelete.mapNotNull { it.suggestedLotLine?.id }.distinct() + println("Rejected lot IDs (clearing holdQty only): $rejectedLotIds") + + // OPTIMIZATION 3: 批量加载和更新 lots + val rejectedLots = if (rejectedLotIds.isNotEmpty()) { + inventoryLotLineRepository.findAllById(rejectedLotIds) + } else { + emptyList() + } + + rejectedLots.forEach { lot -> + val originalHoldQty = lot.holdQty ?: BigDecimal.ZERO + lot.holdQty = BigDecimal.ZERO + println("Cleared holdQty for rejected lot ${lot.id}: $originalHoldQty -> 0") + } + + if (rejectedLots.isNotEmpty()) { + inventoryLotLineRepository.saveAll(rejectedLots) + println("Batch cleared holdQty for ${rejectedLots.size} rejected lots") + } + + // Delete old suggestions + if (suggestionsToDelete.isNotEmpty()) { + suggestedPickLotRepository.deleteAll(suggestionsToDelete) + println("Deleted ${suggestionsToDelete.size} old suggestions") + } + + // ✅ 优化:只调用一次 SuggestedPickLotService.resuggestPickOrder + // SuggestedPickLotService 会处理所有相关的 pick orders,避免重复处理 + // 注意:PickExecutionIssueService 已经处理了 stockOutLine rejection, + // 所以 SuggestedPickLotService 只需要重新生成 suggestions + try { + suggestedPickLotService.resuggestPickOrder(pickOrderId) + println("Resuggested pick orders via SuggestedPickLotService") } catch (e: Exception) { - println("Error during resuggest: ${e.message}") + println("Error resuggesting pick orders: ${e.message}") e.printStackTrace() - // 不重新抛出异常,避免影响主事务 } + + return MessageResponse( + id = pickOrderId, + name = "Pick order resuggested successfully", + code = "SUCCESS", + type = "resuggest", + message = "Resuggested ${allPickOrdersToResuggest.size} pick orders", + errorPosition = null + ) + } catch (e: Exception) { + println("Error in resuggestPickOrder: ${e.message}") + e.printStackTrace() + return MessageResponse( + id = null, + name = "Failed to resuggest pick order", + code = "ERROR", + type = "resuggest", + message = "Error: ${e.message}", + errorPosition = null + ) } } @@ -1015,30 +1496,47 @@ open fun getMissItemList(issueCategory: String = "lot_issue"): List - StockIssueResponse( - id = issue.id ?: 0L, - itemId = issue.itemId, - itemCode = issue.itemCode, - itemDescription = issue.itemDescription, - lotId = issue.lotId, - lotNo = issue.lotNo, - storeLocation = issue.storeLocation, - requiredQty = issue.requiredQty, - actualPickQty = issue.actualPickQty, - missQty = issue.missQty, - badItemQty = issue.badItemQty, - bookQty = issue.bookQty ?: BigDecimal.ZERO, - issueQty = issue.issueQty ?: BigDecimal.ZERO, // 返回计算的问题数量 - issueRemark = issue.issueRemark, - pickerName = issue.pickerName, - handleStatus = issue.handleStatus.name, - handleDate = issue.handleDate, - handledBy = issue.handledBy - ) - } + + // Group by lotId and itemId (same lot can have different items) + val groupedByLot = issues + .filter { it.lotId != null } // Only include issues with lotId + .groupBy { Pair(it.lotId, it.itemId) } + + return groupedByLot.map { (lotItemPair, issueList) -> + val lotId = lotItemPair.first + val itemId = lotItemPair.second + + // Get the first issue for non-aggregated fields + val firstIssue = issueList.first() + + // Sum missQty for this lot (as requested: sum of missQty, not issueQty) + val totalMissQty = issueList.sumOf { it.missQty ?: BigDecimal.ZERO } + + StockIssueResponse( + id = firstIssue.id ?: 0L, // Use first issue's id + itemId = itemId, + itemCode = firstIssue.itemCode, + itemDescription = firstIssue.itemDescription, + lotId = lotId, + lotNo = firstIssue.lotNo, + storeLocation = firstIssue.storeLocation, + requiredQty = issueList.sumOf { it.requiredQty ?: BigDecimal.ZERO }, + actualPickQty = issueList.sumOf { it.actualPickQty ?: BigDecimal.ZERO }, + missQty = totalMissQty, // Sum of missQty + badItemQty = issueList.sumOf { it.badItemQty ?: BigDecimal.ZERO }, + bookQty = firstIssue.bookQty ?: BigDecimal.ZERO, + issueQty = totalMissQty, // For miss items, issueQty = sum of missQty + issueRemark = issueList.mapNotNull { it.issueRemark }.distinct().joinToString("; "), + pickerName = firstIssue.pickerName, + handleStatus = firstIssue.handleStatus.name, + handleDate = firstIssue.handleDate, + handledBy = firstIssue.handledBy + ) + } } open fun getBadItemList(issueCategory: String = "lot_issue"): List { @@ -1047,31 +1545,49 @@ open fun getBadItemList(issueCategory: String = "lot_issue"): List - StockIssueResponse( - id = issue.id ?: 0L, - itemId = issue.itemId, - itemCode = issue.itemCode, - itemDescription = issue.itemDescription, - lotId = issue.lotId, - lotNo = issue.lotNo, - storeLocation = issue.storeLocation, - requiredQty = issue.requiredQty, - actualPickQty = issue.actualPickQty, - missQty = issue.missQty, - badItemQty = issue.badItemQty, - bookQty = issue.bookQty ?: BigDecimal.ZERO, - issueQty = issue.issueQty ?: BigDecimal.ZERO, // 返回计算的问题数量 - issueRemark = issue.issueRemark, - pickerName = issue.pickerName, - handleStatus = issue.handleStatus.name, - handleDate = issue.handleDate, - handledBy = issue.handledBy - ) - } + + // Group by lotId and itemId (same lot can have different items) + val groupedByLot = issues + .filter { it.lotId != null } // Only include issues with lotId + .groupBy { Pair(it.lotId, it.itemId) } + + return groupedByLot.map { (lotItemPair, issueList) -> + val lotId = lotItemPair.first + val itemId = lotItemPair.second + + // Get the first issue for non-aggregated fields + val firstIssue = issueList.first() + + // Sum issueQty for this lot (as requested: sum of issueQty) + val totalIssueQty = issueList.sumOf { it.issueQty ?: BigDecimal.ZERO } + + StockIssueResponse( + id = firstIssue.id ?: 0L, // Use first issue's id + itemId = itemId, + itemCode = firstIssue.itemCode, + itemDescription = firstIssue.itemDescription, + lotId = lotId, + lotNo = firstIssue.lotNo, + storeLocation = firstIssue.storeLocation, + requiredQty = issueList.sumOf { it.requiredQty ?: BigDecimal.ZERO }, + actualPickQty = issueList.sumOf { it.actualPickQty ?: BigDecimal.ZERO }, + missQty = issueList.sumOf { it.missQty ?: BigDecimal.ZERO }, + badItemQty = issueList.sumOf { it.badItemQty ?: BigDecimal.ZERO }, + bookQty = firstIssue.bookQty ?: BigDecimal.ZERO, + issueQty = totalIssueQty, // For bad items, issueQty = sum of issueQty + issueRemark = issueList.mapNotNull { it.issueRemark }.distinct().joinToString("; "), + pickerName = firstIssue.pickerName, + handleStatus = firstIssue.handleStatus.name, + handleDate = firstIssue.handleDate, + handledBy = firstIssue.handledBy + ) + } } + open fun getBadItemOnlyList(): List { return pickExecutionIssueRepository.findBadItemOnlyList(IssueCategory.lot_issue) } @@ -1144,6 +1660,17 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse { errorPosition = null ) + // ✅ 修复:在创建和保存 stockOutLine 之前获取 inventory 的当前 onHandQty + // 必须在任何可能触发数据库触发器更新 inventory 的操作之前获取 + val inventoryBeforeUpdate = inventoryRepository.findByItemId(issue.itemId).orElse(null) + val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + + println("=== submitMissItem: Before update ===") + println("Item ID: ${issue.itemId}") + println("Issue Qty: ${issueQty}") + println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") + println("=====================================") + // 修改:使用 issueQty 创建 stock_out_line val stockOutLine = StockOutLine().apply { this.stockOut = stockOut @@ -1157,13 +1684,24 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse { val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) // 修改:使用 issueQty 更新 inventory_lot_line + // 注意:这会触发数据库触发器更新 inventory,所以必须在获取 onHandQtyBeforeUpdate 之后调用 if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty + updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = true) // 使用 issueQty } markIssueHandled(issue, handler) - // Create stock ledger entry - createStockLedgerForStockOut(savedStockOutLine) + + // ✅ 修复:使用更新前的 onHandQty 计算 balance + // balance = 更新前的 onHandQty - 本次出库数量 + val balance = onHandQtyBeforeUpdate - issueQty.toDouble() + + println("=== submitMissItem: Creating stock ledger ===") + println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") + println("Issue Qty (OutQty): ${issueQty.toDouble()}") + println("Calculated Balance: ${balance}") + println("=============================================") + + createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Miss") return MessageResponse( id = stockOut.id, name = "Success", @@ -1230,6 +1768,16 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { errorPosition = null ) + // ✅ 修复:在创建和保存 stockOutLine 之前获取 inventory 的当前 onHandQty + val inventoryBeforeUpdate = inventoryRepository.findByItemId(issue.itemId).orElse(null) + val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + + println("=== submitBadItem: Before update ===") + println("Item ID: ${issue.itemId}") + println("Issue Qty: ${issueQty}") + println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") + println("====================================") + // 修改:使用 issueQty 创建 stock_out_line val stockOutLine = StockOutLine().apply { this.stockOut = stockOut @@ -1242,14 +1790,34 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { } val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - // 修改:使用 issueQty 更新 inventory_lot_line + // ✅ 修复:Bad item 的 issueQty 应该重置为 0,而不是累加/减去 + // 先重置 issueQty 为 0 + if (issue.lotId != null) { + val lotLineForReset = inventoryLotLineRepository.findById(issue.lotId).orElse(null) + if (lotLineForReset != null) { + lotLineForReset.issueQty = BigDecimal.ZERO + inventoryLotLineRepository.saveAndFlush(lotLineForReset) + println("✅ Reset issueQty to 0 for lot ${issue.lotId} before bad item submission") + } + } + + // 修改:使用 issueQty 更新 inventory_lot_line(更新 outQty) if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty + updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = false) // 使用 issueQty } markIssueHandled(issue, handler) - // Create stock ledger entry - createStockLedgerForStockOut(savedStockOutLine) + + // ✅ 修复:使用更新前的 onHandQty 计算 balance + val balance = onHandQtyBeforeUpdate - issueQty.toDouble() + + println("=== submitBadItem: Creating stock ledger ===") + println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") + println("Issue Qty (OutQty): ${issueQty.toDouble()}") + println("Calculated Balance: ${balance}") + println("===========================================") + + createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Bad") return MessageResponse( id = stockOut.id, name = "Success", @@ -1337,7 +1905,7 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse { - updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty) + updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty, isMissItem = false) // Create stock ledger entry createStockLedgerForStockOut(savedStockOutLine) return MessageResponse( @@ -1406,13 +1974,19 @@ open fun batchSubmitMissItem(request: BatchSubmitIssueRequest): MessageResponse } val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + // ✅ 修复:在更新 lot line 之前获取 inventory 的当前 onHandQty,用于计算 balance + val inventoryBeforeUpdate = item?.let { inventoryRepository.findByItemId(it.id!!).orElse(null) } + val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + // 修改:使用 issueQty if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty + updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = true) // 使用 issueQty } markIssueHandled(issue, handler) - createStockLedgerForStockOut(savedStockOutLine) + // ✅ 修复:使用更新前的 onHandQty 计算 balance,避免触发器更新后的错误计算 + val balance = onHandQtyBeforeUpdate - issueQty.toDouble() + createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Miss") } return MessageResponse( @@ -1481,13 +2055,30 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse { } val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + // ✅ 修复:在更新 lot line 之前获取 inventory 的当前 onHandQty,用于计算 balance + val inventoryBeforeUpdate = item?.let { inventoryRepository.findByItemId(it.id!!).orElse(null) } + val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + + // ✅ 修复:Bad item 的 issueQty 应该重置为 0,而不是累加/减去 + // 先重置 issueQty 为 0 + if (issue.lotId != null) { + val lotLineForReset = inventoryLotLineRepository.findById(issue.lotId).orElse(null) + if (lotLineForReset != null) { + lotLineForReset.issueQty = BigDecimal.ZERO + inventoryLotLineRepository.saveAndFlush(lotLineForReset) + println("✅ Reset issueQty to 0 for lot ${issue.lotId} before bad item batch submission") + } + } + // 修改:使用 issueQty if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty + updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = false) // 使用 issueQty } markIssueHandled(issue, handler) - createStockLedgerForStockOut(savedStockOutLine) + // ✅ 修复:使用更新前的 onHandQty 计算 balance,避免触发器更新后的错误计算 + val balance = onHandQtyBeforeUpdate - issueQty.toDouble() + createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Bad") } return MessageResponse( @@ -1558,7 +2149,7 @@ open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageRespon // Update InventoryLotLine - updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty) + updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty, isMissItem = false) createStockLedgerForStockOut(savedStockOutLine) } @@ -1610,14 +2201,20 @@ private fun createIssueStockOutHeader(type: String, remarks: String?, handler: L return stockOutRepository.save(stockOut) } -private fun updateLotLineAfterIssue(lotLineId: Long, qty: BigDecimal) { +private fun updateLotLineAfterIssue(lotLineId: Long, qty: BigDecimal, isMissItem: Boolean = false) { val lotLine = inventoryLotLineRepository.findById(lotLineId).orElse(null) if (lotLine != null) { val currentOutQty = lotLine.outQty ?: BigDecimal.ZERO val newOutQty = currentOutQty.add(qty) lotLine.outQty = newOutQty - val currentIssueQty = lotLine.issueQty ?: BigDecimal.ZERO - lotLine.issueQty = currentIssueQty.subtract(qty) + + // ✅ 修复:Bad item 的 issueQty 应该重置为 0,而不是减去 + // 注意:issueQty 应该在 submitBadItem 中重置,这里不再处理 + // if (!isMissItem) { + // val currentIssueQty = lotLine.issueQty ?: BigDecimal.ZERO + // lotLine.issueQty = currentIssueQty.subtract(qty) + // } + // If outQty != inQty, set status to AVAILABLE val inQty = lotLine.inQty ?: BigDecimal.ZERO if (newOutQty != inQty) { @@ -1695,31 +2292,38 @@ private fun updateInventoryAfterLotLineChange(lotLine: InventoryLotLine) { e.printStackTrace() } } -private fun createStockLedgerForStockOut(stockOutLine: StockOutLine, ledgerType: String? = null) { +private fun createStockLedgerForStockOut( + stockOutLine: StockOutLine, + ledgerType: String? = null, + balance: Double? = null // 新增:可选的 balance 参数 +) { val item = stockOutLine.item ?: return val outQty = stockOutLine.qty?.toDouble() ?: 0.0 - // 修复:重新查询 inventory 以获取触发器更新后的最新 onHandQty - val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return - - // 计算新的 balance = 当前 onHandQty - 本次出库数量 - val currentOnHandQty = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() - val newBalance = currentOnHandQty - outQty - - // 确保 balance 不为负数 - val finalBalance = if (newBalance < 0) 0.0 else newBalance + // ✅ 修复:如果传入了 balance,直接使用;否则重新查询 inventory + val finalBalance = if (balance != null) { + if (balance < 0) 0.0 else balance + } else { + // 重新查询 inventory 以获取触发器更新后的最新 onHandQty + val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return + val currentOnHandQty = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + val newBalance = currentOnHandQty - outQty + if (newBalance < 0) 0.0 else newBalance + } // 使用传入的 ledgerType,如果没有则使用 stockOutLine.type val ledgerTypeToUse = ledgerType ?: stockOutLine.type ?: "Nor" + val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return + val stockLedger = StockLedger().apply { this.stockOutLine = stockOutLine this.inventory = inventory this.inQty = null this.outQty = outQty this.balance = finalBalance - this.type = ledgerTypeToUse // 使用指定的 type + this.type = ledgerTypeToUse this.itemId = item.id this.itemCode = item.code this.date = LocalDate.now() @@ -1918,4 +2522,313 @@ private fun handleBothMissAndBadItemPackageProblem(request: PickExecutionIssueRe println("Both miss and bad (package): Did NOT resuggest (only bad item with package problem)") } } +open fun getLotIssueDetails(lotId: Long, itemId: Long, issueType: String): LotIssueDetailResponse { + val issues = if (issueType == "miss") { + pickExecutionIssueRepository.findMissItemList(IssueCategory.lot_issue) + .filter { + it.lotId == lotId && + it.itemId == itemId && + it.handleStatus != HandleStatus.completed + } + } else { + pickExecutionIssueRepository.findBadItemListByCategory(IssueCategory.lot_issue) + .filter { + it.lotId == lotId && + it.itemId == itemId && + it.handleStatus != HandleStatus.completed + } + } + + if (issues.isEmpty()) { + throw IllegalArgumentException("No issues found for lot $lotId and item $itemId") + } + + val firstIssue = issues.first() + + // Get DO/JO order codes + val issueDetails = issues.map { issue -> + var doOrderCode: String? = null + var joOrderCode: String? = null + + // Get DO order code + if (issue.doPickOrderId != null) { + val doPickOrder = doPickOrderRepository.findById(issue.doPickOrderId).orElse(null) + doOrderCode = doPickOrder?.deliveryOrderCode + } + + // Get JO order code + if (issue.joPickOrderId != null) { + val joPickOrder = joPickOrderRepository.findById(issue.joPickOrderId).orElse(null) + joOrderCode = joPickOrder?.jobOrderCode // Use jobOrderCode directly instead of jobOrder?.code + } + + IssueDetailItem( + issueId = issue.id ?: 0L, + pickerName = issue.pickerName, + missQty = issue.missQty, + issueQty = issue.issueQty, + pickOrderCode = issue.pickOrderCode, + doOrderCode = doOrderCode, + joOrderCode = joOrderCode, + issueRemark = issue.issueRemark + ) + } + + return LotIssueDetailResponse( + lotId = firstIssue.lotId, + lotNo = firstIssue.lotNo, + itemId = firstIssue.itemId, + itemCode = firstIssue.itemCode, + itemDescription = firstIssue.itemDescription, + storeLocation = firstIssue.storeLocation, + issues = issueDetails + ) +} + +// New: Submit with custom quantity +@Transactional(rollbackFor = [Exception::class]) +open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse { + try { + // Find all issues for this lot and item + val issues = if (request.issueType == "miss") { + pickExecutionIssueRepository.findMissItemList(IssueCategory.lot_issue) + .filter { + it.lotId == request.lotId && + it.itemId == request.itemId && + it.handleStatus != HandleStatus.completed + } + } else { + pickExecutionIssueRepository.findBadItemListByCategory(IssueCategory.lot_issue) + .filter { + it.lotId == request.lotId && + it.itemId == request.itemId && + it.handleStatus != HandleStatus.completed + } + } + + if (issues.isEmpty()) { + return MessageResponse( + id = null, + name = "Error", + code = "NOT_FOUND", + type = "stock_issue", + message = "No issues found for this lot", + errorPosition = null + ) + } + val firstIssue = issues.first() + val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L + val isMissItem = request.issueType == "miss" + + // Use custom quantity instead of sum + val submitQty = request.submitQty + + if (submitQty <= BigDecimal.ZERO) { + return MessageResponse( + id = null, + name = "Error", + code = "INVALID", + type = "stock_issue", + message = "Submit quantity must be greater than 0", + errorPosition = null + ) + } + + // ✅ 修复:在创建和保存 stockOutLine 之前获取 inventory 的当前 onHandQty + val inventoryBeforeUpdate = inventoryRepository.findByItemId(request.itemId).orElse(null) + val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + + println("=== submitIssueWithQty: Before update ===") + println("Item ID: ${request.itemId}") + println("Submit Qty: ${submitQty}") + println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") + println("==========================================") + + val stockOut = createIssueStockOutHeader( + if (isMissItem) "MISS_ITEM" else "BAD_ITEM", + firstIssue.issueRemark, + handler + ) + + val pickOrderLine = firstIssue.pickOrderLineId?.let { + pickOrderLineRepository.findById(it).orElse(null) + } + + val lotLine = request.lotId.let { + inventoryLotLineRepository.findById(it).orElse(null) + } + + val item = itemsRepository.findById(request.itemId).orElse(null) + ?: return MessageResponse( + id = null, + name = "Error", + code = "ITEM_NOT_FOUND", + type = "stock_issue", + message = "Item not found", + errorPosition = null + ) + + // Create stock_out_line with custom quantity + val stockOutLine = StockOutLine().apply { + this.stockOut = stockOut + this.inventoryLotLine = lotLine + this.item = item + this.qty = submitQty.toDouble() // Use custom quantity + this.status = StockOutLineStatus.COMPLETE.status + this.pickOrderLine = pickOrderLine + this.type = if (isMissItem) "Miss" else "Bad" + } + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + if (!isMissItem && request.lotId != null) { + val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null) + if (lotLineForReset != null) { + lotLineForReset.issueQty = BigDecimal.ZERO + inventoryLotLineRepository.saveAndFlush(lotLineForReset) + println("✅ Reset issueQty to 0 for lot ${request.lotId} before bad item submission (submitIssueWithQty)") + } + } + // Update inventory_lot_line with custom quantity - pass isMissItem flag + if (request.lotId != null) { + updateLotLineAfterIssue(request.lotId, submitQty, isMissItem) + } + + // Mark all issues as handled + issues.forEach { issue -> + markIssueHandled(issue, handler) + } + + // ✅ 修复:使用更新前的 onHandQty 计算 balance + val balance = onHandQtyBeforeUpdate - submitQty.toDouble() + + println("=== submitIssueWithQty: Creating stock ledger ===") + println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") + println("Submit Qty (OutQty): ${submitQty.toDouble()}") + println("Calculated Balance: ${balance}") + println("===================================================") + + createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad") + + return MessageResponse( + id = stockOut.id, + name = "Success", + code = "SUCCESS", + type = "stock_issue", + message = "Successfully submitted ${request.issueType} item with quantity $submitQty", + errorPosition = null + ) + } catch (e: Exception) { + return MessageResponse( + id = null, + name = "Error", + code = "ERROR", + type = "stock_issue", + message = "Failed to submit: ${e.message}", + errorPosition = null + ) + } +} + +// New: Create stock ledger with explicit balance (to avoid double subtraction) +private fun createStockLedgerForStockOutWithBalance( + stockOutLine: StockOutLine, + balance: Double, + ledgerType: String? = null +) { + val item = stockOutLine.item ?: return + + val outQty = stockOutLine.qty?.toDouble() ?: 0.0 + + // Use the provided balance instead of calculating from inventory + val finalBalance = if (balance < 0) 0.0 else balance + + // Use传入的 ledgerType,如果没有则使用 stockOutLine.type + val ledgerTypeToUse = ledgerType ?: stockOutLine.type ?: "Nor" + + val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return + + val stockLedger = StockLedger().apply { + this.stockOutLine = stockOutLine + this.inventory = inventory + this.inQty = null + this.outQty = outQty + this.balance = finalBalance + this.type = ledgerTypeToUse + this.itemId = item.id + this.itemCode = item.code + this.date = LocalDate.now() + } + + stockLedgerRepository.saveAndFlush(stockLedger) + + println("=== CREATED STOCK LEDGER ===") + println("Type: ${ledgerTypeToUse}") + println("OutQty: ${outQty}") + println("Balance: ${finalBalance}") + println("===========================") +} + +// ✅ 修复:直接在 PickExecutionIssueService 中实现检查逻辑,避免循环依赖 +private fun checkPickOrderLineCompletion(pickOrderLineId: Long) { + val allStockOutLines = stockOutLineRepository + .findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) + + println("=== checkPickOrderLineCompletion for pickOrderLineId: $pickOrderLineId ===") + println("Total stock out lines: ${allStockOutLines.size}") + allStockOutLines.forEach { sol -> + println(" StockOutLine ${sol.id}: status=${sol.status}, qty=${sol.qty}") + } + + val unfinishedLine = allStockOutLines.filter { + val status = it.status?.trim()?.lowercase() + val isComplete = status == StockOutLineStatus.COMPLETE.status.lowercase() + val isRejected = status == StockOutLineStatus.REJECTED.status.lowercase() + val isPartiallyComplete = status == StockOutLineStatus.PARTIALLY_COMPLETE.status.lowercase() + + // Check if partially_completed should be considered as finished + // (if total picked qty meets required qty) + val shouldConsiderPartiallyCompleteAsFinished = if (isPartiallyComplete) { + val pickOrderLine = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) + if (pickOrderLine != null && pickOrderLine.qty != null) { + val totalPickedQty = allStockOutLines + .filter { sol -> + val solStatus = sol.status?.trim()?.lowercase() + solStatus == "completed" || solStatus == "partially_completed" + } + .fold(BigDecimal.ZERO) { acc, sol -> + val qtyValue = sol.qty ?: 0.0 + acc + BigDecimal(qtyValue.toString()) + } + totalPickedQty >= pickOrderLine.qty + } else { + false + } + } else { + false + } + + !isComplete && !isRejected && !shouldConsiderPartiallyCompleteAsFinished + } + + println("Unfinished lines: ${unfinishedLine.size}") + if (unfinishedLine.isNotEmpty()) { + unfinishedLine.forEach { sol -> + println(" - StockOutLine ${sol.id}: status=${sol.status}, qty=${sol.qty}") + } + } + + if (unfinishedLine.isEmpty()) { + // set pick order line status to complete + val pol = pickOrderLineRepository.findById(pickOrderLineId).orElseThrow() + val previousStatus = pol.status + pickOrderLineRepository.save( + pol.apply { + this.status = PickOrderLineStatus.COMPLETED + } + ) + println("✅ Updated pick order line $pickOrderLineId from $previousStatus to COMPLETED") + } else { + println("⏳ Pick order line $pickOrderLineId not completed yet - has ${unfinishedLine.size} unfinished stock out lines") + } +} + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt index 3b84803..148fbc9 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt @@ -92,5 +92,18 @@ fun submitExpiryItem(@RequestBody request: SubmitExpiryRequest): MessageResponse fun batchSubmitExpiryItem(@RequestBody request: BatchSubmitExpiryRequest): MessageResponse { return pickExecutionIssueService.batchSubmitExpiryItem(request) } +@GetMapping("/lotIssueDetails") +fun getLotIssueDetails( + @RequestParam lotId: Long, + @RequestParam itemId: Long, + @RequestParam issueType: String +): LotIssueDetailResponse { + return pickExecutionIssueService.getLotIssueDetails(lotId, itemId, issueType) +} + +@PostMapping("/submitIssueWithQty") +fun submitIssueWithQty(@RequestBody request: SubmitIssueWithQtyRequest): MessageResponse { + return pickExecutionIssueService.submitIssueWithQty(request) +} } diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SubmitIssueRequest.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SubmitIssueRequest.kt index 5ba18de..c5480d0 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SubmitIssueRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SubmitIssueRequest.kt @@ -31,4 +31,40 @@ data class ExpiryItemResponse( val storeLocation: String?, val expiryDate: LocalDate?, val remainingQty: BigDecimal, +) +data class LotIssueDetailRequest( + val lotId: Long, + val itemId: Long, + val issueType: String // "miss" or "bad" +) + +// New: Response for lot issue details +data class LotIssueDetailResponse( + val lotId: Long?, + val lotNo: String?, + val itemId: Long, + val itemCode: String?, + val itemDescription: String?, + val storeLocation: String?, + val issues: List +) + +data class IssueDetailItem( + val issueId: Long, + val pickerName: String?, + val missQty: BigDecimal?, + val issueQty: BigDecimal?, + val pickOrderCode: String, + val doOrderCode: String?, + val joOrderCode: String?, + val issueRemark: String? +) + +// New: Request to submit with custom quantity +data class SubmitIssueWithQtyRequest( + val lotId: Long, + val itemId: Long, + val issueType: String, // "miss" or "bad" + val submitQty: BigDecimal, // User input quantity + val handler: Long? = null ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt index 6e95fcc..83287c1 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt @@ -49,6 +49,7 @@ data class ProductProcessInfo( data class ProductProcessLineInfo( val id:Long?, + val processId: Long?, val bomprocessId: Long?, val operatorId: Long?, val operatorName: String?, diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt index 52824e7..f4612b2 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt @@ -499,7 +499,7 @@ open class ProductProcessService( // get bom materials val bomMaterials = jobOrderBomMaterialRepository.findAllByJobOrderId(jobOrder?.id?:0) val bomProcessIds = bomProcess.mapNotNull { it.id } - + val itemIds = bomMaterials.mapNotNull { it.item?.id } // calculate each item's available stock @@ -637,6 +637,7 @@ val sufficientStockQty = bomMaterials println("line id${line.id}") ProductProcessLineInfo( id = line.id?:0, + processId = process.id?:0, bomprocessId = line.bomProcess?.id?:0, operatorId = line.operator?.id?:0, operatorName = line.operator?.name?:"", diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt index ac69d44..540a61a 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt @@ -69,4 +69,15 @@ fun findAllByPickOrderLineIdInAndInventoryLotLineIdInAndDeletedFalse( @Param("pickOrderLineIds") pickOrderLineIds: List, @Param("inventoryLotLineIds") inventoryLotLineIds: List ): List + +// 查找所有使用指定 lot 的 stockOutLines(排除已完成和已拒绝的) +@Query(""" + SELECT sol FROM StockOutLine sol + WHERE sol.inventoryLotLine.id IN :inventoryLotLineIds + AND sol.deleted = false + AND sol.status NOT IN ('completed', 'rejected', 'COMPLETE', 'REJECTED') +""") +fun findAllByInventoryLotLineIdInAndNotCompletedOrRejected( + @Param("inventoryLotLineIds") inventoryLotLineIds: List +): List } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 69ad4fa..367fa7e 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt @@ -339,28 +339,66 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { // } @Transactional fun checkIsStockOutLineCompleted(pickOrderLineId: Long) { - val unfinishedLine = stockOutLineRepository + val allStockOutLines = stockOutLineRepository .findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) - .filter { - it.status != StockOutLineStatus.COMPLETE.status - && it.status != StockOutLineStatus.REJECTED.status + + println("=== checkIsStockOutLineCompleted for pickOrderLineId: $pickOrderLineId ===") + println("Total stock out lines: ${allStockOutLines.size}") + allStockOutLines.forEach { sol -> + println(" StockOutLine ${sol.id}: status=${sol.status}, qty=${sol.qty}") + } + + val unfinishedLine = allStockOutLines.filter { + val status = it.status?.trim()?.lowercase() + val isComplete = status == StockOutLineStatus.COMPLETE.status.lowercase() + val isRejected = status == StockOutLineStatus.REJECTED.status.lowercase() + val isPartiallyComplete = status == StockOutLineStatus.PARTIALLY_COMPLETE.status.lowercase() + + // Check if partially_completed should be considered as finished + // (if total picked qty meets required qty) + val shouldConsiderPartiallyCompleteAsFinished = if (isPartiallyComplete) { + // ✅ 修复:从 repository 获取 pickOrderLine,避免懒加载问题 + val pickOrderLine = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) + if (pickOrderLine != null && pickOrderLine.qty != null) { + val totalPickedQty = allStockOutLines + .filter { sol -> + val solStatus = sol.status?.trim()?.lowercase() + solStatus == "completed" || solStatus == "partially_completed" + } + .fold(BigDecimal.ZERO) { acc, sol -> + val qtyValue = sol.qty ?: 0.0 + acc + BigDecimal(qtyValue.toString()) + } + totalPickedQty >= pickOrderLine.qty + } else { + false + } + } else { + false + } + + !isComplete && !isRejected && !shouldConsiderPartiallyCompleteAsFinished + } + + println("Unfinished lines: ${unfinishedLine.size}") + if (unfinishedLine.isNotEmpty()) { + unfinishedLine.forEach { sol -> + println(" - StockOutLine ${sol.id}: status=${sol.status}, qty=${sol.qty}") } + } + if (unfinishedLine.isEmpty()) { // set pick order line status to complete val pol = pickOrderLineRepository.findById(pickOrderLineId).orElseThrow() + val previousStatus = pol.status pickOrderLineRepository.save( pol.apply { this.status = PickOrderLineStatus.COMPLETED } ) + println("✅ Updated pick order line $pickOrderLineId from $previousStatus to COMPLETED") } else { - // return unfinished ids to frontend - println(unfinishedLine.map { - Pair( - it.id, - it.status - ) - }) + println("⏳ Pick order line $pickOrderLineId not completed yet - has ${unfinishedLine.size} unfinished stock out lines") } } private fun completeDoIfAllPickOrdersCompleted(pickOrderId: Long) { @@ -1124,11 +1162,15 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { )) // Inventory updates - 修复:使用增量数量 - if (submitQty > BigDecimal.ZERO && line.inventoryLotLineId != null) { - println(" Updating inventory lot line ${line.inventoryLotLineId} with qty $submitQty") + // ✅ 修复:如果 inventoryLotLineId 为 null,从 stock_out_line 中获取 + val actualInventoryLotLineId = line.inventoryLotLineId + ?: stockOutLine.inventoryLotLine?.id + + if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { + println(" Updating inventory lot line ${actualInventoryLotLineId} with qty $submitQty") inventoryLotLineService.updateInventoryLotLineQuantities( UpdateInventoryLotLineQuantitiesRequest( - inventoryLotLineId = line.inventoryLotLineId, + inventoryLotLineId = actualInventoryLotLineId, qty = submitQty, operation = "pick" ) @@ -1136,6 +1178,10 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { if (submitQty > BigDecimal.ZERO) { createStockLedgerForPickDelta(line.stockOutLineId, submitQty) } + } else if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId == null) { + // ✅ 修复:即使没有 inventoryLotLineId,也应该创建 stock_ledger(用于无批次物品或批次切换后的情况) + println(" Warning: No inventoryLotLineId found, but creating stock ledger anyway for stockOutLineId ${line.stockOutLineId}") + createStockLedgerForPickDelta(line.stockOutLineId, submitQty) } try { val stockOutLine = stockOutLines[line.stockOutLineId] @@ -1189,6 +1235,22 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { // inventoryLotLineRepository.saveAll(lotLines.values.toList()) // inventoryRepository.saveAll(inventories.values.toList()) + // ✅ 修复:批处理完成后,检查所有受影响的 pick order lines 是否应该标记为完成 + val affectedPickOrderLineIds = request.lines + .mapNotNull { line -> + stockOutLines[line.stockOutLineId]?.pickOrderLine?.id + } + .distinct() + + println("=== Checking ${affectedPickOrderLineIds.size} affected pick order lines after batch submit ===") + affectedPickOrderLineIds.forEach { pickOrderLineId -> + try { + checkIsStockOutLineCompleted(pickOrderLineId) + } catch (e: Exception) { + println(" ✗ Error checking pick order line $pickOrderLineId: ${e.message}") + } + } + val msg = if (errors.isEmpty()) { "Batch submit success (${processedIds.size} lines)." } else { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index ae13524..3519e4b 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt @@ -36,6 +36,7 @@ import com.ffii.fpsms.modules.stock.entity.FailInventoryLotLineRepository import com.ffii.fpsms.modules.stock.entity.StockOutRepository import com.ffii.fpsms.modules.master.entity.ItemsRepository import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus +import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository import com.ffii.fpsms.modules.stock.web.model.StockOutStatus @@ -533,24 +534,28 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { println("Pick Order Code: ${pickOrder.code}") println("Pick Order Status: ${pickOrder.status}") - // NEW: Get ALL pick orders for the same items - val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct() - println("Item IDs in current pick order: $itemIds") - - val allCompetingPickOrders = mutableListOf() - - itemIds.forEach { itemId -> - val competingOrders = pickOrderLineRepository.findAllPickOrdersByItemId(itemId) - .filter { it.id != pickOrderId } // Exclude current pick order - println("Found ${competingOrders.size} competing pick orders for item $itemId") - competingOrders.forEach { order -> - println(" - Competing Order: ${order.code} (ID: ${order.id}, Status: ${order.status})") - } - allCompetingPickOrders.addAll(competingOrders) + // NEW: Get ALL pick orders for the same items +val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct() +println("Item IDs in current pick order: $itemIds") + +// 收集所有競爭 pick order,並按 id 去重 +// ✅ 优化:只考虑活跃的 pick orders(排除已完成和已取消的) +val allCompetingPickOrders = itemIds.flatMap { itemId -> + val competingOrders = pickOrderLineRepository.findAllPickOrdersByItemId(itemId) + .filter { + it.id != pickOrderId && // Exclude current pick order + it.status != PickOrderStatus.COMPLETED } - - // OPTIMIZATION 3: 批量收集所有需要检查的 pick order line IDs - val allPickOrderLineIdsToCheck = (listOf(pickOrder) + allCompetingPickOrders) + if (competingOrders.isNotEmpty()) { + println("Found ${competingOrders.size} active competing pick orders for item $itemId") + } + competingOrders +}.distinctBy { it.id } + +// basePickOrders 也做一次去重,避免後面 filter 產生大量重複記錄 +val basePickOrders = (listOf(pickOrder) + allCompetingPickOrders).distinctBy { it.id } + + val allPickOrderLineIdsToCheck = basePickOrders .flatMap { it.pickOrderLines } .mapNotNull { it.id } .distinct() @@ -558,7 +563,6 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { println("Checking ${allPickOrderLineIdsToCheck.size} pick order lines for rejected stock out lines") // OPTIMIZATION 3: 批量查询所有 stock out lines(一次查询代替 N*M 次查询) - // 注意:findAllByPickOrderLineIdInAndDeletedFalse 返回 List(实体类) val allStockOutLines = if (allPickOrderLineIdsToCheck.isNotEmpty()) { stockOutLIneRepository.findAllByPickOrderLineIdInAndDeletedFalse(allPickOrderLineIdsToCheck) } else { @@ -568,32 +572,52 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { println("Found ${allStockOutLines.size} stock out lines in batch query (1 query instead of ${allPickOrderLineIdsToCheck.size} queries)") // OPTIMIZATION 3: 按 pickOrderLineId 分组,便于后续查找 - // 注意:StockOutLine 实体使用 pickOrderLine?.id,不是 pickOrderLineId val stockOutLinesByPickOrderLineId = allStockOutLines .groupBy { it.pickOrderLine?.id } - // FIX: Only resuggest pick orders that have rejected stock out lines - val allPickOrdersToResuggest = (listOf(pickOrder) + allCompetingPickOrders) - .filter { pickOrderToCheck -> - // Only resuggest if the pick order has rejected stock out lines - pickOrderToCheck.pickOrderLines.any { pol -> - // OPTIMIZATION 3: 从预加载的 Map 中获取,而不是查询数据库 - val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() - val hasRejectedStockOutLine = stockOutLines.any { - it.status?.equals("rejected", ignoreCase = true) == true - } - - if (hasRejectedStockOutLine) { - println("Pick Order ${pickOrderToCheck.code} has rejected stock out lines - will resuggest") - stockOutLines.filter { it.status?.equals("rejected", ignoreCase = true) == true }.forEach { sol -> + // NEW: 預先查出 basePickOrders 所有行對應的 suggestions,用來判斷是否用了 UNAVAILABLE lot + val allSuggestionsForBaseOrders = if (allPickOrderLineIdsToCheck.isNotEmpty()) { + suggestedPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIdsToCheck) + } else { + emptyList() + } + + val suggestionsByPickOrderLineId = allSuggestionsForBaseOrders.groupBy { it.pickOrderLine?.id } + val pickOrderIdsWithChanges = mutableSetOf() + // FIX: 重算條件 = 有 rejected 行,或者 有使用 UNAVAILABLE lot 的 suggestion + val allPickOrdersToResuggest = basePickOrders.filter { po -> + val hasRejected = po.pickOrderLines.any { pol -> + val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() + val hasRejectedStockOutLine = stockOutLines.any { + it.status?.equals("rejected", ignoreCase = true) == true + } + + if (hasRejectedStockOutLine) { + println("Pick Order ${po.code} has rejected stock out lines - will resuggest") + stockOutLines + .filter { it.status?.equals("rejected", ignoreCase = true) == true } + .forEach { sol -> println(" - Rejected stock out line: ${sol.id} (lot: ${sol.inventoryLotLine?.id}, qty: ${sol.qty})") } - } - - hasRejectedStockOutLine } + + hasRejectedStockOutLine } + val hasUnavailableLot = po.pickOrderLines.any { pol -> + val suggestions = suggestionsByPickOrderLineId[pol.id] ?: emptyList() + val usedUnavailable = suggestions.any { + it.suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE + } + if (usedUnavailable) { + println("Pick Order ${po.code} has suggestions using UNAVAILABLE lots - will resuggest") + } + usedUnavailable + } + + hasRejected || hasUnavailableLot + }.distinctBy { it.id } + println("=== RESUGGEST DEBUG ===") println("Original pick orders: ${(listOf(pickOrder) + allCompetingPickOrders).size}") println("Filtered pick orders to resuggest: ${allPickOrdersToResuggest.size}") @@ -656,35 +680,88 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { // FIX: Separate suggestions to keep (those WITHOUT rejected stock out lines) and delete val suggestionsToKeep = allSuggestions.filter { suggestion -> val pickOrderLineId = suggestion.pickOrderLine?.id - val suggestedLotLineId = suggestion.suggestedLotLine?.id + val suggestedLotLine = suggestion.suggestedLotLine + val suggestedLotLineId = suggestedLotLine?.id + + val isLotUnavailable = suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE + if (isLotUnavailable) { + println("⏭️ Suggestion ${suggestion.id} uses UNAVAILABLE lot ${suggestedLotLine?.id}, will be deleted") + return@filter false + } + if (pickOrderLineId != null && suggestedLotLineId != null) { - // OPTIMIZATION 3: 从预加载的 Map 中获取 val stockOutLines = stockOutLinesByLineAndLot[pickOrderLineId to suggestedLotLineId] ?: emptyList() - // 保留没有 rejected stock out lines 的 suggestions !stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } } else { - true // 保留有问题的 suggestions 用于调试 + true } } // 只删除有 rejected stock out lines 的 suggestions val suggestionsToDelete = allSuggestions.filter { suggestion -> val pickOrderLineId = suggestion.pickOrderLine?.id - val suggestedLotLineId = suggestion.suggestedLotLine?.id - + val suggestedLotLine = suggestion.suggestedLotLine + val suggestedLotLineId = suggestedLotLine?.id + + val isLotUnavailable = suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE + if (pickOrderLineId != null && suggestedLotLineId != null) { - // OPTIMIZATION 3: 从预加载的 Map 中获取 val stockOutLines = stockOutLinesByLineAndLot[pickOrderLineId to suggestedLotLineId] ?: emptyList() - stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } // 只删除 rejected 的 + val hasRejected = stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } + + // ✅ 逻辑:只要 (lot UNAVAILABLE) 或 (有 rejected 的 stock_out_line),就删掉这条 suggestion + if (isLotUnavailable || hasRejected) { + println("🗑 Deleting suggestion ${suggestion.id} for lot $suggestedLotLineId, " + + "unavailable=$isLotUnavailable, hasRejected=$hasRejected") + true + } else { + false + } } else { - suggestedLotLineId == null + // lotId == null 的老逻辑保留 + suggestedLotLineId == null } } println("Suggestions to keep: ${suggestionsToKeep.size}") println("Suggestions to delete: ${suggestionsToDelete.size}") + // ✅ FIX: 更新使用 UNAVAILABLE lot 的 stockOutLine 状态为 rejected + val unavailableLotIds = suggestionsToDelete + .filter { it.suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE } + .mapNotNull { it.suggestedLotLine?.id } + .distinct() + + if (unavailableLotIds.isNotEmpty()) { + println("=== Updating stock out lines for UNAVAILABLE lots ===") + println("UNAVAILABLE lot IDs: $unavailableLotIds") + + // ✅ FIX: Find ALL stockOutLines using unavailable lots (not just those with suggestions) + val stockOutLinesToReject = stockOutLIneRepository + .findAllByInventoryLotLineIdInAndNotCompletedOrRejected(unavailableLotIds) + .filter { sol -> + sol.status != StockOutLineStatus.REJECTED.status && + sol.status != StockOutLineStatus.COMPLETE.status + } + + println("Found ${stockOutLinesToReject.size} stock out lines to reject for UNAVAILABLE lots (across all pick orders)") + + stockOutLinesToReject.forEach { sol -> + val oldStatus = sol.status + sol.status = StockOutLineStatus.REJECTED.status + sol.modified = LocalDateTime.now() + sol.modifiedBy = "system" + println(" - Updated stock out line ${sol.id} (lot: ${sol.inventoryLotLine?.id}, pickOrderLine: ${sol.pickOrderLine?.id}) status: $oldStatus -> rejected") + } + + if (stockOutLinesToReject.isNotEmpty()) { + stockOutLIneRepository.saveAll(stockOutLinesToReject) + stockOutLIneRepository.flush() + println("✅ Updated ${stockOutLinesToReject.size} stock out lines to REJECTED status") + } + } + // FIX: Clear holdQty ONLY for lots that have rejected stock out lines val rejectedLotIds = suggestionsToDelete.mapNotNull { it.suggestedLotLine?.id }.distinct() println("Rejected lot IDs (clearing holdQty only): $rejectedLotIds") @@ -737,17 +814,27 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { println("Final existing holdQtyMap: $existingHoldQtyMap") - // FIX: Create new suggestions for all pick orders to resuggest allPickOrdersToResuggest.forEach { pickOrderToResuggest -> - // 只获取有 rejected stock out lines 的 pick order lines - // OPTIMIZATION 3: 使用预加载的 Map + // 获取需要重新建议的 pick order lines: + // 1. 有 rejected stock out lines 的行 + // 2. 有使用 UNAVAILABLE lot 的 suggestion 的行 val problematicPickOrderLines = pickOrderToResuggest.pickOrderLines.filter { pol -> + // 情况1: 有 rejected stock out line val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() - stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } + val hasRejected = stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } + + // 情况2: 有使用 UNAVAILABLE lot 的 suggestion + val suggestions = suggestionsByPickOrderLineId[pol.id] ?: emptyList() + val hasUnavailableLot = suggestions.any { + it.suggestedLotLine?.status == InventoryLotLineStatus.UNAVAILABLE + } + + hasRejected || hasUnavailableLot } if (problematicPickOrderLines.isNotEmpty()) { println("=== Creating new suggestions for pick order: ${pickOrderToResuggest.code} ===") + println(" - Problematic lines: ${problematicPickOrderLines.map { it.id }}") // 调用 suggestionForPickOrderLines 生成新的 suggestions val request = SuggestedPickLotForPolRequest( @@ -760,7 +847,7 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { if (response.suggestedList.isNotEmpty()) { println("Saving ${response.suggestedList.size} new suggestions") - + pickOrderToResuggest.id?.let { pickOrderIdsWithChanges.add(it) } // 获取现有的 pending/checked 状态的 suggestions(可以更新的) val existingUpdatableSuggestions = suggestionsToKeep .filter { it.suggestedLotLine?.id != null } @@ -879,7 +966,8 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { } } - // 更新 holdQty + // ✅ 优化:批量更新 holdQty(避免逐个保存) + val lotsToUpdate = mutableListOf() response.holdQtyMap.forEach { (lotId, newHoldQty) -> if (lotId != null && newHoldQty != null && newHoldQty > BigDecimal.ZERO) { val lot = inventoryLotLineRepository.findById(lotId).orElse(null) @@ -889,12 +977,16 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { val additionalHoldQty = newHoldQty.minus(existingHoldQty) val finalHoldQty = currentHoldQty.plus(additionalHoldQty) it.holdQty = finalHoldQty - inventoryLotLineRepository.save(it) + lotsToUpdate.add(it) existingHoldQtyMap[lotId] = newHoldQty - println("Updated holdQty for lot $lotId: $currentHoldQty + $additionalHoldQty = $finalHoldQty") } } } + // 批量保存所有更新的 lots + if (lotsToUpdate.isNotEmpty()) { + inventoryLotLineRepository.saveAll(lotsToUpdate) + println("Batch updated holdQty for ${lotsToUpdate.size} lots") + } } else { // 如果完全没有生成任何 suggestions println("No suggestions generated at all for pick order: ${pickOrderToResuggest.code}") @@ -918,12 +1010,38 @@ open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { } } - // FIX: Update inventory table for each pick order - allPickOrdersToResuggest.forEach { pickOrderToUpdate -> - println("=== Updating inventory table for pick order: ${pickOrderToUpdate.code} ===") - updateInventoryTableAfterResuggest(pickOrderToUpdate) - } + // ✅ 优化:批量更新 inventory table(收集所有 item IDs,只更新一次) + val allItemIdsToUpdate = allPickOrdersToResuggest + .flatMap { it.pickOrderLines } + .mapNotNull { it.item?.id } + .distinct() + if (allItemIdsToUpdate.isNotEmpty()) { + println("=== Batch updating inventory table for ${allItemIdsToUpdate.size} items ===") + allItemIdsToUpdate.forEach { itemId -> + // Calculate onHoldQty for ALL pick orders that use this item + // ✅ FIX: Use enum directly, not .value + val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) + .sumOf { it.holdQty ?: BigDecimal.ZERO } + + // ✅ FIX: Use enum directly, not .value + val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE) + .sumOf { + val inQty = it.inQty ?: BigDecimal.ZERO + val outQty = it.outQty ?: BigDecimal.ZERO + val remainingQty = inQty.minus(outQty) + remainingQty + } + + val inventory = inventoryRepository.findByItemId(itemId).orElse(null) + if (inventory != null) { + inventory.onHoldQty = onHoldQty + inventory.unavailableQty = unavailableQty + inventoryRepository.save(inventory) + } + } + println("✅ Batch updated inventory table for ${allItemIdsToUpdate.size} items") + } println("=== RESUGGEST DEBUG END ===") return MessageResponse(