diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index abfba71..128c38c 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt @@ -315,7 +315,7 @@ open class DeliveryOrderService( } /** - * DO 輕量搜尋 v2:車線關鍵字正規化(`車線-X`/`x`/`車線-` 前綴含未指派)、 + * DO 輕量搜索 v2:車線關鍵字正規化(`車線-X`/`x`/`車線-` 前綴含未指派)、 * 以允許供應商 + 分批掃描取代單次 100000 筆載入;無車線條件時等同 [searchDoLiteByPage] 無車線分支。 */ open fun searchDoLiteByPageV2( diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt index a407689..27841b6 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt @@ -3,7 +3,7 @@ package com.ffii.fpsms.modules.deliveryOrder.service import java.util.Locale /** - * 車線搜尋正規化(search-do-lite-v2)。 + * 車線搜索正規化(search-do-lite-v2)。 * 實際 [com.ffii.fpsms.modules.pickOrder.entity.Truck] 編碼主要為 `車線-…` 或 `P06B_…`;未指派預設列為 `車線-X`(shopId 可為 null)。 */ object TruckLaneSearchSpec { 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 fffbe62..635c8cc 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 @@ -631,6 +631,21 @@ open class StockTakeRecordService( return LatestStockTakeRoundMetaResponse(stockTakeRoundId = roundKey, planStartDate = planStart) } + /** + * 與盤點員列表一致:先取全系統 MAX(stockTakeRoundId),再在該輪內選一筆代表 stock_take(供審核 API 解析整輪)。 + */ + private fun resolveLatestRoundRepresentativeStockTake(): StockTake? { + val roundKey = resolveGlobalLatestStockTakeRoundKey() ?: return null + val inRound = stockTakeRepository.findAllByStockTakeRoundIdAndDeletedIsFalse(roundKey) + if (inRound.isNotEmpty()) { + return inRound.maxWithOrNull( + compareBy { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } + .thenBy { it.id ?: 0L } + ) + } + return stockTakeRepository.findByIdAndDeletedIsFalse(roundKey) + } + open fun getApproverInventoryLotDetailsAll( stockTakeId: Long? = null, pageNum: Int = 0, @@ -883,11 +898,7 @@ open class StockTakeRecordService( val warehouseIds = allWarehouses.mapNotNull { it.id } if (warehouseIds.isEmpty()) return emptyList() - val allStockTakes = stockTakeRepository.findAll().filter { !it.deleted } - if (allStockTakes.isEmpty()) return emptyList() - - val latestBaseStockTake = allStockTakes - .maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } + val latestBaseStockTake = resolveLatestRoundRepresentativeStockTake() ?: return emptyList() // 优先用 stocktakerecord.stockTakeRoundId 直接取该轮次记录(避免再算 stockTakeId 列表 + in 查询) @@ -966,11 +977,21 @@ open class StockTakeRecordService( * 用于前端先拿 stockTakeId,再调用 pending/approved 明细接口。 */ open fun getLatestApproverStockTakeHeader(): AllPickedStockTakeListReponse? { - val latestBaseStockTake = stockTakeRepository.findAll() - .filter { !it.deleted } - .maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } + val latestBaseStockTake = resolveLatestRoundRepresentativeStockTake() ?: return null + val roundKey = latestBaseStockTake.stockTakeRoundId ?: latestBaseStockTake.id + val roundStockTakes = if (roundKey != null) { + stockTakeRepository.findAllByStockTakeRoundIdAndDeletedIsFalse(roundKey) + } else { + emptyList() + } + val planStartDate = if (roundStockTakes.isNotEmpty()) { + roundStockTakes.mapNotNull { it.planStart }.maxOrNull()?.toLocalDate() + } else { + latestBaseStockTake.planStart?.toLocalDate() + } + val statusValue = latestBaseStockTake.status?.let { st -> if (st == StockTakeStatus.APPROVING || st == StockTakeStatus.COMPLETED) st.value else "" } ?: "" @@ -983,14 +1004,14 @@ open class StockTakeRecordService( currentStockTakeItemNumber = 0, totalInventoryLotNumber = 0, stockTakeId = latestBaseStockTake.id ?: 0, - stockTakeRoundId = latestBaseStockTake.stockTakeRoundId ?: latestBaseStockTake.id, + stockTakeRoundId = roundKey, stockTakerName = null, approverName = null, TotalItemNumber = 0, startTime = latestBaseStockTake.actualStart, endTime = latestBaseStockTake.actualEnd, ReStockTakeTrueFalse = false, - planStartDate = latestBaseStockTake.planStart?.toLocalDate(), + planStartDate = planStartDate, stockTakeSectionDescription = null, warehouseArea = null, storeId = null @@ -1674,6 +1695,64 @@ open class StockTakeRecordService( } } + private fun resolveApproverFinalQuantities( + request: SaveApproverStockTakeRecordRequest + ): Pair { + 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 + } + return Pair(finalQty, finalBadQty) + } + + private fun resolveApproverLastSelect( + request: SaveApproverStockTakeRecordRequest, + stockTakeRecord: StockTakeRecord + ): Int { + return request.lastSelect ?: run { + if (request.approverQty != null && request.approverBadQty != null) { + 3 + } else if ( + stockTakeRecord.pickerSecondStockTakeQty != null && + stockTakeRecord.pickerSecondStockTakeQty!!.compareTo(request.qty) == 0 + ) { + 2 + } else { + 1 + } + } + } + + private fun applyApproverSaveToRecord( + stockTakeRecord: StockTakeRecord, + request: SaveApproverStockTakeRecordRequest, + approverId: Long, + approverName: String? + ): Pair { + val (finalQty, finalBadQty) = resolveApproverFinalQuantities(request) + val varianceQty = finalQty.subtract(stockTakeRecord.bookQty ?: BigDecimal.ZERO) + stockTakeRecord.apply { + this.approverId = approverId + this.approverName = approverName + this.approverStockTakeQty = finalQty + this.approverBadQty = finalBadQty + this.varianceQty = varianceQty + this.status = "completed" + this.approverTime = java.time.LocalDateTime.now() + this.lastSelect = resolveApproverLastSelect(request, this) + if (this.stockTakeEndTime == null) { + this.stockTakeEndTime = java.time.LocalDateTime.now() + } + } + return Pair(finalQty, varianceQty) + } + open fun saveApproverStockTakeRecord( request: SaveApproverStockTakeRecordRequest, stockTakeId: Long @@ -1691,53 +1770,17 @@ open class StockTakeRecordService( 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) + val (finalQty, varianceQty) = applyApproverSaveToRecord( + stockTakeRecord, + request, + request.approverId ?: 0L, + user?.name + ) println("finalQty: $finalQty") println("stockTakeRecord.bookQty: ${stockTakeRecord.bookQty}") println("varianceQty: $varianceQty") - stockTakeRecord.apply { - this.approverId = request.approverId - this.approverName = user?.name - this.approverStockTakeQty = finalQty - this.approverBadQty = finalBadQty - this.varianceQty = varianceQty - this.status = "completed" - this.approverTime = java.time.LocalDateTime.now() - this.lastSelect = request.lastSelect ?: run { - // 兼容:旧客户端未传 lastSelect 时,尝试根据请求类型推断 - if (request.approverQty != null && request.approverBadQty != null) { - 3 - } else if ( - this.pickerSecondStockTakeQty != null && - this.pickerSecondStockTakeQty!!.compareTo(request.qty) == 0 - ) { - 2 - } else { - 1 - } - } - // stockTakeEndTime 目前只在 saveStockTakeRecord「第二次盤點」時寫入;只做第一次盤點時會一直是 null。 - // 審核通過時若仍為空,補上時間,讓列表「審核/完成時間」有值(不覆寫已有第二次盤點結束時間)。 - if (this.stockTakeEndTime == null) { - this.stockTakeEndTime = java.time.LocalDateTime.now() - } - } - val savedRecord = stockTakeRecordRepository.save(stockTakeRecord) // variance != 0:過帳並更新/新建 StockTakeLine;= 0:僅將開輪預建 line 標記完成(不查庫存) @@ -2034,8 +2077,24 @@ open fun batchSaveApproverStockTakeRecordsByIds( ): BatchSaveApproverStockTakeRecordResponse { val totalStartNs = System.nanoTime() fun elapsedMs(startNs: Long): Long = (System.nanoTime() - startNs) / 1_000_000 - logger.info("batchSaveApproverStockTakeRecordsByIds start: stockTakeId={}, ids={}", request.stockTakeId, request.recordIds.size) - if (request.recordIds.isEmpty()) { + + val recordPayloadById = request.records + .mapNotNull { r -> r.stockTakeRecordId?.let { id -> id to r } } + .toMap() + val useClientPayload = recordPayloadById.isNotEmpty() + val idsToLoad = when { + useClientPayload -> recordPayloadById.keys.toList() + request.recordIds.isNotEmpty() -> request.recordIds + else -> emptyList() + } + + logger.info( + "batchSaveApproverStockTakeRecordsByIds start: stockTakeId={}, ids={}, clientPayload={}", + request.stockTakeId, + idsToLoad.size, + useClientPayload + ) + if (idsToLoad.isEmpty()) { return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No record IDs provided")) } @@ -2044,8 +2103,8 @@ open fun batchSaveApproverStockTakeRecordsByIds( val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") - val idSet = request.recordIds.toSet() - val stockTakeRecords = stockTakeRecordRepository.findAllById(request.recordIds) + val idSet = idsToLoad.toSet() + val stockTakeRecords = stockTakeRecordRepository.findAllById(idsToLoad) .filter { !it.deleted && (it.id in idSet) && @@ -2081,35 +2140,48 @@ open fun batchSaveApproverStockTakeRecordsByIds( stockTakeRecords.forEach { record -> try { - val qty: BigDecimal - val badQty: BigDecimal - - if (record.pickerSecondStockTakeQty != null && record.pickerSecondStockTakeQty!! > BigDecimal.ZERO) { - qty = record.pickerSecondStockTakeQty!! - badQty = record.pickerSecondBadQty ?: BigDecimal.ZERO - } else { - qty = record.pickerFirstStockTakeQty ?: BigDecimal.ZERO - badQty = record.pickerFirstBadQty ?: BigDecimal.ZERO + val payload = recordPayloadById[record.id] + if (useClientPayload && payload == null) { + errorCount++ + errors.add("No client payload for record ${record.id}") + return@forEach } - val bookQty = record.bookQty ?: BigDecimal.ZERO - val varianceQty = qty.subtract(bookQty) - - record.apply { - this.approverId = request.approverId - this.approverName = user?.name - this.approverStockTakeQty = qty - this.approverBadQty = badQty - this.varianceQty = varianceQty - this.status = "completed" - this.approverTime = java.time.LocalDateTime.now() - this.lastSelect = if ( - record.pickerSecondStockTakeQty != null && - record.pickerSecondStockTakeQty!! > BigDecimal.ZERO - ) 2 else 1 - if (this.stockTakeEndTime == null) { - this.stockTakeEndTime = java.time.LocalDateTime.now() + val (qty, varianceQty) = if (payload != null) { + applyApproverSaveToRecord( + record, + payload.copy(approverId = request.approverId), + request.approverId, + user?.name + ) + } else { + val legacyQty: BigDecimal + val legacyBadQty: BigDecimal + if (record.pickerSecondStockTakeQty != null && record.pickerSecondStockTakeQty!! > BigDecimal.ZERO) { + legacyQty = record.pickerSecondStockTakeQty!! + legacyBadQty = record.pickerSecondBadQty ?: BigDecimal.ZERO + } else { + legacyQty = record.pickerFirstStockTakeQty ?: BigDecimal.ZERO + legacyBadQty = record.pickerFirstBadQty ?: BigDecimal.ZERO + } + val legacyVariance = legacyQty.subtract(record.bookQty ?: BigDecimal.ZERO) + record.apply { + this.approverId = request.approverId + this.approverName = user?.name + this.approverStockTakeQty = legacyQty + this.approverBadQty = legacyBadQty + this.varianceQty = legacyVariance + this.status = "completed" + this.approverTime = java.time.LocalDateTime.now() + this.lastSelect = if ( + record.pickerSecondStockTakeQty != null && + record.pickerSecondStockTakeQty!! > BigDecimal.ZERO + ) 2 else 1 + if (this.stockTakeEndTime == null) { + this.stockTakeEndTime = java.time.LocalDateTime.now() + } } + Pair(legacyQty, legacyVariance) } recordsToPersist.add(record) 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 cf94076..9fa6f33 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 @@ -162,7 +162,9 @@ data class BatchSaveApproverStockTakeAllRequest( data class BatchSaveApproverStockTakeByIdsRequest( val stockTakeId: Long, val approverId: Long, - val recordIds: List, + val recordIds: List = emptyList(), + /** 與單筆 saveApproverStockTakeRecord 相同;有值時依每筆 qty / lastSelect 保存。 */ + val records: List = emptyList(), ) data class BatchSaveApproverStockTakeRecordResponse(