| @@ -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<String, Any>() | |||
| 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<ByteArray> { | |||
| 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<Map<String, Any>>, | |||
| 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() | |||
| } | |||
| } | |||