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