diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx deleted file mode 100644 index b28b04f..0000000 --- a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/models/QcCategoryWithQcItemCount.kt b/src/main/java/com/ffii/fpsms/modules/master/web/models/QcCategoryWithQcItemCount.kt index 8ac2b77..9718c47 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/models/QcCategoryWithQcItemCount.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/models/QcCategoryWithQcItemCount.kt @@ -9,3 +9,4 @@ data class QcCategoryWithQcItemCount( ) + diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt index acf8c17..5b181c0 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt @@ -67,6 +67,10 @@ val doPickOrderId: Long? = null, @Column(name = "bad_item_qty", precision = 10, scale = 2) val badItemQty: BigDecimal = BigDecimal.ZERO, + @Column(name = "book_qty", precision = 10, scale = 2) + val bookQty: BigDecimal = BigDecimal.ZERO, + @Column(name = "issue_qty", precision = 10, scale = 2) + val issueQty: BigDecimal = BigDecimal.ZERO, @Column(name = "issue_remark", columnDefinition = "TEXT") val issueRemark: String? = null, @@ -120,6 +124,8 @@ val doPickOrderId: Long? = null, storeLocation = null, requiredQty = null, actualPickQty = null, + bookQty = BigDecimal.ZERO, + issueQty = BigDecimal.ZERO, missQty = BigDecimal.ZERO, badItemQty = BigDecimal.ZERO, issueRemark = null, 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 d6f13a3..7821fa8 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,7 @@ 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.web.model.PickExecutionIssueRequest + import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus import org.springframework.stereotype.Service @@ -38,7 +38,10 @@ import com.ffii.fpsms.modules.stock.entity.StockOut import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine import com.ffii.fpsms.modules.master.entity.ItemsRepository import com.ffii.fpsms.modules.common.CodeGenerator -import com.ffii.fpsms.modules.pickOrder.web.models.SubmitIssueRequest + +import com.ffii.fpsms.modules.stock.entity.StockLedger +import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository + @Service open class PickExecutionIssueService( private val pickExecutionIssueRepository: PickExecutionIssueRepository, @@ -53,151 +56,223 @@ open class PickExecutionIssueService( private val doPickOrderService: DoPickOrderService, private val joPickOrderRepository: JoPickOrderRepository, private val joPickOrderRecordRepository: JoPickOrderRecordRepository, - private val itemsRepository: ItemsRepository + private val itemsRepository: ItemsRepository, + private val stockLedgerRepository: StockLedgerRepository ) { @Transactional(rollbackFor = [Exception::class]) - open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse { - try { - // 1. 检查是否已经存在相同的 pick execution issue 记录 - val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( - request.pickOrderLineId, - request.lotId ?: 0L +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 ) + } + + // 5. 计算 issueQty(实际的问题数量) + val issueQty = when { + // 情况1: 已拣完但有坏品 + actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { + badItemQty // issueQty = badItemQty + } - 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 - ) + // 情况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 } - val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null) - // 2. 创建 pick execution issue 记录 - val pickExecutionIssue = PickExecutionIssue( - id = null, // 添加 id - 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, - issueRemark = request.issueRemark, - pickerName = request.pickerName, // 确保从 request 中获取 - handleStatus = HandleStatus.pending, // 添加 - handleDate = null, // 添加 - handledBy = request.handledBy, - created = LocalDateTime.now(), - createdBy = "system", - version = 0, // 添加 - modified = LocalDateTime.now(), - modifiedBy = "system", - deleted = false // 添加 - ) + + 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) + val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue) - // 3. 获取相关数据 - val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO - val missQty = request.missQty ?: BigDecimal.ZERO - val badItemQty = request.badItemQty ?: BigDecimal.ZERO - val lotId = request.lotId - val itemId = request.itemId + // 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: ${actualPickQty}") - println("Miss Qty: ${missQty}") - println("Bad Item Qty: ${badItemQty}") - println("Lot ID: ${lotId}") - println("Item ID: ${itemId}") - println("================================================") + 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("================================================") - // 4. 新的统一处理逻辑 - when { - // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) - actualPickQty == BigDecimal.ZERO && missQty > BigDecimal.ZERO && badItemQty == BigDecimal.ZERO -> { - handleMissItemOnly(request, missQty) - } - - // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) - badItemQty > BigDecimal.ZERO && missQty == BigDecimal.ZERO -> { - handleBadItemOnly(request, badItemQty) - } - - // 情况3: 既有 miss item 又有 bad item - missQty > BigDecimal.ZERO && badItemQty > BigDecimal.ZERO -> { - handleBothMissAndBadItem(request, missQty, badItemQty) - } - - // 修复:情况4: 有 miss item 的情况(无论 actualPickQty 是多少) - missQty > BigDecimal.ZERO -> { - handleMissItemWithPartialPick(request, actualPickQty, missQty) - } - - // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) - actualPickQty > BigDecimal.ZERO -> { - handleNormalPick(request, actualPickQty) - } - - else -> { - println("Unknown case: actualPickQty=${actualPickQty}, missQty=${missQty}, badItemQty=${badItemQty}") - } + // 8. 新的统一处理逻辑 + when { + // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) + actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> { + handleMissItemOnly(request, missQtyForProcessing) } - - 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}") - } + // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) + badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { + handleBadItemOnly(request, badItemQtyForProcessing) } - - - 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 - ) + + // 情况3: 既有 miss item 又有 bad item + missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { + 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}") + } + } - } 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 - ) + 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 + ) } +} private fun generateIssueNo(): String { val now = LocalDateTime.now() val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM")) @@ -383,7 +458,7 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac inventoryLotLine.modified = LocalDateTime.now() inventoryLotLine.modifiedBy = "system" - inventoryLotLineRepository.save(inventoryLotLine) + inventoryLotLineRepository.saveAndFlush(inventoryLotLine) // 使用 saveAndFlush println("Miss item with partial pick: Updated lot ${lotId}") println(" - Added to outQty: ${actualPickQty} (${currentOutQty} -> ${newOutQty})") @@ -395,7 +470,33 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac updateInventoryUnavailableQty(itemId, missQty) // 修复5:更新 stock_out_line 状态为 rejected(因为还有 miss item) - updateStockOutLineStatus(request, "rejected") + // 修复:同时创建 stock_ledger 记录 + val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( + request.pickOrderLineId, + request.lotId ?: 0L + ) + + stockOutLines.forEach { stockOutLine -> + stockOutLine.status = "rejected" + + // 更新 qty 为 actualPickQty + if (request.actualPickQty != null) { + stockOutLine.qty = request.actualPickQty.toDouble() + println("Updated stock out line ${stockOutLine.id} qty to: ${request.actualPickQty}") + } + + stockOutLine.modified = LocalDateTime.now() + stockOutLine.modifiedBy = "system" + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + + println("Updated stock out line ${stockOutLine.id} status to: rejected") + + // 修复:为实际拣货的部分创建 stock_ledger 记录(即使状态是 rejected) + // 因为这部分确实从库存中出库了 + if (request.actualPickQty != null && request.actualPickQty > BigDecimal.ZERO) { + createStockLedgerForStockOut(savedStockOutLine, "Nor") // 实际拣货的部分 + } + } // 重新建议拣货批次(针对 miss 的数量) try { @@ -417,12 +518,12 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) if (inventoryLotLine != null) { - // 修复:Miss item 意味着剩余的所有物品都找不到 + val currentInQty = inventoryLotLine.inQty ?: BigDecimal.ZERO val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO val remainingQty = currentInQty.minus(currentOutQty) - // 标记批次为 unavailable + inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE inventoryLotLine.modified = LocalDateTime.now() inventoryLotLine.modifiedBy = "system" @@ -434,16 +535,15 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD println(" - Unavailable qty (should be remaining qty): ${remainingQty}") } - // 修复:更新 inventory 表的 unavailableQty - // 应该是剩余的数量,而不是 missQty + val currentInQty = inventoryLotLine?.inQty ?: BigDecimal.ZERO val currentOutQty = inventoryLotLine?.outQty ?: BigDecimal.ZERO val remainingQty = currentInQty.minus(currentOutQty) - // 修复:只增加剩余数量,不要重复计算 missQty + updateInventoryUnavailableQty(itemId, remainingQty) - // 修复:更新 stock_out_line 状态为 rejected + updateStockOutLineStatus(request, "rejected") // 重新建议拣货批次 @@ -555,11 +655,14 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty stockOutLine.qty = actualPickQtyDouble // 直接设置,不累积 stockOutLine.modified = LocalDateTime.now() stockOutLine.modifiedBy = "system" - stockOutLineRepository.save(stockOutLine) + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) // 使用 saveAndFlush println("Updated stock out line ${stockOutLine.id}: status=${newStatus}, qty=${actualPickQtyDouble}") + + // 修复:为正常拣货创建 stock_ledger 记录 + createStockLedgerForStockOut(savedStockOutLine, "Nor") } - + // 修复:更新 inventory_lot_line 的 outQty val lotId = request.lotId ?: return val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) @@ -572,7 +675,7 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty inventoryLotLine.outQty = newOutQty inventoryLotLine.modified = LocalDateTime.now() inventoryLotLine.modifiedBy = "system" - inventoryLotLineRepository.save(inventoryLotLine) + inventoryLotLineRepository.saveAndFlush(inventoryLotLine) // 使用 saveAndFlush 确保触发器执行 println("Updated inventory lot line ${lotId} outQty: ${currentOutQty} -> ${newOutQty}") } @@ -801,22 +904,68 @@ open fun createBatchReleaseIssue( } } -open fun getMissItemList(issueCategory: String = "lot_issue"): List { +open fun getMissItemList(issueCategory: String = "lot_issue"): List { val category = try { IssueCategory.valueOf(issueCategory) } catch (e: Exception) { IssueCategory.lot_issue } - return pickExecutionIssueRepository.findMissItemList(category).filter { it.handleStatus != HandleStatus.completed } + return pickExecutionIssueRepository.findMissItemList(category) + .filter { it.handleStatus != HandleStatus.completed } + .map { issue -> + StockIssueResponse( + id = issue.id ?: 0L, + itemId = issue.itemId, + itemCode = issue.itemCode, + itemDescription = issue.itemDescription, + lotId = issue.lotId, + lotNo = issue.lotNo, + storeLocation = issue.storeLocation, + requiredQty = issue.requiredQty, + actualPickQty = issue.actualPickQty, + missQty = issue.missQty, + badItemQty = issue.badItemQty, + bookQty = issue.bookQty ?: BigDecimal.ZERO, + issueQty = issue.issueQty ?: BigDecimal.ZERO, // 返回计算的问题数量 + issueRemark = issue.issueRemark, + pickerName = issue.pickerName, + handleStatus = issue.handleStatus.name, + handleDate = issue.handleDate, + handledBy = issue.handledBy + ) + } } -open fun getBadItemList(issueCategory: String = "lot_issue"): List { +open fun getBadItemList(issueCategory: String = "lot_issue"): List { val category = try { IssueCategory.valueOf(issueCategory) } catch (e: Exception) { IssueCategory.lot_issue } - return pickExecutionIssueRepository.findBadItemListByCategory(category).filter { it.handleStatus != HandleStatus.completed } + return pickExecutionIssueRepository.findBadItemListByCategory(category) + .filter { it.handleStatus != HandleStatus.completed } + .map { issue -> + StockIssueResponse( + id = issue.id ?: 0L, + itemId = issue.itemId, + itemCode = issue.itemCode, + itemDescription = issue.itemDescription, + lotId = issue.lotId, + lotNo = issue.lotNo, + storeLocation = issue.storeLocation, + requiredQty = issue.requiredQty, + actualPickQty = issue.actualPickQty, + missQty = issue.missQty, + badItemQty = issue.badItemQty, + bookQty = issue.bookQty ?: BigDecimal.ZERO, + issueQty = issue.issueQty ?: BigDecimal.ZERO, // 返回计算的问题数量 + issueRemark = issue.issueRemark, + pickerName = issue.pickerName, + handleStatus = issue.handleStatus.name, + handleDate = issue.handleDate, + handledBy = issue.handledBy + ) + } } open fun getBadItemOnlyList(): List { return pickExecutionIssueRepository.findBadItemOnlyList(IssueCategory.lot_issue) @@ -856,13 +1005,15 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse { errorPosition = null ) - if (issue.missQty <= BigDecimal.ZERO || issue.deleted) { + // 修改:使用 issueQty 而不是 missQty + val issueQty = issue.issueQty ?: BigDecimal.ZERO + if (issueQty <= BigDecimal.ZERO || issue.deleted) { return MessageResponse( id = null, name = "Error", code = "INVALID", type = "stock_issue", - message = "Invalid issue or no miss quantity", + message = "Invalid issue or no issue quantity", errorPosition = null ) } @@ -888,22 +1039,26 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse { errorPosition = null ) + // 修改:使用 issueQty 创建 stock_out_line val stockOutLine = StockOutLine().apply { this.stockOut = stockOut this.inventoryLotLine = lotLine this.item = item - this.qty = issue.missQty.toDouble() - this.status = StockOutLineStatus.PENDING.status + this.qty = issueQty.toDouble() // 使用 issueQty 而不是 missQty + this.status = StockOutLineStatus.COMPLETE.status this.pickOrderLine = pickOrderLine + this.type = "Miss" } - stockOutLineRepository.save(stockOutLine) + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + // 修改:使用 issueQty 更新 inventory_lot_line if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issue.missQty) + updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty } markIssueHandled(issue, handler) - + // Create stock ledger entry + createStockLedgerForStockOut(savedStockOutLine) return MessageResponse( id = stockOut.id, name = "Success", @@ -923,7 +1078,6 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse { ) } } - @Transactional(rollbackFor = [Exception::class]) open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { try { @@ -937,13 +1091,15 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { errorPosition = null ) - if (issue.badItemQty <= BigDecimal.ZERO || issue.deleted) { + // 修改:使用 issueQty 而不是 badItemQty + val issueQty = issue.issueQty ?: BigDecimal.ZERO + if (issueQty <= BigDecimal.ZERO || issue.deleted) { return MessageResponse( id = null, name = "Error", code = "INVALID", type = "stock_issue", - message = "Invalid issue or no bad item quantity", + message = "Invalid issue or no issue quantity", errorPosition = null ) } @@ -969,22 +1125,26 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { errorPosition = null ) + // 修改:使用 issueQty 创建 stock_out_line val stockOutLine = StockOutLine().apply { this.stockOut = stockOut this.inventoryLotLine = lotLine this.item = item - this.qty = issue.badItemQty.toDouble() - this.status = StockOutLineStatus.PENDING.status + this.qty = issueQty.toDouble() // 使用 issueQty 而不是 badItemQty + this.status = StockOutLineStatus.COMPLETE.status this.pickOrderLine = pickOrderLine + this.type = "Bad" } - stockOutLineRepository.save(stockOutLine) + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + // 修改:使用 issueQty 更新 inventory_lot_line if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issue.badItemQty) + updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty } markIssueHandled(issue, handler) - + // Create stock ledger entry + createStockLedgerForStockOut(savedStockOutLine) return MessageResponse( id = stockOut.id, name = "Success", @@ -1065,12 +1225,16 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse { this.inventoryLotLine = lotLine this.item = item this.qty = remainingQty.toDouble() - this.status = StockOutLineStatus.PENDING.status + this.status = StockOutLineStatus.COMPLETE.status + this.type = "Expiry" } - stockOutLineRepository.save(stockOutLine) + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty) + + updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty) + // Create stock ledger entry + createStockLedgerForStockOut(savedStockOutLine) return MessageResponse( id = stockOut.id, name = "Success", @@ -1091,12 +1255,12 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse { } } -// Fix batchSubmitMissItem method (around line 835): @Transactional(rollbackFor = [Exception::class]) open fun batchSubmitMissItem(request: BatchSubmitIssueRequest): MessageResponse { try { + // 修改:检查 issueQty 而不是 missQty val issues = pickExecutionIssueRepository.findAllById(request.issueIds) - .filter { it.missQty > BigDecimal.ZERO && !it.deleted } + .filter { (it.issueQty ?: BigDecimal.ZERO) > BigDecimal.ZERO && !it.deleted } if (issues.isEmpty()) { return MessageResponse( @@ -1110,44 +1274,40 @@ open fun batchSubmitMissItem(request: BatchSubmitIssueRequest): MessageResponse } val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L - - // Create single StockOut header for all submissions val stockOut = createIssueStockOutHeader("MISS_ITEM", "Batch miss item submission", handler) issues.forEach { issue -> + val issueQty = issue.issueQty ?: BigDecimal.ZERO - // Get pickOrderLine if available val pickOrderLine = issue.pickOrderLineId?.let { pickOrderLineRepository.findById(it).orElse(null) } - // Get inventoryLotLine val lotLine = issue.lotId?.let { inventoryLotLineRepository.findById(it).orElse(null) } - // Get item from issue (it has itemId) val item = itemsRepository.findById(issue.itemId).orElse(null) - - // Create StockOutLine + // 修改:使用 issueQty val stockOutLine = StockOutLine().apply { this.stockOut = stockOut this.inventoryLotLine = lotLine this.item = item - this.qty = issue.missQty.toDouble() - this.status = StockOutLineStatus.PENDING.status + this.qty = issueQty.toDouble() // 使用 issueQty + this.status = StockOutLineStatus.COMPLETE.status this.pickOrderLine = pickOrderLine + this.type = "Miss" } - stockOutLineRepository.save(stockOutLine) + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - // Update InventoryLotLine + // 修改:使用 issueQty if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issue.missQty) + updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty } - // Mark issue as handled markIssueHandled(issue, handler) + createStockLedgerForStockOut(savedStockOutLine) } return MessageResponse( @@ -1170,12 +1330,12 @@ open fun batchSubmitMissItem(request: BatchSubmitIssueRequest): MessageResponse } } -// Fix batchSubmitBadItem method (around line 890) - same pattern: @Transactional(rollbackFor = [Exception::class]) open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse { try { + // 修改:检查 issueQty 而不是 badItemQty val issues = pickExecutionIssueRepository.findAllById(request.issueIds) - .filter { it.badItemQty > BigDecimal.ZERO && !it.deleted } + .filter { (it.issueQty ?: BigDecimal.ZERO) > BigDecimal.ZERO && !it.deleted } if (issues.isEmpty()) { return MessageResponse( @@ -1189,44 +1349,40 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse { } val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L - - // Create single StockOut header for all submissions val stockOut = createIssueStockOutHeader("BAD_ITEM", "Batch bad item submission", handler) issues.forEach { issue -> + val issueQty = issue.issueQty ?: BigDecimal.ZERO - // Get pickOrderLine if available val pickOrderLine = issue.pickOrderLineId?.let { pickOrderLineRepository.findById(it).orElse(null) } - // Get inventoryLotLine val lotLine = issue.lotId?.let { inventoryLotLineRepository.findById(it).orElse(null) } - // Get item from issue val item = itemsRepository.findById(issue.itemId).orElse(null) - - // Create StockOutLine + // 修改:使用 issueQty val stockOutLine = StockOutLine().apply { this.stockOut = stockOut this.inventoryLotLine = lotLine this.item = item - this.qty = issue.badItemQty.toDouble() - this.status = StockOutLineStatus.PENDING.status + this.qty = issueQty.toDouble() // 使用 issueQty + this.status = StockOutLineStatus.COMPLETE.status this.pickOrderLine = pickOrderLine + this.type = "Bad" } - stockOutLineRepository.save(stockOutLine) + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - // Update InventoryLotLine + // 修改:使用 issueQty if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issue.badItemQty) + updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty } - // Mark issue as handled markIssueHandled(issue, handler) + createStockLedgerForStockOut(savedStockOutLine) } return MessageResponse( @@ -1248,7 +1404,6 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse { ) } } - // Fix batchSubmitExpiryItem method (around line 945): @Transactional(rollbackFor = [Exception::class]) open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageResponse { @@ -1289,12 +1444,17 @@ open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageRespon this.inventoryLotLine = lotLine this.item = item this.qty = remainingQty.toDouble() - this.status = StockOutLineStatus.PENDING.status + this.status = StockOutLineStatus.COMPLETE.status + this.type = "Expiry" } - stockOutLineRepository.save(stockOutLine) + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + + // Create stock ledger entry + // Update InventoryLotLine updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty) + createStockLedgerForStockOut(savedStockOutLine) } return MessageResponse( @@ -1360,7 +1520,111 @@ private fun updateLotLineAfterIssue(lotLineId: Long, qty: BigDecimal) { lotLine.modified = LocalDateTime.now() lotLine.modifiedBy = "system" - inventoryLotLineRepository.save(lotLine) + // 修复:使用 saveAndFlush 确保立即提交到数据库,触发触发器 + inventoryLotLineRepository.saveAndFlush(lotLine) + updateInventoryAfterLotLineChange(lotLine) + } +} +private fun updateInventoryAfterLotLineChange(lotLine: InventoryLotLine) { + try { + val item = lotLine.inventoryLot?.item ?: return + val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return + + // 使用 SQL 查询计算所有相关 lot lines 的总和 + val sql = """ + SELECT + SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) as totalOnHandQty, + SUM(COALESCE(ill.holdQty, 0)) as totalOnHoldQty, + SUM(CASE + WHEN ill.status = 'unavailable' + THEN COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0) + ELSE 0 + END) as totalUnavailableQty + FROM inventory_lot_line ill + INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId + WHERE il.itemId = :itemId + AND ill.deleted = 0 + """.trimIndent() + + val result = jdbcDao.queryForMap(sql, mapOf("itemId" to item.id!!)).orElse(null) + + if (result != null) { + val totalOnHandQty = (result["totalOnHandQty"] as? Number)?.let { + BigDecimal(it.toString()) + } ?: BigDecimal.ZERO + + val totalOnHoldQty = (result["totalOnHoldQty"] as? Number)?.let { + BigDecimal(it.toString()) + } ?: BigDecimal.ZERO + + val totalUnavailableQty = (result["totalUnavailableQty"] as? Number)?.let { + BigDecimal(it.toString()) + } ?: BigDecimal.ZERO + + // 更新 inventory + inventory.onHandQty = totalOnHandQty + inventory.onHoldQty = totalOnHoldQty + inventory.unavailableQty = totalUnavailableQty + inventory.status = if (totalOnHandQty.subtract(totalOnHoldQty).subtract(totalUnavailableQty) > BigDecimal.ZERO) { + "available" + } else { + "unavailable" + } + inventory.modified = LocalDateTime.now() + inventory.modifiedBy = "system" + + inventoryRepository.saveAndFlush(inventory) + + println("=== MANUALLY UPDATED INVENTORY ===") + println("Item ID: ${item.id}") + println("Total OnHandQty: ${totalOnHandQty}") + println("Total OnHoldQty: ${totalOnHoldQty}") + println("Total UnavailableQty: ${totalUnavailableQty}") + println("Status: ${inventory.status}") + println("==================================") + } + + } catch (e: Exception) { + println("Error updating inventory manually: ${e.message}") + e.printStackTrace() } } +private fun createStockLedgerForStockOut(stockOutLine: StockOutLine, ledgerType: String? = null) { + val item = stockOutLine.item ?: return + + val outQty = stockOutLine.qty?.toDouble() ?: 0.0 + + // 修复:重新查询 inventory 以获取触发器更新后的最新 onHandQty + val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return + + // 计算新的 balance = 当前 onHandQty - 本次出库数量 + val currentOnHandQty = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + val newBalance = currentOnHandQty - outQty + + // 确保 balance 不为负数 + val finalBalance = if (newBalance < 0) 0.0 else newBalance + + // 使用传入的 ledgerType,如果没有则使用 stockOutLine.type + val ledgerTypeToUse = ledgerType ?: stockOutLine.type ?: "Nor" + + val stockLedger = StockLedger().apply { + this.stockOutLine = stockOutLine + this.inventory = inventory + this.inQty = null + this.outQty = outQty + this.balance = finalBalance + this.type = ledgerTypeToUse // 使用指定的 type + this.itemId = item.id + this.itemCode = item.code + this.date = LocalDate.now() + } + + stockLedgerRepository.saveAndFlush(stockLedger) + + println("=== CREATED STOCK LEDGER ===") + println("Type: ${ledgerTypeToUse}") + println("OutQty: ${outQty}") + println("Balance: ${finalBalance}") + println("===========================") +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt index e235f0d..3b84803 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt @@ -5,9 +5,9 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssue import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum import com.ffii.fpsms.modules.pickOrder.service.PickExecutionIssueService // 修复导入路径 -import com.ffii.fpsms.modules.stock.web.model.PickExecutionIssueRequest -import org.springframework.web.bind.annotation.* import com.ffii.fpsms.modules.pickOrder.web.models.* +import org.springframework.web.bind.annotation.* + @RestController @RequestMapping("/pickExecution") @@ -45,18 +45,18 @@ class PickExecutionIssueController( return pickExecutionIssueService.getBadItemList(status) } @GetMapping("/issues/missItem") -fun getMissItemIssues( - @RequestParam(required = false, defaultValue = "lot_issue") issueCategory: String -): List { - return pickExecutionIssueService.getMissItemList(issueCategory) -} - -@GetMapping("/issues/badItem") -fun getBadItemIssues( - @RequestParam(required = false, defaultValue = "lot_issue") issueCategory: String -): List { - return pickExecutionIssueService.getBadItemList(issueCategory) -} + fun getMissItemIssues( + @RequestParam(required = false, defaultValue = "lot_issue") issueCategory: String + ): List { + return pickExecutionIssueService.getMissItemList(issueCategory) + } + + @GetMapping("/issues/badItem") + fun getBadItemIssues( + @RequestParam(required = false, defaultValue = "lot_issue") issueCategory: String + ): List { + return pickExecutionIssueService.getBadItemList(issueCategory) + } @GetMapping("/issues/expiryItem") fun getExpiryItemIssues(): List { 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 0223f6e..960ab93 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 @@ -1,5 +1,5 @@ // FPSMS-backend/src/main/java/com/ffii/fpsms/modules/stock/web/model/PickExecutionIssueRequest.kt -package com.ffii.fpsms.modules.stock.web.model +package com.ffii.fpsms.modules.pickOrder.web.models import java.math.BigDecimal import java.time.LocalDate @@ -30,33 +30,24 @@ data class PickExecutionIssueRequest( val handledBy: Long? = null ) -data class SubmitIssueRequest( - val issueId: Long, - val handler: Long? = null, -) - -data class BatchSubmitIssueRequest( - val issueIds: List, - val handler: Long? = null, -) -data class SubmitExpiryRequest( - val lotLineId: Long, - val handler: Long? = null, -) - -data class BatchSubmitExpiryRequest( - val lotLineIds: List, - val handler: Long? = null, -) -data class ExpiryItemResponse( - val id: Long, // InventoryLotLine ID +data class StockIssueResponse( + val id: Long, val itemId: Long, - val itemCode: String, + val itemCode: String?, val itemDescription: String?, - val lotId: Long, // InventoryLot ID + val lotId: Long?, val lotNo: String?, val storeLocation: String?, - val expiryDate: LocalDate?, - val remainingQty: BigDecimal, + val requiredQty: BigDecimal?, + val actualPickQty: BigDecimal?, + val missQty: BigDecimal, + val badItemQty: BigDecimal, + val bookQty: BigDecimal, + val issueQty: BigDecimal, + val issueRemark: String?, + val pickerName: String?, + val handleStatus: String, + val handleDate: LocalDate?, + val handledBy: Long? ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index ce6f2ec..e5f02de 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 @@ -844,7 +844,7 @@ println("Keeping all suggestions (including rejected ones for display)") ) } println("Creating stock out line for suggestion") - createStockOutLineForSuggestion(suggestion, pickOrderToResuggest) + //createStockOutLineForSuggestion(suggestion, pickOrderToResuggest) println("Stock out line created") } } diff --git a/src/main/resources/db/changelog/changes/20260118_01_Enson/01_alter_table.sql b/src/main/resources/db/changelog/changes/20260118_01_Enson/01_alter_table.sql new file mode 100644 index 0000000..cc3017e --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260118_01_Enson/01_alter_table.sql @@ -0,0 +1,6 @@ +--liquibase formatted sql +--changeset author:add_time_fields_to_productprocessline + +ALTER TABLE `pick_execution_issue` +ADD COLUMN `bookQty` INT(11) NULL AFTER `bad_item_qty`, +ADD COLUMN `issueQty` INT(11) NULL AFTER `bookQty`; diff --git a/src/main/resources/db/changelog/changes/20260118_01_Enson/02_alter_table.sql b/src/main/resources/db/changelog/changes/20260118_01_Enson/02_alter_table.sql new file mode 100644 index 0000000..23b1031 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260118_01_Enson/02_alter_table.sql @@ -0,0 +1,8 @@ +--liquibase formatted sql +--changeset author:add_time_fields_to_productprocessline + +ALTER TABLE `pick_execution_issue` +DROP COLUMN `bookQty`, +DROP COLUMN `issueQty`, +ADD COLUMN `book_qty` DECIMAL(10,2) NULL AFTER `bad_item_qty`, +ADD COLUMN `issue_qty` DECIMAL(10,2) NULL AFTER `book_qty`;