Преглед на файлове

Created Excel for stockIssue. Modified stockLotOnHand excel to show "lot line" and "remarks".

undefined
Harry Groves преди 1 седмица
родител
ревизия
8aad7a378d
променени са 10 файла, в които са добавени 499 реда и са изтрити 34 реда
  1. +0
    -0
      gradlew
  2. +6
    -4
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  3. +33
    -4
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt
  4. +247
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertExcelBuilder.kt
  5. +126
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/LotExpiryAlertReportService.kt
  6. +9
    -7
      src/main/java/com/ffii/fpsms/modules/report/service/StockLotOnhandReportService.kt
  7. +54
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/LotExpiryAlertReportController.kt
  8. +22
    -17
      src/main/java/com/ffii/fpsms/modules/report/web/StockLotOnhandReportController.kt
  9. +1
    -1
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  10. +1
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/StockIssueService.kt

+ 0
- 0
gradlew Целия файл


+ 6
- 4
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Целия файл

@@ -1814,14 +1814,16 @@ open fun getBadItemOnlyList(): List<PickExecutionIssue> {
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<ExpiryItemResponse> {
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 {


+ 33
- 4
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<ExpiryItemResponse> {
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<ByteArray> {
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)


+ 247
- 0
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<Map<String, Any>>,
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
}
}

+ 126
- 0
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<Map<String, Any>> = 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<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
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<Map<String, Any>>
}

/** 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)
}
}

+ 9
- 7
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<Long>, out: MutableMap<Long, Pair<String, String>>) {
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<Long>, hits: MutableMap<Long, SilSolHit>): 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<Long>, hits: MutableMap<Long, SilSolHit>): 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<Long>, hits: MutableMap<Long, SilSolHit>): 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<Map<String, Any>>,
lastTrnByLot: Map<Long, Pair<String, String>>,
@@ -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<String, Any>(22)
val row = HashMap<String, Any>(24)
row["inventoryLotLineId"] = lotId.toString()
row["remarks"] = r["remarks"] ?: ""
row["itemNo"] = r["itemNo"] ?: ""
row["itemName"] = r["itemName"] ?: ""
row["unitOfMeasure"] = r["unitOfMeasure"] ?: ""


+ 54
- 0
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<ByteArray> {
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<ByteArray> {
val excelBytes = lotExpiryAlertReportService.exportExcel(
SearchFilters(
itemCode = itemCode,
itemName = itemName,
lotNo = lotNo,
bucket = bucket,
daysAhead = daysAhead,
),
)
return lotExpiryAlertExcelResponse(excelBytes)
}
}

+ 22
- 17
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<Map<String, Any>>,
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()


+ 1
- 1
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?,


+ 1
- 1
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<StockIssueHandleRecordResponse> {
val hasHandledDateFilter =
request.handledStartDate != null || request.handledEndDate != null


Зареждане…
Отказ
Запис