| @@ -1,13 +1,22 @@ | |||||
| package com.ffii.fpsms.modules.report.web | package com.ffii.fpsms.modules.report.web | ||||
| import net.sf.jasperreports.engine.* | |||||
| import org.springframework.http.* | import org.springframework.http.* | ||||
| import org.springframework.web.bind.annotation.* | import org.springframework.web.bind.annotation.* | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.LocalTime | import java.time.LocalTime | ||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| import java.math.BigDecimal | |||||
| import com.ffii.fpsms.modules.report.service.ItemQcFailReportService | import com.ffii.fpsms.modules.report.service.ItemQcFailReportService | ||||
| import com.ffii.fpsms.modules.report.service.ReportService | 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 | @RestController | ||||
| @RequestMapping("/report") | @RequestMapping("/report") | ||||
| @@ -27,13 +36,12 @@ class ItemQcFailReportController( | |||||
| val parameters = mutableMapOf<String, Any>() | val parameters = mutableMapOf<String, Any>() | ||||
| parameters["stockCategory"] = stockCategory ?: "All" | parameters["stockCategory"] = stockCategory ?: "All" | ||||
| parameters["stockSubCategory"] = stockCategory ?: "All" // 你定义 stock sub category = items.type | |||||
| parameters["stockSubCategory"] = stockCategory ?: "All" | |||||
| parameters["itemNo"] = itemCode ?: "All" | parameters["itemNo"] = itemCode ?: "All" | ||||
| parameters["year"] = java.time.LocalDate.now().year.toString() | parameters["year"] = java.time.LocalDate.now().year.toString() | ||||
| parameters["reportDate"] = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")) | 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")) | parameters["reportTime"] = java.time.LocalTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")) | ||||
| // jrxml 里有这些参数,先给空 | |||||
| parameters["storeLocation"] = "" | parameters["storeLocation"] = "" | ||||
| parameters["balanceFilterStart"] = "" | parameters["balanceFilterStart"] = "" | ||||
| parameters["balanceFilterEnd"] = "" | parameters["balanceFilterEnd"] = "" | ||||
| @@ -62,6 +70,274 @@ class ItemQcFailReportController( | |||||
| } | } | ||||
| return org.springframework.http.ResponseEntity(pdfBytes, headers, org.springframework.http.HttpStatus.OK) | 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() | |||||
| } | |||||
| } | } | ||||