| @@ -2322,6 +2322,12 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo | |||||
| } | } | ||||
| @Transactional | @Transactional | ||||
| /** Reject picker/approver writes when the stock_take header (or its round) is already closed. */ | |||||
| private fun assertStockTakeWritable(stockTake: StockTake) { | |||||
| if (stockTake.status?.isOpen() == true) return | |||||
| throw IllegalArgumentException("Stock take is closed; cannot save") | |||||
| } | |||||
| open fun saveStockTakeRecord( | open fun saveStockTakeRecord( | ||||
| request: SaveStockTakeRecordRequest, | request: SaveStockTakeRecordRequest, | ||||
| stockTakeId: Long, | stockTakeId: Long, | ||||
| @@ -2341,6 +2347,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo | |||||
| ?: throw IllegalArgumentException("Warehouse not found") | ?: throw IllegalArgumentException("Warehouse not found") | ||||
| val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) | val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) | ||||
| ?: throw IllegalArgumentException("Stock take not found: $stockTakeId") | ?: throw IllegalArgumentException("Stock take not found: $stockTakeId") | ||||
| assertStockTakeWritable(stockTake) | |||||
| // 2. 计算 availableQty | // 2. 计算 availableQty | ||||
| val availableQty = (inventoryLotLine.inQty ?: BigDecimal.ZERO) | val availableQty = (inventoryLotLine.inQty ?: BigDecimal.ZERO) | ||||
| @@ -2356,10 +2363,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo | |||||
| val existingRecord = stockTakeRecordRepository.findByIdAndDeletedIsFalse(request.stockTakeRecordId) | val existingRecord = stockTakeRecordRepository.findByIdAndDeletedIsFalse(request.stockTakeRecordId) | ||||
| ?: throw IllegalArgumentException("Stock take record not found: ${request.stockTakeRecordId}") | ?: throw IllegalArgumentException("Stock take record not found: ${request.stockTakeRecordId}") | ||||
| val headerStatus = stockTake.status | |||||
| val headerStillOpen = headerStatus == StockTakeStatus.PENDING || | |||||
| headerStatus == StockTakeStatus.STOCKTAKING || | |||||
| headerStatus == StockTakeStatus.APPROVING | |||||
| val headerStillOpen = stockTake.status?.isOpen() == true | |||||
| val revisingOpenPass = existingRecord.pickerFirstStockTakeQty != null && | val revisingOpenPass = existingRecord.pickerFirstStockTakeQty != null && | ||||
| existingRecord.pickerSecondStockTakeQty == null && | existingRecord.pickerSecondStockTakeQty == null && | ||||
| existingRecord.approverStockTakeQty == null && | existingRecord.approverStockTakeQty == null && | ||||
| @@ -2576,6 +2580,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo | |||||
| // 1. 获取 stock take | // 1. 获取 stock take | ||||
| val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) | val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) | ||||
| ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") | ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") | ||||
| assertStockTakeWritable(stockTake) | |||||
| // 2. 使用 stockTakeSection 获取所有 inventory lot lines(类似 getInventoryLotDetailsByStockTakeSection) | // 2. 使用 stockTakeSection 获取所有 inventory lot lines(类似 getInventoryLotDetailsByStockTakeSection) | ||||
| val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(request.stockTakeSection) | val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(request.stockTakeSection) | ||||
| @@ -2869,6 +2874,11 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo | |||||
| val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) | val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) | ||||
| ?: throw IllegalArgumentException("Stock take not found: $stockTakeId") | ?: throw IllegalArgumentException("Stock take not found: $stockTakeId") | ||||
| // Prefer the record's own header (Overall may pass a round representative id) | |||||
| val writableHeader = stockTakeRecord.stockTake?.takeIf { it.id != null } | |||||
| ?.let { st -> stockTakeRepository.findByIdAndDeletedIsFalse(st.id!!) ?: st } | |||||
| ?: stockTake | |||||
| assertStockTakeWritable(writableHeader) | |||||
| val (finalQty, varianceQty) = applyApproverSaveToRecord( | val (finalQty, varianceQty) = applyApproverSaveToRecord( | ||||
| stockTakeRecord, | stockTakeRecord, | ||||
| @@ -2885,9 +2895,9 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo | |||||
| // variance != 0:過帳並更新/新建 StockTakeLine;= 0:僅將開輪預建 line 標記完成(不查庫存) | // variance != 0:過帳並更新/新建 StockTakeLine;= 0:僅將開輪預建 line 標記完成(不查庫存) | ||||
| if (varianceQty != BigDecimal.ZERO) { | if (varianceQty != BigDecimal.ZERO) { | ||||
| applyVarianceAdjustment(stockTake, stockTakeRecord, finalQty!!, varianceQty, request.approverId) | |||||
| applyVarianceAdjustment(writableHeader, stockTakeRecord, finalQty!!, varianceQty, request.approverId) | |||||
| } else { | } else { | ||||
| completeStockTakeLineForApproverNoVariance(stockTake, savedRecord, finalQty!!) | |||||
| completeStockTakeLineForApproverNoVariance(writableHeader, savedRecord, finalQty!!) | |||||
| } | } | ||||
| val stockTakeSection = savedRecord.stockTakeSection | val stockTakeSection = savedRecord.stockTakeSection | ||||
| if (stockTakeSection != null) { | if (stockTakeSection != null) { | ||||
| @@ -2908,7 +2918,7 @@ open fun batchSaveApproverStockTakeRecords( | |||||
| // 1. Get stock take | // 1. Get stock take | ||||
| val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) | val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) | ||||
| ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") | ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") | ||||
| assertStockTakeWritable(stockTake) | |||||
| val stockTakeRecords = stockTakeRecordRepository | val stockTakeRecords = stockTakeRecordRepository | ||||
| .findPendingApproverRecordsByStockTakeAndSection( | .findPendingApproverRecordsByStockTakeAndSection( | ||||
| @@ -3029,6 +3039,7 @@ open fun batchSaveApproverStockTakeRecordsAll( | |||||
| val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) | val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) | ||||
| ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") | ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") | ||||
| assertStockTakeWritable(stockTake) | |||||
| val roundStockTakeIds: Set<Long> = resolveRoundStockTakeIds(stockTake) | val roundStockTakeIds: Set<Long> = resolveRoundStockTakeIds(stockTake) | ||||
| @@ -3204,6 +3215,7 @@ open fun batchSaveApproverStockTakeRecordsByIds( | |||||
| val user = userRepository.findById(request.approverId).orElse(null) | val user = userRepository.findById(request.approverId).orElse(null) | ||||
| val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) | val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) | ||||
| ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") | ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") | ||||
| assertStockTakeWritable(stockTake) | |||||
| val idSet = idsToLoad.toSet() | val idSet = idsToLoad.toSet() | ||||
| val stockTakeRecords = stockTakeRecordRepository.findAllById(idsToLoad) | val stockTakeRecords = stockTakeRecordRepository.findAllById(idsToLoad) | ||||