From 2be079ca2e24e52557f151a04eb8c123733ee30f Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Fri, 27 Feb 2026 19:35:58 +0800 Subject: [PATCH] update stock take --- .../stock/service/StockTakeRecordService.kt | 349 ++++++++++-------- .../modules/stock/service/StockTakeService.kt | 29 +- .../stock/web/model/StockTakeRecordReponse.kt | 2 + 3 files changed, 228 insertions(+), 152 deletions(-) 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 1693aa8..0f31b18 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 @@ -42,6 +42,8 @@ import com.ffii.fpsms.modules.stock.entity.InventoryRepository import com.ffii.fpsms.modules.stock.entity.StockLedger import java.time.LocalDate import com.ffii.fpsms.modules.stock.entity.InventoryLotLine +import java.math.RoundingMode +import com.ffii.fpsms.modules.stock.entity.StockTake @Service class StockTakeRecordService( val stockTakeRepository: StockTakeRepository, @@ -392,7 +394,8 @@ class StockTakeRecordService( remarks = null, warehouseSlot = warehouse?.slot, warehouseArea = warehouse?.area, - warehouse = warehouse?.warehouse + warehouse = warehouse?.warehouse, + bookQty = null, ) } @@ -483,20 +486,26 @@ class StockTakeRecordService( approverQty = stockTakeRecord?.approverStockTakeQty, approverBadQty = stockTakeRecord?.approverBadQty, finalQty = stockTakeLine?.finalQty, + bookQty = stockTakeRecord?.bookQty, ) } // Apply pagination val pageable = PageRequest.of(pageNum, pageSize) - val startIndex = pageable.offset.toInt() - val endIndex = minOf(startIndex + pageSize, allResults.size) - val paginatedResult = if (startIndex < allResults.size) { - allResults.subList(startIndex, endIndex) - } else { - emptyList() - } - - return RecordsRes(paginatedResult, allResults.size) +val startIndex = pageable.offset.toInt() +val filteredResults = allResults.filter { response -> + val av = response.availableQty ?: BigDecimal.ZERO + // 显示: availableQty > 0,或已有盘点记录(即使盘点结果为 0) + av.compareTo(BigDecimal.ZERO) > 0 || response.stockTakeRecordId != null +} +// endIndex 必須基於 filteredResults.size,不能用 allResults.size +val endIndex = minOf(startIndex + pageSize, filteredResults.size) +val paginatedResult = if (startIndex < filteredResults.size) { + filteredResults.subList(startIndex, endIndex) +} else { + emptyList() +} +return RecordsRes(paginatedResult, filteredResults.size) } open fun saveStockTakeRecord( @@ -888,142 +897,10 @@ class StockTakeRecordService( inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found") ).orElse(null) ?: throw IllegalArgumentException("Inventory not found for item") - // 只有 variance != 0 时才做调整 - if (varianceQty != BigDecimal.ZERO) { - // 新增一条 StockTakeLine 记录 - val stockTakeLine = StockTakeLine().apply { - this.stockTake = stockTake - this.inventoryLotLine = inventoryLotLine - this.initialQty = stockTakeRecord.bookQty - this.finalQty = finalQty - this.status = StockTakeLineStatus.COMPLETED - this.completeDate = java.time.LocalDateTime.now() - } - stockTakeLineRepository.save(stockTakeLine) - - // variance < 0:做出库,影响 outQty(减少库存) - if (varianceQty < BigDecimal.ZERO) { - val absVariance = varianceQty.negate() // 正数,表示实际出库数量 - - var stockOut = stockOutRepository.findByStockTakeIdAndDeletedFalse(stockTakeId) - ?: StockOut().apply { - this.type = "stockTake" - this.status = "completed" - this.handler = request.approverId - }.also { stockOutRepository.save(it) } - - val stockOutLine = StockOutLine().apply { - this.item = inventoryLot.item - this.qty = absVariance.toDouble() - this.stockOut = stockOut - this.inventoryLotLine = inventoryLotLine - this.status = "completed" - this.type = "ADJ" - } - stockOutLineRepository.save(stockOutLine) - - val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() - absVariance.toDouble() - val stockLedger = StockLedger().apply { - this.inventory = inventory - this.itemId = inventoryLot.item?.id - this.itemCode = stockTakeRecord.itemCode - this.inQty = null - this.outQty = absVariance.toDouble() - this.stockOutLine = stockOutLine - this.balance = newBalance - this.type = "TKE" - this.date = LocalDate.now() - } - stockLedgerRepository.save(stockLedger) - - // 更新原来这条 InventoryLotLine 的 outQty,而不是 inQty - val newOutQty = (inventoryLotLine.outQty ?: BigDecimal.ZERO).add(absVariance) - val updateRequest = SaveInventoryLotLineRequest( - id = inventoryLotLine.id, - inventoryLotId = inventoryLotLine.inventoryLot?.id, - warehouseId = inventoryLotLine.warehouse?.id, - stockUomId = inventoryLotLine.stockUom?.id, - inQty = inventoryLotLine.inQty, // 不改 inQty - outQty = newOutQty, // 增加出库数量 - holdQty = inventoryLotLine.holdQty, - status = inventoryLotLine.status?.value, - remarks = inventoryLotLine.remarks - ) - inventoryLotLineService.saveInventoryLotLine(updateRequest) - } - - // variance > 0:做入库,创建新 lot / lotLine,StockInLine 指向新 lot / lotLine - if (varianceQty > BigDecimal.ZERO) { - val plusQty = varianceQty - - var stockIn = stockInRepository.findByStockTakeIdAndDeletedFalse(stockTakeId) - ?: StockIn().apply { - this.code = stockTake.code - this.status = "completed" - this.stockTake = stockTake - }.also { stockInRepository.save(it) } - - // 1. 先创建 StockInLine(不设置 inventoryLot,因为还没有创建) - val stockInLine = StockInLine().apply { - this.stockTakeLine = stockTakeLine - this.item = inventoryLot.item - this.itemNo = stockTakeRecord.itemCode - this.stockIn = stockIn - this.demandQty = plusQty - this.acceptedQty = plusQty - this.expiryDate = inventoryLot.expiryDate - this.lotNo = inventoryLot.lotNo - this.status = "completed" - this.type = "TKE" - // 注意:此时不设置 inventoryLot 和 inventoryLotLine,因为还没有创建 - } - stockInLineRepository.save(stockInLine) - - // 2. 创建 InventoryLot(设置 stockInLine) - val newInventoryLot = InventoryLot().apply { - this.item = inventoryLot.item - this.lotNo = inventoryLot.lotNo - this.expiryDate = inventoryLot.expiryDate - this.productionDate = inventoryLot.productionDate - this.stockInDate = LocalDateTime.now() - this.stockInLine = stockInLine // 设置 stockInLine - } - inventoryLotRepository.save(newInventoryLot) - - // 3. 创建 InventoryLotLine - val newInventoryLotLine = InventoryLotLine().apply { - this.inventoryLot = newInventoryLot - this.warehouse = inventoryLotLine.warehouse - this.stockUom = inventoryLotLine.stockUom - this.inQty = plusQty - this.outQty = BigDecimal.ZERO - this.holdQty = BigDecimal.ZERO - this.status = inventoryLotLine.status - this.remarks = "Stock take adjustment (+$plusQty)" - } - inventoryLotLineRepository.save(newInventoryLotLine) - - // 4. 更新 StockInLine,设置 inventoryLot 和 inventoryLotLine - stockInLine.inventoryLot = newInventoryLot - stockInLine.inventoryLotLine = newInventoryLotLine - stockInLineRepository.save(stockInLine) - - val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + plusQty.toDouble() - val stockLedger = StockLedger().apply { - this.inventory = inventory - this.itemId = newInventoryLot.item?.id - this.itemCode = newInventoryLot.item?.code - this.inQty = plusQty.toDouble() - this.outQty = null - this.stockInLine = stockInLine - this.balance = newBalance - this.type = "Adj" - this.date = LocalDate.now() - } - stockLedgerRepository.save(stockLedger) - // 注意:正差额不再修改原来的 inventoryLotLine + // 只有 variance != 0 时才做调整 + if (varianceQty != BigDecimal.ZERO) { + applyVarianceAdjustment(stockTake, stockTakeRecord, finalQty!!, varianceQty, request.approverId) } - } val stockTakeSection = savedRecord.stockTakeSection if (stockTakeSection != null) { checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection) @@ -1083,11 +960,23 @@ open fun batchSaveApproverStockTakeRecords( val bookQty = record.bookQty ?: BigDecimal.ZERO val varianceQty = qty.subtract(bookQty) - - - if (varianceQty.compareTo(BigDecimal.ZERO) != 0) { + + val tolerancePercent = request.variancePercentTolerance ?: BigDecimal.ZERO + val shouldSkip = if (tolerancePercent.compareTo(BigDecimal.ZERO) <= 0) { + varianceQty.compareTo(BigDecimal.ZERO) != 0 // 原有逻辑:仅 variance=0 可保存 + } else { + if (bookQty.compareTo(BigDecimal.ZERO) == 0) { + varianceQty.compareTo(BigDecimal.ZERO) != 0 + } else { + val threshold = bookQty.abs() + .multiply(tolerancePercent) + .divide(BigDecimal("100"), 10, RoundingMode.HALF_UP) + varianceQty.abs().compareTo(threshold) > 0 // |variance| > threshold 则跳过 + } + } + if (shouldSkip) { skippedCount++ - println("Skipping record ${record.id}: variance = $varianceQty (qty=$qty, bookQty=$bookQty)") + println("Skipping record ${record.id}: |variance| > ${tolerancePercent}% of bookQty (variance=$varianceQty, bookQty=$bookQty)") return@forEach } @@ -1102,6 +991,16 @@ open fun batchSaveApproverStockTakeRecords( } stockTakeRecordRepository.save(record) + if (varianceQty != BigDecimal.ZERO) { + try { + applyVarianceAdjustment(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 + } + } successCount++ } catch (e: Exception) { errorCount++ @@ -1124,7 +1023,158 @@ open fun batchSaveApproverStockTakeRecords( ) } +/** + * 根据 variance 调整库存并创建 Stock Ledger。 + * 当 variance != 0 时:创建 StockTakeLine,并根据 variance 正负创建 StockIn/StockOut 及 Ledger。 + */ +private fun applyVarianceAdjustment( + stockTake: StockTake, + stockTakeRecord: StockTakeRecord, + finalQty: BigDecimal, + varianceQty: BigDecimal, + approverId: Long? +) { + if (varianceQty == BigDecimal.ZERO) return + + val inventoryLotLine = inventoryLotLineRepository.findByIdAndDeletedIsFalse( + stockTakeRecord.inventoryLotId ?: throw IllegalArgumentException("Inventory lot ID not found") + ) ?: throw IllegalArgumentException("Inventory lot line not found") + + val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse( + inventoryLotLine.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found") + ) ?: 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") + + // 1. 创建 StockTakeLine + val stockTakeLine = StockTakeLine().apply { + this.stockTake = stockTake + this.inventoryLotLine = inventoryLotLine + this.initialQty = stockTakeRecord.bookQty + this.finalQty = finalQty + this.status = StockTakeLineStatus.COMPLETED + this.completeDate = java.time.LocalDateTime.now() + } + stockTakeLineRepository.save(stockTakeLine) + // 2. variance < 0:出库 + StockLedger + if (varianceQty < BigDecimal.ZERO) { + val absVariance = varianceQty.negate() + + var stockOut = stockOutRepository.findByStockTakeIdAndDeletedFalse(stockTake.id!!) + ?: StockOut().apply { + this.type = "stockTake" + this.status = "completed" + this.handler = approverId + }.also { stockOutRepository.save(it) } + + val stockOutLine = StockOutLine().apply { + this.item = inventoryLot.item + this.qty = absVariance.toDouble() + this.stockOut = stockOut + this.inventoryLotLine = inventoryLotLine + this.status = "completed" + this.type = "TKE" + } + stockOutLineRepository.save(stockOutLine) + + val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() - absVariance.toDouble() + val stockLedger = StockLedger().apply { + this.inventory = inventory + this.itemId = inventoryLot.item?.id + this.itemCode = stockTakeRecord.itemCode + this.inQty = null + this.outQty = absVariance.toDouble() + this.stockOutLine = stockOutLine + this.balance = newBalance + this.type = "TKE" + this.date = LocalDate.now() + } + stockLedgerRepository.save(stockLedger) + + val newOutQty = (inventoryLotLine.outQty ?: BigDecimal.ZERO).add(absVariance) + val updateRequest = SaveInventoryLotLineRequest( + id = inventoryLotLine.id, + inventoryLotId = inventoryLotLine.inventoryLot?.id, + warehouseId = inventoryLotLine.warehouse?.id, + stockUomId = inventoryLotLine.stockUom?.id, + inQty = inventoryLotLine.inQty, + outQty = newOutQty, + holdQty = inventoryLotLine.holdQty, + status = inventoryLotLine.status?.value, + remarks = inventoryLotLine.remarks + ) + inventoryLotLineService.saveInventoryLotLine(updateRequest) + } + + // 3. variance > 0:入库 + StockLedger + if (varianceQty > BigDecimal.ZERO) { + val plusQty = varianceQty + + var stockIn = stockInRepository.findByStockTakeIdAndDeletedFalse(stockTake.id!!) + ?: StockIn().apply { + this.code = stockTake.code + this.status = "completed" + this.stockTake = stockTake + }.also { stockInRepository.save(it) } + + val stockInLine = StockInLine().apply { + this.stockTakeLine = stockTakeLine + this.item = inventoryLot.item + this.itemNo = stockTakeRecord.itemCode + this.stockIn = stockIn + this.demandQty = plusQty + this.acceptedQty = plusQty + this.expiryDate = inventoryLot.expiryDate + this.lotNo = inventoryLot.lotNo + this.status = "completed" + this.type = "TKE" + } + stockInLineRepository.save(stockInLine) + + val newInventoryLot = InventoryLot().apply { + this.item = inventoryLot.item + this.lotNo = inventoryLot.lotNo + this.expiryDate = inventoryLot.expiryDate + this.productionDate = inventoryLot.productionDate + this.stockInDate = LocalDateTime.now() + this.stockInLine = stockInLine + } + inventoryLotRepository.save(newInventoryLot) + + val newInventoryLotLine = InventoryLotLine().apply { + this.inventoryLot = newInventoryLot + this.warehouse = inventoryLotLine.warehouse + this.stockUom = inventoryLotLine.stockUom + this.inQty = plusQty + this.outQty = BigDecimal.ZERO + this.holdQty = BigDecimal.ZERO + this.status = inventoryLotLine.status + this.remarks = "Stock take adjustment (+$plusQty)" + } + inventoryLotLineRepository.save(newInventoryLotLine) + + stockInLine.inventoryLot = newInventoryLot + stockInLine.inventoryLotLine = newInventoryLotLine + stockInLineRepository.save(stockInLine) + + val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + plusQty.toDouble() + val stockLedger = StockLedger().apply { + this.inventory = inventory + this.itemId = newInventoryLot.item?.id + this.itemCode = newInventoryLot.item?.code + this.inQty = plusQty.toDouble() + this.outQty = null + this.stockInLine = stockInLine + this.balance = newBalance + this.type = "TKE" + this.date = LocalDate.now() + } + stockLedgerRepository.save(stockLedger) + } +} open fun updateStockTakeRecordStatusToNotMatch(stockTakeRecordId: Long): StockTakeRecord { println("updateStockTakeRecordStatusToNotMatch called with stockTakeRecordId: $stockTakeRecordId") @@ -1233,6 +1283,7 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch( approverQty = stockTakeRecord.approverStockTakeQty, approverBadQty = stockTakeRecord.approverBadQty, finalQty = stockTakeLine?.finalQty, + bookQty = stockTakeRecord.bookQty, ) } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt index 8ab4a3e..6c28f70 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt @@ -251,7 +251,7 @@ class StockTakeService( val allStockTakes = stockTakeRepository.findAll() .filter { !it.deleted } .groupBy { it.stockTakeSection } - + /* // 3. 为每个 stockTakeSection 检查并创建 distinctSections.forEach { section -> val stockTakesForSection = allStockTakes[section] ?: emptyList() @@ -288,9 +288,32 @@ class StockTakeService( logger.info("Skipped section $section: Has non-completed records") } } - + */ // 移除 null section 处理逻辑,因为 warehouse 表中没有 null 的 stockTakeSection - + distinctSections.forEach { section -> + try { + val now = LocalDateTime.now() + val code = assignStockTakeNo() + + val saveStockTakeReq = SaveStockTakeRequest( + code = code, + planStart = now, + planEnd = now.plusDays(1), + actualStart = null, + actualEnd = null, + status = StockTakeStatus.PENDING.value, + remarks = null, + stockTakeSection = section + ) + + val savedStockTake = saveStockTake(saveStockTakeReq) + result[section] = "Created: ${savedStockTake.code}" + logger.info("Created stock take for section $section: ${savedStockTake.code}") + } catch (e: Exception) { + result[section] = "Error: ${e.message}" + logger.error("Error creating stock take for section $section: ${e.message}") + } + } logger.info("--------- End - Create Stock Take for Sections -------") return result } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt index 4f2cef1..aa41fcd 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt @@ -57,6 +57,7 @@ data class InventoryLotDetailResponse( val approverQty: BigDecimal? = null, val approverBadQty: BigDecimal? = null, val finalQty: BigDecimal? = null, + val bookQty: BigDecimal? = null, ) data class InventoryLotLineListRequest( val warehouseCode: String @@ -94,6 +95,7 @@ data class BatchSaveApproverStockTakeRecordRequest( val stockTakeId: Long, val stockTakeSection: String, val approverId: Long, + val variancePercentTolerance: BigDecimal? = null, ) data class BatchSaveApproverStockTakeRecordResponse(