From dac9b90e7412d91a49c73a1e5986035333d5f879 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Fri, 23 Jan 2026 14:52:08 +0800 Subject: [PATCH] fix scan lot and scan not match lt and new issue handle --- .../service/PickExecutionIssueService.kt | 593 +++++++++----- .../pickOrder/service/PickOrderService.kt | 42 +- .../web/models/PickExecutionIssueRequest.kt | 3 +- .../modules/stock/entity/InventoryLotLine.kt | 3 + .../stock/entity/StockOutLIneRepository.kt | 22 + .../stock/service/SuggestedPickLotService.kt | 772 ++++++++++-------- .../20260122_01_Enson/01_alter_table.sql | 5 + 7 files changed, 865 insertions(+), 575 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20260122_01_Enson/01_alter_table.sql 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 7821fa8..32a0f09 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 @@ -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.InventoryLotLineRepository 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.entity.enum.InventoryLotLineStatus 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.stock.web.model.StockOutLineStatus 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.web.models.* import com.ffii.fpsms.modules.common.SecurityUtils @@ -52,6 +54,7 @@ open class PickExecutionIssueService( private val pickOrderRepository: PickOrderRepository, private val jdbcDao: JdbcDao, private val stockOutRepository: StockOutRepository, + private val stockOutLineService: StockOutLineService, private val pickOrderLineRepository: PickOrderLineRepository, private val doPickOrderService: DoPickOrderService, private val joPickOrderRepository: JoPickOrderRepository, @@ -62,217 +65,227 @@ open class PickExecutionIssueService( ) { @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 { val now = LocalDateTime.now() 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 newOutQty = currentOutQty.add(qty) lotLine.outQty = newOutQty - + 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) { @@ -1627,4 +1641,189 @@ private fun createStockLedgerForStockOut(stockOutLine: StockOutLine, ledgerType: println("Balance: ${finalBalance}") 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)") + } +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt index e05da2d..48e56ba 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt @@ -3601,8 +3601,8 @@ ORDER BY val enrichedResults = filteredResults return enrichedResults } - // ... existing code ... - open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map { + // 修改后的逻辑 +open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map { println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (Repository-based) ===") println("userId filter: $userId") @@ -3612,8 +3612,9 @@ ORDER BY 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 { it.deleted == false && it.assignTo?.id == userId && @@ -3624,26 +3625,26 @@ ORDER BY 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( "fgInfo" to null, "pickOrders" to emptyList() ) } - 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 - val doPickOrderLineRecords = pickOrderIds.flatMap { poId -> + val doPickOrderLineRecords = activePickOrderIds.flatMap { poId -> doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(poId) } val doPickOrderIds = doPickOrderLineRecords.mapNotNull { it.doPickOrderId }.distinct() 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( "fgInfo" to null, "pickOrders" to emptyList() @@ -3665,11 +3666,22 @@ ORDER BY val doPickOrderId = doPickOrder.id!! 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, "holdQty" to ill.holdQty, "lotStatus" to ill.status?.value, + "stockInLineId" to il?.stockInLine?.id, "lotAvailability" to when { isExpired -> "expired" stockOutLine?.status == "rejected" -> "rejected" @@ -3866,6 +3879,7 @@ ORDER BY "outQty" to ill.outQty, "holdQty" to ill.holdQty, "lotStatus" to ill.status?.value, + "stockInLineId" to il?.stockInLine?.id, "lotAvailability" to when { isExpired -> "expired" sol.status == "rejected" -> "rejected" diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt index 960ab93..39eff36 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt @@ -27,7 +27,8 @@ data class PickExecutionIssueRequest( val badItemQty: BigDecimal = BigDecimal.ZERO, val issueRemark: String? = null, val pickerName: String? = null, - val handledBy: Long? = null + val handledBy: Long? = null, + val badReason: String? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLine.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLine.kt index 123f6ce..6db837d 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLine.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLine.kt @@ -45,6 +45,9 @@ open class InventoryLotLine : BaseEntity() { @Column(name = "holdQty") open var holdQty: BigDecimal? = null + @Column(name = "issueQty") + open var issueQty: BigDecimal? = null + @Convert(converter = InventoryLotLineStringConverter::class) @Column(name = "status") open var status: InventoryLotLineStatus? = null 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 507a16c..ac69d44 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 @@ -47,4 +47,26 @@ fun searchStockOutLines( @Query("SELECT sol FROM StockOutLine sol WHERE sol.item.id IN :itemIds AND sol.deleted = false") fun findAllByItemIdInAndDeletedFalse(itemIds: List): List + + +@Query(""" + SELECT sol FROM StockOutLine sol + WHERE sol.pickOrderLine.id IN :pickOrderLineIds + AND sol.deleted = false +""") +fun findAllByPickOrderLineIdInAndDeletedFalse( + @Param("pickOrderLineIds") pickOrderLineIds: List +): List + +// 添加批量查询方法:按 (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, + @Param("inventoryLotLineIds") inventoryLotLineIds: List +): List } 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 276a6fb..ae13524 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 @@ -523,249 +523,301 @@ open class SuggestedPickLotService( 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() - - 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() + + 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(实体类) + 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(实体类) + 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() + // 只删除有 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() + + // 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 - .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:更新现有的或创建新的 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 { - // 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 { - // 没有现有的 suggestion,创建新的 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() val updatedSuggestions = suggestionsToSave.filter { it.id != null } // 有 id 的是更新的 @@ -789,175 +841,169 @@ println("Keeping all suggestions (including rejected ones for display)") val savedSuggestions = allSavedSuggestions 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,需要转换为合适的类型 + val firstRejectedLine = rejectedStockOutLines.first() createResuggestFailureIssue( pickOrder = pickOrderToResuggest, 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 { val now = LocalDateTime.now() val yearMonth = now.format(DateTimeFormatter.ofPattern("yyMM")) diff --git a/src/main/resources/db/changelog/changes/20260122_01_Enson/01_alter_table.sql b/src/main/resources/db/changelog/changes/20260122_01_Enson/01_alter_table.sql new file mode 100644 index 0000000..42a8aeb --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260122_01_Enson/01_alter_table.sql @@ -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`;