| @@ -62,7 +62,10 @@ class SemiFGProductionAnalysisReportService( | |||||
| * - Include only stock_in_line rows with non-null jobOrderId | * - Include only stock_in_line rows with non-null jobOrderId | ||||
| * - Exclude stock_in_line rows with status = 'Pending' | * - Exclude stock_in_line rows with status = 'Pending' | ||||
| * - stockCategory → items.type (exact, comma-separated); itemCode → items.code (LIKE, comma-separated) | * - 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) | |||||
| * - Date path: | |||||
| * stock_in_line.productLotNo (must be present) -> | |||||
| * stock_in_line.inventoryLotId -> inventory_lot.id -> | |||||
| * inventory_lot.stockInDate (used for month/year/date filters) | |||||
| * - Quantity source: stock_in_line.acceptedQty | * - Quantity source: stock_in_line.acceptedQty | ||||
| * - QC any fail → line qty 0 (same as traceability stockInQty) | * - 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) | * - One row per stockInLineId per month before pivot; all lines counted (not only job orders) | ||||
| @@ -89,7 +92,7 @@ class SemiFGProductionAnalysisReportService( | |||||
| val yearSql = if (!year.isNullOrBlank() && year != "All") { | val yearSql = if (!year.isNullOrBlank() && year != "All") { | ||||
| args["year"] = year | args["year"] = year | ||||
| "AND YEAR(si.productionDate) = :year" | |||||
| "AND YEAR(CASE WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate ELSE si.productionDate END) = :year" | |||||
| } else { | } else { | ||||
| "" | "" | ||||
| } | } | ||||
| @@ -97,13 +100,13 @@ class SemiFGProductionAnalysisReportService( | |||||
| val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { | val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { | ||||
| val formattedDate = lastOutDateStart.replace("/", "-") | val formattedDate = lastOutDateStart.replace("/", "-") | ||||
| args["lastOutDateStart"] = formattedDate | args["lastOutDateStart"] = formattedDate | ||||
| "AND si.productionDate IS NOT NULL AND DATE(si.productionDate) >= DATE(:lastOutDateStart)" | |||||
| "AND DATE(CASE WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate ELSE si.productionDate END) >= DATE(:lastOutDateStart)" | |||||
| } else "" | } else "" | ||||
| val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { | val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { | ||||
| val formattedDate = lastOutDateEnd.replace("/", "-") | val formattedDate = lastOutDateEnd.replace("/", "-") | ||||
| args["lastOutDateEnd"] = formattedDate | args["lastOutDateEnd"] = formattedDate | ||||
| "AND si.productionDate IS NOT NULL AND DATE(si.productionDate) <= DATE(:lastOutDateEnd)" | |||||
| "AND DATE(CASE WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate ELSE si.productionDate END) <= DATE(:lastOutDateEnd)" | |||||
| } else "" | } else "" | ||||
| val sql = """ | val sql = """ | ||||
| @@ -121,7 +124,12 @@ class SemiFGProductionAnalysisReportService( | |||||
| COALESCE(it.name, '') AS itemName, | COALESCE(it.name, '') AS itemName, | ||||
| COALESCE(ic.sub, '') AS stockSubCategory, | COALESCE(ic.sub, '') AS stockSubCategory, | ||||
| COALESCE(uc.udfudesc, '') AS unitOfMeasure, | COALESCE(uc.udfudesc, '') AS unitOfMeasure, | ||||
| MONTH(si.productionDate) AS mon, | |||||
| MONTH( | |||||
| CASE | |||||
| WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate | |||||
| ELSE si.productionDate | |||||
| END | |||||
| ) AS mon, | |||||
| si.id AS stockInLineId, | si.id AS stockInLineId, | ||||
| CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN 0 | CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN 0 | ||||
| ELSE COALESCE(si.acceptedQty, 0) | ELSE COALESCE(si.acceptedQty, 0) | ||||
| @@ -129,12 +137,20 @@ class SemiFGProductionAnalysisReportService( | |||||
| FROM stock_in_line si | FROM stock_in_line si | ||||
| INNER JOIN items it ON si.itemId = it.id | INNER JOIN items it ON si.itemId = it.id | ||||
| INNER JOIN bom b ON b.code = it.code AND b.deleted = false | INNER JOIN bom b ON b.code = it.code AND b.deleted = false | ||||
| LEFT JOIN inventory_lot il ON il.id = si.inventoryLotId AND il.deleted = false | |||||
| LEFT JOIN qr_agg ON qr_agg.stockInLineId = si.id | LEFT JOIN qr_agg ON qr_agg.stockInLineId = si.id | ||||
| LEFT JOIN item_category ic ON it.categoryId = ic.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 item_uom iu ON it.id = iu.itemId AND iu.stockUnit = true | ||||
| LEFT JOIN uom_conversion uc ON iu.uomId = uc.id | LEFT JOIN uom_conversion uc ON iu.uomId = uc.id | ||||
| WHERE si.deleted = false | WHERE si.deleted = false | ||||
| AND si.productionDate IS NOT NULL | |||||
| AND si.productLotNo IS NOT NULL | |||||
| AND TRIM(si.productLotNo) <> '' | |||||
| AND ( | |||||
| CASE | |||||
| WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate | |||||
| ELSE si.productionDate | |||||
| END | |||||
| ) IS NOT NULL | |||||
| AND si.jobOrderId IS NOT NULL | AND si.jobOrderId IS NOT NULL | ||||
| AND (si.status IS NULL OR si.status <> 'Pending') | AND (si.status IS NULL OR si.status <> 'Pending') | ||||
| $stockCategorySql | $stockCategorySql | ||||