|
|
|
@@ -51,6 +51,8 @@ import com.ffii.fpsms.modules.stock.entity.InventoryLotLine |
|
|
|
import jakarta.persistence.EntityManager |
|
|
|
import jakarta.persistence.PersistenceContext |
|
|
|
import java.util.UUID |
|
|
|
import org.springframework.http.HttpStatus |
|
|
|
import org.springframework.web.server.ResponseStatusException |
|
|
|
@Service |
|
|
|
open class StockOutLineService( |
|
|
|
private val jdbcDao: JdbcDao, |
|
|
|
@@ -1589,6 +1591,135 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { |
|
|
|
return savedStockOutLine |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Stock adjustment should only reduce physical on-hand (inQty - outQty) and must NOT affect holdQty. |
|
|
|
* |
|
|
|
* Rule: delta outQty cannot exceed availableQty = inQty - outQty - holdQty. |
|
|
|
* This prevents ending in an invalid state where onHand < onHold and avoids negative trigger-derived quantities. |
|
|
|
*/ |
|
|
|
open fun createStockOutForAdjustment(request: StockOutRequest): StockOutLine { |
|
|
|
val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() |
|
|
|
val qtyBd = BigDecimal.valueOf(request.qty) |
|
|
|
if (qtyBd <= BigDecimal.ZERO) { |
|
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Adjustment qty must be > 0") |
|
|
|
} |
|
|
|
|
|
|
|
val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO |
|
|
|
val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO |
|
|
|
val holdQty = inventoryLotLine.holdQty ?: BigDecimal.ZERO |
|
|
|
val availableQty = inQty.subtract(outQty).subtract(holdQty) |
|
|
|
if (qtyBd > availableQty) { |
|
|
|
throw ResponseStatusException( |
|
|
|
HttpStatus.BAD_REQUEST, |
|
|
|
"Adjustment qty exceeds availableQty (availableQty=$availableQty, requested=$qtyBd). " + |
|
|
|
"Rule: requested <= inQty - outQty - holdQty." |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
// Step 1: Increase outQty only; keep holdQty unchanged |
|
|
|
val updatedInventoryLotLine = inventoryLotLine.apply { |
|
|
|
this.outQty = outQty.add(qtyBd) |
|
|
|
this.status = inventoryLotLineService.deriveInventoryLotLineStatus( |
|
|
|
this.status, |
|
|
|
this.inQty, |
|
|
|
this.outQty, |
|
|
|
this.holdQty |
|
|
|
) |
|
|
|
} |
|
|
|
inventoryLotLineRepository.save(updatedInventoryLotLine) |
|
|
|
// inventory aggregates: handled by DB trigger `inventory_lot_line_AFTER_UPDATE` |
|
|
|
|
|
|
|
val itemId = updatedInventoryLotLine.inventoryLot?.item?.id |
|
|
|
?: throw IllegalArgumentException("InventoryLotLine must have an associated item") |
|
|
|
|
|
|
|
// Step 2: Create stock_out header |
|
|
|
val currentUser = SecurityUtils.getUser().orElseThrow() |
|
|
|
val stockOut = StockOut().apply { |
|
|
|
this.type = request.type |
|
|
|
this.completeDate = LocalDateTime.now() |
|
|
|
this.handler = currentUser.id |
|
|
|
this.status = StockOutStatus.COMPLETE.status |
|
|
|
} |
|
|
|
val savedStockOut = stockOutRepository.save(stockOut) |
|
|
|
|
|
|
|
// Step 3: Create stock_out_line |
|
|
|
val item = itemRepository.findById(itemId).orElseThrow() |
|
|
|
val stockOutLine = StockOutLine().apply { |
|
|
|
this.item = item |
|
|
|
this.qty = request.qty |
|
|
|
this.stockOut = savedStockOut |
|
|
|
this.inventoryLotLine = updatedInventoryLotLine |
|
|
|
this.status = StockOutLineStatus.COMPLETE.status |
|
|
|
this.pickTime = LocalDateTime.now() |
|
|
|
this.handledBy = currentUser.id |
|
|
|
this.type = request.type |
|
|
|
} |
|
|
|
val savedStockOutLine = saveAndFlush(stockOutLine) |
|
|
|
|
|
|
|
// Step 4: ledger |
|
|
|
createStockLedgerForStockOut(savedStockOutLine) |
|
|
|
return savedStockOutLine |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Removal flow (stock adjustment "remove lot line"): |
|
|
|
* - Physically reduce on-hand to zero by setting outQty = inQty |
|
|
|
* - Clear holdQty = 0 to avoid invalid state (onHand < onHold) and negative available calculations |
|
|
|
* |
|
|
|
* A stock-out document/ledger is still created for audit trail. |
|
|
|
*/ |
|
|
|
open fun createStockOutForRemoval(inventoryLotLineId: Long, type: String = "ADJ"): StockOutLine { |
|
|
|
val inventoryLotLine = inventoryLotLineRepository.findById(inventoryLotLineId).orElseThrow() |
|
|
|
val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO |
|
|
|
val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO |
|
|
|
val deltaOut = inQty.subtract(outQty).coerceAtLeast(BigDecimal.ZERO) |
|
|
|
|
|
|
|
// Step 1: Set outQty=inQty and clear holdQty |
|
|
|
val updatedInventoryLotLine = inventoryLotLine.apply { |
|
|
|
this.outQty = inQty |
|
|
|
this.holdQty = BigDecimal.ZERO |
|
|
|
this.status = inventoryLotLineService.deriveInventoryLotLineStatus( |
|
|
|
this.status, |
|
|
|
this.inQty, |
|
|
|
this.outQty, |
|
|
|
this.holdQty |
|
|
|
) |
|
|
|
} |
|
|
|
inventoryLotLineRepository.save(updatedInventoryLotLine) |
|
|
|
// inventory aggregates: handled by DB trigger `inventory_lot_line_AFTER_UPDATE` |
|
|
|
|
|
|
|
val itemId = updatedInventoryLotLine.inventoryLot?.item?.id |
|
|
|
?: throw IllegalArgumentException("InventoryLotLine must have an associated item") |
|
|
|
|
|
|
|
// Step 2: Create stock_out header |
|
|
|
val currentUser = SecurityUtils.getUser().orElseThrow() |
|
|
|
val stockOut = StockOut().apply { |
|
|
|
this.type = type |
|
|
|
this.completeDate = LocalDateTime.now() |
|
|
|
this.handler = currentUser.id |
|
|
|
this.status = StockOutStatus.COMPLETE.status |
|
|
|
} |
|
|
|
val savedStockOut = stockOutRepository.save(stockOut) |
|
|
|
|
|
|
|
// Step 3: Create stock_out_line (qty = physical delta) |
|
|
|
val item = itemRepository.findById(itemId).orElseThrow() |
|
|
|
val stockOutLine = StockOutLine().apply { |
|
|
|
this.item = item |
|
|
|
this.qty = deltaOut.toDouble() |
|
|
|
this.stockOut = savedStockOut |
|
|
|
this.inventoryLotLine = updatedInventoryLotLine |
|
|
|
this.status = StockOutLineStatus.COMPLETE.status |
|
|
|
this.pickTime = LocalDateTime.now() |
|
|
|
this.handledBy = currentUser.id |
|
|
|
this.type = type |
|
|
|
} |
|
|
|
val savedStockOutLine = saveAndFlush(stockOutLine) |
|
|
|
|
|
|
|
// Step 4: ledger |
|
|
|
createStockLedgerForStockOut(savedStockOutLine) |
|
|
|
return savedStockOutLine |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun createStockLedgerForPickDelta( |
|
|
|
stockOutLine: StockOutLine, |
|
|
|
|