Просмотр исходного кода

reopen stock lot ledger report and improve stock ledger report and if INT show as x.00

fix負數倉
CANCERYS\kw093 9 часов назад
Родитель
Сommit
4941bfcf39
14 измененных файлов: 639 добавлений и 298 удалений
  1. +73
    -30
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  2. +517
    -238
      src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt
  3. +2
    -2
      src/main/java/com/ffii/fpsms/modules/report/web/FGStockOutTraceabilityReportController.kt
  4. +2
    -2
      src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt
  5. +2
    -2
      src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt
  6. +9
    -9
      src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt
  7. +1
    -1
      src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt
  8. +1
    -1
      src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt
  9. +25
    -5
      src/main/java/com/ffii/fpsms/modules/report/web/StockLedgerReportController.kt
  10. +1
    -1
      src/main/java/com/ffii/fpsms/modules/report/web/StockLotBalanceReportController.kt
  11. +1
    -1
      src/main/java/com/ffii/fpsms/modules/report/web/StockLotLedgerReportController.kt
  12. +3
    -4
      src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt
  13. +1
    -1
      src/main/resources/jasper/StockBalanceReport.jrxml
  14. +1
    -1
      src/main/resources/jasper/StockLedgarReport.jrxml

+ 73
- 30
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Просмотреть файл

@@ -26,6 +26,7 @@ open class ReportService(
private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
) {
private val log = LoggerFactory.getLogger(javaClass)
private val compiledReports = java.util.concurrent.ConcurrentHashMap<String, JasperReport>()
/**
* Queries the database for inventory data based on dates and optional item type.
*/
@@ -1498,8 +1499,8 @@ return result
}

/**
* FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.1 | 2026-09-16
* Stock Balance Report (date-driven). One summarized row per item.
* FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.2 | 2026-09-21
* Stock Balance Report (date-driven). One summarized row per item + stock UOM.
*
* Qty comes from [stock_lot_day] (daily lot close) instead of scanning all stock_ledger:
* - 期初存量: historical = that day's opening; today = yesterday's closing
@@ -1507,8 +1508,8 @@ return result
* (today is not closed yet). If the day row is missing, fall back to previous
* close + that day's ledger net.
* - 當日入/出/MISS/BAD/TKE: that calendar day's stock_ledger only
* - 單位均價: items.AverageUnitPrice
* - 庫存總價值: 單位均價 * 現存存貨
* - 單位均價: items.AverageUnitPrice (HKD / stock UOM), converted by item_uom ratio
* - 庫存總價值: 該 UOM 均價 * 該 UOM 現存存貨
*/
fun searchStockBalanceReportByDate(
stockCategory: String?,
@@ -1540,8 +1541,10 @@ return result
FROM inventory_lot_line ill
INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId AND il.deleted = 0
INNER JOIN warehouse wh ON wh.id = ill.warehouseId AND wh.deleted = 0
LEFT JOIN item_uom iu_loc ON iu_loc.id = ill.stockItemUomId AND iu_loc.deleted = 0
WHERE ill.deleted = 0
AND il.itemId = q.itemId
AND iu_loc.uomId <=> q.uomId
AND wh.code LIKE :storeLocation
)
""".trimIndent()
@@ -1554,7 +1557,7 @@ return result
val inventoryScopeUnion = if (useLiveInventory) {
"""
UNION
SELECT itemId FROM inventory_per_item WHERE currentBalance <> 0
SELECT itemId, uomId FROM inventory_per_item WHERE currentBalance <> 0
""".trimIndent()
} else {
""
@@ -1564,6 +1567,7 @@ return result
WITH day0 AS (
SELECT
d.itemId,
d.uomId,
SUM(COALESCE(d.opening, 0)) AS openingBalance,
SUM(COALESCE(d.closing, 0)) AS currentBalance
FROM stock_lot_day d
@@ -1571,33 +1575,36 @@ return result
AND d.date = :d0
AND d.itemId IS NOT NULL
$itemCodeSqlDay
GROUP BY d.itemId
GROUP BY d.itemId, d.uomId
),
day1 AS (
SELECT
d.itemId,
d.uomId,
SUM(COALESCE(d.closing, 0)) AS openingBalance
FROM stock_lot_day d
WHERE IFNULL(d.deleted, 0) = 0
AND d.date = :d1
AND d.itemId IS NOT NULL
$itemCodeSqlDay
GROUP BY d.itemId
GROUP BY d.itemId, d.uomId
),
inventory_per_item AS (
SELECT
iv.itemId,
iv.stockUomId AS uomId,
SUM(COALESCE(iv.onHandQty, 0)) AS currentBalance
FROM inventory iv
INNER JOIN items it ON it.id = iv.itemId AND it.deleted = 0
WHERE iv.deleted = 0
$itemCodeSql
$stockCategorySql
GROUP BY iv.itemId
GROUP BY iv.itemId, iv.stockUomId
),
ledger_moves AS (
SELECT
sl.itemId,
sl.uomId,
SUM(
CASE
WHEN UPPER(TRIM(COALESCE(sl.type, ''))) IN ('NOR', 'ADJ')
@@ -1644,14 +1651,14 @@ return result
AND sl.date >= :d0Start
AND sl.date < :d0EndExclusive
$itemCodeSqlSl
GROUP BY sl.itemId
GROUP BY sl.itemId, sl.uomId
),
item_ids AS (
SELECT itemId FROM day0
SELECT itemId, uomId FROM day0
UNION
SELECT itemId FROM day1
SELECT itemId, uomId FROM day1
UNION
SELECT itemId FROM ledger_moves
SELECT itemId, uomId FROM ledger_moves
$inventoryScopeUnion
),
item_scope AS (
@@ -1661,9 +1668,9 @@ return result
it.name AS itemName,
COALESCE(CAST(NULLIF(it.AverageUnitPrice, '') AS DECIMAL(14, 4)), 0) AS avgUnitPriceRaw
FROM items it
INNER JOIN item_ids ids ON ids.itemId = it.id
WHERE it.deleted = 0
AND it.code IS NOT NULL AND it.code <> ''
AND it.id IN (SELECT DISTINCT itemId FROM item_ids)
$itemCodeSql
$stockCategorySql
),
@@ -1680,6 +1687,7 @@ return result
qty_per_item AS (
SELECT
ids.itemId,
ids.uomId,
COALESCE(
CASE WHEN $liveFlag = 1 THEN d1.openingBalance ELSE d0.openingBalance END,
d1.openingBalance,
@@ -1700,10 +1708,10 @@ return result
COALESCE(lm.variance, 0) AS variance,
COALESCE(lm.defectiveGoods, 0) AS defectiveGoods
FROM item_ids ids
LEFT JOIN day0 d0 ON d0.itemId = ids.itemId
LEFT JOIN day1 d1 ON d1.itemId = ids.itemId
LEFT JOIN inventory_per_item invc ON invc.itemId = ids.itemId
LEFT JOIN ledger_moves lm ON lm.itemId = ids.itemId
LEFT JOIN day0 d0 ON d0.itemId = ids.itemId AND d0.uomId <=> ids.uomId
LEFT JOIN day1 d1 ON d1.itemId = ids.itemId AND d1.uomId <=> ids.uomId
LEFT JOIN inventory_per_item invc ON invc.itemId = ids.itemId AND invc.uomId <=> ids.uomId
LEFT JOIN ledger_moves lm ON lm.itemId = ids.itemId AND lm.uomId <=> ids.uomId
)
SELECT
'' AS stockSubCategory,
@@ -1732,12 +1740,46 @@ return result
CASE WHEN q.misInputAndLost < 0 THEN CONCAT('(', FORMAT(-q.misInputAndLost, 0), ')') ELSE FORMAT(q.misInputAndLost, 0) END AS totalMisInputAndLost,
CASE WHEN q.variance < 0 THEN CONCAT('(', FORMAT(-q.variance, 0), ')') ELSE FORMAT(q.variance, 0) END AS totalVariance,
CASE WHEN q.defectiveGoods < 0 THEN CONCAT('(', FORMAT(-q.defectiveGoods, 0), ')') ELSE FORMAT(q.defectiveGoods, 0) END AS totalDefectiveGoods,
FORMAT(ROUND(COALESCE(s.avgUnitPriceRaw, 0), 2), 2) AS avgUnitPrice,
FORMAT(ROUND(COALESCE(s.avgUnitPriceRaw, 0) * COALESCE(q.currentBalance, 0), 2), 2) AS totalStockBalance
FORMAT(ROUND(
CASE
WHEN iu.id IS NULL OR so.id IS NULL THEN COALESCE(s.avgUnitPriceRaw, 0)
WHEN iu.uomId <=> so.uomId THEN COALESCE(s.avgUnitPriceRaw, 0)
ELSE ROUND(
COALESCE(s.avgUnitPriceRaw, 0)
* (COALESCE(iu.ratioN, 1) / COALESCE(NULLIF(iu.ratioD, 0), 1))
* (COALESCE(so.ratioD, 1) / COALESCE(NULLIF(so.ratioN, 0), 1)),
4
)
END
, 2), 2) AS avgUnitPrice,
FORMAT(ROUND(
CASE
WHEN iu.id IS NULL OR so.id IS NULL THEN COALESCE(s.avgUnitPriceRaw, 0)
WHEN iu.uomId <=> so.uomId THEN COALESCE(s.avgUnitPriceRaw, 0)
ELSE ROUND(
COALESCE(s.avgUnitPriceRaw, 0)
* (COALESCE(iu.ratioN, 1) / COALESCE(NULLIF(iu.ratioD, 0), 1))
* (COALESCE(so.ratioD, 1) / COALESCE(NULLIF(so.ratioN, 0), 1)),
4
)
END
* COALESCE(q.currentBalance, 0)
, 2), 2) AS totalStockBalance
FROM qty_per_item q
INNER JOIN item_scope s ON s.itemId = q.itemId
LEFT JOIN stock_one so ON so.itemId = q.itemId
LEFT JOIN uom_conversion uc ON so.uomId = uc.id
LEFT JOIN item_uom iu
ON iu.itemId = q.itemId
AND iu.deleted = 0
AND iu.uomId <=> q.uomId
AND iu.id = (
SELECT MIN(iu2.id)
FROM item_uom iu2
WHERE iu2.itemId = q.itemId
AND iu2.deleted = 0
AND iu2.uomId <=> q.uomId
)
LEFT JOIN uom_conversion uc ON uc.id = COALESCE(iu.uomId, q.uomId)
WHERE 1=1
$storeLocationSql
""".trimIndent()
@@ -1765,7 +1807,7 @@ return result
}

val t0 = System.nanoTime()
val rows = jdbcDao.queryForList("$finalSql ORDER BY itemNo", args)
val rows = jdbcDao.queryForList("$finalSql ORDER BY itemNo, unitOfMeasure", args)
log.info(
"stock-balance-by-date date={} liveInventory={} itemCode={} rows={} {}ms",
stockLocalDate,
@@ -1780,9 +1822,7 @@ return result
* Compiles and fills a Jasper Report, then exports to Excel (.xlsx). Same layout/columns as the report template.
*/
fun createExcelResponse(templatePath: String, params: Map<String, Any>, dataList: List<Map<String, Any>>): ByteArray {
val stream = this::class.java.getResourceAsStream(templatePath)
?: throw RuntimeException("Report template not found: $templatePath")
val jasperReport = JasperCompileManager.compileReport(stream)
val jasperReport = compiledReport(templatePath)
val dataSource = JRMapCollectionDataSource(dataList)
val jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource)
val out = ByteArrayOutputStream()
@@ -1797,17 +1837,20 @@ return result
* Compiles and fills a Jasper Report, returning the PDF as a ByteArray.
*/
fun createPdfResponse(templatePath: String, params: Map<String, Any>, dataList: List<Map<String, Any>>): ByteArray {
val stream = this::class.java.getResourceAsStream(templatePath)
?: throw RuntimeException("Report template not found: $templatePath")
val jasperReport = JasperCompileManager.compileReport(stream)
val jasperReport = compiledReport(templatePath)
val dataSource = JRMapCollectionDataSource(dataList)
val jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource)
return JasperExportManager.exportReportToPdf(jasperPrint)
}

private fun compiledReport(templatePath: String): JasperReport {
return compiledReports.computeIfAbsent(templatePath) {
val stream = this::class.java.getResourceAsStream(templatePath)
?: throw RuntimeException("Report template not found: $templatePath")
stream.use { JasperCompileManager.compileReport(it) }
}
}

fun mergePdfBytes(parts: List<ByteArray>): ByteArray {
if (parts.isEmpty()) return ByteArray(0)
if (parts.size == 1) return parts[0]


+ 517
- 238
src/main/java/com/ffii/fpsms/modules/report/service/StockLedgerReportService.kt Просмотреть файл

@@ -1,7 +1,10 @@
package com.ffii.fpsms.modules.report.service

import com.ffii.core.support.JdbcDao
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@@ -9,7 +12,7 @@ import java.time.format.DateTimeFormatter
open class StockLedgerReportService(
private val jdbcDao: JdbcDao,
) {
private val log = LoggerFactory.getLogger(StockLedgerReportService::class.java)
private val dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd")

/**
@@ -18,11 +21,12 @@ open class StockLedgerReportService(
*
* - stockSubCategory = items.type
* - trnDate = stock_ledger.date
* - trnRefNo = stock_ledger.type
* - cumBalance = stock_ledger.balance(異動後纍計存量)
* - cumOpeningBal = balance - inQty + outQty(異動前纍計期初
* - trnRefNo = stock_ledger.type(ADJ 且關聯盤點單時改為盤點)
* - cumBalance = stock_ledger.balance(異動後貨品纍計存量)
* - cumOpeningBal = balance - inQty + outQty(異動前貨品纍計)
*
* 只查 [start, end] 期間列,不掃起日以前全歷史。
* 帳本先依日期區間撈窄欄,批號/單據再依這段出現的 id 分批補。
* 餘額用帳本上的貨品纍計,排序為貨品 → 時間 → id。
*/
fun searchStockLedgerReport(
stockCategory: String?,
@@ -31,264 +35,500 @@ open class StockLedgerReportService(
reportPeriodStart: String?,
reportPeriodEnd: String?,
): List<Map<String, Any>> {
val tAll = System.nanoTime()
val timings = mutableListOf<String>()
fun <T> timed(name: String, extra: (T) -> String, block: () -> T): T {
val t0 = System.nanoTime()
val result = block()
val line = "$name ${extra(result)} ${(System.nanoTime() - t0) / 1_000_000}ms"
timings.add(line)
log.info("stock-ledger {}", line)
return result
}

val args = mutableMapOf<String, Any>()

val reportPeriodEnd = (reportPeriodEnd?.replace("/", "-")
val endDate = (reportPeriodEnd?.replace("/", "-")
?: LocalDate.now().format(dateFmt))

val reportPeriodStart = if (!reportPeriodStart.isNullOrBlank()) {
val startDate = if (!reportPeriodStart.isNullOrBlank()) {
reportPeriodStart.replace("/", "-")
} else {
val minDateSql = """
val minDateRow = jdbcDao.queryForList(
"""
SELECT DATE_FORMAT(MIN(sl.date), '%Y-%m-%d') AS firstDate
FROM stock_ledger sl
WHERE sl.deleted = 0
AND sl.itemCode IS NOT NULL
AND sl.itemCode <> ''
""".trimIndent()

val minDateRow = jdbcDao.queryForList(minDateSql, emptyMap<String, Any>()).firstOrNull()
(minDateRow?.get("firstDate") as? String)
?: reportPeriodEnd
""".trimIndent(),
emptyMap<String, Any>(),
).firstOrNull()
(minDateRow?.get("firstDate") as? String) ?: endDate
}
val endExclusive = LocalDate.parse(endDate).plusDays(1).format(dateFmt)

val endExclusive = LocalDate.parse(reportPeriodEnd).plusDays(1).format(dateFmt)
args["reportPeriodStart"] = reportPeriodStart
args["reportPeriodEndExclusive"] = endExclusive

val stockCategorySql = buildMultiValueExactClause(
stockCategory,
"it.type",
"stockCategory",
args
)

val itemCodeSql = buildMultiValueLikeClause(
itemCode,
"sl.itemCode",
"itemCode",
args
val args = mutableMapOf<String, Any>(
"reportPeriodStart" to LocalDate.parse(startDate).atStartOfDay(),
"reportPeriodEndExclusive" to LocalDate.parse(endExclusive).atStartOfDay(),
)

// 用 lot 子查詢的 storeLocation,避免 ill_in 放大列數
val storeLocationSql = if (!storeLocation.isNullOrBlank()) {
args["storeLocation"] = "%$storeLocation%"
"AND lot.storeLocation LIKE :storeLocation"
val itemCodeSql = buildMultiValueLikeClause(itemCode, "sl.itemCode", "itemCode", args)
val categorySql = buildCategoryItemFilter(stockCategory, args)
val forceLedgerIndex = if (itemCode.isNullOrBlank()) {
"FORCE INDEX (idx_sl_deleted_date)"
} else {
""
}

val sql = """
SELECT
stockSubCategory,
itemNo,
itemName,
unitOfMeasure,
lotNo,
expiryDate,
trnDate,
CASE trnRefNoRaw
WHEN 'OPEN' THEN '開倉'
WHEN 'NOR' THEN '出入倉'
WHEN 'TRF' THEN '轉倉'
WHEN 'ADJ' THEN '調整'
WHEN 'MISS' THEN '遺失'
WHEN 'BAD' THEN '不良品'
WHEN 'TKE' THEN '盤點'
ELSE trnRefNoRaw
END AS trnRefNo,
storeLocation,
orderRefNo,
jobOrderNo,

openingBalance,
cumStockIn,
cumStockOut,
currentBalance,
lastInDate,
lastOutDate,
reOrderLevel,
reOrderQty,
val moves = timed("1.ledger", { rows: List<LedgerMove> -> "rows=${rows.size}" }) {
jdbcDao.queryForList(
"""
SELECT
sl.id AS slId,
DATE_FORMAT(sl.date, '%Y-%m-%d') AS trnDate,
TRIM(COALESCE(sl.type, '')) AS rawType,
sl.itemId AS itemId,
sl.itemCode AS itemCode,
COALESCE(sl.inQty, 0) AS inQty,
COALESCE(sl.outQty, 0) AS outQty,
COALESCE(sl.balance, 0) AS bal,
sl.inventoryLotLineId AS inventoryLotLineId,
sl.stockInLineId AS stockInLineId,
sl.stockOutLineId AS stockOutLineId
FROM stock_ledger sl $forceLedgerIndex
WHERE sl.deleted = 0
AND sl.itemCode IS NOT NULL
AND sl.itemCode <> ''
AND sl.date >= :reportPeriodStart
AND sl.date < :reportPeriodEndExclusive
$itemCodeSql
$categorySql
""".trimIndent(),
args,
).map { r ->
LedgerMove(
slId = toLong(r["slId"]),
trnDate = r["trnDate"]?.toString().orEmpty(),
rawType = r["rawType"]?.toString().orEmpty(),
itemId = toLong(r["itemId"]),
itemCode = r["itemCode"]?.toString().orEmpty(),
inQty = toDouble(r["inQty"]),
outQty = toDouble(r["outQty"]),
bal = toDouble(r["bal"]),
lotLineId = toLong(r["inventoryLotLineId"]),
stockInLineId = toLong(r["stockInLineId"]),
stockOutLineId = toLong(r["stockOutLineId"]),
)
}
}
if (moves.isEmpty()) {
log.info("stock-ledger DONE rows=0 {}", timings.joinToString(" | "))
return emptyList()
}

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,
val lineByIn = HashMap<Long, Pair<Long, Long>>()
val lineByOut = HashMap<Long, Long>()
timed("2.lot_keys", { _: Unit ->
"in=${lineByIn.size} out=${lineByOut.size}"
}) {
val missingIn = moves.filter { it.lotLineId <= 0 && it.stockInLineId > 0 }
.map { it.stockInLineId }
val missingOut = moves.filter { it.lotLineId <= 0 && it.stockOutLineId > 0 }
.map { it.stockOutLineId }
for (r in queryChunks(
missingIn,
"""
SELECT id, inventoryLotLineId, inventoryLotId
FROM stock_in_line
WHERE deleted = 0 AND id IN (:ids)
""".trimIndent(),
)) {
lineByIn[toLong(r["id"])] = toLong(r["inventoryLotLineId"]) to toLong(r["inventoryLotId"])
}
for (r in queryChunks(
missingOut,
"""
SELECT id, inventoryLotLineId
FROM stock_out_line
WHERE deleted = 0 AND id IN (:ids)
""".trimIndent(),
)) {
lineByOut[toLong(r["id"])] = toLong(r["inventoryLotLineId"])
}
}

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,
data class LotKey(val lineId: Long, val lotId: Long)
val lotKeyBySl = HashMap<Long, LotKey>(moves.size * 2)
for (m in moves) {
val fromIn = lineByIn[m.stockInLineId]
val inLine = fromIn?.first ?: 0L
val inLot = fromIn?.second ?: 0L
val outLine = lineByOut[m.stockOutLineId] ?: 0L
val lineId = when {
m.lotLineId > 0 -> m.lotLineId
inLine > 0 -> inLine
inLot > 0 -> 0L
else -> outLine
}
val lotId = if (lineId <= 0 && inLot > 0) inLot else 0L
if (lineId > 0 || lotId > 0) {
lotKeyBySl[m.slId] = LotKey(lineId, lotId)
}
}

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
val lotByLine = HashMap<Long, LotInfo>()
val lotByHeader = HashMap<Long, LotInfo>()
timed("3.lots", { _: Unit -> "lines=${lotByLine.size} headers=${lotByHeader.size}" }) {
for (r in queryChunks(
lotKeyBySl.values.map { it.lineId },
"""
SELECT
ill.id AS lineId,
COALESCE(il.lotNo, '') AS lotNo,
COALESCE(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
COALESCE(wh.code, '') AS storeLocation
FROM inventory_lot_line ill
INNER JOIN inventory_lot il
ON il.id = ill.inventoryLotId AND il.deleted = 0
LEFT JOIN warehouse wh
ON wh.id = ill.warehouseId AND wh.deleted = 0
WHERE ill.deleted = 0
AND ill.id IN (:ids)
""".trimIndent(),
)) {
lotByLine[toLong(r["lineId"])] = LotInfo(
lotNo = r["lotNo"]?.toString().orEmpty(),
expiryDate = r["expiryDate"]?.toString().orEmpty(),
storeLocation = r["storeLocation"]?.toString().orEmpty(),
)
}
val headerIds = lotKeyBySl.values
.filter { it.lineId <= 0 && it.lotId > 0 }
.map { it.lotId }
for (r in queryChunks(
headerIds,
"""
SELECT
il.id AS lotId,
COALESCE(il.lotNo, '') AS lotNo,
COALESCE(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
COALESCE(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 wh.id = ill.warehouseId AND wh.deleted = 0
WHERE il.deleted = 0
AND il.id IN (:ids)
GROUP BY il.id, il.lotNo, il.expiryDate
""".trimIndent(),
)) {
lotByHeader[toLong(r["lotId"])] = LotInfo(
lotNo = r["lotNo"]?.toString().orEmpty(),
expiryDate = r["expiryDate"]?.toString().orEmpty(),
storeLocation = r["storeLocation"]?.toString().orEmpty(),
)
}
}

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,
val locationFilter = storeLocation?.trim().orEmpty()
val withLot = ArrayList<MoveWithLot>(moves.size)
for (m in moves) {
val key = lotKeyBySl[m.slId] ?: continue
val lot = if (key.lineId > 0) lotByLine[key.lineId] else lotByHeader[key.lotId]
if (lot == null) continue
if (locationFilter.isNotEmpty() &&
!lot.storeLocation.contains(locationFilter, ignoreCase = true)
) {
continue
}
withLot.add(MoveWithLot(m, lot))
}
timings.add("4.filter rows=${withLot.size}")
if (withLot.isEmpty()) {
log.info("stock-ledger DONE rows=0 {}", timings.joinToString(" | "))
return emptyList()
}

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,
data class ItemInfo(
val type: String,
val code: String,
val name: String,
val uom: String,
)
val items = HashMap<Long, ItemInfo>()
timed("5.items", { _: Unit -> "items=${items.size}" }) {
for (r in queryChunks(
withLot.map { it.move.itemId },
"""
SELECT
it.id AS itemId,
COALESCE(it.type, '') AS stockSubCategory,
COALESCE(it.code, '') AS itemNo,
COALESCE(it.name, '') AS itemName,
COALESCE(uc.udfudesc, '') AS unitOfMeasure
FROM items it
LEFT JOIN (
SELECT itemId, MIN(id) AS id
FROM item_uom
WHERE deleted = 0 AND stockUnit = 1 AND itemId IN (:ids)
GROUP BY itemId
) one_uom ON one_uom.itemId = it.id
LEFT JOIN item_uom iu
ON iu.id = one_uom.id
LEFT JOIN uom_conversion uc
ON uc.id = iu.uomId
WHERE it.deleted = 0
AND it.id IN (:ids)
""".trimIndent(),
)) {
items[toLong(r["itemId"])] = ItemInfo(
type = r["stockSubCategory"]?.toString().orEmpty(),
code = r["itemNo"]?.toString().orEmpty(),
name = r["itemName"]?.toString().orEmpty(),
uom = r["unitOfMeasure"]?.toString().orEmpty(),
)
}
}

it.type AS stockSubCategory,
it.code AS itemNo,
it.name AS itemName,
uc.udfudesc AS unitOfMeasure,
data class InRef(val orderRefNo: String?, val jobOrderNo: String, val stockTake: Boolean)
data class OutRef(val orderRefNo: String?, val stockTake: Boolean)
val inRefs = HashMap<Long, InRef>()
val outRefs = HashMap<Long, OutRef>()
timed("6.refs", { _: Unit -> "in=${inRefs.size} out=${outRefs.size}" }) {
for (r in queryChunks(
withLot.map { it.move.stockOutLineId },
"""
SELECT
sol.id AS lineId,
COALESCE(TRIM(do.code), TRIM(jo_po.code)) AS orderRefNo,
CASE
WHEN so.stockTakeId IS NOT NULL OR so.type = 'stockTake' THEN 1
ELSE 0
END AS isStockTake
FROM stock_out_line sol
LEFT JOIN stock_out so
ON so.id = sol.stockOutId AND so.deleted = 0
LEFT JOIN pick_order_line pol
ON pol.id = sol.pickOrderLineId AND pol.deleted = 0
LEFT JOIN pick_order po_out
ON po_out.id = pol.poId AND po_out.deleted = 0
LEFT JOIN job_order jo_po
ON jo_po.id = po_out.joId AND jo_po.deleted = 0
LEFT JOIN delivery_order do
ON do.id = po_out.doId AND do.deleted = 0
WHERE sol.deleted = 0
AND sol.id IN (:ids)
""".trimIndent(),
)) {
outRefs[toLong(r["lineId"])] = OutRef(
orderRefNo = r["orderRefNo"]?.toString(),
stockTake = toLong(r["isStockTake"]) == 1L,
)
}
for (r in queryChunks(
withLot.map { it.move.stockInLineId },
"""
SELECT
sil.id AS lineId,
COALESCE(TRIM(jo.code), TRIM(po.code)) AS orderRefNo,
COALESCE(TRIM(jo.code), '') AS jobOrderNo,
CASE WHEN si.stockTakeId IS NOT NULL THEN 1 ELSE 0 END AS isStockTake
FROM stock_in_line sil
LEFT JOIN stock_in si
ON si.id = sil.stockInId AND si.deleted = 0
LEFT JOIN job_order jo
ON jo.id = sil.jobOrderId AND jo.deleted = 0
LEFT JOIN purchase_order po
ON po.id = sil.purchaseOrderId AND po.deleted = 0
WHERE sil.deleted = 0
AND sil.id IN (:ids)
""".trimIndent(),
)) {
inRefs[toLong(r["lineId"])] = InRef(
orderRefNo = r["orderRefNo"]?.toString(),
jobOrderNo = r["jobOrderNo"]?.toString().orEmpty(),
stockTake = toLong(r["isStockTake"]) == 1L,
)
}
}

lot.lotNo AS lotNo,
COALESCE(DATE_FORMAT(lot.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
lot.storeLocation AS storeLocation,
val totals = HashMap<String, ItemTotal>()
for (row in withLot) {
val m = row.move
val total = totals.getOrPut(m.itemCode) { ItemTotal() }
total.inSum += m.inQty
total.outSum += m.outQty
val opening = m.bal - m.inQty + m.outQty
if (total.isBeforeFirst(m.trnDate, m.slId)) {
total.firstDate = m.trnDate
total.firstId = m.slId
total.opening = opening
}
if (total.isAfterLast(m.trnDate, m.slId)) {
total.lastDate = m.trnDate
total.lastId = m.slId
total.closing = m.bal
}
}

COALESCE(TRIM(do.code), TRIM(jo_po.code), TRIM(jo.code), TRIM(po.code), '') AS orderRefNo,
COALESCE(TRIM(jo.code), '') AS jobOrderNo,
val rows = timed("7.assemble", { list: List<Map<String, Any>> -> "rows=${list.size}" }) {
val sorted = withLot.sortedWith(
compareBy<MoveWithLot> { items[it.move.itemId]?.code.orEmpty() }
.thenBy { it.move.trnDate }
.thenBy { it.move.slId }
.thenBy { it.lot.lotNo },
)
sorted.map { row ->
val m = row.move
val item = items[m.itemId]
val inRef = inRefs[m.stockInLineId]
val outRef = outRefs[m.stockOutLineId]
val norm = m.rawType.uppercase()
val refType = when {
norm == "TKE" -> "TKE"
norm == "ADJ" && (outRef?.stockTake == true || inRef?.stockTake == true) -> "TKE"
else -> m.rawType
}
val total = totals[m.itemCode]
reportRow(
stockSubCategory = item?.type.orEmpty(),
itemNo = item?.code.orEmpty(),
itemName = item?.name.orEmpty(),
unitOfMeasure = item?.uom.orEmpty(),
lotNo = row.lot.lotNo,
expiryDate = row.lot.expiryDate,
trnDate = m.trnDate,
trnRefNo = typeLabel(refType),
storeLocation = row.lot.storeLocation,
orderRefNo = outRef?.orderRefNo ?: inRef?.orderRefNo ?: "",
jobOrderNo = inRef?.jobOrderNo.orEmpty(),
inQty = m.inQty,
outQty = m.outQty,
opening = m.bal - m.inQty + m.outQty,
balance = m.bal,
totalOpening = total?.opening ?: 0.0,
totalIn = total?.inSum ?: 0.0,
totalOut = total?.outSum ?: 0.0,
totalBalance = total?.closing ?: 0.0,
)
}
}

'' 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
val totalMs = (System.nanoTime() - tAll) / 1_000_000
timings.add("search.total ${totalMs}ms")
log.info("stock-ledger DONE {}", timings.joinToString(" | "))
return rows
}

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
private fun reportRow(
stockSubCategory: String,
itemNo: String,
itemName: String,
unitOfMeasure: String,
lotNo: String,
expiryDate: String,
trnDate: String,
trnRefNo: String,
storeLocation: String,
orderRefNo: String,
jobOrderNo: String,
inQty: Double,
outQty: Double,
opening: Double,
balance: Double,
totalOpening: Double,
totalIn: Double,
totalOut: Double,
totalBalance: Double,
): Map<String, Any> {
val row = HashMap<String, Any>(32)
row["stockSubCategory"] = stockSubCategory
row["itemNo"] = itemNo
row["itemName"] = itemName
row["unitOfMeasure"] = unitOfMeasure
row["lotNo"] = lotNo
row["expiryDate"] = expiryDate
row["trnDate"] = trnDate
row["trnRefNo"] = trnRefNo
row["storeLocation"] = storeLocation
row["orderRefNo"] = orderRefNo
row["jobOrderNo"] = jobOrderNo
row["openingBalance"] = ""
row["cumStockIn"] = ""
row["cumStockOut"] = ""
row["currentBalance"] = ""
row["lastInDate"] = ""
row["lastOutDate"] = ""
row["reOrderLevel"] = ""
row["reOrderQty"] = ""
row["stockIn"] = formatQty(inQty)
row["stockOut"] = formatQty(outQty)
row["cumOpeningBal"] = formatQty(opening)
row["cumBalance"] = formatQty(balance)
row["totalCumOpeningBal"] = formatQty(totalOpening)
row["totalStockIn"] = formatQty(totalIn)
row["totalStockOut"] = formatQty(totalOut)
row["totalCumBalance"] = formatQty(totalBalance)
return row
}

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)
private fun typeLabel(raw: String): String = when (raw.uppercase()) {
"OPEN" -> "開倉"
"NOR" -> "出入倉"
"TRF" -> "轉倉"
"ADJ" -> "調整"
"MISS" -> "遺失"
"BAD" -> "不良品"
"TKE" -> "盤點"
else -> raw
}

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
/** Same shape as MySQL FORMAT(n, 0), negatives in parentheses. */
private fun formatQty(value: Double): String {
val rounded = BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP)
val negative = rounded.signum() < 0
val digits = rounded.abs().toPlainString()
val grouped = digits.reversed().chunked(3).joinToString(",").reversed()
return if (negative) "($grouped)" else grouped
}

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
itemNo,
trnDateRaw,
slId,
lotNo
""".trimIndent()
private fun buildCategoryItemFilter(
stockCategory: String?,
args: MutableMap<String, Any>,
): String {
if (stockCategory.isNullOrBlank()) return ""
val values = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() }
if (values.isEmpty()) return ""
val ors = values.mapIndexed { index, value ->
val name = "stockCategory_$index"
args[name] = value
"it.type = :$name"
}
return """
AND sl.itemId IN (
SELECT it.id FROM items it
WHERE it.deleted = 0 AND (${ors.joinToString(" OR ")})
)
""".trimIndent()
}

return jdbcDao.queryForList(sql, args)
private fun queryChunks(ids: Collection<Long>, sql: String): List<Map<String, Any>> {
val distinct = ids.filter { it > 0 }.distinct()
if (distinct.isEmpty()) return emptyList()
val out = ArrayList<Map<String, Any>>()
for (chunk in distinct.chunked(800)) {
out.addAll(jdbcDao.queryForList(sql, mapOf("ids" to chunk)))
}
return out
}

/** LIKE 多值工具方法 */
private fun buildMultiValueLikeClause(
paramValue: String?,
columnName: String,
paramPrefix: String,
args: MutableMap<String, Any>
args: MutableMap<String, Any>,
): String {
if (paramValue.isNullOrBlank()) return ""
val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
if (values.isEmpty()) return ""

val conditions = values.mapIndexed { index, value ->
val paramName = "${paramPrefix}_$index"
args[paramName] = "%$value%"
@@ -297,22 +537,61 @@ ORDER BY
return "AND (${conditions.joinToString(" OR ")})"
}

/** = 多值工具方法 */
private fun buildMultiValueExactClause(
paramValue: String?,
columnName: String,
paramPrefix: String,
args: MutableMap<String, Any>
): String {
if (paramValue.isNullOrBlank()) return ""
val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
if (values.isEmpty()) return ""
private fun toDouble(v: Any?): Double {
if (v == null) return 0.0
if (v is Number) return v.toDouble()
return v.toString().replace(",", "").toDoubleOrNull() ?: 0.0
}

val conditions = values.mapIndexed { index, value ->
val paramName = "${paramPrefix}_$index"
args[paramName] = value
"$columnName = :$paramName"
private fun toLong(v: Any?): Long {
if (v == null) return 0L
if (v is Number) return v.toLong()
return v.toString().toLongOrNull() ?: 0L
}

private data class LedgerMove(
val slId: Long,
val trnDate: String,
val rawType: String,
val itemId: Long,
val itemCode: String,
val inQty: Double,
val outQty: Double,
val bal: Double,
val lotLineId: Long,
val stockInLineId: Long,
val stockOutLineId: Long,
)

private data class MoveWithLot(
val move: LedgerMove,
val lot: LotInfo,
)

private data class LotInfo(
val lotNo: String,
val expiryDate: String,
val storeLocation: String,
)

private class ItemTotal {
var firstDate: String? = null
var firstId: Long = 0L
var lastDate: String? = null
var lastId: Long = 0L
var opening: Double = 0.0
var closing: Double = 0.0
var inSum: Double = 0.0
var outSum: Double = 0.0

fun isBeforeFirst(date: String, id: Long): Boolean {
val current = firstDate ?: return true
return date < current || (date == current && id < firstId)
}

fun isAfterLast(date: String, id: Long): Boolean {
val current = lastDate ?: return true
return date > current || (date == current && id > lastId)
}
return "AND (${conditions.joinToString(" OR ")})"
}
}

+ 2
- 2
src/main/java/com/ffii/fpsms/modules/report/web/FGStockOutTraceabilityReportController.kt Просмотреть файл

@@ -159,7 +159,7 @@ class FGStockOutTraceabilityReportController(
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
}

val dashStyle = workbook.createCellStyle().apply {
@@ -216,7 +216,7 @@ class FGStockOutTraceabilityReportController(
alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
borderTop = BorderStyle.THICK
borderBottom = BorderStyle.THICK
borderLeft = BorderStyle.THIN


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt Просмотреть файл

@@ -271,7 +271,7 @@ class ItemQcFailReportController(
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val df = workbook.creationHelper.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
}

val decimalNumberStyle = workbook.createCellStyle().apply {
@@ -282,7 +282,7 @@ class ItemQcFailReportController(
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val df = workbook.creationHelper.createDataFormat()
dataFormat = df.getFormat("#,##0.##")
dataFormat = df.getFormat("#,##0.00")
}

val headers = listOf(


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/report/web/MaterialStockOutTraceabilityReportController.kt Просмотреть файл

@@ -184,7 +184,7 @@ class MaterialStockOutTraceabilityReportController(
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
}

val dashStyle = workbook.createCellStyle().apply {
@@ -222,7 +222,7 @@ class MaterialStockOutTraceabilityReportController(
alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THICK
borderLeft = BorderStyle.THIN


+ 9
- 9
src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt Просмотреть файл

@@ -482,7 +482,7 @@ class ReportController(
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
}
val dashStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
alignment = HorizontalAlignment.RIGHT
@@ -874,7 +874,7 @@ class ReportController(
setTextCell(r, 4, m["dnNo"], styles.text)
setTextCell(r, 5, m["deliveryOrderNo"], styles.text)
setTextCell(r, 6, m["customerName"], styles.text)
setNumberCellFromFormatted(r, 7, m["qty"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 7, m["qty"], styles.int, styles.dash, preferInt = false)
setTextCell(r, 8, m["truckNo"], styles.center)

itemTotalQty += parseSignedNumber(m["qty"]) ?: 0.0
@@ -956,13 +956,13 @@ class ReportController(
setTextCell(r, 0, m["itemNo"], styles.text)
setTextCell(r, 1, m["itemName"], styles.text)
setTextCell(r, 2, m["unitOfMeasure"], styles.center)
setNumberCellFromFormatted(r, 3, m["totalOpeningBalance"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 4, m["totalCumStockIn"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 5, m["totalCumStockOut"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 6, m["totalMisInputAndLost"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 7, m["totalVariance"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 8, m["totalDefectiveGoods"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 9, m["totalCurrentBalance"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 3, m["totalOpeningBalance"], styles.number, styles.dash, preferInt = false)
setNumberCellFromFormatted(r, 4, m["totalCumStockIn"], styles.number, styles.dash, preferInt = false)
setNumberCellFromFormatted(r, 5, m["totalCumStockOut"], styles.number, styles.dash, preferInt = false)
setNumberCellFromFormatted(r, 6, m["totalMisInputAndLost"], styles.number, styles.dash, preferInt = false)
setNumberCellFromFormatted(r, 7, m["totalVariance"], styles.number, styles.dash, preferInt = false)
setNumberCellFromFormatted(r, 8, m["totalDefectiveGoods"], styles.number, styles.dash, preferInt = false)
setNumberCellFromFormatted(r, 9, m["totalCurrentBalance"], styles.number, styles.dash, preferInt = false)
setNumberCellFromFormatted(r, 10, m["avgUnitPrice"], styles.number, styles.dash, preferInt = false, emptyAsBlank = true)
setNumberCellFromFormatted(r, 11, m["totalStockBalance"], styles.number, styles.dash, preferInt = false, emptyAsBlank = true)
}


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt Просмотреть файл

@@ -174,7 +174,7 @@ class SemiFGProductionAnalysisReportController(
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
dataFormat = workbook.creationHelper.createDataFormat().getFormat("#,##0")
dataFormat = workbook.creationHelper.createDataFormat().getFormat("#,##0.00")
}

val dashStyle = workbook.createCellStyle().apply {


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt Просмотреть файл

@@ -191,7 +191,7 @@ class StockItemConsumptionTrendReportController(
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
dataFormat = workbook.creationHelper.createDataFormat().getFormat("#,##0")
dataFormat = workbook.creationHelper.createDataFormat().getFormat("#,##0.00")
}

val dashStyle = workbook.createCellStyle().apply {


+ 25
- 5
src/main/java/com/ffii/fpsms/modules/report/web/StockLedgerReportController.kt Просмотреть файл

@@ -1,4 +1,5 @@
package com.ffii.fpsms.modules.report.web
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.*
import org.springframework.http.*
import java.time.LocalDate
@@ -26,6 +27,7 @@ class StockLedgerReportController(
private val reportService: ReportService,
private val stockLedgerReportService: StockLedgerReportService,
) {
private val log = LoggerFactory.getLogger(StockLedgerReportController::class.java)
private data class ExcelStyles(
val title: CellStyle,
val subtitle: CellStyle,
@@ -64,6 +66,7 @@ class StockLedgerReportController(
parameters["reportPeriodStart"] = reportPeriodStart ?: ""
parameters["reportPeriodEnd"] = reportPeriodEnd ?: ""

val tSearch = System.nanoTime()
val dbData = stockLedgerReportService.searchStockLedgerReport(
stockCategory = stockCategory,
itemCode = itemCode,
@@ -71,12 +74,20 @@ class StockLedgerReportController(
reportPeriodStart = reportPeriodStart,
reportPeriodEnd = reportPeriodEnd,
)

val searchMs = (System.nanoTime() - tSearch) / 1_000_000
val tPdf = System.nanoTime()
val pdfBytes = reportService.createPdfResponse(
"/jasper/StockLedgarReport.jrxml",
parameters,
dbData,
)
log.info(
"stock-ledger pdf rows={} search={}ms pdf={}ms bytes={}",
dbData.size,
searchMs,
(System.nanoTime() - tPdf) / 1_000_000,
pdfBytes.size,
)

val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_PDF
@@ -95,6 +106,7 @@ class StockLedgerReportController(
@RequestParam(name = "lastInDateStart", required = false) reportPeriodStart: String?,
@RequestParam(name = "lastInDateEnd", required = false) reportPeriodEnd: String?,
): ResponseEntity<ByteArray> {
val tSearch = System.nanoTime()
val dbData = stockLedgerReportService.searchStockLedgerReport(
stockCategory = stockCategory,
itemCode = itemCode,
@@ -102,12 +114,20 @@ class StockLedgerReportController(
reportPeriodStart = reportPeriodStart,
reportPeriodEnd = reportPeriodEnd,
)

val searchMs = (System.nanoTime() - tSearch) / 1_000_000
val tExcel = System.nanoTime()
val excelBytes = createStockLedgerExcel(
dbData = dbData,
reportPeriodStart = reportPeriodStart ?: "",
reportPeriodEnd = reportPeriodEnd ?: "",
)
log.info(
"stock-ledger excel rows={} search={}ms excel={}ms bytes={}",
dbData.size,
searchMs,
(System.nanoTime() - tExcel) / 1_000_000,
excelBytes.size,
)

val headers = HttpHeaders().apply {
contentType = MediaType.parseMediaType(
@@ -174,7 +194,7 @@ class StockLedgerReportController(
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
}
val dashStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT
@@ -188,7 +208,7 @@ class StockLedgerReportController(
alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
borderTop = BorderStyle.THICK
borderBottom = BorderStyle.THICK
borderLeft = BorderStyle.THIN
@@ -270,7 +290,7 @@ class StockLedgerReportController(
return
}
if (parsed < 0) {
val s = "%,d".format(kotlin.math.abs(parsed).toLong())
val s = "%,.2f".format(kotlin.math.abs(parsed))
cell.setCellValue("($s)")
cell.cellStyle = dashStyle
} else {


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/report/web/StockLotBalanceReportController.kt Просмотреть файл

@@ -151,7 +151,7 @@ class StockLotBalanceReportController(
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0.##")
dataFormat = df.getFormat("#,##0.00")
}
val dashStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/report/web/StockLotLedgerReportController.kt Просмотреть файл

@@ -145,7 +145,7 @@ class StockLotLedgerReportController(
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0.##")
dataFormat = df.getFormat("#,##0.00")
}
val dashStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT


+ 3
- 4
src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt Просмотреть файл

@@ -328,7 +328,7 @@ class StockTakeVarianceReportController(
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
}

val dashStyle = workbook.createCellStyle().apply {
@@ -344,7 +344,7 @@ class StockTakeVarianceReportController(
alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER
val df: DataFormat = workbook.createDataFormat()
dataFormat = df.getFormat("#,##0")
dataFormat = df.getFormat("#,##0.00")
borderTop = BorderStyle.THICK
borderBottom = BorderStyle.THICK
borderLeft = BorderStyle.THIN
@@ -578,8 +578,7 @@ class StockTakeVarianceReportController(
}

private fun formatAbsInt(v: Double): String {
val abs = kotlin.math.abs(v).toLong()
return "%,d".format(abs)
return "%,.2f".format(kotlin.math.abs(v))
}

private data class Rep012ReportData(


+ 1
- 1
src/main/resources/jasper/StockBalanceReport.jrxml Просмотреть файл

@@ -71,7 +71,7 @@
<field name="defectiveGoods" class="java.lang.String"/>
<field name="variance" class="java.lang.String"/>
<group name="Group1" keepTogether="true">
<groupExpression><![CDATA[$F{itemNo}]]></groupExpression>
<groupExpression><![CDATA[$F{itemNo} + "|" + $F{unitOfMeasure}]]></groupExpression>
<groupHeader>
<band height="24">
<textField>


+ 1
- 1
src/main/resources/jasper/StockLedgarReport.jrxml Просмотреть файл

@@ -73,7 +73,7 @@
<field name="cumBalance" class="java.lang.String"/>
<field name="jobOrderNo" class="java.lang.String"/>
<field name="orderRefNo" class="java.lang.String"/>
<group name="Group1" keepTogether="true">
<group name="Group1" keepTogether="false">
<groupExpression><![CDATA[$F{itemNo}]]></groupExpression>
<groupHeader>
<band height="18">


Загрузка…
Отмена
Сохранить