| @@ -2665,12 +2665,13 @@ return MessageResponse( | |||||
| } | } | ||||
| private fun resolveWorkbenchPreviousBalance( | private fun resolveWorkbenchPreviousBalance( | ||||
| itemId: Long, | |||||
| inventory: Inventory, | inventory: Inventory, | ||||
| onHandQtyBeforeUpdate: Double?, | onHandQtyBeforeUpdate: Double?, | ||||
| ): Double { | ): Double { | ||||
| if (onHandQtyBeforeUpdate != null) return onHandQtyBeforeUpdate | 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() | return latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | ||||
| } | } | ||||
| @@ -482,8 +482,7 @@ open class StockLedgerFixService( | |||||
| } | } | ||||
| open fun inventoryScopeDetail(inventoryId: Long): StockLedgerFixScopeDetail { | 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( | val head = jdbcDao.queryForList( | ||||
| """ | """ | ||||
| SELECT | 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 { | open fun fixInventoryScope(inventoryId: Long): StockLedgerFixRunResponse { | ||||
| if (inventoryId <= 0) throw IllegalArgumentException("inventoryId is required") | if (inventoryId <= 0) throw IllegalArgumentException("inventoryId is required") | ||||
| if (!inFlight.add(runLock)) throw IllegalStateException("fix already running") | if (!inFlight.add(runLock)) throw IllegalStateException("fix already running") | ||||
| val today = LocalDate.now() | |||||
| val toExclusive = inventoryScopeToExclusive() | |||||
| val t0 = System.nanoTime() | val t0 = System.nanoTime() | ||||
| try { | try { | ||||
| val inv = jdbcDao.queryForList( | val inv = jdbcDao.queryForList( | ||||
| @@ -572,26 +575,26 @@ open class StockLedgerFixService( | |||||
| "inventoryId" to inventoryId, | "inventoryId" to inventoryId, | ||||
| "itemId" to (inv["itemId"] ?: 0), | "itemId" to (inv["itemId"] ?: 0), | ||||
| "uomId" to (inv["uomId"] ?: 0), | "uomId" to (inv["uomId"] ?: 0), | ||||
| "today" to today, | |||||
| "today" to toExclusive, | |||||
| ) | ) | ||||
| val filledLotLineId = timed("inv 2.1") { fillLotLineIdScope(args, byInventory = true) } | val filledLotLineId = timed("inv 2.1") { fillLotLineIdScope(args, byInventory = true) } | ||||
| val filledUomId = timed("inv 2.2") { fillUomIdScope(args, byInventory = true) } | val filledUomId = timed("inv 2.2") { fillUomIdScope(args, byInventory = true) } | ||||
| val filledInventoryId = timed("inv 2.3") { fillInventoryIdScope(args, byInventory = true) } | val filledInventoryId = timed("inv 2.3") { fillInventoryIdScope(args, byInventory = true) } | ||||
| val filledLotQty = timed("inv 2.4") { fillLotQtyHistory(args, byInventory = true) } | val filledLotQty = timed("inv 2.4") { fillLotQtyHistory(args, byInventory = true) } | ||||
| val filledBalance = timed("inv 2.5") { fillBalanceHistory(args) } | 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) { | val dayRows = if (range != null) { | ||||
| timed("inv 2.6") { upsertStockLotDayRange(lotIds, range.first, range.second) } | timed("inv 2.6") { upsertStockLotDayRange(lotIds, range.first, range.second) } | ||||
| } else 0 | } 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( | log.info( | ||||
| "stock-ledger-fix inventory {} lotLine={} uom={} inv={} lotQty={} bal={} dayRows={} total={}ms", | "stock-ledger-fix inventory {} lotLine={} uom={} inv={} lotQty={} bal={} dayRows={} total={}ms", | ||||
| inventoryId, filledLotLineId, filledUomId, filledInventoryId, filledLotQty, filledBalance, dayRows, | inventoryId, filledLotLineId, filledUomId, filledInventoryId, filledLotQty, filledBalance, dayRows, | ||||
| (System.nanoTime() - t0) / 1_000_000, | (System.nanoTime() - t0) / 1_000_000, | ||||
| ) | ) | ||||
| return StockLedgerFixRunResponse( | return StockLedgerFixRunResponse( | ||||
| date = range?.first?.toString() ?: today.toString(), | |||||
| date = range?.first?.toString() ?: LocalDate.now().toString(), | |||||
| filledLotLineId = filledLotLineId, | filledLotLineId = filledLotLineId, | ||||
| filledUomId = filledUomId, | filledUomId = filledUomId, | ||||
| filledInventoryId = filledInventoryId, | 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 { | private fun fillBalanceHistory(args: Map<String, Any>): Int { | ||||
| return jdbcDao.executeUpdate( | return jdbcDao.executeUpdate( | ||||
| """ | """ | ||||
| @@ -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.StockOutLIneRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | 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.entity.InventoryRepository | ||||
| import com.ffii.fpsms.modules.stock.service.StockOutLineService | 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.StockLedger | ||||
| import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository | import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository | ||||
| import com.ffii.fpsms.modules.stock.service.StockLedgerLotSnapshot | 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.entity.DeliveryOrderRepository | ||||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | ||||
| import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository | import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository | ||||
| @@ -72,7 +74,8 @@ open class PickExecutionIssueService( | |||||
| private val stockLedgerRepository: StockLedgerRepository, | private val stockLedgerRepository: StockLedgerRepository, | ||||
| private val deliveryOrderRepository: DeliveryOrderRepository, | private val deliveryOrderRepository: DeliveryOrderRepository, | ||||
| private val suggestedPickLotRepository: SuggestPickLotRepository, | private val suggestedPickLotRepository: SuggestPickLotRepository, | ||||
| private val doPickOrderRepository: DoPickOrderRepository | |||||
| private val doPickOrderRepository: DoPickOrderRepository, | |||||
| private val inventoryBucketResolver: InventoryBucketResolver, | |||||
| ) { | ) { | ||||
| // 配置属性:是否在批次拒绝时自动重新建议批次(默认为 false,不自动重新建议) | // 配置属性:是否在批次拒绝时自动重新建议批次(默认为 false,不自动重新建议) | ||||
| @@ -858,13 +861,8 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac | |||||
| println("Miss Qty: ${missQty}") | println("Miss Qty: ${missQty}") | ||||
| val lotId = request.lotId ?: return | val lotId = request.lotId ?: return | ||||
| val itemId = request.itemId ?: return | |||||
| val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) | 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) { | if (inventoryLotLine != null) { | ||||
| // ✅ 修改:更新 outQty 为 actualPickQty | // ✅ 修改:更新 outQty 为 actualPickQty | ||||
| val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO | val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO | ||||
| @@ -927,9 +925,7 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD | |||||
| val itemId = request.itemId ?: return | val itemId = request.itemId ?: return | ||||
| val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) | 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) { | if (inventoryLotLine != null) { | ||||
| // ✅ 修改:释放 holdQty(减少 holdQty) | // ✅ 修改:释放 holdQty(减少 holdQty) | ||||
| @@ -1128,10 +1124,9 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty | |||||
| val lotId = request.lotId ?: return | val lotId = request.lotId ?: return | ||||
| val itemId = request.itemId ?: 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 | // ✅ 修复:更新 stock_out_line,但不要累积 qty | ||||
| val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | ||||
| @@ -1163,7 +1158,6 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty | |||||
| } | } | ||||
| // ✅ 修复:更新 inventory_lot_line 的 outQty | // ✅ 修复:更新 inventory_lot_line 的 outQty | ||||
| val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null) | |||||
| if (inventoryLotLine != null) { | if (inventoryLotLine != null) { | ||||
| // ✅ 修复:计算新的 outQty,考虑之前的 outQty | // ✅ 修复:计算新的 outQty,考虑之前的 outQty | ||||
| val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO | val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO | ||||
| @@ -2324,23 +2318,19 @@ private fun createStockLedgerForStockOut( | |||||
| val item = stockOutLine.item ?: return | val item = stockOutLine.item ?: return | ||||
| val outQty = stockOutLine.qty?.toDouble() ?: 0.0 | val outQty = stockOutLine.qty?.toDouble() ?: 0.0 | ||||
| // ✅ 修复:如果传入了 balance,直接使用;否则重新查询 inventory | |||||
| val inventory = resolveLedgerInventory(item.id!!, stockOutLine.inventoryLotLine) ?: return | |||||
| val finalBalance = if (balance != null) { | val finalBalance = if (balance != null) { | ||||
| if (balance < 0) 0.0 else balance | if (balance < 0) 0.0 else balance | ||||
| } else { | } 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 | if (newBalance < 0) 0.0 else newBalance | ||||
| } | } | ||||
| // 使用传入的 ledgerType,如果没有则使用 stockOutLine.type | // 使用传入的 ledgerType,如果没有则使用 stockOutLine.type | ||||
| val ledgerTypeToUse = ledgerType ?: stockOutLine.type ?: "Nor" | val ledgerTypeToUse = ledgerType ?: stockOutLine.type ?: "Nor" | ||||
| val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return | |||||
| val ill = stockOutLine.inventoryLotLine | val ill = stockOutLine.inventoryLotLine | ||||
| val outQtyBd = BigDecimal.valueOf(outQty) | val outQtyBd = BigDecimal.valueOf(outQty) | ||||
| val lotAfter = ill?.let { StockLedgerLotSnapshot.availableQty(it) } | 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() | val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() | ||||
| println("=== submitIssueWithQty: Before update ===") | println("=== submitIssueWithQty: Before update ===") | ||||
| @@ -2746,9 +2739,6 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse | |||||
| println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") | println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") | ||||
| println("==========================================") | println("==========================================") | ||||
| val lotLine = request.lotId.let { | |||||
| inventoryLotLineRepository.findById(it).orElse(null) | |||||
| } | |||||
| println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}") | println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}") | ||||
| val item = itemsRepository.findById(request.itemId).orElse(null) | val item = itemsRepository.findById(request.itemId).orElse(null) | ||||
| @@ -2835,7 +2825,7 @@ private fun createStockLedgerForStockOutWithBalance( | |||||
| // Use传入的 ledgerType,如果没有则使用 stockOutLine.type | // Use传入的 ledgerType,如果没有则使用 stockOutLine.type | ||||
| val ledgerTypeToUse = ledgerType ?: stockOutLine.type ?: "Nor" | 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 ill = stockOutLine.inventoryLotLine | ||||
| val outQtyBd = BigDecimal.valueOf(outQty) | val outQtyBd = BigDecimal.valueOf(outQty) | ||||
| @@ -2947,4 +2937,20 @@ private fun resolveLotUomInfo(lotId: Long?): Pair<String?, String?> { | |||||
| return Pair(uomCode, uomDesc) | 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() | |||||
| } | |||||
| } | } | ||||
| @@ -60,6 +60,7 @@ interface StockLedgerRepository: AbstractRepository<StockLedger, Long> { | |||||
| """) | """) | ||||
| fun findLatestByItemId(@Param("itemId") itemId: Long): List<StockLedger> | fun findLatestByItemId(@Param("itemId") itemId: Long): List<StockLedger> | ||||
| fun findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId: Long): StockLedger? | fun findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId: Long): StockLedger? | ||||
| fun findFirstByInventory_IdAndDeletedFalseOrderByDateDescIdDesc(inventoryId: Long): StockLedger? | |||||
| @Query(""" | @Query(""" | ||||
| SELECT sl FROM StockLedger sl | SELECT sl FROM StockLedger sl | ||||
| @@ -1074,7 +1074,7 @@ open class StockInLineService( | |||||
| if (putAwayDeltaStockQty > BigDecimal.ZERO) { | if (putAwayDeltaStockQty > BigDecimal.ZERO) { | ||||
| val _tLedger = System.nanoTime() | val _tLedger = System.nanoTime() | ||||
| createStockLedgerForStockIn(stockInLine, putAwayDeltaStockQty.toDouble()) | createStockLedgerForStockIn(stockInLine, putAwayDeltaStockQty.toDouble()) | ||||
| _logStep("createStockLedgerForStockIn(includes findLatestByItemId)", _tLedger) | |||||
| _logStep("createStockLedgerForStockIn(includes findLatestByInventoryId)", _tLedger) | |||||
| } | } | ||||
| val _tQtyCalc = System.nanoTime() | val _tQtyCalc = System.nanoTime() | ||||
| @@ -1396,13 +1396,14 @@ open class StockInLineService( | |||||
| val inventory = inventoryBucketResolver.findInventoryBucket(item.id!!, stockInLine.inventoryLotLine) ?: return | val inventory = inventoryBucketResolver.findInventoryBucket(item.id!!, stockInLine.inventoryLotLine) ?: return | ||||
| _logStep("inventoryBucketResolver.findInventoryBucket", _tInv) | _logStep("inventoryBucketResolver.findInventoryBucket", _tInv) | ||||
| // ✅ 修复:查询最新的 stock_ledger 记录,基于前一笔 balance 计算 | |||||
| // Chain running balance on this inventory bucket only (not the whole item). | |||||
| val _tLatest = System.nanoTime() | 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 previousBalance = latestLedger?.balance ?: 0.0 | ||||
| val newBalance = previousBalance + inQty | val newBalance = previousBalance + inQty | ||||
| @@ -1450,9 +1450,7 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||||
| val inventory = inventoryBucketResolver.findInventoryBucket(item.id!!, stockOutLine.inventoryLotLine) ?: return | val inventory = inventoryBucketResolver.findInventoryBucket(item.id!!, stockOutLine.inventoryLotLine) ?: return | ||||
| val outQty = stockOutLine.qty?.toDouble() ?: 0.0 | 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 newBalance = previousBalance - outQty | ||||
| val ill = stockOutLine.inventoryLotLine | val ill = stockOutLine.inventoryLotLine | ||||
| @@ -1726,7 +1724,6 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||||
| } | } | ||||
| val previousBalance = resolvePreviousBalance( | val previousBalance = resolvePreviousBalance( | ||||
| itemId = item.id!!, | |||||
| inventory = inventory, | inventory = inventory, | ||||
| onHandQtyBeforeUpdate = onHandQtyBeforeUpdate | onHandQtyBeforeUpdate = onHandQtyBeforeUpdate | ||||
| ) | ) | ||||
| @@ -1774,14 +1771,15 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||||
| } | } | ||||
| private fun resolvePreviousBalance( | private fun resolvePreviousBalance( | ||||
| itemId: Long, | |||||
| inventory: Inventory, | inventory: Inventory, | ||||
| onHandQtyBeforeUpdate: Double? = null | onHandQtyBeforeUpdate: Double? = null | ||||
| ): Double { | ): Double { | ||||
| if (onHandQtyBeforeUpdate != null) { | if (onHandQtyBeforeUpdate != null) { | ||||
| return onHandQtyBeforeUpdate | return onHandQtyBeforeUpdate | ||||
| } | } | ||||
| val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId) | |||||
| val latestLedger = inventory.id?.let { | |||||
| stockLedgerRepository.findFirstByInventory_IdAndDeletedFalseOrderByDateDescIdDesc(it) | |||||
| } | |||||
| return latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | return latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | ||||
| } | } | ||||
| @@ -2137,10 +2135,7 @@ fun applyStockOutLineDelta( | |||||
| val item = savedSol.item ?: return savedSol | val item = savedSol.item ?: return savedSol | ||||
| val inventory = inventoryBucketResolver.findInventoryBucket(item.id!!, savedSol.inventoryLotLine) ?: 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 outQty = deltaQty.toDouble() | ||||
| val newBalance = previousBalance - outQty | val newBalance = previousBalance - outQty | ||||
| @@ -2287,10 +2282,7 @@ open fun createStockOutBatch(request: BatchStockOutRequest): BatchStockOutResult | |||||
| ?: return@forEach | ?: return@forEach | ||||
| val delta = BigDecimal.valueOf(sol.qty ?: 0.0) | val delta = BigDecimal.valueOf(sol.qty ?: 0.0) | ||||
| val prevBalance = runningLedgerBalanceByBucket[bucketKey] | 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 newBalance = prevBalance - delta.toDouble() | ||||
| val ill = sol.inventoryLotLine | val ill = sol.inventoryLotLine | ||||
| @@ -547,12 +547,13 @@ if (updated == 0) { | |||||
| } | } | ||||
| private fun resolveWorkbenchPreviousBalance( | private fun resolveWorkbenchPreviousBalance( | ||||
| itemId: Long, | |||||
| inventory: Inventory, | inventory: Inventory, | ||||
| onHandQtyBeforeUpdate: Double?, | onHandQtyBeforeUpdate: Double?, | ||||
| ): Double { | ): Double { | ||||
| if (onHandQtyBeforeUpdate != null) return onHandQtyBeforeUpdate | 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() | return latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | ||||
| } | } | ||||
| @@ -2482,7 +2482,7 @@ private fun applyVarianceAdjustment( | |||||
| ?: throw IllegalArgumentException("Item ID not found for stock take ledger") | ?: throw IllegalArgumentException("Item ID not found for stock take ledger") | ||||
| val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory) | val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory) | ||||
| val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey) | val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey) | ||||
| ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | |||||
| ?: previousBalanceForInventoryBucket(inventory) | |||||
| val newBalance = previousBalance - qtyToRemove.toDouble() | val newBalance = previousBalance - qtyToRemove.toDouble() | ||||
| // FP-MTMS Version Checklist | Functions Ref. No. 46 | v1.0.1 | 2026-08-05 | // 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)") | ?: throw IllegalArgumentException("Item ID not found for stock take ledger (in)") | ||||
| val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory) | val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory) | ||||
| val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey) | val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey) | ||||
| ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | |||||
| ?: previousBalanceForInventoryBucket(inventory) | |||||
| val newBalance = previousBalance + plusQty.toDouble() | val newBalance = previousBalance + plusQty.toDouble() | ||||
| // latestLine.inQty already includes plusQty above — snapshot live lot line after move. | // latestLine.inQty already includes plusQty above — snapshot live lot line after move. | ||||
| @@ -2680,6 +2680,15 @@ private fun stockTakeLedgerBucketKey( | |||||
| return itemId to stockUomId | 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( | private data class BatchAdjustmentCache( | ||||
| val inventoryLotLineByWarehouseLot: Map<Pair<Long, Long>, InventoryLotLine>, | val inventoryLotLineByWarehouseLot: Map<Pair<Long, Long>, InventoryLotLine>, | ||||
| val inventoryLotById: Map<Long, InventoryLot>, | val inventoryLotById: Map<Long, InventoryLot>, | ||||