From 4cb5d4cf477fcfac32edafc2e7683a8b452233c8 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 10 Feb 2026 11:28:33 +0800 Subject: [PATCH] update --- .../modules/report/service/ReportService.kt | 344 +++++++++++ .../modules/report/web/ReportController.kt | 93 ++- .../jasper/FGStockOutTraceabilityReport.jrxml | 538 ++++++++++++++++++ .../jasper/MaterialStockOutTraceability.jrxml | 534 +++++++++++++++++ 4 files changed, 1508 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/jasper/FGStockOutTraceabilityReport.jrxml create mode 100644 src/main/resources/jasper/MaterialStockOutTraceability.jrxml 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 fedf0ca..8d84a22 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 @@ -231,6 +231,7 @@ CAST(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 2) AS CHAR) AS qty, ON pol.id = sol.pickOrderLineId AND sol.itemId = it.id AND sol.deleted = 0 + AND sol.status = 'completed' LEFT JOIN inventory_lot_line ill ON sol.inventoryLotLineId = ill.id AND ill.deleted = 0 @@ -278,6 +279,349 @@ if (result.size > 50) { return result } + + fun searchFGStockOutTraceabilityReport( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: 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()) { + args["lastOutDateStart"] = lastOutDateStart + "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) >= :lastOutDateStart" + } else "" + + val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { + args["lastOutDateEnd"] = lastOutDateEnd + "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) < :lastOutDateEnd" + } else "" + + 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, + CAST( + SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code) AS DECIMAL(14,2) + ) AS qtyNumeric, + CAST(ROUND(IFNULL(sol.qty, 0), 2) AS CHAR) 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, + CAST(ROUND(IFNULL(sol.qty, 0), 2) AS CHAR) 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, + CAST( + SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code) AS CHAR + ) 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.pickerId = 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.pickerId IS NULL + WHERE + dpolr.deleted = 0 + $stockCategorySql + $itemCodeSql + $yearSql + $lastOutDateStartSql + $lastOutDateEndSql + 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, + 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 +} +fun searchMaterialStockOutTraceabilityReport( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: 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()) { + args["lastOutDateStart"] = lastOutDateStart + "AND DATE(sol.endTime) >= :lastOutDateStart" + } else { + "" + } + + val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { + args["lastOutDateEnd"] = lastOutDateEnd + "AND DATE(sol.endTime) < :lastOutDateEnd" + } else { + "" + } + + val sql = """ + SELECT + IFNULL(it.code, '') AS itemNo, + IFNULL(it.name, '') AS itemName, + IFNULL(it.categoryId, 0) AS stockSubCategory, + IFNULL(uc.udfudesc, '') AS unitOfMeasure, + '' AS jobOrderNo, + IFNULL(po.consoCode, '') AS stockReqNo, + IFNULL(il.lotNo, '') AS lotNo, + IFNULL(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate, + CAST(ROUND(IFNULL(sol.qty, 0), 2) AS CHAR) AS stockOutQty, + IFNULL(po.code, '') AS materialPickOrderNo, + COALESCE( + picker_user.name, + created_user.name, -- 新增:用 createdBy 找到的 user + modified_user.name, + '' + ) AS handler, + COALESCE(wh.code, '') AS storeLocation, + '' AS pickRemark, + CAST( + SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code) AS CHAR + ) 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 ('consumable', 'jo') + AND po.joId IS NULL + AND po.doId IS NULL + 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.pickerId = picker_user.id + AND picker_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.pickerId IS NULL + WHERE + sol.deleted = 0 + AND (sol.inventoryLotLineId IS NULL OR ill.id IS NOT NULL) + $stockCategorySql + $stockSubCategorySql + $itemCodeSql + $yearSql + $lastOutDateStartSql + $lastOutDateEndSql + 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(" 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/ReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt index e71dc35..87b2631 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 @@ -164,7 +164,98 @@ class ReportController( return ResponseEntity(pdfBytes, headers, HttpStatus.OK) } - + @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? + ): 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 + ) + + 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-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? + ): 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 + ) + + 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?, diff --git a/src/main/resources/jasper/FGStockOutTraceabilityReport.jrxml b/src/main/resources/jasper/FGStockOutTraceabilityReport.jrxml new file mode 100644 index 0000000..be6a93e --- /dev/null +++ b/src/main/resources/jasper/FGStockOutTraceabilityReport.jrxml @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/jasper/MaterialStockOutTraceability.jrxml b/src/main/resources/jasper/MaterialStockOutTraceability.jrxml new file mode 100644 index 0000000..a1f8da1 --- /dev/null +++ b/src/main/resources/jasper/MaterialStockOutTraceability.jrxml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +