diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/FGStockOutTraceabilityReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/FGStockOutTraceabilityReportService.kt new file mode 100644 index 0000000..7c0857e --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/service/FGStockOutTraceabilityReportService.kt @@ -0,0 +1,252 @@ +package com.ffii.fpsms.modules.report.service + +import com.ffii.core.support.JdbcDao +import org.springframework.stereotype.Service + +@Service +class FGStockOutTraceabilityReportService( + private val jdbcDao: JdbcDao, +) { + fun getDistinctHandlersForFGStockOutTraceability(): List { + val sql = """ + SELECT DISTINCT COALESCE(picker_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 = 'do' + LEFT JOIN user picker_user ON sol.handled_by = picker_user.id AND picker_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?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, + handler: String?, + ): List> { + val args = mutableMapOf() + + val stockCategorySql = buildMultiValueExactClause( + stockCategory, + "it.type", + "stockCategory", + args, + ) + + // Keep parameter for API compatibility; currently no SQL filter is needed. + val stockSubCategorySql = "" + + val itemCodeSql = buildMultiValueLikeClause( + itemCode, + "it.code", + "itemCode", + args, + ) + + val yearSql = if (!year.isNullOrBlank()) { + args["year"] = year + "AND YEAR(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) = :year" + } else { + "" + } + + val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { + val formattedDate = lastOutDateStart.replace("/", "-") + args["lastOutDateStart"] = formattedDate + "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) >= DATE(:lastOutDateStart)" + } else { + "" + } + + val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { + val formattedDate = lastOutDateEnd.replace("/", "-") + args["lastOutDateEnd"] = formattedDate + "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)" + } else { + "" + } + + val handlerSql = buildMultiValueExactClause( + handler, + "COALESCE(picker_user.name, modified_user.name, '')", + "handler", + args, + ) + + val sql = """ + SELECT + IFNULL(DATE_FORMAT( + IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate), + '%Y-%m-%d' + ), '') AS deliveryDate, + IFNULL(it.code, '') AS itemNo, + IFNULL(it.name, '') AS itemName, + IFNULL(uc.udfudesc, '') AS unitOfMeasure, + IFNULL(dpor.deliveryNoteCode, '') AS dnNo, + CAST(IFNULL(sp.id, 0) AS CHAR) AS customerId, + IFNULL(sp.name, '') AS customerName, + FORMAT( + ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0 + ) AS qtyNumeric, + FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS qty, + '' AS truckNo, + '' AS driver, + IFNULL(do.code, '') AS deliveryOrderNo, + IFNULL(po.code, '') AS fgPickOrderNo, + IFNULL(po.code, '') 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, + COALESCE( + picker_user.name, + modified_user.name, + '' + ) AS handler, + COALESCE( + picker_user.name, + modified_user.name, + '' + ) AS pickedBy, + GROUP_CONCAT(DISTINCT wh.code ORDER BY wh.code SEPARATOR ', ') AS storeLocation, + '' AS pickRemark, + FORMAT( + ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0 + ) AS totalStockOutQty, + 0 AS stockSubCategory + FROM do_pick_order_line_record dpolr + LEFT JOIN do_pick_order_record dpor + ON dpolr.record_id = dpor.id + AND dpor.deleted = 0 + AND dpor.ticket_status = 'completed' + INNER JOIN delivery_order do + ON dpolr.do_order_id = do.id + AND do.deleted = 0 + LEFT JOIN shop sp + ON do.shopId = sp.id + AND sp.deleted = 0 + LEFT JOIN delivery_order_line dol + ON do.id = dol.deliveryOrderId + AND dol.deleted = 0 + LEFT JOIN items it + ON dol.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 pick_order_line pol + ON dpolr.pick_order_id = pol.poId + AND pol.itemId = it.id + AND pol.deleted = 0 + LEFT JOIN pick_order po + ON pol.poId = po.id + AND po.deleted = 0 + LEFT JOIN stock_out_line sol + ON pol.id = sol.pickOrderLineId + AND sol.itemId = it.id + AND sol.deleted = 0 + LEFT JOIN stock_out so + ON sol.stockOutId = so.id + AND so.deleted = 0 + AND so.type = 'do' + 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 user modified_user + ON sol.modifiedBy = modified_user.staffNo + AND modified_user.deleted = 0 + AND sol.handled_by IS NULL + WHERE + dpolr.deleted = 0 + $stockCategorySql + $stockSubCategorySql + $itemCodeSql + $yearSql + $lastOutDateStartSql + $lastOutDateEndSql + $handlerSql + GROUP BY + sol.id, + dpor.RequiredDeliveryDate, + do.estimatedArrivalDate, + it.code, + it.name, + uc.udfudesc, + dpor.deliveryNoteCode, + sp.id, + sp.name, + sol.qty, + picker_user.name, + modified_user.name, + po.code, + do.code, + il.lotNo, + il.expiryDate + ORDER BY + it.code, + deliveryDate, + il.lotNo +""".trimIndent() + + return jdbcDao.queryForList(sql, args) + } + + 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 37b38dd..605c08c 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 @@ -272,225 +272,6 @@ return result } - fun getDistinctHandlersForFGStockOutTraceability(): List { - val sql = """ - SELECT DISTINCT COALESCE(picker_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 = 'do' - LEFT JOIN user picker_user ON sol.handled_by = picker_user.id AND picker_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?, - itemCode: String?, - year: String?, - lastOutDateStart: String?, - lastOutDateEnd: String?, - handler: String? -): List> { - val args = mutableMapOf() - - // Stock Category 过滤:通过 items.type - val stockCategorySql = if (!stockCategory.isNullOrBlank()) { - val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() } - if (categories.isNotEmpty()) { - val conditions = categories.mapIndexed { index, cat -> - val paramName = "stockCategory_$index" - args[paramName] = cat - "it.type = :$paramName" - } - "AND (${conditions.joinToString(" OR ")})" - } else { - "" - } - } else { - "" - } - - // 移除 stockSubCategory 过滤(不需要) - - val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args) - - val yearSql = if (!year.isNullOrBlank()) { - args["year"] = year - "AND YEAR(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) = :year" - } else { - "" - } - - val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { - val formattedDate = lastOutDateStart.replace("/", "-") - args["lastOutDateStart"] = formattedDate - "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) >= DATE(:lastOutDateStart)" - } else "" - - val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { - val formattedDate = lastOutDateEnd.replace("/", "-") - args["lastOutDateEnd"] = formattedDate - "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)" - } else "" - - val handlerSql = buildMultiValueExactClause( - handler, - "COALESCE(picker_user.name, modified_user.name, '')", - "handler", - args - ) - - val sql = """ - SELECT - IFNULL(DATE_FORMAT( - IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate), - '%Y-%m-%d' - ), '') AS deliveryDate, - IFNULL(it.code, '') AS itemNo, - IFNULL(it.name, '') AS itemName, - IFNULL(uc.udfudesc, '') AS unitOfMeasure, - IFNULL(dpor.deliveryNoteCode, '') AS dnNo, - CAST(IFNULL(sp.id, 0) AS CHAR) AS customerId, - IFNULL(sp.name, '') AS customerName, - FORMAT( - ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0 - ) AS qtyNumeric, - FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS qty, - '' AS truckNo, - '' AS driver, - IFNULL(do.code, '') AS deliveryOrderNo, - IFNULL(po.code, '') AS fgPickOrderNo, - IFNULL(po.code, '') 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, - COALESCE( - picker_user.name, - modified_user.name, - '' - ) AS handler, - COALESCE( - picker_user.name, - modified_user.name, - '' - ) AS pickedBy, - GROUP_CONCAT(DISTINCT wh.code ORDER BY wh.code SEPARATOR ', ') AS storeLocation, - '' AS pickRemark, - FORMAT( - ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0 - ) AS totalStockOutQty, - 0 AS stockSubCategory - FROM do_pick_order_line_record dpolr - LEFT JOIN do_pick_order_record dpor - ON dpolr.record_id = dpor.id - AND dpor.deleted = 0 - AND dpor.ticket_status = 'completed' - INNER JOIN delivery_order do - ON dpolr.do_order_id = do.id - AND do.deleted = 0 - LEFT JOIN shop sp - ON do.shopId = sp.id - AND sp.deleted = 0 - LEFT JOIN delivery_order_line dol - ON do.id = dol.deliveryOrderId - AND dol.deleted = 0 - LEFT JOIN items it - ON dol.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 pick_order_line pol - ON dpolr.pick_order_id = pol.poId - AND pol.itemId = it.id - AND pol.deleted = 0 - LEFT JOIN pick_order po - ON pol.poId = po.id - AND po.deleted = 0 - LEFT JOIN stock_out_line sol - ON pol.id = sol.pickOrderLineId - AND sol.itemId = it.id - AND sol.deleted = 0 - LEFT JOIN stock_out so - ON sol.stockOutId = so.id - AND so.deleted = 0 - AND so.type = 'do' - 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 user modified_user - ON sol.modifiedBy = modified_user.staffNo - AND modified_user.deleted = 0 - AND sol.handled_by IS NULL - WHERE - dpolr.deleted = 0 - $stockCategorySql - $itemCodeSql - $yearSql - $lastOutDateStartSql - $lastOutDateEndSql - $handlerSql - GROUP BY - sol.id, - dpor.RequiredDeliveryDate, - do.estimatedArrivalDate, - it.code, - it.name, - uc.udfudesc, - dpor.deliveryNoteCode, - sp.id, - sp.name, - sol.qty, - picker_user.name, - modified_user.name, - po.code, - do.code, - il.lotNo, - il.expiryDate - ORDER BY - it.code, - deliveryDate, - il.lotNo -""".trimIndent() - - val result = jdbcDao.queryForList(sql, args) - - // 打印查询结果 - println("=== Query Result (Total: ${result.size} rows) ===") - result.take(50).forEachIndexed { index, row -> - println("Row $index:") - println(" deliveryDate: ${row["deliveryDate"]}") - println(" itemNo: ${row["itemNo"]}") - println(" itemName: ${row["itemName"]}") - println(" qty: ${row["qty"]}") - println(" qtyNumeric: ${row["qtyNumeric"]}") - println(" deliveryOrderNo: ${row["deliveryOrderNo"]}") - println(" dnNo: ${row["dnNo"]}") - println(" fgPickOrderNo: ${row["fgPickOrderNo"]}") - println(" pickedBy: ${row["pickedBy"]}") - 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/FGStockOutTraceabilityReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/FGStockOutTraceabilityReportController.kt new file mode 100644 index 0000000..2284d38 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/web/FGStockOutTraceabilityReportController.kt @@ -0,0 +1,74 @@ +package com.ffii.fpsms.modules.report.web + +import com.ffii.fpsms.modules.report.service.FGStockOutTraceabilityReportService +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 FGStockOutTraceabilityReportController( + private val reportService: ReportService, + private val fgStockOutTraceabilityReportService: FGStockOutTraceabilityReportService, +) { + @GetMapping("/fg-stock-out-traceability-handlers") + fun getFGStockOutTraceabilityHandlers(): List = + fgStockOutTraceabilityReportService.getDistinctHandlersForFGStockOutTraceability() + + @GetMapping("/print-fg-stock-out-traceability") + fun generateFGStockOutTraceabilityReport( + @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() + + 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 = fgStockOutTraceabilityReportService.searchFGStockOutTraceabilityReport( + stockCategory, + stockSubCategory, + itemCode, + year, + lastOutDateStart, + lastOutDateEnd, + handler, + ) + + val pdfBytes = reportService.createPdfResponse( + "/jasper/FGStockOutTraceabilityReport.jrxml", + parameters, + dbData, + ) + + val headers = HttpHeaders().apply { + contentType = MediaType.APPLICATION_PDF + setContentDispositionFormData("attachment", "FGStockOutTraceabilityReport.pdf") + set("filename", "FGStockOutTraceabilityReport.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 d94978c..0f157fe 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 @@ -165,58 +165,6 @@ class ReportController( return ResponseEntity(pdfBytes, headers, HttpStatus.OK) } - @GetMapping("/fg-stock-out-traceability-handlers") - fun getFGStockOutTraceabilityHandlers(): List = - reportService.getDistinctHandlersForFGStockOutTraceability() - - @GetMapping("/print-fg-stock-out-traceability") - fun generateFGStockOutTraceabilityReport( - @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.searchFGStockOutTraceabilityReport( - stockCategory, - stockSubCategory, - itemCode, - year, - lastOutDateStart, - lastOutDateEnd, - handler - ) - - val pdfBytes = reportService.createPdfResponse( - "/jasper/FGStockOutTraceabilityReport.jrxml", - parameters, - dbData - ) - - val headers = HttpHeaders().apply { - contentType = MediaType.APPLICATION_PDF - setContentDispositionFormData("attachment", "FGStockOutTraceabilityReport.pdf") - set("filename", "FGStockOutTraceabilityReport.pdf") - } - - return ResponseEntity(pdfBytes, headers, HttpStatus.OK) - } @GetMapping("/print-stock-balance") fun generateStockBalanceReport( @RequestParam(required = false) stockCategory: String?,