|
|
|
@@ -2,8 +2,21 @@ package com.ffii.fpsms.modules.report.web |
|
|
|
|
|
|
|
import net.sf.jasperreports.engine.* |
|
|
|
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource |
|
|
|
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.XSSFCellStyle |
|
|
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook |
|
|
|
import org.springframework.http.* |
|
|
|
import org.springframework.web.bind.annotation.* |
|
|
|
import java.io.ByteArrayOutputStream |
|
|
|
import java.io.InputStream |
|
|
|
import java.time.LocalDate |
|
|
|
import java.time.LocalTime |
|
|
|
@@ -16,6 +29,20 @@ import com.ffii.fpsms.modules.report.service.ReportService |
|
|
|
class ReportController( |
|
|
|
private val reportService: ReportService, |
|
|
|
) { |
|
|
|
private data class ExcelStyles( |
|
|
|
val title: XSSFCellStyle, |
|
|
|
val subtitle: XSSFCellStyle, |
|
|
|
val header: XSSFCellStyle, |
|
|
|
val text: XSSFCellStyle, |
|
|
|
val center: XSSFCellStyle, |
|
|
|
val number: XSSFCellStyle, |
|
|
|
val int: XSSFCellStyle, |
|
|
|
val dash: XSSFCellStyle, |
|
|
|
val sumQty: XSSFCellStyle, |
|
|
|
val sumLabel: XSSFCellStyle, |
|
|
|
val sumEmpty: XSSFCellStyle, |
|
|
|
val sumHidden: XSSFCellStyle, |
|
|
|
) |
|
|
|
|
|
|
|
@GetMapping("/stock-take-rounds") |
|
|
|
fun getStockTakeRounds(): List<Map<String, Any>> = |
|
|
|
@@ -123,6 +150,36 @@ class ReportController( |
|
|
|
|
|
|
|
return ResponseEntity(pdfBytes, headers, HttpStatus.OK) |
|
|
|
} |
|
|
|
|
|
|
|
@GetMapping("/print-stock-in-traceability-excel") |
|
|
|
fun exportStockInTraceabilityReportExcel( |
|
|
|
@RequestParam(required = false) stockCategory: String?, |
|
|
|
@RequestParam(required = false) itemCode: String?, |
|
|
|
@RequestParam(required = false) lastInDateStart: String?, |
|
|
|
@RequestParam(required = false) lastInDateEnd: String?, |
|
|
|
): ResponseEntity<ByteArray> { |
|
|
|
val dbData = reportService.searchStockInTraceabilityReport( |
|
|
|
stockCategory, |
|
|
|
itemCode, |
|
|
|
lastInDateStart, |
|
|
|
lastInDateEnd, |
|
|
|
) |
|
|
|
|
|
|
|
val excelBytes = createStockInTraceabilityExcel( |
|
|
|
dbData = dbData, |
|
|
|
lastInDateStart = lastInDateStart ?: "", |
|
|
|
lastInDateEnd = lastInDateEnd ?: "", |
|
|
|
) |
|
|
|
|
|
|
|
val headers = HttpHeaders().apply { |
|
|
|
contentType = MediaType.parseMediaType( |
|
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
|
|
|
) |
|
|
|
setContentDispositionFormData("attachment", "StockInTraceabilityReport.xlsx") |
|
|
|
set("filename", "StockInTraceabilityReport.xlsx") |
|
|
|
} |
|
|
|
return ResponseEntity(excelBytes, headers, HttpStatus.OK) |
|
|
|
} |
|
|
|
@GetMapping("/print-fg-delivery-report") |
|
|
|
fun generateFGDeliveryReport( |
|
|
|
@RequestParam(required = false) stockCategory: String?, |
|
|
|
@@ -169,6 +226,41 @@ class ReportController( |
|
|
|
|
|
|
|
return ResponseEntity(pdfBytes, headers, HttpStatus.OK) |
|
|
|
} |
|
|
|
|
|
|
|
@GetMapping("/print-fg-delivery-report-excel") |
|
|
|
fun exportFGDeliveryReportExcel( |
|
|
|
@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 = reportService.searchFGDeliveryReport( |
|
|
|
stockCategory, |
|
|
|
stockSubCategory, |
|
|
|
itemCode, |
|
|
|
year, |
|
|
|
lastOutDateStart, |
|
|
|
lastOutDateEnd, |
|
|
|
) |
|
|
|
|
|
|
|
val excelBytes = createFGDeliveryExcel( |
|
|
|
dbData = dbData, |
|
|
|
year = year ?: LocalDate.now().year.toString(), |
|
|
|
lastOutDateStart = lastOutDateStart ?: "", |
|
|
|
lastOutDateEnd = lastOutDateEnd ?: "", |
|
|
|
) |
|
|
|
|
|
|
|
val headers = HttpHeaders().apply { |
|
|
|
contentType = MediaType.parseMediaType( |
|
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
|
|
|
) |
|
|
|
setContentDispositionFormData("attachment", "FGDeliveryReport.xlsx") |
|
|
|
set("filename", "FGDeliveryReport.xlsx") |
|
|
|
} |
|
|
|
return ResponseEntity(excelBytes, headers, HttpStatus.OK) |
|
|
|
} |
|
|
|
@GetMapping("/print-stock-balance") |
|
|
|
fun generateStockBalanceReport( |
|
|
|
@RequestParam(required = false) stockCategory: String?, |
|
|
|
@@ -236,6 +328,559 @@ class ReportController( |
|
|
|
return ResponseEntity(pdfBytes, headers, HttpStatus.OK) |
|
|
|
} |
|
|
|
|
|
|
|
@GetMapping("/print-stock-balance-excel") |
|
|
|
fun exportStockBalanceReportExcel( |
|
|
|
@RequestParam(required = false) stockCategory: String?, |
|
|
|
@RequestParam(required = false) itemCode: String?, |
|
|
|
@RequestParam(required = false) stockDate: String?, |
|
|
|
@RequestParam(required = false) balanceFilterStart: String?, |
|
|
|
@RequestParam(required = false) balanceFilterEnd: String?, |
|
|
|
@RequestParam(required = false) storeLocation: String?, |
|
|
|
@RequestParam(required = false) lastInDateStart: String?, |
|
|
|
@RequestParam(required = false) lastInDateEnd: String?, |
|
|
|
@RequestParam(required = false) lastOutDateStart: String?, |
|
|
|
@RequestParam(required = false) lastOutDateEnd: String?, |
|
|
|
@RequestParam(required = false, defaultValue = "0") stockTakeRoundId: Long, |
|
|
|
): ResponseEntity<ByteArray> { |
|
|
|
val dbData = |
|
|
|
if (!stockDate.isNullOrBlank()) { |
|
|
|
reportService.searchStockBalanceReportByDate( |
|
|
|
stockCategory = stockCategory, |
|
|
|
itemCode = itemCode, |
|
|
|
stockDate = stockDate, |
|
|
|
balanceFilterStart = balanceFilterStart, |
|
|
|
balanceFilterEnd = balanceFilterEnd, |
|
|
|
storeLocation = storeLocation, |
|
|
|
) |
|
|
|
} else { |
|
|
|
reportService.searchStockBalanceReport( |
|
|
|
stockCategory, |
|
|
|
itemCode, |
|
|
|
balanceFilterStart, |
|
|
|
balanceFilterEnd, |
|
|
|
storeLocation, |
|
|
|
lastInDateStart, |
|
|
|
lastInDateEnd, |
|
|
|
lastOutDateStart, |
|
|
|
lastOutDateEnd, |
|
|
|
stockTakeRoundId |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
val excelBytes = createStockBalanceExcel( |
|
|
|
dbData = dbData, |
|
|
|
reportDate = (stockDate ?: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))), |
|
|
|
) |
|
|
|
|
|
|
|
val headers = HttpHeaders().apply { |
|
|
|
contentType = MediaType.parseMediaType( |
|
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
|
|
|
) |
|
|
|
setContentDispositionFormData("attachment", "StockBalanceReport.xlsx") |
|
|
|
set("filename", "StockBalanceReport.xlsx") |
|
|
|
} |
|
|
|
return ResponseEntity(excelBytes, headers, HttpStatus.OK) |
|
|
|
} |
|
|
|
|
|
|
|
private fun createCommonWorkbookStyles(workbook: XSSFWorkbook): ExcelStyles { |
|
|
|
val titleStyle = (workbook.createCellStyle() as XSSFCellStyle).apply { |
|
|
|
alignment = HorizontalAlignment.CENTER |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
val font = workbook.createFont().apply { |
|
|
|
bold = true |
|
|
|
fontHeightInPoints = 14 |
|
|
|
} |
|
|
|
setFont(font) |
|
|
|
} |
|
|
|
val subtitleStyle = (workbook.createCellStyle() as XSSFCellStyle).apply { |
|
|
|
alignment = HorizontalAlignment.LEFT |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
val font = workbook.createFont().apply { |
|
|
|
bold = true |
|
|
|
fontHeightInPoints = 12 |
|
|
|
} |
|
|
|
setFont(font) |
|
|
|
} |
|
|
|
val headerStyle = (workbook.createCellStyle() as XSSFCellStyle).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() as XSSFCellStyle).apply { |
|
|
|
alignment = HorizontalAlignment.LEFT |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
borderTop = BorderStyle.THIN |
|
|
|
borderBottom = BorderStyle.THIN |
|
|
|
borderLeft = BorderStyle.THIN |
|
|
|
borderRight = BorderStyle.THIN |
|
|
|
} |
|
|
|
val centerStyle = (workbook.createCellStyle() as XSSFCellStyle).apply { |
|
|
|
alignment = HorizontalAlignment.CENTER |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
borderTop = BorderStyle.THIN |
|
|
|
borderBottom = BorderStyle.THIN |
|
|
|
borderLeft = BorderStyle.THIN |
|
|
|
borderRight = BorderStyle.THIN |
|
|
|
} |
|
|
|
val numberStyle = (workbook.createCellStyle() as XSSFCellStyle).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.00") |
|
|
|
} |
|
|
|
val intStyle = (workbook.createCellStyle() as XSSFCellStyle).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() as XSSFCellStyle).apply { |
|
|
|
alignment = HorizontalAlignment.RIGHT |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
borderTop = BorderStyle.THIN |
|
|
|
borderBottom = BorderStyle.THIN |
|
|
|
borderLeft = BorderStyle.THIN |
|
|
|
borderRight = BorderStyle.THIN |
|
|
|
} |
|
|
|
val summaryQtyThickBottomStyle = (workbook.createCellStyle() as XSSFCellStyle).apply { |
|
|
|
alignment = HorizontalAlignment.RIGHT |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
val df: DataFormat = workbook.createDataFormat() |
|
|
|
dataFormat = df.getFormat("#,##0.00") |
|
|
|
borderTop = BorderStyle.THICK |
|
|
|
borderBottom = BorderStyle.THICK |
|
|
|
borderLeft = BorderStyle.THIN |
|
|
|
borderRight = BorderStyle.THIN |
|
|
|
val font = workbook.createFont().apply { bold = true } |
|
|
|
setFont(font) |
|
|
|
} |
|
|
|
val summaryLabelThickBottomStyle = (workbook.createCellStyle() as XSSFCellStyle).apply { |
|
|
|
alignment = HorizontalAlignment.RIGHT |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
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() as XSSFCellStyle).apply { |
|
|
|
alignment = HorizontalAlignment.LEFT |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
borderTop = BorderStyle.THICK |
|
|
|
borderBottom = BorderStyle.THICK |
|
|
|
borderLeft = BorderStyle.THIN |
|
|
|
borderRight = BorderStyle.THIN |
|
|
|
} |
|
|
|
val summaryHiddenKeyStyle = (workbook.createCellStyle() as XSSFCellStyle).apply { |
|
|
|
alignment = HorizontalAlignment.LEFT |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
borderTop = BorderStyle.THICK |
|
|
|
borderBottom = BorderStyle.THICK |
|
|
|
borderLeft = BorderStyle.THIN |
|
|
|
borderRight = BorderStyle.THIN |
|
|
|
val font = workbook.createFont().apply { color = IndexedColors.WHITE.index } |
|
|
|
setFont(font) |
|
|
|
} |
|
|
|
return ExcelStyles( |
|
|
|
title = titleStyle, |
|
|
|
subtitle = subtitleStyle, |
|
|
|
header = headerStyle, |
|
|
|
text = textStyle, |
|
|
|
center = centerStyle, |
|
|
|
number = numberStyle, |
|
|
|
int = intStyle, |
|
|
|
dash = dashStyle, |
|
|
|
sumQty = summaryQtyThickBottomStyle, |
|
|
|
sumLabel = summaryLabelThickBottomStyle, |
|
|
|
sumEmpty = summaryEmptyThickBottomStyle, |
|
|
|
sumHidden = summaryHiddenKeyStyle, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
private fun setTextCell(row: Row, col: Int, value: Any?, style: XSSFCellStyle) { |
|
|
|
row.createCell(col).apply { |
|
|
|
setCellValue(value?.toString() ?: "") |
|
|
|
cellStyle = style |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun parseSignedNumber(value: Any?): Double? { |
|
|
|
val raw = value?.toString()?.trim().orEmpty() |
|
|
|
if (raw.isBlank() || raw == "-" || raw.equals("null", ignoreCase = true)) return null |
|
|
|
val negative = raw.startsWith("(") && raw.endsWith(")") |
|
|
|
val cleaned = raw.removePrefix("(").removeSuffix(")").replace(",", "").replace("%", "").trim() |
|
|
|
val n = cleaned.toDoubleOrNull() ?: return null |
|
|
|
return if (negative) -n else n |
|
|
|
} |
|
|
|
|
|
|
|
private fun setNumberCellFromFormatted( |
|
|
|
row: Row, |
|
|
|
col: Int, |
|
|
|
value: Any?, |
|
|
|
numberStyle: XSSFCellStyle, |
|
|
|
dashStyle: XSSFCellStyle, |
|
|
|
preferInt: Boolean = false, |
|
|
|
) { |
|
|
|
val cell = row.createCell(col) |
|
|
|
val parsed = parseSignedNumber(value) |
|
|
|
if (parsed == null) { |
|
|
|
cell.setCellValue("-") |
|
|
|
cell.cellStyle = dashStyle |
|
|
|
return |
|
|
|
} |
|
|
|
if (parsed < 0) { |
|
|
|
val abs = kotlin.math.abs(parsed) |
|
|
|
val s = if (preferInt) "%,d".format(abs.toLong()) else "%,.2f".format(abs) |
|
|
|
cell.setCellValue("($s)") |
|
|
|
cell.cellStyle = dashStyle |
|
|
|
return |
|
|
|
} |
|
|
|
cell.setCellValue(parsed) |
|
|
|
cell.cellStyle = numberStyle |
|
|
|
} |
|
|
|
|
|
|
|
private fun createStockInTraceabilityExcel( |
|
|
|
dbData: List<Map<String, Any>>, |
|
|
|
lastInDateStart: String, |
|
|
|
lastInDateEnd: String, |
|
|
|
): ByteArray { |
|
|
|
val workbook = XSSFWorkbook() |
|
|
|
val styles = createCommonWorkbookStyles(workbook) |
|
|
|
val reportTitle = "入倉追蹤報告" |
|
|
|
val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(reportTitle)) |
|
|
|
|
|
|
|
val headers = listOf( |
|
|
|
"貨品編號", "貨品名稱", "單位", |
|
|
|
"送貨單編號", "批號", "到期日", |
|
|
|
"上架數量", "入庫檢查樣品數量", "入庫檢查缺陷數量", |
|
|
|
"入庫檢查缺陷百分比", "入庫檢查結果", "入庫檢查備註", |
|
|
|
"存貨位置", "供應商名稱", |
|
|
|
) |
|
|
|
val totalColumns = headers.size |
|
|
|
var rowIndex = 0 |
|
|
|
|
|
|
|
val titleRow = sheet.createRow(rowIndex++) |
|
|
|
titleRow.createCell(0).apply { |
|
|
|
setCellValue(reportTitle) |
|
|
|
cellStyle = styles.title |
|
|
|
} |
|
|
|
sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1)) |
|
|
|
|
|
|
|
val reportDateTime = |
|
|
|
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + |
|
|
|
"(" + |
|
|
|
LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + |
|
|
|
")" |
|
|
|
val subtitleRow = sheet.createRow(rowIndex++) |
|
|
|
subtitleRow.createCell(0).apply { |
|
|
|
setCellValue("報告日期:$reportDateTime") |
|
|
|
cellStyle = styles.subtitle |
|
|
|
} |
|
|
|
sheet.addMergedRegion(CellRangeAddress(1, 1, 0, 5)) |
|
|
|
subtitleRow.createCell(6).apply { |
|
|
|
val cap = (lastInDateStart.trim().ifBlank { "" }) + " 至 " + (lastInDateEnd.trim().ifBlank { "" }) |
|
|
|
setCellValue("最後入倉日期:${cap.trim()}") |
|
|
|
cellStyle = styles.subtitle |
|
|
|
} |
|
|
|
sheet.addMergedRegion(CellRangeAddress(1, 1, 6, totalColumns - 1)) |
|
|
|
sheet.createRow(rowIndex++) |
|
|
|
|
|
|
|
val headerRowIndex = rowIndex |
|
|
|
val headerRow = sheet.createRow(rowIndex++) |
|
|
|
headers.forEachIndexed { i, h -> |
|
|
|
headerRow.createCell(i).apply { |
|
|
|
setCellValue(h) |
|
|
|
cellStyle = styles.header |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
fun addItemSummaryRow( |
|
|
|
itemNo: String, |
|
|
|
itemName: String, |
|
|
|
uom: String, |
|
|
|
totalStockIn: Any?, |
|
|
|
_totalSample: Any?, |
|
|
|
_totalDefect: Any?, |
|
|
|
) { |
|
|
|
val r = sheet.createRow(rowIndex++) |
|
|
|
r.createCell(0).apply { setCellValue(itemNo); cellStyle = styles.sumHidden } |
|
|
|
r.createCell(1).apply { setCellValue(itemName); cellStyle = styles.sumHidden } |
|
|
|
r.createCell(2).apply { setCellValue(uom); cellStyle = styles.sumHidden } |
|
|
|
for (c in 3 until totalColumns) { |
|
|
|
r.createCell(c).apply { setCellValue(""); cellStyle = styles.sumEmpty } |
|
|
|
} |
|
|
|
// Put totals in the three adjacent qty columns to avoid displacing later columns: |
|
|
|
// - 上架數量 (col 6) |
|
|
|
// - 入庫檢查樣品數量 (col 7) |
|
|
|
// - 入庫檢查缺陷數量 (col 8) |
|
|
|
r.getCell(5).apply { setCellValue("合計:"); cellStyle = styles.sumLabel } |
|
|
|
setNumberCellFromFormatted(r, 6, totalStockIn, styles.sumQty, styles.dash, preferInt = false) |
|
|
|
setNumberCellFromFormatted(r, 7, _totalSample, styles.sumQty, styles.dash, preferInt = false) |
|
|
|
setNumberCellFromFormatted(r, 8, _totalDefect, styles.sumQty, styles.dash, preferInt = false) |
|
|
|
} |
|
|
|
|
|
|
|
if (dbData.isEmpty()) { |
|
|
|
val r = sheet.createRow(rowIndex++) |
|
|
|
for (c in 0 until totalColumns) { |
|
|
|
r.createCell(c).apply { setCellValue("-"); cellStyle = styles.text } |
|
|
|
} |
|
|
|
} else { |
|
|
|
var currentItemNo: String? = null |
|
|
|
var currentItemName = "" |
|
|
|
var currentUom = "" |
|
|
|
var lastTotals: Triple<Any?, Any?, Any?> = Triple(null, null, null) |
|
|
|
|
|
|
|
dbData.forEach { m -> |
|
|
|
val itemNo = m["itemNo"]?.toString().orEmpty() |
|
|
|
val itemName = m["itemName"]?.toString().orEmpty() |
|
|
|
val uom = m["unitOfMeasure"]?.toString().orEmpty() |
|
|
|
|
|
|
|
if (currentItemNo != null && itemNo != currentItemNo) { |
|
|
|
addItemSummaryRow( |
|
|
|
currentItemNo!!, |
|
|
|
currentItemName, |
|
|
|
currentUom, |
|
|
|
lastTotals.first, |
|
|
|
lastTotals.second, |
|
|
|
lastTotals.third, |
|
|
|
) |
|
|
|
sheet.createRow(rowIndex++) |
|
|
|
} |
|
|
|
|
|
|
|
val r = sheet.createRow(rowIndex++) |
|
|
|
setTextCell(r, 0, itemNo, styles.text) |
|
|
|
setTextCell(r, 1, itemName, styles.text) |
|
|
|
setTextCell(r, 2, uom, styles.center) |
|
|
|
setTextCell(r, 3, m["dnNo"], styles.text) |
|
|
|
setTextCell(r, 4, m["lotNo"], styles.text) |
|
|
|
setTextCell(r, 5, m["expiryDate"], styles.center) |
|
|
|
setNumberCellFromFormatted(r, 6, m["stockInQty"], styles.number, styles.dash, preferInt = false) |
|
|
|
setNumberCellFromFormatted(r, 7, m["iqcSampleQty"], styles.number, styles.dash, preferInt = false) |
|
|
|
setNumberCellFromFormatted(r, 8, m["iqcDefectQty"], styles.number, styles.dash, preferInt = false) |
|
|
|
// percentage as text with % |
|
|
|
val pct = (m["iqcDefectPercentage"]?.toString()?.trim().orEmpty()).let { if (it.isBlank()) "" else "$it%" } |
|
|
|
setTextCell(r, 9, pct, styles.dash) |
|
|
|
setTextCell(r, 10, m["iqcResult"], styles.center) |
|
|
|
setTextCell(r, 11, m["iqcRemarks"], styles.text) |
|
|
|
setTextCell(r, 12, m["storeLocation"], styles.center) |
|
|
|
setTextCell(r, 13, m["supplierName"], styles.text) |
|
|
|
|
|
|
|
currentItemNo = itemNo |
|
|
|
currentItemName = itemName |
|
|
|
currentUom = uom |
|
|
|
lastTotals = Triple(m["totalStockInQty"], m["totalIqcSampleQty"], m["totalIqcDefectQty"]) |
|
|
|
} |
|
|
|
|
|
|
|
addItemSummaryRow( |
|
|
|
currentItemNo ?: "", |
|
|
|
currentItemName, |
|
|
|
currentUom, |
|
|
|
lastTotals.first, |
|
|
|
lastTotals.second, |
|
|
|
lastTotals.third, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
val lastRowIndex = rowIndex - 1 |
|
|
|
if (lastRowIndex >= headerRowIndex) { |
|
|
|
sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0)) |
|
|
|
} |
|
|
|
|
|
|
|
val widths = intArrayOf(14, 26, 10, 18, 16, 12, 14, 18, 18, 18, 14, 22, 14, 20) |
|
|
|
widths.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) } |
|
|
|
|
|
|
|
val out = ByteArrayOutputStream() |
|
|
|
workbook.use { it.write(out) } |
|
|
|
return out.toByteArray() |
|
|
|
} |
|
|
|
|
|
|
|
private fun createFGDeliveryExcel( |
|
|
|
dbData: List<Map<String, Any>>, |
|
|
|
year: String, |
|
|
|
lastOutDateStart: String, |
|
|
|
lastOutDateEnd: String, |
|
|
|
): ByteArray { |
|
|
|
val workbook = XSSFWorkbook() |
|
|
|
val styles = createCommonWorkbookStyles(workbook) |
|
|
|
val reportTitle = "成品出倉報告" |
|
|
|
val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(reportTitle)) |
|
|
|
|
|
|
|
val headers = listOf( |
|
|
|
"貨品編號", "貨品名稱", "單位", |
|
|
|
"送貨日期", "DN編號", "送貨訂單編號", "門店名稱", "數量", "貨車", |
|
|
|
) |
|
|
|
val totalColumns = headers.size |
|
|
|
var rowIndex = 0 |
|
|
|
|
|
|
|
val titleRow = sheet.createRow(rowIndex++) |
|
|
|
titleRow.createCell(0).apply { |
|
|
|
setCellValue(reportTitle) |
|
|
|
cellStyle = styles.title |
|
|
|
} |
|
|
|
sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1)) |
|
|
|
|
|
|
|
val reportDateTime = |
|
|
|
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + |
|
|
|
"(" + |
|
|
|
LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + |
|
|
|
")" |
|
|
|
val subtitleRow = sheet.createRow(rowIndex++) |
|
|
|
subtitleRow.createCell(0).apply { |
|
|
|
setCellValue("報告日期:$reportDateTime") |
|
|
|
cellStyle = styles.subtitle |
|
|
|
} |
|
|
|
sheet.addMergedRegion(CellRangeAddress(1, 1, 0, 2)) |
|
|
|
subtitleRow.createCell(3).apply { |
|
|
|
setCellValue("成品出倉日期:${lastOutDateStart.trim()} 至 ${lastOutDateEnd.trim()}") |
|
|
|
cellStyle = styles.subtitle |
|
|
|
} |
|
|
|
sheet.addMergedRegion(CellRangeAddress(1, 1, 3, 6)) |
|
|
|
subtitleRow.createCell(7).apply { |
|
|
|
setCellValue("年份:$year") |
|
|
|
cellStyle = styles.subtitle |
|
|
|
} |
|
|
|
sheet.addMergedRegion(CellRangeAddress(1, 1, 7, totalColumns - 1)) |
|
|
|
sheet.createRow(rowIndex++) |
|
|
|
|
|
|
|
val headerRowIndex = rowIndex |
|
|
|
val headerRow = sheet.createRow(rowIndex++) |
|
|
|
headers.forEachIndexed { i, h -> |
|
|
|
headerRow.createCell(i).apply { |
|
|
|
setCellValue(h) |
|
|
|
cellStyle = styles.header |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (dbData.isEmpty()) { |
|
|
|
val r = sheet.createRow(rowIndex++) |
|
|
|
for (c in 0 until totalColumns) { |
|
|
|
r.createCell(c).apply { setCellValue("-"); cellStyle = styles.text } |
|
|
|
} |
|
|
|
} else { |
|
|
|
dbData.forEach { m -> |
|
|
|
val r = sheet.createRow(rowIndex++) |
|
|
|
setTextCell(r, 0, m["itemNo"], styles.text) |
|
|
|
setTextCell(r, 1, m["itemName"], styles.text) |
|
|
|
setTextCell(r, 2, m["unitOfMeasure"], styles.center) |
|
|
|
setTextCell(r, 3, m["deliveryDate"], styles.center) |
|
|
|
setTextCell(r, 4, m["dnNo"], styles.text) |
|
|
|
setTextCell(r, 5, m["deliveryOrderNo"], styles.text) |
|
|
|
setTextCell(r, 6, m["customerName"], styles.text) |
|
|
|
setNumberCellFromFormatted(r, 7, m["qty"], styles.int, styles.dash, preferInt = true) |
|
|
|
setTextCell(r, 8, m["truckNo"], styles.center) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
val lastRowIndex = rowIndex - 1 |
|
|
|
if (lastRowIndex >= headerRowIndex) { |
|
|
|
sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0)) |
|
|
|
} |
|
|
|
val widths = intArrayOf(14, 26, 10, 12, 18, 18, 26, 10, 12) |
|
|
|
widths.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) } |
|
|
|
|
|
|
|
val out = ByteArrayOutputStream() |
|
|
|
workbook.use { it.write(out) } |
|
|
|
return out.toByteArray() |
|
|
|
} |
|
|
|
|
|
|
|
private fun createStockBalanceExcel( |
|
|
|
dbData: List<Map<String, Any>>, |
|
|
|
reportDate: String, |
|
|
|
): ByteArray { |
|
|
|
val workbook = XSSFWorkbook() |
|
|
|
val styles = createCommonWorkbookStyles(workbook) |
|
|
|
val reportTitle = "庫存結餘報告" |
|
|
|
val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(reportTitle)) |
|
|
|
|
|
|
|
val headers = listOf( |
|
|
|
"貨品編號", "貨品名稱", "單位", |
|
|
|
"期初存量", "纍計存入量", "纍計存出量", |
|
|
|
"錯誤輸入或遺失", "盤盈虧", "不良品棄置", |
|
|
|
"現存存貨", "單位均價", "庫存總價值", |
|
|
|
) |
|
|
|
val totalColumns = headers.size |
|
|
|
var rowIndex = 0 |
|
|
|
|
|
|
|
val titleRow = sheet.createRow(rowIndex++) |
|
|
|
titleRow.createCell(0).apply { |
|
|
|
setCellValue(reportTitle) |
|
|
|
cellStyle = styles.title |
|
|
|
} |
|
|
|
sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1)) |
|
|
|
|
|
|
|
val reportDateTime = |
|
|
|
reportDate + |
|
|
|
"(" + |
|
|
|
LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + |
|
|
|
")" |
|
|
|
val subtitleRow = sheet.createRow(rowIndex++) |
|
|
|
subtitleRow.createCell(0).apply { |
|
|
|
setCellValue("報告日期:$reportDateTime") |
|
|
|
cellStyle = styles.subtitle |
|
|
|
} |
|
|
|
sheet.addMergedRegion(CellRangeAddress(1, 1, 0, totalColumns - 1)) |
|
|
|
sheet.createRow(rowIndex++) |
|
|
|
|
|
|
|
val headerRowIndex = rowIndex |
|
|
|
val headerRow = sheet.createRow(rowIndex++) |
|
|
|
headers.forEachIndexed { i, h -> |
|
|
|
headerRow.createCell(i).apply { |
|
|
|
setCellValue(h) |
|
|
|
cellStyle = styles.header |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (dbData.isEmpty()) { |
|
|
|
val r = sheet.createRow(rowIndex++) |
|
|
|
for (c in 0 until totalColumns) { |
|
|
|
r.createCell(c).apply { setCellValue("-"); cellStyle = styles.text } |
|
|
|
} |
|
|
|
} else { |
|
|
|
dbData.forEach { m -> |
|
|
|
val r = sheet.createRow(rowIndex++) |
|
|
|
setTextCell(r, 0, m["itemNo"], styles.text) |
|
|
|
setTextCell(r, 1, m["itemName"], styles.text) |
|
|
|
setTextCell(r, 2, m["unitOfMeasure"], styles.center) |
|
|
|
setNumberCellFromFormatted(r, 3, m["totalOpeningBalance"], styles.int, styles.dash, preferInt = true) |
|
|
|
setNumberCellFromFormatted(r, 4, m["totalCumStockIn"], styles.int, styles.dash, preferInt = true) |
|
|
|
setNumberCellFromFormatted(r, 5, m["totalCumStockOut"], styles.int, styles.dash, preferInt = true) |
|
|
|
setNumberCellFromFormatted(r, 6, m["totalMisInputAndLost"], styles.int, styles.dash, preferInt = true) |
|
|
|
setNumberCellFromFormatted(r, 7, m["totalVariance"], styles.int, styles.dash, preferInt = true) |
|
|
|
setNumberCellFromFormatted(r, 8, m["totalDefectiveGoods"], styles.int, styles.dash, preferInt = true) |
|
|
|
setNumberCellFromFormatted(r, 9, m["totalCurrentBalance"], styles.int, styles.dash, preferInt = true) |
|
|
|
setNumberCellFromFormatted(r, 10, m["avgUnitPrice"], styles.number, styles.dash, preferInt = false) |
|
|
|
setNumberCellFromFormatted(r, 11, m["totalStockBalance"], styles.number, styles.dash, preferInt = false) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
val lastRowIndex = rowIndex - 1 |
|
|
|
if (lastRowIndex >= headerRowIndex) { |
|
|
|
sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0)) |
|
|
|
} |
|
|
|
val widths = intArrayOf(14, 26, 10, 12, 12, 12, 14, 12, 12, 12, 12, 14) |
|
|
|
widths.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) } |
|
|
|
|
|
|
|
val out = ByteArrayOutputStream() |
|
|
|
workbook.use { it.write(out) } |
|
|
|
return out.toByteArray() |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* GRN (Goods Received Note) report data for Excel export. |
|
|
|
* Query by receipt date range and optional item code. Returns JSON { "rows": [ ... ] }. |
|
|
|
|