Parcourir la source

庫存品質檢測報告 add qc item select control, search date change from stock in line date to qc reuslt create date

庫存明細報告 improve speed

送貨訂單與倉存單位不符 excel 預計到貨→預計送貨
fix負數倉
CANCERYS\kw093 il y a 1 mois
Parent
révision
01d5cde8c8
6 fichiers modifiés avec 403 ajouts et 376 suppressions
  1. +6
    -2
      src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt
  2. +182
    -194
      src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt
  3. +2
    -2
      src/main/java/com/ffii/fpsms/modules/report/web/DoInventoryUomMismatchReportController.kt
  4. +11
    -5
      src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt
  5. +201
    -172
      src/main/java/com/ffii/fpsms/modules/report/web/StockLedgerReportController.kt
  6. +1
    -1
      src/main/resources/jasper/StockLedgarReport.jrxml

+ 6
- 2
src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt Voir le fichier

@@ -7,6 +7,7 @@ import org.springframework.stereotype.Service
open class ItemQcFailReportService( open class ItemQcFailReportService(
private val jdbcDao: JdbcDao, private val jdbcDao: JdbcDao,
) { ) {
/** FP-MTMS Version Checklist | Functions Ref. No. 64 | v1.0.0 | 2026-08-11 */
fun searchItemQcFailReport( fun searchItemQcFailReport(
stockCategory: String?, stockCategory: String?,
itemCode: String?, itemCode: String?,
@@ -33,16 +34,19 @@ open class ItemQcFailReportService(
val qcItemScopeSql = buildQcItemScopeClause(measurable, other) val qcItemScopeSql = buildQcItemScopeClause(measurable, other)
val measuredValueSql = buildMeasuredValueClause(measurable, scope) val measuredValueSql = buildMeasuredValueClause(measurable, scope)


// Date filter = QC result created time (true QC finish), not receiptDate / put-away.
// JO EPQC: receiptDate is often SIL create day; put-away ≈ QC via auto put-away.
// PO IQC: put-away can be days after QC — must not use ill.created for this filter.
val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) { val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) {
val formattedDate = lastInDateStart.replace("/", "-") val formattedDate = lastInDateStart.replace("/", "-")
args["lastInDateStart"] = formattedDate args["lastInDateStart"] = formattedDate
"AND DATE(sil.receiptDate) >= DATE(:lastInDateStart)"
"AND DATE(qr.created) >= DATE(:lastInDateStart)"
} else "" } else ""


val lastInDateEndSql = if (!lastInDateEnd.isNullOrBlank()) { val lastInDateEndSql = if (!lastInDateEnd.isNullOrBlank()) {
val formattedDate = lastInDateEnd.replace("/", "-") val formattedDate = lastInDateEnd.replace("/", "-")
args["lastInDateEnd"] = formattedDate args["lastInDateEnd"] = formattedDate
"AND DATE(sil.receiptDate) <= DATE(:lastInDateEnd)"
"AND DATE(qr.created) <= DATE(:lastInDateEnd)"
} else "" } else ""


val sql = """ val sql = """


+ 182
- 194
src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt Voir le fichier

@@ -4,17 +4,25 @@ import com.ffii.core.support.JdbcDao
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter

@Service @Service
open class StockLedgerReportService( open class StockLedgerReportService(
private val jdbcDao: JdbcDao, private val jdbcDao: JdbcDao,
) { ) {


private val dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd")

/** /**
* FP-MTMS Version Checklist | Functions Ref. No. 65 | v1.0.0 | 2026-08-11
* Stock Ledger 報表查詢 * Stock Ledger 報表查詢
* *
* - stockSubCategory = items.type * - stockSubCategory = items.type
* - trnDate = stock_ledger.date * - trnDate = stock_ledger.date
* - trnRefNo = stock_ledger.type * - trnRefNo = stock_ledger.type
* - cumBalance = stock_ledger.balance(異動後纍計存量)
* - cumOpeningBal = balance - inQty + outQty(異動前纍計期初)
*
* 只查 [start, end] 期間列,不掃起日以前全歷史。
*/ */
fun searchStockLedgerReport( fun searchStockLedgerReport(
stockCategory: String?, stockCategory: String?,
@@ -23,18 +31,15 @@ open class StockLedgerReportService(
reportPeriodStart: String?, reportPeriodStart: String?,
reportPeriodEnd: String?, reportPeriodEnd: String?,
): List<Map<String, Any>> { ): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>() val args = mutableMapOf<String, Any>()
// 1) 先決定 reportPeriodEnd:如果有填 end,就用使用者的;否則用今天

val reportPeriodEnd = (reportPeriodEnd?.replace("/", "-") val reportPeriodEnd = (reportPeriodEnd?.replace("/", "-")
?: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
// 2) 如果有填 start,就用使用者的;否則從 DB 查最早一筆日期
?: LocalDate.now().format(dateFmt))

val reportPeriodStart = if (!reportPeriodStart.isNullOrBlank()) { val reportPeriodStart = if (!reportPeriodStart.isNullOrBlank()) {
reportPeriodStart.replace("/", "-") reportPeriodStart.replace("/", "-")
} else { } else {
// 用簡單 SQL 查全表最早一筆日期(可加上類似 stockCategory/itemCode 過濾)
val minDateSql = """ val minDateSql = """
SELECT DATE_FORMAT(MIN(sl.date), '%Y-%m-%d') AS firstDate SELECT DATE_FORMAT(MIN(sl.date), '%Y-%m-%d') AS firstDate
FROM stock_ledger sl FROM stock_ledger sl
@@ -42,188 +47,39 @@ open class StockLedgerReportService(
AND sl.itemCode IS NOT NULL AND sl.itemCode IS NOT NULL
AND sl.itemCode <> '' AND sl.itemCode <> ''
""".trimIndent() """.trimIndent()
val minDateRow = jdbcDao.queryForList(minDateSql, emptyMap<String, Any>()).firstOrNull() val minDateRow = jdbcDao.queryForList(minDateSql, emptyMap<String, Any>()).firstOrNull()
(minDateRow?.get("firstDate") as? String) (minDateRow?.get("firstDate") as? String)
?: reportPeriodEnd // 如果表是空的,就退回用今天
?: reportPeriodEnd
} }
// 3) 把 from/to 塞到 args,供後面 SQL 使用
val endExclusive = LocalDate.parse(reportPeriodEnd).plusDays(1).format(dateFmt)
args["reportPeriodStart"] = reportPeriodStart args["reportPeriodStart"] = reportPeriodStart
args["reportPeriodEnd"] = reportPeriodEnd
// 4) 之後再用你原來的 stockCategorySql / itemCodeSql / storeLocationSql
args["reportPeriodEndExclusive"] = endExclusive

val stockCategorySql = buildMultiValueExactClause( val stockCategorySql = buildMultiValueExactClause(
stockCategory, stockCategory,
"it.type", "it.type",
"stockCategory", "stockCategory",
args args
) )
val itemCodeSql = buildMultiValueLikeClause( val itemCodeSql = buildMultiValueLikeClause(
itemCode, itemCode,
"sl.itemCode", "sl.itemCode",
"itemCode", "itemCode",
args args
) )

// 用 lot 子查詢的 storeLocation,避免 ill_in 放大列數
val storeLocationSql = if (!storeLocation.isNullOrBlank()) { val storeLocationSql = if (!storeLocation.isNullOrBlank()) {
args["storeLocation"] = "%$storeLocation%" args["storeLocation"] = "%$storeLocation%"
"AND (wh_in.code LIKE :storeLocation OR wh_out.code LIKE :storeLocation)"
"AND lot.storeLocation LIKE :storeLocation"
} else { } else {
"" ""
} }
val reportPeriodEndSql = "AND DATE(sl.date) <= :reportPeriodEnd"


val sql = """ val sql = """
WITH base AS (
SELECT
sl.id AS slId,
DATE(sl.date) AS trnDateRaw,
DATE_FORMAT(sl.date, '%Y-%m-%d') AS trnDate,
CASE
WHEN UPPER(TRIM(COALESCE(sl.type, ''))) = 'TKE' THEN 'TKE'
WHEN UPPER(TRIM(COALESCE(sl.type, ''))) = 'ADJ'
AND (so.stockTakeId IS NOT NULL OR so.type = 'stockTake' OR si.stockTakeId IS NOT NULL) THEN 'TKE'
ELSE COALESCE(sl.type, '')
END AS trnRefNoRaw,

sl.itemId AS itemId,
sl.itemCode AS itemCode,

COALESCE(sl.inQty, 0) AS inQty,
COALESCE(sl.outQty, 0) AS outQty,
(COALESCE(sl.inQty, 0) - COALESCE(sl.outQty, 0)) AS delta,

it.type AS stockSubCategory,
it.code AS itemNo,
it.name AS itemName,
uc.udfudesc AS unitOfMeasure,

lot.lotNo AS lotNo,
COALESCE(DATE_FORMAT(lot.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
lot.storeLocation AS storeLocation,

COALESCE(TRIM(do.code), TRIM(jo_po.code), TRIM(jo.code), TRIM(po.code), '') AS orderRefNo,
COALESCE(TRIM(jo.code), '') AS jobOrderNo,

'' AS openingBalance,
'' AS cumStockIn,
'' AS cumStockOut,
'' AS currentBalance,
'' AS lastInDate,
'' AS lastOutDate,
'' AS reOrderLevel,
'' AS reOrderQty
FROM stock_ledger sl
LEFT JOIN stock_in_line sil
ON sl.stockInLineId = sil.id
AND sil.deleted = 0
LEFT JOIN inventory_lot il_in
ON sil.inventoryLotId = il_in.id
AND il_in.deleted = 0

LEFT JOIN stock_out_line sol
ON sl.stockOutLineId = sol.id
AND sol.deleted = 0
LEFT JOIN inventory_lot_line ill_out
ON sol.inventoryLotLineId = ill_out.id
AND ill_out.deleted = 0
LEFT JOIN inventory_lot il_out
ON ill_out.inventoryLotId = il_out.id
AND il_out.deleted = 0

LEFT JOIN (
SELECT
il.id AS lotId,
il.lotNo AS lotNo,
il.expiryDate AS expiryDate,
MAX(wh.code) AS storeLocation
FROM inventory_lot il
LEFT JOIN inventory_lot_line ill
ON ill.inventoryLotId = il.id
AND ill.deleted = 0
LEFT JOIN warehouse wh
ON ill.warehouseId = wh.id
AND wh.deleted = 0
GROUP BY
il.id, il.lotNo, il.expiryDate
) lot
ON lot.lotId = COALESCE(il_in.id, il_out.id)

LEFT JOIN items it
ON sl.itemId = it.id
AND it.deleted = 0
LEFT JOIN item_uom iu
ON it.id = iu.itemId
AND iu.stockUnit = 1
AND iu.deleted = 0
LEFT JOIN uom_conversion uc
ON iu.uomId = uc.id

-- 這兩個 alias 是為了配合你上面 storeLocationSql 的 wh_in / wh_out
LEFT JOIN inventory_lot_line ill_in
ON il_in.id = ill_in.inventoryLotId
AND ill_in.deleted = 0
LEFT JOIN warehouse wh_in
ON ill_in.warehouseId = wh_in.id
AND wh_in.deleted = 0
LEFT JOIN warehouse wh_out
ON ill_out.warehouseId = wh_out.id
AND wh_out.deleted = 0
LEFT JOIN stock_out so
ON sol.stockOutId = so.id
AND so.deleted = 0
LEFT JOIN pick_order_line pol
ON sol.pickOrderLineId = pol.id
AND pol.deleted = 0
LEFT JOIN pick_order po_out
ON pol.poId = po_out.id
AND po_out.deleted = 0
LEFT JOIN job_order jo_po
ON po_out.joId = jo_po.id
AND jo_po.deleted = 0
LEFT JOIN delivery_order do
ON po_out.doId = do.id
AND do.deleted = 0
LEFT JOIN stock_in si
ON sil.stockInId = si.id
AND si.deleted = 0
LEFT JOIN job_order jo
ON sil.jobOrderId = jo.id
AND jo.deleted = 0
LEFT JOIN purchase_order po
ON sil.purchaseOrderId = po.id
AND po.deleted = 0
WHERE
sl.deleted = 0
AND sl.itemCode IS NOT NULL
AND sl.itemCode <> ''
AND DATE(sl.date) <= :reportPeriodEnd
$stockCategorySql
$itemCodeSql
$storeLocationSql
AND lot.lotId IS NOT NULL
),
opening AS (
SELECT
itemCode,
COALESCE(SUM(delta), 0) AS openingBeforeStart
FROM base
WHERE trnDateRaw < :reportPeriodStart
GROUP BY itemCode
),
period AS (
SELECT
b.*,
COALESCE(o.openingBeforeStart, 0) AS openingBeforeStart
FROM base b
LEFT JOIN opening o
ON o.itemCode = b.itemCode
WHERE b.trnDateRaw BETWEEN :reportPeriodStart AND :reportPeriodEnd
)
SELECT SELECT
stockSubCategory, stockSubCategory,
itemNo, itemNo,
@@ -231,7 +87,7 @@ SELECT
unitOfMeasure, unitOfMeasure,
lotNo, lotNo,
expiryDate, expiryDate,
trnDate,
trnDate,
CASE trnRefNoRaw CASE trnRefNoRaw
WHEN 'OPEN' THEN '開倉' WHEN 'OPEN' THEN '開倉'
WHEN 'NOR' THEN '出入倉' WHEN 'NOR' THEN '出入倉'
@@ -255,39 +111,171 @@ SELECT
reOrderLevel, reOrderLevel,
reOrderQty, reOrderQty,


-- jrxml 需要 String;負數括號顯示,無小數
CASE WHEN COALESCE(inQty, 0) < 0 THEN CONCAT('(', FORMAT(-inQty, 0), ')') ELSE FORMAT(COALESCE(inQty, 0), 0) END AS stockIn,
CASE WHEN COALESCE(outQty, 0) < 0 THEN CONCAT('(', FORMAT(-outQty, 0), ')') ELSE FORMAT(COALESCE(outQty, 0), 0) END AS stockOut,

-- 累計存量(跨 lot:只用 itemCode 分區)
CASE WHEN (openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId)) < 0
THEN CONCAT('(', FORMAT(-(openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId)), 0), ')')
ELSE FORMAT(openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId), 0) END AS cumBalance,

-- 累計期初存量 = 本行累計 - 本行異動
CASE WHEN (openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId) - delta) < 0
THEN CONCAT('(', FORMAT(-(openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId) - delta), 0), ')')
ELSE FORMAT(openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId) - delta, 0) END AS cumOpeningBal,

-- footer totals(同樣輸出 String)
CASE WHEN COALESCE(openingBeforeStart, 0) < 0 THEN CONCAT('(', FORMAT(-openingBeforeStart, 0), ')') ELSE FORMAT(COALESCE(openingBeforeStart, 0), 0) END AS totalCumOpeningBal,
CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemCode) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemCode), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemCode), 0) END AS totalStockIn,
CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemCode) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemCode), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemCode), 0) END AS totalStockOut,
CASE WHEN (openingBeforeStart + SUM(inQty) OVER (PARTITION BY itemCode) - SUM(outQty) OVER (PARTITION BY itemCode)) < 0
THEN CONCAT('(', FORMAT(-(openingBeforeStart + SUM(inQty) OVER (PARTITION BY itemCode) - SUM(outQty) OVER (PARTITION BY itemCode)), 0), ')')
ELSE FORMAT(openingBeforeStart + SUM(inQty) OVER (PARTITION BY itemCode) - SUM(outQty) OVER (PARTITION BY itemCode), 0) END AS totalCumBalance

FROM period
CASE WHEN inQty < 0 THEN CONCAT('(', FORMAT(-inQty, 0), ')') ELSE FORMAT(inQty, 0) END AS stockIn,
CASE WHEN outQty < 0 THEN CONCAT('(', FORMAT(-outQty, 0), ')') ELSE FORMAT(outQty, 0) END AS stockOut,

CASE WHEN cumOpeningBalRaw < 0
THEN CONCAT('(', FORMAT(-cumOpeningBalRaw, 0), ')')
ELSE FORMAT(cumOpeningBalRaw, 0) END AS cumOpeningBal,
CASE WHEN bal < 0
THEN CONCAT('(', FORMAT(-bal, 0), ')')
ELSE FORMAT(bal, 0) END AS cumBalance,

CASE WHEN totalCumOpeningBalRaw < 0
THEN CONCAT('(', FORMAT(-totalCumOpeningBalRaw, 0), ')')
ELSE FORMAT(totalCumOpeningBalRaw, 0) END AS totalCumOpeningBal,
CASE WHEN totalStockInRaw < 0
THEN CONCAT('(', FORMAT(-totalStockInRaw, 0), ')')
ELSE FORMAT(totalStockInRaw, 0) END AS totalStockIn,
CASE WHEN totalStockOutRaw < 0
THEN CONCAT('(', FORMAT(-totalStockOutRaw, 0), ')')
ELSE FORMAT(totalStockOutRaw, 0) END AS totalStockOut,
CASE WHEN totalCumBalanceRaw < 0
THEN CONCAT('(', FORMAT(-totalCumBalanceRaw, 0), ')')
ELSE FORMAT(totalCumBalanceRaw, 0) END AS totalCumBalance

FROM (
SELECT
x.*,
FIRST_VALUE(cumOpeningBalRaw) OVER (
PARTITION BY itemCode ORDER BY trnDateRaw, slId
) AS totalCumOpeningBalRaw,
SUM(inQty) OVER (PARTITION BY itemCode) AS totalStockInRaw,
SUM(outQty) OVER (PARTITION BY itemCode) AS totalStockOutRaw,
FIRST_VALUE(bal) OVER (
PARTITION BY itemCode ORDER BY trnDateRaw DESC, slId DESC
) AS totalCumBalanceRaw
FROM (
SELECT
sl.id AS slId,
DATE(sl.date) AS trnDateRaw,
DATE_FORMAT(sl.date, '%Y-%m-%d') AS trnDate,
CASE
WHEN UPPER(TRIM(COALESCE(sl.type, ''))) = 'TKE' THEN 'TKE'
WHEN UPPER(TRIM(COALESCE(sl.type, ''))) = 'ADJ'
AND (so.stockTakeId IS NOT NULL OR so.type = 'stockTake' OR si.stockTakeId IS NOT NULL) THEN 'TKE'
ELSE COALESCE(sl.type, '')
END AS trnRefNoRaw,

sl.itemCode AS itemCode,
COALESCE(sl.inQty, 0) AS inQty,
COALESCE(sl.outQty, 0) AS outQty,
COALESCE(sl.balance, 0) AS bal,
COALESCE(sl.balance, 0)
- COALESCE(sl.inQty, 0)
+ COALESCE(sl.outQty, 0) AS cumOpeningBalRaw,

it.type AS stockSubCategory,
it.code AS itemNo,
it.name AS itemName,
uc.udfudesc AS unitOfMeasure,

lot.lotNo AS lotNo,
COALESCE(DATE_FORMAT(lot.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
lot.storeLocation AS storeLocation,

COALESCE(TRIM(do.code), TRIM(jo_po.code), TRIM(jo.code), TRIM(po.code), '') AS orderRefNo,
COALESCE(TRIM(jo.code), '') AS jobOrderNo,

'' AS openingBalance,
'' AS cumStockIn,
'' AS cumStockOut,
'' AS currentBalance,
'' AS lastInDate,
'' AS lastOutDate,
'' AS reOrderLevel,
'' AS reOrderQty
FROM stock_ledger sl
LEFT JOIN stock_in_line sil
ON sl.stockInLineId = sil.id
AND sil.deleted = 0
LEFT JOIN inventory_lot il_in
ON sil.inventoryLotId = il_in.id
AND il_in.deleted = 0

LEFT JOIN stock_out_line sol
ON sl.stockOutLineId = sol.id
AND sol.deleted = 0
LEFT JOIN inventory_lot_line ill_out
ON sol.inventoryLotLineId = ill_out.id
AND ill_out.deleted = 0
LEFT JOIN inventory_lot il_out
ON ill_out.inventoryLotId = il_out.id
AND il_out.deleted = 0

LEFT JOIN (
SELECT
il.id AS lotId,
il.lotNo AS lotNo,
il.expiryDate AS expiryDate,
MAX(wh.code) AS storeLocation
FROM inventory_lot il
LEFT JOIN inventory_lot_line ill
ON ill.inventoryLotId = il.id
AND ill.deleted = 0
LEFT JOIN warehouse wh
ON ill.warehouseId = wh.id
AND wh.deleted = 0
WHERE il.deleted = 0
GROUP BY
il.id, il.lotNo, il.expiryDate
) lot
ON lot.lotId = COALESCE(il_in.id, il_out.id)

LEFT JOIN items it
ON sl.itemId = it.id
AND it.deleted = 0
LEFT JOIN item_uom iu
ON it.id = iu.itemId
AND iu.stockUnit = 1
AND iu.deleted = 0
LEFT JOIN uom_conversion uc
ON iu.uomId = uc.id

LEFT JOIN stock_out so
ON sol.stockOutId = so.id
AND so.deleted = 0
LEFT JOIN pick_order_line pol
ON sol.pickOrderLineId = pol.id
AND pol.deleted = 0
LEFT JOIN pick_order po_out
ON pol.poId = po_out.id
AND po_out.deleted = 0
LEFT JOIN job_order jo_po
ON po_out.joId = jo_po.id
AND jo_po.deleted = 0
LEFT JOIN delivery_order do
ON po_out.doId = do.id
AND do.deleted = 0
LEFT JOIN stock_in si
ON sil.stockInId = si.id
AND si.deleted = 0
LEFT JOIN job_order jo
ON sil.jobOrderId = jo.id
AND jo.deleted = 0
LEFT JOIN purchase_order po
ON sil.purchaseOrderId = po.id
AND po.deleted = 0
WHERE
sl.deleted = 0
AND sl.itemCode IS NOT NULL
AND sl.itemCode <> ''
AND sl.date >= :reportPeriodStart
AND sl.date < :reportPeriodEndExclusive
$stockCategorySql
$itemCodeSql
$storeLocationSql
AND lot.lotId IS NOT NULL
) x
) y
ORDER BY ORDER BY
itemNo, itemNo,
trnDateRaw, trnDateRaw,
slId, slId,
lotNo lotNo
""".trimIndent() """.trimIndent()
val result = jdbcDao.queryForList(sql, args)


return result
return jdbcDao.queryForList(sql, args)
} }


/** LIKE 多值工具方法 */ /** LIKE 多值工具方法 */
@@ -327,4 +315,4 @@ ORDER BY
} }
return "AND (${conditions.joinToString(" OR ")})" return "AND (${conditions.joinToString(" OR ")})"
} }
}
}

+ 2
- 2
src/main/java/com/ffii/fpsms/modules/report/web/DoInventoryUomMismatchReportController.kt Voir le fichier

@@ -23,7 +23,7 @@ import java.io.ByteArrayOutputStream
class DoInventoryUomMismatchReportController( class DoInventoryUomMismatchReportController(
private val doInventoryUomMismatchReportService: DoInventoryUomMismatchReportService, private val doInventoryUomMismatchReportService: DoInventoryUomMismatchReportService,
) { ) {
/** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.0 | 2026-08-10 */
/** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.1 | 2026-08-11 */
@GetMapping("/print-do-inventory-uom-mismatch-excel") @GetMapping("/print-do-inventory-uom-mismatch-excel")
fun exportExcel( fun exportExcel(
@RequestParam(required = false) deliveryDate: String?, @RequestParam(required = false) deliveryDate: String?,
@@ -69,7 +69,7 @@ class DoInventoryUomMismatchReportController(
} }


val columns = listOf( val columns = listOf(
"deliveryDate" to "預計貨日期",
"deliveryDate" to "預計貨日期",
"doCode" to "送貨單號", "doCode" to "送貨單號",
"supplierCode" to "供應商編號", "supplierCode" to "供應商編號",
"supplierName" to "供應商名稱", "supplierName" to "供應商名稱",


+ 11
- 5
src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt Voir le fichier

@@ -21,6 +21,7 @@ class ItemQcFailReportController(
private val itemQcFailReportService: ItemQcFailReportService, private val itemQcFailReportService: ItemQcFailReportService,
) { ) {


/** FP-MTMS Version Checklist | Functions Ref. No. 64 | v1.0.0 | 2026-08-11 */
@GetMapping("/print-item-qc-fail") @GetMapping("/print-item-qc-fail")
fun generateItemQcFailReport( fun generateItemQcFailReport(
@RequestParam(required = false) stockCategory: String?, @RequestParam(required = false) stockCategory: String?,
@@ -29,7 +30,7 @@ class ItemQcFailReportController(
@RequestParam(required = false) lastInDateEnd: String?, @RequestParam(required = false) lastInDateEnd: String?,
@RequestParam(required = false) qcType: String?, @RequestParam(required = false) qcType: String?,
@RequestParam(required = false, defaultValue = "true") includeMeasurable: String?, @RequestParam(required = false, defaultValue = "true") includeMeasurable: String?,
@RequestParam(required = false, defaultValue = "false") includeOther: String?,
@RequestParam(required = false, defaultValue = "true") includeOther: String?,
@RequestParam(required = false, defaultValue = "all") measurableScope: String?, @RequestParam(required = false, defaultValue = "all") measurableScope: String?,
): ResponseEntity<ByteArray> { ): ResponseEntity<ByteArray> {
val dbData = itemQcFailReportService.searchItemQcFailReport( val dbData = itemQcFailReportService.searchItemQcFailReport(
@@ -70,6 +71,7 @@ class ItemQcFailReportController(
return ResponseEntity(pdfBytes, headers, HttpStatus.OK) return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 64 | v1.0.0 | 2026-08-11 */
@GetMapping("/print-item-qc-fail-excel") @GetMapping("/print-item-qc-fail-excel")
fun exportItemQcFailReportExcel( fun exportItemQcFailReportExcel(
@RequestParam(required = false) stockCategory: String?, @RequestParam(required = false) stockCategory: String?,
@@ -78,7 +80,7 @@ class ItemQcFailReportController(
@RequestParam(required = false) lastInDateEnd: String?, @RequestParam(required = false) lastInDateEnd: String?,
@RequestParam(required = false) qcType: String?, @RequestParam(required = false) qcType: String?,
@RequestParam(required = false, defaultValue = "true") includeMeasurable: String?, @RequestParam(required = false, defaultValue = "true") includeMeasurable: String?,
@RequestParam(required = false, defaultValue = "false") includeOther: String?,
@RequestParam(required = false, defaultValue = "true") includeOther: String?,
@RequestParam(required = false, defaultValue = "all") measurableScope: String?, @RequestParam(required = false, defaultValue = "all") measurableScope: String?,
): ResponseEntity<ByteArray> { ): ResponseEntity<ByteArray> {
val dbData = itemQcFailReportService.searchItemQcFailReport( val dbData = itemQcFailReportService.searchItemQcFailReport(
@@ -297,7 +299,7 @@ class ItemQcFailReportController(
"不合格數量", "不合格數量",
"實測值", "實測值",
"備註", "備註",
"訂單/工單"
"訂單/工單",
) )


run { run {
@@ -321,12 +323,16 @@ class ItemQcFailReportController(


fun writeNumber(col: Int, value: Any?) { fun writeNumber(col: Int, value: Any?) {
val raw = value?.toString()?.trim() ?: "" val raw = value?.toString()?.trim() ?: ""
val cleaned = raw.removeSuffix(".")
// SQL FORMAT() may emit thousand separators (e.g. "1,021") — strip before parse.
val cleaned = raw
.replace(",", "")
.replace(" ", "")
.removeSuffix(".")
val bd = cleaned.toBigDecimalOrNull() val bd = cleaned.toBigDecimalOrNull()
val cell = row.createCell(col) val cell = row.createCell(col)


if (bd == null) { if (bd == null) {
cell.setCellValue(cleaned)
cell.setCellValue(raw)
cell.cellStyle = textStyle cell.cellStyle = textStyle
} else { } else {
val stripped = bd.stripTrailingZeros() val stripped = bd.stripTrailingZeros()


+ 201
- 172
src/main/java/com/ffii/fpsms/modules/report/web/StockLedgerReportController.kt Voir le fichier

@@ -14,10 +14,10 @@ import org.apache.poi.ss.usermodel.HorizontalAlignment
import org.apache.poi.ss.usermodel.IndexedColors import org.apache.poi.ss.usermodel.IndexedColors
import org.apache.poi.ss.usermodel.Row import org.apache.poi.ss.usermodel.Row
import org.apache.poi.ss.usermodel.VerticalAlignment 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.CellRangeAddress
import org.apache.poi.ss.util.WorkbookUtil import org.apache.poi.ss.util.WorkbookUtil
import org.apache.poi.xssf.usermodel.XSSFCellStyle
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.apache.poi.xssf.streaming.SXSSFWorkbook
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream


@RestController @RestController
@@ -27,66 +27,66 @@ class StockLedgerReportController(
private val stockLedgerReportService: StockLedgerReportService, private val stockLedgerReportService: StockLedgerReportService,
) { ) {
private data class ExcelStyles( private data class ExcelStyles(
val title: XSSFCellStyle,
val subtitle: XSSFCellStyle,
val header: XSSFCellStyle,
val text: XSSFCellStyle,
val center: XSSFCellStyle,
val int: XSSFCellStyle,
val dash: XSSFCellStyle,
val sumQty: XSSFCellStyle,
val sumLabel: XSSFCellStyle,
val sumEmpty: XSSFCellStyle,
val sumHidden: XSSFCellStyle,
val title: CellStyle,
val subtitle: CellStyle,
val header: CellStyle,
val text: CellStyle,
val center: CellStyle,
val int: CellStyle,
val dash: CellStyle,
val sumQty: CellStyle,
val sumLabel: CellStyle,
val sumEmpty: CellStyle,
val sumHidden: CellStyle,
) )


/** FP-MTMS Version Checklist | Functions Ref. No. 65 | v1.0.0 | 2026-08-11 */
@GetMapping("/print-stock-ledger") @GetMapping("/print-stock-ledger")
fun generateStockLedgerReport(
@RequestParam(required = false) stockCategory: String?,
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false) storeLocation: String?,
// URL 參數名仍然是 lastInDateStart / lastInDateEnd
@RequestParam(name = "lastInDateStart", required = false) reportPeriodStart: String?,
@RequestParam(name = "lastInDateEnd", required = false) reportPeriodEnd: String?,
): ResponseEntity<ByteArray> {
val parameters = mutableMapOf<String, Any>()

parameters["stockCategory"] = stockCategory ?: "All"
parameters["stockSubCategory"] = stockCategory ?: "All"
parameters["itemNo"] = itemCode ?: "All"
parameters["year"] = LocalDate.now().year.toString()
parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))
fun generateStockLedgerReport(
@RequestParam(required = false) stockCategory: String?,
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false) storeLocation: String?,
@RequestParam(name = "lastInDateStart", required = false) reportPeriodStart: String?,
@RequestParam(name = "lastInDateEnd", required = false) reportPeriodEnd: String?,
): ResponseEntity<ByteArray> {
val parameters = mutableMapOf<String, Any>()


parameters["storeLocation"] = storeLocation ?: ""
parameters["balanceFilterStart"] = ""
parameters["balanceFilterEnd"] = ""
parameters["reportPeriodStart"] = reportPeriodStart ?: ""
parameters["reportPeriodEnd"] = reportPeriodEnd ?: ""
parameters["stockCategory"] = stockCategory ?: "All"
parameters["stockSubCategory"] = stockCategory ?: "All"
parameters["itemNo"] = itemCode ?: "All"
parameters["year"] = LocalDate.now().year.toString()
parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))


parameters["storeLocation"] = storeLocation ?: ""
parameters["balanceFilterStart"] = ""
parameters["balanceFilterEnd"] = ""
parameters["reportPeriodStart"] = reportPeriodStart ?: ""
parameters["reportPeriodEnd"] = reportPeriodEnd ?: ""


val dbData = stockLedgerReportService.searchStockLedgerReport(
stockCategory = stockCategory,
itemCode = itemCode,
storeLocation = storeLocation,
reportPeriodStart = reportPeriodStart,
reportPeriodEnd = reportPeriodEnd,
)
val dbData = stockLedgerReportService.searchStockLedgerReport(
stockCategory = stockCategory,
itemCode = itemCode,
storeLocation = storeLocation,
reportPeriodStart = reportPeriodStart,
reportPeriodEnd = reportPeriodEnd,
)


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


val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_PDF
setContentDispositionFormData("attachment", "StockLedgerReport.pdf")
set("filename", "StockLedgerReport.pdf")
val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_PDF
setContentDispositionFormData("attachment", "StockLedgerReport.pdf")
set("filename", "StockLedgerReport.pdf")
}
return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
} }
return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
}


/** FP-MTMS Version Checklist | Functions Ref. No. 65 | v1.0.0 | 2026-08-11 */
@GetMapping("/print-stock-ledger-excel") @GetMapping("/print-stock-ledger-excel")
fun exportStockLedgerReportExcel( fun exportStockLedgerReportExcel(
@RequestParam(required = false) stockCategory: String?, @RequestParam(required = false) stockCategory: String?,
@@ -119,8 +119,8 @@ fun generateStockLedgerReport(
return ResponseEntity(excelBytes, headers, HttpStatus.OK) return ResponseEntity(excelBytes, headers, HttpStatus.OK)
} }


private fun createStyles(workbook: XSSFWorkbook): ExcelStyles {
val titleStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
private fun createStyles(workbook: Workbook): ExcelStyles {
val titleStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.CENTER alignment = HorizontalAlignment.CENTER
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
val font = workbook.createFont().apply { val font = workbook.createFont().apply {
@@ -129,7 +129,7 @@ fun generateStockLedgerReport(
} }
setFont(font) setFont(font)
} }
val subtitleStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
val subtitleStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.LEFT alignment = HorizontalAlignment.LEFT
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
val font = workbook.createFont().apply { val font = workbook.createFont().apply {
@@ -138,7 +138,7 @@ fun generateStockLedgerReport(
} }
setFont(font) setFont(font)
} }
val headerStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
val headerStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.CENTER alignment = HorizontalAlignment.CENTER
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
fillForegroundColor = IndexedColors.GREY_25_PERCENT.index fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
@@ -150,7 +150,7 @@ fun generateStockLedgerReport(
val font = workbook.createFont().apply { bold = true } val font = workbook.createFont().apply { bold = true }
setFont(font) setFont(font)
} }
val textStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
val textStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.LEFT alignment = HorizontalAlignment.LEFT
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THIN borderTop = BorderStyle.THIN
@@ -158,7 +158,7 @@ fun generateStockLedgerReport(
borderLeft = BorderStyle.THIN borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN borderRight = BorderStyle.THIN
} }
val centerStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
val centerStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.CENTER alignment = HorizontalAlignment.CENTER
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THIN borderTop = BorderStyle.THIN
@@ -166,7 +166,7 @@ fun generateStockLedgerReport(
borderLeft = BorderStyle.THIN borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN borderRight = BorderStyle.THIN
} }
val intStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
val intStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THIN borderTop = BorderStyle.THIN
@@ -176,7 +176,7 @@ fun generateStockLedgerReport(
val df: DataFormat = workbook.createDataFormat() val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0") dataFormat = df.getFormat("#,##0")
} }
val dashStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
val dashStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THIN borderTop = BorderStyle.THIN
@@ -184,7 +184,7 @@ fun generateStockLedgerReport(
borderLeft = BorderStyle.THIN borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN borderRight = BorderStyle.THIN
} }
val sumQty = (workbook.createCellStyle() as XSSFCellStyle).apply {
val sumQty = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
val df: DataFormat = workbook.createDataFormat() val df: DataFormat = workbook.createDataFormat()
@@ -196,7 +196,7 @@ fun generateStockLedgerReport(
val font = workbook.createFont().apply { bold = true } val font = workbook.createFont().apply { bold = true }
setFont(font) setFont(font)
} }
val sumLabel = (workbook.createCellStyle() as XSSFCellStyle).apply {
val sumLabel = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THICK borderTop = BorderStyle.THICK
@@ -206,7 +206,7 @@ fun generateStockLedgerReport(
val font = workbook.createFont().apply { bold = true } val font = workbook.createFont().apply { bold = true }
setFont(font) setFont(font)
} }
val sumEmpty = (workbook.createCellStyle() as XSSFCellStyle).apply {
val sumEmpty = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.LEFT alignment = HorizontalAlignment.LEFT
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THICK borderTop = BorderStyle.THICK
@@ -214,7 +214,7 @@ fun generateStockLedgerReport(
borderLeft = BorderStyle.THIN borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN borderRight = BorderStyle.THIN
} }
val sumHidden = (workbook.createCellStyle() as XSSFCellStyle).apply {
val sumHidden = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.LEFT alignment = HorizontalAlignment.LEFT
verticalAlignment = VerticalAlignment.CENTER verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THICK borderTop = BorderStyle.THICK
@@ -239,7 +239,7 @@ fun generateStockLedgerReport(
) )
} }


private fun setTextCell(row: Row, col: Int, value: Any?, style: XSSFCellStyle) {
private fun setTextCell(row: Row, col: Int, value: Any?, style: CellStyle) {
row.createCell(col).apply { row.createCell(col).apply {
setCellValue(value?.toString() ?: "") setCellValue(value?.toString() ?: "")
cellStyle = style cellStyle = style
@@ -259,8 +259,8 @@ fun generateStockLedgerReport(
row: Row, row: Row,
col: Int, col: Int,
value: Any?, value: Any?,
intStyle: XSSFCellStyle,
dashStyle: XSSFCellStyle,
intStyle: CellStyle,
dashStyle: CellStyle,
) { ) {
val cell = row.createCell(col) val cell = row.createCell(col)
val parsed = parseSignedNumber(value) val parsed = parseSignedNumber(value)
@@ -279,131 +279,160 @@ fun generateStockLedgerReport(
} }
} }


/**
* SXSSF keeps only a sliding window of rows in memory to avoid OOM on large exports.
*/
private fun createStockLedgerExcel( private fun createStockLedgerExcel(
dbData: List<Map<String, Any>>, dbData: List<Map<String, Any>>,
reportPeriodStart: String, reportPeriodStart: String,
reportPeriodEnd: String, reportPeriodEnd: String,
): ByteArray { ): ByteArray {
val workbook = XSSFWorkbook()
val styles = createStyles(workbook)
val reportTitle = "庫存明細報告"
val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(reportTitle))

val headers = listOf(
"貨品編號", "貨品名稱", "單位",
"出入賬日期", "類型", "批號", "到期日",
"纍計期初存量", "入庫", "出庫", "纍計存量",
"參考編號", "存貨位置",
)
val totalColumns = headers.size
var rowIndex = 0
val workbook = SXSSFWorkbook(100)
workbook.setCompressTempFiles(true)
try {
val styles = createStyles(workbook)
val reportTitle = "庫存明細報告"
val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(reportTitle))


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, 4))
subtitleRow.createCell(5).apply {
setCellValue("報告期間:${reportPeriodStart.trim()} 至 ${reportPeriodEnd.trim()}")
cellStyle = styles.subtitle
}
sheet.addMergedRegion(CellRangeAddress(1, 1, 5, totalColumns - 1))
sheet.createRow(rowIndex++)
val headers = listOf(
"貨品編號", "貨品名稱", "單位",
"出入賬日期", "類型", "批號", "到期日",
"纍計期初存量", "入庫", "出庫", "纍計存量",
"參考編號", "存貨位置",
)
val totalColumns = headers.size
var rowIndex = 0


val headerRowIndex = rowIndex
val headerRow = sheet.createRow(rowIndex++)
headers.forEachIndexed { i, h ->
headerRow.createCell(i).apply {
setCellValue(h)
cellStyle = styles.header
val titleRow = sheet.createRow(rowIndex++)
titleRow.createCell(0).apply {
setCellValue(reportTitle)
cellStyle = styles.title
} }
}
sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1))


fun addItemSummaryRow(itemNo: String, itemName: String, uom: String, totalIn: Any?, totalOut: Any?, totalBal: 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 }
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
} }
// totals should align with numeric columns (shift right by 1)
r.getCell(7).apply { setCellValue("貨品總量:"); cellStyle = styles.sumLabel }
setIntCellFromFormatted(r, 8, totalIn, styles.sumQty, styles.dash)
setIntCellFromFormatted(r, 9, totalOut, styles.sumQty, styles.dash)
setIntCellFromFormatted(r, 10, totalBal, styles.sumQty, styles.dash)
}

if (dbData.isEmpty()) {
val r = sheet.createRow(rowIndex++)
for (c in 0 until totalColumns) {
r.createCell(c).apply { setCellValue("-"); cellStyle = styles.text }
sheet.addMergedRegion(CellRangeAddress(1, 1, 0, 4))
subtitleRow.createCell(5).apply {
setCellValue("報告期間:${reportPeriodStart.trim()} 至 ${reportPeriodEnd.trim()}")
cellStyle = styles.subtitle
} }
} else {
var currentItemNo: String? = null
var currentItemName = ""
var currentUom = ""
var lastTotals: Triple<Any?, Any?, Any?> = Triple(null, null, null)
sheet.addMergedRegion(CellRangeAddress(1, 1, 5, totalColumns - 1))
sheet.createRow(rowIndex++)


dbData.forEach { m ->
val itemNo = m["itemNo"]?.toString().orEmpty()
val itemName = m["itemName"]?.toString().orEmpty()
val uom = m["unitOfMeasure"]?.toString().orEmpty()
val headerRowIndex = rowIndex
val headerRow = sheet.createRow(rowIndex++)
headers.forEachIndexed { i, h ->
headerRow.createCell(i).apply {
setCellValue(h)
cellStyle = styles.header
}
}


if (currentItemNo != null && itemNo != currentItemNo) {
addItemSummaryRow(currentItemNo!!, currentItemName, currentUom, lastTotals.first, lastTotals.second, lastTotals.third)
sheet.createRow(rowIndex++)
fun addItemSummaryRow(
itemNo: String,
itemName: String,
uom: String,
totalIn: Any?,
totalOut: Any?,
totalBal: 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 }
} }
r.getCell(7).apply { setCellValue("貨品總量:"); cellStyle = styles.sumLabel }
setIntCellFromFormatted(r, 8, totalIn, styles.sumQty, styles.dash)
setIntCellFromFormatted(r, 9, totalOut, styles.sumQty, styles.dash)
setIntCellFromFormatted(r, 10, totalBal, styles.sumQty, styles.dash)
}


if (dbData.isEmpty()) {
val r = 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["trnDate"], styles.center)
val typeText = m["trnRefNo"]?.toString()?.trim().orEmpty().let { t ->
if (t.equals("Expiry", ignoreCase = true)) "過期" else t
for (c in 0 until totalColumns) {
r.createCell(c).apply { setCellValue("-"); cellStyle = styles.text }
} }
setTextCell(r, 4, typeText, styles.center)
setTextCell(r, 5, m["lotNo"], styles.text)
setTextCell(r, 6, m["expiryDate"], styles.center)
setIntCellFromFormatted(r, 7, m["cumOpeningBal"], styles.int, styles.dash)
setIntCellFromFormatted(r, 8, m["stockIn"], styles.int, styles.dash)
setIntCellFromFormatted(r, 9, m["stockOut"], styles.int, styles.dash)
setIntCellFromFormatted(r, 10, m["cumBalance"], styles.int, styles.dash)
setTextCell(r, 11, m["orderRefNo"], styles.text)
setTextCell(r, 12, m["storeLocation"], styles.center)
} 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()


currentItemNo = itemNo
currentItemName = itemName
currentUom = uom
lastTotals = Triple(m["totalStockIn"], m["totalStockOut"], m["totalCumBalance"])
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["trnDate"], styles.center)
val typeText = m["trnRefNo"]?.toString()?.trim().orEmpty().let { t ->
if (t.equals("Expiry", ignoreCase = true)) "過期" else t
}
setTextCell(r, 4, typeText, styles.center)
setTextCell(r, 5, m["lotNo"], styles.text)
setTextCell(r, 6, m["expiryDate"], styles.center)
setIntCellFromFormatted(r, 7, m["cumOpeningBal"], styles.int, styles.dash)
setIntCellFromFormatted(r, 8, m["stockIn"], styles.int, styles.dash)
setIntCellFromFormatted(r, 9, m["stockOut"], styles.int, styles.dash)
setIntCellFromFormatted(r, 10, m["cumBalance"], styles.int, styles.dash)
setTextCell(r, 11, m["orderRefNo"], styles.text)
setTextCell(r, 12, m["storeLocation"], styles.center)

currentItemNo = itemNo
currentItemName = itemName
currentUom = uom
lastTotals = Triple(m["totalStockIn"], m["totalStockOut"], m["totalCumBalance"])
}

addItemSummaryRow(
currentItemNo ?: "",
currentItemName,
currentUom,
lastTotals.first,
lastTotals.second,
lastTotals.third,
)
} }


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, 12, 10, 16, 12, 14, 10, 10, 12, 18, 12)
widths.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) }


val lastRowIndex = rowIndex - 1
if (lastRowIndex >= headerRowIndex) {
sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0))
val out = ByteArrayOutputStream()
workbook.write(out)
return out.toByteArray()
} finally {
workbook.dispose()
workbook.close()
} }
val widths = intArrayOf(14, 26, 10, 12, 10, 16, 12, 14, 10, 10, 12, 18, 12)
widths.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) }

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

+ 1
- 1
src/main/resources/jasper/StockLedgarReport.jrxml Voir le fichier

@@ -79,7 +79,7 @@
<band height="18"> <band height="18">
<textField textAdjust="StretchHeight"> <textField textAdjust="StretchHeight">
<reportElement isPrintRepeatedValues="false" x="11" y="0" width="579" height="18" uuid="5b2d2e95-26eb-4e8c-93ba-99eaed3490df"/> <reportElement isPrintRepeatedValues="false" x="11" y="0" width="579" height="18" uuid="5b2d2e95-26eb-4e8c-93ba-99eaed3490df"/>
<textElement textAlignment="Left" verticalAlignment="Top" markup="styled">
<textElement textAlignment="Left" verticalAlignment="Top" markup="none">
<font fontName="微軟正黑體" isBold="true"/> <font fontName="微軟正黑體" isBold="true"/>
</textElement> </textElement>
<textFieldExpression><![CDATA[$F{itemNo}+" "+$F{itemName}+" "+$F{unitOfMeasure}]]></textFieldExpression> <textFieldExpression><![CDATA[$F{itemNo}+" "+$F{itemName}+" "+$F{unitOfMeasure}]]></textFieldExpression>


Chargement…
Annuler
Enregistrer