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 330ae9d..9b19025 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 @@ -9,6 +9,95 @@ open class StockTakeVarianceReportService( private val jdbcDao: JdbcDao, ) { + companion object { + private const val ORIGIN_STOCK_IN_JOIN_SQL = """ + LEFT JOIN stock_in_line origin_sil + ON origin_sil.id = il.stockInLineId AND origin_sil.deleted = 0 + LEFT JOIN purchase_order po + ON po.id = origin_sil.purchaseOrderId AND po.deleted = 0 +""" + + private const val LOT_ROOT_ORIGIN_CTE_SQL = """ +lot_trace AS ( + SELECT + il.id AS lotId, + il.stockInLineId AS silId, + 0 AS depth + FROM inventory_lot il + 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 +)""" + + private const val ROOT_STOCK_IN_JOIN_SQL = """ + LEFT JOIN lot_root_origin lro + ON lro.lotId = il.id + LEFT JOIN stock_in_line root_sil + ON root_sil.id = lro.rootSilId AND root_sil.deleted = 0 + LEFT JOIN purchase_order root_po + ON root_po.id = root_sil.purchaseOrderId AND root_po.deleted = 0 +""" + + private const val LOT_ROOT_CATEGORY_SQL = """ + CASE + WHEN root_sil.jobOrderId IS NOT NULL THEN '工廠生產' + WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN' THEN '期初存貨' + WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ' THEN '倉存調整' + WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP' THEN 'PP' + WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF' THEN 'PF' + WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'TOA' THEN 'TOA' + WHEN root_sil.purchaseOrderId IS NOT NULL THEN '採購入倉' + ELSE '其他入倉' + END""" + + private const val LOT_TYPE_DISPLAY_SQL = """ + CASE + WHEN UPPER(TRIM(COALESCE(origin_sil.type, ''))) = 'TRF' + OR origin_sil.stockTransferId IS NOT NULL + THEN CONCAT('轉倉(原:', $LOT_ROOT_CATEGORY_SQL, ')') + ELSE $LOT_ROOT_CATEGORY_SQL + END""" + } + /** * Stock Take Variance 報表查詢 * @@ -40,6 +129,7 @@ open class StockTakeVarianceReportService( storeLocation: String?, stockTakeDateStart: String?, stockTakeDateEnd: String?, + type: String? = null, ): List> { val args = mutableMapOf() @@ -62,6 +152,7 @@ open class StockTakeVarianceReportService( } else { "" } + val lotTypeFilterSql = buildLotTypeFilterClause(type) // 1) toDate:有填用傳入,沒填用今天 val toDate = (stockTakeDateEnd?.replace("/", "-") @@ -87,7 +178,9 @@ open class StockTakeVarianceReportService( args["toDate"] = toDate val sql = """ -WITH latest_str AS ( +WITH RECURSIVE +$LOT_ROOT_ORIGIN_CTE_SQL, +latest_str AS ( SELECT s1.lotId, s1.warehouseId, @@ -207,7 +300,8 @@ data AS ( ls.bookQty AS stkBookQty, ls.approverStockTakeQty AS stkApproverQty, ls.varianceQty AS stkVarianceQty, - ls.date AS stockTakeDateRaw + ls.date AS stockTakeDateRaw, + $LOT_TYPE_DISPLAY_SQL AS lotTypeRaw FROM inventory_lot_line ill INNER JOIN inventory_lot il ON ill.inventoryLotId = il.id @@ -218,6 +312,8 @@ data AS ( INNER JOIN warehouse wh ON ill.warehouseId = wh.id AND wh.deleted = 0 + $ORIGIN_STOCK_IN_JOIN_SQL + $ROOT_STOCK_IN_JOIN_SQL LEFT JOIN item_uom iu ON it.id = iu.itemId @@ -237,6 +333,7 @@ data AS ( $stockCategorySql $itemCodeSql $storeLocationSql + $lotTypeFilterSql ) SELECT @@ -284,7 +381,8 @@ SELECT CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn, 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(currentQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(currentQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(currentQty, 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 + 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 FROM data ORDER BY @@ -307,6 +405,7 @@ ORDER BY itemCode: String?, storeId: String?, status: String?, + type: String? = null, ): List> { val countSql = """ SELECT COUNT(*) AS c FROM stocktakerecord s @@ -356,9 +455,12 @@ ORDER BY "itemCode", args ) + val lotTypeFilterSql = buildLotTypeFilterClause(type) val sql = """ -WITH rb AS ( +WITH RECURSIVE +$LOT_ROOT_ORIGIN_CTE_SQL, +rb AS ( SELECT COALESCE(MIN(s.date), CURRENT_DATE) AS fromDate, COALESCE(MAX(s.date), CURRENT_DATE) AS toDate @@ -476,7 +578,8 @@ data AS ( ls.varianceQty AS stkVarianceQty, ls.strDate AS stockTakeDateRaw, ls.approverTime AS approvalDateTimeRaw, - ls.stockTakeRecordStatus AS stockTakeRecordStatus + ls.stockTakeRecordStatus AS stockTakeRecordStatus, + $LOT_TYPE_DISPLAY_SQL AS lotTypeRaw FROM latest_str ls INNER JOIN inventory_lot il ON ls.lotId = il.id @@ -491,6 +594,8 @@ data AS ( INNER JOIN warehouse wh ON wh.id = ls.warehouseId AND wh.deleted = 0 + $ORIGIN_STOCK_IN_JOIN_SQL + $ROOT_STOCK_IN_JOIN_SQL LEFT JOIN item_uom iu ON it.id = iu.itemId @@ -505,6 +610,7 @@ data AS ( WHERE 1=1 $itemCodeSql $storeIdSql + $lotTypeFilterSql ) SELECT @@ -553,7 +659,8 @@ SELECT CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn, 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 + 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 FROM data ORDER BY @@ -574,6 +681,7 @@ ORDER BY itemCode: String?, storeId: String?, limitedToRoundIds: List? = null, + type: String? = null, ): List> { val args = mutableMapOf() @@ -605,9 +713,12 @@ ORDER BY "itemCode", args ) + val lotTypeFilterSql = buildLotTypeFilterClause(type) val sql = """ -WITH round_bounds AS ( +WITH RECURSIVE +$LOT_ROOT_ORIGIN_CTE_SQL, +round_bounds AS ( SELECT s.stockTakeRoundId, COALESCE(MIN(s.date), CURRENT_DATE) AS fromDate, @@ -766,12 +877,15 @@ data AS ( lb.varianceQty AS stkVarianceQty, lb.strDate AS stockTakeDateRaw, lb.approverTime AS approvalDateTimeRaw, - lb.stockTakeRecordStatus AS stockTakeRecordStatus + lb.stockTakeRecordStatus AS stockTakeRecordStatus, + $LOT_TYPE_DISPLAY_SQL AS lotTypeRaw FROM line_bounds lb INNER JOIN inventory_lot_line ill ON ill.id = lb.inventoryLotLineId INNER JOIN inventory_lot il ON ill.inventoryLotId = il.id AND il.deleted = 0 INNER JOIN items it ON il.itemId = it.id AND it.deleted = 0 INNER JOIN warehouse wh ON wh.id = lb.warehouseId AND wh.deleted = 0 + $ORIGIN_STOCK_IN_JOIN_SQL + $ROOT_STOCK_IN_JOIN_SQL LEFT JOIN item_uom iu ON it.id = iu.itemId @@ -786,6 +900,7 @@ data AS ( WHERE 1=1 $itemCodeSql $storeIdSql + $lotTypeFilterSql ) SELECT @@ -834,7 +949,8 @@ SELECT CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn, 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 + 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 FROM data ORDER BY @@ -883,6 +999,36 @@ ORDER BY return if (!cap.isNullOrBlank()) cap else "盤點輪次$stockTakeRoundId" } + /** + * 類型篩選:依「轉倉追溯後」之根來源類型過濾。 + * + * 需與 [ORIGIN_STOCK_IN_JOIN_SQL] + [ROOT_STOCK_IN_JOIN_SQL] 一併使用: + * - 顯示用 origin_sil(判斷是否轉倉) + * - 篩選用 root_sil / root_po(根來源類型) + */ + private fun buildLotTypeFilterClause(type: String?): String { + val normalized = type?.trim().orEmpty() + if (normalized.isBlank() || normalized.equals("all", ignoreCase = true)) return "" + return when (normalized) { + "PP" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP'" + "PF" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF'" + "TOA" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'TOA'" + "工廠生產" -> "AND root_sil.jobOrderId IS NOT NULL" + "倉存調整" -> "AND UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ'" + "期初存貨" -> "AND UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN'" + "採購入倉" -> """ + AND root_sil.purchaseOrderId IS NOT NULL + AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) NOT IN ('PP', 'PF', 'TOA') + """.trimIndent() + "其他入倉" -> """ + AND (root_sil.purchaseOrderId IS NULL OR root_sil.purchaseOrderId = 0) + AND root_sil.jobOrderId IS NULL + AND UPPER(TRIM(COALESCE(root_sil.type, ''))) NOT IN ('OPEN', 'ADJ') + """.trimIndent() + else -> "" + } + } + /** LIKE 多值工具方法 */ private fun buildMultiValueLikeClause( paramValue: String?, 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 a181bf8..bfaca59 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 @@ -49,6 +49,7 @@ class StockTakeVarianceReportController( @RequestParam(required = false) storeLocation: String?, @RequestParam(required = false) stockTakeDateStart: String?, @RequestParam(required = false) stockTakeDateEnd: String?, + @RequestParam(required = false) type: String?, ): ResponseEntity { val parameters = mutableMapOf() @@ -76,6 +77,7 @@ class StockTakeVarianceReportController( storeLocation = storeLocation, stockTakeDateStart = stockTakeDateStart, stockTakeDateEnd = stockTakeDateEnd, + type = type, ) val stockTakeDateDisplay = dbData .mapNotNull { it["stockTakeDate"] as? String } @@ -108,6 +110,7 @@ class StockTakeVarianceReportController( @RequestParam(required = false) storeLocation: String?, @RequestParam(required = false) stockTakeDateStart: String?, @RequestParam(required = false) stockTakeDateEnd: String?, + @RequestParam(required = false) type: String?, ): ResponseEntity { val dbData = stockTakeVarianceReportService.searchStockTakeVarianceReport( stockCategory = stockCategory, @@ -115,6 +118,7 @@ class StockTakeVarianceReportController( storeLocation = storeLocation, stockTakeDateStart = stockTakeDateStart, stockTakeDateEnd = stockTakeDateEnd, + type = type, ) val excelBytes = createStockTakeVarianceExcel( @@ -146,8 +150,9 @@ class StockTakeVarianceReportController( @RequestParam(required = false) itemCode: String?, @RequestParam(required = false, name = "store_id") storeId: String?, @RequestParam(required = false) status: String?, + @RequestParam(required = false) type: String?, ): ResponseEntity { - val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status) + val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status, type) val parameters = mutableMapOf() @@ -200,8 +205,9 @@ class StockTakeVarianceReportController( @RequestParam(required = false) itemCode: String?, @RequestParam(required = false, name = "store_id") storeId: String?, @RequestParam(required = false) status: String?, + @RequestParam(required = false) type: String?, ): ResponseEntity { - val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status) + val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status, type) val cap = rep012.caption val dbData = rep012.dbData @@ -257,6 +263,7 @@ class StockTakeVarianceReportController( "盤點數", "盤盈虧", "盤盈虧百分比", + "類型", "審核時間", ) val totalColumns = headers.size @@ -501,7 +508,8 @@ class StockTakeVarianceReportController( setNumberCellFromFormatted(r, 7, rowMap["stockTakeQty"], numberStyle, dashStyle) setNumberCellFromFormatted(r, 8, rowMap["variance"], numberStyle, dashStyle) setTextCell(r, 9, rowMap["variancePercentage"], dashStyle) - setTextCell(r, 10, rowMap["stockTakeDate"], centerStyle) + setTextCell(r, 10, rowMap["type"], centerStyle) + setTextCell(r, 11, rowMap["stockTakeDate"], centerStyle) currentItemNo = itemNo currentItemName = itemName @@ -518,7 +526,7 @@ class StockTakeVarianceReportController( sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0)) } - val widths = intArrayOf(14, 26, 10, 18, 14, 14, 14, 12, 12, 14, 22) + val widths = intArrayOf(14, 26, 10, 18, 14, 14, 14, 12, 12, 14, 18, 22) widths.forEachIndexed { idx, w -> sheet.setColumnWidth(idx, w * 256) } val output = ByteArrayOutputStream() @@ -597,6 +605,7 @@ class StockTakeVarianceReportController( itemCode: String?, storeId: String?, status: String?, + type: String?, ): Rep012ReportData { val roundIds = parseStockTakeRoundIds(stockTakeRoundId) val useLegacyOverall = overall == true && roundIds.isEmpty() @@ -611,6 +620,7 @@ class StockTakeVarianceReportController( dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2Overall( itemCode = itemCode, storeId = storeId, + type = type, ), ) } @@ -625,6 +635,7 @@ class StockTakeVarianceReportController( itemCode = itemCode, storeId = storeId, status = status, + type = type, ), ) } @@ -636,6 +647,7 @@ class StockTakeVarianceReportController( itemCode = itemCode, storeId = storeId, limitedToRoundIds = roundIds, + type = type, ), ) }