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 575affb..2eb5b77 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 @@ -92,10 +92,18 @@ open class PickExecutionIssueService( println(" issueCategory: ${request.issueCategory}") println("========================================") - // 1. 检查是否已经存在相同的 pick execution issue 记录 + // 1. 解析 lot: + // request.lotId 在前端目前传的是 inventory_lot_line.id(用于 SOL 关联/计算 bookQty 等) + // 但 pick_execution_issue.lot_id 在 DB 上外键指向 inventory_lot.id + val inventoryLotLine = request.lotId?.let { + inventoryLotLineRepository.findById(it).orElse(null) + } + val inventoryLotIdForIssue = inventoryLotLine?.inventoryLot?.id + + // 2. 检查是否已经存在相同的 pick execution issue 记录(以 inventory_lot.id 去重) val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( - request.pickOrderLineId, - request.lotId ?: 0L + request.pickOrderLineId, + inventoryLotIdForIssue ?: 0L ) println("Checking for existing issues...") @@ -119,12 +127,8 @@ open class PickExecutionIssueService( val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null) println("Pick order: id=${pickOrder?.id}, code=${pickOrder?.code}, type=${pickOrder?.type?.value}") - // 2. 获取 inventory_lot_line 并计算账面数量 (bookQty) - val inventoryLotLine = request.lotId?.let { - inventoryLotLineRepository.findById(it).orElse(null) - } - - println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}") + // 3. 计算账面数量 (bookQty)(用 inventory_lot_line 快照) + println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}, inventoryLotId=${inventoryLotIdForIssue}") // 计算账面数量(创建 issue 时的快照) val bookQty = if (inventoryLotLine != null) { @@ -138,19 +142,19 @@ open class PickExecutionIssueService( BigDecimal.ZERO } - // 3. 获取数量值 + // 4. 获取数量值 val requiredQty = request.requiredQty ?: BigDecimal.ZERO val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO val missQty = request.missQty ?: BigDecimal.ZERO val badItemQty = request.badItemQty ?: BigDecimal.ZERO val badReason = request.badReason ?: "quantity_problem" - val stockOutLines = stockOutLineRepository + val relatedStockOutLines = stockOutLineRepository .findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( request.pickOrderLineId, request.lotId ?: 0L ) - val currentStatus = stockOutLines.firstOrNull()?.status ?: "" + val currentStatus = relatedStockOutLines.firstOrNull()?.status ?: "" if (currentStatus.equals("pending", ignoreCase = true) && actualPickQty > BigDecimal.ZERO @@ -187,7 +191,7 @@ open class PickExecutionIssueService( println(" Bad Reason: $badReason") println(" Book Qty: $bookQty") - // 4. 计算 issueQty(实际的问题数量) + // 5. 计算 issueQty(实际的问题数量) val issueQty = when { // Bad item 或 bad package:一律用用户输入的 bad 数量,不用 bookQty - actualPickQty badItemQty > BigDecimal.ZERO -> { @@ -215,9 +219,10 @@ open class PickExecutionIssueService( println("================================================") println("=== Processing Logic Selection ===") - // 5. 创建 pick execution issue 记录 + // 6. 创建 pick execution issue 记录 val issueNo = generateIssueNo() println("Generated issue number: $issueNo") + val lotNoForIssue = request.lotNo ?: inventoryLotLine?.inventoryLot?.lotNo val pickExecutionIssue = PickExecutionIssue( id = null, @@ -235,8 +240,8 @@ open class PickExecutionIssueService( itemId = request.itemId, itemCode = request.itemCode, itemDescription = request.itemDescription, - lotId = request.lotId, - lotNo = request.lotNo, + lotId = inventoryLotIdForIssue, + lotNo = lotNoForIssue, storeLocation = request.storeLocation, requiredQty = request.requiredQty, actualPickQty = request.actualPickQty, @@ -265,7 +270,7 @@ open class PickExecutionIssueService( println(" Handle Status: ${savedIssue.handleStatus}") println(" Issue Qty: ${savedIssue.issueQty}") - // 6. NEW: Update inventory_lot_line.issueQty + // 7. NEW: Update inventory_lot_line.issueQty(仍然用 lotLineId) if (request.lotId != null && inventoryLotLine != null) { println("Updating inventory_lot_line.issueQty...") // ✅ 修改:如果只有 missQty,不更新 issueQty @@ -305,100 +310,19 @@ open class PickExecutionIssueService( } } - // 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("=== Processing Logic Selection ===") - 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 -> { - println("→ Handling: Miss Item Only") - handleMissItemOnly(request, missQtyForProcessing) - } - - // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) - badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { - println("→ Handling: Bad Item Only") - // NEW: Check bad reason - if (request.badReason == "package_problem") { - println(" Bad reason is 'package_problem' - calling handleBadItemPackageProblem") - handleBadItemPackageProblem(request, badItemQtyForProcessing) - } else { - println(" Bad reason is 'quantity_problem' - calling handleBadItemOnly") - // quantity_problem or default: handle as normal bad item - handleBadItemOnly(request, badItemQtyForProcessing) - } - } - - // 情况3: 既有 miss item 又有 bad item - missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { - println("→ Handling: Both Miss and Bad Item") - // NEW: Check bad reason - if (request.badReason == "package_problem") { - println(" Bad reason is 'package_problem' - calling handleBothMissAndBadItemPackageProblem") - handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing) - } else { - println(" Bad reason is 'quantity_problem' - calling handleBothMissAndBadItem") - handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing) - } - } - - // 情况4: 有 miss item 的情况(无论 actualPickQty 是多少) - missQtyForProcessing > BigDecimal.ZERO -> { - println("→ Handling: Miss Item With Partial Pick") - handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) - } - actualPickQtyForProcessing == BigDecimal.ZERO && - missQtyForProcessing == BigDecimal.ZERO && - badItemQtyForProcessing == BigDecimal.ZERO -> { - println("→ Handling: All zero, mark stock out line as completed") - handleAllZeroMarkCompleted(request) - } - // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) - actualPickQtyForProcessing > BigDecimal.ZERO -> { - println("→ Handling: Normal Pick") - 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.isNullOrBlank()) { - // 优先走原来的 consoCode 逻辑(兼容已有 DO 流程) - println("🔍 Checking if pick order $consoCode should be completed after lot rejection...") - try { - checkAndCompletePickOrder(consoCode) - } catch (e: Exception) { - println("⚠️ Error checking pick order completion by consoCode: ${e.message}") - } - } else if (pickOrderForCompletion != null) { - // 🔁 没有 consoCode 的情况:改用 pickOrderId 去检查是否可以完结 - println("🔍 Checking if pick order ${pickOrderForCompletion.code} (ID=${pickOrderForCompletion.id}) " + - "should be completed after lot rejection (no consoCode)...") - try { - checkAndCompletePickOrderByPickOrderId(pickOrderForCompletion.id!!) - } catch (e: Exception) { - println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}") - } + // 7. 按规则:issue form 只记录问题 +(可选)把 SOL 标记为 checked + val stockOutLines = stockOutLineRepository + .findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( + request.pickOrderLineId, + request.lotId ?: 0L + ) + stockOutLines.forEach { sol -> + sol.status = "checked" + sol.modified = LocalDateTime.now() + sol.modifiedBy = "system" + stockOutLineRepository.save(sol) } + stockOutLineRepository.flush() println("=== recordPickExecutionIssue: SUCCESS ===") println("Issue ID: ${savedIssue.id}, Issue No: ${savedIssue.issueNo}") @@ -433,11 +357,12 @@ open class PickExecutionIssueService( ) stockOutLines.forEach { sol -> - sol.status = "completed" + // issue form 不完结,只标记 checked,让 submit/batch submit 决定 completed(允许 0) + sol.status = "checked" sol.modified = LocalDateTime.now() sol.modifiedBy = "system" stockOutLineRepository.save(sol) - println("All-zero case: mark stock out line ${sol.id} as completed (qty kept as ${sol.qty})") + println("All-zero case: mark stock out line ${sol.id} as checked (qty kept as ${sol.qty})") } stockOutLineRepository.flush() } @@ -773,34 +698,18 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac // ✅ 修改:不更新 unavailableQty(因为不 reject lot) - // ✅ 修改:不 reject stock_out_line,根据 actualPickQty 设置状态 + // ✅ 按规则:issue form 不负责完结/数量提交,只记录问题 +(可选)把 SOL 标记为 checked val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( request.pickOrderLineId, request.lotId ?: 0L ) stockOutLines.forEach { stockOutLine -> - val requiredQty = request.requiredQty?.toDouble() ?: 0.0 - val actualPickQtyDouble = actualPickQty.toDouble() - - // 设置状态:如果 actualPickQty >= requiredQty,则为 completed,否则为 partially_completed - val newStatus = if (actualPickQtyDouble >= requiredQty) { - "completed" - } else { - "partially_completed" - } - - stockOutLine.status = newStatus - stockOutLine.qty = actualPickQtyDouble + stockOutLine.status = "checked" stockOutLine.modified = LocalDateTime.now() stockOutLine.modifiedBy = "system" - val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - - println("Updated stock out line ${stockOutLine.id} status to: ${newStatus} (NOT rejected)") - - // ✅ 修复:使用更新前的 onHandQty 计算 balance - val balance = onHandQtyBeforeUpdate - actualPickQtyDouble - createStockLedgerForStockOut(savedStockOutLine, "Nor", balance) + stockOutLineRepository.saveAndFlush(stockOutLine) + println("Issue form: marked stock out line ${stockOutLine.id} as checked (no completion)") } // ✅ 修复:检查 pick order line 是否应该标记为完成 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 b7c311c..70f86df 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 @@ -24,6 +24,7 @@ import com.ffii.fpsms.modules.pickOrder.web.models.* import com.ffii.fpsms.modules.stock.entity.InventoryLotLine import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository import com.ffii.fpsms.modules.stock.entity.StockOut +import com.ffii.fpsms.modules.stock.entity.StockOutLine import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository import com.ffii.fpsms.modules.stock.entity.StockOutRepository import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus @@ -1459,7 +1460,7 @@ open class PickOrderService( println("=== DEBUG: checkAndCompletePickOrderByConsoCode ===") println("consoCode: $consoCode") - val stockOut = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null) + val stockOut = stockOutRepository.findFirstByConsoPickOrderCodeOrderByIdDesc(consoCode) if (stockOut == null) { println("❌ No stock_out found for consoCode: $consoCode") return MessageResponse( @@ -3955,6 +3956,103 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto } val oldIll = spl.suggestedLotLine + + // Load stock out line (if provided) to decide "bind vs split" + val existingSol = if (req.stockOutLineId != null && req.stockOutLineId > 0) { + stockOutLIneRepository.findById(req.stockOutLineId).orElse(null) + } else null + val pickedQty = existingSol?.qty?.let { numToBigDecimal(it as? Number) } ?: zero + + // Switch lot rule: + // - actual pick == 0: replace/bind (no new line) + // - actual pick > 0: split remaining qty into a NEW suggested pick lot + stock out line + if (pickedQty.compareTo(zero) > 0) { + val remaining = qtyToHold.subtract(pickedQty) + if (remaining.compareTo(zero) <= 0) { + return MessageResponse( + id = null, + name = "No remaining qty", + code = "REJECT", + type = "pickorder", + message = "Reject switch lot: picked=$pickedQty already >= required=$qtyToHold", + errorPosition = null + ) + } + + // Move HOLD for remaining qty (old -> new) + if (oldIll != null && oldIll.id != null && oldIll.id != newIll.id) { + val oldHold = oldIll.holdQty ?: zero + val newOldHold = oldHold.subtract(remaining) + oldIll.holdQty = if (newOldHold.compareTo(zero) < 0) zero else newOldHold + inventoryLotLineRepository.save(oldIll) + + val newHold = (newIll.holdQty ?: zero).add(remaining) + newIll.holdQty = newHold + inventoryLotLineRepository.save(newIll) + } + if (oldIll == null) { + val newHold = (newIll.holdQty ?: zero).add(remaining) + newIll.holdQty = newHold + inventoryLotLineRepository.save(newIll) + } + + // Lock current suggestion qty to the picked qty (picked part stays on oldIll) + spl.qty = pickedQty + suggestPickLotRepository.saveAndFlush(spl) + + // Create a NEW stock out line + suggestion for the remaining qty + val stockOut: StockOut = if (existingSol != null) { + existingSol.stockOut ?: throw IllegalStateException("Existing StockOutLine has null stockOut") + } else { + val consoCode = pol.pickOrder?.consoCode ?: "" + val existing = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null) + if (existing != null) { + existing + } else { + val handlerId = pol.pickOrder?.assignTo?.id ?: SecurityUtils.getUser().orElse(null)?.id + require(handlerId != null) { "Cannot create StockOut: handlerId is null" } + val newStockOut = StockOut().apply { + this.consoPickOrderCode = consoCode + this.type = pol.pickOrder?.type?.value ?: "" + this.status = StockOutStatus.PENDING.status + this.handler = handlerId + } + stockOutRepository.save(newStockOut) + } + } + + val newSol = StockOutLine().apply { + this.stockOut = stockOut + this.pickOrderLine = pol + this.item = pol.item + this.inventoryLotLine = newIll + this.qty = 0.0 + this.status = StockOutLineStatus.CHECKED.status + this.startTime = LocalDateTime.now() + this.type = existingSol?.type ?: "Nor" + } + stockOutLIneRepository.saveAndFlush(newSol) + + val newSpl = SuggestedPickLot().apply { + this.type = spl.type + this.pickOrderLine = pol + this.suggestedLotLine = newIll + this.stockOutLine = newSol + this.qty = remaining + this.pickSuggested = spl.pickSuggested + } + suggestPickLotRepository.saveAndFlush(newSpl) + + val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo + return MessageResponse( + id = null, + name = "Lot substitution confirmed (split line)", + code = "SUCCESS", + type = "pickorder", + message = "Picked=$pickedQty, created new line for remaining=$remaining on lotNo '$newLotNo'", + errorPosition = null + ) + } // If oldIll exists and different: move hold old -> new if (oldIll != null && oldIll.id != null && oldIll.id != newIll.id) { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutRepository.kt index 1878207..902e5a4 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutRepository.kt @@ -7,5 +7,7 @@ import java.util.Optional @Repository interface StockOutRepository: AbstractRepository { fun findByConsoPickOrderCode(consoPickOrderCode: String) : Optional + // consoPickOrderCode 可能在 DB 中存在重复,避免 single-result exception + fun findFirstByConsoPickOrderCodeOrderByIdDesc(consoPickOrderCode: String): StockOut? fun findByStockTakeIdAndDeletedFalse(stockTakeId: Long): StockOut? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 766f72c..019fb62 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt @@ -72,6 +72,35 @@ private val inventoryLotLineService: InventoryLotLineService, private val inventoryRepository: InventoryRepository, private val pickExecutionIssueRepository: PickExecutionIssueRepository ): AbstractBaseEntityService(jdbcDao, stockOutLineRepository) { + private fun isEndStatus(status: String?): Boolean { + val s = status?.trim()?.lowercase() ?: return false + return s == "completed" || s == "rejected" || s == "partially_completed" + } + + @Transactional + private fun tryCompletePickOrderLine(pickOrderLineId: Long) { + val sols = stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) + if (sols.isEmpty()) return + + val allEnded = sols.all { isEndStatus(it.status) } + if (!allEnded) return + + val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return + if (pol.status != PickOrderLineStatus.COMPLETED) { + pol.status = PickOrderLineStatus.COMPLETED + pickOrderLineRepository.save(pol) + } + + // Optionally bubble up to pick order completion (safe no-op if not ready) + val consoCode = pol.pickOrder?.consoCode + if (!consoCode.isNullOrBlank()) { + try { + pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode) + } catch (e: Exception) { + println("⚠️ Error checking pick order completion for consoCode=$consoCode: ${e.message}") + } + } + } @Throws(IOException::class) @Transactional open fun findAllByStockOutId(stockOutId: Long): List { @@ -624,6 +653,10 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { } val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}") + // If this stock out line is in end status, try completing its pick order line + if (isEndStatus(savedStockOutLine.status)) { + savedStockOutLine.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) } + } try { val item = savedStockOutLine.item val inventoryLotLine = savedStockOutLine.inventoryLotLine