Przeglądaj źródła

Stock balance report: download time improved

fix負數倉
Harry Groves 4 dni temu
rodzic
commit
97ee03e82e
1 zmienionych plików z 165 dodań i 116 usunięć
  1. +165
    -116
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt

+ 165
- 116
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Wyświetl plik

@@ -12,9 +12,11 @@ import com.ffii.fpsms.modules.master.enums.ShopType
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService
import java.math.BigDecimal
import java.time.LocalDate
import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter
import net.sf.jasperreports.export.SimpleExporterInput
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput
import org.slf4j.LoggerFactory

@Service
open class ReportService(
@@ -23,6 +25,7 @@ open class ReportService(
private val shopRepository: ShopRepository,
private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
) {
private val log = LoggerFactory.getLogger(javaClass)
/**
* Queries the database for inventory data based on dates and optional item type.
*/
@@ -1495,13 +1498,16 @@ return result
}

/**
* Stock Balance Report (date-driven).
* FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.1 | 2026-09-16
* Stock Balance Report (date-driven). One summarized row per item.
*
* - 期初存量: stockDate - 1 的最後一筆 stock_ledger.balance (以 sl.date, sl.id 排序)
* - 現存存貨: stockDate 的最後一筆 stock_ledger.balance (以 sl.date, sl.id 排序)
* - 單位均價: 依 item 判斷是否 BOM
* - BOM: 用 delivery_order_line (prefer up, else price/qty, else price) 算加權平均
* - 非 BOM: 用 purchase_order_line (prefer up, else price/qty, else price) 算加權平均
* Qty comes from [stock_lot_day] (daily lot close) instead of scanning all stock_ledger:
* - 期初存量: historical = that day's opening; today = yesterday's closing
* - 現存存貨: historical = that day's closing; today = inventory.onHandQty
* (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
* - 庫存總價值: 單位均價 * 現存存貨
*/
fun searchStockBalanceReportByDate(
@@ -1514,101 +1520,80 @@ return result
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val formattedStockDate = stockDate.replace("/", "-")
args["stockDate"] = formattedStockDate
val stockLocalDate = LocalDate.parse(formattedStockDate)
val useLiveInventory = !stockLocalDate.isBefore(LocalDate.now())
val liveFlag = if (useLiveInventory) "1" else "0"
args["d0"] = stockLocalDate
args["d1"] = stockLocalDate.minusDays(1)
args["d0Start"] = stockLocalDate.atStartOfDay()
args["d0EndExclusive"] = stockLocalDate.plusDays(1).atStartOfDay()

val stockCategorySql = buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args)
val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
val itemCodeSqlDay = buildMultiValueLikeClause(itemCode, "d.itemCode", "itemCodeDay", args)
val itemCodeSqlSl = buildMultiValueLikeClause(itemCode, "sl.itemCode", "itemCodeSl", args)
val storeLocationSql = if (!storeLocation.isNullOrBlank()) {
args["storeLocation"] = "%$storeLocation%"
"AND COALESCE(store_location.storeLocation, '') LIKE :storeLocation"
"""
AND EXISTS (
SELECT 1
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
WHERE ill.deleted = 0
AND il.itemId = q.itemId
AND wh.code LIKE :storeLocation
)
""".trimIndent()
} else ""
val forceLedgerIndex = if (itemCode.isNullOrBlank()) {
"FORCE INDEX (idx_sl_deleted_date)"
} else {
""
}
val inventoryScopeUnion = if (useLiveInventory) {
"""
UNION
SELECT itemId FROM inventory_per_item WHERE currentBalance <> 0
""".trimIndent()
} else {
""
}

val baseSql = """
WITH params AS (
WITH day0 AS (
SELECT
DATE(:stockDate) AS d0,
DATE_SUB(DATE(:stockDate), INTERVAL 1 DAY) AS d1
d.itemId,
SUM(COALESCE(d.opening, 0)) AS openingBalance,
SUM(COALESCE(d.closing, 0)) AS currentBalance
FROM stock_lot_day d
WHERE IFNULL(d.deleted, 0) = 0
AND d.date = :d0
AND d.itemId IS NOT NULL
$itemCodeSqlDay
GROUP BY d.itemId
),
ledger_item_ids AS (
SELECT DISTINCT sl.itemId
FROM stock_ledger sl
INNER JOIN params p ON 1=1
WHERE sl.deleted = 0
AND sl.itemId IS NOT NULL
AND DATE(sl.date) <= p.d0
day1 AS (
SELECT
d.itemId,
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
),
item_scope AS (
inventory_per_item AS (
SELECT
it.id AS itemId,
it.code AS itemNo,
it.name AS itemName,
it.type AS itemType,
COALESCE(CAST(NULLIF(it.AverageUnitPrice, '') AS DECIMAL(14, 4)), 0) AS avgUnitPriceRaw
FROM items it
INNER JOIN ledger_item_ids li ON li.itemId = it.id
WHERE it.deleted = 0
AND it.code IS NOT NULL AND it.code <> ''
iv.itemId,
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
),
store_location AS (
SELECT
sl.itemId,
MAX(wh.code) AS storeLocation
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 inventory_lot_line ill_any ON ill_any.inventoryLotId = COALESCE(il_in.id, il_out.id) AND ill_any.deleted = 0
LEFT JOIN warehouse wh ON ill_any.warehouseId = wh.id AND wh.deleted = 0
WHERE sl.deleted = 0
GROUP BY sl.itemId
),
opening_ranked AS (
SELECT
sl.itemId,
COALESCE(sl.balance, 0) AS openingBalance,
ROW_NUMBER() OVER (PARTITION BY sl.itemId ORDER BY sl.date DESC, sl.id DESC) AS rn
FROM stock_ledger sl
INNER JOIN params p ON 1=1
WHERE sl.deleted = 0
AND sl.itemId IS NOT NULL
AND DATE(sl.date) <= p.d1
),
opening_per_item AS (
SELECT itemId, openingBalance
FROM opening_ranked
WHERE rn = 1
),
current_ranked AS (
SELECT
sl.itemId,
COALESCE(sl.balance, 0) AS currentBalance,
ROW_NUMBER() OVER (PARTITION BY sl.itemId ORDER BY sl.date DESC, sl.id DESC) AS rn
FROM stock_ledger sl
INNER JOIN params p ON 1=1
WHERE sl.deleted = 0
AND sl.itemId IS NOT NULL
AND DATE(sl.date) <= p.d0
),
current_per_item AS (
SELECT itemId, currentBalance
FROM current_ranked
WHERE rn = 1
),
last_in_out AS (
SELECT
sl.itemId,
MAX(CASE WHEN COALESCE(sl.inQty, 0) > 0 THEN sl.date END) AS lastInDate,
MAX(CASE WHEN COALESCE(sl.outQty, 0) > 0 THEN sl.date END) AS lastOutDate
FROM stock_ledger sl
INNER JOIN params p ON 1=1
WHERE sl.deleted = 0
AND sl.itemId IS NOT NULL
AND DATE(sl.date) <= p.d0
GROUP BY sl.itemId
GROUP BY iv.itemId
),
ledger_moves AS (
SELECT
@@ -1651,14 +1636,74 @@ return result
THEN COALESCE(sl.inQty, 0) - COALESCE(sl.outQty, 0)
ELSE 0
END
) AS variance
FROM stock_ledger sl
INNER JOIN params p ON 1=1
) AS variance,
SUM(COALESCE(sl.inQty, 0) - COALESCE(sl.outQty, 0)) AS dayNet
FROM stock_ledger sl $forceLedgerIndex
WHERE sl.deleted = 0
AND sl.itemId IS NOT NULL
AND DATE(sl.date) > p.d1
AND DATE(sl.date) <= p.d0
AND sl.date >= :d0Start
AND sl.date < :d0EndExclusive
$itemCodeSqlSl
GROUP BY sl.itemId
),
item_ids AS (
SELECT itemId FROM day0
UNION
SELECT itemId FROM day1
UNION
SELECT itemId FROM ledger_moves
$inventoryScopeUnion
),
item_scope AS (
SELECT
it.id AS itemId,
it.code AS itemNo,
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 <> ''
$itemCodeSql
$stockCategorySql
),
stock_one AS (
SELECT iu.*
FROM item_uom iu
INNER JOIN (
SELECT itemId, MIN(id) AS id
FROM item_uom
WHERE deleted = 0 AND stockUnit = 1
GROUP BY itemId
) x ON x.id = iu.id
),
qty_per_item AS (
SELECT
ids.itemId,
COALESCE(
CASE WHEN $liveFlag = 1 THEN d1.openingBalance ELSE d0.openingBalance END,
d1.openingBalance,
0
) AS openingBalance,
COALESCE(
CASE WHEN $liveFlag = 1 THEN invc.currentBalance ELSE d0.currentBalance END,
COALESCE(
CASE WHEN $liveFlag = 1 THEN d1.openingBalance ELSE d0.openingBalance END,
d1.openingBalance,
0
) + COALESCE(lm.dayNet, 0),
0
) AS currentBalance,
COALESCE(lm.cumStockIn, 0) AS cumStockIn,
COALESCE(lm.cumStockOut, 0) AS cumStockOut,
COALESCE(lm.misInputAndLost, 0) AS misInputAndLost,
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
)
SELECT
'' AS stockSubCategory,
@@ -1672,31 +1717,27 @@ return result
'' AS cumStockOut,
'' AS currentBalance,
'' AS reOrderQty,
COALESCE(store_location.storeLocation, '') AS storeLocation,
COALESCE(DATE_FORMAT(lio.lastInDate, '%Y-%m-%d'), '') AS lastInDate,
COALESCE(DATE_FORMAT(lio.lastOutDate, '%Y-%m-%d'), '') AS lastOutDate,
COALESCE(op.openingBalance, 0) AS openingBalanceRaw,
COALESCE(cp.currentBalance, 0) AS currentBalanceRaw,
CASE WHEN COALESCE(op.openingBalance, 0) < 0 THEN CONCAT('(', FORMAT(-op.openingBalance, 0), ')') ELSE FORMAT(COALESCE(op.openingBalance, 0), 0) END AS totalOpeningBalance,
CASE WHEN COALESCE(lm.cumStockIn, 0) < 0 THEN CONCAT('(', FORMAT(-lm.cumStockIn, 0), ')') ELSE FORMAT(COALESCE(lm.cumStockIn, 0), 0) END AS totalCumStockIn,
CASE WHEN COALESCE(lm.cumStockOut, 0) < 0 THEN CONCAT('(', FORMAT(-lm.cumStockOut, 0), ')') ELSE FORMAT(COALESCE(lm.cumStockOut, 0), 0) END AS totalCumStockOut,
CASE WHEN COALESCE(cp.currentBalance, 0) < 0 THEN CONCAT('(', FORMAT(-cp.currentBalance, 0), ')') ELSE FORMAT(COALESCE(cp.currentBalance, 0), 0) END AS totalCurrentBalance,
'' AS storeLocation,
'' AS lastInDate,
'' AS lastOutDate,
q.openingBalance AS openingBalanceRaw,
q.currentBalance AS currentBalanceRaw,
CASE WHEN q.openingBalance < 0 THEN CONCAT('(', FORMAT(-q.openingBalance, 0), ')') ELSE FORMAT(q.openingBalance, 0) END AS totalOpeningBalance,
CASE WHEN q.cumStockIn < 0 THEN CONCAT('(', FORMAT(-q.cumStockIn, 0), ')') ELSE FORMAT(q.cumStockIn, 0) END AS totalCumStockIn,
CASE WHEN q.cumStockOut < 0 THEN CONCAT('(', FORMAT(-q.cumStockOut, 0), ')') ELSE FORMAT(q.cumStockOut, 0) END AS totalCumStockOut,
CASE WHEN q.currentBalance < 0 THEN CONCAT('(', FORMAT(-q.currentBalance, 0), ')') ELSE FORMAT(q.currentBalance, 0) END AS totalCurrentBalance,
'' AS misInputAndLost,
'' AS defectiveGoods,
'' AS variance,
CASE WHEN COALESCE(lm.misInputAndLost, 0) < 0 THEN CONCAT('(', FORMAT(-lm.misInputAndLost, 0), ')') ELSE FORMAT(COALESCE(lm.misInputAndLost, 0), 0) END AS totalMisInputAndLost,
CASE WHEN COALESCE(lm.variance, 0) < 0 THEN CONCAT('(', FORMAT(-lm.variance, 0), ')') ELSE FORMAT(COALESCE(lm.variance, 0), 0) END AS totalVariance,
CASE WHEN COALESCE(lm.defectiveGoods, 0) < 0 THEN CONCAT('(', FORMAT(-lm.defectiveGoods, 0), ')') ELSE FORMAT(COALESCE(lm.defectiveGoods, 0), 0) END AS totalDefectiveGoods,
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(cp.currentBalance, 0), 2), 2) AS totalStockBalance
FROM item_scope s
LEFT JOIN opening_per_item op ON op.itemId = s.itemId
LEFT JOIN current_per_item cp ON cp.itemId = s.itemId
LEFT JOIN last_in_out lio ON lio.itemId = s.itemId
LEFT JOIN ledger_moves lm ON lm.itemId = s.itemId
LEFT JOIN item_uom iu ON iu.itemId = s.itemId AND iu.stockUnit = 1 AND iu.deleted = 0
LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
LEFT JOIN store_location ON store_location.itemId = s.itemId
FORMAT(ROUND(COALESCE(s.avgUnitPriceRaw, 0) * 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
WHERE 1=1
$storeLocationSql
""".trimIndent()
@@ -1713,10 +1754,8 @@ return result

val finalSql =
if (filters.isEmpty()) {
// no numeric filter: just order by itemNo
baseSql
} else {
// wrap to filter on raw numeric current balance
"""
SELECT * FROM (
$baseSql
@@ -1725,7 +1764,17 @@ return result
""".trimIndent()
}

return jdbcDao.queryForList("$finalSql ORDER BY itemNo", args)
val t0 = System.nanoTime()
val rows = jdbcDao.queryForList("$finalSql ORDER BY itemNo", args)
log.info(
"stock-balance-by-date date={} liveInventory={} itemCode={} rows={} {}ms",
stockLocalDate,
useLiveInventory,
itemCode ?: "",
rows.size,
(System.nanoTime() - t0) / 1_000_000,
)
return rows
}
/**
* Compiles and fills a Jasper Report, then exports to Excel (.xlsx). Same layout/columns as the report template.


Ładowanie…
Anuluj
Zapisz