From 33ee152b72233edaa48b004d9ad3760dd011ab08 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Thu, 22 Oct 2026 17:57:36 +0800 Subject: [PATCH] imporve report speed --- .../service/StockTakeVarianceReportService.kt | 141 +++++++++++++++--- .../web/StockTakeVarianceReportController.kt | 10 +- 2 files changed, 125 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt index 330964a3..f688166a 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt @@ -96,6 +96,46 @@ lot_root_origin AS ( THEN CONCAT('轉倉(原:', $LOT_ROOT_CATEGORY_SQL, ')') ELSE $LOT_ROOT_CATEGORY_SQL END""" + + /** Excel/畫面顯示用:pending→待盤點、pass→待審核、completed→已審核 */ + private const val STOCK_TAKE_STATUS_LABEL_SQL = """ +CASE + WHEN COALESCE(stockTakeRecordStatus, '') = 'completed' THEN '已審核' + WHEN COALESCE(stockTakeRecordStatus, '') = 'pass' THEN '待審核' + WHEN COALESCE(stockTakeRecordStatus, '') IN ('pending', 'notMatch') THEN '待盤點' + ELSE COALESCE(stockTakeRecordStatus, '') +END""" + } + + /** + * 報表狀態篩選: + * - pending:待盤點(pending / notMatch) + * - pass / approving:待審核(pass) + * - completed:已審核 + */ + private fun buildStockTakeRecordStatusFilterSql(status: String?): String { + val statusNormalized = status?.trim()?.lowercase().orEmpty() + return when (statusNormalized) { + "pending" -> "AND str.status IN ('pending', 'notMatch')" + "pass", "approving" -> "AND str.status = 'pass'" + "completed" -> "AND str.status = 'completed'" + else -> "" + } + } + + /** + * Overall/多輪「最新一筆」用的 status 條件。 + * @param defaultToCompleted 未指定 status 時是否仍限制 completed(舊 overall/多輪預設) + */ + private fun buildOverallStatusPredicateSql(alias: String, status: String?, defaultToCompleted: Boolean): String { + val statusNormalized = status?.trim()?.lowercase().orEmpty() + val pred = when (statusNormalized) { + "pending" -> "$alias.status IN ('pending', 'notMatch')" + "pass", "approving" -> "$alias.status = 'pass'" + "completed" -> "$alias.status = 'completed'" + else -> if (defaultToCompleted) "$alias.status = 'completed'" else null + } + return if (pred == null) "" else "AND $pred" } /** @@ -422,20 +462,7 @@ ORDER BY val args = mutableMapOf() args["stockTakeRoundId"] = stockTakeRoundId - val statusNormalized = status?.trim()?.lowercase().orEmpty() - // status 映射规则: - // - All/null:不过滤 - // - pending:包含 pending/pass/notMatch - // - completed:只看 completed - val statusLatestSql = when (statusNormalized) { - "pending" -> """ - AND str.status IN ('pending', 'pass', 'notMatch') - """.trimIndent() - "completed" -> """ - AND str.status = 'completed' - """.trimIndent() - else -> "" - } + val statusLatestSql = buildStockTakeRecordStatusFilterSql(status) val storeIdSql = run { val normalized = storeId?.trim() @@ -457,9 +484,9 @@ ORDER BY ) val lotTypeFilterSql = buildLotTypeFilterClause(type) + // 效能:先取本輪 latest_str,再只對相關 lot / lot_line 做追溯與 in/out 聚合 val sql = """ WITH RECURSIVE -$LOT_ROOT_ORIGIN_CTE_SQL, rb AS ( SELECT COALESCE(MIN(s.date), CURRENT_DATE) AS fromDate, @@ -484,6 +511,64 @@ latest_str AS ( AND str.stockTakeRoundId = :stockTakeRoundId $statusLatestSql ), +lot_trace AS ( + SELECT + il.id AS lotId, + il.stockInLineId AS silId, + 0 AS depth + FROM inventory_lot il + INNER JOIN (SELECT DISTINCT lotId FROM latest_str) seed ON seed.lotId = il.id + WHERE il.deleted = 0 + AND il.stockInLineId IS NOT NULL + UNION ALL + SELECT + lt.lotId AS lotId, + il_src.stockInLineId AS silId, + lt.depth + 1 AS depth + FROM lot_trace lt + INNER JOIN stock_in_line sil + ON sil.id = lt.silId + AND sil.deleted = 0 + INNER JOIN stock_transfer_record tr + ON tr.id = sil.stockTransferId + AND tr.deleted = 0 + INNER JOIN stock_out_line sol + ON sol.id = tr.stockOutLineId + AND sol.deleted = 0 + INNER JOIN inventory_lot_line ill_src + ON ill_src.id = sol.inventoryLotLineId + AND ill_src.deleted = 0 + INNER JOIN inventory_lot il_src + ON il_src.id = ill_src.inventoryLotId + AND il_src.deleted = 0 + WHERE lt.depth < 8 + AND il_src.stockInLineId IS NOT NULL + AND ( + UPPER(TRIM(COALESCE(sil.type, ''))) = 'TRF' + OR sil.stockTransferId IS NOT NULL + ) +), +lot_root_origin AS ( + SELECT + lotId, + silId AS rootSilId + FROM ( + SELECT + lotId, + silId, + ROW_NUMBER() OVER (PARTITION BY lotId ORDER BY depth DESC) AS rn + FROM lot_trace + ) t + WHERE t.rn = 1 +), +round_ill AS ( + SELECT DISTINCT ill.id AS inventoryLotLineId + FROM latest_str ls + INNER JOIN inventory_lot_line ill + ON ill.inventoryLotId = ls.lotId + AND ill.warehouseId = ls.warehouseId + AND ill.deleted = 0 +), in_agg AS ( SELECT ill.id AS inventoryLotLineId, @@ -505,6 +590,7 @@ in_agg AS ( ELSE 0 END) AS inDuring, MAX(CASE WHEN sil.receiptDate IS NOT NULL THEN DATE(sil.receiptDate) END) AS lastInDate FROM inventory_lot_line ill + INNER JOIN round_ill ri ON ri.inventoryLotLineId = ill.id CROSS JOIN rb INNER JOIN inventory_lot il ON ill.inventoryLotId = il.id @@ -534,6 +620,7 @@ out_agg AS ( SUM(CASE WHEN DATE(sol.endTime) BETWEEN rb.fromDate AND rb.toDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outDuring, MAX(CASE WHEN sol.endTime IS NOT NULL THEN DATE(sol.endTime) END) AS lastOutDate FROM inventory_lot_line ill + INNER JOIN round_ill ri ON ri.inventoryLotLineId = ill.id CROSS JOIN rb LEFT JOIN stock_out_line sol ON sol.inventoryLotLineId = ill.id @@ -660,7 +747,8 @@ SELECT CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockOut, CASE WHEN SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCurrentBalance, CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty, - lotTypeRaw AS type + lotTypeRaw AS type, + $STOCK_TAKE_STATUS_LABEL_SQL AS stockTakeStatus FROM data ORDER BY @@ -673,15 +761,16 @@ ORDER BY } /** - * V2 Overall:每個 (lotId, warehouseId) 取 **status=completed** 且 **stockTakeRoundId 最大** 之一筆盤點紀錄 - * (同輪多筆時取 id 較大者)。期初/累計區間依該筆所屬輪次之 MIN~MAX(`date`)。 - * @param limitedToRoundIds null=全系統所有輪次;非 null=僅在指定輪次內取最新已完成 + * V2 Overall:每個 (lotId, warehouseId) 取符合 status 篩選且 **stockTakeRoundId 最大** 之一筆盤點紀錄 + * (同輪多筆時取 id 較大者;未指定 status 時預設 completed)。期初/累計區間依該筆所屬輪次之 MIN~MAX(`date`)。 + * @param limitedToRoundIds null=全系統所有輪次;非 null=僅在指定輪次內取最新 */ fun searchStockTakeVarianceReportV2Overall( itemCode: String?, storeId: String?, limitedToRoundIds: List? = null, type: String? = null, + status: String? = null, ): List> { val args = mutableMapOf() @@ -696,6 +785,11 @@ ORDER BY "AND s.stockTakeRoundId IN ($inClause)" } + // 未指定 status 時維持舊行為(只取 completed) + val statusSSql = buildOverallStatusPredicateSql("s", status, defaultToCompleted = true) + val statusStrSql = buildOverallStatusPredicateSql("str", status, defaultToCompleted = true) + val statusStr2Sql = buildOverallStatusPredicateSql("str2", status, defaultToCompleted = true) + val storeIdSql = run { val normalized = storeId?.trim() if (normalized.isNullOrBlank() || normalized.equals("all", ignoreCase = true)) { @@ -748,19 +842,19 @@ latest_str AS ( MAX(s.stockTakeRoundId) AS maxRound FROM stocktakerecord s WHERE s.deleted = 0 - AND s.status = 'completed' + $statusSSql $roundIdsFilterSql GROUP BY s.lotId, s.warehouseId ) mx ON mx.lotId = str.lotId AND mx.warehouseId = str.warehouseId AND mx.maxRound = str.stockTakeRoundId WHERE str.deleted = 0 - AND str.status = 'completed' + $statusStrSql AND NOT EXISTS ( SELECT 1 FROM stocktakerecord str2 WHERE str2.deleted = 0 - AND str2.status = 'completed' + $statusStr2Sql AND str2.lotId = str.lotId AND str2.warehouseId = str.warehouseId AND str2.stockTakeRoundId = str.stockTakeRoundId @@ -950,7 +1044,8 @@ SELECT CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockOut, CASE WHEN SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCurrentBalance, CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty, - lotTypeRaw AS type + lotTypeRaw AS type, + $STOCK_TAKE_STATUS_LABEL_SQL AS stockTakeStatus FROM data ORDER BY diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt index bfaca59f..ce01c780 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt @@ -264,6 +264,7 @@ class StockTakeVarianceReportController( "盤盈虧", "盤盈虧百分比", "類型", + "狀態", "審核時間", ) val totalColumns = headers.size @@ -509,7 +510,8 @@ class StockTakeVarianceReportController( setNumberCellFromFormatted(r, 8, rowMap["variance"], numberStyle, dashStyle) setTextCell(r, 9, rowMap["variancePercentage"], dashStyle) setTextCell(r, 10, rowMap["type"], centerStyle) - setTextCell(r, 11, rowMap["stockTakeDate"], centerStyle) + setTextCell(r, 11, rowMap["stockTakeStatus"], centerStyle) + setTextCell(r, 12, rowMap["stockTakeDate"], centerStyle) currentItemNo = itemNo currentItemName = itemName @@ -526,7 +528,7 @@ class StockTakeVarianceReportController( sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0)) } - val widths = intArrayOf(14, 26, 10, 18, 14, 14, 14, 12, 12, 14, 18, 22) + val widths = intArrayOf(14, 26, 10, 18, 14, 14, 14, 12, 12, 14, 18, 12, 22) widths.forEachIndexed { idx, w -> sheet.setColumnWidth(idx, w * 256) } val output = ByteArrayOutputStream() @@ -596,7 +598,7 @@ class StockTakeVarianceReportController( ?: emptyList() /** - * rep-012:單輪=單輪查詢;多輪=已選輪次內各批號/倉最新已完成(status 固定 completed)。 + * rep-012:單輪=單輪查詢;多輪=已選輪次內各批號/倉最新一筆(可依 status 篩;未指定則 completed)。 * overall=true 且未傳輪次時保留舊「全系統輪次」行為。 */ private fun resolveRep012ReportData( @@ -621,6 +623,7 @@ class StockTakeVarianceReportController( itemCode = itemCode, storeId = storeId, type = type, + status = status, ), ) } @@ -648,6 +651,7 @@ class StockTakeVarianceReportController( storeId = storeId, limitedToRoundIds = roundIds, type = type, + status = status, ), ) }