Pārlūkot izejas kodu

update stock take batch handle efficient

production
CANCERYS\kw093 pirms 1 nedēļas
vecāks
revīzija
777d962f12
4 mainītis faili ar 393 papildinājumiem un 82 dzēšanām
  1. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt
  2. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt
  3. +383
    -82
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  4. +8
    -0
      src/main/resources/db/changelog/changes/20260429_01_Enson/01_alter_stock_take.sql

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt Parādīt failu

@@ -40,6 +40,7 @@ interface InventoryRepository: AbstractRepository<Inventory, Long> {
fun findInventoryInfoByItemInAndDeletedIsFalse(items: List<Items>): List<InventoryInfo>

fun findByItemId(itemId: Long): Optional<Inventory>
fun findAllByItemIdInAndDeletedIsFalse(itemIds: Collection<Long>): List<Inventory>

@Query("SELECT i FROM Inventory i WHERE i.item.id = :itemId AND i.deleted = false")
fun findAllByItemIdAndDeletedIsFalse(itemId: Long): List<Inventory>


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt Parādīt failu

@@ -9,6 +9,7 @@ interface StockTakeLineRepository : AbstractRepository<StockTakeLine, Long> {
fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeLine?;
fun findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId: Serializable, stockTakeId: Serializable): StockTakeLine?;
fun findByStockTakeRecord_IdAndDeletedIsFalse(stockTakeRecordId: Long): StockTakeLine?
fun findAllByStockTakeRecord_IdInAndDeletedIsFalse(stockTakeRecordIds: Collection<Long>): List<StockTakeLine>
fun findAllByStockTakeIdInAndInventoryLotLineIdInAndDeletedIsFalse(
stockTakeIds: Collection<Long>,
inventoryLotLineIds: Collection<Long>


+ 383
- 82
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Parādīt failu

@@ -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<String>()
val processedStockTakes = mutableSetOf<Pair<Long, String>>()
val prepareStartNs = System.nanoTime()
val recordsToPersist = mutableListOf<StockTakeRecord>()
val postPersistActions = mutableListOf<Triple<StockTakeRecord, BigDecimal, BigDecimal>>()

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<Pair<Long, Long>, InventoryLotLine>,
val inventoryLotById: Map<Long, InventoryLot>,
val inventoryByItemId: Map<Long, com.ffii.fpsms.modules.stock.entity.Inventory>,
val stockTakeLineByRecordId: Map<Long, StockTakeLine>
)

private data class BatchAdjustmentRuntimeCache(
val stockOutByStockTakeId: MutableMap<Long, StockOut>,
val stockInByStockTakeId: MutableMap<Long, StockIn>,
val runningLedgerBalanceByItemId: MutableMap<Long, Double>
)

private data class StockTakeAdjustmentBatchContext(
val stockTakeLineByRecordId: MutableMap<Long, StockTakeLine> = LinkedHashMap(),
val stockOutLines: MutableList<StockOutLine> = mutableListOf(),
val stockInLines: MutableList<StockInLine> = mutableListOf(),
val inventoryLotLineById: MutableMap<Long, InventoryLotLine> = LinkedHashMap(),
val stockLedgers: MutableList<StockLedger> = 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<StockTakeRecord>): 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 <T : Any, K : Any> Iterable<T>.associateByNotNull(keySelector: (T) -> K?): Map<K, T> {
val destination = LinkedHashMap<K, T>()
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")


+ 8
- 0
src/main/resources/db/changelog/changes/20260429_01_Enson/01_alter_stock_take.sql Parādīt failu

@@ -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);

Notiek ielāde…
Atcelt
Saglabāt