From d19e3bcdd89a49fdf688b0dd549783b85536ab5c Mon Sep 17 00:00:00 2001 From: "Tommy\\2Fi-Staff" Date: Mon, 13 Apr 2026 13:33:47 +0800 Subject: [PATCH] update report --- .../modules/report/service/ReportService.kt | 15 +- .../SemiFGProductionAnalysisReportService.kt | 181 ++++++++---------- ...emiFGProductionAnalysisReportController.kt | 2 +- .../SemiFGProductionAnalysisReport.jrxml | 2 +- .../jasper/StockInTraceabilityReport.jrxml | 4 +- 5 files changed, 90 insertions(+), 114 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index 424625a..26fea5e 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -652,6 +652,7 @@ return result * Queries the database for Stock In Traceability Report data (入倉追蹤 PDF). * Joins stock_in_line, stock_in, items, qc_result, inventory_lot, inventory_lot_line, warehouse, and shop tables. * Supports comma-separated values for stockCategory (items.type) and itemCode. + * Date range [lastInDateStart, lastInDateEnd] filters on stock_in_line.productionDate (完成生產日期), same basis as 成品/半成品生產分析報告. */ fun searchStockInTraceabilityReport( stockCategory: String?, @@ -673,13 +674,13 @@ return result val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) { val formattedDate = lastInDateStart.replace("/", "-") args["lastInDateStart"] = formattedDate - "AND DATE(sil.receiptDate) >= DATE(:lastInDateStart)" + "AND sil.productionDate IS NOT NULL AND DATE(sil.productionDate) >= DATE(:lastInDateStart)" } else "" val lastInDateEndSql = if (!lastInDateEnd.isNullOrBlank()) { val formattedDate = lastInDateEnd.replace("/", "-") args["lastInDateEnd"] = formattedDate - "AND DATE(sil.receiptDate) <= DATE(:lastInDateEnd)" + "AND sil.productionDate IS NOT NULL AND DATE(sil.productionDate) <= DATE(:lastInDateEnd)" } else "" val sql = """ @@ -691,7 +692,7 @@ return result COALESCE(sil.lotNo, il.lotNo, '') as lotNo, COALESCE(DATE_FORMAT(COALESCE(sil.expiryDate, il.expiryDate), '%Y-%m-%d'), '') as expiryDate, CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN '0' - ELSE TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(sil.acceptedQty, 0), 2))) + ELSE TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(pa_sil.putAwayQtySum, 0), 2))) END as stockInQty, TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(sil.acceptedQty, 0), 2))) as iqcSampleQty, TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(qr_agg.failQtySum, 0), 2))) as iqcDefectQty, @@ -706,7 +707,7 @@ return result COALESCE(wh.code, '') as storeLocation, COALESCE(sp_si.code, sp_po.code, '') as supplierID, COALESCE(sp_si.name, sp_po.name, '') as supplierName, - TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(sil.acceptedQty, 0)) OVER (PARTITION BY it.id), 2))) as totalStockInQty, + TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(pa_sil.putAwayQtySum, 0)) OVER (PARTITION BY it.id), 2))) as totalStockInQty, TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(sil.acceptedQty, 0)) OVER (PARTITION BY it.id), 2))) as totalIqcSampleQty FROM stock_in_line sil LEFT JOIN stock_in si ON sil.stockInId = si.id @@ -715,6 +716,12 @@ return result LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = true LEFT JOIN uom_conversion uc ON iu.uomId = uc.id LEFT JOIN inventory_lot il ON sil.inventoryLotId = il.id + LEFT JOIN ( + SELECT inventoryLotId, SUM(COALESCE(inQty, 0)) AS putAwayQtySum + FROM inventory_lot_line + WHERE deleted = false + GROUP BY inventoryLotId + ) pa_sil ON pa_sil.inventoryLotId = sil.inventoryLotId LEFT JOIN inventory_lot_line ill ON il.id = ill.inventoryLotId LEFT JOIN warehouse wh ON ill.warehouseId = wh.id LEFT JOIN shop sp_si ON si.supplierId = sp_si.id diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt index 3861af1..02c0dac 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt @@ -57,11 +57,13 @@ class SemiFGProductionAnalysisReportService( /** * Queries the database for Semi FG Production Analysis Report data. - * Flow: - * 1. Filter bom by description (FG/WIP) to get bom.code values - * 2. Match bom.code with stock_ledger.itemCode - * 3. Join stock_in_line; aggregate by calendar month of stock_in_line.productionDate (完成生產日期), not stock_ledger.modified - * Supports comma-separated values for stockCategory, stockSubCategory, and itemCode. + * Aligned with [ReportService.searchStockInTraceabilityReport] totals for the same filters: + * - stock_in_line driven (no stock_ledger gate); INNER JOIN bom so only items that exist as BOM rows appear + * - stockCategory → items.type (exact, comma-separated); itemCode → items.code (LIKE, comma-separated) + * - Date range / year on productionDate (with IS NOT NULL when range bound is set) + * - Put-away qty: SUM(inventory_lot_line.inQty) by sil.inventoryLotId (same as traceability pa_sil) + * - QC any fail → line qty 0 (same as traceability stockInQty) + * - One row per stockInLineId per month before pivot; all lines counted (not only job orders) */ fun searchSemiFGProductionAnalysisReport( stockCategory: String?, @@ -72,82 +74,72 @@ class SemiFGProductionAnalysisReportService( lastOutDateEnd: String? ): List> { val args = mutableMapOf() - - // Filter by stockCategory from bom.description (FG/WIP) - this finds which bom.code values match - // Supports multiple categories separated by comma (e.g., "FG,WIP") - // If "All" is selected or contains "All", don't filter by description + val stockCategorySql = if (!itemCode.isNullOrBlank()) { - // When itemCode is provided, skip stockCategory filter "" } else if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { - // Handle multiple categories (comma-separated) - val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" } - if (categories.isNotEmpty()) { - val conditions = categories.mapIndexed { index, cat -> - val paramName = "stockCategory_$index" - args[paramName] = cat - "b.description = :$paramName" - } - "AND (${conditions.joinToString(" OR ")})" - } else { - "" - } + buildMultiValueExactClause(stockCategory, "it.type", "semiSc", args) } else { "" } val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args) - // Filter by itemCode - match bom.code (user input should match bom.code, which then matches stock_ledger.itemCode) - val itemCodeSql = buildMultiValueExactClause(itemCode, "b.code", "itemCode", args) - + val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "semiItem", args) + val yearSql = if (!year.isNullOrBlank() && year != "All") { args["year"] = year "AND YEAR(si.productionDate) = :year" } else { "" } - + val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { val formattedDate = lastOutDateStart.replace("/", "-") args["lastOutDateStart"] = formattedDate - "AND DATE(si.productionDate) >= DATE(:lastOutDateStart)" + "AND si.productionDate IS NOT NULL AND DATE(si.productionDate) >= DATE(:lastOutDateStart)" } else "" - + val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { val formattedDate = lastOutDateEnd.replace("/", "-") args["lastOutDateEnd"] = formattedDate - "AND DATE(si.productionDate) <= DATE(:lastOutDateEnd)" + "AND si.productionDate IS NOT NULL AND DATE(si.productionDate) <= DATE(:lastOutDateEnd)" } else "" val sql = """ - WITH base AS ( + WITH qr_agg AS ( SELECT - COALESCE(sl.itemCode, '') as itemNo, - COALESCE(b.name, '') as itemName, - COALESCE(ic.sub, '') as stockSubCategory, - COALESCE(uc.udfudesc, '') as unitOfMeasure, - MONTH(si.productionDate) as mon, - si.id as stockInLineId, - si.acceptedQty as acceptedQty, - si.jobOrderId as jobOrderId - FROM stock_ledger sl - INNER JOIN bom b - ON sl.itemCode = b.code AND b.deleted = false - INNER JOIN stock_in_line si - ON si.id = sl.stockInLineId - AND si.deleted = false - AND si.productionDate IS NOT NULL - LEFT JOIN items it - ON sl.itemId = it.id - LEFT JOIN item_category ic - ON it.categoryId = ic.id - LEFT JOIN item_uom iu - ON it.id = iu.itemId - AND iu.stockUnit = true - LEFT JOIN uom_conversion uc - ON iu.uomId = uc.id - WHERE sl.deleted = false - AND sl.inQty IS NOT NULL - AND sl.inQty > 0 + qr.stockInLineId, + MAX(CASE WHEN qr.qcPassed = 0 THEN 1 ELSE 0 END) AS qcFailed + FROM qc_result qr + WHERE qr.deleted = 0 + GROUP BY qr.stockInLineId + ), + pa_sil AS ( + SELECT inventoryLotId, SUM(COALESCE(inQty, 0)) AS putAwayQtySum + FROM inventory_lot_line + WHERE deleted = false + GROUP BY inventoryLotId + ), + base AS ( + SELECT + COALESCE(it.code, '') AS itemNo, + COALESCE(it.name, '') AS itemName, + COALESCE(ic.sub, '') AS stockSubCategory, + COALESCE(uc.udfudesc, '') AS unitOfMeasure, + MONTH(si.productionDate) AS mon, + si.id AS stockInLineId, + CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN 0 + ELSE COALESCE(pa_sil.putAwayQtySum, 0) + END AS linePutAwayQty + FROM stock_in_line si + INNER JOIN items it ON si.itemId = it.id + INNER JOIN bom b ON b.code = it.code AND b.deleted = false + LEFT JOIN qr_agg ON qr_agg.stockInLineId = si.id + LEFT JOIN pa_sil ON pa_sil.inventoryLotId = si.inventoryLotId + LEFT JOIN item_category ic ON it.categoryId = ic.id + LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = true + LEFT JOIN uom_conversion uc ON iu.uomId = uc.id + WHERE si.deleted = false + AND si.productionDate IS NOT NULL $stockCategorySql $stockSubCategorySql $itemCodeSql @@ -155,7 +147,6 @@ class SemiFGProductionAnalysisReportService( $lastOutDateStartSql $lastOutDateEndSql ), - -- Deduplicate: stock_in_line can join to multiple stock_ledger rows; acceptedQty must be counted once per stockInLineId. dedup AS ( SELECT itemNo, @@ -164,36 +155,34 @@ class SemiFGProductionAnalysisReportService( unitOfMeasure, mon, stockInLineId, - MAX(COALESCE(acceptedQty, 0)) as acceptedQty, - MAX(jobOrderId) as jobOrderId + MAX(linePutAwayQty) AS linePutAwayQty FROM base GROUP BY itemNo, itemName, stockSubCategory, unitOfMeasure, mon, stockInLineId ) SELECT - MAX(d.stockSubCategory) as stockSubCategory, - d.itemNo as itemNo, - MAX(d.itemName) as itemName, - MAX(d.unitOfMeasure) as unitOfMeasure, - CAST(COALESCE(SUM(CASE WHEN d.mon = 1 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJan, - CAST(COALESCE(SUM(CASE WHEN d.mon = 2 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyFeb, - CAST(COALESCE(SUM(CASE WHEN d.mon = 3 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMar, - CAST(COALESCE(SUM(CASE WHEN d.mon = 4 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyApr, - CAST(COALESCE(SUM(CASE WHEN d.mon = 5 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMay, - CAST(COALESCE(SUM(CASE WHEN d.mon = 6 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJun, - CAST(COALESCE(SUM(CASE WHEN d.mon = 7 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJul, - CAST(COALESCE(SUM(CASE WHEN d.mon = 8 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyAug, - CAST(COALESCE(SUM(CASE WHEN d.mon = 9 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtySep, - CAST(COALESCE(SUM(CASE WHEN d.mon = 10 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyOct, - CAST(COALESCE(SUM(CASE WHEN d.mon = 11 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyNov, - CAST(COALESCE(SUM(CASE WHEN d.mon = 12 AND d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyDec, - -- Keep as CHAR for Jasper compatibility (previous template expects String). - CAST(COALESCE(SUM(CASE WHEN d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) AS CHAR) as totalProductionQty + MAX(d.stockSubCategory) AS stockSubCategory, + d.itemNo AS itemNo, + MAX(d.itemName) AS itemName, + MAX(d.unitOfMeasure) AS unitOfMeasure, + CAST(COALESCE(SUM(CASE WHEN d.mon = 1 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyJan, + CAST(COALESCE(SUM(CASE WHEN d.mon = 2 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyFeb, + CAST(COALESCE(SUM(CASE WHEN d.mon = 3 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyMar, + CAST(COALESCE(SUM(CASE WHEN d.mon = 4 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyApr, + CAST(COALESCE(SUM(CASE WHEN d.mon = 5 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyMay, + CAST(COALESCE(SUM(CASE WHEN d.mon = 6 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyJun, + CAST(COALESCE(SUM(CASE WHEN d.mon = 7 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyJul, + CAST(COALESCE(SUM(CASE WHEN d.mon = 8 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyAug, + CAST(COALESCE(SUM(CASE WHEN d.mon = 9 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtySep, + CAST(COALESCE(SUM(CASE WHEN d.mon = 10 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyOct, + CAST(COALESCE(SUM(CASE WHEN d.mon = 11 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyNov, + CAST(COALESCE(SUM(CASE WHEN d.mon = 12 THEN d.linePutAwayQty ELSE 0 END), 0) AS DECIMAL(18,2)) AS qtyDec, + CAST(COALESCE(SUM(d.linePutAwayQty), 0) AS CHAR) AS totalProductionQty FROM dedup d GROUP BY d.itemNo - HAVING COALESCE(SUM(CASE WHEN d.jobOrderId IS NOT NULL AND TRIM(CAST(d.jobOrderId AS CHAR)) <> '' THEN d.acceptedQty ELSE 0 END), 0) > 0 + HAVING COALESCE(SUM(d.linePutAwayQty), 0) > 0 ORDER BY d.itemNo """.trimIndent() - + return jdbcDao.queryForList(sql, args) } @@ -208,25 +197,15 @@ class SemiFGProductionAnalysisReportService( val args = mutableMapOf() val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { - // Handle multiple categories (comma-separated) - val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" } - if (categories.isNotEmpty()) { - val conditions = categories.mapIndexed { index, cat -> - val paramName = "stockCategory_$index" - args[paramName] = cat - "b.description = :$paramName" - } - "AND (${conditions.joinToString(" OR ")})" - } else { - "" - } + buildMultiValueExactClause(stockCategory, "it.type", "semiFgCodesSc", args) } else { "" } val sql = """ - SELECT DISTINCT b.code, COALESCE(b.name, '') as name + SELECT DISTINCT b.code, COALESCE(it.name, b.name, '') AS name FROM bom b + INNER JOIN items it ON it.code = b.code AND it.deleted = false WHERE b.deleted = false AND b.code IS NOT NULL AND b.code != '' @@ -255,25 +234,15 @@ class SemiFGProductionAnalysisReportService( val args = mutableMapOf() val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { - // Handle multiple categories (comma-separated) - val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" } - if (categories.isNotEmpty()) { - val conditions = categories.mapIndexed { index, cat -> - val paramName = "stockCategory_$index" - args[paramName] = cat - "b.description = :$paramName" - } - "AND (${conditions.joinToString(" OR ")})" - } else { - "" - } + buildMultiValueExactClause(stockCategory, "it.type", "semiFgCodesCatSc", args) } else { "" } val sql = """ - SELECT DISTINCT b.code, COALESCE(b.description, '') as category, COALESCE(b.name, '') as name + SELECT DISTINCT b.code, COALESCE(it.type, '') AS category, COALESCE(it.name, b.name, '') AS name FROM bom b + INNER JOIN items it ON it.code = b.code AND it.deleted = false WHERE b.deleted = false AND b.code IS NOT NULL AND b.code != '' diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt index 31475a5..2362797 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt @@ -225,7 +225,7 @@ class SemiFGProductionAnalysisReportController( "十月", "十一月", "十二月", - "總和" + "上架總計" ) val headerRow = sheet.createRow(rowIndex++) diff --git a/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml b/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml index fb5a959..67a25fe 100644 --- a/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml +++ b/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml @@ -535,7 +535,7 @@ - + diff --git a/src/main/resources/jasper/StockInTraceabilityReport.jrxml b/src/main/resources/jasper/StockInTraceabilityReport.jrxml index a3ce02c..6ee11df 100644 --- a/src/main/resources/jasper/StockInTraceabilityReport.jrxml +++ b/src/main/resources/jasper/StockInTraceabilityReport.jrxml @@ -87,7 +87,7 @@ - + @@ -159,7 +159,7 @@ - +