From 07cc0e8389b5f2e43321f4d7e0b4e772829f2d05 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Fri, 20 Mar 2026 16:34:38 +0800 Subject: [PATCH] MaterialStockOutTraceabilityReport Update --- ...terialStockOutTraceabilityReportService.kt | 235 ++++++++++++++++++ .../modules/report/service/ReportService.kt | 187 -------------- ...ialStockOutTraceabilityReportController.kt | 76 ++++++ .../modules/report/web/ReportController.kt | 52 ---- 4 files changed, 311 insertions(+), 239 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/report/service/MaterialStockOutTraceabilityReportService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/MaterialStockOutTraceabilityReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/MaterialStockOutTraceabilityReportService.kt new file mode 100644 index 0000000..5facee6 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/service/MaterialStockOutTraceabilityReportService.kt @@ -0,0 +1,235 @@ +package com.ffii.fpsms.modules.report.service + +import com.ffii.core.support.JdbcDao +import org.springframework.stereotype.Service + +@Service +class MaterialStockOutTraceabilityReportService( + private val jdbcDao: JdbcDao, +) { + fun getDistinctHandlersForMaterialStockOutTraceability(): List { + val sql = """ + SELECT DISTINCT COALESCE(picker_user.name, jpo_handler_user.name, created_user.name, modified_user.name, '') AS handler + FROM stock_out_line sol + INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0 AND so.type = 'job' + INNER JOIN pick_order_line pol ON sol.pickOrderLineId = pol.id AND pol.deleted = 0 + INNER JOIN pick_order po ON pol.poId = po.id AND po.deleted = 0 AND po.type IN ('jo', 'JOB_ORDER') AND po.joId IS NOT NULL + LEFT JOIN user picker_user ON sol.handled_by = picker_user.id AND picker_user.deleted = 0 + LEFT JOIN jo_pick_order jpo ON po.id = jpo.pick_order_id AND pol.itemId = jpo.item_id AND jpo.deleted = 0 + LEFT JOIN user jpo_handler_user ON jpo.handled_by = jpo_handler_user.id AND jpo_handler_user.deleted = 0 + LEFT JOIN user created_user ON sol.createdBy = created_user.username AND created_user.deleted = 0 + LEFT JOIN user modified_user ON sol.modifiedBy = modified_user.staffNo AND modified_user.deleted = 0 AND sol.handled_by IS NULL + WHERE sol.deleted = 0 + ORDER BY handler + """.trimIndent() + + return jdbcDao + .queryForList(sql, emptyMap()) + .map { row -> (row["handler"]?.toString() ?: "").trim() } + .filter { it.isNotBlank() } + } + + fun searchMaterialStockOutTraceabilityReport( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, + handler: String?, + ): List> { + val args = mutableMapOf() + + // Stock Category filtering (items.type) + val stockCategorySql = buildMultiValueExactClause( + stockCategory, + "it.type", + "stockCategory", + args, + ) + + // Keep as-is (no filter yet) + val stockSubCategorySql = "" + + val itemCodeSql = buildMultiValueLikeClause( + itemCode, + "it.code", + "itemCode", + args, + ) + + // Filter by sol.endTime year + val yearSql = if (!year.isNullOrBlank()) { + args["year"] = year + "AND YEAR(sol.endTime) = :year" + } else "" + + val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { + val formattedDate = lastOutDateStart.replace("/", "-") + args["lastOutDateStart"] = formattedDate + "AND DATE(sol.endTime) >= DATE(:lastOutDateStart)" + } else "" + + val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { + val formattedDate = lastOutDateEnd.replace("/", "-") + args["lastOutDateEnd"] = formattedDate + "AND DATE(sol.endTime) <= DATE(:lastOutDateEnd)" + } else "" + + val handlerSql = buildMultiValueExactClause( + handler, + "COALESCE(picker_user.name, jpo_handler_user.name, created_user.name, modified_user.name, '')", + "handler", + args, + ) + + val sql = """ + SELECT + IFNULL(it.code, '') AS itemNo, + IFNULL(it.name, '') AS itemName, + IFNULL(it.categoryId, 0) AS stockSubCategory, + IFNULL(uc.udfudesc, '') AS unitOfMeasure, + IFNULL(jo.code, '') AS jobOrderNo, + IFNULL(po.consoCode, '') AS stockReqNo, + IFNULL(il.lotNo, '') AS lotNo, + IFNULL(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate, + FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS stockOutQty, + IFNULL(po.code, '') AS materialPickOrderNo, + COALESCE( + picker_user.name, + jpo_handler_user.name, + created_user.name, + modified_user.name, + '' + ) AS handler, + COALESCE(wh.code, '') AS storeLocation, + '' AS pickRemark, + FORMAT( + ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0 + ) AS totalStockOutQty + FROM stock_out_line sol + INNER JOIN stock_out so + ON sol.stockOutId = so.id + AND so.deleted = 0 + AND so.type = 'job' + INNER JOIN pick_order_line pol + ON sol.pickOrderLineId = pol.id + AND pol.deleted = 0 + INNER JOIN pick_order po + ON pol.poId = po.id + AND po.deleted = 0 + AND po.type IN ('jo', 'JOB_ORDER') + AND po.joId IS NOT NULL + LEFT JOIN job_order jo + ON po.joId = jo.id + AND jo.deleted = 0 + INNER JOIN items it + ON sol.itemId = it.id + AND it.deleted = 0 + LEFT JOIN item_uom iu + ON it.id = iu.itemId + AND iu.stockUnit = 1 + LEFT JOIN uom_conversion uc + ON iu.uomId = uc.id + LEFT JOIN inventory_lot_line ill + ON sol.inventoryLotLineId = ill.id + AND ill.deleted = 0 + LEFT JOIN inventory_lot il + ON ill.inventoryLotId = il.id + AND il.deleted = 0 + LEFT JOIN warehouse wh + ON ill.warehouseId = wh.id + AND wh.deleted = 0 + LEFT JOIN user picker_user + ON sol.handled_by = picker_user.id + AND picker_user.deleted = 0 + LEFT JOIN jo_pick_order jpo + ON po.id = jpo.pick_order_id + AND pol.itemId = jpo.item_id + AND jpo.deleted = 0 + LEFT JOIN user jpo_handler_user + ON jpo.handled_by = jpo_handler_user.id + AND jpo_handler_user.deleted = 0 + LEFT JOIN user created_user + ON sol.createdBy = created_user.username + AND created_user.deleted = 0 + LEFT JOIN user modified_user + ON sol.modifiedBy = modified_user.staffNo + AND modified_user.deleted = 0 + AND sol.handled_by IS NULL + WHERE + sol.deleted = 0 + AND (sol.inventoryLotLineId IS NULL OR ill.id IS NOT NULL) + $stockCategorySql + $stockSubCategorySql + $itemCodeSql + $yearSql + $lastOutDateStartSql + $lastOutDateEndSql + $handlerSql + ORDER BY + it.code, + il.lotNo, + sol.endTime + """.trimIndent() + + val result = jdbcDao.queryForList(sql, args) + + println("=== Material Stock Out Traceability (Total: ${result.size} rows) ===") + result.take(50).forEachIndexed { index, row -> + println("Row $index:") + println(" itemNo: ${row["itemNo"]}") + println(" itemName: ${row["itemName"]}") + println(" jobOrderNo: ${row["jobOrderNo"]}") + println(" stockOutQty: ${row["stockOutQty"]}") + println(" totalStockOutQty: ${row["totalStockOutQty"]}") + println(" materialPickOrderNo: ${row["materialPickOrderNo"]}") + println(" handler: ${row["handler"]}") + println(" storeLocation: ${row["storeLocation"]}") + println(" ---") + } + + return result + } + + private fun buildMultiValueLikeClause( + paramValue: String?, + columnName: String, + paramPrefix: String, + args: MutableMap, + ): String { + if (paramValue.isNullOrBlank()) return "" + + val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() } + if (values.isEmpty()) return "" + + val conditions = values.mapIndexed { index, value -> + val paramName = "${paramPrefix}_$index" + args[paramName] = "%$value%" + "$columnName LIKE :$paramName" + } + + return "AND (${conditions.joinToString(" OR ")})" + } + + private fun buildMultiValueExactClause( + paramValue: String?, + columnName: String, + paramPrefix: String, + args: MutableMap, + ): String { + if (paramValue.isNullOrBlank()) return "" + + val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() } + if (values.isEmpty()) return "" + + val conditions = values.mapIndexed { index, value -> + val paramName = "${paramPrefix}_$index" + args[paramName] = value + "$columnName = :$paramName" + } + + return "AND (${conditions.joinToString(" OR ")})" + } +} + 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 9fad3ef..fdb047b 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 @@ -282,24 +282,6 @@ return result return jdbcDao.queryForList(sql, emptyMap()).map { row -> (row["handler"]?.toString() ?: "").trim() }.filter { it.isNotBlank() } } - fun getDistinctHandlersForMaterialStockOutTraceability(): List { - val sql = """ - SELECT DISTINCT COALESCE(picker_user.name, jpo_handler_user.name, created_user.name, modified_user.name, '') AS handler - FROM stock_out_line sol - INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0 AND so.type = 'job' - INNER JOIN pick_order_line pol ON sol.pickOrderLineId = pol.id AND pol.deleted = 0 - INNER JOIN pick_order po ON pol.poId = po.id AND po.deleted = 0 AND po.type IN ('jo', 'JOB_ORDER') AND po.joId IS NOT NULL - LEFT JOIN user picker_user ON sol.handled_by = picker_user.id AND picker_user.deleted = 0 - LEFT JOIN jo_pick_order jpo ON po.id = jpo.pick_order_id AND pol.itemId = jpo.item_id AND jpo.deleted = 0 - LEFT JOIN user jpo_handler_user ON jpo.handled_by = jpo_handler_user.id AND jpo_handler_user.deleted = 0 - LEFT JOIN user created_user ON sol.createdBy = created_user.username AND created_user.deleted = 0 - LEFT JOIN user modified_user ON sol.modifiedBy = modified_user.staffNo AND modified_user.deleted = 0 AND sol.handled_by IS NULL - WHERE sol.deleted = 0 - ORDER BY handler - """.trimIndent() - return jdbcDao.queryForList(sql, emptyMap()).map { row -> (row["handler"]?.toString() ?: "").trim() }.filter { it.isNotBlank() } -} - fun searchFGStockOutTraceabilityReport( stockCategory: String?, stockSubCategory: String?, @@ -506,175 +488,6 @@ return result return result } -fun searchMaterialStockOutTraceabilityReport( - stockCategory: String?, - stockSubCategory: String?, - itemCode: String?, - year: String?, - lastOutDateStart: String?, - lastOutDateEnd: String?, - handler: String? -): List> { - val args = mutableMapOf() - - // Stock Category 过滤:通过 items.type - val stockCategorySql = buildMultiValueExactClause( - stockCategory, - "it.type", - "stockCategory", - args - ) - - val stockSubCategorySql = "" - - val itemCodeSql = buildMultiValueLikeClause( - itemCode, - "it.code", - "itemCode", - args - ) - - // 年份过滤:使用 sol.endTime 的年份 - val yearSql = if (!year.isNullOrBlank()) { - args["year"] = year - "AND YEAR(sol.endTime) = :year" - } else { - "" - } - - val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { - val formattedDate = lastOutDateStart.replace("/", "-") - args["lastOutDateStart"] = formattedDate - "AND DATE(sol.endTime) >= DATE(:lastOutDateStart)" - } else { - "" - } - - val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { - val formattedDate = lastOutDateEnd.replace("/", "-") - args["lastOutDateEnd"] = formattedDate - "AND DATE(sol.endTime) <= DATE(:lastOutDateEnd)" - } else { - "" - } - - val handlerSql = buildMultiValueExactClause( - handler, - "COALESCE(picker_user.name, jpo_handler_user.name, created_user.name, modified_user.name, '')", - "handler", - args - ) - - val sql = """ - SELECT - IFNULL(it.code, '') AS itemNo, - IFNULL(it.name, '') AS itemName, - IFNULL(it.categoryId, 0) AS stockSubCategory, - IFNULL(uc.udfudesc, '') AS unitOfMeasure, - IFNULL(jo.code, '') AS jobOrderNo, - IFNULL(po.consoCode, '') AS stockReqNo, - IFNULL(il.lotNo, '') AS lotNo, - IFNULL(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate, - FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS stockOutQty, - IFNULL(po.code, '') AS materialPickOrderNo, - COALESCE( - picker_user.name, - jpo_handler_user.name, - created_user.name, - modified_user.name, - '' - ) AS handler, - COALESCE(wh.code, '') AS storeLocation, - '' AS pickRemark, - FORMAT( - ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0 - ) AS totalStockOutQty - FROM stock_out_line sol - INNER JOIN stock_out so - ON sol.stockOutId = so.id - AND so.deleted = 0 - AND so.type = 'job' - INNER JOIN pick_order_line pol - ON sol.pickOrderLineId = pol.id - AND pol.deleted = 0 - INNER JOIN pick_order po - ON pol.poId = po.id - AND po.deleted = 0 - AND po.type IN ('jo', 'JOB_ORDER') - AND po.joId IS NOT NULL - LEFT JOIN job_order jo - ON po.joId = jo.id - AND jo.deleted = 0 - INNER JOIN items it - ON sol.itemId = it.id - AND it.deleted = 0 - LEFT JOIN item_uom iu - ON it.id = iu.itemId - AND iu.stockUnit = 1 - LEFT JOIN uom_conversion uc - ON iu.uomId = uc.id - LEFT JOIN inventory_lot_line ill - ON sol.inventoryLotLineId = ill.id - AND ill.deleted = 0 - LEFT JOIN inventory_lot il - ON ill.inventoryLotId = il.id - AND il.deleted = 0 - LEFT JOIN warehouse wh - ON ill.warehouseId = wh.id - AND wh.deleted = 0 - LEFT JOIN user picker_user - ON sol.handled_by = picker_user.id - AND picker_user.deleted = 0 - LEFT JOIN jo_pick_order jpo - ON po.id = jpo.pick_order_id - AND pol.itemId = jpo.item_id - AND jpo.deleted = 0 - LEFT JOIN user jpo_handler_user - ON jpo.handled_by = jpo_handler_user.id - AND jpo_handler_user.deleted = 0 - LEFT JOIN user created_user - ON sol.createdBy = created_user.username - AND created_user.deleted = 0 - LEFT JOIN user modified_user - ON sol.modifiedBy = modified_user.staffNo - AND modified_user.deleted = 0 - AND sol.handled_by IS NULL - WHERE - sol.deleted = 0 - AND (sol.inventoryLotLineId IS NULL OR ill.id IS NOT NULL) - $stockCategorySql - $stockSubCategorySql - $itemCodeSql - $yearSql - $lastOutDateStartSql - $lastOutDateEndSql - $handlerSql - ORDER BY - it.code, - il.lotNo, - sol.endTime - """.trimIndent() - val result = jdbcDao.queryForList(sql, args) - - println("=== Material Stock Out Traceability (Total: ${result.size} rows) ===") - result.take(50).forEachIndexed { index, row -> - println("Row $index:") - println(" itemNo: ${row["itemNo"]}") - println(" itemName: ${row["itemName"]}") - println(" jobOrderNo: ${row["jobOrderNo"]}") - println(" stockOutQty: ${row["stockOutQty"]}") - println(" totalStockOutQty: ${row["totalStockOutQty"]}") - println(" materialPickOrderNo: ${row["materialPickOrderNo"]}") - println(" handler: ${row["handler"]}") - println(" storeLocation: ${row["storeLocation"]}") - println(" ---") - } - if (result.size > 50) { - println("... (showing first 50 rows, total ${result.size} rows)") - } - - return result -} /** * Helper function to build SQL clause for comma-separated values. * Supports multiple values like "val1, val2, val3" and generates OR conditions with LIKE. diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt new file mode 100644 index 0000000..846f9cb --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt @@ -0,0 +1,76 @@ +package com.ffii.fpsms.modules.report.web + +import com.ffii.fpsms.modules.report.service.MaterialStockOutTraceabilityReportService +import com.ffii.fpsms.modules.report.service.ReportService +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@RestController +@RequestMapping("/report") +class MaterialStockOutTraceabilityReportController( + private val reportService: ReportService, + private val materialStockOutTraceabilityReportService: MaterialStockOutTraceabilityReportService, +) { + @GetMapping("/material-stock-out-traceability-handlers") + fun getMaterialStockOutTraceabilityHandlers(): List = + materialStockOutTraceabilityReportService.getDistinctHandlersForMaterialStockOutTraceability() + + @GetMapping("/print-material-stock-out-traceability") + fun generateMaterialStockOutTraceabilityReport( + @RequestParam(required = false) stockCategory: String?, + @RequestParam(required = false) stockSubCategory: String?, + @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) year: String?, + @RequestParam(required = false) lastOutDateStart: String?, + @RequestParam(required = false) lastOutDateEnd: String?, + @RequestParam(required = false) handler: String?, + ): ResponseEntity { + val parameters = mutableMapOf() + + // Set report header parameters + parameters["stockCategory"] = stockCategory ?: "All" + parameters["stockSubCategory"] = stockSubCategory ?: "All" + parameters["itemNo"] = itemCode ?: "All" + parameters["year"] = year ?: LocalDate.now().year.toString() + parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + parameters["lastOutDateStart"] = lastOutDateStart ?: "" + parameters["lastOutDateEnd"] = lastOutDateEnd ?: "" + parameters["deliveryPeriodStart"] = "" + parameters["deliveryPeriodEnd"] = "" + + val dbData = materialStockOutTraceabilityReportService.searchMaterialStockOutTraceabilityReport( + stockCategory, + stockSubCategory, + itemCode, + year, + lastOutDateStart, + lastOutDateEnd, + handler, + ) + + val pdfBytes = reportService.createPdfResponse( + "/jasper/MaterialStockOutTraceability.jrxml", + parameters, + dbData, + ) + + val headers = HttpHeaders().apply { + contentType = MediaType.APPLICATION_PDF + setContentDispositionFormData("attachment", "MaterialStockOutTraceabilityReport.pdf") + set("filename", "MaterialStockOutTraceabilityReport.pdf") + } + + return ResponseEntity(pdfBytes, headers, HttpStatus.OK) + } +} + diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt index f997622..54e935f 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt @@ -168,10 +168,6 @@ class ReportController( fun getFGStockOutTraceabilityHandlers(): List = reportService.getDistinctHandlersForFGStockOutTraceability() - @GetMapping("/material-stock-out-traceability-handlers") - fun getMaterialStockOutTraceabilityHandlers(): List = - reportService.getDistinctHandlersForMaterialStockOutTraceability() - @GetMapping("/print-fg-stock-out-traceability") fun generateFGStockOutTraceabilityReport( @RequestParam(required = false) stockCategory: String?, @@ -220,54 +216,6 @@ class ReportController( return ResponseEntity(pdfBytes, headers, HttpStatus.OK) } - @GetMapping("/print-material-stock-out-traceability") - fun generateMaterialStockOutTraceabilityReport( - @RequestParam(required = false) stockCategory: String?, - @RequestParam(required = false) stockSubCategory: String?, - @RequestParam(required = false) itemCode: String?, - @RequestParam(required = false) year: String?, - @RequestParam(required = false) lastOutDateStart: String?, - @RequestParam(required = false) lastOutDateEnd: String?, - @RequestParam(required = false) handler: String? - ): ResponseEntity { - val parameters = mutableMapOf() - - // Set report header parameters - parameters["stockCategory"] = stockCategory ?: "All" - parameters["stockSubCategory"] = stockSubCategory ?: "All" - parameters["itemNo"] = itemCode ?: "All" - parameters["year"] = year ?: LocalDate.now().year.toString() - parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) - parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) - parameters["lastOutDateStart"] = lastOutDateStart ?: "" - parameters["lastOutDateEnd"] = lastOutDateEnd ?: "" - parameters["deliveryPeriodStart"] = "" - parameters["deliveryPeriodEnd"] = "" - - val dbData = reportService.searchMaterialStockOutTraceabilityReport( - stockCategory, - stockSubCategory, - itemCode, - year, - lastOutDateStart, - lastOutDateEnd, - handler - ) - - val pdfBytes = reportService.createPdfResponse( - "/jasper/MaterialStockOutTraceability.jrxml", - parameters, - dbData - ) - - val headers = HttpHeaders().apply { - contentType = MediaType.APPLICATION_PDF - setContentDispositionFormData("attachment", "MaterialStockOutTraceabilityReport.pdf") - set("filename", "MaterialStockOutTraceabilityReport.pdf") - } - - return ResponseEntity(pdfBytes, headers, HttpStatus.OK) - } @GetMapping("/print-stock-balance") fun generateStockBalanceReport( @RequestParam(required = false) stockCategory: String?,