| @@ -364,187 +364,83 @@ class StockTakeRecordService( | |||||
| return RecordsRes(paginatedResult, filteredResults.size) | return RecordsRes(paginatedResult, filteredResults.size) | ||||
| } | } | ||||
| open fun AllApproverStockTakeList(): List<AllPickedStockTakeListReponse> { | 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 = "", | 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 | stockTakeSectionDescription = null | ||||
| ) | ) | ||||
| } | |||||
| return roundCards.sortedByDescending { it.planStartDate ?: LocalDate.MIN } | |||||
| ) | |||||
| } | } | ||||
| open fun getInventoryLotDetailsByWarehouseCode(warehouseCode: String): List<InventoryLotDetailResponse> { | open fun getInventoryLotDetailsByWarehouseCode(warehouseCode: String): List<InventoryLotDetailResponse> { | ||||