| @@ -10,6 +10,18 @@ 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.DataFormat | |||
| 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.util.CellRangeAddress | |||
| import org.apache.poi.ss.util.WorkbookUtil | |||
| import org.apache.poi.xssf.usermodel.XSSFWorkbook | |||
| import java.io.ByteArrayOutputStream | |||
| import java.time.LocalDate | |||
| import java.time.LocalTime | |||
| import java.time.format.DateTimeFormatter | |||
| @@ -71,4 +83,301 @@ class FGStockOutTraceabilityReportController( | |||
| return ResponseEntity(pdfBytes, headers, HttpStatus.OK) | |||
| } | |||
| @GetMapping("/print-fg-stock-out-traceability-excel") | |||
| fun exportFGStockOutTraceabilityReportExcel( | |||
| @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<ByteArray> { | |||
| val dbData = fgStockOutTraceabilityReportService.searchFGStockOutTraceabilityReport( | |||
| stockCategory, | |||
| stockSubCategory, | |||
| itemCode, | |||
| year, | |||
| lastOutDateStart, | |||
| lastOutDateEnd, | |||
| handler, | |||
| ) | |||
| val excelBytes = createFGStockOutTraceabilityExcel(dbData) | |||
| val headers = HttpHeaders().apply { | |||
| contentType = MediaType.parseMediaType( | |||
| "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |||
| ) | |||
| setContentDispositionFormData( | |||
| "attachment", | |||
| "FGStockOutTraceabilityReport.xlsx", | |||
| ) | |||
| set("filename", "FGStockOutTraceabilityReport.xlsx") | |||
| } | |||
| return ResponseEntity(excelBytes, headers, HttpStatus.OK) | |||
| } | |||
| private fun createFGStockOutTraceabilityExcel(dbData: List<Map<String, Any>>): 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( | |||
| "貨品編號", | |||
| "貨品名稱", | |||
| "單位", | |||
| "出貨單號", | |||
| "提料單號", | |||
| "批號", | |||
| "到期日", | |||
| "出貨數量", | |||
| "提料人", | |||
| "存貨位置", | |||
| "提料備註", | |||
| ) | |||
| val titleStyle = workbook.createCellStyle().apply { | |||
| alignment = HorizontalAlignment.CENTER | |||
| verticalAlignment = VerticalAlignment.CENTER | |||
| val font = workbook.createFont().apply { | |||
| bold = true | |||
| fontHeightInPoints = 14 | |||
| } | |||
| setFont(font) | |||
| } | |||
| // Excel heading row | |||
| val titleRow = sheet.createRow(rowIndex++) | |||
| titleRow.createCell(0).apply { | |||
| setCellValue(reportTitle) | |||
| cellStyle = titleStyle | |||
| } | |||
| sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1)) | |||
| sheet.createRow(rowIndex++) | |||
| val headerRowIndex = rowIndex | |||
| 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.THICK | |||
| 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.THICK | |||
| 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.THICK | |||
| borderBottom = BorderStyle.THICK | |||
| borderLeft = BorderStyle.THIN | |||
| borderRight = BorderStyle.THIN | |||
| } | |||
| val summaryHiddenKeyStyle = workbook.createCellStyle().apply { | |||
| alignment = HorizontalAlignment.LEFT | |||
| verticalAlignment = VerticalAlignment.CENTER | |||
| borderTop = BorderStyle.THICK | |||
| borderBottom = BorderStyle.THICK | |||
| borderLeft = BorderStyle.THIN | |||
| borderRight = BorderStyle.THIN | |||
| // Keep value for Excel filtering/grouping while visually hiding text. | |||
| val font = workbook.createFont().apply { | |||
| color = IndexedColors.WHITE.index | |||
| } | |||
| setFont(font) | |||
| } | |||
| fun addItemSummaryRow(totalQty: Double, _itemCode: String, _itemName: String) { | |||
| val summaryRow = sheet.createRow(rowIndex++) | |||
| summaryRow.createCell(0).apply { | |||
| setCellValue(_itemCode) | |||
| cellStyle = summaryHiddenKeyStyle | |||
| } | |||
| summaryRow.createCell(1).apply { | |||
| setCellValue(_itemName) | |||
| cellStyle = summaryHiddenKeyStyle | |||
| } | |||
| 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 = "" | |||
| var currentItemTotalQty = 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["deliveryOrderNo"], 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 | |||
| } | |||
| addItemSummaryRow(currentItemTotalQty, currentItemNo ?: "", currentItemName) | |||
| } | |||
| val lastRowIndex = rowIndex - 1 | |||
| if (lastRowIndex >= headerRowIndex) { | |||
| sheet.setAutoFilter(CellRangeAddress(headerRowIndex, 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 | |||
| } | |||
| } | |||
| } | |||