Parcourir la source

fix stock leger balance part

fix負數倉
CANCERYS\kw093 il y a 5 jours
Parent
révision
dbfbf8534a
8 fichiers modifiés avec 78 ajouts et 61 suppressions
  1. +3
    -2
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  2. +14
    -8
      src/main/java/com/ffii/fpsms/modules/master/service/StockLedgerFixService.kt
  3. +33
    -27
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  4. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt
  5. +7
    -6
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  6. +6
    -14
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  7. +3
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt
  8. +11
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt

+ 3
- 2
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt Voir le fichier

@@ -2665,12 +2665,13 @@ return MessageResponse(
}

private fun resolveWorkbenchPreviousBalance(
itemId: Long,
inventory: Inventory,
onHandQtyBeforeUpdate: Double?,
): Double {
if (onHandQtyBeforeUpdate != null) return onHandQtyBeforeUpdate
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId)
val latestLedger = inventory.id?.let {
stockLedgerRepository.findFirstByInventory_IdAndDeletedFalseOrderByDateDescIdDesc(it)
}
return latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
}



+ 14
- 8
src/main/java/com/ffii/fpsms/modules/master/service/StockLedgerFixService.kt Voir le fichier

@@ -482,8 +482,7 @@ open class StockLedgerFixService(
}

open fun inventoryScopeDetail(inventoryId: Long): StockLedgerFixScopeDetail {
val today = LocalDate.now()
val args = mapOf("inventoryId" to inventoryId, "today" to today)
val args = mapOf("inventoryId" to inventoryId, "today" to inventoryScopeToExclusive())
val head = jdbcDao.queryForList(
"""
SELECT
@@ -556,10 +555,14 @@ open class StockLedgerFixService(
)
}

/**
* Rebuild 2.1–2.6 for one inventory bucket through calendar today
* (`date < tomorrow`). Calendar day-fix still excludes today unless Allow today.
*/
open fun fixInventoryScope(inventoryId: Long): StockLedgerFixRunResponse {
if (inventoryId <= 0) throw IllegalArgumentException("inventoryId is required")
if (!inFlight.add(runLock)) throw IllegalStateException("fix already running")
val today = LocalDate.now()
val toExclusive = inventoryScopeToExclusive()
val t0 = System.nanoTime()
try {
val inv = jdbcDao.queryForList(
@@ -572,26 +575,26 @@ open class StockLedgerFixService(
"inventoryId" to inventoryId,
"itemId" to (inv["itemId"] ?: 0),
"uomId" to (inv["uomId"] ?: 0),
"today" to today,
"today" to toExclusive,
)
val filledLotLineId = timed("inv 2.1") { fillLotLineIdScope(args, byInventory = true) }
val filledUomId = timed("inv 2.2") { fillUomIdScope(args, byInventory = true) }
val filledInventoryId = timed("inv 2.3") { fillInventoryIdScope(args, byInventory = true) }
val filledLotQty = timed("inv 2.4") { fillLotQtyHistory(args, byInventory = true) }
val filledBalance = timed("inv 2.5") { fillBalanceHistory(args) }
val lotIds = lotIdsForInventory(inventoryId, today)
val range = dateRangeForFilter("inventoryId = :inventoryId", mapOf("inventoryId" to inventoryId, "today" to today))
val lotIds = lotIdsForInventory(inventoryId, toExclusive)
val range = dateRangeForFilter("inventoryId = :inventoryId", mapOf("inventoryId" to inventoryId, "today" to toExclusive))
val dayRows = if (range != null) {
timed("inv 2.6") { upsertStockLotDayRange(lotIds, range.first, range.second) }
} else 0
val still = stillMissScope("inventoryId = :inventoryId", mapOf("inventoryId" to inventoryId, "today" to today))
val still = stillMissScope("inventoryId = :inventoryId", mapOf("inventoryId" to inventoryId, "today" to toExclusive))
log.info(
"stock-ledger-fix inventory {} lotLine={} uom={} inv={} lotQty={} bal={} dayRows={} total={}ms",
inventoryId, filledLotLineId, filledUomId, filledInventoryId, filledLotQty, filledBalance, dayRows,
(System.nanoTime() - t0) / 1_000_000,
)
return StockLedgerFixRunResponse(
date = range?.first?.toString() ?: today.toString(),
date = range?.first?.toString() ?: LocalDate.now().toString(),
filledLotLineId = filledLotLineId,
filledUomId = filledUomId,
filledInventoryId = filledInventoryId,
@@ -1730,6 +1733,9 @@ open class StockLedgerFixService(
)
}

/** Exclusive bound for inventory-scope inspect/fix: include calendar today. */
private fun inventoryScopeToExclusive(): LocalDate = LocalDate.now().plusDays(1)

private fun fillBalanceHistory(args: Map<String, Any>): Int {
return jdbcDao.executeUpdate(
"""


+ 33
- 27
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Voir le fichier

@@ -10,6 +10,7 @@ import com.ffii.fpsms.modules.pickOrder.entity.HandleStatus
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.Inventory
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.stock.service.StockOutLineService

@@ -46,6 +47,7 @@ import com.ffii.fpsms.modules.stock.web.model.BatchStockOutResult
import com.ffii.fpsms.modules.stock.entity.StockLedger
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.service.StockLedgerLotSnapshot
import com.ffii.fpsms.modules.stock.service.InventoryBucketResolver
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
@@ -72,7 +74,8 @@ open class PickExecutionIssueService(
private val stockLedgerRepository: StockLedgerRepository,
private val deliveryOrderRepository: DeliveryOrderRepository,
private val suggestedPickLotRepository: SuggestPickLotRepository,
private val doPickOrderRepository: DoPickOrderRepository
private val doPickOrderRepository: DoPickOrderRepository,
private val inventoryBucketResolver: InventoryBucketResolver,

) {
// 配置属性:是否在批次拒绝时自动重新建议批次(默认为 false,不自动重新建议)
@@ -858,13 +861,8 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac
println("Miss Qty: ${missQty}")
val lotId = request.lotId ?: return
val itemId = request.itemId ?: return
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
// ✅ 修复:在更新 lot line 之前获取 inventory 的 onHandQty
val inventoryBeforeUpdate = inventoryRepository.findByItemId(itemId).orElse(null)
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
if (inventoryLotLine != null) {
// ✅ 修改:更新 outQty 为 actualPickQty
val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
@@ -927,9 +925,7 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD
val itemId = request.itemId ?: return
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
// ✅ 修复:在更新 lot line 之前获取 inventory 的 onHandQty
val inventoryBeforeUpdate = inventoryRepository.findByItemId(itemId).orElse(null)
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
val onHandQtyBeforeUpdate = bucketOnHandQty(itemId, inventoryLotLine)
if (inventoryLotLine != null) {
// ✅ 修改:释放 holdQty(减少 holdQty)
@@ -1128,10 +1124,9 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty
val lotId = request.lotId ?: return
val itemId = request.itemId ?: return
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
// ✅ 修复:在更新 lot line 之前获取 inventory 的 onHandQty
val inventoryBeforeUpdate = inventoryRepository.findByItemId(itemId).orElse(null)
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
val onHandQtyBeforeUpdate = bucketOnHandQty(itemId, inventoryLotLine)
// ✅ 修复:更新 stock_out_line,但不要累积 qty
val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
@@ -1163,7 +1158,6 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty
}
// ✅ 修复:更新 inventory_lot_line 的 outQty
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
if (inventoryLotLine != null) {
// ✅ 修复:计算新的 outQty,考虑之前的 outQty
val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
@@ -2324,23 +2318,19 @@ private fun createStockLedgerForStockOut(
val item = stockOutLine.item ?: return
val outQty = stockOutLine.qty?.toDouble() ?: 0.0
// ✅ 修复:如果传入了 balance,直接使用;否则重新查询 inventory
val inventory = resolveLedgerInventory(item.id!!, stockOutLine.inventoryLotLine) ?: return
val finalBalance = if (balance != null) {
if (balance < 0) 0.0 else balance
} else {
// 重新查询 inventory 以获取触发器更新后的最新 onHandQty
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return
val currentOnHandQty = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val newBalance = currentOnHandQty - outQty
val previous = previousBalanceForInventory(inventory)
val newBalance = previous - outQty
if (newBalance < 0) 0.0 else newBalance
}
// 使用传入的 ledgerType,如果没有则使用 stockOutLine.type
val ledgerTypeToUse = ledgerType ?: stockOutLine.type ?: "Nor"
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return
val ill = stockOutLine.inventoryLotLine
val outQtyBd = BigDecimal.valueOf(outQty)
val lotAfter = ill?.let { StockLedgerLotSnapshot.availableQty(it) }
@@ -2736,8 +2726,11 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
)
}
// ✅ 修复:在创建和保存 stockOutLine 之前获取 inventory 的当前 onHandQty
val inventoryBeforeUpdate = inventoryRepository.findByItemId(request.itemId).orElse(null)
val lotLine = request.lotId.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
val inventoryBeforeUpdate = inventoryBucketResolver.findInventoryBucket(request.itemId, lotLine)
?: inventoryRepository.findByItemId(request.itemId).orElse(null)
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
println("=== submitIssueWithQty: Before update ===")
@@ -2746,9 +2739,6 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}")
println("==========================================")
val lotLine = request.lotId.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}")
val item = itemsRepository.findById(request.itemId).orElse(null)
@@ -2835,7 +2825,7 @@ private fun createStockLedgerForStockOutWithBalance(
// Use传入的 ledgerType,如果没有则使用 stockOutLine.type
val ledgerTypeToUse = ledgerType ?: stockOutLine.type ?: "Nor"
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return
val inventory = resolveLedgerInventory(item.id!!, stockOutLine.inventoryLotLine) ?: return
val ill = stockOutLine.inventoryLotLine
val outQtyBd = BigDecimal.valueOf(outQty)
@@ -2947,4 +2937,20 @@ private fun resolveLotUomInfo(lotId: Long?): Pair<String?, String?> {

return Pair(uomCode, uomDesc)
}

private fun bucketOnHandQty(itemId: Long, lotLine: InventoryLotLine?): Double {
val inventory = resolveLedgerInventory(itemId, lotLine)
return (inventory?.onHandQty ?: BigDecimal.ZERO).toDouble()
}

private fun resolveLedgerInventory(itemId: Long, lotLine: InventoryLotLine?): Inventory? =
inventoryBucketResolver.findInventoryBucket(itemId, lotLine)
?: inventoryRepository.findByItemId(itemId).orElse(null)

private fun previousBalanceForInventory(inventory: Inventory): Double {
val latest = inventory.id?.let {
stockLedgerRepository.findFirstByInventory_IdAndDeletedFalseOrderByDateDescIdDesc(it)
}
return latest?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
}
}

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt Voir le fichier

@@ -60,6 +60,7 @@ interface StockLedgerRepository: AbstractRepository<StockLedger, Long> {
""")
fun findLatestByItemId(@Param("itemId") itemId: Long): List<StockLedger>
fun findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId: Long): StockLedger?
fun findFirstByInventory_IdAndDeletedFalseOrderByDateDescIdDesc(inventoryId: Long): StockLedger?

@Query("""
SELECT sl FROM StockLedger sl


+ 7
- 6
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Voir le fichier

@@ -1074,7 +1074,7 @@ open class StockInLineService(
if (putAwayDeltaStockQty > BigDecimal.ZERO) {
val _tLedger = System.nanoTime()
createStockLedgerForStockIn(stockInLine, putAwayDeltaStockQty.toDouble())
_logStep("createStockLedgerForStockIn(includes findLatestByItemId)", _tLedger)
_logStep("createStockLedgerForStockIn(includes findLatestByInventoryId)", _tLedger)
}

val _tQtyCalc = System.nanoTime()
@@ -1396,13 +1396,14 @@ open class StockInLineService(
val inventory = inventoryBucketResolver.findInventoryBucket(item.id!!, stockInLine.inventoryLotLine) ?: return
_logStep("inventoryBucketResolver.findInventoryBucket", _tInv)

// ✅ 修复:查询最新的 stock_ledger 记录,基于前一笔 balance 计算
// Chain running balance on this inventory bucket only (not the whole item).
val _tLatest = System.nanoTime()
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(item.id!!)
_logStep("stockLedgerRepository.findLatestByItemId", _tLatest)
val latestLedger = inventory.id?.let {
stockLedgerRepository.findFirstByInventory_IdAndDeletedFalseOrderByDateDescIdDesc(it)
}
_logStep("stockLedgerRepository.findLatestByInventoryId", _tLatest)

// ✅ 修复:如果 latestLedger 为 null(第一次 stock in),previousBalance 应该是 0
// 因为 inventory.onHandQty 已经被触发器更新为包含本次 stock in 的数量
// First ledger on this bucket starts at 0: inventory.onHandQty already includes this stock-in.
val previousBalance = latestLedger?.balance ?: 0.0

val newBalance = previousBalance + inQty


+ 6
- 14
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Voir le fichier

@@ -1450,9 +1450,7 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
val inventory = inventoryBucketResolver.findInventoryBucket(item.id!!, stockOutLine.inventoryLotLine) ?: return

val outQty = stockOutLine.qty?.toDouble() ?: 0.0
// Use latest ledger balance (same pattern as createStockLedgerForStockIn) so balance is correct when multiple actions run in one transaction
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(item.id!!)
val previousBalance = latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val previousBalance = resolvePreviousBalance(inventory = inventory)
val newBalance = previousBalance - outQty

val ill = stockOutLine.inventoryLotLine
@@ -1726,7 +1724,6 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
}

val previousBalance = resolvePreviousBalance(
itemId = item.id!!,
inventory = inventory,
onHandQtyBeforeUpdate = onHandQtyBeforeUpdate
)
@@ -1774,14 +1771,15 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
}

private fun resolvePreviousBalance(
itemId: Long,
inventory: Inventory,
onHandQtyBeforeUpdate: Double? = null
): Double {
if (onHandQtyBeforeUpdate != null) {
return onHandQtyBeforeUpdate
}
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId)
val latestLedger = inventory.id?.let {
stockLedgerRepository.findFirstByInventory_IdAndDeletedFalseOrderByDateDescIdDesc(it)
}
return latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
}

@@ -2137,10 +2135,7 @@ fun applyStockOutLineDelta(
val item = savedSol.item ?: return savedSol
val inventory = inventoryBucketResolver.findInventoryBucket(item.id!!, savedSol.inventoryLotLine) ?: return savedSol

val previousBalance = resolvePreviousBalance(
itemId = item.id!!,
inventory = inventory
)
val previousBalance = resolvePreviousBalance(inventory = inventory)
val outQty = deltaQty.toDouble()
val newBalance = previousBalance - outQty

@@ -2287,10 +2282,7 @@ open fun createStockOutBatch(request: BatchStockOutRequest): BatchStockOutResult
?: return@forEach
val delta = BigDecimal.valueOf(sol.qty ?: 0.0)
val prevBalance = runningLedgerBalanceByBucket[bucketKey]
?: run {
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId)
latestLedger?.balance ?: (inv.onHandQty ?: BigDecimal.ZERO).toDouble()
}
?: resolvePreviousBalance(inventory = inv)
val newBalance = prevBalance - delta.toDouble()

val ill = sol.inventoryLotLine


+ 3
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt Voir le fichier

@@ -547,12 +547,13 @@ if (updated == 0) {
}

private fun resolveWorkbenchPreviousBalance(
itemId: Long,
inventory: Inventory,
onHandQtyBeforeUpdate: Double?,
): Double {
if (onHandQtyBeforeUpdate != null) return onHandQtyBeforeUpdate
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId)
val latestLedger = inventory.id?.let {
stockLedgerRepository.findFirstByInventory_IdAndDeletedFalseOrderByDateDescIdDesc(it)
}
return latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
}



+ 11
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Voir le fichier

@@ -2482,7 +2482,7 @@ private fun applyVarianceAdjustment(
?: throw IllegalArgumentException("Item ID not found for stock take ledger")
val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory)
val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey)
?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
?: previousBalanceForInventoryBucket(inventory)
val newBalance = previousBalance - qtyToRemove.toDouble()

// FP-MTMS Version Checklist | Functions Ref. No. 46 | v1.0.1 | 2026-08-05
@@ -2618,7 +2618,7 @@ private fun applyVarianceAdjustment(
?: throw IllegalArgumentException("Item ID not found for stock take ledger (in)")
val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory)
val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey)
?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
?: previousBalanceForInventoryBucket(inventory)
val newBalance = previousBalance + plusQty.toDouble()

// latestLine.inQty already includes plusQty above — snapshot live lot line after move.
@@ -2680,6 +2680,15 @@ private fun stockTakeLedgerBucketKey(
return itemId to stockUomId
}

private fun previousBalanceForInventoryBucket(
inventory: com.ffii.fpsms.modules.stock.entity.Inventory,
): Double {
val latest = inventory.id?.let {
stockLedgerRepository.findFirstByInventory_IdAndDeletedFalseOrderByDateDescIdDesc(it)
}
return latest?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
}

private data class BatchAdjustmentCache(
val inventoryLotLineByWarehouseLot: Map<Pair<Long, Long>, InventoryLotLine>,
val inventoryLotById: Map<Long, InventoryLot>,


Chargement…
Annuler
Enregistrer