diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt index 1945ba8..269f09d 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt @@ -1627,11 +1627,18 @@ open fun getCompletedJobOrderPickOrders(completedDate: LocalDate?): List + +@Query( + """ + SELECT po FROM PickOrder po + WHERE po.status = :status + AND po.deleted = false + AND po.jobOrder IS NOT NULL + AND po.jobOrder.planStart IS NOT NULL + AND po.jobOrder.planStart >= :planStartFrom + AND po.jobOrder.planStart < :planStartToExclusive + """ +) +fun findAllCompletedWithJobOrderPlanStartOnDay( + @Param("status") status: PickOrderStatus, + @Param("planStartFrom") planStartFrom: LocalDateTime, + @Param("planStartToExclusive") planStartToExclusive: LocalDateTime, +): List } \ No newline at end of file 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 6b66ea2..b4e049c 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 @@ -401,6 +401,142 @@ open class PickExecutionIssueService( ) } } + + /** + * Jo / 无异常量场景:不写入 pick_execution_issue、不更新 inventory_lot_line.issue_qty。 + * 仅 (1) 按 actualPickQty - requiredQty 调整 holdQty;(5) 将对应 stock_out_line 标为 checked(不改 qty)。 + * 可重复调用,避免 DUPLICATE。 + */ + open fun applyPickHoldAndMarkSolChecked(request: PickExecutionIssueRequest): MessageResponse { + try { + println("=== applyPickHoldAndMarkSolChecked: START ===") + val missQty = request.missQty ?: BigDecimal.ZERO + val badItemQty = request.badItemQty ?: BigDecimal.ZERO + if (missQty.compareTo(BigDecimal.ZERO) > 0 || badItemQty.compareTo(BigDecimal.ZERO) > 0) { + return MessageResponse( + id = null, + name = "Invalid request for hold-only API", + code = "ERROR", + type = "pick_execution_adjustment", + message = "This endpoint accepts only actual pick adjustment (miss and bad quantities must be zero). Use /recordIssue for issues.", + errorPosition = null + ) + } + if (request.lotId == null) { + return MessageResponse( + id = null, + name = "lotId required", + code = "ERROR", + type = "pick_execution_adjustment", + message = "inventory lot line id (lotId) is required", + errorPosition = null + ) + } + + val inventoryLotLine = inventoryLotLineRepository.findById(request.lotId).orElse(null) + val bookQty = if (inventoryLotLine != null) { + val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO + val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO + inQty.subtract(outQty) + } else { + BigDecimal.ZERO + } + + val requiredQty = request.requiredQty ?: BigDecimal.ZERO + val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO + val lotRemainAvailable = bookQty + val maxAllowed = requiredQty.add(lotRemainAvailable) + + if (actualPickQty > maxAllowed) { + return MessageResponse( + id = null, + name = "Actual pick qty too large", + code = "ERROR", + type = "pick_execution_adjustment", + message = "Actual pick qty cannot exceed required qty plus lot remaining available.", + errorPosition = null + ) + } + + if (inventoryLotLine != null) { + val deltaHold = actualPickQty.subtract(requiredQty) + if (deltaHold.compareTo(BigDecimal.ZERO) != 0) { + val latestLotLine = inventoryLotLineRepository.findById(request.lotId).orElse(null) + ?: throw IllegalArgumentException("Inventory lot line not found: ${request.lotId}") + + val currentHold = latestLotLine.holdQty ?: BigDecimal.ZERO + val currentOut = latestLotLine.outQty ?: BigDecimal.ZERO + val currentIn = latestLotLine.inQty ?: BigDecimal.ZERO + + val newHold = currentHold.add(deltaHold) + if (newHold < BigDecimal.ZERO) { + return MessageResponse( + id = null, + name = "Invalid hold quantity adjustment", + code = "ERROR", + type = "pick_execution_adjustment", + message = "Cannot adjust holdQty by $deltaHold. Current holdQty=$currentHold, requiredQty=$requiredQty, actualPickQty=$actualPickQty", + errorPosition = null + ) + } + + if (deltaHold > BigDecimal.ZERO) { + val remaining = currentIn.subtract(currentOut).subtract(currentHold) + if (deltaHold > remaining) { + return MessageResponse( + id = null, + name = "Insufficient remaining quantity", + code = "ERROR", + type = "pick_execution_adjustment", + message = "Cannot reserve additional $deltaHold. Remaining=$remaining (in=$currentIn, out=$currentOut, hold=$currentHold)", + errorPosition = null + ) + } + } + + latestLotLine.holdQty = newHold + latestLotLine.modified = LocalDateTime.now() + latestLotLine.modifiedBy = "system" + inventoryLotLineRepository.saveAndFlush(latestLotLine) + println("✅ [hold-only] Adjusted inventory_lot_line ${request.lotId} holdQty: $currentHold -> $newHold (delta=$deltaHold)") + } + } + + val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( + request.pickOrderLineId, + request.lotId + ) + stockOutLines.forEach { sol -> + sol.status = "checked" + sol.modified = LocalDateTime.now() + sol.modifiedBy = "system" + stockOutLineRepository.save(sol) + } + stockOutLineRepository.flush() + + println("=== applyPickHoldAndMarkSolChecked: SUCCESS (${stockOutLines.size} SOL checked) ===") + return MessageResponse( + id = stockOutLines.firstOrNull()?.id, + name = "Pick hold adjusted and lines marked checked", + code = "SUCCESS", + type = "pick_execution_adjustment", + message = "Pick hold adjusted and stock out lines marked checked", + errorPosition = null + ) + } catch (e: Exception) { + println("=== applyPickHoldAndMarkSolChecked: ERROR === ${e.message}") + e.printStackTrace() + return MessageResponse( + id = null, + name = "Failed to apply pick hold adjustment", + code = "ERROR", + type = "pick_execution_adjustment", + message = "Error: ${e.message}", + errorPosition = null + ) + } + } + private fun handleAllZeroMarkCompleted(request: PickExecutionIssueRequest) { val stockOutLines = stockOutLineRepository .findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( 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 148fbc9..6c70395 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 @@ -20,6 +20,12 @@ class PickExecutionIssueController( return pickExecutionIssueService.recordPickExecutionIssue(request) } + /** 无 miss/bad:仅调整 hold + SOL 标 checked,不写 pick_execution_issue(可重复提交)。 */ + @PostMapping("/applyHoldAndChecked") + fun applyPickHoldAndMarkSolChecked(@RequestBody request: PickExecutionIssueRequest): MessageResponse { + return pickExecutionIssueService.applyPickHoldAndMarkSolChecked(request) + } + @GetMapping("/issues/pickOrder/{pickOrderId}") fun getPickExecutionIssuesByPickOrder(@PathVariable pickOrderId: Long): List { return pickExecutionIssueService.getPickExecutionIssuesByPickOrder(pickOrderId)