Quellcode durchsuchen

SemiFGProductionAnalysisReport Excel Version

master
B.E.N.S.O.N vor 1 Tag
Ursprung
Commit
e985eb91eb
3 geänderte Dateien mit 389 neuen und 4 gelöschten Zeilen
  1. +1
    -0
      .gitignore
  2. +1
    -1
      src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt
  3. +387
    -3
      src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt

+ 1
- 0
.gitignore Datei anzeigen

@@ -35,3 +35,4 @@ out/

### VS Code ###
.vscode/
package-lock.json

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt Datei anzeigen

@@ -99,7 +99,7 @@ class SemiFGProductionAnalysisReportService(
// Filter by itemCode - match bom.code (user input should match bom.code, which then matches stock_ledger.itemCode)
val itemCodeSql = buildMultiValueExactClause(itemCode, "b.code", "itemCode", args)
val yearSql = if (!year.isNullOrBlank()) {
val yearSql = if (!year.isNullOrBlank() && year != "All") {
args["year"] = year
"AND YEAR(sl.modified) = :year"
} else {


+ 387
- 3
src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt Datei anzeigen

@@ -3,11 +3,33 @@ package com.ffii.fpsms.modules.report.web
import net.sf.jasperreports.engine.*
import org.springframework.http.*
import org.springframework.web.bind.annotation.*
import java.io.ByteArrayOutputStream
import java.time.LocalDate
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.report.service.SemiFGProductionAnalysisReportService
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.util.WorkbookUtil
import org.apache.poi.ss.usermodel.VerticalAlignment
import org.apache.poi.ss.usermodel.DataValidationConstraint
import org.apache.poi.ss.usermodel.DataValidationHelper
import org.apache.poi.ss.usermodel.SheetVisibility
import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.ss.util.CellRangeAddressList
import org.apache.poi.ss.util.CellReference
import org.apache.poi.xddf.usermodel.chart.AxisPosition
import org.apache.poi.xddf.usermodel.chart.BarDirection
import org.apache.poi.xddf.usermodel.chart.ChartTypes
import org.apache.poi.xddf.usermodel.chart.LegendPosition
import org.apache.poi.xddf.usermodel.chart.XDDFBarChartData
import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory
import org.apache.poi.xssf.usermodel.XSSFClientAnchor
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import kotlin.math.roundToLong

@RestController
@RequestMapping("/report")
@@ -50,20 +72,382 @@ class SemiFGProductionAnalysisReportController(
)

val pdfBytes = reportService.createPdfResponse(
"/jasper/SemiFGProductionAnalysisReport.jrxml",
parameters,
"/jasper/SemiFGProductionAnalysisReport.jrxml",
parameters,
dbData
)

val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_PDF
setContentDispositionFormData("attachment", "SemiFGProductionAnalysisReport.pdf")
set("filename", "SemiFGProductionAnalysisReport.pdf")
set("filename", "SemiFGProductionAnalysisReport.pdf")
}

return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
}

@GetMapping("/print-semi-fg-production-analysis-excel")
fun exportSemiFGProductionAnalysisReportExcel(
@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?
): ResponseEntity<ByteArray> {
val dbData = semiFGProductionAnalysisReportService.searchSemiFGProductionAnalysisReport(
stockCategory,
stockSubCategory,
itemCode,
year,
lastOutDateStart,
lastOutDateEnd
)

val excelBytes = createSemiFGProductionAnalysisExcel(
dbData = dbData,
reportTitle = "成品/半成品生產分析報告",
year = year
)

val headers = HttpHeaders().apply {
contentType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
setContentDispositionFormData("attachment", "SemiFGProductionAnalysisReport.xlsx")
set("filename", "SemiFGProductionAnalysisReport.xlsx")
}

return ResponseEntity(excelBytes, headers, HttpStatus.OK)
}

private fun createSemiFGProductionAnalysisExcel(
dbData: List<Map<String, Any>>,
reportTitle: String,
year: String?
): ByteArray {
val workbook = XSSFWorkbook()
val safeSheetName = WorkbookUtil.createSafeSheetName(reportTitle)
val sheet = workbook.createSheet(safeSheetName)
val totalColumns = 16
var rowIndex = 0

val titleStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.CENTER
verticalAlignment = VerticalAlignment.CENTER
}
val titleFont = workbook.createFont().apply {
bold = true
fontHeightInPoints = 16
}
titleStyle.setFont(titleFont)

val infoStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.LEFT
verticalAlignment = VerticalAlignment.CENTER
}

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 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 numberStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
dataFormat = workbook.creationHelper.createDataFormat().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 titleRow = sheet.createRow(rowIndex++)
titleRow.heightInPoints = 28f
val titleCell = titleRow.createCell(0)
titleCell.setCellValue(reportTitle)
titleCell.cellStyle = titleStyle
sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1))

val infoRow = sheet.createRow(rowIndex++)
val reportDateCell = infoRow.createCell(0)
reportDateCell.setCellValue("報告日期:${LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))}")
reportDateCell.cellStyle = infoStyle
val reportTimeCell = infoRow.createCell(6)
reportTimeCell.setCellValue("報告時間:${LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))}")
reportTimeCell.cellStyle = infoStyle
val reportYearCell = infoRow.createCell(11)
reportYearCell.setCellValue("年份:${year?.takeIf { it.isNotBlank() && it != "All" } ?: "All"}")
reportYearCell.cellStyle = infoStyle
sheet.addMergedRegion(CellRangeAddress(1, 1, 0, 5))
sheet.addMergedRegion(CellRangeAddress(1, 1, 6, 10))
sheet.addMergedRegion(CellRangeAddress(1, 1, 11, 15))

rowIndex++ // spacer row

val headers = listOf(
"貨品編號",
"貨品名稱",
"單位",
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
"總和"
)

val headerRow = sheet.createRow(rowIndex++)
headers.forEachIndexed { index, name ->
val cell = headerRow.createCell(index)
cell.setCellValue(name)
cell.cellStyle = headerStyle
}

if (dbData.isEmpty()) {
val dataRow = sheet.createRow(rowIndex++)
setTextCell(dataRow, 0, "-", textStyle)
setTextCell(dataRow, 1, "-", textStyle)
setTextCell(dataRow, 2, "-", textStyle)
for (col in 3 until totalColumns) {
setDashCell(dataRow, col, dashStyle)
}
} else {
dbData.forEach { row ->
val dataRow = sheet.createRow(rowIndex++)
setTextCell(dataRow, 0, row["itemNo"]?.toString()?.ifBlank { "-" } ?: "-", textStyle)
setTextCell(dataRow, 1, row["itemName"]?.toString()?.ifBlank { "-" } ?: "-", textStyle)
setTextCell(dataRow, 2, row["unitOfMeasure"]?.toString()?.ifBlank { "-" } ?: "-", textStyle)
setNumberCell(dataRow, 3, row["qtyJan"], numberStyle, dashStyle)
setNumberCell(dataRow, 4, row["qtyFeb"], numberStyle, dashStyle)
setNumberCell(dataRow, 5, row["qtyMar"], numberStyle, dashStyle)
setNumberCell(dataRow, 6, row["qtyApr"], numberStyle, dashStyle)
setNumberCell(dataRow, 7, row["qtyMay"], numberStyle, dashStyle)
setNumberCell(dataRow, 8, row["qtyJun"], numberStyle, dashStyle)
setNumberCell(dataRow, 9, row["qtyJul"], numberStyle, dashStyle)
setNumberCell(dataRow, 10, row["qtyAug"], numberStyle, dashStyle)
setNumberCell(dataRow, 11, row["qtySep"], numberStyle, dashStyle)
setNumberCell(dataRow, 12, row["qtyOct"], numberStyle, dashStyle)
setNumberCell(dataRow, 13, row["qtyNov"], numberStyle, dashStyle)
setNumberCell(dataRow, 14, row["qtyDec"], numberStyle, dashStyle)
setNumberCell(dataRow, 15, row["totalProductionQty"], numberStyle, dashStyle)
}
}

val widths = intArrayOf(18, 24, 12, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 12)
widths.forEachIndexed { idx, width -> sheet.setColumnWidth(idx, width * 256) }

addSemiFGProductionChartSheets(workbook, dbData)

val output = ByteArrayOutputStream()
workbook.use { it.write(output) }
return output.toByteArray()
}

/**
* Adds a visible chart sheet with:
* - Cell B1 dropdown to choose 貨品編號 or 「全部」(sum of all items)
* - Formulas that pull monthly qty from a very-hidden data sheet
* - Column bar chart bound to those formula cells (updates when selection changes)
*/
private fun addSemiFGProductionChartSheets(workbook: XSSFWorkbook, dbData: List<Map<String, Any>>) {
val monthKeys = listOf(
"qtyJan", "qtyFeb", "qtyMar", "qtyApr", "qtyMay", "qtyJun",
"qtyJul", "qtyAug", "qtySep", "qtyOct", "qtyNov", "qtyDec",
)
val monthLabels = listOf(
"一月", "二月", "三月", "四月", "五月", "六月",
"七月", "八月", "九月", "十月", "十一月", "十二月",
)

val dataSheetName = WorkbookUtil.createSafeSheetName("圖表數據")
val dataSheet = workbook.createSheet(dataSheetName)

val headerRow = dataSheet.createRow(0)
headerRow.createCell(0).setCellValue("貨品編號")
monthLabels.forEachIndexed { i, label ->
headerRow.createCell(1 + i).setCellValue(label)
}

dbData.forEachIndexed { idx, row ->
val r = dataSheet.createRow(1 + idx)
r.createCell(0).setCellValue(row["itemNo"]?.toString() ?: "")
monthKeys.forEachIndexed { mi, key ->
r.createCell(1 + mi).setCellValue(parseNumericForExcelChart(row[key]))
}
}

val zColIndex = CellReference.convertColStringToIndex("Z")
val zHeader = dataSheet.getRow(0) ?: dataSheet.createRow(0)
zHeader.createCell(zColIndex).setCellValue("全部")
dbData.forEachIndexed { idx, row ->
val r = dataSheet.getRow(1 + idx) ?: dataSheet.createRow(1 + idx)
r.createCell(zColIndex).setCellValue(row["itemNo"]?.toString() ?: "")
}

val lastDataRow1Based = 1 + dbData.size
val zListEnd1Based = 1 + dbData.size

val chartSheetName = WorkbookUtil.createSafeSheetName("生產圖表")
val chartSheet = workbook.createSheet(chartSheetName)

val labelR = chartSheet.createRow(0)
labelR.createCell(0).setCellValue("選擇貨品編號")
val filterCell = labelR.createCell(1)
val defaultItemNo = dbData.firstOrNull()?.get("itemNo")?.toString()?.trim()
filterCell.setCellValue(defaultItemNo?.takeIf { it.isNotBlank() } ?: "全部")

val dvHelper: DataValidationHelper = chartSheet.dataValidationHelper
val listFormula = "'$dataSheetName'!\$Z\$1:\$Z\$$zListEnd1Based"
val constraint: DataValidationConstraint = dvHelper.createFormulaListConstraint(listFormula)
val regions = CellRangeAddressList(0, 0, 1, 1)
val validation = dvHelper.createValidation(constraint, regions)
validation.showErrorBox = true
validation.createErrorBox("無效選擇", "請從清單中選擇貨品編號或「全部」。")
validation.showPromptBox = true
validation.createPromptBox("篩選", "選擇貨品編號檢視該品項各月數量;選擇「全部」顯示所有品項加總。")
chartSheet.addValidationData(validation)

val catRowIndex = 3
val valRowIndex = 4
val firstMonthCol = 1
val lastMonthCol = 12

val catRow = chartSheet.createRow(catRowIndex)
val valRow = chartSheet.createRow(valRowIndex)
for (i in 0 until 12) {
catRow.createCell(firstMonthCol + i).setCellValue(monthLabels[i])
val colLetter = CellReference.convertNumToColString(firstMonthCol + i)
val formula = if (lastDataRow1Based < 2) {
"0"
} else {
"IF(TRIM(\$B\$1)=\"全部\",SUM('$dataSheetName'!$colLetter\$2:$colLetter\$$lastDataRow1Based),IFERROR(INDEX('$dataSheetName'!$colLetter\$2:$colLetter\$$lastDataRow1Based,MATCH(TRIM(\$B\$1),'$dataSheetName'!\$A\$2:\$A\$$lastDataRow1Based,0)),0))"
}
valRow.createCell(firstMonthCol + i).setCellFormula(formula)
}
catRow.createCell(lastMonthCol + 1).setCellValue("總和")
valRow.createCell(lastMonthCol + 1).setCellFormula("SUM(B5:M5)")

val drawing = chartSheet.createDrawingPatriarch()
val anchor = XSSFClientAnchor(0, 0, 0, 0, 1, 6, 14, 28)
val chart = drawing.createChart(anchor)
chart.setTitleText("各月生產數量(依選擇之貨品編號)")
chart.setTitleOverlay(false)

val legend = chart.getOrAddLegend()
legend.position = LegendPosition.BOTTOM

val bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM)
bottomAxis.setTitle("月份")
val leftAxis = chart.createValueAxis(AxisPosition.LEFT)
leftAxis.setTitle("生產數量")

val catRangeAddr = CellRangeAddress(catRowIndex, catRowIndex, firstMonthCol, lastMonthCol)
val valRangeAddr = CellRangeAddress(valRowIndex, valRowIndex, firstMonthCol, lastMonthCol)
val categories = XDDFDataSourcesFactory.fromStringCellRange(chartSheet, catRangeAddr)
val values = XDDFDataSourcesFactory.fromNumericCellRange(chartSheet, valRangeAddr)

val barData = chart.createData(ChartTypes.BAR, bottomAxis, leftAxis) as XDDFBarChartData
barData.barDirection = BarDirection.COL
val series = barData.addSeries(categories, values)
series.setTitle("生產數量", null)
chart.plot(barData)

chartSheet.setColumnWidth(0, 18 * 256)
chartSheet.setColumnWidth(1, 10 * 256)
chartSheet.setColumnWidth(lastMonthCol + 1, 12 * 256)

workbook.setSheetVisibility(workbook.getSheetIndex(dataSheet), SheetVisibility.VERY_HIDDEN)
workbook.setSheetOrder(chartSheetName, 1)
}

private fun parseNumericForExcelChart(value: Any?): Double {
if (value == null) return 0.0
return when (value) {
is Number -> value.toDouble()
is String -> {
val s = value.replace(",", "").trim()
if (s.isEmpty() || s == "-") return 0.0
s.toDoubleOrNull() ?: 0.0
}
else -> value.toString().replace(",", "").trim().toDoubleOrNull() ?: 0.0
}
}

private fun setTextCell(
row: org.apache.poi.ss.usermodel.Row,
col: Int,
value: Any?,
style: org.apache.poi.ss.usermodel.CellStyle
) {
val cell = row.createCell(col)
cell.setCellValue(value?.toString() ?: "")
cell.cellStyle = style
}

private fun setNumberCell(
row: org.apache.poi.ss.usermodel.Row,
col: Int,
value: Any?,
style: org.apache.poi.ss.usermodel.CellStyle,
dashStyle: org.apache.poi.ss.usermodel.CellStyle
) {
val cell = row.createCell(col)
val parsed = when (value) {
is Number -> value.toDouble()
is String -> value.replace(",", "").toDoubleOrNull() ?: 0.0
else -> value?.toString()?.replace(",", "")?.toDoubleOrNull() ?: 0.0
}
if (parsed == 0.0) {
cell.setCellValue("-")
cell.cellStyle = dashStyle
} else {
cell.setCellValue(parsed.roundToLong().toDouble())
cell.cellStyle = style
}
}

private fun setDashCell(
row: org.apache.poi.ss.usermodel.Row,
col: Int,
style: org.apache.poi.ss.usermodel.CellStyle
) {
val cell = row.createCell(col)
cell.setCellValue("-")
cell.cellStyle = style
}

@GetMapping("/semi-fg-item-codes")
fun getSemiFGItemCodes(
@RequestParam(required = false) stockCategory: String?


Laden…
Abbrechen
Speichern