diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt index 846f9cb..17834af 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt @@ -10,9 +10,21 @@ 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 org.apache.poi.ss.usermodel.BorderStyle +import org.apache.poi.ss.usermodel.CellStyle +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.Row +import org.apache.poi.ss.usermodel.VerticalAlignment +import org.apache.poi.ss.usermodel.DataFormat +import org.apache.poi.ss.util.CellRangeAddress +import org.apache.poi.ss.util.WorkbookUtil +import org.apache.poi.xssf.usermodel.XSSFWorkbook import java.time.LocalDate import java.time.LocalTime import java.time.format.DateTimeFormatter +import java.io.ByteArrayOutputStream @RestController @RequestMapping("/report") @@ -72,5 +84,299 @@ class MaterialStockOutTraceabilityReportController( return ResponseEntity(pdfBytes, headers, HttpStatus.OK) } + + @GetMapping("/print-material-stock-out-traceability-excel") + fun exportMaterialStockOutTraceabilityReportExcel( + @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 (keep consistent with PDF endpoint) + 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 = materialStockOutTraceabilityReportService.searchMaterialStockOutTraceabilityReport( + stockCategory, + stockSubCategory, + itemCode, + year, + lastOutDateStart, + lastOutDateEnd, + handler, + ) + + val excelBytes = createMaterialStockOutTraceabilityExcel( + dbData = dbData, + year = year, + lastOutDateStart = lastOutDateStart, + lastOutDateEnd = lastOutDateEnd, + ) + + val headers = HttpHeaders().apply { + contentType = MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + setContentDispositionFormData( + "attachment", + "MaterialStockOutTraceabilityReport.xlsx", + ) + set("filename", "MaterialStockOutTraceabilityReport.xlsx") + } + + return ResponseEntity(excelBytes, headers, HttpStatus.OK) + } + + private fun createMaterialStockOutTraceabilityExcel( + dbData: List>, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String?, + ): ByteArray { + val workbook = XSSFWorkbook() + val reportTitle = "物料出倉追蹤報告" + val safeSheetName = WorkbookUtil.createSafeSheetName(reportTitle) + val sheet = workbook.createSheet(safeSheetName) + + val totalColumns = 11 + var rowIndex = 0 + + val headerStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.CENTER + verticalAlignment = VerticalAlignment.CENTER + fillForegroundColor = IndexedColors.GREY_25_PERCENT.index + fillPattern = FillPatternType.SOLID_FOREGROUND + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + val font = workbook.createFont().apply { bold = true } + setFont(font) + } + + val textStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.LEFT + verticalAlignment = VerticalAlignment.CENTER + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + + val numberStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.RIGHT + verticalAlignment = VerticalAlignment.CENTER + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + val df: DataFormat = workbook.createDataFormat() + dataFormat = df.getFormat("#,##0") + } + + val dashStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.RIGHT + verticalAlignment = VerticalAlignment.CENTER + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + + val headers = listOf( + "貨品編號", + "貨品名稱", + "單位", + "工單編號", + "提料單號", + "批號", + "到期日", + "出貨數量", + "提料人", + "存貨位置", + "提料備註", + ) + + // Header row + val headerRow = sheet.createRow(rowIndex++) + headers.forEachIndexed { i, h -> + val cell = headerRow.createCell(i) + cell.setCellValue(h) + cell.cellStyle = headerStyle + } + + val summaryQtyThickBottomStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.RIGHT + verticalAlignment = VerticalAlignment.CENTER + val df: DataFormat = workbook.createDataFormat() + dataFormat = df.getFormat("#,##0") + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THICK + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + val font = workbook.createFont().apply { bold = true } + setFont(font) + } + + val summaryEmptyThickBottomStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.LEFT + verticalAlignment = VerticalAlignment.CENTER + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THICK + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + + val summaryLabelInExpiryColStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.RIGHT + verticalAlignment = VerticalAlignment.CENTER + val font = workbook.createFont().apply { bold = true } + setFont(font) + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THICK + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + + fun addItemSummaryRow(totalQty: Double, itemCode: String, itemName: String) { + val summaryRow = sheet.createRow(rowIndex++) + summaryRow.createCell(0).apply { + setCellValue(itemCode) + cellStyle = summaryEmptyThickBottomStyle + } + summaryRow.createCell(1).apply { + setCellValue(itemName) + cellStyle = summaryEmptyThickBottomStyle + } + for (col in 2..5) { + summaryRow.createCell(col).apply { + setCellValue("") + cellStyle = summaryEmptyThickBottomStyle + } + } + summaryRow.createCell(6).apply { + setCellValue("總出倉量 :") + cellStyle = summaryLabelInExpiryColStyle + } + summaryRow.createCell(7).apply { + setCellValue(totalQty) + cellStyle = summaryQtyThickBottomStyle + } + for (col in 8 until totalColumns) { + summaryRow.createCell(col).apply { + setCellValue("") + cellStyle = summaryEmptyThickBottomStyle + } + } + } + + fun addBlankSeparatorRow() { + sheet.createRow(rowIndex++) + } + + if (dbData.isEmpty()) { + val dataRow = sheet.createRow(rowIndex++) + for (col in 0 until totalColumns) { + val cell = dataRow.createCell(col) + cell.setCellValue("-") + cell.cellStyle = textStyle + } + addItemSummaryRow(0.0, "", "") + } else { + var currentItemNo: String? = null + var currentItemName: String = "" + var currentItemTotalQty: Double = 0.0 + + dbData.forEach { rowMap -> + val itemNo = rowMap["itemNo"]?.toString() ?: "" + if (currentItemNo != null && itemNo != currentItemNo) { + addItemSummaryRow(currentItemTotalQty, currentItemNo!!, currentItemName) + addBlankSeparatorRow() + currentItemTotalQty = 0.0 + } + + val dataRow = sheet.createRow(rowIndex++) + setTextCell(dataRow, 0, rowMap["itemNo"], textStyle) + setTextCell(dataRow, 1, rowMap["itemName"], textStyle) + setTextCell(dataRow, 2, rowMap["unitOfMeasure"], textStyle) + setTextCell(dataRow, 3, rowMap["jobOrderNo"], textStyle) + setTextCell(dataRow, 4, rowMap["stockReqNo"], textStyle) + setTextCell(dataRow, 5, rowMap["lotNo"], textStyle) + setTextCell(dataRow, 6, rowMap["expiryDate"], textStyle) + setNumberCell(dataRow, 7, rowMap["stockOutQty"], numberStyle, dashStyle) + setTextCell(dataRow, 8, rowMap["handler"], textStyle) + setTextCell(dataRow, 9, rowMap["storeLocation"], textStyle) + setTextCell(dataRow, 10, rowMap["pickRemark"], textStyle) + + currentItemNo = itemNo + currentItemName = rowMap["itemName"]?.toString() ?: "" + currentItemTotalQty += parseNullableNumber(rowMap["stockOutQty"]) ?: 0.0 + } + + // last item + addItemSummaryRow( + currentItemTotalQty, + currentItemNo ?: "", + currentItemName, + ) + } + + + val lastRowIndex = rowIndex - 1 + if (lastRowIndex >= 0) { + sheet.setAutoFilter(CellRangeAddress(0, lastRowIndex, 0, 0)) + } + + val widths = intArrayOf(18, 26, 10, 20, 18, 14, 14, 12, 18, 16, 18) + widths.forEachIndexed { idx, w -> sheet.setColumnWidth(idx, w * 256) } + + val output = ByteArrayOutputStream() + workbook.use { it.write(output) } + return output.toByteArray() + } + + private fun setTextCell(row: Row, col: Int, value: Any?, style: CellStyle) { + val cell = row.createCell(col) + cell.setCellValue(value?.toString() ?: "") + cell.cellStyle = style + } + + private fun parseNullableNumber(value: Any?): Double? { + val s = value?.toString()?.replace(",", "")?.trim() + if (s.isNullOrBlank()) return null + if (s == "-" || s == "null") return null + return s.toDoubleOrNull() + } + + private fun setNumberCell( + row: Row, + col: Int, + value: Any?, + numberStyle: CellStyle, + dashStyle: CellStyle, + ) { + val cell = row.createCell(col) + val parsed = parseNullableNumber(value) + if (parsed == null) { + cell.setCellValue("-") + cell.cellStyle = dashStyle + } else { + cell.setCellValue(parsed) + cell.cellStyle = numberStyle + } + } }