Quellcode durchsuchen

庫存查詢 / 盤點調整權限 / 批次現況報表

production
CANCERYS\kw093 vor 1 Woche
Ursprung
Commit
722e3a45c1
4 geänderte Dateien mit 50 neuen und 15 gelöschten Zeilen
  1. +1
    -1
      src/main/java/com/ffii/fpsms/config/security/SecurityConfig.java
  2. +40
    -9
      src/main/java/com/ffii/fpsms/modules/report/service/StockLotOnhandReportService.kt
  3. +4
    -3
      src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt
  4. +5
    -2
      src/main/java/com/ffii/fpsms/modules/report/web/StockLotOnhandReportController.kt

+ 1
- 1
src/main/java/com/ffii/fpsms/config/security/SecurityConfig.java Datei anzeigen

@@ -77,7 +77,7 @@ public class SecurityConfig {
/** /**
* FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13 * FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13
* FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.1 | 2026-08-06 * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.1 | 2026-08-06
* FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.5 | 2026-08-05
* FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.6 | 2026-09-07
* (stockAdjustment/submit → INVENTORY_ADJUST only) * (stockAdjustment/submit → INVENTORY_ADJUST only)
*/ */
@Bean @Bean


+ 40
- 9
src/main/java/com/ffii/fpsms/modules/report/service/StockLotOnhandReportService.kt Datei anzeigen

@@ -9,6 +9,8 @@ import java.time.format.DateTimeFormatter
* 庫存批次現況(Stock Balance):永遠今天。 * 庫存批次現況(Stock Balance):永遠今天。
* 現存讀 [inventory_lot_line](available、未過期、in-out > 0)。 * 現存讀 [inventory_lot_line](available、未過期、in-out > 0)。
* 最後異動:有 inventoryLotLineId 的帳本用 MAX(id);缺的比 SIL/SOL 時間。 * 最後異動:有 inventoryLotLineId 的帳本用 MAX(id);缺的比 SIL/SOL 時間。
* 單位均價:貨品 AverageUnitPrice(HKD/stock UOM)用 item_uom 比率轉成該 lot 單位;
* 同一貨品同一 UOM 共用同一均價。庫存總價值 = 該 UOM 均價 × lot 數量。
* 單位均價/庫存總價值只填 root PO 為 PP/PF 的批(TRF 往回走);其他來源空白。 * 單位均價/庫存總價值只填 root PO 為 PP/PF 的批(TRF 往回走);其他來源空白。
*/ */
@Service @Service
@@ -70,6 +72,19 @@ lot_root_origin AS (
WHERE t.rn = 1 WHERE t.rn = 1
)""" )"""


/** One stock-unit item_uom per item (same pick as item average-price SQL). */
private const val STOCK_ONE_CTE_SQL = """
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
)"""

private const val ROOT_STOCK_IN_JOIN_SQL = """ private const val ROOT_STOCK_IN_JOIN_SQL = """
LEFT JOIN lot_root_origin lro LEFT JOIN lot_root_origin lro
ON lro.lotId = il.id ON lro.lotId = il.id
@@ -85,7 +100,7 @@ lot_root_origin AS (
val stockDate: String, val stockDate: String,
) )


/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */
fun search( fun search(
itemCode: String?, itemCode: String?,
storeId: String?, storeId: String?,
@@ -133,12 +148,13 @@ lot_root_origin AS (
} }
val lotOriginFilterSql = buildLotOriginFilterSql(lotOrigin) val lotOriginFilterSql = buildLotOriginFilterSql(lotOrigin)
val originJoinSql = ROOT_STOCK_IN_JOIN_SQL val originJoinSql = ROOT_STOCK_IN_JOIN_SQL
val ctePrefix = "WITH RECURSIVE\n$LOT_ROOT_ORIGIN_CTE_SQL\n"
val ctePrefix = "WITH RECURSIVE\n$LOT_ROOT_ORIGIN_CTE_SQL,\n$STOCK_ONE_CTE_SQL\n"


val liveLots = jdbcDao.queryForList( val liveLots = jdbcDao.queryForList(
""" """
${ctePrefix}SELECT ${ctePrefix}SELECT
ill.id AS inventoryLotLineId, ill.id AS inventoryLotLineId,
COALESCE(NULLIF(TRIM(ill.remarks), ''), '') AS remarks,
COALESCE(it.code, '') AS itemNo, COALESCE(it.code, '') AS itemNo,
COALESCE(it.name, '') AS itemName, COALESCE(it.name, '') AS itemName,
COALESCE(uc.udfudesc, uc.code, '') AS unitOfMeasure, COALESCE(uc.udfudesc, uc.code, '') AS unitOfMeasure,
@@ -149,7 +165,19 @@ lot_root_origin AS (
COALESCE(wh.area, '') AS areaPart, COALESCE(wh.area, '') AS areaPart,
COALESCE(wh.slot, '') AS slotPart, COALESCE(wh.slot, '') AS slotPart,
(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS lotQtyRaw, (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS lotQtyRaw,
COALESCE(CAST(NULLIF(it.AverageUnitPrice, '') AS DECIMAL(14, 4)), 0) AS avgUnitPriceRaw,
CASE
WHEN iu.id IS NULL OR so.id IS NULL THEN
COALESCE(CAST(NULLIF(TRIM(it.AverageUnitPrice), '') AS DECIMAL(14, 4)), 0)
WHEN iu.uomId = so.uomId THEN
COALESCE(CAST(NULLIF(TRIM(it.AverageUnitPrice), '') AS DECIMAL(14, 4)), 0)
ELSE
ROUND(
COALESCE(CAST(NULLIF(TRIM(it.AverageUnitPrice), '') AS DECIMAL(14, 4)), 0)
* (COALESCE(iu.ratioN, 1) / COALESCE(NULLIF(iu.ratioD, 0), 1))
* (COALESCE(so.ratioD, 1) / COALESCE(NULLIF(so.ratioN, 0), 1)),
4
)
END AS avgUnitPriceRaw,
UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) AS rootPoPrefix UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) AS rootPoPrefix
FROM inventory_lot_line ill FROM inventory_lot_line ill
INNER JOIN inventory_lot il INNER JOIN inventory_lot il
@@ -162,6 +190,8 @@ lot_root_origin AS (
ON iu.id = ill.stockItemUomId AND iu.deleted = 0 ON iu.id = ill.stockItemUomId AND iu.deleted = 0
LEFT JOIN uom_conversion uc LEFT JOIN uom_conversion uc
ON uc.id = iu.uomId ON uc.id = iu.uomId
LEFT JOIN stock_one so
ON so.itemId = it.id
$originJoinSql $originJoinSql
WHERE ill.deleted = 0 WHERE ill.deleted = 0
AND it.code IS NOT NULL AND it.code <> '' AND it.code IS NOT NULL AND it.code <> ''
@@ -200,7 +230,7 @@ lot_root_origin AS (


private data class SilSolHit(val ts: String, val date: String, val kind: String) private data class SilSolHit(val ts: String, val date: String, val kind: String)


/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */
private fun loadLastTrnFromLedger(lotIds: List<Long>, out: MutableMap<Long, Pair<String, String>>) { private fun loadLastTrnFromLedger(lotIds: List<Long>, out: MutableMap<Long, Pair<String, String>>) {
if (lotIds.isEmpty()) return if (lotIds.isEmpty()) return
// idx_ledger_lot_date_id is (lot, date, id) — bad for MAX(id); force the lot-only index // idx_ledger_lot_date_id is (lot, date, id) — bad for MAX(id); force the lot-only index
@@ -233,7 +263,7 @@ lot_root_origin AS (
} }
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */
private fun fillLastTrnFromLotHeaderSil(missing: List<Long>, hits: MutableMap<Long, SilSolHit>): Int { private fun fillLastTrnFromLotHeaderSil(missing: List<Long>, hits: MutableMap<Long, SilSolHit>): Int {
var n = 0 var n = 0
for (chunk in missing.chunked(800)) { for (chunk in missing.chunked(800)) {
@@ -259,7 +289,7 @@ lot_root_origin AS (
return n return n
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */
private fun fillLastTrnFromSilLine(missing: List<Long>, hits: MutableMap<Long, SilSolHit>): Int { private fun fillLastTrnFromSilLine(missing: List<Long>, hits: MutableMap<Long, SilSolHit>): Int {
var n = 0 var n = 0
for (chunk in missing.chunked(800)) { for (chunk in missing.chunked(800)) {
@@ -281,7 +311,7 @@ lot_root_origin AS (
return n return n
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */
private fun fillLastTrnFromSol(missing: List<Long>, hits: MutableMap<Long, SilSolHit>): Int { private fun fillLastTrnFromSol(missing: List<Long>, hits: MutableMap<Long, SilSolHit>): Int {
var n = 0 var n = 0
for (chunk in missing.chunked(800)) { for (chunk in missing.chunked(800)) {
@@ -325,7 +355,7 @@ lot_root_origin AS (
else -> "" else -> ""
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */
private fun assembleRows( private fun assembleRows(
liveLots: List<Map<String, Any>>, liveLots: List<Map<String, Any>>,
lastTrnByLot: Map<Long, Pair<String, String>>, lastTrnByLot: Map<Long, Pair<String, String>>,
@@ -344,8 +374,9 @@ lot_root_origin AS (
val last = lastTrnByLot[lotId] val last = lastTrnByLot[lotId]
val origin = r["rootPoPrefix"]?.toString().orEmpty().uppercase() val origin = r["rootPoPrefix"]?.toString().orEmpty().uppercase()
val showPrice = origin == "PP" || origin == "PF" val showPrice = origin == "PP" || origin == "PF"
val row = HashMap<String, Any>(22)
val row = HashMap<String, Any>(24)
row["inventoryLotLineId"] = lotId.toString() row["inventoryLotLineId"] = lotId.toString()
row["remarks"] = r["remarks"] ?: ""
row["itemNo"] = r["itemNo"] ?: "" row["itemNo"] = r["itemNo"] ?: ""
row["itemName"] = r["itemName"] ?: "" row["itemName"] = r["itemName"] ?: ""
row["unitOfMeasure"] = r["unitOfMeasure"] ?: "" row["unitOfMeasure"] = r["unitOfMeasure"] ?: ""


+ 4
- 3
src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt Datei anzeigen

@@ -540,11 +540,12 @@ class ReportController(
numberStyle: XSSFCellStyle, numberStyle: XSSFCellStyle,
dashStyle: XSSFCellStyle, dashStyle: XSSFCellStyle,
preferInt: Boolean = false, preferInt: Boolean = false,
emptyAsBlank: Boolean = false,
) { ) {
val cell = row.createCell(col) val cell = row.createCell(col)
val parsed = parseSignedNumber(value) val parsed = parseSignedNumber(value)
if (parsed == null) { if (parsed == null) {
cell.setCellValue("-")
cell.setCellValue(if (emptyAsBlank) "" else "-")
cell.cellStyle = dashStyle cell.cellStyle = dashStyle
return return
} }
@@ -914,8 +915,8 @@ class ReportController(
setNumberCellFromFormatted(r, 7, m["totalVariance"], 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, 8, m["totalDefectiveGoods"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 9, m["totalCurrentBalance"], styles.int, styles.dash, preferInt = true) setNumberCellFromFormatted(r, 9, m["totalCurrentBalance"], styles.int, styles.dash, preferInt = true)
setNumberCellFromFormatted(r, 10, m["avgUnitPrice"], styles.number, styles.dash, preferInt = false)
setNumberCellFromFormatted(r, 11, m["totalStockBalance"], 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)
} }
} }




+ 5
- 2
src/main/java/com/ffii/fpsms/modules/report/web/StockLotOnhandReportController.kt Datei anzeigen

@@ -43,7 +43,7 @@ class StockLotOnhandReportController(
val number: CellStyle, val number: CellStyle,
) )


/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */
@GetMapping("/print-stock-lot-onhand-excel") @GetMapping("/print-stock-lot-onhand-excel")
fun exportExcel( fun exportExcel(
@RequestParam(required = false) itemCode: String?, @RequestParam(required = false) itemCode: String?,
@@ -191,6 +191,7 @@ class StockLotOnhandReportController(
return v return v
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.1 | 2026-09-07 */
private fun createExcel( private fun createExcel(
dbData: List<Map<String, Any>>, dbData: List<Map<String, Any>>,
reportDate: String, reportDate: String,
@@ -215,6 +216,7 @@ class StockLotOnhandReportController(
"樓層", "倉庫", "區域", "儲位", "樓層", "倉庫", "區域", "儲位",
"最後異動日", "最後異動(入庫/出庫)", "最後異動日", "最後異動(入庫/出庫)",
"批號結餘", "單位均價", "庫存總價值", "批號結餘", "單位均價", "庫存總價值",
"備註",
) )
val totalColumns = headers.size val totalColumns = headers.size
var rowIndex = 0 var rowIndex = 0
@@ -299,6 +301,7 @@ class StockLotOnhandReportController(
setNumberCell(r, 12, m["lotQtyRaw"], styles.number) setNumberCell(r, 12, m["lotQtyRaw"], styles.number)
setNumberCell(r, 13, m["avgUnitPriceRaw"], styles.number) setNumberCell(r, 13, m["avgUnitPriceRaw"], styles.number)
setNumberCell(r, 14, m["stockValueRaw"], styles.number) setNumberCell(r, 14, m["stockValueRaw"], styles.number)
setTextCell(r, 15, m["remarks"], styles.text)
} }
} }


@@ -307,7 +310,7 @@ class StockLotOnhandReportController(
sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, totalColumns - 1)) sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, totalColumns - 1))
} }
sheet.createFreezePane(0, headerRowIndex + 1) sheet.createFreezePane(0, headerRowIndex + 1)
intArrayOf(14, 28, 14, 8, 20, 12, 10, 12, 10, 10, 12, 20, 12, 12, 14)
intArrayOf(14, 28, 14, 8, 20, 12, 10, 12, 10, 10, 12, 20, 12, 12, 14, 28)
.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) } .forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) }


val out = ByteArrayOutputStream() val out = ByteArrayOutputStream()


Laden…
Abbrechen
Speichern