From ea3a56dfe43951db2314ae40dfa777e157ddae04 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Mon, 23 Mar 2026 18:27:08 +0800 Subject: [PATCH] ItemQCFailReport Excel Version --- .../report/web/ItemQcFailReportController.kt | 282 +++++++++++++++++- 1 file changed, 279 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt index 15e7741..577cc28 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt @@ -1,13 +1,22 @@ package com.ffii.fpsms.modules.report.web -import net.sf.jasperreports.engine.* import org.springframework.http.* import org.springframework.web.bind.annotation.* import java.time.LocalDate import java.time.LocalTime import java.time.format.DateTimeFormatter +import java.math.BigDecimal import com.ffii.fpsms.modules.report.service.ItemQcFailReportService import com.ffii.fpsms.modules.report.service.ReportService +import org.apache.poi.ss.usermodel.BorderStyle +import org.apache.poi.ss.usermodel.FillPatternType +import org.apache.poi.ss.usermodel.HorizontalAlignment +import org.apache.poi.ss.usermodel.IndexedColors +import org.apache.poi.ss.usermodel.VerticalAlignment +import org.apache.poi.ss.usermodel.DataFormat +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.ss.util.WorkbookUtil +import org.apache.poi.xssf.usermodel.XSSFWorkbook @RestController @RequestMapping("/report") @@ -27,13 +36,12 @@ class ItemQcFailReportController( val parameters = mutableMapOf() parameters["stockCategory"] = stockCategory ?: "All" - parameters["stockSubCategory"] = stockCategory ?: "All" // 你定义 stock sub category = items.type + parameters["stockSubCategory"] = stockCategory ?: "All" parameters["itemNo"] = itemCode ?: "All" parameters["year"] = java.time.LocalDate.now().year.toString() parameters["reportDate"] = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")) parameters["reportTime"] = java.time.LocalTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")) - // jrxml 里有这些参数,先给空 parameters["storeLocation"] = "" parameters["balanceFilterStart"] = "" parameters["balanceFilterEnd"] = "" @@ -62,6 +70,274 @@ class ItemQcFailReportController( } return org.springframework.http.ResponseEntity(pdfBytes, headers, org.springframework.http.HttpStatus.OK) } + + @GetMapping("/print-item-qc-fail-excel") + fun exportItemQcFailReportExcel( + @RequestParam(required = false) stockCategory: String?, + @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) lastInDateStart: String?, + @RequestParam(required = false) lastInDateEnd: String?, + ): ResponseEntity { + val dbData = itemQcFailReportService.searchItemQcFailReport( + stockCategory = stockCategory, + itemCode = itemCode, + lastInDateStart = lastInDateStart, + lastInDateEnd = lastInDateEnd, + ) + + val reportTitle = "庫存品質檢測報告" + val excelBytes = createItemQcFailExcel( + dbData = dbData, + reportTitle = reportTitle, + lastInDateStart = lastInDateStart, + lastInDateEnd = lastInDateEnd + ) + + val headers = org.springframework.http.HttpHeaders().apply { + contentType = org.springframework.http.MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + setContentDispositionFormData("attachment", "ItemQCFailReport.xlsx") + set("filename", "ItemQCFailReport.xlsx") + } + return org.springframework.http.ResponseEntity(excelBytes, headers, org.springframework.http.HttpStatus.OK) + } + + private fun createItemQcFailExcel( + dbData: List>, + reportTitle: String, + lastInDateStart: String?, + lastInDateEnd: String? + ): ByteArray { + val workbook: Workbook = XSSFWorkbook() + val safeSheetName = WorkbookUtil.createSafeSheetName(reportTitle) + val sheet = workbook.createSheet(safeSheetName) + + var rowIndex = 0 + val columnCount = 14 + + val titleStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.CENTER + verticalAlignment = VerticalAlignment.CENTER + } + val titleFont = workbook.createFont().apply { + bold = true + fontHeightInPoints = 16 + } + titleStyle.setFont(titleFont) + + // Title row + run { + val titleRowIndex = rowIndex++ + val row = sheet.createRow(titleRowIndex) + val cell = row.createCell(0) + cell.setCellValue(reportTitle) + cell.cellStyle = titleStyle + // Merge title across columns so the text won't be blocked by narrow col A. + sheet.addMergedRegion( + org.apache.poi.ss.util.CellRangeAddress( + titleRowIndex, + titleRowIndex, + 0, + columnCount - 1 + ) + ) + row.heightInPoints = 24f + } + + // Info row + run { + val row = sheet.createRow(rowIndex++) + val cell = row.createCell(0) + val startTxt = lastInDateStart ?: "" + val endTxt = lastInDateEnd ?: "" + cell.setCellValue("QC 不合格日期: ${startTxt} - ${endTxt}") + } + + val headerStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.CENTER + verticalAlignment = VerticalAlignment.TOP + wrapText = true + fillForegroundColor = IndexedColors.GREY_25_PERCENT.index + fillPattern = FillPatternType.SOLID_FOREGROUND + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + val headerFont = workbook.createFont().apply { bold = true } + headerStyle.setFont(headerFont) + + val textStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.LEFT + verticalAlignment = VerticalAlignment.CENTER + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + + val wrappedTextStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.LEFT + verticalAlignment = VerticalAlignment.TOP + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + wrapText = true + } + + val integerNumberStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.RIGHT + verticalAlignment = VerticalAlignment.CENTER + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + val df = workbook.creationHelper.createDataFormat() + dataFormat = df.getFormat("#,##0") + } + + val decimalNumberStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.RIGHT + verticalAlignment = VerticalAlignment.CENTER + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + val df = workbook.creationHelper.createDataFormat() + dataFormat = df.getFormat("#,##0.##") + } + + val headers = listOf( + "庫存類別", + "物料編號", + "物料名稱", + "單位", + "批號", + "到期日", + "QC 類型", + "QC 範本", + "不合格標準", + "批量", + "不合格數量", + "參考資料", + "備註", + "訂單/工單" + ) + + // Header row + run { + val row = sheet.createRow(rowIndex++) + headers.forEachIndexed { col, h -> + val cell = row.createCell(col) + cell.setCellValue(h) + cell.cellStyle = headerStyle + } + // Increase header row height so wrapped header text won't be blocked/truncated. + row.heightInPoints = 35f + } + + dbData.forEach { r -> + val row = sheet.createRow(rowIndex++) + fun writeText(col: Int, value: Any?) { + val cell = row.createCell(col) + cell.setCellValue(value?.toString() ?: "") + cell.cellStyle = textStyle + } + + fun writeNumber(col: Int, value: Any?) { + val raw = value?.toString()?.trim() ?: "" + // Some SQL strings may end with "."; Excel would display it as "4." otherwise. + val cleaned = raw.removeSuffix(".") + val bd = cleaned.toBigDecimalOrNull() + val cell = row.createCell(col) + + if (bd == null) { + cell.setCellValue(cleaned) + cell.cellStyle = textStyle + } else { + val stripped = bd.stripTrailingZeros() + if (stripped.scale() <= 0) { + // Render integer without decimal dot. + cell.setCellValue(stripped.toDouble()) + cell.cellStyle = integerNumberStyle + } else { + cell.setCellValue(stripped.toDouble()) + cell.cellStyle = decimalNumberStyle + } + } + } + + // Keys must match ItemQcFailReportService SQL aliases + writeText(0, r["stockSubCategory"]) + writeText(1, r["itemNo"]) + writeText(2, r["itemName"]) + writeText(3, r["unitOfMeasure"]) + writeText(4, r["lotNo"]) + writeText(5, r["expiryDate"]) + writeText(6, r["qcType"]) + writeText(7, r["qcTemplate"]) + val defectCriteria = r["qcDefectCriteria"] + // Wrap this column because values can be long. + run { + val cell = row.createCell(8) + cell.setCellValue(defectCriteria?.toString() ?: "") + cell.cellStyle = wrappedTextStyle + } + writeNumber(9, r["lotQty"]) + writeNumber(10, r["defectQty"]) + writeText(11, r["refData"]) + writeText(12, r["remark"]) + writeText(13, r["orderRefNo"]) + + // If "不合格標準" is long, increase row height so wrapped text is visible. + val defectText = defectCriteria?.toString() ?: "" + // Estimate wrap lines using the known column width (approx chars per line). + // This avoids ugly/uneven row heights from simple length thresholds. + val charsPerLine = 38.0 + val lines = maxOf( + 1, + kotlin.math.ceil(defectText.length / charsPerLine).toInt() + ) + val baseHeight = 18f // single-line + val lineHeight = 13.5f // additional lines + row.heightInPoints = (baseHeight + (lines - 1) * lineHeight).coerceIn(18f, 110f) + } + + // Column widths: keep reasonable defaults, then auto-size a bit (optional) + run { + // Set explicit column widths (Excel units = 1/256th of a character width). + val widths = intArrayOf( + 12, // stockSubCategory + 16, // itemNo + 18, // itemName + 10, // unit + 14, // lotNo + 12, // expiryDate + 10, // qcType + 20, // qcTemplate + 42, // qcDefectCriteria (wrap) + 12, // lotQty + 14, // defectQty + 22, // refData + 16, // remark + 18, // orderRefNo + ) + for (col in 0 until widths.size) { + sheet.setColumnWidth(col, widths[col] * 256) + } + } + + return workbookToByteArray(workbook) + } + + private fun workbookToByteArray(workbook: Workbook): ByteArray { + val out = java.io.ByteArrayOutputStream() + workbook.write(out) + workbook.close() + return out.toByteArray() + } }