| @@ -364,187 +364,83 @@ class StockTakeRecordService( | |||
| return RecordsRes(paginatedResult, filteredResults.size) | |||
| } | |||
| open fun AllApproverStockTakeList(): List<AllPickedStockTakeListReponse> { | |||
| // 1. 获取 section 与仓库基础数据 | |||
| val allWarehouses = warehouseRepository.findAllByDeletedIsFalse() | |||
| val distinctSections = allWarehouses | |||
| .mapNotNull { it.stockTakeSection } | |||
| .distinct() | |||
| .filter { it.isNotBlank() } | |||
| if (distinctSections.isEmpty()) return emptyList() | |||
| val warehousesBySection = allWarehouses | |||
| .filter { !it.stockTakeSection.isNullOrBlank() } | |||
| .groupBy { it.stockTakeSection!! } | |||
| val sectionByWarehouseId = allWarehouses | |||
| .mapNotNull { w -> w.id?.let { id -> id to (w.stockTakeSection ?: "") } } | |||
| .toMap() | |||
| val allWarehouseIds = allWarehouses.mapNotNull { it.id } | |||
| // Overall 卡:只取“最新一轮”,并且总数口径与 | |||
| // `approverInventoryLotDetailsAll?stockTakeId=...` 保持一致: | |||
| // - availableQty > 0 | |||
| // - 或该 (inventoryLotId, warehouseId) 在该轮次的 stockTakeRecord 已存在(即 stockTakeRecordId != null) | |||
| // 2. 批量加载核心数据(一次拿齐,避免循环中 repository 查询) | |||
| val allStockTakes = stockTakeRepository.findAll() | |||
| .filter { !it.deleted && !it.stockTakeSection.isNullOrBlank() } | |||
| .groupBy { it.stockTakeSection!! } | |||
| val latestStockTakeBySection = allStockTakes.mapValues { (_, takes) -> | |||
| takes.maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } | |||
| } | |||
| val latestStockTakeIds = latestStockTakeBySection.values.mapNotNull { it?.id }.toSet() | |||
| val allStockTakeRecords = if (latestStockTakeIds.isEmpty()) { | |||
| emptyList() | |||
| } else { | |||
| stockTakeRecordRepository.findAllByStockTakeIdInAndDeletedIsFalse(latestStockTakeIds) | |||
| } | |||
| val allInventoryLotLines = if (allWarehouseIds.isEmpty()) { | |||
| emptyList() | |||
| } else { | |||
| inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(allWarehouseIds) | |||
| } | |||
| val inventoryLotLinesBySection = allInventoryLotLines.groupBy { ill -> | |||
| sectionByWarehouseId[ill.warehouse?.id] ?: "" | |||
| } | |||
| // 3. 预计算:同一 stockTake 下已盘点 key、人员名称、notMatch | |||
| val recordKeyByStockTakeId = allStockTakeRecords | |||
| .groupBy { it.stockTake?.id ?: -1L } | |||
| .mapValues { (_, records) -> | |||
| records.mapNotNull { r -> | |||
| val lotId = r.inventoryLotId | |||
| val whId = r.warehouse?.id | |||
| if (lotId != null && whId != null) Pair(lotId, whId) else null | |||
| }.toSet() | |||
| } | |||
| val latestStockTakerNameByStockTakeAndSection = allStockTakeRecords | |||
| .filter { !it.stockTakeSection.isNullOrBlank() && it.stockTake?.id != null && it.stockTakerName != null } | |||
| .groupBy { Pair(it.stockTake!!.id!!, it.stockTakeSection!!) } | |||
| .mapValues { (_, records) -> | |||
| records.maxByOrNull { it.stockTakeStartTime ?: LocalDateTime.MIN }?.stockTakerName | |||
| } | |||
| val latestApproverNameByStockTakeAndSection = allStockTakeRecords | |||
| .filter { !it.stockTakeSection.isNullOrBlank() && it.stockTake?.id != null && it.approverName != null } | |||
| .groupBy { Pair(it.stockTake!!.id!!, it.stockTakeSection!!) } | |||
| .mapValues { (_, records) -> | |||
| records.maxByOrNull { it.stockTakeStartTime ?: LocalDateTime.MIN }?.approverName | |||
| } | |||
| val hasNotMatchByStockTakeAndSection = allStockTakeRecords | |||
| .filter { it.stockTake?.id != null && !it.stockTakeSection.isNullOrBlank() && it.status == "notMatch" } | |||
| .map { Pair(it.stockTake!!.id!!, it.stockTakeSection!!) } | |||
| .toSet() | |||
| val result = mutableListOf<AllPickedStockTakeListReponse>() | |||
| var idCounter = 1L | |||
| val allWarehouses = warehouseRepository.findAllByDeletedIsFalse() | |||
| val warehouseIds = allWarehouses.mapNotNull { it.id } | |||
| if (warehouseIds.isEmpty()) return emptyList() | |||
| // 3. 为每个 stockTakeSection 创建一个卡片 | |||
| distinctSections.forEach { stockTakeSection -> | |||
| // 4. 获取该 section 下的所有 warehouse | |||
| val warehouses = warehousesBySection[stockTakeSection] ?: emptyList() | |||
| val warehouseIds = warehouses.mapNotNull { it.id } | |||
| val allStockTakes = stockTakeRepository.findAll().filter { !it.deleted } | |||
| if (allStockTakes.isEmpty()) return emptyList() | |||
| if (warehouseIds.isEmpty()) { | |||
| return@forEach | |||
| } | |||
| val latestBaseStockTake = allStockTakes | |||
| .maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } | |||
| ?: return emptyList() | |||
| // 5. 获取该 section 相关的所有 stock_take 记录 | |||
| val stockTakesForSection = allStockTakes[stockTakeSection] ?: emptyList() | |||
| val roundStockTakeIds = resolveRoundStockTakeIds(latestBaseStockTake) | |||
| if (roundStockTakeIds.isEmpty()) return emptyList() | |||
| // 6. 获取 lastStockTakeDate:从 completed 状态的记录中,按 actualEnd 排序,取最新的 | |||
| val completedStockTakes = stockTakesForSection | |||
| .filter { it.status == StockTakeStatus.COMPLETED && it.actualEnd != null } | |||
| .sortedByDescending { it.actualEnd } | |||
| // stockTakeRecord 存在性:用来对齐 details 接口的 `stockTakeRecordId != null` | |||
| val roundStockTakeRecords = stockTakeRecordRepository.findAllByStockTakeIdInAndDeletedIsFalse(roundStockTakeIds) | |||
| .filter { it.warehouse?.id in warehouseIds } | |||
| val lastStockTakeDate = completedStockTakes.firstOrNull()?.actualEnd?.toLocalDate() | |||
| val recordKeySet = roundStockTakeRecords.mapNotNull { r -> | |||
| val lotId = r.lotId | |||
| val whId = r.warehouse?.id | |||
| if (lotId != null && whId != null) Pair(lotId, whId) else null | |||
| }.toSet() | |||
| // 7. 获取 status:获取最新的 stock_take 记录(按 actualStart 或 planStart 排序) | |||
| val latestStockTake = stockTakesForSection | |||
| .maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } | |||
| val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) | |||
| // 8. 确定 status:只有 APPROVING 或 COMPLETED 状态才输出,其他为 null | |||
| val status = if (latestStockTake != null) { | |||
| val stockTakeStatus = latestStockTake.status | |||
| // 只有 APPROVING 或 COMPLETED 状态才输出值,其他返回 null | |||
| if (stockTakeStatus == StockTakeStatus.APPROVING || stockTakeStatus == StockTakeStatus.COMPLETED) { | |||
| stockTakeStatus.value | |||
| } else { | |||
| null | |||
| } | |||
| } else { | |||
| null | |||
| } | |||
| val stockTakerName = latestStockTake?.id?.let { latestStockTakerNameByStockTakeAndSection[Pair(it, stockTakeSection)] } | |||
| val approverName = latestStockTake?.id?.let { latestApproverNameByStockTakeAndSection[Pair(it, stockTakeSection)] } | |||
| val reStockTakeTrueFalse = latestStockTake?.id?.let { hasNotMatchByStockTakeAndSection.contains(Pair(it, stockTakeSection)) } ?: false | |||
| // 9. 计算 TotalItemNumber / TotalInventoryLotNumber(只统计“需要盘点的行”): | |||
| // 规则与前端 Picker / Approver 明细一致: | |||
| // - availableQty > 0,或 | |||
| // - 在 latestStockTake 下已经有 stockTakeRecord(即本轮参与盘点的 lot) | |||
| val inventoryLotLinesForSection = inventoryLotLinesBySection[stockTakeSection] ?: emptyList() | |||
| val recordedKeysForLatest = latestStockTake?.id?.let { recordKeyByStockTakeId[it] } ?: emptySet() | |||
| val relevantInventoryLotLines = inventoryLotLinesForSection.filter { ill -> | |||
| val inventoryLot = ill.inventoryLot | |||
| val warehouse = ill.warehouse | |||
| if (inventoryLot?.id == null || warehouse?.id == null || latestStockTake?.id == null) { | |||
| false | |||
| } else { | |||
| val inQty = ill.inQty ?: BigDecimal.ZERO | |||
| val outQty = ill.outQty ?: BigDecimal.ZERO | |||
| val holdQty = ill.holdQty ?: BigDecimal.ZERO | |||
| val availableQty = inQty.subtract(outQty).subtract(holdQty) | |||
| val relevantInventoryLotLines = inventoryLotLines.filter { ill -> | |||
| val inventoryLot = ill.inventoryLot | |||
| val warehouse = ill.warehouse | |||
| val lotId = inventoryLot?.id | |||
| val whId = warehouse?.id | |||
| if (lotId == null || whId == null) return@filter false | |||
| val hasRecordForLatest = recordedKeysForLatest.contains(Pair(inventoryLot.id, warehouse.id)) | |||
| availableQty.compareTo(BigDecimal.ZERO) > 0 || hasRecordForLatest | |||
| } | |||
| } | |||
| val availableQty = (ill.inQty ?: BigDecimal.ZERO) | |||
| .subtract(ill.outQty ?: BigDecimal.ZERO) | |||
| .subtract(ill.holdQty ?: BigDecimal.ZERO) | |||
| val totalItemNumber = relevantInventoryLotLines | |||
| .mapNotNull { it.inventoryLot?.item?.id } | |||
| .distinct() | |||
| .count() | |||
| availableQty.compareTo(BigDecimal.ZERO) > 0 || recordKeySet.contains(Pair(lotId, whId)) | |||
| } | |||
| val totalInventoryLotNumber = relevantInventoryLotLines.size | |||
| val sectionDescription = warehouses | |||
| .mapNotNull { it.stockTakeSectionDescription } | |||
| .distinct() | |||
| .firstOrNull() | |||
| // 9. 使用 stockTakeSection 作为 stockTakeSession | |||
| result.add( | |||
| AllPickedStockTakeListReponse( | |||
| id = idCounter++, | |||
| stockTakeSession = stockTakeSection, | |||
| lastStockTakeDate = latestStockTake?.actualStart?.toLocalDate(), | |||
| status = status ?: "", | |||
| currentStockTakeItemNumber = 0, // 临时设为 0,测试性能 | |||
| totalInventoryLotNumber = totalInventoryLotNumber, // 临时设为 0,测试性能 | |||
| stockTakeId = latestStockTake?.id ?: 0, | |||
| stockTakeRoundId = latestStockTake?.stockTakeRoundId ?: latestStockTake?.id, | |||
| stockTakerName = stockTakerName, | |||
| approverName = approverName, | |||
| TotalItemNumber = totalItemNumber, | |||
| startTime = latestStockTake?.actualStart, | |||
| endTime = latestStockTake?.actualEnd, | |||
| ReStockTakeTrueFalse = reStockTakeTrueFalse, | |||
| planStartDate = latestStockTake?.planStart?.toLocalDate(), | |||
| stockTakeSectionDescription = sectionDescription | |||
| val totalInventoryLotNumber = relevantInventoryLotLines.size | |||
| val totalItemNumber = relevantInventoryLotLines | |||
| .mapNotNull { it.inventoryLot?.item?.id } | |||
| .distinct() | |||
| .count() | |||
| ) | |||
| ) | |||
| } | |||
| val statusValue = latestBaseStockTake.status?.let { st -> | |||
| if (st == StockTakeStatus.APPROVING || st == StockTakeStatus.COMPLETED) st.value else "" | |||
| } ?: "" | |||
| // 10. 以 stockTakeRoundId 聚合为“每轮一张卡”;旧资料 roundId 为空时退回 stockTakeId | |||
| val groupedByRound = result.groupBy { it.stockTakeRoundId ?: it.stockTakeId } | |||
| val roundCards = groupedByRound.values.mapIndexed { idx, cardsInRound -> | |||
| val representative = cardsInRound.maxByOrNull { it.startTime ?: LocalDateTime.MIN } ?: cardsInRound.first() | |||
| val totalItems = cardsInRound.sumOf { it.TotalItemNumber } | |||
| val totalLots = cardsInRound.sumOf { it.totalInventoryLotNumber } | |||
| val anyNotMatch = roundStockTakeRecords.any { it.status == "notMatch" } | |||
| representative.copy( | |||
| id = (idx + 1).toLong(), | |||
| return listOf( | |||
| AllPickedStockTakeListReponse( | |||
| id = 1L, | |||
| stockTakeSession = "", | |||
| TotalItemNumber = totalItems, | |||
| totalInventoryLotNumber = totalLots, | |||
| lastStockTakeDate = latestBaseStockTake.actualStart?.toLocalDate(), | |||
| status = statusValue, | |||
| currentStockTakeItemNumber = 0, | |||
| totalInventoryLotNumber = totalInventoryLotNumber, | |||
| stockTakeId = latestBaseStockTake.id ?: 0, | |||
| stockTakeRoundId = latestBaseStockTake.stockTakeRoundId ?: latestBaseStockTake.id, | |||
| stockTakerName = null, | |||
| approverName = null, | |||
| TotalItemNumber = totalItemNumber, | |||
| startTime = latestBaseStockTake.actualStart, | |||
| endTime = latestBaseStockTake.actualEnd, | |||
| ReStockTakeTrueFalse = anyNotMatch, | |||
| planStartDate = latestBaseStockTake.planStart?.toLocalDate(), | |||
| stockTakeSectionDescription = null | |||
| ) | |||
| } | |||
| return roundCards.sortedByDescending { it.planStartDate ?: LocalDate.MIN } | |||
| ) | |||
| } | |||
| open fun getInventoryLotDetailsByWarehouseCode(warehouseCode: String): List<InventoryLotDetailResponse> { | |||