diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt index 6e0f5f9..e75cb39 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt @@ -40,6 +40,7 @@ interface InventoryRepository: AbstractRepository { fun findInventoryInfoByItemInAndDeletedIsFalse(items: List): List fun findByItemId(itemId: Long): Optional + fun findAllByItemIdInAndDeletedIsFalse(itemIds: Collection): List @Query("SELECT i FROM Inventory i WHERE i.item.id = :itemId AND i.deleted = false") fun findAllByItemIdAndDeletedIsFalse(itemId: Long): List diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt index 648ef52..b5feedf 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt @@ -9,6 +9,7 @@ interface StockTakeLineRepository : AbstractRepository { fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeLine?; fun findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId: Serializable, stockTakeId: Serializable): StockTakeLine?; fun findByStockTakeRecord_IdAndDeletedIsFalse(stockTakeRecordId: Long): StockTakeLine? + fun findAllByStockTakeRecord_IdInAndDeletedIsFalse(stockTakeRecordIds: Collection): List fun findAllByStockTakeIdInAndInventoryLotLineIdInAndDeletedIsFalse( stockTakeIds: Collection, inventoryLotLineIds: Collection diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt index 946ba4d..b15337b 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt @@ -1856,11 +1856,14 @@ if (itemParts.isNotEmpty()) { open fun batchSaveApproverStockTakeRecordsByIds( request: BatchSaveApproverStockTakeByIdsRequest ): BatchSaveApproverStockTakeRecordResponse { - println("batchSaveApproverStockTakeRecordsByIds called for stockTakeId: ${request.stockTakeId}, ids=${request.recordIds.size}") + val totalStartNs = System.nanoTime() + fun elapsedMs(startNs: Long): Long = (System.nanoTime() - startNs) / 1_000_000 + logger.info("batchSaveApproverStockTakeRecordsByIds start: stockTakeId={}, ids={}", request.stockTakeId, request.recordIds.size) if (request.recordIds.isEmpty()) { return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No record IDs provided")) } + val loadStartNs = System.nanoTime() val user = userRepository.findById(request.approverId).orElse(null) val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") @@ -1873,15 +1876,33 @@ open fun batchSaveApproverStockTakeRecordsByIds( (it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) && it.approverStockTakeQty == null } - println("Found ${stockTakeRecords.size} records to process by IDs") + logger.info( + "batchSaveApproverStockTakeRecordsByIds load completed: candidates={}, loadMs={}", + stockTakeRecords.size, + elapsedMs(loadStartNs) + ) if (stockTakeRecords.isEmpty()) { return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No records found matching criteria")) } + val cacheBuildStartNs = System.nanoTime() + val adjustmentCache = buildBatchAdjustmentCache(stockTakeRecords) + logger.info( + "batchSaveApproverStockTakeRecordsByIds cache build completed: lotLinePairs={}, lots={}, inventories={}, stockTakeLines={}, cacheBuildMs={}", + adjustmentCache.inventoryLotLineByWarehouseLot.size, + adjustmentCache.inventoryLotById.size, + adjustmentCache.inventoryByItemId.size, + adjustmentCache.stockTakeLineByRecordId.size, + elapsedMs(cacheBuildStartNs) + ) var successCount = 0 var errorCount = 0 val errors = mutableListOf() val processedStockTakes = mutableSetOf>() + val prepareStartNs = System.nanoTime() + val recordsToPersist = mutableListOf() + val postPersistActions = mutableListOf>() + stockTakeRecords.forEach { record -> try { val qty: BigDecimal @@ -1915,27 +1936,8 @@ open fun batchSaveApproverStockTakeRecordsByIds( } } - stockTakeRecordRepository.save(record) - - if (varianceQty != BigDecimal.ZERO) { - try { - applyVarianceAdjustment(record.stockTake ?: stockTake, record, qty, varianceQty, request.approverId) - } catch (e: Exception) { - logger.error("Failed to apply variance adjustment for record ${record.id}", e) - errorCount++ - errors.add("Record ${record.id}: ${e.message}") - return@forEach - } - } else { - completeStockTakeLineForApproverNoVariance(record.stockTake ?: stockTake, record, qty) - } - - val stId = record.stockTake?.id - val section = record.stockTakeSection - if (stId != null && section != null) { - processedStockTakes.add(Pair(stId, section)) - } - successCount++ + recordsToPersist.add(record) + postPersistActions.add(Triple(record, qty, varianceQty)) } catch (e: Exception) { errorCount++ val errorMsg = "Error processing record ${record.id}: ${e.message}" @@ -1943,14 +1945,118 @@ open fun batchSaveApproverStockTakeRecordsByIds( logger.error(errorMsg, e) } } + logger.info( + "batchSaveApproverStockTakeRecordsByIds prepare completed: readyToPersist={}, precheckErrors={}, prepareMs={}", + recordsToPersist.size, + errorCount, + elapsedMs(prepareStartNs) + ) + + if (recordsToPersist.isNotEmpty()) { + val persistStartNs = System.nanoTime() + stockTakeRecordRepository.saveAll(recordsToPersist) + logger.info( + "batchSaveApproverStockTakeRecordsByIds persist completed: persisted={}, persistMs={}", + recordsToPersist.size, + elapsedMs(persistStartNs) + ) + + val adjustmentStartNs = System.nanoTime() + val runtimeCache = BatchAdjustmentRuntimeCache( + stockOutByStockTakeId = mutableMapOf(), + stockInByStockTakeId = mutableMapOf(), + runningLedgerBalanceByItemId = mutableMapOf() + ) + val adjustmentContext = StockTakeAdjustmentBatchContext() + var varianceCount = 0 + var noVarianceCount = 0 + var varianceMs = 0L + var noVarianceMs = 0L + postPersistActions.forEach { (record, qty, varianceQty) -> + try { + if (varianceQty != BigDecimal.ZERO) { + val varianceStartNs = System.nanoTime() + applyVarianceAdjustment( + record.stockTake ?: stockTake, + record, + qty, + varianceQty, + request.approverId, + adjustmentCache, + runtimeCache, + adjustmentContext + ) + varianceMs += elapsedMs(varianceStartNs) + varianceCount++ + } else { + val noVarianceStartNs = System.nanoTime() + completeStockTakeLineForApproverNoVariance( + record.stockTake ?: stockTake, + record, + qty, + adjustmentCache, + adjustmentContext + ) + noVarianceMs += elapsedMs(noVarianceStartNs) + noVarianceCount++ + } + val stId = record.stockTake?.id + val section = record.stockTakeSection + if (stId != null && section != null) { + processedStockTakes.add(Pair(stId, section)) + } + successCount++ + } catch (e: Exception) { + logger.error("Failed to apply inventory/line update for record ${record.id}", e) + errorCount++ + errors.add("Record ${record.id}: ${e.message}") + } + } + val flushStartNs = System.nanoTime() + flushStockTakeAdjustmentBatchContext(adjustmentContext) + val flushMs = elapsedMs(flushStartNs) + logger.info( + "batchSaveApproverStockTakeRecordsByIds adjustment completed: successSoFar={}, errorsSoFar={}, adjustmentMs={}, varianceCount={}, varianceMs={}, noVarianceCount={}, noVarianceMs={}, flushMs={}", + successCount, + errorCount, + elapsedMs(adjustmentStartNs), + varianceCount, + varianceMs, + noVarianceCount, + noVarianceMs, + flushMs + ) + logger.info( + "batchSaveApproverStockTakeRecordsByIds runtime cache stats: stockOutHeads={}, stockInHeads={}, ledgerItems={}, batchedStockTakeLines={}, batchedOutLines={}, batchedInLines={}, batchedInventoryLotLines={}, batchedLedgers={}", + runtimeCache.stockOutByStockTakeId.size, + runtimeCache.stockInByStockTakeId.size, + runtimeCache.runningLedgerBalanceByItemId.size, + adjustmentContext.stockTakeLineByRecordId.size, + adjustmentContext.stockOutLines.size, + adjustmentContext.stockInLines.size, + adjustmentContext.inventoryLotLineById.size, + adjustmentContext.stockLedgers.size + ) + } if (successCount > 0) { + val statusStartNs = System.nanoTime() processedStockTakes.forEach { (stId, section) -> checkAndUpdateStockTakeStatus(stId, section) } + logger.info( + "batchSaveApproverStockTakeRecordsByIds status update completed: stockTakes={}, statusMs={}", + processedStockTakes.size, + elapsedMs(statusStartNs) + ) } - println("batchSaveApproverStockTakeRecordsByIds completed: success=$successCount, errors=$errorCount") + logger.info( + "batchSaveApproverStockTakeRecordsByIds completed: success={}, errors={}, totalMs={}", + successCount, + errorCount, + elapsedMs(totalStartNs) + ) return BatchSaveApproverStockTakeRecordResponse( successCount = successCount, errorCount = errorCount, @@ -1962,10 +2068,19 @@ open fun batchSaveApproverStockTakeRecordsByIds( * stockTakeRecord 上存的是 inventory_lot.id(批次),不是 inventory_lot_line.id;用倉庫 + 批次找唯一庫存行。 */ private fun resolveInventoryLotLineForStockTakeRecord(record: StockTakeRecord): InventoryLotLine { + return resolveInventoryLotLineForStockTakeRecord(record, null) +} + +private fun resolveInventoryLotLineForStockTakeRecord( + record: StockTakeRecord, + cache: BatchAdjustmentCache? +): InventoryLotLine { val warehouseId = record.warehouse?.id ?: throw IllegalArgumentException("Warehouse not found on stock take record") val lotId = record.inventoryLotId ?: record.lotId ?: throw IllegalArgumentException("Inventory lot ID not found on stock take record") + val cacheKey = Pair(warehouseId, lotId) + cache?.inventoryLotLineByWarehouseLot?.get(cacheKey)?.let { return it } val lines = inventoryLotLineRepository.findAllByWarehouseIdInAndInventoryLotIdInAndDeletedIsFalse( listOf(warehouseId), listOf(lotId) @@ -1985,10 +2100,14 @@ private fun resolveInventoryLotLineForStockTakeRecord(record: StockTakeRecord): private fun completeStockTakeLineForApproverNoVariance( stockTake: StockTake, stockTakeRecord: StockTakeRecord, - finalQty: BigDecimal + finalQty: BigDecimal, + cache: BatchAdjustmentCache? = null, + context: StockTakeAdjustmentBatchContext? = null ) { val rid = stockTakeRecord.id ?: return - val line = stockTakeLineRepository.findByStockTakeRecord_IdAndDeletedIsFalse(rid) ?: return + val line = cache?.stockTakeLineByRecordId?.get(rid) + ?: stockTakeLineRepository.findByStockTakeRecord_IdAndDeletedIsFalse(rid) + ?: return line.apply { this.stockTake = stockTake this.initialQty = this.initialQty ?: stockTakeRecord.bookQty @@ -1997,7 +2116,11 @@ private fun completeStockTakeLineForApproverNoVariance( this.completeDate = LocalDateTime.now() this.stockTakeRecord = stockTakeRecord } - stockTakeLineRepository.save(line) + if (context != null) { + context.stockTakeLineByRecordId[rid] = line + } else { + stockTakeLineRepository.save(line) + } } /** @@ -2011,23 +2134,29 @@ private fun applyVarianceAdjustment( stockTakeRecord: StockTakeRecord, finalQty: BigDecimal, varianceQty: BigDecimal, - approverId: Long? + approverId: Long?, + cache: BatchAdjustmentCache? = null, + runtimeCache: BatchAdjustmentRuntimeCache? = null, + context: StockTakeAdjustmentBatchContext? = null ) { if (varianceQty == BigDecimal.ZERO) return - val inventoryLotLine = resolveInventoryLotLineForStockTakeRecord(stockTakeRecord) + val inventoryLotLine = resolveInventoryLotLineForStockTakeRecord(stockTakeRecord, cache) - val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse( - inventoryLotLine.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found") - ) ?: throw IllegalArgumentException("Inventory lot not found") + val inventoryLotId = inventoryLotLine.inventoryLot?.id + ?: throw IllegalArgumentException("Inventory lot ID not found") + val inventoryLot = cache?.inventoryLotById?.get(inventoryLotId) + ?: inventoryLotRepository.findByIdAndDeletedFalse(inventoryLotId) + ?: throw IllegalArgumentException("Inventory lot not found") - val inventory = inventoryRepository.findByItemId( - inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found") - ).orElse(null) ?: throw IllegalArgumentException("Inventory not found for item") + val itemId = inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found") + val inventory = cache?.inventoryByItemId?.get(itemId) + ?: inventoryRepository.findByItemId(itemId).orElse(null) + ?: throw IllegalArgumentException("Inventory not found for item") // 1. 更新開輪預建的 StockTakeLine,或舊資料無預建時新建 val stockTakeLine = stockTakeRecord.id?.let { rid -> - stockTakeLineRepository.findByStockTakeRecord_IdAndDeletedIsFalse(rid) + cache?.stockTakeLineByRecordId?.get(rid) ?: stockTakeLineRepository.findByStockTakeRecord_IdAndDeletedIsFalse(rid) }?.also { existing -> existing.apply { this.stockTake = stockTake @@ -2047,7 +2176,12 @@ private fun applyVarianceAdjustment( this.completeDate = LocalDateTime.now() this.stockTakeRecord = stockTakeRecord } - stockTakeLineRepository.save(stockTakeLine) + val stockTakeRecordId = stockTakeRecord.id + if (context != null && stockTakeRecordId != null) { + context.stockTakeLineByRecordId[stockTakeRecordId] = stockTakeLine + } else { + stockTakeLineRepository.save(stockTakeLine) + } val zero = BigDecimal.ZERO @@ -2062,12 +2196,24 @@ private fun applyVarianceAdjustment( return } - var stockOut = stockOutRepository.findByStockTakeIdAndDeletedFalse(stockTake.id!!) - ?: StockOut().apply { - this.type = "stockTake" - this.status = "completed" - this.handler = approverId - }.also { stockOutRepository.save(it) } + val stockTakeId = stockTake.id ?: throw IllegalArgumentException("Stock take ID not found") + val stockOut = if (runtimeCache != null) { + runtimeCache.stockOutByStockTakeId.getOrPut(stockTakeId) { + stockOutRepository.findByStockTakeIdAndDeletedFalse(stockTakeId) + ?: StockOut().apply { + this.type = "stockTake" + this.status = "completed" + this.handler = approverId + }.also { stockOutRepository.save(it) } + } + } else { + stockOutRepository.findByStockTakeIdAndDeletedFalse(stockTakeId) + ?: StockOut().apply { + this.type = "stockTake" + this.status = "completed" + this.handler = approverId + }.also { stockOutRepository.save(it) } + } val stockOutLine = StockOutLine().apply { this.item = inventoryLot.item @@ -2077,15 +2223,21 @@ private fun applyVarianceAdjustment( this.status = "completed" this.type = "TKE" } - stockOutLineRepository.save(stockOutLine) + if (context != null) { + context.stockOutLines.add(stockOutLine) + } else { + stockOutLineRepository.save(stockOutLine) + } // 與 StockOutLineService.createStockLedgerForStockOut 一致:依上一筆 ledger balance 扣減, // 避免同一批多筆盤虧時每筆都用同一個 inventory.onHandQty 導致 balance 錯誤。 val itemIdForLedger = inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found for stock take ledger") - val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger) - val previousBalance = latestLedger?.balance - ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + val previousBalance = runtimeCache?.runningLedgerBalanceByItemId?.get(itemIdForLedger) + ?: run { + val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger) + latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + } val newBalance = previousBalance - qtyToRemove.toDouble() val stockLedger = StockLedger().apply { @@ -2101,21 +2253,37 @@ private fun applyVarianceAdjustment( this.date = LocalDate.now() } - stockLedgerRepository.save(stockLedger) + if (context != null) { + context.stockLedgers.add(stockLedger) + } else { + stockLedgerRepository.save(stockLedger) + } + runtimeCache?.runningLedgerBalanceByItemId?.put(itemIdForLedger, newBalance) val newOutQty = (latestLine.outQty ?: zero).add(qtyToRemove) - val updateRequest = SaveInventoryLotLineRequest( - id = latestLine.id, - inventoryLotId = latestLine.inventoryLot?.id, - warehouseId = latestLine.warehouse?.id, - stockUomId = latestLine.stockUom?.id, - inQty = latestLine.inQty, - outQty = newOutQty, - holdQty = latestLine.holdQty, - status = latestLine.status?.value, - remarks = latestLine.remarks + latestLine.outQty = newOutQty + latestLine.status = inventoryLotLineService.deriveInventoryLotLineStatus( + latestLine.status, + latestLine.inQty, + latestLine.outQty, + latestLine.holdQty ) - inventoryLotLineService.saveInventoryLotLine(updateRequest) + if (context != null && latestLine.id != null) { + context.inventoryLotLineById[latestLine.id!!] = latestLine + } else { + val updateRequest = SaveInventoryLotLineRequest( + id = latestLine.id, + inventoryLotId = latestLine.inventoryLot?.id, + warehouseId = latestLine.warehouse?.id, + stockUomId = latestLine.stockUom?.id, + inQty = latestLine.inQty, + outQty = latestLine.outQty, + holdQty = latestLine.holdQty, + status = latestLine.status?.value, + remarks = latestLine.remarks + ) + inventoryLotLineService.saveInventoryLotLine(updateRequest) + } } // 3. variance > 0:入庫加在既有庫存行 inQty + StockLedger @@ -2124,12 +2292,24 @@ private fun applyVarianceAdjustment( val latestLine = inventoryLotLineRepository.findById(inventoryLotLine.id!!).orElseThrow() val newInQty = (latestLine.inQty ?: zero).add(plusQty) - var stockIn = stockInRepository.findByStockTakeIdAndDeletedFalse(stockTake.id!!) - ?: StockIn().apply { - this.code = stockTake.code - this.status = "completed" - this.stockTake = stockTake - }.also { stockInRepository.save(it) } + val stockTakeId = stockTake.id ?: throw IllegalArgumentException("Stock take ID not found") + val stockIn = if (runtimeCache != null) { + runtimeCache.stockInByStockTakeId.getOrPut(stockTakeId) { + stockInRepository.findByStockTakeIdAndDeletedFalse(stockTakeId) + ?: StockIn().apply { + this.code = stockTake.code + this.status = "completed" + this.stockTake = stockTake + }.also { stockInRepository.save(it) } + } + } else { + stockInRepository.findByStockTakeIdAndDeletedFalse(stockTakeId) + ?: StockIn().apply { + this.code = stockTake.code + this.status = "completed" + this.stockTake = stockTake + }.also { stockInRepository.save(it) } + } val stockInLine = StockInLine().apply { this.stockTakeLine = stockTakeLine @@ -2146,26 +2326,43 @@ private fun applyVarianceAdjustment( // 原入庫已綁定之 inventory_lot_line 與 SIL 多為 OneToOne;盤盈改加同一行 inQty,此處不綁 line 以免衝突 this.inventoryLotLine = null } - stockInLineRepository.save(stockInLine) - - val updateRequest = SaveInventoryLotLineRequest( - id = latestLine.id, - inventoryLotId = latestLine.inventoryLot?.id, - warehouseId = latestLine.warehouse?.id, - stockUomId = latestLine.stockUom?.id, - inQty = newInQty, - outQty = latestLine.outQty, - holdQty = latestLine.holdQty, - status = latestLine.status?.value, - remarks = latestLine.remarks + if (context != null) { + context.stockInLines.add(stockInLine) + } else { + stockInLineRepository.save(stockInLine) + } + + latestLine.inQty = newInQty + latestLine.status = inventoryLotLineService.deriveInventoryLotLineStatus( + latestLine.status, + latestLine.inQty, + latestLine.outQty, + latestLine.holdQty ) - inventoryLotLineService.saveInventoryLotLine(updateRequest) + if (context != null && latestLine.id != null) { + context.inventoryLotLineById[latestLine.id!!] = latestLine + } else { + val updateRequest = SaveInventoryLotLineRequest( + id = latestLine.id, + inventoryLotId = latestLine.inventoryLot?.id, + warehouseId = latestLine.warehouse?.id, + stockUomId = latestLine.stockUom?.id, + inQty = latestLine.inQty, + outQty = latestLine.outQty, + holdQty = latestLine.holdQty, + status = latestLine.status?.value, + remarks = latestLine.remarks + ) + inventoryLotLineService.saveInventoryLotLine(updateRequest) + } val itemIdForLedger = inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found for stock take ledger (in)") - val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger) - val previousBalance = latestLedger?.balance - ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + val previousBalance = runtimeCache?.runningLedgerBalanceByItemId?.get(itemIdForLedger) + ?: run { + val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger) + latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + } val newBalance = previousBalance + plusQty.toDouble() val stockLedger = StockLedger().apply { @@ -2181,8 +2378,112 @@ private fun applyVarianceAdjustment( this.date = LocalDate.now() } - stockLedgerRepository.save(stockLedger) + if (context != null) { + context.stockLedgers.add(stockLedger) + } else { + stockLedgerRepository.save(stockLedger) + } + runtimeCache?.runningLedgerBalanceByItemId?.put(itemIdForLedger, newBalance) + } +} + +private data class BatchAdjustmentCache( + val inventoryLotLineByWarehouseLot: Map, InventoryLotLine>, + val inventoryLotById: Map, + val inventoryByItemId: Map, + val stockTakeLineByRecordId: Map +) + +private data class BatchAdjustmentRuntimeCache( + val stockOutByStockTakeId: MutableMap, + val stockInByStockTakeId: MutableMap, + val runningLedgerBalanceByItemId: MutableMap +) + +private data class StockTakeAdjustmentBatchContext( + val stockTakeLineByRecordId: MutableMap = LinkedHashMap(), + val stockOutLines: MutableList = mutableListOf(), + val stockInLines: MutableList = mutableListOf(), + val inventoryLotLineById: MutableMap = LinkedHashMap(), + val stockLedgers: MutableList = mutableListOf() +) + +private fun flushStockTakeAdjustmentBatchContext(context: StockTakeAdjustmentBatchContext) { + if (context.stockTakeLineByRecordId.isNotEmpty()) { + stockTakeLineRepository.saveAll(context.stockTakeLineByRecordId.values.toList()) + } + if (context.stockOutLines.isNotEmpty()) { + stockOutLineRepository.saveAll(context.stockOutLines) + } + if (context.stockInLines.isNotEmpty()) { + stockInLineRepository.saveAll(context.stockInLines) + } + if (context.inventoryLotLineById.isNotEmpty()) { + inventoryLotLineRepository.saveAll(context.inventoryLotLineById.values.toList()) + } + if (context.stockLedgers.isNotEmpty()) { + stockLedgerRepository.saveAll(context.stockLedgers) + } +} + +private fun buildBatchAdjustmentCache(records: List): BatchAdjustmentCache { + val pairs = records.mapNotNull { r -> + val warehouseId = r.warehouse?.id + val lotId = r.inventoryLotId ?: r.lotId + if (warehouseId != null && lotId != null) Pair(warehouseId, lotId) else null + }.toSet() + val warehouseIds = pairs.map { it.first }.toSet() + val lotIds = pairs.map { it.second }.toSet() + val lotLines = + if (warehouseIds.isNotEmpty() && lotIds.isNotEmpty()) { + inventoryLotLineRepository.findAllByWarehouseIdInAndInventoryLotIdInAndDeletedIsFalse(warehouseIds, lotIds) + } else { + emptyList() + } + val inventoryLotLineByWarehouseLot = lotLines + .groupBy { Pair(it.warehouse?.id ?: 0L, it.inventoryLot?.id ?: 0L) } + .mapValues { (_, lines) -> lines.maxByOrNull { it.id ?: 0L }!! } + + val inventoryLotIds = lotLines.mapNotNull { it.inventoryLot?.id }.distinct() + val inventoryLotById = + if (inventoryLotIds.isNotEmpty()) { + inventoryLotRepository.findAllByIdIn(inventoryLotIds).associateByNotNull { it.id } + } else { + emptyMap() + } + val itemIds = inventoryLotById.values.mapNotNull { it.item?.id }.distinct() + val inventoryByItemId = + if (itemIds.isNotEmpty()) { + inventoryRepository.findAllByItemIdInAndDeletedIsFalse(itemIds) + .groupBy { it.item?.id ?: 0L } + .mapValues { (_, list) -> list.minByOrNull { it.id ?: Long.MAX_VALUE }!! } + } else { + emptyMap() + } + val recordIds = records.mapNotNull { it.id }.distinct() + val stockTakeLineByRecordId = + if (recordIds.isNotEmpty()) { + stockTakeLineRepository.findAllByStockTakeRecord_IdInAndDeletedIsFalse(recordIds) + .associateByNotNull { it.stockTakeRecord?.id } + } else { + emptyMap() + } + + return BatchAdjustmentCache( + inventoryLotLineByWarehouseLot = inventoryLotLineByWarehouseLot, + inventoryLotById = inventoryLotById, + inventoryByItemId = inventoryByItemId, + stockTakeLineByRecordId = stockTakeLineByRecordId + ) +} + +private inline fun Iterable.associateByNotNull(keySelector: (T) -> K?): Map { + val destination = LinkedHashMap() + for (element in this) { + val key = keySelector(element) ?: continue + destination[key] = element } + return destination } open fun updateStockTakeRecordStatusToNotMatch(stockTakeRecordId: Long): StockTakeRecord { println("updateStockTakeRecordStatusToNotMatch called with stockTakeRecordId: $stockTakeRecordId") diff --git a/src/main/resources/db/changelog/changes/20260429_01_Enson/01_alter_stock_take.sql b/src/main/resources/db/changelog/changes/20260429_01_Enson/01_alter_stock_take.sql new file mode 100644 index 0000000..a5be2c2 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260429_01_Enson/01_alter_stock_take.sql @@ -0,0 +1,8 @@ +--liquibase formatted sql + +--changeset Enson:20260429-01 +CREATE INDEX idx_stock_out_stockTakeId_deleted +ON stock_out (stockTakeId, deleted); + +CREATE INDEX idx_stock_take_line_record_deleted +ON stock_take_line (stockTakeRecordId, deleted); \ No newline at end of file