浏览代码

branch out stock adjustment and stock transfer

master
kelvin.yau 12 小时前
父节点
当前提交
2be4384779
共有 3 个文件被更改,包括 101 次插入86 次删除
  1. +1
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt
  2. +94
    -74
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  3. +6
    -11
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTransferRecordService.kt

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt 查看文件

@@ -73,7 +73,7 @@ open class StockAdjustmentService(
val stockInLine = stockInLineService.createStockIn(stockInRequest)
saveAdjustmentRecordForStockIn(stockInLine)
} else {
// Branch 3 (qty down): createStockOut
// Branch 3 (qty down): adjustment outbound only (not pick createStockOut)
val stockOutLine = stockOutLineService.createStockOutForAdjustment(
StockOutRequest(
inventoryLotLineId = current.id,


+ 94
- 74
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt 查看文件

@@ -1533,19 +1533,50 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
stockLedgerRepository.saveAndFlush(stockLedger)
}

open fun createStockOut(request: StockOutRequest): StockOutLine {
/** Used for error messages when validating out-only flows (adjustment vs stock transfer). */
private enum class OutOnlyLotLineContext {
ADJUSTMENT,
STOCK_TRANSFER,
}

val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow()
val qtyBd = BigDecimal.valueOf(request.qty)
val oldHold = inventoryLotLine.holdQty ?: BigDecimal.ZERO
/**
* Validates qty for flows that only increase [InventoryLotLine.outQty] (hold unchanged):
* qty > 0 and qty <= inQty - outQty - holdQty.
*/
private fun validateOutOnlyIncrease(
inventoryLotLine: InventoryLotLine,
qtyBd: BigDecimal,
context: OutOnlyLotLineContext,
) {
if (qtyBd <= BigDecimal.ZERO) {
val msg = when (context) {
OutOnlyLotLineContext.ADJUSTMENT -> "Adjustment qty must be > 0"
OutOnlyLotLineContext.STOCK_TRANSFER -> "Stock transfer out qty must be > 0"
}
throw ResponseStatusException(HttpStatus.BAD_REQUEST, msg)
}
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) {
val prefix = when (context) {
OutOnlyLotLineContext.ADJUSTMENT -> "Adjustment qty exceeds availableQty"
OutOnlyLotLineContext.STOCK_TRANSFER -> "Stock transfer qty exceeds availableQty"
}
throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"$prefix (availableQty=$availableQty, requested=$qtyBd). " +
"Rule: requested <= inQty - outQty - holdQty."
)
}
}

// Step 1: Increase outQty in inventory_lot_line table and release hold (same idea as pick / QR scan)
val updatedInventoryLotLine = inventoryLotLine.apply {
val currentOutQty = this.outQty ?: BigDecimal.ZERO
val newOutQty = currentOutQty + qtyBd
this.outQty = newOutQty
val newHold = oldHold.subtract(qtyBd).coerceAtLeast(BigDecimal.ZERO)
this.holdQty = newHold
/** Applies validated out-only delta: increases [outQty] only, [holdQty] unchanged. */
private fun applyOutOnlyIncreaseToLotLine(inventoryLotLine: InventoryLotLine, qtyBd: BigDecimal) {
val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
inventoryLotLine.apply {
this.outQty = outQty.add(qtyBd)
this.status = inventoryLotLineService.deriveInventoryLotLineStatus(
this.status,
this.inQty,
@@ -1553,72 +1584,60 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
this.holdQty
)
}
inventoryLotLineRepository.save(updatedInventoryLotLine)
// inventory.onHandQty / onHoldQty / unavailableQty: 由 DB trigger `inventory_lot_line_AFTER_UPDATE`
// 依 old/new 的 in、out、hold、status 同步;此處勿再手動改 inventory.onHoldQty,否則與 trigger 重複扣會變負數。
}

/**
* After the lot line has been saved: create [StockOut], [StockOutLine], and stock ledger.
* Shared by pick/QR [createStockOut], [createStockOutForAdjustment], and [createStockOutForStockTransfer].
* Not used by [createStockOutForRemoval] (different line semantics).
*/
private fun completeStockOutAfterLotLineSave(
updatedInventoryLotLine: InventoryLotLine,
stockOutType: String,
qty: Double,
): StockOutLine {
val itemId = updatedInventoryLotLine.inventoryLot?.item?.id
?: throw IllegalArgumentException("InventoryLotLine must have an associated item")

// Step 2: Create a row of stock_out
val currentUser = SecurityUtils.getUser().orElseThrow()
val stockOut = StockOut().apply {
this.type = request.type
this.type = stockOutType
this.completeDate = LocalDateTime.now()
this.handler = currentUser.id
this.status = StockOutStatus.COMPLETE.status
}
val savedStockOut = stockOutRepository.save(stockOut)

// Step 3: Create a row in stock_out_line table
val item = itemRepository.findById(itemId).orElseThrow()

val stockOutLine = StockOutLine().apply {
this.item = item
this.qty = request.qty
this.qty = qty
this.stockOut = savedStockOut
this.inventoryLotLine = updatedInventoryLotLine
this.status = StockOutLineStatus.COMPLETE.status
this.pickTime = LocalDateTime.now()
this.handledBy = currentUser.id
this.type = request.type
this.type = stockOutType
}
val savedStockOutLine = saveAndFlush(stockOutLine)

// Step 4: Create a row in stock_ledger table
createStockLedgerForStockOut(savedStockOutLine)

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.
* Pick / QR scan outbound: increases [outQty] and releases [holdQty] up to qty (same behaviour as before TRF was split out).
* For adjustment or stock-transfer source outbound, use [createStockOutForAdjustment] or [createStockOutForStockTransfer].
*/
open fun createStockOutForAdjustment(request: StockOutRequest): StockOutLine {
open fun createStockOut(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."
)
}
val oldHold = inventoryLotLine.holdQty ?: BigDecimal.ZERO

// Step 1: Increase outQty only; keep holdQty unchanged
// Increase outQty and release hold (pick / QR); inventory aggregates via trigger inventory_lot_line_AFTER_UPDATE
val updatedInventoryLotLine = inventoryLotLine.apply {
this.outQty = outQty.add(qtyBd)
val currentOutQty = this.outQty ?: BigDecimal.ZERO
this.outQty = currentOutQty + qtyBd
this.holdQty = oldHold.subtract(qtyBd).coerceAtLeast(BigDecimal.ZERO)
this.status = inventoryLotLineService.deriveInventoryLotLineStatus(
this.status,
this.inQty,
@@ -1627,38 +1646,39 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
)
}
inventoryLotLineRepository.save(updatedInventoryLotLine)
// inventory aggregates: handled by DB trigger `inventory_lot_line_AFTER_UPDATE`
// Do not manually adjust inventory.onHoldQty here — trigger syncs from lot line changes.

val itemId = updatedInventoryLotLine.inventoryLot?.item?.id
?: throw IllegalArgumentException("InventoryLotLine must have an associated item")
return completeStockOutAfterLotLineSave(updatedInventoryLotLine, request.type, request.qty)
}

// 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)
/**
* Stock transfer (TRF) source outbound: [outQty] only, [holdQty] unchanged, qty capped by available on the line.
* Independent of [createStockOut] (pick/QR); shares out-only validation/apply with [createStockOutForAdjustment].
*/
open fun createStockOutForStockTransfer(inventoryLotLineId: Long, qty: Double): StockOutLine {
val inventoryLotLine = inventoryLotLineRepository.findById(inventoryLotLineId).orElseThrow()
val qtyBd = BigDecimal.valueOf(qty)
validateOutOnlyIncrease(inventoryLotLine, qtyBd, OutOnlyLotLineContext.STOCK_TRANSFER)
applyOutOnlyIncreaseToLotLine(inventoryLotLine, qtyBd)
inventoryLotLineRepository.save(inventoryLotLine)
// inventory aggregates: DB trigger inventory_lot_line_AFTER_UPDATE

// 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)
return completeStockOutAfterLotLineSave(inventoryLotLine, "TRF", qty)
}

// Step 4: ledger
createStockLedgerForStockOut(savedStockOutLine)
return savedStockOutLine
/**
* Stock adjustment outbound: only increases [outQty]; must not change [holdQty].
* Rule: qty <= inQty - outQty - holdQty (avoids invalid on-hand vs on-hold and trigger inconsistencies).
*/
open fun createStockOutForAdjustment(request: StockOutRequest): StockOutLine {
val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow()
val qtyBd = BigDecimal.valueOf(request.qty)
validateOutOnlyIncrease(inventoryLotLine, qtyBd, OutOnlyLotLineContext.ADJUSTMENT)
applyOutOnlyIncreaseToLotLine(inventoryLotLine, qtyBd)
inventoryLotLineRepository.save(inventoryLotLine)
// inventory aggregates: DB trigger inventory_lot_line_AFTER_UPDATE

return completeStockOutAfterLotLineSave(inventoryLotLine, request.type, request.qty)
}

/**
@@ -2162,7 +2182,7 @@ fun applyStockOutLineDelta(
}
}

// 3) stock_ledger (shared source with createStockOut/createStockLedgerForStockOut style)
// 3) stock_ledger (same ledger shape as createStockLedgerForStockOut / completeStockOutAfterLotLineSave)
if (!skipLedgerWrite) {
val item = savedSol.item ?: return savedSol
val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!) ?: return savedSol


+ 6
- 11
src/main/java/com/ffii/fpsms/modules/stock/service/StockTransferRecordService.kt 查看文件

@@ -9,7 +9,6 @@ import com.ffii.fpsms.modules.stock.entity.StockTransferRecord
import com.ffii.fpsms.modules.stock.entity.StockTransferRecordRepository
import com.ffii.fpsms.modules.stock.web.model.CreateStockTransferRequest
import com.ffii.fpsms.modules.stock.web.model.StockInRequest
import com.ffii.fpsms.modules.stock.web.model.StockOutRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
@@ -34,13 +33,13 @@ open class StockTransferRecordService(

@Transactional
open fun createStockTransfer(request: CreateStockTransferRequest): MessageResponse {
// Step 1: Stock Out - using generic function
val stockOutLine = createStockOut(request)
// Step 1: TRF outbound on source lot line (out-only; not pick/QR createStockOut)
val stockOutLine = createSourceStockOutForTransfer(request)

// Step 2: Stock In - using generic function
// Step 2: TRF inbound to target warehouse (merge or new lot line)
val (stockInLine, stockInMode) = createStockIn(request)

// Step 3: Create Stock Transfer Record
// Step 3: Persist transfer record linking in/out lines
val stockTransferRecord = createStockTransferRecord(request, stockOutLine, stockInLine)

return MessageResponse(
@@ -59,15 +58,11 @@ open class StockTransferRecordService(
)
}

private fun createStockOut(request: CreateStockTransferRequest): StockOutLine {

val stockOutRequest = StockOutRequest(
private fun createSourceStockOutForTransfer(request: CreateStockTransferRequest): StockOutLine {
return stockOutLineService.createStockOutForStockTransfer(
inventoryLotLineId = request.inventoryLotLineId,
qty = request.transferredQty.toDouble(),
type = "TRF"
)

return stockOutLineService.createStockOut(stockOutRequest)
}

private fun createStockIn(request: CreateStockTransferRequest): Pair<StockInLine, TransferStockInMode> {


正在加载...
取消
保存