From 8aad7a378d739a839b007b72295cb06f9abca80f Mon Sep 17 00:00:00 2001 From: Harry Groves Date: Mon, 7 Sep 2026 16:43:35 +0800 Subject: [PATCH] Created Excel for stockIssue. Modified stockLotOnHand excel to show "lot line" and "remarks". --- gradlew | 0 .../service/PickExecutionIssueService.kt | 10 +- .../web/PickExecutionIssueController.kt | 37 ++- .../service/LotExpiryAlertExcelBuilder.kt | 247 ++++++++++++++++++ .../service/LotExpiryAlertReportService.kt | 126 +++++++++ .../service/StockLotOnhandReportService.kt | 16 +- .../web/LotExpiryAlertReportController.kt | 54 ++++ .../web/StockLotOnhandReportController.kt | 39 +-- .../entity/InventoryLotLineRepository.kt | 2 +- .../stock/service/StockIssueService.kt | 2 +- 10 files changed, 499 insertions(+), 34 deletions(-) mode change 100644 => 100755 gradlew create mode 100644 src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertExcelBuilder.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertReportService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/report/web/LotExpiryAlertReportController.kt diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt index d88f2dd..5fd8fcb 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt @@ -1814,14 +1814,16 @@ open fun getBadItemOnlyList(): List { return pickExecutionIssueRepository.findBadItemOnlyList(IssueCategory.lot_issue) } -/** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.0 | 2026-09-07 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ open fun getExpiryItemList( itemCode: String? = null, itemName: String? = null, lotNo: String? = null, + daysAhead: Int? = null, ): List { val today = LocalDate.now() - val untilDate = today.plusDays(7) + val days = (daysAhead ?: 7).coerceIn(0, 365) + val untilDate = today.plusDays(days.toLong()) val normalizedItemCode = itemCode?.trim()?.takeIf { it.isNotEmpty() } val normalizedItemName = itemName?.trim()?.takeIf { it.isNotEmpty() } val normalizedLotNo = lotNo?.trim()?.takeIf { it.isNotEmpty() } @@ -1999,7 +2001,7 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { } } -/** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.0 | 2026-09-07 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ @Transactional(rollbackFor = [Exception::class]) open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse { try { @@ -2208,7 +2210,7 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse { } } // Fix batchSubmitExpiryItem method (around line 945): -/** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.0 | 2026-09-07 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ @Transactional(rollbackFor = [Exception::class]) open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageResponse { try { diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt index ff34742..956b13d 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt @@ -6,13 +6,18 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssue import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum import com.ffii.fpsms.modules.pickOrder.service.PickExecutionIssueService // 修复导入路径 import com.ffii.fpsms.modules.pickOrder.web.models.* +import com.ffii.fpsms.modules.report.service.LotExpiryAlertReportService +import com.ffii.fpsms.modules.report.service.LotExpiryAlertReportService.SearchFilters +import com.ffii.fpsms.modules.report.web.lotExpiryAlertExcelResponse +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/pickExecution") class PickExecutionIssueController( - private val pickExecutionIssueService: PickExecutionIssueService + private val pickExecutionIssueService: PickExecutionIssueService, + private val lotExpiryAlertReportService: LotExpiryAlertReportService, ) { @PostMapping("/recordIssue") @@ -64,20 +69,44 @@ class PickExecutionIssueController( return pickExecutionIssueService.getBadItemList(issueCategory) } - /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.0 | 2026-09-07 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ @GetMapping("/issues/expiryItem") fun getExpiryItemIssues( @RequestParam(required = false) itemCode: String?, @RequestParam(required = false) itemName: String?, @RequestParam(required = false) lotNo: String?, + @RequestParam(required = false) daysAhead: Int?, ): List { return pickExecutionIssueService.getExpiryItemList( itemCode = itemCode, itemName = itemName, lotNo = lotNo, + daysAhead = daysAhead, ) } + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ + @GetMapping("/issues/expiryItem/excel") + fun exportExpiryItemExcel( + @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) itemName: String?, + @RequestParam(required = false) lotNo: String?, + @RequestParam(required = false) bucket: String?, + @RequestParam(required = false) daysAhead: Int?, + ): ResponseEntity { + return lotExpiryAlertExcelResponse( + lotExpiryAlertReportService.exportExcel( + SearchFilters( + itemCode = itemCode, + itemName = itemName, + lotNo = lotNo, + bucket = bucket, + daysAhead = daysAhead, + ), + ), + ) + } + @PostMapping("/submitMissItem") fun submitMissItem(@RequestBody request: SubmitIssueRequest): MessageResponse { return pickExecutionIssueService.submitMissItem(request) @@ -98,13 +127,13 @@ fun batchSubmitBadItem(@RequestBody request: BatchSubmitIssueRequest): MessageRe return pickExecutionIssueService.batchSubmitBadItem(request) } - /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.0 | 2026-09-07 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ @PostMapping("/submitExpiryItem") fun submitExpiryItem(@RequestBody request: SubmitExpiryRequest): MessageResponse { return pickExecutionIssueService.submitExpiryItem(request) } - /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.0 | 2026-09-07 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ @PostMapping("/batchSubmitExpiryItem") fun batchSubmitExpiryItem(@RequestBody request: BatchSubmitExpiryRequest): MessageResponse { return pickExecutionIssueService.batchSubmitExpiryItem(request) diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertExcelBuilder.kt b/src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertExcelBuilder.kt new file mode 100644 index 0000000..7159446 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertExcelBuilder.kt @@ -0,0 +1,247 @@ +package com.ffii.fpsms.modules.report.service + +import com.ffii.fpsms.modules.report.service.LotExpiryAlertReportService.SearchFilters +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.Workbook +import org.apache.poi.ss.util.CellRangeAddress +import org.apache.poi.ss.util.WorkbookUtil +import org.apache.poi.xssf.streaming.SXSSFWorkbook +import java.io.ByteArrayOutputStream +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +internal object LotExpiryAlertExcelBuilder { + private data class ExcelStyles( + val title: CellStyle, + val subtitle: CellStyle, + val header: CellStyle, + val text: CellStyle, + val center: CellStyle, + val number: CellStyle, + ) + + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ + fun build( + dbData: List>, + filters: SearchFilters, + ): ByteArray { + val workbook = SXSSFWorkbook(100) + workbook.setCompressTempFiles(true) + try { + val styles = createStyles(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.heightInPoints = 24f + titleRow.createCell(0).apply { + setCellValue(reportTitle) + cellStyle = styles.title + } + sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1)) + + val downloadedAt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + val dateRow = sheet.createRow(rowIndex++) + dateRow.heightInPoints = 32f + dateRow.createCell(0).apply { + setCellValue("下載日期:$downloadedAt") + cellStyle = styles.subtitle + } + val dateEndCol = 3.coerceAtMost(totalColumns - 2) + if (dateEndCol > 0) { + sheet.addMergedRegion(CellRangeAddress(1, 1, 0, dateEndCol)) + } + val filterStartCol = dateEndCol + 1 + val filterText = listOf( + "貨品編號=${displayFilter(filters.itemCode)}", + "貨品=${displayFilter(filters.itemName)}", + "批號=${displayFilter(filters.lotNo)}", + "未來天數=${filters.resolvedDaysAhead()}", + "到期分類=${displayBucket(filters.bucket, filters.resolvedDaysAhead())}", + ).joinToString(" ") + dateRow.createCell(filterStartCol).apply { + setCellValue("搜尋條件:$filterText") + cellStyle = styles.subtitle + } + if (filterStartCol < totalColumns - 1) { + sheet.addMergedRegion(CellRangeAddress(1, 1, filterStartCol, totalColumns - 1)) + } + + val headerRowIndex = rowIndex + val headerRow = sheet.createRow(rowIndex++) + headerRow.heightInPoints = 22f + headers.forEachIndexed { i, h -> + headerRow.createCell(i).apply { + setCellValue(h) + cellStyle = styles.header + } + } + + if (dbData.isEmpty()) { + val emptyRowIndex = rowIndex + val r = sheet.createRow(rowIndex++) + r.heightInPoints = 22f + r.createCell(0).apply { + setCellValue("查無資料") + cellStyle = styles.center + } + for (c in 1 until totalColumns) { + r.createCell(c).cellStyle = styles.center + } + sheet.addMergedRegion(CellRangeAddress(emptyRowIndex, emptyRowIndex, 0, totalColumns - 1)) + } else { + dbData.forEach { m -> + val r = sheet.createRow(rowIndex++) + r.heightInPoints = 18f + setTextCell(r, 0, m["expiryAlert"], styles.center) + setTextCell(r, 1, m["storeId"], styles.center) + setTextCell(r, 2, m["expiryDate"], styles.center) + setTextCell(r, 3, m["itemCode"], styles.text) + setTextCell(r, 4, m["itemName"], styles.text) + setTextCell(r, 5, m["lotNumber"], styles.text) + setTextCell(r, 6, m["warehouse"], styles.center) + setTextCell(r, 7, m["area"], styles.center) + setTextCell(r, 8, m["slot"], styles.center) + setNumberCell(r, 9, m["remainingQty"], styles.number) + setTextCell(r, 10, m["unitOfMeasure"], styles.center) + } + } + + val lastRowIndex = rowIndex - 1 + if (lastRowIndex >= headerRowIndex) { + sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, totalColumns - 1)) + } + sheet.createFreezePane(0, headerRowIndex + 1) + intArrayOf(14, 10, 12, 14, 28, 18, 12, 10, 10, 12, 16) + .forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) } + + val out = ByteArrayOutputStream() + workbook.write(out) + return out.toByteArray() + } finally { + workbook.dispose() + workbook.close() + } + } + + private fun createStyles(workbook: Workbook): ExcelStyles { + val numberFormat = workbook.createDataFormat().getFormat("#,##0.00;(#,##0.00)") + fun borders(style: CellStyle) { + style.borderTop = BorderStyle.THIN + style.borderBottom = BorderStyle.THIN + style.borderLeft = BorderStyle.THIN + style.borderRight = BorderStyle.THIN + style.verticalAlignment = VerticalAlignment.CENTER + } + val titleStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.CENTER + verticalAlignment = VerticalAlignment.CENTER + val font = workbook.createFont().apply { + bold = true + fontHeightInPoints = 16 + } + setFont(font) + } + val subtitleStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.LEFT + verticalAlignment = VerticalAlignment.CENTER + wrapText = true + val font = workbook.createFont().apply { + fontHeightInPoints = 10 + } + setFont(font) + } + val headerStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.CENTER + wrapText = true + borders(this) + fillForegroundColor = IndexedColors.DARK_TEAL.index + fillPattern = FillPatternType.SOLID_FOREGROUND + val font = workbook.createFont().apply { + bold = true + color = IndexedColors.WHITE.index + fontHeightInPoints = 10 + } + setFont(font) + } + val textStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.LEFT + borders(this) + } + val centerStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.CENTER + borders(this) + } + val numberStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.RIGHT + borders(this) + dataFormat = numberFormat + } + return ExcelStyles( + title = titleStyle, + subtitle = subtitleStyle, + header = headerStyle, + text = textStyle, + center = centerStyle, + number = numberStyle, + ) + } + + private fun displayFilter(raw: String?): String { + val v = raw?.trim().orEmpty() + return if (v.isEmpty()) "全部" else v + } + + private fun displayBucket(raw: String?, daysAhead: Int): String { + return when (raw?.trim()?.lowercase()) { + "expired" -> "過期尚未處理" + "today" -> "今日到期" + "upcoming" -> "未來 ${daysAhead} 日到期" + else -> "全部" + } + } + + private fun setTextCell(row: Row, col: Int, value: Any?, style: CellStyle) { + row.createCell(col).apply { + setCellValue(value?.toString() ?: "") + cellStyle = style + } + } + + private fun setNumberCell(row: Row, col: Int, value: Any?, numberStyle: CellStyle) { + val cell = row.createCell(col) + val n = when (value) { + null -> null + is Number -> value.toDouble() + else -> value.toString().replace(",", "").toDoubleOrNull() + } + if (n == null) { + cell.setCellValue("") + cell.cellStyle = numberStyle + return + } + cell.setCellValue(n) + cell.cellStyle = numberStyle + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertReportService.kt new file mode 100644 index 0000000..b388c42 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertReportService.kt @@ -0,0 +1,126 @@ +package com.ffii.fpsms.modules.report.service + +import com.ffii.core.support.JdbcDao +import org.springframework.stereotype.Service + +@Service +open class LotExpiryAlertReportService( + private val jdbcDao: JdbcDao, +) { + data class SearchFilters( + val itemCode: String? = null, + val itemName: String? = null, + val lotNo: String? = null, + val bucket: String? = null, + val daysAhead: Int? = null, + ) { + fun resolvedDaysAhead(): Int { + val n = daysAhead ?: DEFAULT_DAYS_AHEAD + return n.coerceIn(0, MAX_DAYS_AHEAD) + } + + companion object { + const val DEFAULT_DAYS_AHEAD = 7 + const val MAX_DAYS_AHEAD = 365 + } + } + + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ + fun search( + itemCode: String? = null, + itemName: String? = null, + lotNo: String? = null, + bucket: String? = null, + daysAhead: Int? = null, + ): List> = search( + SearchFilters( + itemCode = itemCode, + itemName = itemName, + lotNo = lotNo, + bucket = bucket, + daysAhead = daysAhead, + ), + ) + + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ + fun search(filters: SearchFilters): List> { + val args = mutableMapOf() + val daysAhead = filters.resolvedDaysAhead() + args["daysAhead"] = daysAhead + val extraWhere = buildString { + val itemCode = filters.itemCode?.trim().orEmpty() + if (itemCode.isNotEmpty()) { + args["itemCode"] = itemCode + append(" AND LOWER(i.code) LIKE LOWER(CONCAT('%', :itemCode, '%'))") + } + val itemName = filters.itemName?.trim().orEmpty() + if (itemName.isNotEmpty()) { + args["itemName"] = itemName + append(" AND LOWER(i.name) LIKE LOWER(CONCAT('%', :itemName, '%'))") + } + val lotNo = filters.lotNo?.trim().orEmpty() + if (lotNo.isNotEmpty()) { + args["lotNo"] = lotNo + append(" AND LOWER(il.lotNo) LIKE LOWER(CONCAT('%', :lotNo, '%'))") + } + when (filters.bucket?.trim()?.lowercase()) { + "expired" -> append(" AND il.expiryDate < CURRENT_DATE") + "today" -> append(" AND il.expiryDate = CURRENT_DATE") + "upcoming" -> append( + " AND il.expiryDate > CURRENT_DATE AND il.expiryDate <= DATE_ADD(CURRENT_DATE, INTERVAL :daysAhead DAY)", + ) + } + } + return jdbcDao.queryForList( + """ + SELECT + COALESCE(i.code, '') AS itemCode, + COALESCE(i.name, '') AS itemName, + COALESCE(il.lotNo, '') AS lotNumber, + COALESCE(w.store_id, '') AS storeId, + COALESCE(w.warehouse, '') AS warehouse, + COALESCE(w.area, '') AS area, + COALESCE(w.slot, '') AS slot, + COALESCE(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate, + (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS remainingQty, + COALESCE(uc.udfudesc, uc.code, '') AS unitOfMeasure, + CASE + WHEN il.expiryDate IS NULL THEN '' + WHEN il.expiryDate < CURRENT_DATE THEN '已過期' + WHEN il.expiryDate = CURRENT_DATE THEN '今日到期' + WHEN il.expiryDate <= DATE_ADD(CURRENT_DATE, INTERVAL :daysAhead DAY) + THEN '${daysAhead}日內到期' + ELSE '' + END AS expiryAlert + FROM inventory_lot_line ill + LEFT JOIN inventory_lot il ON ill.inventoryLotId = il.id + LEFT JOIN warehouse w ON ill.warehouseId = w.id + LEFT JOIN items i ON il.itemId = i.id + LEFT JOIN item_uom iu ON iu.id = ill.stockItemUomId AND iu.deleted = 0 + LEFT JOIN uom_conversion uc ON uc.id = iu.uomId + WHERE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) > 0 + AND COALESCE(ill.deleted, 0) = 0 + AND COALESCE(il.deleted, 0) = 0 + AND COALESCE(i.deleted, 0) = 0 + AND il.expiryDate IS NOT NULL + AND il.expiryDate <= DATE_ADD(CURRENT_DATE, INTERVAL :daysAhead DAY) + $extraWhere + ORDER BY + CASE + WHEN il.expiryDate < CURRENT_DATE THEN 1 + WHEN il.expiryDate = CURRENT_DATE THEN 2 + ELSE 3 + END, + COALESCE(w.store_id, ''), + il.expiryDate, + COALESCE(i.code, '') + """.trimIndent(), + args, + ) as List> + } + + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ + fun exportExcel(filters: SearchFilters = SearchFilters()): ByteArray { + return LotExpiryAlertExcelBuilder.build(search(filters), filters) + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/StockLotOnhandReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/StockLotOnhandReportService.kt index 3f54285..15fffb4 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/StockLotOnhandReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/StockLotOnhandReportService.kt @@ -100,7 +100,7 @@ stock_one AS ( val stockDate: String, ) - /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */ fun search( itemCode: String?, storeId: String?, @@ -154,6 +154,7 @@ stock_one AS ( """ ${ctePrefix}SELECT ill.id AS inventoryLotLineId, + COALESCE(NULLIF(TRIM(ill.remarks), ''), '') AS remarks, COALESCE(it.code, '') AS itemNo, COALESCE(it.name, '') AS itemName, COALESCE(uc.udfudesc, uc.code, '') AS unitOfMeasure, @@ -229,7 +230,7 @@ stock_one AS ( private data class SilSolHit(val ts: String, val date: String, val kind: String) - /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */ private fun loadLastTrnFromLedger(lotIds: List, out: MutableMap>) { if (lotIds.isEmpty()) return // idx_ledger_lot_date_id is (lot, date, id) — bad for MAX(id); force the lot-only index @@ -262,7 +263,7 @@ stock_one AS ( } } - /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */ private fun fillLastTrnFromLotHeaderSil(missing: List, hits: MutableMap): Int { var n = 0 for (chunk in missing.chunked(800)) { @@ -288,7 +289,7 @@ stock_one AS ( return n } - /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */ private fun fillLastTrnFromSilLine(missing: List, hits: MutableMap): Int { var n = 0 for (chunk in missing.chunked(800)) { @@ -310,7 +311,7 @@ stock_one AS ( return n } - /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */ private fun fillLastTrnFromSol(missing: List, hits: MutableMap): Int { var n = 0 for (chunk in missing.chunked(800)) { @@ -354,7 +355,7 @@ stock_one AS ( else -> "" } - /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */ private fun assembleRows( liveLots: List>, lastTrnByLot: Map>, @@ -373,8 +374,9 @@ stock_one AS ( val last = lastTrnByLot[lotId] val origin = r["rootPoPrefix"]?.toString().orEmpty().uppercase() val showPrice = origin == "PP" || origin == "PF" - val row = HashMap(22) + val row = HashMap(24) row["inventoryLotLineId"] = lotId.toString() + row["remarks"] = r["remarks"] ?: "" row["itemNo"] = r["itemNo"] ?: "" row["itemName"] = r["itemName"] ?: "" row["unitOfMeasure"] = r["unitOfMeasure"] ?: "" diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/LotExpiryAlertReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/LotExpiryAlertReportController.kt new file mode 100644 index 0000000..bcf7d29 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/web/LotExpiryAlertReportController.kt @@ -0,0 +1,54 @@ +package com.ffii.fpsms.modules.report.web + +import com.ffii.fpsms.modules.report.service.LotExpiryAlertReportService +import com.ffii.fpsms.modules.report.service.LotExpiryAlertReportService.SearchFilters +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +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 + +internal fun lotExpiryAlertExcelResponse(bytes: ByteArray): ResponseEntity { + val headers = HttpHeaders().apply { + contentType = MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + setContentDispositionFormData("attachment", "LotExpiryAlertReport.xlsx") + set("filename", "LotExpiryAlertReport.xlsx") + } + return ResponseEntity(bytes, headers, HttpStatus.OK) +} + +/** + * Lot expiry alert (Excel only). + * Excel: /report/print-lot-expiry-alert-excel + */ +@RestController +@RequestMapping("/report") +class LotExpiryAlertReportController( + private val lotExpiryAlertReportService: LotExpiryAlertReportService, +) { + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ + @GetMapping("/print-lot-expiry-alert-excel") + fun exportExcel( + @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) itemName: String?, + @RequestParam(required = false) lotNo: String?, + @RequestParam(required = false) bucket: String?, + @RequestParam(required = false) daysAhead: Int?, + ): ResponseEntity { + val excelBytes = lotExpiryAlertReportService.exportExcel( + SearchFilters( + itemCode = itemCode, + itemName = itemName, + lotNo = lotNo, + bucket = bucket, + daysAhead = daysAhead, + ), + ) + return lotExpiryAlertExcelResponse(excelBytes) + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/StockLotOnhandReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/StockLotOnhandReportController.kt index 7aac550..d7a9b95 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/StockLotOnhandReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/StockLotOnhandReportController.kt @@ -43,7 +43,7 @@ class StockLotOnhandReportController( val number: CellStyle, ) - /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */ @GetMapping("/print-stock-lot-onhand-excel") fun exportExcel( @RequestParam(required = false) itemCode: String?, @@ -191,6 +191,7 @@ class StockLotOnhandReportController( return v } + /** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */ private fun createExcel( dbData: List>, reportDate: String, @@ -210,11 +211,13 @@ class StockLotOnhandReportController( val reportTitle = "庫存批次現況報告" val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(reportTitle)) val headers = listOf( + "庫存批次行", "貨品編號", "貨品名稱", "貨品單位結餘", "單位", "批號", "到期日", "樓層", "倉庫", "區域", "儲位", "最後異動日", "最後異動(入庫/出庫)", "批號結餘", "單位均價", "庫存總價值", + "備註", ) val totalColumns = headers.size var rowIndex = 0 @@ -284,21 +287,23 @@ class StockLotOnhandReportController( dbData.forEach { m -> val r = sheet.createRow(rowIndex++) r.heightInPoints = 18f - setTextCell(r, 0, m["itemNo"], styles.text) - setTextCell(r, 1, m["itemName"], styles.text) - setNumberCell(r, 2, m["totalQtyRaw"], styles.number) - setTextCell(r, 3, m["unitOfMeasure"], styles.center) - setTextCell(r, 4, m["lotNo"], styles.text) - setTextCell(r, 5, m["expiryDate"], styles.center) - setTextCell(r, 6, m["storeId"], styles.center) - setTextCell(r, 7, m["warehousePart"], styles.center) - setTextCell(r, 8, m["areaPart"], styles.center) - setTextCell(r, 9, m["slotPart"], styles.center) - setTextCell(r, 10, m["lastTrnDate"], styles.center) - setTextCell(r, 11, m["lastTrnType"], styles.center) - setNumberCell(r, 12, m["lotQtyRaw"], styles.number) - setNumberCell(r, 13, m["avgUnitPriceRaw"], styles.number) - setNumberCell(r, 14, m["stockValueRaw"], styles.number) + setTextCell(r, 0, m["inventoryLotLineId"], styles.center) + setTextCell(r, 1, m["itemNo"], styles.text) + setTextCell(r, 2, m["itemName"], styles.text) + setNumberCell(r, 3, m["totalQtyRaw"], styles.number) + setTextCell(r, 4, m["unitOfMeasure"], styles.center) + setTextCell(r, 5, m["lotNo"], styles.text) + setTextCell(r, 6, m["expiryDate"], styles.center) + setTextCell(r, 7, m["storeId"], styles.center) + setTextCell(r, 8, m["warehousePart"], styles.center) + setTextCell(r, 9, m["areaPart"], styles.center) + setTextCell(r, 10, m["slotPart"], styles.center) + setTextCell(r, 11, m["lastTrnDate"], styles.center) + setTextCell(r, 12, m["lastTrnType"], styles.center) + setNumberCell(r, 13, m["lotQtyRaw"], styles.number) + setNumberCell(r, 14, m["avgUnitPriceRaw"], styles.number) + setNumberCell(r, 15, m["stockValueRaw"], styles.number) + setTextCell(r, 16, m["remarks"], styles.text) } } @@ -307,7 +312,7 @@ class StockLotOnhandReportController( sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, totalColumns - 1)) } sheet.createFreezePane(0, headerRowIndex + 1) - intArrayOf(14, 28, 14, 8, 20, 12, 10, 12, 10, 10, 12, 20, 12, 12, 14) + intArrayOf(14, 14, 28, 14, 8, 20, 12, 10, 12, 10, 10, 12, 20, 12, 12, 14, 28) .forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) } val out = ByteArrayOutputStream() diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt index 3725512..7b5ff3e 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt @@ -237,7 +237,7 @@ WHERE ill.id = :id AND i.deleted = false ORDER BY il.expiryDate ASC """) - /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.0 | 2026-09-07 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ fun findExpiredItems( @Param("untilDate") untilDate: LocalDate, @Param("itemCode") itemCode: String?, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockIssueService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockIssueService.kt index 3cb41d8..1ba9e02 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockIssueService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockIssueService.kt @@ -86,7 +86,7 @@ open class StockIssueService( return searchHandleRecords(request, "Bad") } - /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.0 | 2026-09-07 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 75 | v1.0.1 | 2026-09-07 */ open fun getExpiryItemHandleRecords(request: SearchStockIssueRecordRequest): RecordsRes { val hasHandledDateFilter = request.handledStartDate != null || request.handledEndDate != null