| @@ -11,7 +11,9 @@ import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository | |||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | import com.ffii.fpsms.modules.stock.entity.InventoryRepository | ||||
| import com.ffii.fpsms.modules.stock.service.StockOutLineService | |||||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus | |||||
| import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService | import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService | ||||
| import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | ||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| @@ -30,7 +32,7 @@ import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository | |||||
| import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum | 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.StockOutLineStatus | ||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | ||||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus | |||||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | ||||
| import com.ffii.fpsms.modules.pickOrder.web.models.* | import com.ffii.fpsms.modules.pickOrder.web.models.* | ||||
| import com.ffii.fpsms.modules.common.SecurityUtils | import com.ffii.fpsms.modules.common.SecurityUtils | ||||
| @@ -52,6 +54,7 @@ open class PickExecutionIssueService( | |||||
| private val pickOrderRepository: PickOrderRepository, | private val pickOrderRepository: PickOrderRepository, | ||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| private val stockOutRepository: StockOutRepository, | private val stockOutRepository: StockOutRepository, | ||||
| private val stockOutLineService: StockOutLineService, | |||||
| private val pickOrderLineRepository: PickOrderLineRepository, | private val pickOrderLineRepository: PickOrderLineRepository, | ||||
| private val doPickOrderService: DoPickOrderService, | private val doPickOrderService: DoPickOrderService, | ||||
| private val joPickOrderRepository: JoPickOrderRepository, | private val joPickOrderRepository: JoPickOrderRepository, | ||||
| @@ -62,217 +65,227 @@ open class PickExecutionIssueService( | |||||
| ) { | ) { | ||||
| @Transactional(rollbackFor = [Exception::class]) | @Transactional(rollbackFor = [Exception::class]) | ||||
| open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse { | |||||
| try { | |||||
| // 1. 检查是否已经存在相同的 pick execution issue 记录 | |||||
| val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( | |||||
| request.pickOrderLineId, | |||||
| request.lotId ?: 0L | |||||
| ) | |||||
| if (existingIssues.isNotEmpty()) { | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Pick execution issue already exists", | |||||
| code = "DUPLICATE", | |||||
| type = "pick_execution_issue", | |||||
| message = "A pick execution issue for this lot has already been recorded", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null) | |||||
| // 2. 获取 inventory_lot_line 并计算账面数量 (bookQty) | |||||
| val inventoryLotLine = request.lotId?.let { | |||||
| inventoryLotLineRepository.findById(it).orElse(null) | |||||
| } | |||||
| // 计算账面数量(创建 issue 时的快照) | |||||
| val bookQty = if (inventoryLotLine != null) { | |||||
| val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO | |||||
| val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO | |||||
| inQty.subtract(outQty) // bookQty = inQty - outQty | |||||
| } else { | |||||
| BigDecimal.ZERO | |||||
| } | |||||
| // 3. 获取数量值 | |||||
| val requiredQty = request.requiredQty ?: BigDecimal.ZERO | |||||
| val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO | |||||
| val missQty = request.missQty ?: BigDecimal.ZERO | |||||
| val badItemQty = request.badItemQty ?: BigDecimal.ZERO | |||||
| // 4. 验证逻辑:如果 actualPickQty == requiredQty,missQty 必须为 0 | |||||
| if (actualPickQty == requiredQty && missQty > BigDecimal.ZERO) { | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Invalid issue", | |||||
| code = "INVALID", | |||||
| type = "pick_execution_issue", | |||||
| message = "If actual pick qty equals required qty, miss qty must be 0", | |||||
| errorPosition = null | |||||
| open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse { | |||||
| try { | |||||
| // 1. 检查是否已经存在相同的 pick execution issue 记录 | |||||
| val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( | |||||
| request.pickOrderLineId, | |||||
| request.lotId ?: 0L | |||||
| ) | ) | ||||
| } | |||||
| // 5. 计算 issueQty(实际的问题数量) | |||||
| val issueQty = when { | |||||
| // 情况1: 已拣完但有坏品 | |||||
| actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { | |||||
| badItemQty // issueQty = badItemQty | |||||
| } | |||||
| // 情况2: 未拣完(有缺失或坏品) | |||||
| actualPickQty < requiredQty -> { | |||||
| // issueQty = bookQty - actualPickQty | |||||
| // 这是实际缺失的数量(账面应该有的数量 - 实际拣到的数量) | |||||
| val calculatedIssueQty = bookQty.subtract(actualPickQty) | |||||
| // 验证:如果用户报告了 missQty,它应该 <= issueQty(但允许用户报告的值) | |||||
| if (missQty > BigDecimal.ZERO && missQty > calculatedIssueQty) { | |||||
| println("⚠️ Warning: User reported missQty (${missQty}) exceeds calculated issueQty (${calculatedIssueQty})") | |||||
| println(" BookQty: ${bookQty}, ActualPickQty: ${actualPickQty}") | |||||
| } | |||||
| calculatedIssueQty | |||||
| if (existingIssues.isNotEmpty()) { | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Pick execution issue already exists", | |||||
| code = "DUPLICATE", | |||||
| type = "pick_execution_issue", | |||||
| message = "A pick execution issue for this lot has already been recorded", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | } | ||||
| else -> BigDecimal.ZERO | |||||
| } | |||||
| println("=== PICK EXECUTION ISSUE PROCESSING ===") | |||||
| println("Required Qty: ${requiredQty}") | |||||
| println("Actual Pick Qty: ${actualPickQty}") | |||||
| println("Miss Qty (Reported): ${missQty}") | |||||
| println("Bad Item Qty: ${badItemQty}") | |||||
| println("Book Qty (inQty - outQty): ${bookQty}") | |||||
| println("Issue Qty (Calculated): ${issueQty}") | |||||
| println("Lot ID: ${request.lotId}") | |||||
| println("Item ID: ${request.itemId}") | |||||
| println("================================================") | |||||
| // 6. 创建 pick execution issue 记录 | |||||
| val pickExecutionIssue = PickExecutionIssue( | |||||
| id = null, | |||||
| pickOrderId = request.pickOrderId, | |||||
| pickOrderCode = request.pickOrderCode, | |||||
| pickOrderCreateDate = request.pickOrderCreateDate, | |||||
| pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(), | |||||
| pickOrderLineId = request.pickOrderLineId, | |||||
| issueNo = generateIssueNo(), | |||||
| joPickOrderId = pickOrder?.jobOrder?.id, | |||||
| doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null, | |||||
| issueCategory = IssueCategory.valueOf( | |||||
| request.issueCategory ?: "lot_issue" | |||||
| ), | |||||
| itemId = request.itemId, | |||||
| itemCode = request.itemCode, | |||||
| itemDescription = request.itemDescription, | |||||
| lotId = request.lotId, | |||||
| lotNo = request.lotNo, | |||||
| storeLocation = request.storeLocation, | |||||
| requiredQty = request.requiredQty, | |||||
| actualPickQty = request.actualPickQty, | |||||
| missQty = request.missQty, | |||||
| badItemQty = request.badItemQty, | |||||
| bookQty = bookQty, // 添加账面数量 | |||||
| issueQty = issueQty, // 添加计算的问题数量 | |||||
| issueRemark = request.issueRemark, | |||||
| pickerName = request.pickerName, | |||||
| handleStatus = HandleStatus.pending, | |||||
| handleDate = null, | |||||
| handledBy = request.handledBy, | |||||
| created = LocalDateTime.now(), | |||||
| createdBy = "system", | |||||
| version = 0, | |||||
| modified = LocalDateTime.now(), | |||||
| modifiedBy = "system", | |||||
| deleted = false | |||||
| ) | |||||
| val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue) | |||||
| // 7. 获取相关数据用于后续处理 | |||||
| val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO | |||||
| val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO | |||||
| val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO | |||||
| val lotId = request.lotId | |||||
| val itemId = request.itemId | |||||
| println("=== PICK EXECUTION ISSUE PROCESSING (NEW LOGIC) ===") | |||||
| println("Actual Pick Qty: ${actualPickQtyForProcessing}") | |||||
| println("Miss Qty: ${missQtyForProcessing}") | |||||
| println("Bad Item Qty: ${badItemQtyForProcessing}") | |||||
| println("Lot ID: ${lotId}") | |||||
| println("Item ID: ${itemId}") | |||||
| println("================================================") | |||||
| // 8. 新的统一处理逻辑 | |||||
| when { | |||||
| // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) | |||||
| actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> { | |||||
| handleMissItemOnly(request, missQtyForProcessing) | |||||
| } | |||||
| val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null) | |||||
| // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) | |||||
| badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { | |||||
| handleBadItemOnly(request, badItemQtyForProcessing) | |||||
| // 2. 获取 inventory_lot_line 并计算账面数量 (bookQty) | |||||
| val inventoryLotLine = request.lotId?.let { | |||||
| inventoryLotLineRepository.findById(it).orElse(null) | |||||
| } | } | ||||
| // 情况3: 既有 miss item 又有 bad item | |||||
| missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { | |||||
| handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing) | |||||
| // 计算账面数量(创建 issue 时的快照) | |||||
| val bookQty = if (inventoryLotLine != null) { | |||||
| val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO | |||||
| val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO | |||||
| inQty.subtract(outQty) // bookQty = inQty - outQty | |||||
| } else { | |||||
| BigDecimal.ZERO | |||||
| } | } | ||||
| // 修复:情况4: 有 miss item 的情况(无论 actualPickQty 是多少) | |||||
| missQtyForProcessing > BigDecimal.ZERO -> { | |||||
| handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) | |||||
| } | |||||
| // 3. 获取数量值 | |||||
| val requiredQty = request.requiredQty ?: BigDecimal.ZERO | |||||
| val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO | |||||
| val missQty = request.missQty ?: BigDecimal.ZERO | |||||
| val badItemQty = request.badItemQty ?: BigDecimal.ZERO | |||||
| // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) | |||||
| actualPickQtyForProcessing > BigDecimal.ZERO -> { | |||||
| handleNormalPick(request, actualPickQtyForProcessing) | |||||
| // 4. 计算 issueQty(实际的问题数量) | |||||
| val issueQty = when { | |||||
| // 情况1: 已拣完但有坏品 | |||||
| actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { | |||||
| badItemQty // issueQty = badItemQty | |||||
| } | |||||
| // 情况2: 未拣完(有缺失或坏品) | |||||
| 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 | |||||
| } | |||||
| else -> BigDecimal.ZERO | |||||
| } | } | ||||
| else -> { | |||||
| println("Unknown case: actualPickQty=${actualPickQtyForProcessing}, missQty=${missQtyForProcessing}, badItemQty=${badItemQtyForProcessing}") | |||||
| println("=== PICK EXECUTION ISSUE PROCESSING ===") | |||||
| println("Required Qty: ${requiredQty}") | |||||
| println("Actual Pick Qty: ${actualPickQty}") | |||||
| println("Miss Qty (Reported): ${missQty}") | |||||
| println("Bad Item Qty: ${badItemQty}") | |||||
| println("Book Qty (inQty - outQty): ${bookQty}") | |||||
| println("Issue Qty (Calculated): ${issueQty}") | |||||
| println("Bad Reason: ${request.badReason}") | |||||
| println("Lot ID: ${request.lotId}") | |||||
| println("Item ID: ${request.itemId}") | |||||
| println("================================================") | |||||
| // 5. 创建 pick execution issue 记录 | |||||
| val pickExecutionIssue = PickExecutionIssue( | |||||
| id = null, | |||||
| pickOrderId = request.pickOrderId, | |||||
| pickOrderCode = request.pickOrderCode, | |||||
| pickOrderCreateDate = request.pickOrderCreateDate, | |||||
| pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(), | |||||
| pickOrderLineId = request.pickOrderLineId, | |||||
| issueNo = generateIssueNo(), | |||||
| joPickOrderId = pickOrder?.jobOrder?.id, | |||||
| doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null, | |||||
| issueCategory = IssueCategory.valueOf( | |||||
| request.issueCategory ?: "lot_issue" | |||||
| ), | |||||
| itemId = request.itemId, | |||||
| itemCode = request.itemCode, | |||||
| itemDescription = request.itemDescription, | |||||
| lotId = request.lotId, | |||||
| lotNo = request.lotNo, | |||||
| storeLocation = request.storeLocation, | |||||
| requiredQty = request.requiredQty, | |||||
| actualPickQty = request.actualPickQty, | |||||
| missQty = request.missQty, | |||||
| badItemQty = request.badItemQty, | |||||
| bookQty = bookQty, | |||||
| issueQty = issueQty, | |||||
| issueRemark = request.issueRemark, | |||||
| pickerName = request.pickerName, | |||||
| handleStatus = HandleStatus.pending, | |||||
| handleDate = null, | |||||
| handledBy = request.handledBy, | |||||
| created = LocalDateTime.now(), | |||||
| createdBy = "system", | |||||
| version = 0, | |||||
| modified = LocalDateTime.now(), | |||||
| modifiedBy = "system", | |||||
| deleted = false | |||||
| ) | |||||
| val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue) | |||||
| // 6. NEW: Update inventory_lot_line.issueQty | |||||
| if (request.lotId != null && inventoryLotLine != null) { | |||||
| val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO | |||||
| val newIssueQty = currentIssueQty.add(issueQty) | |||||
| inventoryLotLine.issueQty = newIssueQty | |||||
| inventoryLotLine.modified = LocalDateTime.now() | |||||
| inventoryLotLine.modifiedBy = "system" | |||||
| inventoryLotLineRepository.saveAndFlush(inventoryLotLine) | |||||
| println("Updated inventory_lot_line ${request.lotId} issueQty: ${currentIssueQty} -> ${newIssueQty}") | |||||
| } | } | ||||
| } | |||||
| val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null) | |||||
| val consoCode = pickOrderForCompletion?.consoCode | |||||
| if (consoCode != null) { | |||||
| 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}") | |||||
| // 7. 获取相关数据用于后续处理 | |||||
| val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO | |||||
| val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO | |||||
| val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO | |||||
| val lotId = request.lotId | |||||
| val itemId = request.itemId | |||||
| println("=== PICK EXECUTION ISSUE PROCESSING (NEW LOGIC) ===") | |||||
| println("Actual Pick Qty: ${actualPickQtyForProcessing}") | |||||
| println("Miss Qty: ${missQtyForProcessing}") | |||||
| println("Bad Item Qty: ${badItemQtyForProcessing}") | |||||
| println("Bad Reason: ${request.badReason}") | |||||
| println("Lot ID: ${lotId}") | |||||
| println("Item ID: ${itemId}") | |||||
| println("================================================") | |||||
| // 8. 新的统一处理逻辑(根据 badReason 决定处理方式) | |||||
| when { | |||||
| // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) | |||||
| actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> { | |||||
| handleMissItemOnly(request, missQtyForProcessing) | |||||
| } | |||||
| // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) | |||||
| badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { | |||||
| // NEW: Check bad reason | |||||
| if (request.badReason == "package_problem") { | |||||
| handleBadItemPackageProblem(request, badItemQtyForProcessing) | |||||
| } else { | |||||
| // quantity_problem or default: handle as normal bad item | |||||
| handleBadItemOnly(request, badItemQtyForProcessing) | |||||
| } | |||||
| } | |||||
| // 情况3: 既有 miss item 又有 bad item | |||||
| missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { | |||||
| // NEW: Check bad reason | |||||
| if (request.badReason == "package_problem") { | |||||
| handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing) | |||||
| } else { | |||||
| handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing) | |||||
| } | |||||
| } | |||||
| // 情况4: 有 miss item 的情况(无论 actualPickQty 是多少) | |||||
| missQtyForProcessing > BigDecimal.ZERO -> { | |||||
| handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) | |||||
| } | |||||
| // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) | |||||
| actualPickQtyForProcessing > BigDecimal.ZERO -> { | |||||
| handleNormalPick(request, actualPickQtyForProcessing) | |||||
| } | |||||
| else -> { | |||||
| println("Unknown case: actualPickQty=${actualPickQtyForProcessing}, missQty=${missQtyForProcessing}, badItemQty=${badItemQtyForProcessing}") | |||||
| } | |||||
| } | } | ||||
| val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null) | |||||
| val consoCode = pickOrderForCompletion?.consoCode | |||||
| if (consoCode != null) { | |||||
| 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}") | |||||
| } | |||||
| } | |||||
| return MessageResponse( | |||||
| id = savedIssue.id, | |||||
| name = "Pick execution issue recorded successfully", | |||||
| code = "SUCCESS", | |||||
| type = "pick_execution_issue", | |||||
| message = "Pick execution issue recorded successfully", | |||||
| errorPosition = null | |||||
| ) | |||||
| } catch (e: Exception) { | |||||
| println("=== ERROR IN recordPickExecutionIssue ===") | |||||
| e.printStackTrace() | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Failed to record pick execution issue", | |||||
| code = "ERROR", | |||||
| type = "pick_execution_issue", | |||||
| message = "Error: ${e.message}", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | } | ||||
| return MessageResponse( | |||||
| id = savedIssue.id, | |||||
| name = "Pick execution issue recorded successfully", | |||||
| code = "SUCCESS", | |||||
| type = "pick_execution_issue", | |||||
| message = "Pick execution issue recorded successfully", | |||||
| errorPosition = null | |||||
| ) | |||||
| } catch (e: Exception) { | |||||
| println("=== ERROR IN recordPickExecutionIssue ===") | |||||
| e.printStackTrace() | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Failed to record pick execution issue", | |||||
| code = "ERROR", | |||||
| type = "pick_execution_issue", | |||||
| message = "Error: ${e.message}", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | } | ||||
| } | |||||
| private fun generateIssueNo(): String { | private fun generateIssueNo(): String { | ||||
| val now = LocalDateTime.now() | val now = LocalDateTime.now() | ||||
| val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM")) | val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM")) | ||||
| @@ -1511,7 +1524,8 @@ private fun updateLotLineAfterIssue(lotLineId: Long, qty: BigDecimal) { | |||||
| val currentOutQty = lotLine.outQty ?: BigDecimal.ZERO | val currentOutQty = lotLine.outQty ?: BigDecimal.ZERO | ||||
| val newOutQty = currentOutQty.add(qty) | val newOutQty = currentOutQty.add(qty) | ||||
| lotLine.outQty = newOutQty | lotLine.outQty = newOutQty | ||||
| val currentIssueQty = lotLine.issueQty ?: BigDecimal.ZERO | |||||
| lotLine.issueQty = currentIssueQty.subtract(qty) | |||||
| // If outQty != inQty, set status to AVAILABLE | // If outQty != inQty, set status to AVAILABLE | ||||
| val inQty = lotLine.inQty ?: BigDecimal.ZERO | val inQty = lotLine.inQty ?: BigDecimal.ZERO | ||||
| if (newOutQty != inQty) { | if (newOutQty != inQty) { | ||||
| @@ -1627,4 +1641,189 @@ private fun createStockLedgerForStockOut(stockOutLine: StockOutLine, ledgerType: | |||||
| println("Balance: ${finalBalance}") | println("Balance: ${finalBalance}") | ||||
| println("===========================") | println("===========================") | ||||
| } | } | ||||
| @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class]) | |||||
| private fun handleBadItemPackageProblem(request: PickExecutionIssueRequest, badItemQty: BigDecimal) { | |||||
| println("=== HANDLING BAD ITEM PACKAGE PROBLEM (DON'T REJECT LOT) ===") | |||||
| println("Bad Item Qty: ${badItemQty}") | |||||
| val lotId = request.lotId ?: return | |||||
| val itemId = request.itemId ?: return | |||||
| val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) | |||||
| if (inventoryLotLine != null) { | |||||
| // Package problem: Don't mark as unavailable, just record the issue | |||||
| // Update outQty for actual picked items if any | |||||
| if (request.actualPickQty != null && request.actualPickQty > BigDecimal.ZERO) { | |||||
| val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO | |||||
| val newOutQty = currentOutQty.add(request.actualPickQty) | |||||
| inventoryLotLine.outQty = newOutQty | |||||
| println("Package problem: Updated outQty: ${currentOutQty} -> ${newOutQty}") | |||||
| } | |||||
| // IssueQty is already updated in recordPickExecutionIssue | |||||
| inventoryLotLine.modified = LocalDateTime.now() | |||||
| inventoryLotLine.modifiedBy = "system" | |||||
| inventoryLotLineRepository.saveAndFlush(inventoryLotLine) | |||||
| println("Package problem: Updated lot ${lotId}, but did NOT mark as unavailable") | |||||
| } | |||||
| // Update inventory unavailableQty (still need to track unavailable quantity) | |||||
| updateInventoryUnavailableQty(itemId, badItemQty) | |||||
| // DON'T reject stock_out_line for package problem | |||||
| val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||||
| request.pickOrderLineId, | |||||
| request.lotId ?: 0L | |||||
| ) | |||||
| stockOutLines.forEach { stockOutLine -> | |||||
| if (request.actualPickQty != null) { | |||||
| stockOutLine.qty = request.actualPickQty.toDouble() | |||||
| } | |||||
| // Determine status based on actual pick qty vs required qty | |||||
| val requiredQty = request.requiredQty?.toDouble() ?: 0.0 | |||||
| val actualPickQtyDouble = request.actualPickQty?.toDouble() ?: 0.0 | |||||
| val newStatus = if (actualPickQtyDouble >= requiredQty) { | |||||
| "completed" | |||||
| } else { | |||||
| "partially_completed" | |||||
| } | |||||
| stockOutLine.status = newStatus | |||||
| stockOutLine.modified = LocalDateTime.now() | |||||
| stockOutLine.modifiedBy = "system" | |||||
| val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) | |||||
| // Create stock ledger for actual picked items | |||||
| if (request.actualPickQty != null && request.actualPickQty > BigDecimal.ZERO) { | |||||
| createStockLedgerForStockOut(savedStockOutLine, "Nor") | |||||
| } | |||||
| println("Package problem: Updated stock out line ${stockOutLine.id} status to: ${newStatus} (NOT rejected)") | |||||
| } | |||||
| try { | |||||
| // 确保所有更改都已刷新到数据库 | |||||
| stockOutLineRepository.flush() | |||||
| // 直接查询并检查该 pick order line 下的所有 stock out lines | |||||
| val allStockOutLines = stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(request.pickOrderLineId) | |||||
| val unfinishedLines = allStockOutLines.filter { | |||||
| it.status != StockOutLineStatus.COMPLETE.status | |||||
| && it.status != StockOutLineStatus.REJECTED.status | |||||
| } | |||||
| println("Package problem: 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("✅ Package problem: Updated pick order line ${request.pickOrderLineId} status to COMPLETED") | |||||
| } else { | |||||
| println("⚠️ Package problem: Pick order line ${request.pickOrderLineId} not found") | |||||
| } | |||||
| } else { | |||||
| println("⚠️ Package problem: Pick order line ${request.pickOrderLineId} still has ${unfinishedLines.size} unfinished lines") | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| println("⚠️ Error checking pick order line completion: ${e.message}") | |||||
| e.printStackTrace() | |||||
| } | |||||
| // Don't resuggest for package problem (lot is still available) | |||||
| println("Package problem: Did NOT resuggest pick order (lot remains available)") | |||||
| } | |||||
| // NEW: Handle both miss and bad item with package problem | |||||
| @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class]) | |||||
| private fun handleBothMissAndBadItemPackageProblem(request: PickExecutionIssueRequest, missQty: BigDecimal, badItemQty: BigDecimal) { | |||||
| println("=== HANDLING BOTH MISS AND BAD ITEM PACKAGE PROBLEM ===") | |||||
| println("Miss Qty: ${missQty}, Bad Item Qty: ${badItemQty}") | |||||
| val lotId = request.lotId ?: return | |||||
| val itemId = request.itemId ?: return | |||||
| val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) | |||||
| if (inventoryLotLine != null) { | |||||
| // Update outQty for actual picked items | |||||
| if (request.actualPickQty != null && request.actualPickQty > BigDecimal.ZERO) { | |||||
| val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO | |||||
| val newOutQty = currentOutQty.add(request.actualPickQty) | |||||
| inventoryLotLine.outQty = newOutQty | |||||
| println("Both miss and bad (package): Updated outQty: ${currentOutQty} -> ${newOutQty}") | |||||
| } | |||||
| // Miss item: mark as unavailable | |||||
| if (missQty > BigDecimal.ZERO) { | |||||
| inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE | |||||
| println("Both miss and bad (package): Set status to UNAVAILABLE due to missQty: ${missQty}") | |||||
| } | |||||
| // IssueQty is already updated in recordPickExecutionIssue | |||||
| inventoryLotLine.modified = LocalDateTime.now() | |||||
| inventoryLotLine.modifiedBy = "system" | |||||
| inventoryLotLineRepository.saveAndFlush(inventoryLotLine) | |||||
| } | |||||
| // Update inventory unavailableQty (miss qty + bad qty) | |||||
| val totalUnavailableQty = missQty.add(badItemQty) | |||||
| updateInventoryUnavailableQty(itemId, totalUnavailableQty) | |||||
| // Update stock_out_line: reject only if missQty > 0, otherwise partial/completed | |||||
| val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||||
| request.pickOrderLineId, | |||||
| request.lotId ?: 0L | |||||
| ) | |||||
| stockOutLines.forEach { stockOutLine -> | |||||
| if (request.actualPickQty != null) { | |||||
| stockOutLine.qty = request.actualPickQty.toDouble() | |||||
| } | |||||
| val requiredQty = request.requiredQty?.toDouble() ?: 0.0 | |||||
| val actualPickQtyDouble = request.actualPickQty?.toDouble() ?: 0.0 | |||||
| // If missQty > 0, reject; otherwise check completion status | |||||
| val newStatus = if (missQty > BigDecimal.ZERO) { | |||||
| "rejected" | |||||
| } else if (actualPickQtyDouble >= requiredQty) { | |||||
| "completed" | |||||
| } else { | |||||
| "partially_completed" | |||||
| } | |||||
| stockOutLine.status = newStatus | |||||
| stockOutLine.modified = LocalDateTime.now() | |||||
| stockOutLine.modifiedBy = "system" | |||||
| val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) | |||||
| if (request.actualPickQty != null && request.actualPickQty > BigDecimal.ZERO) { | |||||
| createStockLedgerForStockOut(savedStockOutLine, "Nor") | |||||
| } | |||||
| println("Both miss and bad (package): Updated stock out line ${stockOutLine.id} status to: ${newStatus}") | |||||
| } | |||||
| // Resuggest only for miss qty | |||||
| if (missQty > BigDecimal.ZERO) { | |||||
| try { | |||||
| resuggestPickOrder(request.pickOrderId) | |||||
| println("Resuggested pick order for miss qty: ${missQty}") | |||||
| } catch (e: Exception) { | |||||
| println("Error during resuggest: ${e.message}") | |||||
| } | |||||
| } else { | |||||
| println("Both miss and bad (package): Did NOT resuggest (only bad item with package problem)") | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -3601,8 +3601,8 @@ ORDER BY | |||||
| val enrichedResults = filteredResults | val enrichedResults = filteredResults | ||||
| return enrichedResults | return enrichedResults | ||||
| } | } | ||||
| // ... existing code ... | |||||
| open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | |||||
| // 修改后的逻辑 | |||||
| open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | |||||
| println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (Repository-based) ===") | println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (Repository-based) ===") | ||||
| println("userId filter: $userId") | println("userId filter: $userId") | ||||
| @@ -3612,8 +3612,9 @@ ORDER BY | |||||
| return emptyMap() | return emptyMap() | ||||
| } | } | ||||
| // Step 1: 先找到分配给该用户的所有 pick orders | |||||
| val userPickOrders = pickOrderRepository.findAll() | |||||
| // Step 1: 先找到用户有活跃任务的 do_pick_order(通过 do_pick_order_line) | |||||
| // 这样可以确保只获取同一个 ticket 的 pick orders | |||||
| val userPickOrdersForDo = pickOrderRepository.findAll() | |||||
| .filter { | .filter { | ||||
| it.deleted == false && | it.deleted == false && | ||||
| it.assignTo?.id == userId && | it.assignTo?.id == userId && | ||||
| @@ -3624,26 +3625,26 @@ ORDER BY | |||||
| it.status == PickOrderStatus.ASSIGNED) | it.status == PickOrderStatus.ASSIGNED) | ||||
| } | } | ||||
| if (userPickOrders.isEmpty()) { | |||||
| println("❌ No pick orders found for user $userId") | |||||
| if (userPickOrdersForDo.isEmpty()) { | |||||
| println("❌ No active pick orders found for user $userId") | |||||
| return mapOf( | return mapOf( | ||||
| "fgInfo" to null, | "fgInfo" to null, | ||||
| "pickOrders" to emptyList<Any>() | "pickOrders" to emptyList<Any>() | ||||
| ) | ) | ||||
| } | } | ||||
| println(" Found ${userPickOrders.size} pick orders assigned to user $userId") | |||||
| val pickOrderIds = userPickOrders.mapNotNull { it.id } | |||||
| val activePickOrderIds = userPickOrdersForDo.mapNotNull { it.id } | |||||
| println(" Found ${activePickOrderIds.size} active pick orders assigned to user $userId") | |||||
| // Step 2: 通过 do_pick_order_line 找到相关的 do_pick_order | // Step 2: 通过 do_pick_order_line 找到相关的 do_pick_order | ||||
| val doPickOrderLineRecords = pickOrderIds.flatMap { poId -> | |||||
| val doPickOrderLineRecords = activePickOrderIds.flatMap { poId -> | |||||
| doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(poId) | doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(poId) | ||||
| } | } | ||||
| val doPickOrderIds = doPickOrderLineRecords.mapNotNull { it.doPickOrderId }.distinct() | val doPickOrderIds = doPickOrderLineRecords.mapNotNull { it.doPickOrderId }.distinct() | ||||
| if (doPickOrderIds.isEmpty()) { | if (doPickOrderIds.isEmpty()) { | ||||
| println("❌ No do_pick_order found for pick orders: $pickOrderIds") | |||||
| println("❌ No do_pick_order found for pick orders: $activePickOrderIds") | |||||
| return mapOf( | return mapOf( | ||||
| "fgInfo" to null, | "fgInfo" to null, | ||||
| "pickOrders" to emptyList<Any>() | "pickOrders" to emptyList<Any>() | ||||
| @@ -3665,11 +3666,22 @@ ORDER BY | |||||
| val doPickOrderId = doPickOrder.id!! | val doPickOrderId = doPickOrder.id!! | ||||
| println(" Using do_pick_order ID: $doPickOrderId") | println(" Using do_pick_order ID: $doPickOrderId") | ||||
| // Step 4: 使用 Repository 加载 pick orders 及其关联数据 | |||||
| val pickOrders = pickOrderRepository.findAllById(pickOrderIds) | |||||
| .filter { it.deleted == false } | |||||
| // Step 4: 关键修改 - 只获取这个 do_pick_order 下的所有 pick orders(无论状态) | |||||
| val allDoPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrderId) | |||||
| val allPickOrderIdsForThisTicket = allDoPickOrderLines.mapNotNull { it.pickOrderId }.distinct() | |||||
| println(" Loaded ${pickOrders.size} pick orders") | |||||
| println(" Found ${allPickOrderIdsForThisTicket.size} pick orders in this do_pick_order (including completed)") | |||||
| // Step 5: 加载这些 pick orders(包括 COMPLETED 状态) | |||||
| val pickOrders = pickOrderRepository.findAllById(allPickOrderIdsForThisTicket) | |||||
| .filter { | |||||
| it.deleted == false && | |||||
| it.assignTo?.id == userId && // 确保是分配给该用户的 | |||||
| it.type?.value == "do" | |||||
| // 不限制状态,包括 COMPLETED | |||||
| } | |||||
| println(" Loaded ${pickOrders.size} pick orders (including completed)") | |||||
| // 收集所有需要的数据 | // 收集所有需要的数据 | ||||
| // 收集所有需要的数据 | // 收集所有需要的数据 | ||||
| @@ -3780,6 +3792,7 @@ ORDER BY | |||||
| "outQty" to ill.outQty, | "outQty" to ill.outQty, | ||||
| "holdQty" to ill.holdQty, | "holdQty" to ill.holdQty, | ||||
| "lotStatus" to ill.status?.value, | "lotStatus" to ill.status?.value, | ||||
| "stockInLineId" to il?.stockInLine?.id, | |||||
| "lotAvailability" to when { | "lotAvailability" to when { | ||||
| isExpired -> "expired" | isExpired -> "expired" | ||||
| stockOutLine?.status == "rejected" -> "rejected" | stockOutLine?.status == "rejected" -> "rejected" | ||||
| @@ -3866,6 +3879,7 @@ ORDER BY | |||||
| "outQty" to ill.outQty, | "outQty" to ill.outQty, | ||||
| "holdQty" to ill.holdQty, | "holdQty" to ill.holdQty, | ||||
| "lotStatus" to ill.status?.value, | "lotStatus" to ill.status?.value, | ||||
| "stockInLineId" to il?.stockInLine?.id, | |||||
| "lotAvailability" to when { | "lotAvailability" to when { | ||||
| isExpired -> "expired" | isExpired -> "expired" | ||||
| sol.status == "rejected" -> "rejected" | sol.status == "rejected" -> "rejected" | ||||
| @@ -27,7 +27,8 @@ data class PickExecutionIssueRequest( | |||||
| val badItemQty: BigDecimal = BigDecimal.ZERO, | val badItemQty: BigDecimal = BigDecimal.ZERO, | ||||
| val issueRemark: String? = null, | val issueRemark: String? = null, | ||||
| val pickerName: String? = null, | val pickerName: String? = null, | ||||
| val handledBy: Long? = null | |||||
| val handledBy: Long? = null, | |||||
| val badReason: String? = null, | |||||
| ) | ) | ||||
| @@ -45,6 +45,9 @@ open class InventoryLotLine : BaseEntity<Long>() { | |||||
| @Column(name = "holdQty") | @Column(name = "holdQty") | ||||
| open var holdQty: BigDecimal? = null | open var holdQty: BigDecimal? = null | ||||
| @Column(name = "issueQty") | |||||
| open var issueQty: BigDecimal? = null | |||||
| @Convert(converter = InventoryLotLineStringConverter::class) | @Convert(converter = InventoryLotLineStringConverter::class) | ||||
| @Column(name = "status") | @Column(name = "status") | ||||
| open var status: InventoryLotLineStatus? = null | open var status: InventoryLotLineStatus? = null | ||||
| @@ -47,4 +47,26 @@ fun searchStockOutLines( | |||||
| @Query("SELECT sol FROM StockOutLine sol WHERE sol.item.id IN :itemIds AND sol.deleted = false") | @Query("SELECT sol FROM StockOutLine sol WHERE sol.item.id IN :itemIds AND sol.deleted = false") | ||||
| fun findAllByItemIdInAndDeletedFalse(itemIds: List<Long>): List<StockOutLine> | fun findAllByItemIdInAndDeletedFalse(itemIds: List<Long>): List<StockOutLine> | ||||
| @Query(""" | |||||
| SELECT sol FROM StockOutLine sol | |||||
| WHERE sol.pickOrderLine.id IN :pickOrderLineIds | |||||
| AND sol.deleted = false | |||||
| """) | |||||
| fun findAllByPickOrderLineIdInAndDeletedFalse( | |||||
| @Param("pickOrderLineIds") pickOrderLineIds: List<Long> | |||||
| ): List<StockOutLine> | |||||
| // 添加批量查询方法:按 (pickOrderLineId, inventoryLotLineId) 组合查询 | |||||
| @Query(""" | |||||
| SELECT sol FROM StockOutLine sol | |||||
| WHERE sol.pickOrderLine.id IN :pickOrderLineIds | |||||
| AND sol.inventoryLotLine.id IN :inventoryLotLineIds | |||||
| AND sol.deleted = false | |||||
| """) | |||||
| fun findAllByPickOrderLineIdInAndInventoryLotLineIdInAndDeletedFalse( | |||||
| @Param("pickOrderLineIds") pickOrderLineIds: List<Long>, | |||||
| @Param("inventoryLotLineIds") inventoryLotLineIds: List<Long> | |||||
| ): List<StockOutLine> | |||||
| } | } | ||||
| @@ -523,249 +523,301 @@ open class SuggestedPickLotService( | |||||
| return null | return null | ||||
| } | } | ||||
| } | } | ||||
| @Transactional(rollbackFor = [Exception::class]) | |||||
| 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") | |||||
| val allCompetingPickOrders = mutableListOf<PickOrder>() | |||||
| 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) | |||||
| @Transactional(rollbackFor = [Exception::class]) | |||||
| 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") | |||||
| val allCompetingPickOrders = mutableListOf<PickOrder>() | |||||
| 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})") | |||||
| } | } | ||||
| // 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 -> | |||||
| val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!) | |||||
| val hasRejectedStockOutLine = stockOutLines.any { it.status.equals("rejected", ignoreCase = true)} | |||||
| if (hasRejectedStockOutLine) { | |||||
| println("Pick Order ${pickOrderToCheck.code} has rejected stock out lines - will resuggest") | |||||
| stockOutLines.filter { it.status == "rejected" }.forEach { sol -> | |||||
| println(" - Rejected stock out line: ${sol.id} (lot: ${sol.inventoryLotLineId}, qty: ${sol.qty})") | |||||
| } | |||||
| allCompetingPickOrders.addAll(competingOrders) | |||||
| } | |||||
| // OPTIMIZATION 3: 批量收集所有需要检查的 pick order line IDs | |||||
| val allPickOrderLineIdsToCheck = (listOf(pickOrder) + allCompetingPickOrders) | |||||
| .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 次查询) | |||||
| // 注意:findAllByPickOrderLineIdInAndDeletedFalse 返回 List<StockOutLine>(实体类) | |||||
| val allStockOutLines = if (allPickOrderLineIdsToCheck.isNotEmpty()) { | |||||
| stockOutLIneRepository.findAllByPickOrderLineIdInAndDeletedFalse(allPickOrderLineIdsToCheck) | |||||
| } else { | |||||
| emptyList() | |||||
| } | |||||
| 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 -> | |||||
| println(" - Rejected stock out line: ${sol.id} (lot: ${sol.inventoryLotLine?.id}, qty: ${sol.qty})") | |||||
| } | } | ||||
| hasRejectedStockOutLine | |||||
| } | |||||
| hasRejectedStockOutLine | |||||
| } | |||||
| } | } | ||||
| println("=== RESUGGEST DEBUG ===") | |||||
| println("Original pick orders: ${(listOf(pickOrder) + allCompetingPickOrders).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") | |||||
| // 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() | |||||
| println("=== RESUGGEST DEBUG ===") | |||||
| println("Original pick orders: ${(listOf(pickOrder) + allCompetingPickOrders).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 | |||||
| // OPTIMIZATION 3: 使用批量查询方法(一次查询代替 N*M 次查询) | |||||
| // 注意:返回 List<StockOutLine>(实体类) | |||||
| if (suggestionLineIds.isNotEmpty() && suggestionLotIds.isNotEmpty()) { | |||||
| stockOutLIneRepository.findAllByPickOrderLineIdInAndInventoryLotLineIdInAndDeletedFalse( | |||||
| suggestionLineIds, | |||||
| suggestionLotIds | |||||
| ) | ) | ||||
| } else { | |||||
| emptyList() | |||||
| } | } | ||||
| // 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") | |||||
| // FIX: Get all existing suggestions for these pick order lines | |||||
| val allSuggestions = suggestedPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIds) | |||||
| println("Found ${allSuggestions.size} existing suggestions") | |||||
| // 删除第 376-395 行的旧代码,替换为: | |||||
| // 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 | |||||
| if (pickOrderLineId != null && suggestedLotLineId != null) { | |||||
| val stockOutLines = stockOutLIneRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||||
| pickOrderLineId, | |||||
| suggestedLotLineId | |||||
| ) | |||||
| // 保留没有 rejected stock out lines 的 suggestions | |||||
| !stockOutLines.any { it.status == "rejected" } | |||||
| } else { | |||||
| true // 保留有问题的 suggestions 用于调试 | |||||
| } | |||||
| } | |||||
| // 只删除有 rejected stock out lines 的 suggestions | |||||
| val suggestionsToDelete = allSuggestions.filter { suggestion -> | |||||
| val pickOrderLineId = suggestion.pickOrderLine?.id | |||||
| val suggestedLotLineId = suggestion.suggestedLotLine?.id | |||||
| if (pickOrderLineId != null && suggestedLotLineId != null) { | |||||
| val stockOutLines = stockOutLIneRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||||
| pickOrderLineId, | |||||
| suggestedLotLineId | |||||
| ) | |||||
| stockOutLines.any { it.status == "rejected" } // 只删除 rejected 的 | |||||
| } else { | |||||
| suggestedLotLineId == null | |||||
| } | |||||
| } | |||||
| } else { | |||||
| emptyList() | |||||
| } | |||||
| println("Found ${allSuggestionStockOutLines.size} stock out lines for suggestions in batch query (1 query instead of ${allSuggestions.size} queries)") | |||||
| // OPTIMIZATION 3: 按 (pickOrderLineId, inventoryLotLineId) 分组 | |||||
| // 注意:StockOutLine 实体使用 pickOrderLine?.id 和 inventoryLotLine?.id | |||||
| 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 suggestedLotLineId = suggestion.suggestedLotLine?.id | |||||
| println("Suggestions to keep (with rejected stock out lines): ${suggestionsToKeep.size}") | |||||
| println("Suggestions to delete: ${suggestionsToDelete.size}") | |||||
| // 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") | |||||
| rejectedLotIds.forEach { lotId -> | |||||
| val lot = inventoryLotLineRepository.findById(lotId).orElse(null) | |||||
| lot?.let { | |||||
| val originalHoldQty = it.holdQty ?: BigDecimal.ZERO | |||||
| it.holdQty = BigDecimal.ZERO | |||||
| inventoryLotLineRepository.save(it) | |||||
| println("Cleared holdQty for rejected lot ${lot.id}: $originalHoldQty -> 0") | |||||
| } | |||||
| 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 用于调试 | |||||
| } | } | ||||
| } | |||||
| println("Keeping all suggestions (including rejected ones for display)") | |||||
| // NEW: Build holdQtyMap with existing holdQty from other pick orders | |||||
| val existingHoldQtyMap = mutableMapOf<Long?, BigDecimal?>() | |||||
| // 只删除有 rejected stock out lines 的 suggestions | |||||
| val suggestionsToDelete = allSuggestions.filter { suggestion -> | |||||
| val pickOrderLineId = suggestion.pickOrderLine?.id | |||||
| val suggestedLotLineId = suggestion.suggestedLotLine?.id | |||||
| // Get all lots that are being used by other pick orders (including those NOT being resuggested) | |||||
| val allOtherPickOrderLineIds = allCompetingPickOrders | |||||
| .flatMap { it.pickOrderLines } | |||||
| .mapNotNull { it.id } | |||||
| println("Other pick order line IDs: $allOtherPickOrderLineIds") | |||||
| 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 的 | |||||
| } else { | |||||
| suggestedLotLineId == null | |||||
| } | |||||
| } | |||||
| println("Suggestions to keep: ${suggestionsToKeep.size}") | |||||
| println("Suggestions to delete: ${suggestionsToDelete.size}") | |||||
| // 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") | |||||
| } | |||||
| println("Keeping all suggestions (including rejected ones for display)") | |||||
| // NEW: Build holdQtyMap with existing holdQty from other pick orders | |||||
| val existingHoldQtyMap = mutableMapOf<Long?, BigDecimal?>() | |||||
| // Get all lots that are being used by other pick orders (including those NOT being resuggested) | |||||
| val allOtherPickOrderLineIds = allCompetingPickOrders | |||||
| .flatMap { it.pickOrderLines } | |||||
| .mapNotNull { it.id } | |||||
| println("Other pick order line IDs: $allOtherPickOrderLineIds") | |||||
| if (allOtherPickOrderLineIds.isNotEmpty()) { | |||||
| val otherSuggestions = suggestedPickLotRepository.findAllByPickOrderLineIdIn(allOtherPickOrderLineIds) | |||||
| println("Found ${otherSuggestions.size} other suggestions") | |||||
| if (allOtherPickOrderLineIds.isNotEmpty()) { | |||||
| val otherSuggestions = suggestedPickLotRepository.findAllByPickOrderLineIdIn(allOtherPickOrderLineIds) | |||||
| println("Found ${otherSuggestions.size} other suggestions") | |||||
| otherSuggestions.forEach { suggestion -> | |||||
| val lotId = suggestion.suggestedLotLine?.id | |||||
| val qty = suggestion.qty | |||||
| otherSuggestions.forEach { suggestion -> | |||||
| val lotId = suggestion.suggestedLotLine?.id | |||||
| val qty = suggestion.qty | |||||
| println("Processing other suggestion ${suggestion.id}: lotId=$lotId, qty=$qty") | |||||
| if (lotId != null && qty != null) { | |||||
| val currentHoldQty = existingHoldQtyMap[lotId] ?: BigDecimal.ZERO | |||||
| val newHoldQty = currentHoldQty.plus(qty) | |||||
| existingHoldQtyMap[lotId] = newHoldQty | |||||
| println(" Updated holdQty for lot $lotId: $currentHoldQty + $qty = $newHoldQty") | |||||
| } | |||||
| if (lotId != null && qty != null) { | |||||
| val currentHoldQty = existingHoldQtyMap[lotId] ?: BigDecimal.ZERO | |||||
| val newHoldQty = currentHoldQty.plus(qty) | |||||
| existingHoldQtyMap[lotId] = newHoldQty | |||||
| } | } | ||||
| } | } | ||||
| } | |||||
| 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 | |||||
| val problematicPickOrderLines = pickOrderToResuggest.pickOrderLines.filter { pol -> | |||||
| val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() | |||||
| stockOutLines.any { it.status?.equals("rejected", ignoreCase = true) == true } | |||||
| } | |||||
| 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 | |||||
| val problematicPickOrderLines = pickOrderToResuggest.pickOrderLines.filter { pol -> | |||||
| val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!) | |||||
| stockOutLines.any { it.status == "rejected" } | |||||
| } | |||||
| if (problematicPickOrderLines.isNotEmpty()) { | |||||
| println("=== Creating new suggestions for pick order: ${pickOrderToResuggest.code} ===") | |||||
| if (problematicPickOrderLines.isNotEmpty()) { | |||||
| println("=== Creating new suggestions for pick order: ${pickOrderToResuggest.code} ===") | |||||
| // 调用 suggestionForPickOrderLines 生成新的 suggestions | |||||
| val request = SuggestedPickLotForPolRequest( | |||||
| pickOrderLines = problematicPickOrderLines, | |||||
| holdQtyMap = existingHoldQtyMap.toMutableMap() | |||||
| ) | |||||
| val response = suggestionForPickOrderLines(request) | |||||
| println("Generated ${response.suggestedList.size} new suggestions") | |||||
| response.suggestedList.forEach { suggestion -> | |||||
| println(" - Suggestion: lotId=${suggestion.suggestedLotLine?.id}, qty=${suggestion.qty}") | |||||
| } | |||||
| // 调用 suggestionForPickOrderLines 生成新的 suggestions | |||||
| val request = SuggestedPickLotForPolRequest( | |||||
| pickOrderLines = problematicPickOrderLines, | |||||
| holdQtyMap = existingHoldQtyMap.toMutableMap() | |||||
| ) | |||||
| val response = suggestionForPickOrderLines(request) | |||||
| println("Generated ${response.suggestedList.size} new suggestions") | |||||
| if (response.suggestedList.isNotEmpty()) { | |||||
| println("Saving ${response.suggestedList.size} new suggestions") | |||||
| if (response.suggestedList.isNotEmpty()) { | |||||
| println("Saving ${response.suggestedList.size} new suggestions") | |||||
| // 获取现有的 pending/checked 状态的 suggestions(可以更新的) | |||||
| // 获取现有的 pending/checked 状态的 suggestions(可以更新的) | |||||
| val existingUpdatableSuggestions = suggestionsToKeep | val existingUpdatableSuggestions = suggestionsToKeep | ||||
| .filter { it.suggestedLotLine?.id != null } | |||||
| .groupBy { it.pickOrderLine?.id to it.suggestedLotLine?.id } | |||||
| .mapValues { it.value.first() } // 每个 (lineId, lotId) 只取第一个 | |||||
| .filter { it.suggestedLotLine?.id != null } | |||||
| .groupBy { it.pickOrderLine?.id to it.suggestedLotLine?.id } | |||||
| .mapValues { it.value.first() } // 每个 (lineId, lotId) 只取第一个 | |||||
| // 处理新的 suggestions:更新现有的或创建新的 | // 处理新的 suggestions:更新现有的或创建新的 | ||||
| val suggestionsToSave = response.suggestedList.mapNotNull { newSugg -> | val suggestionsToSave = response.suggestedList.mapNotNull { newSugg -> | ||||
| val key = newSugg.pickOrderLine?.id to newSugg.suggestedLotLine?.id | |||||
| val lineId = newSugg.pickOrderLine?.id | |||||
| val lotId = newSugg.suggestedLotLine?.id | |||||
| val key = newSugg.pickOrderLine?.id to newSugg.suggestedLotLine?.id | |||||
| val lineId = newSugg.pickOrderLine?.id | |||||
| val lotId = newSugg.suggestedLotLine?.id | |||||
| if (lineId != null && lotId != null) { | |||||
| // 检查这个 lot 是否已有 suggestion | |||||
| val existingSugg = existingUpdatableSuggestions[key] | |||||
| if (existingSugg != null) { | |||||
| // 检查现有 suggestion 的 stock_out_line 状态 | |||||
| val stockOutLines = stockOutLIneRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||||
| lineId, lotId | |||||
| ) | |||||
| if (lineId != null && lotId != null) { | |||||
| // 检查这个 lot 是否已有 suggestion | |||||
| val existingSugg = existingUpdatableSuggestions[key] | |||||
| val canUpdate = stockOutLines.isEmpty() || stockOutLines.all { | |||||
| it.status == "pending" || it.status == "checked" || it.status == "partially_completed" | |||||
| if (existingSugg != null) { | |||||
| // OPTIMIZATION 3: 从预加载的 Map 中获取 | |||||
| val stockOutLines = stockOutLinesByLineAndLot[lineId to lotId] ?: emptyList() | |||||
| val canUpdate = stockOutLines.isEmpty() || stockOutLines.all { | |||||
| val status = it.status?.lowercase() | |||||
| status == "pending" || status == "checked" || status == "partially_completed" | |||||
| } | |||||
| if (canUpdate) { | |||||
| // Case 1: 更新现有的 suggestion | |||||
| existingSugg.qty = newSugg.qty | |||||
| existingSugg.modified = LocalDateTime.now() | |||||
| existingSugg.modifiedBy = "system" | |||||
| println("⚠️ Updated existing suggestion ${existingSugg.id} for lot $lotId: new qty=${newSugg.qty}") | |||||
| existingSugg | |||||
| } else { | |||||
| // Case 2: 已完成/拒绝,跳过(不更新,也不创建新的) | |||||
| val firstStatus = stockOutLines.firstOrNull()?.status ?: "unknown" | |||||
| println("⏭️ Skipping lot $lotId - already $firstStatus") | |||||
| null | |||||
| } | |||||
| } else { | |||||
| // 没有现有的 suggestion,创建新的 | |||||
| newSugg | |||||
| } | } | ||||
| } else if (lotId == null) { | |||||
| // lotId=null:检查是否已有 resuggest_issue | |||||
| val existingResuggestIssues = pickExecutionIssueRepository | |||||
| .findByPickOrderLineIdAndDeletedFalse(lineId ?: 0L) | |||||
| .filter { it.issueCategory.name == "resuggest_issue" } | |||||
| if (canUpdate) { | |||||
| // Case 1: 更新现有的 suggestion | |||||
| existingSugg.qty = newSugg.qty | |||||
| existingSugg.modified = LocalDateTime.now() | |||||
| existingSugg.modifiedBy = "system" | |||||
| println("⚠️ Updated existing suggestion ${existingSugg.id} for lot $lotId: new qty=${newSugg.qty}") | |||||
| existingSugg | |||||
| if (existingResuggestIssues.isEmpty()) { | |||||
| newSugg // 创建新的 null suggestion(后续会创建 issue) | |||||
| } else { | } else { | ||||
| // Case 2: 已完成/拒绝,跳过(不更新,也不创建新的) | |||||
| println("⏭️ Skipping lot $lotId - already ${stockOutLines.first().status}") | |||||
| null | |||||
| println("⏭️ Resuggest issue already exists for line $lineId, skipping null suggestion") | |||||
| null // 跳过,避免创建重复的 resuggest_issue | |||||
| } | } | ||||
| } else { | } else { | ||||
| // 没有现有的 suggestion,创建新的 | |||||
| newSugg | newSugg | ||||
| } | } | ||||
| } else if (lotId == null) { | |||||
| // lotId=null:检查是否已有 resuggest_issue | |||||
| val existingResuggestIssues = pickExecutionIssueRepository | |||||
| .findByPickOrderLineIdAndDeletedFalse(lineId ?: 0L) | |||||
| .filter { it.issueCategory.name == "resuggest_issue" } | |||||
| if (existingResuggestIssues.isEmpty()) { | |||||
| newSugg // 创建新的 null suggestion(后续会创建 issue) | |||||
| } else { | |||||
| println("⏭️ Resuggest issue already exists for line $lineId, skipping null suggestion") | |||||
| null // 跳过,避免创建重复的 resuggest_issue | |||||
| } | |||||
| } else { | |||||
| newSugg | |||||
| } | |||||
| }.filterNotNull() | }.filterNotNull() | ||||
| val updatedSuggestions = suggestionsToSave.filter { it.id != null } // 有 id 的是更新的 | val updatedSuggestions = suggestionsToSave.filter { it.id != null } // 有 id 的是更新的 | ||||
| @@ -789,175 +841,169 @@ println("Keeping all suggestions (including rejected ones for display)") | |||||
| val savedSuggestions = allSavedSuggestions | val savedSuggestions = allSavedSuggestions | ||||
| println("Saved/Updated ${savedSuggestions.size} suggestions") | println("Saved/Updated ${savedSuggestions.size} suggestions") | ||||
| // 为每个新 suggestion 创建 stock out line 或 issue | |||||
| savedSuggestions.forEach { suggestion -> | |||||
| if (suggestion.suggestedLotLine != null) { | |||||
| val isNewSuggestion = response.suggestedList.any { | |||||
| it.pickOrderLine?.id == suggestion.pickOrderLine?.id && | |||||
| it.suggestedLotLine?.id == suggestion.suggestedLotLine?.id && | |||||
| it.id == null // 新的 suggestion 还没有 id | |||||
| } | |||||
| val stockOutLine = createStockOutLineForSuggestion(suggestion, pickOrderToResuggest) | |||||
| if (stockOutLine != null) { | |||||
| println(" Created stock out line ${stockOutLine.id} for suggestion ${suggestion.id}") | |||||
| } | |||||
| } else { | |||||
| // 如果 lot 是 null,表示没有可用的 lot,创建 resuggest_issue | |||||
| println("❌ No available lot for pick order line ${suggestion.pickOrderLine?.id}, creating resuggest_issue") | |||||
| val pickOrderLine = suggestion.pickOrderLine | |||||
| if (pickOrderLine != null) { | |||||
| // 获取这个 line 的 rejected stock out lines | |||||
| val rejectedStockOutLines = stockOutLIneRepository | |||||
| .findAllByPickOrderLineIdAndDeletedFalse(pickOrderLine.id!!) | |||||
| .filter { it.status == "rejected" } | |||||
| println("Rejected stock out lines: ${rejectedStockOutLines.size}") | |||||
| // 修复:只创建一个 resuggest_issue(如果有 rejected lines) | |||||
| if (rejectedStockOutLines.isNotEmpty()) { | |||||
| println("Creating resuggest failure issue") | |||||
| createResuggestFailureIssue( | |||||
| pickOrder = pickOrderToResuggest, | |||||
| pickOrderLine = pickOrderLine, | |||||
| rejectedStockOutLine = rejectedStockOutLines.first() | |||||
| ) | |||||
| } | |||||
| println("Creating stock out line for suggestion") | |||||
| //createStockOutLineForSuggestion(suggestion, pickOrderToResuggest) | |||||
| println("Stock out line created") | |||||
| } | |||||
| // 为每个新 suggestion 创建 stock out line 或 issue | |||||
| savedSuggestions.forEach { suggestion -> | |||||
| if (suggestion.suggestedLotLine != null) { | |||||
| val isNewSuggestion = response.suggestedList.any { | |||||
| it.pickOrderLine?.id == suggestion.pickOrderLine?.id && | |||||
| it.suggestedLotLine?.id == suggestion.suggestedLotLine?.id && | |||||
| it.id == null // 新的 suggestion 还没有 id | |||||
| } | } | ||||
| } | |||||
| // 更新 holdQty | |||||
| response.holdQtyMap.forEach { (lotId, newHoldQty) -> | |||||
| if (lotId != null && newHoldQty != null && newHoldQty > BigDecimal.ZERO) { | |||||
| val lot = inventoryLotLineRepository.findById(lotId).orElse(null) | |||||
| lot?.let { | |||||
| val currentHoldQty = it.holdQty ?: BigDecimal.ZERO | |||||
| val existingHoldQty = existingHoldQtyMap[lotId] ?: BigDecimal.ZERO | |||||
| val additionalHoldQty = newHoldQty.minus(existingHoldQty) | |||||
| val finalHoldQty = currentHoldQty.plus(additionalHoldQty) | |||||
| it.holdQty = finalHoldQty | |||||
| inventoryLotLineRepository.save(it) | |||||
| existingHoldQtyMap[lotId] = newHoldQty | |||||
| println("Updated holdQty for lot $lotId: $currentHoldQty + $additionalHoldQty = $finalHoldQty") | |||||
| } | |||||
| val stockOutLine = createStockOutLineForSuggestion(suggestion, pickOrderToResuggest) | |||||
| if (stockOutLine != null) { | |||||
| println(" Created stock out line ${stockOutLine.id} for suggestion ${suggestion.id}") | |||||
| } | } | ||||
| } | |||||
| } else { | |||||
| // 如果完全没有生成任何 suggestions | |||||
| println("No suggestions generated at all for pick order: ${pickOrderToResuggest.code}") | |||||
| problematicPickOrderLines.forEach { pickOrderLine -> | |||||
| val rejectedStockOutLines = stockOutLIneRepository | |||||
| .findAllByPickOrderLineIdAndDeletedFalse(pickOrderLine.id!!) | |||||
| .filter { it.status == "rejected" } | |||||
| } else { | |||||
| // 如果 lot 是 null,表示没有可用的 lot,创建 resuggest_issue | |||||
| println("❌ No available lot for pick order line ${suggestion.pickOrderLine?.id}, creating resuggest_issue") | |||||
| if (rejectedStockOutLines.isNotEmpty()) { | |||||
| rejectedStockOutLines.forEach { rejectedLine -> | |||||
| val pickOrderLine = suggestion.pickOrderLine | |||||
| if (pickOrderLine != null) { | |||||
| // OPTIMIZATION 3: 从预加载的 Map 中获取 | |||||
| val rejectedStockOutLines = stockOutLinesByPickOrderLineId[pickOrderLine.id] | |||||
| ?.filter { it.status?.equals("rejected", ignoreCase = true) == true } ?: emptyList() | |||||
| println("Rejected stock out lines: ${rejectedStockOutLines.size}") | |||||
| // 修复:只创建一个 resuggest_issue(如果有 rejected lines) | |||||
| if (rejectedStockOutLines.isNotEmpty()) { | |||||
| println("Creating resuggest failure issue") | |||||
| // 注意:rejectedStockOutLines 是 List<StockOutLine>,需要转换为合适的类型 | |||||
| val firstRejectedLine = rejectedStockOutLines.first() | |||||
| createResuggestFailureIssue( | createResuggestFailureIssue( | ||||
| pickOrder = pickOrderToResuggest, | pickOrder = pickOrderToResuggest, | ||||
| pickOrderLine = pickOrderLine, | pickOrderLine = pickOrderLine, | ||||
| rejectedStockOutLine = rejectedLine | |||||
| rejectedStockOutLine = firstRejectedLine | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| } | |||||
| // 更新 holdQty | |||||
| response.holdQtyMap.forEach { (lotId, newHoldQty) -> | |||||
| if (lotId != null && newHoldQty != null && newHoldQty > BigDecimal.ZERO) { | |||||
| val lot = inventoryLotLineRepository.findById(lotId).orElse(null) | |||||
| lot?.let { | |||||
| val currentHoldQty = it.holdQty ?: BigDecimal.ZERO | |||||
| val existingHoldQty = existingHoldQtyMap[lotId] ?: BigDecimal.ZERO | |||||
| val additionalHoldQty = newHoldQty.minus(existingHoldQty) | |||||
| val finalHoldQty = currentHoldQty.plus(additionalHoldQty) | |||||
| it.holdQty = finalHoldQty | |||||
| inventoryLotLineRepository.save(it) | |||||
| existingHoldQtyMap[lotId] = newHoldQty | |||||
| println("Updated holdQty for lot $lotId: $currentHoldQty + $additionalHoldQty = $finalHoldQty") | |||||
| } | |||||
| } | |||||
| } | |||||
| } else { | |||||
| // 如果完全没有生成任何 suggestions | |||||
| println("No suggestions generated at all for pick order: ${pickOrderToResuggest.code}") | |||||
| problematicPickOrderLines.forEach { pickOrderLine -> | |||||
| // OPTIMIZATION 3: 从预加载的 Map 中获取 | |||||
| val rejectedStockOutLines = stockOutLinesByPickOrderLineId[pickOrderLine.id] | |||||
| ?.filter { it.status?.equals("rejected", ignoreCase = true) == true } ?: emptyList() | |||||
| if (rejectedStockOutLines.isNotEmpty()) { | |||||
| rejectedStockOutLines.forEach { rejectedLine -> | |||||
| createResuggestFailureIssue( | |||||
| pickOrder = pickOrderToResuggest, | |||||
| pickOrderLine = pickOrderLine, | |||||
| rejectedStockOutLine = rejectedLine | |||||
| ) | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| // FIX: Update inventory table for each pick order | |||||
| allPickOrdersToResuggest.forEach { pickOrderToUpdate -> | |||||
| println("=== Updating inventory table for pick order: ${pickOrderToUpdate.code} ===") | |||||
| updateInventoryTableAfterResuggest(pickOrderToUpdate) | |||||
| } | |||||
| println("=== RESUGGEST DEBUG END ===") | |||||
| 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 | |||||
| ) | |||||
| } | } | ||||
| // FIX: Update inventory table for each pick order | |||||
| allPickOrdersToResuggest.forEach { pickOrderToUpdate -> | |||||
| println("=== Updating inventory table for pick order: ${pickOrderToUpdate.code} ===") | |||||
| updateInventoryTableAfterResuggest(pickOrderToUpdate) | |||||
| } | |||||
| println("=== RESUGGEST DEBUG END ===") | |||||
| 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 | |||||
| ) | |||||
| } | } | ||||
| } | |||||
| private fun createResuggestFailureIssue( | |||||
| pickOrder: PickOrder, | |||||
| pickOrderLine: PickOrderLine, | |||||
| rejectedStockOutLine: StockOutLineInfo // 使用 StockOutLineInfo | |||||
| ) { | |||||
| try { | |||||
| val item = pickOrderLine.item | |||||
| // 从 StockOutLineInfo 获取 inventoryLotLineId | |||||
| val inventoryLotLineId = rejectedStockOutLine.inventoryLotLineId | |||||
| val inventoryLotLine = if (inventoryLotLineId != null) { | |||||
| inventoryLotLineRepository.findById(inventoryLotLineId).orElse(null) | |||||
| } else { | |||||
| null | |||||
| } | |||||
| val issue = PickExecutionIssue( | |||||
| id = null, | |||||
| pickOrderId = pickOrder.id!!, | |||||
| pickOrderCode = pickOrder.code!!, | |||||
| pickOrderCreateDate = pickOrder.created?.toLocalDate(), | |||||
| pickExecutionDate = LocalDate.now(), | |||||
| pickOrderLineId = pickOrderLine.id!!, | |||||
| issueNo = generateIssueNo(), | |||||
| joPickOrderId=pickOrder.jobOrder?.id, | |||||
| doPickOrderId=pickOrder.deliveryOrder?.id, | |||||
| issueCategory = IssueCategory.resuggest_issue, | |||||
| itemId = item?.id!!, | |||||
| itemCode = item.code, | |||||
| itemDescription = item.name, | |||||
| lotId = inventoryLotLine?.id, | |||||
| lotNo = inventoryLotLine?.inventoryLot?.lotNo, | |||||
| storeLocation = inventoryLotLine?.warehouse?.code, | |||||
| requiredQty = pickOrderLine.qty, | |||||
| actualPickQty = rejectedStockOutLine.qty ?: BigDecimal.ZERO, // 直接使用,不需要 toBigDecimal() | |||||
| missQty = (pickOrderLine.qty ?: BigDecimal.ZERO).minus(rejectedStockOutLine.qty ?: BigDecimal.ZERO), // 直接使用 | |||||
| badItemQty = BigDecimal.ZERO, | |||||
| issueRemark = "Resuggest failed: No alternative lots available for rejected lot ${inventoryLotLine?.inventoryLot?.lotNo}", | |||||
| 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(" Created resuggest_issue: ${issue.issueNo} for pick order ${pickOrder.code}") | |||||
| } catch (e: Exception) { | |||||
| println("❌ Error creating resuggest_issue: ${e.message}") | |||||
| e.printStackTrace() | |||||
| } | |||||
| private fun createResuggestFailureIssue( | |||||
| pickOrder: PickOrder, | |||||
| pickOrderLine: PickOrderLine, | |||||
| rejectedStockOutLine: StockOutLine // 改为 StockOutLine | |||||
| ) { | |||||
| try { | |||||
| val item = pickOrderLine.item | |||||
| // 从 StockOutLine 获取 inventoryLotLineId | |||||
| val inventoryLotLine = rejectedStockOutLine.inventoryLotLine | |||||
| val inventoryLotLineId = inventoryLotLine?.id | |||||
| val issue = PickExecutionIssue( | |||||
| id = null, | |||||
| pickOrderId = pickOrder.id!!, | |||||
| pickOrderCode = pickOrder.code!!, | |||||
| pickOrderCreateDate = pickOrder.created?.toLocalDate(), | |||||
| pickExecutionDate = LocalDate.now(), | |||||
| pickOrderLineId = pickOrderLine.id!!, | |||||
| issueNo = generateIssueNo(), | |||||
| joPickOrderId = pickOrder.jobOrder?.id, | |||||
| doPickOrderId = pickOrder.deliveryOrder?.id, | |||||
| issueCategory = IssueCategory.resuggest_issue, | |||||
| itemId = item?.id!!, | |||||
| itemCode = item.code, | |||||
| itemDescription = item.name, | |||||
| lotId = inventoryLotLineId, | |||||
| lotNo = inventoryLotLine?.inventoryLot?.lotNo, | |||||
| storeLocation = inventoryLotLine?.warehouse?.code, | |||||
| requiredQty = pickOrderLine.qty, | |||||
| actualPickQty = rejectedStockOutLine.qty?.let { BigDecimal.valueOf(it) } ?: BigDecimal.ZERO, | |||||
| missQty = (pickOrderLine.qty ?: BigDecimal.ZERO).minus( | |||||
| rejectedStockOutLine.qty?.let { BigDecimal.valueOf(it) } ?: BigDecimal.ZERO | |||||
| ), | |||||
| badItemQty = BigDecimal.ZERO, | |||||
| issueRemark = "Resuggest failed: No alternative lots available for rejected lot ${inventoryLotLine?.inventoryLot?.lotNo}", | |||||
| pickerName = null, | |||||
| handleStatus = HandleStatus.pending, | |||||
| handleDate = null, | |||||
| handledBy = null, | |||||
| created = LocalDateTime.now(), | |||||
| createdBy = "system", | |||||
| version = 0, | |||||
| modified = LocalDateTime.now(), | |||||
| modifiedBy = "system", | |||||
| deleted = false | |||||
| ) | |||||
| val savedIssue = pickExecutionIssueRepository.save(issue) | |||||
| println(" Created resuggest_issue: ${savedIssue.issueNo} for pick order ${pickOrder.code}") | |||||
| } catch (e: Exception) { | |||||
| println("❌ Error creating resuggest_issue: ${e.message}") | |||||
| e.printStackTrace() | |||||
| } | } | ||||
| } | |||||
| private fun generateIssueNo(): String { | private fun generateIssueNo(): String { | ||||
| val now = LocalDateTime.now() | val now = LocalDateTime.now() | ||||
| val yearMonth = now.format(DateTimeFormatter.ofPattern("yyMM")) | val yearMonth = now.format(DateTimeFormatter.ofPattern("yyMM")) | ||||
| @@ -0,0 +1,5 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset author:add_time_fields_to_productprocessline | |||||
| ALTER TABLE `inventory_lot_line` | |||||
| ADD COLUMN `issueQty` DECIMAL(18,2) NULL AFTER `holdQty`; | |||||