|
|
|
@@ -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( |
|
|
|
|