瀏覽代碼

update report

master
Tommy\2Fi-Staff 9 小時之前
父節點
當前提交
d19e3bcdd8
共有 5 個文件被更改,包括 90 次插入114 次删除
  1. +11
    -4
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  2. +75
    -106
      src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt
  3. +1
    -1
      src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt
  4. +1
    -1
      src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml
  5. +2
    -2
      src/main/resources/jasper/StockInTraceabilityReport.jrxml

+ 11
- 4
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


+ 75
- 106
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<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
// 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<String, Any>()
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<String, Any>()
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 != ''


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt 查看文件

@@ -225,7 +225,7 @@ class SemiFGProductionAnalysisReportController(
"十月",
"十一月",
"十二月",
"總和"
"上架總計"
)

val headerRow = sheet.createRow(rowIndex++)


+ 1
- 1
src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml 查看文件

@@ -535,7 +535,7 @@
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10" isBold="true"/>
</textElement>
<text><![CDATA[總和]]></text>
<text><![CDATA[上架總計]]></text>
</staticText>
<staticText>
<reportElement x="560" y="0" width="45" height="20" uuid="85c77a9b-c044-4bc2-8cd9-3b0058e4b74e">


+ 2
- 2
src/main/resources/jasper/StockInTraceabilityReport.jrxml 查看文件

@@ -87,7 +87,7 @@
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[總入倉數量:]]></text>
<text><![CDATA[總上架數量:]]></text>
</staticText>
<textField>
<reportElement x="280" y="0" width="50" height="18" uuid="d98c4478-22bd-4fd6-9be4-b3777f91de6d">
@@ -159,7 +159,7 @@
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[入庫數量]]></text>
<text><![CDATA[上架數量]]></text>
</staticText>
<staticText>
<reportElement stretchType="RelativeToTallestObject" x="10" y="80" width="110" height="28" uuid="3fa7c301-1c2a-430b-8985-338ebf7aa6cf">


Loading…
取消
儲存