Kaynağa Gözat

stocktake report update

production
tommy 4 gün önce
ebeveyn
işleme
ac238e7ba3
2 değiştirilmiş dosya ile 171 ekleme ve 13 silme
  1. +155
    -9
      src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt
  2. +16
    -4
      src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt

+ 155
- 9
src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt Dosyayı Görüntüle

@@ -9,6 +9,95 @@ open class StockTakeVarianceReportService(
private val jdbcDao: JdbcDao,
) {

companion object {
private const val ORIGIN_STOCK_IN_JOIN_SQL = """
LEFT JOIN stock_in_line origin_sil
ON origin_sil.id = il.stockInLineId AND origin_sil.deleted = 0
LEFT JOIN purchase_order po
ON po.id = origin_sil.purchaseOrderId AND po.deleted = 0
"""

private const val LOT_ROOT_ORIGIN_CTE_SQL = """
lot_trace AS (
SELECT
il.id AS lotId,
il.stockInLineId AS silId,
0 AS depth
FROM inventory_lot il
WHERE il.deleted = 0
AND il.stockInLineId IS NOT NULL
UNION ALL
SELECT
lt.lotId AS lotId,
il_src.stockInLineId AS silId,
lt.depth + 1 AS depth
FROM lot_trace lt
INNER JOIN stock_in_line sil
ON sil.id = lt.silId
AND sil.deleted = 0
INNER JOIN stock_transfer_record tr
ON tr.id = sil.stockTransferId
AND tr.deleted = 0
INNER JOIN stock_out_line sol
ON sol.id = tr.stockOutLineId
AND sol.deleted = 0
INNER JOIN inventory_lot_line ill_src
ON ill_src.id = sol.inventoryLotLineId
AND ill_src.deleted = 0
INNER JOIN inventory_lot il_src
ON il_src.id = ill_src.inventoryLotId
AND il_src.deleted = 0
WHERE lt.depth < 8
AND il_src.stockInLineId IS NOT NULL
AND (
UPPER(TRIM(COALESCE(sil.type, ''))) = 'TRF'
OR sil.stockTransferId IS NOT NULL
)
),
lot_root_origin AS (
SELECT
lotId,
silId AS rootSilId
FROM (
SELECT
lotId,
silId,
ROW_NUMBER() OVER (PARTITION BY lotId ORDER BY depth DESC) AS rn
FROM lot_trace
) t
WHERE t.rn = 1
)"""

private const val ROOT_STOCK_IN_JOIN_SQL = """
LEFT JOIN lot_root_origin lro
ON lro.lotId = il.id
LEFT JOIN stock_in_line root_sil
ON root_sil.id = lro.rootSilId AND root_sil.deleted = 0
LEFT JOIN purchase_order root_po
ON root_po.id = root_sil.purchaseOrderId AND root_po.deleted = 0
"""

private const val LOT_ROOT_CATEGORY_SQL = """
CASE
WHEN root_sil.jobOrderId IS NOT NULL THEN '工廠生產'
WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN' THEN '期初存貨'
WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ' THEN '倉存調整'
WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP' THEN 'PP'
WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF' THEN 'PF'
WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'TOA' THEN 'TOA'
WHEN root_sil.purchaseOrderId IS NOT NULL THEN '採購入倉'
ELSE '其他入倉'
END"""

private const val LOT_TYPE_DISPLAY_SQL = """
CASE
WHEN UPPER(TRIM(COALESCE(origin_sil.type, ''))) = 'TRF'
OR origin_sil.stockTransferId IS NOT NULL
THEN CONCAT('轉倉(原:', $LOT_ROOT_CATEGORY_SQL, ')')
ELSE $LOT_ROOT_CATEGORY_SQL
END"""
}

/**
* Stock Take Variance 報表查詢
*
@@ -40,6 +129,7 @@ open class StockTakeVarianceReportService(
storeLocation: String?,
stockTakeDateStart: String?,
stockTakeDateEnd: String?,
type: String? = null,
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
@@ -62,6 +152,7 @@ open class StockTakeVarianceReportService(
} else {
""
}
val lotTypeFilterSql = buildLotTypeFilterClause(type)
// 1) toDate:有填用傳入,沒填用今天
val toDate = (stockTakeDateEnd?.replace("/", "-")
@@ -87,7 +178,9 @@ open class StockTakeVarianceReportService(
args["toDate"] = toDate
val sql = """
WITH latest_str AS (
WITH RECURSIVE
$LOT_ROOT_ORIGIN_CTE_SQL,
latest_str AS (
SELECT
s1.lotId,
s1.warehouseId,
@@ -207,7 +300,8 @@ data AS (
ls.bookQty AS stkBookQty,
ls.approverStockTakeQty AS stkApproverQty,
ls.varianceQty AS stkVarianceQty,
ls.date AS stockTakeDateRaw
ls.date AS stockTakeDateRaw,
$LOT_TYPE_DISPLAY_SQL AS lotTypeRaw
FROM inventory_lot_line ill
INNER JOIN inventory_lot il
ON ill.inventoryLotId = il.id
@@ -218,6 +312,8 @@ data AS (
INNER JOIN warehouse wh
ON ill.warehouseId = wh.id
AND wh.deleted = 0
$ORIGIN_STOCK_IN_JOIN_SQL
$ROOT_STOCK_IN_JOIN_SQL

LEFT JOIN item_uom iu
ON it.id = iu.itemId
@@ -237,6 +333,7 @@ data AS (
$stockCategorySql
$itemCodeSql
$storeLocationSql
$lotTypeFilterSql
)

SELECT
@@ -284,7 +381,8 @@ SELECT
CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn,
CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockOut,
CASE WHEN SUM(COALESCE(currentQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(currentQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(currentQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCurrentBalance,
CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty
CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty,
lotTypeRaw AS type

FROM data
ORDER BY
@@ -307,6 +405,7 @@ ORDER BY
itemCode: String?,
storeId: String?,
status: String?,
type: String? = null,
): List<Map<String, Any>> {
val countSql = """
SELECT COUNT(*) AS c FROM stocktakerecord s
@@ -356,9 +455,12 @@ ORDER BY
"itemCode",
args
)
val lotTypeFilterSql = buildLotTypeFilterClause(type)

val sql = """
WITH rb AS (
WITH RECURSIVE
$LOT_ROOT_ORIGIN_CTE_SQL,
rb AS (
SELECT
COALESCE(MIN(s.date), CURRENT_DATE) AS fromDate,
COALESCE(MAX(s.date), CURRENT_DATE) AS toDate
@@ -476,7 +578,8 @@ data AS (
ls.varianceQty AS stkVarianceQty,
ls.strDate AS stockTakeDateRaw,
ls.approverTime AS approvalDateTimeRaw,
ls.stockTakeRecordStatus AS stockTakeRecordStatus
ls.stockTakeRecordStatus AS stockTakeRecordStatus,
$LOT_TYPE_DISPLAY_SQL AS lotTypeRaw
FROM latest_str ls
INNER JOIN inventory_lot il
ON ls.lotId = il.id
@@ -491,6 +594,8 @@ data AS (
INNER JOIN warehouse wh
ON wh.id = ls.warehouseId
AND wh.deleted = 0
$ORIGIN_STOCK_IN_JOIN_SQL
$ROOT_STOCK_IN_JOIN_SQL

LEFT JOIN item_uom iu
ON it.id = iu.itemId
@@ -505,6 +610,7 @@ data AS (
WHERE 1=1
$itemCodeSql
$storeIdSql
$lotTypeFilterSql
)

SELECT
@@ -553,7 +659,8 @@ SELECT
CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn,
CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockOut,
CASE WHEN SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCurrentBalance,
CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty
CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty,
lotTypeRaw AS type

FROM data
ORDER BY
@@ -574,6 +681,7 @@ ORDER BY
itemCode: String?,
storeId: String?,
limitedToRoundIds: List<Long>? = null,
type: String? = null,
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()

@@ -605,9 +713,12 @@ ORDER BY
"itemCode",
args
)
val lotTypeFilterSql = buildLotTypeFilterClause(type)

val sql = """
WITH round_bounds AS (
WITH RECURSIVE
$LOT_ROOT_ORIGIN_CTE_SQL,
round_bounds AS (
SELECT
s.stockTakeRoundId,
COALESCE(MIN(s.date), CURRENT_DATE) AS fromDate,
@@ -766,12 +877,15 @@ data AS (
lb.varianceQty AS stkVarianceQty,
lb.strDate AS stockTakeDateRaw,
lb.approverTime AS approvalDateTimeRaw,
lb.stockTakeRecordStatus AS stockTakeRecordStatus
lb.stockTakeRecordStatus AS stockTakeRecordStatus,
$LOT_TYPE_DISPLAY_SQL AS lotTypeRaw
FROM line_bounds lb
INNER JOIN inventory_lot_line ill ON ill.id = lb.inventoryLotLineId
INNER JOIN inventory_lot il ON ill.inventoryLotId = il.id AND il.deleted = 0
INNER JOIN items it ON il.itemId = it.id AND it.deleted = 0
INNER JOIN warehouse wh ON wh.id = lb.warehouseId AND wh.deleted = 0
$ORIGIN_STOCK_IN_JOIN_SQL
$ROOT_STOCK_IN_JOIN_SQL

LEFT JOIN item_uom iu
ON it.id = iu.itemId
@@ -786,6 +900,7 @@ data AS (
WHERE 1=1
$itemCodeSql
$storeIdSql
$lotTypeFilterSql
)

SELECT
@@ -834,7 +949,8 @@ SELECT
CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn,
CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockOut,
CASE WHEN SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCurrentBalance,
CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty
CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty,
lotTypeRaw AS type

FROM data
ORDER BY
@@ -883,6 +999,36 @@ ORDER BY
return if (!cap.isNullOrBlank()) cap else "盤點輪次$stockTakeRoundId"
}

/**
* 類型篩選:依「轉倉追溯後」之根來源類型過濾。
*
* 需與 [ORIGIN_STOCK_IN_JOIN_SQL] + [ROOT_STOCK_IN_JOIN_SQL] 一併使用:
* - 顯示用 origin_sil(判斷是否轉倉)
* - 篩選用 root_sil / root_po(根來源類型)
*/
private fun buildLotTypeFilterClause(type: String?): String {
val normalized = type?.trim().orEmpty()
if (normalized.isBlank() || normalized.equals("all", ignoreCase = true)) return ""
return when (normalized) {
"PP" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP'"
"PF" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF'"
"TOA" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'TOA'"
"工廠生產" -> "AND root_sil.jobOrderId IS NOT NULL"
"倉存調整" -> "AND UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ'"
"期初存貨" -> "AND UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN'"
"採購入倉" -> """
AND root_sil.purchaseOrderId IS NOT NULL
AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) NOT IN ('PP', 'PF', 'TOA')
""".trimIndent()
"其他入倉" -> """
AND (root_sil.purchaseOrderId IS NULL OR root_sil.purchaseOrderId = 0)
AND root_sil.jobOrderId IS NULL
AND UPPER(TRIM(COALESCE(root_sil.type, ''))) NOT IN ('OPEN', 'ADJ')
""".trimIndent()
else -> ""
}
}

/** LIKE 多值工具方法 */
private fun buildMultiValueLikeClause(
paramValue: String?,


+ 16
- 4
src/main/java/com/ffii/fpsms/modules/report/web/StockTakeVarianceReportController.kt Dosyayı Görüntüle

@@ -49,6 +49,7 @@ class StockTakeVarianceReportController(
@RequestParam(required = false) storeLocation: String?,
@RequestParam(required = false) stockTakeDateStart: String?,
@RequestParam(required = false) stockTakeDateEnd: String?,
@RequestParam(required = false) type: String?,
): ResponseEntity<ByteArray> {
val parameters = mutableMapOf<String, Any>()

@@ -76,6 +77,7 @@ class StockTakeVarianceReportController(
storeLocation = storeLocation,
stockTakeDateStart = stockTakeDateStart,
stockTakeDateEnd = stockTakeDateEnd,
type = type,
)
val stockTakeDateDisplay = dbData
.mapNotNull { it["stockTakeDate"] as? String }
@@ -108,6 +110,7 @@ class StockTakeVarianceReportController(
@RequestParam(required = false) storeLocation: String?,
@RequestParam(required = false) stockTakeDateStart: String?,
@RequestParam(required = false) stockTakeDateEnd: String?,
@RequestParam(required = false) type: String?,
): ResponseEntity<ByteArray> {
val dbData = stockTakeVarianceReportService.searchStockTakeVarianceReport(
stockCategory = stockCategory,
@@ -115,6 +118,7 @@ class StockTakeVarianceReportController(
storeLocation = storeLocation,
stockTakeDateStart = stockTakeDateStart,
stockTakeDateEnd = stockTakeDateEnd,
type = type,
)

val excelBytes = createStockTakeVarianceExcel(
@@ -146,8 +150,9 @@ class StockTakeVarianceReportController(
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false, name = "store_id") storeId: String?,
@RequestParam(required = false) status: String?,
@RequestParam(required = false) type: String?,
): ResponseEntity<ByteArray> {
val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status)
val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status, type)

val parameters = mutableMapOf<String, Any>()

@@ -200,8 +205,9 @@ class StockTakeVarianceReportController(
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false, name = "store_id") storeId: String?,
@RequestParam(required = false) status: String?,
@RequestParam(required = false) type: String?,
): ResponseEntity<ByteArray> {
val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status)
val rep012 = resolveRep012ReportData(overall, stockTakeRoundId, itemCode, storeId, status, type)
val cap = rep012.caption
val dbData = rep012.dbData

@@ -257,6 +263,7 @@ class StockTakeVarianceReportController(
"盤點數",
"盤盈虧",
"盤盈虧百分比",
"類型",
"審核時間",
)
val totalColumns = headers.size
@@ -501,7 +508,8 @@ class StockTakeVarianceReportController(
setNumberCellFromFormatted(r, 7, rowMap["stockTakeQty"], numberStyle, dashStyle)
setNumberCellFromFormatted(r, 8, rowMap["variance"], numberStyle, dashStyle)
setTextCell(r, 9, rowMap["variancePercentage"], dashStyle)
setTextCell(r, 10, rowMap["stockTakeDate"], centerStyle)
setTextCell(r, 10, rowMap["type"], centerStyle)
setTextCell(r, 11, rowMap["stockTakeDate"], centerStyle)

currentItemNo = itemNo
currentItemName = itemName
@@ -518,7 +526,7 @@ class StockTakeVarianceReportController(
sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0))
}

val widths = intArrayOf(14, 26, 10, 18, 14, 14, 14, 12, 12, 14, 22)
val widths = intArrayOf(14, 26, 10, 18, 14, 14, 14, 12, 12, 14, 18, 22)
widths.forEachIndexed { idx, w -> sheet.setColumnWidth(idx, w * 256) }

val output = ByteArrayOutputStream()
@@ -597,6 +605,7 @@ class StockTakeVarianceReportController(
itemCode: String?,
storeId: String?,
status: String?,
type: String?,
): Rep012ReportData {
val roundIds = parseStockTakeRoundIds(stockTakeRoundId)
val useLegacyOverall = overall == true && roundIds.isEmpty()
@@ -611,6 +620,7 @@ class StockTakeVarianceReportController(
dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2Overall(
itemCode = itemCode,
storeId = storeId,
type = type,
),
)
}
@@ -625,6 +635,7 @@ class StockTakeVarianceReportController(
itemCode = itemCode,
storeId = storeId,
status = status,
type = type,
),
)
}
@@ -636,6 +647,7 @@ class StockTakeVarianceReportController(
itemCode = itemCode,
storeId = storeId,
limitedToRoundIds = roundIds,
type = type,
),
)
}


Yükleniyor…
İptal
Kaydet