|
|
|
@@ -0,0 +1,419 @@ |
|
|
|
package com.ffii.fpsms.modules.report.service |
|
|
|
|
|
|
|
import com.ffii.core.support.JdbcDao |
|
|
|
import org.springframework.stereotype.Service |
|
|
|
import java.time.LocalDate |
|
|
|
import java.time.format.DateTimeFormatter |
|
|
|
|
|
|
|
/** |
|
|
|
* 庫存批次現況(Stock Balance):永遠今天。 |
|
|
|
* 現存讀 [inventory_lot_line](available、未過期、in-out > 0)。 |
|
|
|
* 最後異動:有 inventoryLotLineId 的帳本用 MAX(id);缺的比 SIL/SOL 時間。 |
|
|
|
* 單位均價/庫存總價值只填 root PO 為 PP/PF 的批(TRF 往回走);其他來源空白。 |
|
|
|
*/ |
|
|
|
@Service |
|
|
|
open class StockLotOnhandReportService( |
|
|
|
private val jdbcDao: JdbcDao, |
|
|
|
) { |
|
|
|
private val dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd") |
|
|
|
|
|
|
|
companion object { |
|
|
|
/** Same TRF walk as stock-take variance: root stock-in / PO after transfers. */ |
|
|
|
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 |
|
|
|
""" |
|
|
|
} |
|
|
|
|
|
|
|
data class SearchResult( |
|
|
|
val rows: List<Map<String, Any>>, |
|
|
|
val stockDate: String, |
|
|
|
) |
|
|
|
|
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ |
|
|
|
fun search( |
|
|
|
itemCode: String?, |
|
|
|
storeId: String?, |
|
|
|
warehouse: String?, |
|
|
|
area: String?, |
|
|
|
slot: String?, |
|
|
|
lotNo: String?, |
|
|
|
stockTakeSectionDescription: String?, |
|
|
|
lotOrigin: String?, |
|
|
|
): SearchResult { |
|
|
|
val asOfDateStr = LocalDate.now().format(dateFmt) |
|
|
|
|
|
|
|
val masterArgs = HashMap<String, Any>() |
|
|
|
val itemCodeSqlIt = buildMultiValueLikeClause(itemCode, "it.code", "itemCodeIt", masterArgs) |
|
|
|
val lotNoSql = buildMultiValueLikeClause(lotNo, "il.lotNo", "lotNo", masterArgs) |
|
|
|
val storeIdSql = if (!storeId.isNullOrBlank() && storeId.trim() != "All") { |
|
|
|
masterArgs["storeId"] = storeId.trim() |
|
|
|
"AND REPLACE(COALESCE(wh.store_id, ''), '/', '') = REPLACE(:storeId, '/', '')" |
|
|
|
} else { |
|
|
|
"" |
|
|
|
} |
|
|
|
val warehouseSql = if (!warehouse.isNullOrBlank() && warehouse.trim() != "All") { |
|
|
|
buildMultiValueLikeClause(warehouse, "wh.warehouse", "warehousePart", masterArgs) |
|
|
|
} else { |
|
|
|
"" |
|
|
|
} |
|
|
|
val areaSql = if (!area.isNullOrBlank() && area.trim() != "All") { |
|
|
|
buildMultiValueLikeClause(area, "wh.area", "areaPart", masterArgs) |
|
|
|
} else { |
|
|
|
"" |
|
|
|
} |
|
|
|
val slotSql = if (!slot.isNullOrBlank() && slot.trim() != "All") { |
|
|
|
buildMultiValueLikeClause(slot, "wh.slot", "slotPart", masterArgs) |
|
|
|
} else { |
|
|
|
"" |
|
|
|
} |
|
|
|
val sectionDescSql = if ( |
|
|
|
!stockTakeSectionDescription.isNullOrBlank() && |
|
|
|
stockTakeSectionDescription.trim() != "All" |
|
|
|
) { |
|
|
|
masterArgs["stockTakeSectionDescription"] = stockTakeSectionDescription.trim() |
|
|
|
"AND COALESCE(wh.stockTakeSectionDescription, '') = :stockTakeSectionDescription" |
|
|
|
} else { |
|
|
|
"" |
|
|
|
} |
|
|
|
val lotOriginFilterSql = buildLotOriginFilterSql(lotOrigin) |
|
|
|
val originJoinSql = ROOT_STOCK_IN_JOIN_SQL |
|
|
|
val ctePrefix = "WITH RECURSIVE\n$LOT_ROOT_ORIGIN_CTE_SQL\n" |
|
|
|
|
|
|
|
val liveLots = jdbcDao.queryForList( |
|
|
|
""" |
|
|
|
${ctePrefix}SELECT |
|
|
|
ill.id AS inventoryLotLineId, |
|
|
|
COALESCE(it.code, '') AS itemNo, |
|
|
|
COALESCE(it.name, '') AS itemName, |
|
|
|
COALESCE(uc.udfudesc, uc.code, '') AS unitOfMeasure, |
|
|
|
COALESCE(il.lotNo, '') AS lotNo, |
|
|
|
COALESCE(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate, |
|
|
|
COALESCE(wh.store_id, '') AS storeId, |
|
|
|
COALESCE(wh.warehouse, '') AS warehousePart, |
|
|
|
COALESCE(wh.area, '') AS areaPart, |
|
|
|
COALESCE(wh.slot, '') AS slotPart, |
|
|
|
(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS lotQtyRaw, |
|
|
|
COALESCE(CAST(NULLIF(it.AverageUnitPrice, '') AS DECIMAL(14, 4)), 0) AS avgUnitPriceRaw, |
|
|
|
UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) AS rootPoPrefix |
|
|
|
FROM inventory_lot_line ill |
|
|
|
INNER JOIN inventory_lot il |
|
|
|
ON il.id = ill.inventoryLotId AND il.deleted = 0 |
|
|
|
INNER JOIN items it |
|
|
|
ON it.id = il.itemId AND it.deleted = 0 |
|
|
|
INNER JOIN warehouse wh |
|
|
|
ON wh.id = ill.warehouseId AND wh.deleted = 0 |
|
|
|
LEFT JOIN item_uom iu |
|
|
|
ON iu.id = ill.stockItemUomId AND iu.deleted = 0 |
|
|
|
LEFT JOIN uom_conversion uc |
|
|
|
ON uc.id = iu.uomId |
|
|
|
$originJoinSql |
|
|
|
WHERE ill.deleted = 0 |
|
|
|
AND it.code IS NOT NULL AND it.code <> '' |
|
|
|
AND LOWER(COALESCE(ill.status, '')) = 'available' |
|
|
|
AND (il.expiryDate IS NULL OR il.expiryDate >= CURRENT_DATE) |
|
|
|
AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) > 0 |
|
|
|
$itemCodeSqlIt |
|
|
|
$storeIdSql |
|
|
|
$warehouseSql |
|
|
|
$areaSql |
|
|
|
$slotSql |
|
|
|
$sectionDescSql |
|
|
|
$lotNoSql |
|
|
|
$lotOriginFilterSql |
|
|
|
""".trimIndent(), |
|
|
|
masterArgs, |
|
|
|
) |
|
|
|
|
|
|
|
val lotIds = liveLots.map { toLong(it["inventoryLotLineId"]) }.filter { it > 0 } |
|
|
|
val lastTrnByLot = HashMap<Long, Pair<String, String>>(lotIds.size * 2) |
|
|
|
loadLastTrnFromLedger(lotIds, lastTrnByLot) |
|
|
|
val missing = lotIds.distinct().filter { it !in lastTrnByLot } |
|
|
|
if (missing.isNotEmpty()) { |
|
|
|
val silSolHits = HashMap<Long, SilSolHit>(missing.size * 2) |
|
|
|
fillLastTrnFromLotHeaderSil(missing, silSolHits) |
|
|
|
fillLastTrnFromSilLine(missing, silSolHits) |
|
|
|
fillLastTrnFromSol(missing, silSolHits) |
|
|
|
for ((lotId, hit) in silSolHits) { |
|
|
|
lastTrnByLot[lotId] = hit.date to hit.kind |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
val rows = assembleRows(liveLots, lastTrnByLot) |
|
|
|
return SearchResult(rows = rows, stockDate = asOfDateStr) |
|
|
|
} |
|
|
|
|
|
|
|
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 */ |
|
|
|
private fun loadLastTrnFromLedger(lotIds: List<Long>, out: MutableMap<Long, Pair<String, String>>) { |
|
|
|
if (lotIds.isEmpty()) return |
|
|
|
// idx_ledger_lot_date_id is (lot, date, id) — bad for MAX(id); force the lot-only index |
|
|
|
// (InnoDB secondary includes PK id, so MAX(id) per lot is an index tail lookup). |
|
|
|
for (chunk in lotIds.distinct().chunked(2000)) { |
|
|
|
val rows = jdbcDao.queryForList( |
|
|
|
""" |
|
|
|
SELECT |
|
|
|
sl.inventoryLotLineId, |
|
|
|
DATE_FORMAT(sl.date, '%Y-%m-%d') AS lastTrnDate, |
|
|
|
COALESCE(sl.inQty, 0) AS inQty, |
|
|
|
COALESCE(sl.outQty, 0) AS outQty |
|
|
|
FROM stock_ledger sl |
|
|
|
INNER JOIN ( |
|
|
|
SELECT inventoryLotLineId, MAX(id) AS maxId |
|
|
|
FROM stock_ledger FORCE INDEX (idx_ledger_inventoryLotLineId) |
|
|
|
WHERE deleted = 0 |
|
|
|
AND inventoryLotLineId IN (:lotIds) |
|
|
|
GROUP BY inventoryLotLineId |
|
|
|
) t ON t.maxId = sl.id |
|
|
|
""".trimIndent(), |
|
|
|
mapOf("lotIds" to chunk), |
|
|
|
) |
|
|
|
for (r in rows) { |
|
|
|
val lotId = toLong(r["inventoryLotLineId"]) |
|
|
|
if (lotId <= 0) continue |
|
|
|
val date = r["lastTrnDate"]?.toString().orEmpty() |
|
|
|
out[lotId] = date to lastTrnType(toDouble(r["inQty"]), toDouble(r["outQty"])) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ |
|
|
|
private fun fillLastTrnFromLotHeaderSil(missing: List<Long>, hits: MutableMap<Long, SilSolHit>): Int { |
|
|
|
var n = 0 |
|
|
|
for (chunk in missing.chunked(800)) { |
|
|
|
val rows = jdbcDao.queryForList( |
|
|
|
""" |
|
|
|
SELECT |
|
|
|
ill.id AS lotLineId, |
|
|
|
DATE_FORMAT(COALESCE(sil.receiptDate, sil.modified, sil.created), '%Y-%m-%d %H:%i:%s') AS lastTs, |
|
|
|
DATE_FORMAT(COALESCE(sil.receiptDate, sil.modified, sil.created), '%Y-%m-%d') AS lastTrnDate |
|
|
|
FROM inventory_lot_line ill |
|
|
|
INNER JOIN inventory_lot il |
|
|
|
ON il.id = ill.inventoryLotId AND il.deleted = 0 |
|
|
|
INNER JOIN stock_in_line sil |
|
|
|
ON sil.id = il.stockInLineId AND sil.deleted = 0 |
|
|
|
WHERE ill.deleted = 0 |
|
|
|
AND ill.id IN (:lotIds) |
|
|
|
""".trimIndent(), |
|
|
|
mapOf("lotIds" to chunk), |
|
|
|
) |
|
|
|
n += rows.size |
|
|
|
applySilSolHits(rows, hits, "入庫") |
|
|
|
} |
|
|
|
return n |
|
|
|
} |
|
|
|
|
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ |
|
|
|
private fun fillLastTrnFromSilLine(missing: List<Long>, hits: MutableMap<Long, SilSolHit>): Int { |
|
|
|
var n = 0 |
|
|
|
for (chunk in missing.chunked(800)) { |
|
|
|
val rows = jdbcDao.queryForList( |
|
|
|
""" |
|
|
|
SELECT |
|
|
|
sil.inventoryLotLineId AS lotLineId, |
|
|
|
DATE_FORMAT(COALESCE(sil.receiptDate, sil.modified, sil.created), '%Y-%m-%d %H:%i:%s') AS lastTs, |
|
|
|
DATE_FORMAT(COALESCE(sil.receiptDate, sil.modified, sil.created), '%Y-%m-%d') AS lastTrnDate |
|
|
|
FROM stock_in_line sil |
|
|
|
WHERE sil.deleted = 0 |
|
|
|
AND sil.inventoryLotLineId IN (:lotIds) |
|
|
|
""".trimIndent(), |
|
|
|
mapOf("lotIds" to chunk), |
|
|
|
) |
|
|
|
n += rows.size |
|
|
|
applySilSolHits(rows, hits, "入庫") |
|
|
|
} |
|
|
|
return n |
|
|
|
} |
|
|
|
|
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ |
|
|
|
private fun fillLastTrnFromSol(missing: List<Long>, hits: MutableMap<Long, SilSolHit>): Int { |
|
|
|
var n = 0 |
|
|
|
for (chunk in missing.chunked(800)) { |
|
|
|
val rows = jdbcDao.queryForList( |
|
|
|
""" |
|
|
|
SELECT |
|
|
|
sol.inventoryLotLineId AS lotLineId, |
|
|
|
DATE_FORMAT(COALESCE(sol.pickTime, sol.modified, sol.created), '%Y-%m-%d %H:%i:%s') AS lastTs, |
|
|
|
DATE_FORMAT(COALESCE(sol.pickTime, sol.modified, sol.created), '%Y-%m-%d') AS lastTrnDate |
|
|
|
FROM stock_out_line sol |
|
|
|
WHERE sol.deleted = 0 |
|
|
|
AND sol.inventoryLotLineId IN (:lotIds) |
|
|
|
""".trimIndent(), |
|
|
|
mapOf("lotIds" to chunk), |
|
|
|
) |
|
|
|
n += rows.size |
|
|
|
applySilSolHits(rows, hits, "出庫") |
|
|
|
} |
|
|
|
return n |
|
|
|
} |
|
|
|
|
|
|
|
private fun applySilSolHits(rows: List<Map<String, Any>>, hits: MutableMap<Long, SilSolHit>, kind: String) { |
|
|
|
for (r in rows) { |
|
|
|
val lotId = toLong(r["lotLineId"]) |
|
|
|
val ts = r["lastTs"]?.toString().orEmpty() |
|
|
|
if (lotId <= 0 || ts.isBlank()) continue |
|
|
|
val date = r["lastTrnDate"]?.toString().orEmpty() |
|
|
|
val prev = hits[lotId] |
|
|
|
val newer = prev == null || |
|
|
|
ts > prev.ts || |
|
|
|
(ts == prev.ts && kind == "出庫" && prev.kind != "出庫") |
|
|
|
if (newer) hits[lotId] = SilSolHit(ts, date, kind) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun lastTrnType(inQty: Double, outQty: Double): String = |
|
|
|
when { |
|
|
|
outQty > 0 && inQty <= 0 -> "出庫" |
|
|
|
inQty > 0 && outQty <= 0 -> "入庫" |
|
|
|
inQty > 0 && outQty > 0 -> if (outQty >= inQty) "出庫" else "入庫" |
|
|
|
else -> "" |
|
|
|
} |
|
|
|
|
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 68 | v1.0.0 | 2026-08-31 */ |
|
|
|
private fun assembleRows( |
|
|
|
liveLots: List<Map<String, Any>>, |
|
|
|
lastTrnByLot: Map<Long, Pair<String, String>>, |
|
|
|
): List<Map<String, Any>> { |
|
|
|
val tot = HashMap<String, Double>() |
|
|
|
for (r in liveLots) { |
|
|
|
val key = "${r["itemNo"]}|${r["unitOfMeasure"]}" |
|
|
|
tot[key] = (tot[key] ?: 0.0) + toDouble(r["lotQtyRaw"]) |
|
|
|
} |
|
|
|
val out = ArrayList<Map<String, Any>>(liveLots.size) |
|
|
|
for (r in liveLots) { |
|
|
|
val lotId = toLong(r["inventoryLotLineId"]) |
|
|
|
val lotQty = toDouble(r["lotQtyRaw"]) |
|
|
|
val avg = toDouble(r["avgUnitPriceRaw"]) |
|
|
|
val key = "${r["itemNo"]}|${r["unitOfMeasure"]}" |
|
|
|
val last = lastTrnByLot[lotId] |
|
|
|
val origin = r["rootPoPrefix"]?.toString().orEmpty().uppercase() |
|
|
|
val showPrice = origin == "PP" || origin == "PF" |
|
|
|
val row = HashMap<String, Any>(22) |
|
|
|
row["inventoryLotLineId"] = lotId.toString() |
|
|
|
row["itemNo"] = r["itemNo"] ?: "" |
|
|
|
row["itemName"] = r["itemName"] ?: "" |
|
|
|
row["unitOfMeasure"] = r["unitOfMeasure"] ?: "" |
|
|
|
row["lotNo"] = r["lotNo"] ?: "" |
|
|
|
row["expiryDate"] = r["expiryDate"] ?: "" |
|
|
|
row["storeId"] = r["storeId"] ?: "" |
|
|
|
row["warehousePart"] = r["warehousePart"] ?: "" |
|
|
|
row["areaPart"] = r["areaPart"] ?: "" |
|
|
|
row["slotPart"] = r["slotPart"] ?: "" |
|
|
|
row["lotQtyRaw"] = lotQty |
|
|
|
row["totalQtyRaw"] = tot[key] ?: 0.0 |
|
|
|
row["avgUnitPriceRaw"] = if (showPrice) avg else "" |
|
|
|
row["stockValueRaw"] = if (showPrice) avg * lotQty else "" |
|
|
|
row["lastTrnDate"] = last?.first ?: "" |
|
|
|
row["lastTrnType"] = last?.second ?: "" |
|
|
|
out.add(row) |
|
|
|
} |
|
|
|
out.sortWith( |
|
|
|
compareBy<Map<String, Any>> { it["itemNo"]?.toString().orEmpty() } |
|
|
|
.thenBy { it["unitOfMeasure"]?.toString().orEmpty() } |
|
|
|
.thenBy { it["lotNo"]?.toString().orEmpty() } |
|
|
|
.thenBy { it["storeId"]?.toString().orEmpty() } |
|
|
|
.thenBy { it["warehousePart"]?.toString().orEmpty() } |
|
|
|
.thenBy { it["areaPart"]?.toString().orEmpty() } |
|
|
|
.thenBy { it["slotPart"]?.toString().orEmpty() } |
|
|
|
.thenBy { it["inventoryLotLineId"]?.toString().orEmpty() }, |
|
|
|
) |
|
|
|
return out |
|
|
|
} |
|
|
|
|
|
|
|
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 |
|
|
|
} |
|
|
|
|
|
|
|
private fun toLong(v: Any?): Long { |
|
|
|
if (v == null) return 0L |
|
|
|
if (v is Number) return v.toLong() |
|
|
|
return v.toString().toLongOrNull() ?: 0L |
|
|
|
} |
|
|
|
|
|
|
|
/** PP/PF = root PO code prefix after TRF walk; other = not PP/PF (ADJ, TRF of other origin, JO, OPEN, …). */ |
|
|
|
private fun buildLotOriginFilterSql(lotOrigin: String?): String { |
|
|
|
val v = lotOrigin?.trim().orEmpty() |
|
|
|
if (v.isBlank() || v.equals("All", ignoreCase = true)) return "" |
|
|
|
return when (v.lowercase()) { |
|
|
|
"pp" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP'" |
|
|
|
"pf" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF'" |
|
|
|
"other" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) NOT IN ('PP', 'PF')" |
|
|
|
else -> "" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun buildMultiValueLikeClause( |
|
|
|
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 "" |
|
|
|
val conditions = values.mapIndexed { index, value -> |
|
|
|
val paramName = "${paramPrefix}_$index" |
|
|
|
args[paramName] = "%$value%" |
|
|
|
"$columnName LIKE :$paramName" |
|
|
|
} |
|
|
|
return "AND (${conditions.joinToString(" OR ")})" |
|
|
|
} |
|
|
|
} |