From a7ea2bcc5f41b5166d076af73f5ef04efd9d7f33 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Thu, 12 Feb 2026 14:46:48 +0800 Subject: [PATCH] update --- .../service/PickExecutionIssueService.kt | 96 +- .../modules/report/service/ReportService.kt | 98 +- .../stock/entity/StockOutLIneRepository.kt | 6 + .../stock/service/StockOutLineService.kt | 18 +- .../stock/service/StockTakeRecordService.kt | 1026 +++++++++-------- 5 files changed, 676 insertions(+), 568 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt index 15979b2..4ae2896 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt @@ -155,21 +155,15 @@ open class PickExecutionIssueService( // 4. 计算 issueQty(实际的问题数量) val issueQty = when { - // 情况1: 已拣完但有坏品 - actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { - println(" Case 1: actualPickQty == requiredQty && badItemQty > 0") - println(" issueQty = badItemQty = $badItemQty") - badItemQty // issueQty = badItemQty - } - badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> { - println(" Case 2: badReason == 'package_problem' && badItemQty > 0") - println(" issueQty = badItemQty = $badItemQty") + // Bad item 或 bad package:一律用用户输入的 bad 数量,不用 bookQty - actualPickQty + badItemQty > BigDecimal.ZERO -> { + println(" Bad item/package: issueQty = badItemQty = $badItemQty") badItemQty } + // 仅 miss/少拣:用短少数量 actualPickQty < requiredQty -> { - println(" Case 3: actualPickQty < requiredQty") val calculatedIssueQty = bookQty.subtract(actualPickQty) - println(" issueQty = bookQty - actualPickQty = $bookQty - $actualPickQty = $calculatedIssueQty") + println(" Miss/short: issueQty = bookQty - actualPickQty = $bookQty - $actualPickQty = $calculatedIssueQty") if (missQty > BigDecimal.ZERO && missQty > calculatedIssueQty) { println("⚠️ Warning: User reported missQty ($missQty) exceeds calculated issueQty ($calculatedIssueQty)") println(" BookQty: $bookQty, ActualPickQty: $actualPickQty") @@ -177,8 +171,7 @@ open class PickExecutionIssueService( calculatedIssueQty } else -> { - println(" Case 4: Default case") - println(" issueQty = 0") + println(" Default: issueQty = 0") BigDecimal.ZERO } } @@ -255,14 +248,23 @@ open class PickExecutionIssueService( println(" hasMissItemWithPartialPick: $hasMissItemWithPartialPick") if (!isMissItemOnly && !hasMissItemWithPartialPick) { - // 只有非 miss item 的情况才更新 issueQty - val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO - val newIssueQty = currentIssueQty.add(issueQty) - inventoryLotLine.issueQty = newIssueQty - inventoryLotLine.modified = LocalDateTime.now() - inventoryLotLine.modifiedBy = "system" - inventoryLotLineRepository.saveAndFlush(inventoryLotLine) - println("✅ Updated inventory_lot_line ${request.lotId} issueQty: $currentIssueQty -> $newIssueQty") + // Bad item only(含 package_problem):lot.issueQty = 本次输入数量(= bad_item_qty / issue_qty),不累加 + val isBadItemOnly = badItemQty > BigDecimal.ZERO && missQty == BigDecimal.ZERO + if (isBadItemOnly) { + inventoryLotLine.issueQty = issueQty // = badItemQty,与 pick_execution_issue.bad_item_qty / issue_qty 一致 + inventoryLotLine.modified = LocalDateTime.now() + inventoryLotLine.modifiedBy = "system" + inventoryLotLineRepository.saveAndFlush(inventoryLotLine) + println("✅ Updated inventory_lot_line ${request.lotId} issueQty = input qty only: $issueQty (bad item only / package problem)") + } else { + val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO + val newIssueQty = currentIssueQty.add(issueQty) + inventoryLotLine.issueQty = newIssueQty + inventoryLotLine.modified = LocalDateTime.now() + inventoryLotLine.modifiedBy = "system" + inventoryLotLineRepository.saveAndFlush(inventoryLotLine) + println("✅ Updated inventory_lot_line ${request.lotId} issueQty: $currentIssueQty -> $newIssueQty") + } } else { println("⏭️ Skipped updating issueQty for miss item (lot ${request.lotId})") } @@ -2706,13 +2708,40 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse // ✅ 新增:如果提交数量为0,只标记issue为已处理,不创建stock_out_line if (submitQty == BigDecimal.ZERO) { println("ℹ️ Submit quantity is 0 - marking issues as handled without creating stock out") - - // Mark all issues as handled + val isMissItem = request.issueType == "miss" + + // Bad + 有 lotId:重置 lot(issueQty=0, status=AVAILABLE),并释放该 lot 下 rejected 的 stock out line + if (!isMissItem && request.lotId != null) { + val lotLine = inventoryLotLineRepository.findById(request.lotId).orElse(null) + if (lotLine != null) { + lotLine.issueQty = BigDecimal.ZERO + lotLine.status = InventoryLotLineStatus.AVAILABLE + lotLine.modified = LocalDateTime.now() + lotLine.modifiedBy = "system" + inventoryLotLineRepository.saveAndFlush(lotLine) + updateInventoryAfterLotLineChange(lotLine) + println("✅ Reset lot ${request.lotId}: issueQty=0, status=AVAILABLE") + } + val rejectedLines = stockOutLineRepository + .findByInventoryLotLineIdAndStatusAndDeletedFalse( + request.lotId, + StockOutLineStatus.REJECTED.status + ) + if (rejectedLines.isNotEmpty()) { + rejectedLines.forEach { sol -> + sol.status = "pending" + sol.modified = LocalDateTime.now() + sol.modifiedBy = "system" + } + stockOutLineRepository.saveAllAndFlush(rejectedLines) + println("✅ Released ${rejectedLines.size} rejected stock out line(s) for lot ${request.lotId} to pending") + } + } + issues.forEach { issue -> println(" Marking issue ${issue.id} as handled by handler $handler") markIssueHandled(issue, handler) } - println("✅ All issues marked as handled (no stock out created)") return MessageResponse( id = null, @@ -2776,12 +2805,19 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse println("Created stock out line: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}") if (!isMissItem && request.lotId != null) { - val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null) - if (lotLineForReset != null) { - val oldIssueQty = lotLineForReset.issueQty - lotLineForReset.issueQty = BigDecimal.ZERO - inventoryLotLineRepository.saveAndFlush(lotLineForReset) - println("✅ Reset issueQty for lot ${request.lotId}: $oldIssueQty -> 0") + val rejectedLines = stockOutLineRepository + .findByInventoryLotLineIdAndStatusAndDeletedFalse( + request.lotId, + StockOutLineStatus.REJECTED.status + ) + if (rejectedLines.isNotEmpty()) { + rejectedLines.forEach { sol -> + sol.status = "pending" // 或 StockOutLineStatus.PENDING.status(若有该枚举) + sol.modified = LocalDateTime.now() + sol.modifiedBy = "system" + } + stockOutLineRepository.saveAllAndFlush(rejectedLines) + println("✅ Released ${rejectedLines.size} rejected stock out line(s) for lot ${request.lotId} to pending") } } diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index bb09b19..99e6654 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -766,12 +766,11 @@ fun searchMaterialStockOutTraceabilityReport( return jdbcDao.queryForList(sql, args) } - /** * Queries the database for Stock Balance Report data. - * Shows stock balances by item code and lot number, including opening balance, - * cumulative stock in/out, current balance, store locations, and last in/out dates. - * Opening balance comes from stock_ledger where type = 'adj'. + * Shows stock balances by item code and lot number (per-lot quantities from inventory_lot_line), + * including opening balance (item-level from stock_ledger type='adj'), cumulative stock in/out, + * current balance, store locations, and last in/out dates per lot. */ fun searchStockBalanceReport( stockCategory: String?, @@ -802,19 +801,19 @@ fun searchMaterialStockOutTraceabilityReport( COALESCE(uc.code, '') as unitOfMeasure, COALESCE(il.lotNo, sil.lotNo, '') as lotNo, COALESCE(DATE_FORMAT(COALESCE(il.expiryDate, sil.expiryDate), '%Y-%m-%d'), '') as expiryDate, - FORMAT(ROUND(COALESCE(opening_bal.openingBalance, 0), 0), 0) as openingBalance, - FORMAT(ROUND(COALESCE(cum_in.cumStockIn, 0), 0), 0) as cumStockIn, - FORMAT(ROUND(COALESCE(cum_out.cumStockOut, 0), 0), 0) as cumStockOut, - FORMAT(ROUND(COALESCE(opening_bal.openingBalance, 0) + COALESCE(cum_in.cumStockIn, 0) - COALESCE(cum_out.cumStockOut, 0), 0), 0) as currentBalance, + FORMAT(ROUND(COALESCE(lot_agg.lotOpening, 0), 0), 0) as openingBalance, + FORMAT(ROUND(COALESCE(lot_agg.lotCumIn, 0), 0), 0) as cumStockIn, + FORMAT(ROUND(COALESCE(lot_agg.lotCumOut, 0), 0), 0) as cumStockOut, + FORMAT(ROUND(COALESCE(lot_agg.lotOpening, 0) + COALESCE(lot_agg.lotCumIn, 0) - COALESCE(lot_agg.lotCumOut, 0), 0), 0) as currentBalance, '' as reOrderLevel, '' as reOrderQty, COALESCE(GROUP_CONCAT(DISTINCT wh.code ORDER BY wh.code SEPARATOR ', '), '') as storeLocation, - COALESCE(DATE_FORMAT(cum_in.lastInDate, '%Y-%m-%d'), '') as lastInDate, - COALESCE(DATE_FORMAT(cum_out.lastOutDate, '%Y-%m-%d'), '') as lastOutDate, - FORMAT(ROUND(SUM(COALESCE(opening_bal.openingBalance, 0)) OVER (PARTITION BY it.code), 0), 0) as totalOpeningBalance, - FORMAT(ROUND(SUM(COALESCE(cum_in.cumStockIn, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCumStockIn, - FORMAT(ROUND(SUM(COALESCE(cum_out.cumStockOut, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCumStockOut, - FORMAT(ROUND(SUM(COALESCE(opening_bal.openingBalance, 0) + COALESCE(cum_in.cumStockIn, 0) - COALESCE(cum_out.cumStockOut, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCurrentBalance + COALESCE(DATE_FORMAT(lot_agg.lotLastInDate, '%Y-%m-%d'), '') as lastInDate, + COALESCE(DATE_FORMAT(lot_agg.lotLastOutDate, '%Y-%m-%d'), '') as lastOutDate, + FORMAT(ROUND(MAX(COALESCE(opening_bal.openingBalance, 0)) OVER (PARTITION BY it.code), 0), 0) as totalOpeningBalance, + FORMAT(ROUND(SUM(COALESCE(lot_agg.lotCumIn, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCumStockIn, + FORMAT(ROUND(SUM(COALESCE(lot_agg.lotCumOut, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCumStockOut, + FORMAT(ROUND(SUM(COALESCE(lot_agg.lotOpening, 0) + COALESCE(lot_agg.lotCumIn, 0) - COALESCE(lot_agg.lotCumOut, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCurrentBalance FROM inventory_lot il LEFT JOIN items it ON il.itemId = it.id AND it.deleted = false LEFT JOIN stock_in_line sil ON il.stockInLineId = sil.id AND sil.deleted = false @@ -824,84 +823,85 @@ fun searchMaterialStockOutTraceabilityReport( LEFT JOIN uom_conversion uc ON iu.uomId = uc.id LEFT JOIN ( SELECT - sl.itemCode, - SUM(COALESCE(sl.balance, 0)) as openingBalance - FROM stock_ledger sl - WHERE sl.deleted = false - AND sl.type = 'adj' - AND sl.itemCode IS NOT NULL - AND sl.itemCode != '' - GROUP BY sl.itemCode - ) opening_bal ON it.code = opening_bal.itemCode + ill_agg.inventoryLotId, + SUM(COALESCE(ill_agg.inQty, 0)) as lotCumIn, + SUM(COALESCE(ill_agg.outQty, 0)) as lotCumOut, + NULL as lotOpening, + last_in.lotLastInDate, + last_out.lotLastOutDate + FROM inventory_lot_line ill_agg + LEFT JOIN ( + SELECT sil2.inventoryLotId, MAX(sil2.receiptDate) as lotLastInDate + FROM stock_in_line sil2 + WHERE sil2.deleted = false AND sil2.inventoryLotId IS NOT NULL + GROUP BY sil2.inventoryLotId + ) last_in ON last_in.inventoryLotId = ill_agg.inventoryLotId + LEFT JOIN ( + SELECT ill2.inventoryLotId, MAX(sol.endTime) as lotLastOutDate + FROM stock_out_line sol + INNER JOIN inventory_lot_line ill2 ON sol.inventoryLotLineId = ill2.id AND ill2.deleted = false + WHERE sol.deleted = false + GROUP BY ill2.inventoryLotId + ) last_out ON last_out.inventoryLotId = ill_agg.inventoryLotId + WHERE ill_agg.deleted = false + GROUP BY ill_agg.inventoryLotId, last_in.lotLastInDate, last_out.lotLastOutDate + ) lot_agg ON lot_agg.inventoryLotId = il.id LEFT JOIN ( SELECT sl.itemCode, - SUM(COALESCE(sl.inQty, 0)) as cumStockIn, - MAX(CASE WHEN sl.inQty > 0 THEN sl.date ELSE NULL END) as lastInDate - FROM stock_ledger sl - WHERE sl.deleted = false - AND sl.itemCode IS NOT NULL - AND sl.itemCode != '' - AND COALESCE(sl.inQty, 0) > 0 - GROUP BY sl.itemCode - ) cum_in ON it.code = cum_in.itemCode - LEFT JOIN ( - SELECT - sl.itemCode, - SUM(COALESCE(sl.outQty, 0)) as cumStockOut, - MAX(CASE WHEN sl.outQty > 0 THEN sl.date ELSE NULL END) as lastOutDate + SUM(COALESCE(sl.balance, 0)) as openingBalance FROM stock_ledger sl WHERE sl.deleted = false + AND LOWER(sl.type) = 'adj' AND sl.itemCode IS NOT NULL AND sl.itemCode != '' - AND COALESCE(sl.outQty, 0) > 0 GROUP BY sl.itemCode - ) cum_out ON it.code = cum_out.itemCode + ) opening_bal ON it.code = opening_bal.itemCode WHERE il.deleted = false $stockCategorySql $itemCodeSql $storeLocationSql - GROUP BY it.code, it.name, uc.code, il.lotNo, sil.lotNo, il.expiryDate, sil.expiryDate, - opening_bal.openingBalance, cum_in.cumStockIn, cum_in.lastInDate, - cum_out.cumStockOut, cum_out.lastOutDate + GROUP BY it.code, it.name, uc.code, il.id, il.lotNo, sil.lotNo, il.expiryDate, sil.expiryDate, + lot_agg.lotCumIn, lot_agg.lotCumOut, lot_agg.lotOpening, lot_agg.lotLastInDate, lot_agg.lotLastOutDate, + opening_bal.openingBalance HAVING 1=1 """.trimIndent() - // Apply filters that need to be in HAVING clause val havingConditions = mutableListOf() + val lotCurrentBalanceExpr = "(COALESCE(lot_agg.lotOpening, 0) + COALESCE(lot_agg.lotCumIn, 0) - COALESCE(lot_agg.lotCumOut, 0))" if (!balanceFilterStart.isNullOrBlank()) { args["balanceFilterStart"] = balanceFilterStart.toDoubleOrNull() ?: 0.0 - havingConditions.add("(COALESCE(opening_bal.openingBalance, 0) + COALESCE(cum_in.cumStockIn, 0) - COALESCE(cum_out.cumStockOut, 0)) >= :balanceFilterStart") + havingConditions.add("$lotCurrentBalanceExpr >= :balanceFilterStart") } if (!balanceFilterEnd.isNullOrBlank()) { args["balanceFilterEnd"] = balanceFilterEnd.toDoubleOrNull() ?: 0.0 - havingConditions.add("(COALESCE(opening_bal.openingBalance, 0) + COALESCE(cum_in.cumStockIn, 0) - COALESCE(cum_out.cumStockOut, 0)) <= :balanceFilterEnd") + havingConditions.add("$lotCurrentBalanceExpr <= :balanceFilterEnd") } if (!lastInDateStart.isNullOrBlank()) { val formattedDate = lastInDateStart.replace("/", "-") args["lastInDateStart"] = formattedDate - havingConditions.add("(cum_in.lastInDate IS NOT NULL AND DATE(cum_in.lastInDate) >= DATE(:lastInDateStart))") + havingConditions.add("(lot_agg.lotLastInDate IS NOT NULL AND DATE(lot_agg.lotLastInDate) >= DATE(:lastInDateStart))") } if (!lastInDateEnd.isNullOrBlank()) { val formattedDate = lastInDateEnd.replace("/", "-") args["lastInDateEnd"] = formattedDate - havingConditions.add("(cum_in.lastInDate IS NOT NULL AND DATE(cum_in.lastInDate) <= DATE(:lastInDateEnd))") + havingConditions.add("(lot_agg.lotLastInDate IS NOT NULL AND DATE(lot_agg.lotLastInDate) <= DATE(:lastInDateEnd))") } if (!lastOutDateStart.isNullOrBlank()) { val formattedDate = lastOutDateStart.replace("/", "-") args["lastOutDateStart"] = formattedDate - havingConditions.add("(cum_out.lastOutDate IS NOT NULL AND DATE(cum_out.lastOutDate) >= DATE(:lastOutDateStart))") + havingConditions.add("(lot_agg.lotLastOutDate IS NOT NULL AND DATE(lot_agg.lotLastOutDate) >= DATE(:lastOutDateStart))") } if (!lastOutDateEnd.isNullOrBlank()) { val formattedDate = lastOutDateEnd.replace("/", "-") args["lastOutDateEnd"] = formattedDate - havingConditions.add("(cum_out.lastOutDate IS NOT NULL AND DATE(cum_out.lastOutDate) <= DATE(:lastOutDateEnd))") + havingConditions.add("(lot_agg.lotLastOutDate IS NOT NULL AND DATE(lot_agg.lotLastOutDate) <= DATE(:lastOutDateEnd))") } val finalSql = if (havingConditions.isNotEmpty()) { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt index 540a61a..fe492f7 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt @@ -80,4 +80,10 @@ fun findAllByPickOrderLineIdInAndInventoryLotLineIdInAndDeletedFalse( fun findAllByInventoryLotLineIdInAndNotCompletedOrRejected( @Param("inventoryLotLineIds") inventoryLotLineIds: List ): List + + +fun findByInventoryLotLineIdAndStatusAndDeletedFalse( + inventoryLotLineId: Long, + status: String +): List } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index fb040df..871acc2 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt @@ -1263,14 +1263,22 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { // inventoryRepository.saveAll(inventories.values.toList()) // ✅ 修复:批处理完成后,检查所有受影响的 pick order lines 是否应该标记为完成 - val affectedPickOrderLineIds = request.lines + val affectedPickOrderIds = request.lines .mapNotNull { line -> - stockOutLines[line.stockOutLineId]?.pickOrderLine?.id + stockOutLines[line.stockOutLineId]?.pickOrderLine?.pickOrder?.id } .distinct() - - println("=== Checking ${affectedPickOrderLineIds.size} affected pick order lines after batch submit ===") - affectedPickOrderLineIds.forEach { pickOrderLineId -> + + val allPickOrderLineIdsToCheck = if (affectedPickOrderIds.isNotEmpty()) { + affectedPickOrderIds.flatMap { pickOrderId -> + pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId).mapNotNull { it.id } + }.distinct() + } else { + emptyList() + } + + println("=== Checking ${allPickOrderLineIdsToCheck.size} pick order lines (all lines of affected pick orders) after batch submit ===") + allPickOrderLineIdsToCheck.forEach { pickOrderLineId -> try { checkIsStockOutLineCompleted(pickOrderLineId) } catch (e: Exception) { 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 cbc64f5..a845b3d 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 @@ -16,7 +16,6 @@ import java.math.BigDecimal import com.ffii.fpsms.modules.user.entity.UserRepository import org.springframework.data.domain.PageRequest import com.ffii.core.response.RecordsRes -import java.time.LocalDate import com.ffii.fpsms.modules.stock.service.InventoryLotLineService import com.ffii.fpsms.modules.stock.entity.StockTakeLine import com.ffii.fpsms.modules.stock.entity.StockTakeLineRepository @@ -41,6 +40,8 @@ import java.sql.ResultSet import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository 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 @Service class StockTakeRecordService( val stockTakeRepository: StockTakeRepository, @@ -68,56 +69,56 @@ class StockTakeRecordService( .mapNotNull { it.stockTakeSection } .distinct() .filter { !it.isBlank() } - + if (distinctSections.isEmpty()) { return emptyList() } - - + + val warehousesBySection = allWarehouses .filter { it.stockTakeSection != null && !it.stockTakeSection!!.isBlank() } .groupBy { it.stockTakeSection!! } - + val allStockTakes = stockTakeRepository.findAll() .filter { !it.deleted } .groupBy { it.stockTakeSection } - + val allStockTakeRecords = stockTakeRecordRepository.findAll() .filter { !it.deleted } - + val recordsByWarehouseId = allStockTakeRecords .filter { it.warehouse?.id != null } .groupBy { it.warehouse!!.id } - + val result = mutableListOf() var idCounter = 1L - + // 3. 为每个 stockTakeSection 创建一个卡片 distinctSections.forEach { stockTakeSection -> // 4. 获取该 section 下的所有 warehouse val warehouses = warehousesBySection[stockTakeSection] ?: emptyList() val warehouseIds = warehouses.mapNotNull { it.id } - + if (warehouseIds.isEmpty()) { return@forEach } - + // 5. 获取该 section 相关的所有 stock_take 记录 val stockTakesForSection = allStockTakes[stockTakeSection] ?: emptyList() - + // 6. 获取 lastStockTakeDate:从 completed 状态的记录中,按 actualEnd 排序,取最新的 val completedStockTakes = stockTakesForSection .filter { it.status == StockTakeStatus.COMPLETED && it.actualEnd != null } .sortedByDescending { it.actualEnd } - + val lastStockTakeDate = completedStockTakes.firstOrNull()?.actualEnd - - + + val latestStockTake = stockTakesForSection .maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } - + val status = if (latestStockTake != null) { latestStockTake.status?.value ?: "pending" } else { @@ -126,13 +127,13 @@ class StockTakeRecordService( val stockTakerName = if (latestStockTake != null) { // 从该 stock take 的 records 中获取最新的 stockTakerName val recordsForStockTake = allStockTakeRecords - .filter { + .filter { it.stockTake?.id == latestStockTake.id && - it.stockTakeSection == stockTakeSection && - it.stockTakerName != null + it.stockTakeSection == stockTakeSection && + it.stockTakerName != null } .sortedByDescending { it.stockTakeStartTime ?: LocalDateTime.MIN } - + recordsForStockTake.firstOrNull()?.stockTakerName } else { null @@ -140,13 +141,13 @@ class StockTakeRecordService( val approverName = if (latestStockTake != null) { // 从该 stock take 的 records 中获取最新的 approverName val recordsForStockTake = allStockTakeRecords - .filter { + .filter { it.stockTake?.id == latestStockTake.id && - it.stockTakeSection == stockTakeSection && - it.approverName != null + it.stockTakeSection == stockTakeSection && + it.approverName != null } .sortedByDescending { it.stockTakeStartTime ?: LocalDateTime.MIN } - + recordsForStockTake.firstOrNull()?.approverName } else { null @@ -159,9 +160,9 @@ class StockTakeRecordService( // 检查该 stock take 下该 section 的记录中是否有 notMatch 状态 val hasNotMatch = allStockTakeRecords.any { !it.deleted && - it.stockTake?.id == latestStockTake.id && - it.stockTakeSection == stockTakeSection && - it.status == "notMatch" + it.stockTake?.id == latestStockTake.id && + it.stockTakeSection == stockTakeSection && + it.status == "notMatch" } hasNotMatch } else { @@ -172,9 +173,9 @@ class StockTakeRecordService( id = idCounter++, stockTakeSession = stockTakeSection, lastStockTakeDate = latestStockTake?.actualStart?.toLocalDate(), - status = status?:"", - currentStockTakeItemNumber = 0, - totalInventoryLotNumber = totalInventoryLotNumber, + status = status ?: "", + currentStockTakeItemNumber = 0, + totalInventoryLotNumber = totalInventoryLotNumber, stockTakeId = latestStockTake?.id ?: 0, stockTakerName = stockTakerName, TotalItemNumber = totalItemNumber, @@ -186,9 +187,10 @@ class StockTakeRecordService( ) ) } - + return result.sortedBy { it.stockTakeSession } } + open fun AllApproverStockTakeList(): List { // 1. 获取所有不同的 stockTakeSection(从 warehouse 表) val allWarehouses = warehouseRepository.findAllByDeletedIsFalse() @@ -196,58 +198,58 @@ class StockTakeRecordService( .mapNotNull { it.stockTakeSection } .distinct() .filter { !it.isBlank() } - + if (distinctSections.isEmpty()) { return emptyList() } - + // 2. 批量获取所有相关的数据(优化性能 - 只查询一次) // 2.1 按 section 分组 warehouse val warehousesBySection = allWarehouses .filter { it.stockTakeSection != null && !it.stockTakeSection!!.isBlank() } .groupBy { it.stockTakeSection!! } - + // 2.2 批量获取所有 stock_take 记录(只获取一次),按 stockTakeSection 分组 val allStockTakes = stockTakeRepository.findAll() .filter { !it.deleted } .groupBy { it.stockTakeSection } - + // 2.3 批量获取所有 stocktakeRecord(只获取一次) val allStockTakeRecords = stockTakeRecordRepository.findAll() .filter { !it.deleted } - + // 2.4 按 warehouseId 分组 stocktakeRecord,便于快速查找 val recordsByWarehouseId = allStockTakeRecords .filter { it.warehouse?.id != null } .groupBy { it.warehouse!!.id } - + val result = mutableListOf() var idCounter = 1L - + // 3. 为每个 stockTakeSection 创建一个卡片 distinctSections.forEach { stockTakeSection -> // 4. 获取该 section 下的所有 warehouse val warehouses = warehousesBySection[stockTakeSection] ?: emptyList() val warehouseIds = warehouses.mapNotNull { it.id } - + if (warehouseIds.isEmpty()) { return@forEach } - + // 5. 获取该 section 相关的所有 stock_take 记录 val stockTakesForSection = allStockTakes[stockTakeSection] ?: emptyList() - + // 6. 获取 lastStockTakeDate:从 completed 状态的记录中,按 actualEnd 排序,取最新的 val completedStockTakes = stockTakesForSection .filter { it.status == StockTakeStatus.COMPLETED && it.actualEnd != null } .sortedByDescending { it.actualEnd } - + val lastStockTakeDate = completedStockTakes.firstOrNull()?.actualEnd?.toLocalDate() - + // 7. 获取 status:获取最新的 stock_take 记录(按 actualStart 或 planStart 排序) val latestStockTake = stockTakesForSection .maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } - + // 8. 确定 status:只有 APPROVING 或 COMPLETED 状态才输出,其他为 null val status = if (latestStockTake != null) { val stockTakeStatus = latestStockTake.status @@ -263,13 +265,13 @@ class StockTakeRecordService( val stockTakerName = if (latestStockTake != null) { // 从该 stock take 的 records 中获取最新的 stockTakerName val recordsForStockTake = allStockTakeRecords - .filter { + .filter { it.stockTake?.id == latestStockTake.id && - it.stockTakeSection == stockTakeSection && - it.stockTakerName != null + it.stockTakeSection == stockTakeSection && + it.stockTakerName != null } .sortedByDescending { it.stockTakeStartTime ?: LocalDateTime.MIN } - + recordsForStockTake.firstOrNull()?.stockTakerName } else { null @@ -277,13 +279,13 @@ class StockTakeRecordService( val approverName = if (latestStockTake != null) { // 从该 stock take 的 records 中获取最新的 approverName val recordsForStockTake = allStockTakeRecords - .filter { + .filter { it.stockTake?.id == latestStockTake.id && - it.stockTakeSection == stockTakeSection && - it.approverName != null + it.stockTakeSection == stockTakeSection && + it.approverName != null } .sortedByDescending { it.stockTakeStartTime ?: LocalDateTime.MIN } - + recordsForStockTake.firstOrNull()?.approverName } else { null @@ -292,9 +294,9 @@ class StockTakeRecordService( // 检查该 stock take 下该 section 的记录中是否有 notMatch 状态 val hasNotMatch = allStockTakeRecords.any { !it.deleted && - it.stockTake?.id == latestStockTake.id && - it.stockTakeSection == stockTakeSection && - it.status == "notMatch" + it.stockTake?.id == latestStockTake.id && + it.stockTakeSection == stockTakeSection && + it.status == "notMatch" } hasNotMatch } else { @@ -309,7 +311,7 @@ class StockTakeRecordService( id = idCounter++, stockTakeSession = stockTakeSection, lastStockTakeDate = latestStockTake?.actualStart?.toLocalDate(), - status = status?:"", + status = status ?: "", currentStockTakeItemNumber = 0, // 临时设为 0,测试性能 totalInventoryLotNumber = totalInventoryLotNumber, // 临时设为 0,测试性能 stockTakeId = latestStockTake?.id ?: 0, @@ -318,24 +320,25 @@ class StockTakeRecordService( TotalItemNumber = totalItemNumber, startTime = latestStockTake?.actualStart, endTime = latestStockTake?.actualEnd, - + ReStockTakeTrueFalse = reStockTakeTrueFalse ) ) } - + return result.sortedBy { it.stockTakeSession } } + open fun getInventoryLotDetailsByWarehouseCode(warehouseCode: String): List { println("getInventoryLotDetailsByWarehouseCode called with code: $warehouseCode") - + // 1. 先查找 warehouse,可能有多个相同 code 的记录,选择 id 最小的(最早的) val warehouses = warehouseRepository.findAllByCodeAndDeletedIsFalse(warehouseCode) if (warehouses.isEmpty()) { logger.warn("Warehouse not found with code: $warehouseCode") return emptyList() } - + // 按 id 排序,取第一个(id 最小的,即最早的) val warehouse = warehouses.minByOrNull { it.id ?: Long.MAX_VALUE } if (warehouse == null) { @@ -343,20 +346,21 @@ class StockTakeRecordService( return emptyList() } println("Found warehouse: id=${warehouse.id}, code=${warehouse.code} (total ${warehouses.size} found, using earliest)") - + // 2. 根据 warehouse id 查找所有相关的 InventoryLotLine - val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(listOf(warehouse.id!!)) + val inventoryLotLines = + inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(listOf(warehouse.id!!)) println("Found ${inventoryLotLines.size} inventory lot lines") - + // 3. 转换为 Response return inventoryLotLines.map { ill -> val inventoryLot = ill.inventoryLot val item = inventoryLot?.item val warehouse = ill.warehouse - val availableQty = (ill.inQty ?: BigDecimal.ZERO) + val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) .subtract(ill.holdQty ?: BigDecimal.ZERO) - + InventoryLotDetailResponse( id = ill.id ?: 0L, inventoryLotId = inventoryLot?.id ?: 0L, @@ -375,7 +379,7 @@ class StockTakeRecordService( warehouseCode = warehouse?.code, warehouseName = warehouse?.name, status = ill.status?.name, - stockTakeRecordStatus = null, + stockTakeRecordStatus = null, stockTakeRecordId = null, firstStockTakeQty = null, secondStockTakeQty = null, @@ -383,8 +387,8 @@ class StockTakeRecordService( varianceQty = null, approverBadQty = null, finalQty = null, - firstBadQty = null, - secondBadQty = null, + firstBadQty = null, + secondBadQty = null, remarks = null, warehouseSlot = warehouse?.slot, warehouseArea = warehouse?.area, @@ -393,103 +397,108 @@ class StockTakeRecordService( ) } } + open fun getInventoryLotDetailsByStockTakeSection( - stockTakeSection: String, - stockTakeId: Long? = null, - pageNum: Int = 0, - pageSize: Int = 10 -): RecordsRes { - println("getInventoryLotDetailsByStockTakeSection called with section: $stockTakeSection, stockTakeId: $stockTakeId, pageNum: $pageNum, pageSize: $pageSize") - - val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection) - if (warehouses.isEmpty()) { - logger.warn("No warehouses found for stockTakeSection: $stockTakeSection") - return RecordsRes(emptyList(), 0) - } - - val warehouseIds = warehouses.mapNotNull { it.id } - println("Found ${warehouses.size} warehouses for section $stockTakeSection") - - val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) - println("Found ${inventoryLotLines.size} inventory lot lines") - - val stockTakeRecordsMap = if (stockTakeId != null) { - val allStockTakeRecords = stockTakeRecordRepository.findAll() - .filter { - !it.deleted && - it.stockTake?.id == stockTakeId && - it.warehouse?.id in warehouseIds + stockTakeSection: String, + stockTakeId: Long? = null, + pageNum: Int = 0, + pageSize: Int = 10 + ): RecordsRes { + println("getInventoryLotDetailsByStockTakeSection called with section: $stockTakeSection, stockTakeId: $stockTakeId, pageNum: $pageNum, pageSize: $pageSize") + + val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection) + if (warehouses.isEmpty()) { + logger.warn("No warehouses found for stockTakeSection: $stockTakeSection") + return RecordsRes(emptyList(), 0) + } + + val warehouseIds = warehouses.mapNotNull { it.id } + println("Found ${warehouses.size} warehouses for section $stockTakeSection") + + val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) + println("Found ${inventoryLotLines.size} inventory lot lines") + + val stockTakeRecordsMap = if (stockTakeId != null) { + val allStockTakeRecords = stockTakeRecordRepository.findAll() + .filter { + !it.deleted && + it.stockTake?.id == stockTakeId && + it.warehouse?.id in warehouseIds + } + // 按 lotId 和 warehouseId 建立映射 + allStockTakeRecords.associateBy { + Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L) } - // 按 lotId 和 warehouseId 建立映射 - allStockTakeRecords.associateBy { - Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L) + } else { + emptyMap() } - } else { - emptyMap() - } - - val allResults = inventoryLotLines.map { ill -> - val inventoryLot = ill.inventoryLot - val item = inventoryLot?.item - val warehouse = ill.warehouse - val availableQty = (ill.inQty ?: BigDecimal.ZERO) - .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) - - val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) { - stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)] + + val allResults = inventoryLotLines.map { ill -> + val inventoryLot = ill.inventoryLot + val item = inventoryLot?.item + val warehouse = ill.warehouse + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + .subtract(ill.holdQty ?: BigDecimal.ZERO) + + val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) { + stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)] + } else { + null + } + val inventoryLotLineId = ill.id + val stockTakeLine = stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse( + inventoryLotLineId, + stockTakeId!! + ) + InventoryLotDetailResponse( + id = ill.id ?: 0L, + inventoryLotId = inventoryLot?.id ?: 0L, + itemId = item?.id ?: 0L, + itemCode = item?.code, + itemName = item?.name, + lotNo = inventoryLot?.lotNo, + expiryDate = inventoryLot?.expiryDate, + productionDate = inventoryLot?.productionDate, + stockInDate = inventoryLot?.stockInDate, + inQty = ill.inQty, + remarks = stockTakeRecord?.remarks, + outQty = ill.outQty, + holdQty = ill.holdQty, + availableQty = availableQty, + uom = ill.stockUom?.uom?.udfudesc, + warehouseCode = warehouse?.code, + warehouseName = warehouse?.name, + status = ill.status?.name, + warehouseSlot = warehouse?.slot, + warehouseArea = warehouse?.area, + warehouse = warehouse?.warehouse, + varianceQty = stockTakeRecord?.varianceQty, + stockTakeRecordId = stockTakeRecord?.id, + stockTakeRecordStatus = stockTakeRecord?.status, + firstStockTakeQty = stockTakeRecord?.pickerFirstStockTakeQty, + secondStockTakeQty = stockTakeRecord?.pickerSecondStockTakeQty, + firstBadQty = stockTakeRecord?.pickerFirstBadQty, + secondBadQty = stockTakeRecord?.pickerSecondBadQty, + approverQty = stockTakeRecord?.approverStockTakeQty, + approverBadQty = stockTakeRecord?.approverBadQty, + finalQty = stockTakeLine?.finalQty, + ) + } + + // 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 { - null + emptyList() } - val inventoryLotLineId = ill.id - val stockTakeLine = stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId, stockTakeId!!) - InventoryLotDetailResponse( - id = ill.id ?: 0L, - inventoryLotId = inventoryLot?.id ?: 0L, - itemId = item?.id ?: 0L, - itemCode = item?.code, - itemName = item?.name, - lotNo = inventoryLot?.lotNo, - expiryDate = inventoryLot?.expiryDate, - productionDate = inventoryLot?.productionDate, - stockInDate = inventoryLot?.stockInDate, - inQty = ill.inQty, - remarks = stockTakeRecord?.remarks, - outQty = ill.outQty, - holdQty = ill.holdQty, - availableQty = availableQty, - uom = ill.stockUom?.uom?.udfudesc, - warehouseCode = warehouse?.code, - warehouseName = warehouse?.name, - status = ill.status?.name, - warehouseSlot = warehouse?.slot, - warehouseArea = warehouse?.area, - warehouse = warehouse?.warehouse, - varianceQty = stockTakeRecord?.varianceQty, - stockTakeRecordId = stockTakeRecord?.id, - stockTakeRecordStatus = stockTakeRecord?.status, - firstStockTakeQty = stockTakeRecord?.pickerFirstStockTakeQty, - secondStockTakeQty = stockTakeRecord?.pickerSecondStockTakeQty, - firstBadQty = stockTakeRecord?.pickerFirstBadQty, - secondBadQty = stockTakeRecord?.pickerSecondBadQty, - approverQty = stockTakeRecord?.approverStockTakeQty , - approverBadQty = stockTakeRecord?.approverBadQty, - finalQty = stockTakeLine?.finalQty, - ) - } - - // 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) } - - return RecordsRes(paginatedResult, allResults.size) -} + open fun saveStockTakeRecord( request: SaveStockTakeRecordRequest, stockTakeId: Long, @@ -500,7 +509,7 @@ class StockTakeRecordService( // 1. 获取 inventory lot line val inventoryLotLine = inventoryLotLineRepository.findByIdAndDeletedIsFalse(request.inventoryLotLineId) ?: throw IllegalArgumentException("Inventory lot line not found: ${request.inventoryLotLineId}") - + val inventoryLot = inventoryLotLine.inventoryLot ?: throw IllegalArgumentException("Inventory lot not found") val item = inventoryLot.item @@ -509,31 +518,31 @@ class StockTakeRecordService( ?: throw IllegalArgumentException("Warehouse not found") val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) ?: throw IllegalArgumentException("Stock take not found: $stockTakeId") - + // 2. 计算 availableQty val availableQty = (inventoryLotLine.inQty ?: BigDecimal.ZERO) .subtract(inventoryLotLine.outQty ?: BigDecimal.ZERO) .subtract(inventoryLotLine.holdQty ?: BigDecimal.ZERO) - + // 3. 判断是创建还是更新 val stockTakeRecord = if (request.stockTakeRecordId != null) { // 更新现有记录(第二次盘点) val existingRecord = stockTakeRecordRepository.findByIdAndDeletedIsFalse(request.stockTakeRecordId) ?: throw IllegalArgumentException("Stock take record not found: ${request.stockTakeRecordId}") - + // 第二次盘点:允许不匹配,但根据匹配情况设置状态 val totalInputQty = request.qty.add(request.badQty) val isMatched = totalInputQty.compareTo(availableQty) == 0 - val varianceQty = availableQty-request.qty-request.badQty + val varianceQty = availableQty - request.qty - request.badQty // 更新字段(第二次盘点) existingRecord.apply { this.pickerSecondStockTakeQty = request.qty this.pickerSecondBadQty = request.badQty // 更新 badQty - this.status ="pass" + this.status = "pass" this.remarks = request.remark ?: this.remarks this.stockTakerName = user?.name this.stockTakeEndTime = java.time.LocalDateTime.now() - + this.varianceQty = varianceQty } existingRecord @@ -541,8 +550,8 @@ class StockTakeRecordService( val totalInputQty = request.qty.add(request.badQty) val isMatched = totalInputQty.compareTo(availableQty) == 0 - val isCompled=inventoryLotLine.inQty==inventoryLotLine.outQty - val varianceQty = availableQty-request.qty-request.badQty + val isCompled = inventoryLotLine.inQty == inventoryLotLine.outQty + val varianceQty = availableQty - request.qty - request.badQty StockTakeRecord().apply { this.itemId = item.id this.lotId = inventoryLot.id @@ -558,7 +567,7 @@ class StockTakeRecordService( this.varianceQty = varianceQty this.uom = inventoryLotLine.stockUom?.uom?.udfudesc this.date = java.time.LocalDate.now() - this.status = if (isCompled) "completed" else "pass" + this.status = if (isCompled) "completed" else "pass" this.remarks = request.remark this.itemCode = item.code this.itemName = item.name @@ -567,19 +576,19 @@ class StockTakeRecordService( this.stockTakeStartTime = java.time.LocalDateTime.now() } } - - + + val savedRecord = stockTakeRecordRepository.save(stockTakeRecord) if (request.stockTakeRecordId == null) { val existingRecordsCount = stockTakeRecordRepository.findAll() - .filter { - !it.deleted && - it.stockTake?.id == stockTakeId && - it.id != savedRecord.id // 排除刚创建的这条记录 + .filter { + !it.deleted && + it.stockTake?.id == stockTakeId && + it.id != savedRecord.id // 排除刚创建的这条记录 } .count() - - + + if (existingRecordsCount == 0 && stockTake.actualStart == null) { stockTake.actualStart = java.time.LocalDateTime.now() stockTake.status = StockTakeStatus.STOCKTAKING @@ -587,265 +596,274 @@ class StockTakeRecordService( println("Stock take $stockTakeId actualStart updated - first record created") } } - - val stockTakeSection = savedRecord.stockTakeSection - if (stockTakeSection != null) { - checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection) - } - - return savedRecord -} - - -open fun batchSaveStockTakeRecords( - request: BatchSaveStockTakeRecordRequest -): BatchSaveStockTakeRecordResponse { - - println("batchSaveStockTakeRecords called for section: ${request.stockTakeSection}, stockTakeId: ${request.stockTakeId}") - val user = userRepository.findById(request.stockTakerId).orElse(null) - // 1. 获取 stock take - val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) - ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") - - // 2. 使用 stockTakeSection 获取所有 inventory lot lines(类似 getInventoryLotDetailsByStockTakeSection) - val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(request.stockTakeSection) - if (warehouses.isEmpty()) { - logger.warn("No warehouses found for stockTakeSection: ${request.stockTakeSection}") - return BatchSaveStockTakeRecordResponse(0, 0, listOf("No warehouses found for section: ${request.stockTakeSection}")) - } - - val warehouseIds = warehouses.mapNotNull { it.id } - println("Found ${warehouses.size} warehouses for section ${request.stockTakeSection}") - - // 3. 批量获取所有相关的 InventoryLotLine - val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) - println("Found ${inventoryLotLines.size} inventory lot lines") - - // 4. 使用 stockTakeId 获取已创建的记录,建立映射以排除它们 - val existingRecordsMap = stockTakeRecordRepository.findAll() - .filter { - !it.deleted && - it.stockTake?.id == request.stockTakeId && - it.warehouse?.id in warehouseIds - } - .associateBy { - Pair(it.inventoryLotId ?: 0L, it.warehouse?.id ?: 0L) - } - - println("Found ${existingRecordsMap.size} existing stock take records") - - // 5. 找出未创建的记录 - val uncreatedLines = inventoryLotLines.filter { ill -> - val inventoryLot = ill.inventoryLot - val warehouse = ill.warehouse - if (inventoryLot?.id == null || warehouse?.id == null) { - false - } else { - val key = Pair(inventoryLot.id, warehouse.id) - !existingRecordsMap.containsKey(key) + val stockTakeSection = savedRecord.stockTakeSection + if (stockTakeSection != null) { + checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection) } + + return savedRecord } - - println("Found ${uncreatedLines.size} uncreated inventory lot lines to process") - - if (uncreatedLines.isEmpty()) { - return BatchSaveStockTakeRecordResponse(0, 0, listOf("No uncreated records found")) - } - - // 6. 批量创建记录 - var successCount = 0 - var errorCount = 0 - val errors = mutableListOf() - - uncreatedLines.forEach { ill -> - try { - val inventoryLot = ill.inventoryLot - ?: throw IllegalArgumentException("Inventory lot not found") - val itemEntity = inventoryLot.item - ?: throw IllegalArgumentException("Item not found") - val warehouse = ill.warehouse - ?: throw IllegalArgumentException("Warehouse not found") - - // 计算 availableQty - val availableQty = (ill.inQty ?: BigDecimal.ZERO) - .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) - - // 使用 availableQty 作为 qty,badQty 为 0 - val qty = availableQty - val badQty = BigDecimal.ZERO - - // 判断是否匹配 - val totalInputQty = qty.add(badQty) - val isMatched = totalInputQty.compareTo(availableQty) == 0 - val varianceQty = availableQty-qty-badQty - val isCompleted = (ill.inQty ?: BigDecimal.ZERO) == (ill.outQty ?: BigDecimal.ZERO) - // 创建新记录 - val stockTakeRecord = StockTakeRecord().apply { - this.itemId = itemEntity.id - this.lotId = inventoryLot.id - this.warehouse = warehouse - this.stockTake = stockTake - this.stockTakeSection = request.stockTakeSection - this.inventoryLotId = inventoryLot.id - this.stockTakerId = request.stockTakerId - this.stockTakerName = user?.name - this.pickerFirstStockTakeQty = qty - this.pickerFirstBadQty = badQty - this.bookQty = availableQty - this.varianceQty = varianceQty - this.uom = ill.stockUom?.uom?.udfudesc - this.date = java.time.LocalDate.now() - this.status = if (isCompleted) "completed" else "pass" - this.remarks = null - this.itemCode = itemEntity.code - this.itemName = itemEntity.name - this.lotNo = inventoryLot.lotNo - this.expiredDate = inventoryLot.expiryDate - this.stockTakeStartTime = java.time.LocalDateTime.now() - this.stockTakeEndTime = java.time.LocalDateTime.now() - } - - stockTakeRecordRepository.save(stockTakeRecord) - successCount++ - } catch (e: Exception) { - errorCount++ - val errorMsg = "Error saving inventoryLotLineId ${ill.id}: ${e.message}" - errors.add(errorMsg) - logger.error(errorMsg, e) - } - } - if (successCount > 0) { - val existingRecordsCount = stockTakeRecordRepository.findAll() - .filter { - !it.deleted && - it.stockTake?.id == request.stockTakeId - } - .count() - checkAndUpdateStockTakeStatus(request.stockTakeId, request.stockTakeSection) - } - println("batchSaveStockTakeRecords completed: success=$successCount, errors=$errorCount") - return BatchSaveStockTakeRecordResponse( - successCount = successCount, - errorCount = errorCount, - errors = errors - ) -} -open fun checkAndUpdateStockTakeStatus(stockTakeId: Long, stockTakeSection: String): Map { - try { - val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) - ?: return mapOf("success" to false, "message" to "Stock take not found") - - // 1. 获取该 section 下的所有 warehouse - val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection) + + open fun batchSaveStockTakeRecords( + request: BatchSaveStockTakeRecordRequest + ): BatchSaveStockTakeRecordResponse { + + println("batchSaveStockTakeRecords called for section: ${request.stockTakeSection}, stockTakeId: ${request.stockTakeId}") + val user = userRepository.findById(request.stockTakerId).orElse(null) + // 1. 获取 stock take + val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) + ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") + + // 2. 使用 stockTakeSection 获取所有 inventory lot lines(类似 getInventoryLotDetailsByStockTakeSection) + val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(request.stockTakeSection) if (warehouses.isEmpty()) { - return mapOf("success" to false, "message" to "No warehouses found for section") + logger.warn("No warehouses found for stockTakeSection: ${request.stockTakeSection}") + return BatchSaveStockTakeRecordResponse( + 0, + 0, + listOf("No warehouses found for section: ${request.stockTakeSection}") + ) } - + val warehouseIds = warehouses.mapNotNull { it.id } - - // 2. 获取该 section 下的所有 inventory lot lines + println("Found ${warehouses.size} warehouses for section ${request.stockTakeSection}") + + // 3. 批量获取所有相关的 InventoryLotLine val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) - - // 3. 获取该 stock take 下该 section 的所有记录 - val stockTakeRecords = stockTakeRecordRepository.findAll() - .filter { - !it.deleted && - it.stockTake?.id == stockTakeId && - it.warehouse?.id in warehouseIds && - it.stockTakeSection == stockTakeSection + println("Found ${inventoryLotLines.size} inventory lot lines") + + // 4. 使用 stockTakeId 获取已创建的记录,建立映射以排除它们 + val existingRecordsMap = stockTakeRecordRepository.findAll() + .filter { + !it.deleted && + it.stockTake?.id == request.stockTakeId && + it.warehouse?.id in warehouseIds } - - // 4. 检查是否所有 inventory lot lines 都有对应的记录 - val allLinesHaveRecords = inventoryLotLines.all { ill -> + .associateBy { + Pair(it.inventoryLotId ?: 0L, it.warehouse?.id ?: 0L) + } + + println("Found ${existingRecordsMap.size} existing stock take records") + + // 5. 找出未创建的记录 + val uncreatedLines = inventoryLotLines.filter { ill -> val inventoryLot = ill.inventoryLot val warehouse = ill.warehouse if (inventoryLot?.id == null || warehouse?.id == null) { false } else { - stockTakeRecords.any { record -> - record.inventoryLotId == inventoryLot.id && - record.warehouse?.id == warehouse.id + val key = Pair(inventoryLot.id, warehouse.id) + !existingRecordsMap.containsKey(key) + } + } + + println("Found ${uncreatedLines.size} uncreated inventory lot lines to process") + + if (uncreatedLines.isEmpty()) { + return BatchSaveStockTakeRecordResponse(0, 0, listOf("No uncreated records found")) + } + + // 6. 批量创建记录 + var successCount = 0 + var errorCount = 0 + val errors = mutableListOf() + + uncreatedLines.forEach { ill -> + try { + val inventoryLot = ill.inventoryLot + ?: throw IllegalArgumentException("Inventory lot not found") + val itemEntity = inventoryLot.item + ?: throw IllegalArgumentException("Item not found") + val warehouse = ill.warehouse + ?: throw IllegalArgumentException("Warehouse not found") + + // 计算 availableQty + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + .subtract(ill.holdQty ?: BigDecimal.ZERO) + + // 使用 availableQty 作为 qty,badQty 为 0 + val qty = availableQty + val badQty = BigDecimal.ZERO + + // 判断是否匹配 + val totalInputQty = qty.add(badQty) + val isMatched = totalInputQty.compareTo(availableQty) == 0 + val varianceQty = availableQty - qty - badQty + val isCompleted = (ill.inQty ?: BigDecimal.ZERO) == (ill.outQty ?: BigDecimal.ZERO) + // 创建新记录 + val stockTakeRecord = StockTakeRecord().apply { + this.itemId = itemEntity.id + this.lotId = inventoryLot.id + this.warehouse = warehouse + this.stockTake = stockTake + this.stockTakeSection = request.stockTakeSection + this.inventoryLotId = inventoryLot.id + this.stockTakerId = request.stockTakerId + this.stockTakerName = user?.name + this.pickerFirstStockTakeQty = qty + this.pickerFirstBadQty = badQty + this.bookQty = availableQty + this.varianceQty = varianceQty + this.uom = ill.stockUom?.uom?.udfudesc + this.date = java.time.LocalDate.now() + this.status = if (isCompleted) "completed" else "pass" + this.remarks = null + this.itemCode = itemEntity.code + this.itemName = itemEntity.name + this.lotNo = inventoryLot.lotNo + this.expiredDate = inventoryLot.expiryDate + this.stockTakeStartTime = java.time.LocalDateTime.now() + this.stockTakeEndTime = java.time.LocalDateTime.now() } + + stockTakeRecordRepository.save(stockTakeRecord) + successCount++ + } catch (e: Exception) { + errorCount++ + val errorMsg = "Error saving inventoryLotLineId ${ill.id}: ${e.message}" + errors.add(errorMsg) + logger.error(errorMsg, e) } } - - - val allRecordsPassed = stockTakeRecords.isNotEmpty() && - stockTakeRecords.all { it.status == "pass" || it.status == "completed" } - val allRecordsCompleted = stockTakeRecords.isNotEmpty() && - stockTakeRecords.all { it.status == "completed" } - // 6. 如果所有记录都已创建且都是 "pass",更新 stock take 状态为 "approving" - if (allLinesHaveRecords && allRecordsCompleted) { - stockTake.status = StockTakeStatus.COMPLETED - stockTake.planEnd = java.time.LocalDateTime.now() - stockTakeRepository.save(stockTake) - println("Stock take $stockTakeId status updated to COMPLETED - all records are completed") - return mapOf("success" to true, "message" to "Stock take status updated to COMPLETED", "updated" to true) - } else if (allLinesHaveRecords && allRecordsPassed) { - // 如果所有记录都已创建且都是 "pass" 或 "completed",更新 stock take 状态为 "approving" - stockTake.status = StockTakeStatus.APPROVING - stockTake.actualEnd = java.time.LocalDateTime.now() - stockTakeRepository.save(stockTake) - - println("Stock take $stockTakeId status updated to APPROVING - all records are pass") - return mapOf( - "success" to true, - "message" to "Stock take status updated to APPROVING", - "updated" to true - ) - } else { - return mapOf( - "success" to true, - "message" to "Conditions not met for status update", - "updated" to false, - "allLinesHaveRecords" to allLinesHaveRecords, - "allRecordsPassed" to allRecordsPassed, - "allRecordsCompleted" to allRecordsCompleted - ) + if (successCount > 0) { + val existingRecordsCount = stockTakeRecordRepository.findAll() + .filter { + !it.deleted && + it.stockTake?.id == request.stockTakeId + } + .count() + checkAndUpdateStockTakeStatus(request.stockTakeId, request.stockTakeSection) } - } catch (e: Exception) { - logger.error("Error checking and updating stock take status for stockTakeId: $stockTakeId", e) - return mapOf("success" to false, "message" to "Error: ${e.message}") + println("batchSaveStockTakeRecords completed: success=$successCount, errors=$errorCount") + return BatchSaveStockTakeRecordResponse( + successCount = successCount, + errorCount = errorCount, + errors = errors + ) } -} -open fun saveApproverStockTakeRecord( + + open fun checkAndUpdateStockTakeStatus(stockTakeId: Long, stockTakeSection: String): Map { + try { + val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) + ?: return mapOf("success" to false, "message" to "Stock take not found") + + // 1. 获取该 section 下的所有 warehouse + val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection) + if (warehouses.isEmpty()) { + return mapOf("success" to false, "message" to "No warehouses found for section") + } + + val warehouseIds = warehouses.mapNotNull { it.id } + + // 2. 获取该 section 下的所有 inventory lot lines + val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) + + // 3. 获取该 stock take 下该 section 的所有记录 + val stockTakeRecords = stockTakeRecordRepository.findAll() + .filter { + !it.deleted && + it.stockTake?.id == stockTakeId && + it.warehouse?.id in warehouseIds && + it.stockTakeSection == stockTakeSection + } + + // 4. 检查是否所有 inventory lot lines 都有对应的记录 + val allLinesHaveRecords = inventoryLotLines.all { ill -> + val inventoryLot = ill.inventoryLot + val warehouse = ill.warehouse + if (inventoryLot?.id == null || warehouse?.id == null) { + false + } else { + stockTakeRecords.any { record -> + record.inventoryLotId == inventoryLot.id && + record.warehouse?.id == warehouse.id + } + } + } + + + val allRecordsPassed = stockTakeRecords.isNotEmpty() && + stockTakeRecords.all { it.status == "pass" || it.status == "completed" } + val allRecordsCompleted = stockTakeRecords.isNotEmpty() && + stockTakeRecords.all { it.status == "completed" } + // 6. 如果所有记录都已创建且都是 "pass",更新 stock take 状态为 "approving" + if (allLinesHaveRecords && allRecordsCompleted) { + stockTake.status = StockTakeStatus.COMPLETED + stockTake.planEnd = java.time.LocalDateTime.now() + stockTakeRepository.save(stockTake) + println("Stock take $stockTakeId status updated to COMPLETED - all records are completed") + return mapOf( + "success" to true, + "message" to "Stock take status updated to COMPLETED", + "updated" to true + ) + } else if (allLinesHaveRecords && allRecordsPassed) { + // 如果所有记录都已创建且都是 "pass" 或 "completed",更新 stock take 状态为 "approving" + stockTake.status = StockTakeStatus.APPROVING + stockTake.actualEnd = java.time.LocalDateTime.now() + stockTakeRepository.save(stockTake) + + println("Stock take $stockTakeId status updated to APPROVING - all records are pass") + return mapOf( + "success" to true, + "message" to "Stock take status updated to APPROVING", + "updated" to true + ) + } else { + return mapOf( + "success" to true, + "message" to "Conditions not met for status update", + "updated" to false, + "allLinesHaveRecords" to allLinesHaveRecords, + "allRecordsPassed" to allRecordsPassed, + "allRecordsCompleted" to allRecordsCompleted + ) + } + } catch (e: Exception) { + logger.error("Error checking and updating stock take status for stockTakeId: $stockTakeId", e) + return mapOf("success" to false, "message" to "Error: ${e.message}") + } + } + + open fun saveApproverStockTakeRecord( request: SaveApproverStockTakeRecordRequest, stockTakeId: Long ): StockTakeRecord { println("saveApproverStockTakeRecord called with stockTakeRecordId: ${request.stockTakeRecordId}") val user = userRepository.findById(request.approverId ?: 0L).orElse(null) - - + val stockTakeRecord = if (request.stockTakeRecordId != null) { stockTakeRecordRepository.findByIdAndDeletedIsFalse(request.stockTakeRecordId) ?: throw IllegalArgumentException("Stock take record not found: ${request.stockTakeRecordId}") } else { throw IllegalArgumentException("stockTakeRecordId is required for approver") } - + val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) ?: throw IllegalArgumentException("Stock take not found: $stockTakeId") - val finalQty = if (request.approverQty != null && request.approverBadQty != null) { request.approverQty } else { request.qty } - + val finalBadQty = if (request.approverQty != null && request.approverBadQty != null) { request.approverBadQty } else { request.badQty } + val varianceQty = (finalQty ?: BigDecimal.ZERO) - .subtract( stockTakeRecord.bookQty?: BigDecimal.ZERO) + .subtract(stockTakeRecord.bookQty ?: BigDecimal.ZERO) + println("finalQty: $finalQty") println("stockTakeRecord.bookQty: ${stockTakeRecord.bookQty}") println("varianceQty: $varianceQty") + stockTakeRecord.apply { this.approverId = request.approverId this.approverName = user?.name @@ -854,21 +872,25 @@ open fun saveApproverStockTakeRecord( this.varianceQty = varianceQty this.status = "completed" } - - + val savedRecord = stockTakeRecordRepository.save(stockTakeRecord) - - + + // ------- 取当前库存行 / 库存信息 ------- 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") + 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") - if (varianceQty !=BigDecimal.ZERO ) { + val inventory = inventoryRepository.findByItemId( + 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 @@ -876,101 +898,137 @@ open fun saveApproverStockTakeRecord( this.finalQty = finalQty this.status = StockTakeLineStatus.COMPLETED this.completeDate = java.time.LocalDateTime.now() - } stockTakeLineRepository.save(stockTakeLine) - if (varianceQty < BigDecimal.ZERO ) { - 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=(-varianceQty)?.toDouble() - this.stockOut = stockOut - this.inventoryLotLine = inventoryLotLine - this.status = "completed" - this.type = "ADJ" - - } - stockOutLineRepository.save(stockOutLine) - val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()+(varianceQty).toDouble() - val stockLedger = StockLedger().apply { - this.inventory=inventory - this.itemId=inventoryLot.item?.id - this.itemCode=stockTakeRecord.itemCode - this.inQty=null - this.outQty=(-varianceQty)?.toDouble() - this.stockOutLine=stockOutLine - this.balance=newBalance - this.type="Adj" - this.date = LocalDate.now() - } - stockLedgerRepository.save(stockLedger) - } - if (varianceQty > BigDecimal.ZERO ) { - var stockIn = 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 - this.item=inventoryLot.item - this.itemNo=stockTakeRecord.itemCode - this.stockIn = stockIn - this.demandQty=varianceQty - this.acceptedQty=varianceQty - this.expiryDate=inventoryLot.expiryDate - this.inventoryLot=inventoryLot - this.inventoryLotLine=inventoryLotLine - this.lotNo=inventoryLot.lotNo - this.status = "completed" - this.type = "ADJ" + // 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 = "Adj" + 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) } - stockInLineRepository.save(stockInLine) - val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()+varianceQty.toDouble() - val stockLedger = StockLedger().apply { - this.inventory=inventory - this.itemId=inventoryLot.item?.id - this.itemCode=inventoryLot.item?.code - this.inQty=varianceQty?.toDouble() - this.stockInLine=stockInLine - this.outQty=null - this.balance=newBalance - this.type="Adj" - this.date = LocalDate.now() + + // 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 = "ADJ" + // 注意:此时不设置 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 } - stockLedgerRepository.save(stockLedger) + } + val stockTakeSection = savedRecord.stockTakeSection + if (stockTakeSection != null) { + checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection) } - - val updateRequest = SaveInventoryLotLineRequest( - id = inventoryLotLine.id, - inventoryLotId = inventoryLotLine.inventoryLot?.id, - warehouseId = inventoryLotLine.warehouse?.id, - stockUomId = inventoryLotLine.stockUom?.id, - inQty = finalQty, - outQty = inventoryLotLine.outQty, - holdQty = inventoryLotLine.holdQty, - status = inventoryLotLine.status?.value, - remarks = inventoryLotLine.remarks - ) - inventoryLotLineService.saveInventoryLotLine(updateRequest) -} -val stockTakeSection = savedRecord.stockTakeSection -if (stockTakeSection != null) { - checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection) -} return savedRecord }