|
|
|
@@ -1339,15 +1339,98 @@ lot_root_origin AS ( |
|
|
|
.sorted() |
|
|
|
} |
|
|
|
|
|
|
|
/** 所有 open 輪次中尚未刪除的盤點列 (lotId, warehouseId),供建立時避免重疊。 */ |
|
|
|
/** |
|
|
|
* 建立區域輪前預覽:所選 section 內已被 open「依貨品」輪佔用的貨品 |
|
|
|
*(Option B 會略過這些 lot,本輪不會盤到)。 |
|
|
|
*/ |
|
|
|
open fun previewSectionCreateItemOverlaps(sections: Collection<String>): List<SectionCreateItemOverlap> { |
|
|
|
val wanted = sections.map { it.trim() }.filter { it.isNotBlank() } |
|
|
|
.map { it.lowercase() } |
|
|
|
.toSet() |
|
|
|
if (wanted.isEmpty()) return emptyList() |
|
|
|
|
|
|
|
val openItemRows = stockTakeRepository.findOpenStockTakeRows().filter { row -> |
|
|
|
StockTakeRoundMode.ITEM.equals(row.getStockTakeRoundMode()?.trim(), ignoreCase = true) && |
|
|
|
wanted.contains(row.getStockTakeSection()?.trim()?.lowercase()) |
|
|
|
} |
|
|
|
if (openItemRows.isEmpty()) return emptyList() |
|
|
|
|
|
|
|
val roundIds = openItemRows.mapNotNull { it.getStockTakeRoundId()?.takeIf { id -> id > 0L } }.distinct() |
|
|
|
val headerIds = openItemRows.map { it.getId() }.filter { it > 0L }.distinct() |
|
|
|
val headers = buildList { |
|
|
|
if (roundIds.isNotEmpty()) { |
|
|
|
addAll(stockTakeRepository.findAllByDeletedIsFalseAndStockTakeRoundIdIn(roundIds)) |
|
|
|
} |
|
|
|
if (headerIds.isNotEmpty()) { |
|
|
|
addAll(stockTakeRepository.findAllByDeletedIsFalseAndIdIn(headerIds)) |
|
|
|
} |
|
|
|
}.distinctBy { it.id } |
|
|
|
val roundNameByKey = headers.associate { st -> |
|
|
|
val key = st.stockTakeRoundId?.takeIf { it > 0L } ?: st.id!! |
|
|
|
key to st.stockTakeRoundName?.trim()?.takeIf { it.isNotBlank() } |
|
|
|
} |
|
|
|
|
|
|
|
val records = buildList { |
|
|
|
if (roundIds.isNotEmpty()) { |
|
|
|
addAll(stockTakeRecordRepository.findAllByStockTakeRoundIdInAndDeletedIsFalse(roundIds)) |
|
|
|
} |
|
|
|
if (headerIds.isNotEmpty()) { |
|
|
|
addAll(stockTakeRecordRepository.findAllByStockTakeIdInAndDeletedIsFalse(headerIds)) |
|
|
|
} |
|
|
|
}.distinctBy { it.id } |
|
|
|
|
|
|
|
val seen = linkedSetOf<String>() |
|
|
|
val out = ArrayList<SectionCreateItemOverlap>() |
|
|
|
for (rec in records) { |
|
|
|
val section = rec.stockTakeSection?.trim().orEmpty() |
|
|
|
if (section.isEmpty() || section.lowercase() !in wanted) continue |
|
|
|
val itemId = rec.itemId?.takeIf { it > 0L } ?: continue |
|
|
|
val roundKey = rec.stockTakeRoundId?.takeIf { it > 0L } |
|
|
|
?: rec.stockTake?.id |
|
|
|
?: continue |
|
|
|
val dedupe = "$itemId|$roundKey|${section.lowercase()}" |
|
|
|
if (!seen.add(dedupe)) continue |
|
|
|
out.add( |
|
|
|
SectionCreateItemOverlap( |
|
|
|
itemId = itemId, |
|
|
|
itemCode = rec.itemCode, |
|
|
|
itemName = rec.itemName, |
|
|
|
stockTakeSection = section, |
|
|
|
stockTakeRoundId = roundKey, |
|
|
|
stockTakeRoundName = roundNameByKey[roundKey], |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
return out.sortedWith( |
|
|
|
compareBy({ it.stockTakeSection.orEmpty() }, { it.itemCode.orEmpty() }, { it.stockTakeRoundId }), |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 所有 open 輪次中尚未刪除的盤點列 (lotId, warehouseId),供建立時避免重疊。 |
|
|
|
* 無 open header 時直接空集合(不掃 stocktakerecord);有則依 roundId / stockTakeId IN 查。 |
|
|
|
*/ |
|
|
|
open fun busyOpenLotWarehouseKeys(): Set<Pair<Long, Long>> { |
|
|
|
return stockTakeRecordRepository.findBusyLotWarehouseKeysFromOpenRounds() |
|
|
|
.mapNotNull { row -> |
|
|
|
val lotId = row.getLotId().takeIf { it > 0L } ?: return@mapNotNull null |
|
|
|
val warehouseId = row.getWarehouseId().takeIf { it > 0L } ?: return@mapNotNull null |
|
|
|
lotId to warehouseId |
|
|
|
val open = stockTakeRepository.findOpenStockTakeRows() |
|
|
|
if (open.isEmpty()) return emptySet() |
|
|
|
|
|
|
|
val roundIds = open.mapNotNull { it.getStockTakeRoundId()?.takeIf { id -> id > 0L } }.distinct() |
|
|
|
val headerIds = open.map { it.getId() }.filter { it > 0L }.distinct() |
|
|
|
if (roundIds.isEmpty() && headerIds.isEmpty()) return emptySet() |
|
|
|
|
|
|
|
val rows = buildList { |
|
|
|
if (roundIds.isNotEmpty()) { |
|
|
|
addAll(stockTakeRecordRepository.findBusyLotWarehouseKeysByRoundIds(roundIds)) |
|
|
|
} |
|
|
|
.toSet() |
|
|
|
if (headerIds.isNotEmpty()) { |
|
|
|
addAll(stockTakeRecordRepository.findBusyLotWarehouseKeysByStockTakeIds(headerIds)) |
|
|
|
} |
|
|
|
} |
|
|
|
return rows.mapNotNull { row -> |
|
|
|
val lotId = row.getLotId().takeIf { it > 0L } ?: return@mapNotNull null |
|
|
|
val warehouseId = row.getWarehouseId().takeIf { it > 0L } ?: return@mapNotNull null |
|
|
|
lotId to warehouseId |
|
|
|
}.toSet() |
|
|
|
} |
|
|
|
|
|
|
|
/** PP/PF = root PO 前綴;other = 非 PP/PF(與庫存批次現況報告一致) */ |
|
|
|
@@ -1362,6 +1445,44 @@ lot_root_origin AS ( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** 來源=All 不必溯源;僅 PP/PF/其他才跑 root-PO recursive CTE。 */ |
|
|
|
private fun needsCreateLotOriginTrace(lotOrigin: String?): Boolean { |
|
|
|
val v = lotOrigin?.trim().orEmpty() |
|
|
|
return v.isNotEmpty() && !v.equals("All", ignoreCase = true) |
|
|
|
} |
|
|
|
|
|
|
|
private fun mapCreateLotOptionRows( |
|
|
|
rows: List<Map<String, Any>>, |
|
|
|
busy: Set<Pair<Long, Long>>, |
|
|
|
): List<StockTakeCreateLotOption> { |
|
|
|
return rows.mapNotNull { row -> |
|
|
|
val lineId = (row["inventoryLotLineId"] as? Number)?.toLong() ?: return@mapNotNull null |
|
|
|
val itemId = (row["itemId"] as? Number)?.toLong() ?: return@mapNotNull null |
|
|
|
val lotId = (row["lotId"] as? Number)?.toLong() ?: return@mapNotNull null |
|
|
|
val warehouseId = (row["warehouseId"] as? Number)?.toLong() ?: return@mapNotNull null |
|
|
|
val qty = when (val q = row["availableQty"]) { |
|
|
|
is BigDecimal -> q |
|
|
|
is Number -> BigDecimal.valueOf(q.toDouble()) |
|
|
|
else -> BigDecimal.ZERO |
|
|
|
} |
|
|
|
StockTakeCreateLotOption( |
|
|
|
inventoryLotLineId = lineId, |
|
|
|
itemId = itemId, |
|
|
|
itemCode = row["itemCode"]?.toString(), |
|
|
|
itemName = row["itemName"]?.toString(), |
|
|
|
lotId = lotId, |
|
|
|
lotNo = row["lotNo"]?.toString(), |
|
|
|
expiryDate = row["expiryDate"]?.toString(), |
|
|
|
warehouseId = warehouseId, |
|
|
|
warehouseCode = row["warehouseCode"]?.toString(), |
|
|
|
stockTakeSection = row["stockTakeSection"]?.toString()?.trim(), |
|
|
|
storeId = row["storeId"]?.toString(), |
|
|
|
availableQty = qty, |
|
|
|
busy = (lotId to warehouseId) in busy, |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 搜尋「有可用 lot」的貨品(建立 item 輪左側列表)。 |
|
|
|
* @param lotOrigin All / PP / PF / other |
|
|
|
@@ -1374,8 +1495,7 @@ lot_root_origin AS ( |
|
|
|
): List<StockTakeCreateItemOption> { |
|
|
|
val codeTrim = code?.trim().orEmpty() |
|
|
|
val nameTrim = name?.trim().orEmpty() |
|
|
|
val originTrim = lotOrigin?.trim().orEmpty() |
|
|
|
val hasOrigin = originTrim.isNotEmpty() && !originTrim.equals("All", ignoreCase = true) |
|
|
|
val hasOrigin = needsCreateLotOriginTrace(lotOrigin) |
|
|
|
// 來源已選 PP/PF/其他 → 可只依來源列出;來源=全部則仍需編號或名稱 |
|
|
|
if (codeTrim.isEmpty() && nameTrim.isEmpty() && !hasOrigin) return emptyList() |
|
|
|
|
|
|
|
@@ -1388,12 +1508,33 @@ lot_root_origin AS ( |
|
|
|
args["itemName"] = "%$nameTrim%" |
|
|
|
"AND it.name LIKE :itemName" |
|
|
|
} else "" |
|
|
|
val seedFilter = "$codeSql\n $nameSql" |
|
|
|
val originSql = buildCreateLotOriginFilterSql(lotOrigin) |
|
|
|
val cte = CREATE_LOT_ROOT_ORIGIN_CTE.replace("__SEED_ITEM_FILTER__", seedFilter) |
|
|
|
args["rowLimit"] = limit.coerceIn(1, 2000) |
|
|
|
|
|
|
|
val sql = """ |
|
|
|
val sql = if (!hasOrigin) { |
|
|
|
// Fast path:不溯源,只查有可用庫存的貨品 |
|
|
|
""" |
|
|
|
SELECT DISTINCT |
|
|
|
it.id AS id, |
|
|
|
it.code AS code, |
|
|
|
it.name AS name |
|
|
|
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 |
|
|
|
WHERE ill.deleted = 0 |
|
|
|
AND LOWER(COALESCE(ill.status, '')) = 'available' |
|
|
|
AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) > 0 |
|
|
|
AND TRIM(COALESCE(wh.stockTakeSection, '')) <> '' |
|
|
|
$codeSql |
|
|
|
$nameSql |
|
|
|
ORDER BY it.code |
|
|
|
LIMIT :rowLimit |
|
|
|
""".trimIndent() |
|
|
|
} else { |
|
|
|
val seedFilter = "$codeSql\n $nameSql" |
|
|
|
val originSql = buildCreateLotOriginFilterSql(lotOrigin) |
|
|
|
val cte = CREATE_LOT_ROOT_ORIGIN_CTE.replace("__SEED_ITEM_FILTER__", seedFilter) |
|
|
|
""" |
|
|
|
WITH RECURSIVE |
|
|
|
$cte |
|
|
|
SELECT DISTINCT |
|
|
|
@@ -1411,6 +1552,7 @@ WHERE 1=1 |
|
|
|
ORDER BY it.code |
|
|
|
LIMIT :rowLimit |
|
|
|
""".trimIndent() |
|
|
|
} |
|
|
|
|
|
|
|
return jdbcDao.queryForList(sql, args).mapNotNull { row -> |
|
|
|
val id = (row["id"] as? Number)?.toLong() ?: return@mapNotNull null |
|
|
|
@@ -1440,11 +1582,39 @@ LIMIT :rowLimit |
|
|
|
args[key] = id |
|
|
|
":$key" |
|
|
|
}.joinToString(", ") |
|
|
|
val seedFilter = "AND it.id IN ($inClause)" |
|
|
|
val originSql = buildCreateLotOriginFilterSql(lotOrigin) |
|
|
|
val cte = CREATE_LOT_ROOT_ORIGIN_CTE.replace("__SEED_ITEM_FILTER__", seedFilter) |
|
|
|
val hasOrigin = needsCreateLotOriginTrace(lotOrigin) |
|
|
|
|
|
|
|
val sql = """ |
|
|
|
val sql = if (!hasOrigin) { |
|
|
|
""" |
|
|
|
SELECT |
|
|
|
ill.id AS inventoryLotLineId, |
|
|
|
it.id AS itemId, |
|
|
|
it.code AS itemCode, |
|
|
|
it.name AS itemName, |
|
|
|
il.id AS lotId, |
|
|
|
il.lotNo AS lotNo, |
|
|
|
DATE_FORMAT(il.expiryDate, '%Y-%m-%d') AS expiryDate, |
|
|
|
wh.id AS warehouseId, |
|
|
|
wh.code AS warehouseCode, |
|
|
|
wh.stockTakeSection AS stockTakeSection, |
|
|
|
wh.store_id AS storeId, |
|
|
|
(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS availableQty |
|
|
|
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 |
|
|
|
WHERE ill.deleted = 0 |
|
|
|
AND LOWER(COALESCE(ill.status, '')) = 'available' |
|
|
|
AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) > 0 |
|
|
|
AND TRIM(COALESCE(wh.stockTakeSection, '')) <> '' |
|
|
|
AND it.id IN ($inClause) |
|
|
|
ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
|
""".trimIndent() |
|
|
|
} else { |
|
|
|
val seedFilter = "AND it.id IN ($inClause)" |
|
|
|
val originSql = buildCreateLotOriginFilterSql(lotOrigin) |
|
|
|
val cte = CREATE_LOT_ROOT_ORIGIN_CTE.replace("__SEED_ITEM_FILTER__", seedFilter) |
|
|
|
""" |
|
|
|
WITH RECURSIVE |
|
|
|
$cte |
|
|
|
SELECT |
|
|
|
@@ -1476,33 +1646,9 @@ WHERE ill.deleted = 0 |
|
|
|
$originSql |
|
|
|
ORDER BY it.code, wh.stockTakeSection, il.lotNo |
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
return jdbcDao.queryForList(sql, args).mapNotNull { row -> |
|
|
|
val lineId = (row["inventoryLotLineId"] as? Number)?.toLong() ?: return@mapNotNull null |
|
|
|
val itemId = (row["itemId"] as? Number)?.toLong() ?: return@mapNotNull null |
|
|
|
val lotId = (row["lotId"] as? Number)?.toLong() ?: return@mapNotNull null |
|
|
|
val warehouseId = (row["warehouseId"] as? Number)?.toLong() ?: return@mapNotNull null |
|
|
|
val qty = when (val q = row["availableQty"]) { |
|
|
|
is BigDecimal -> q |
|
|
|
is Number -> BigDecimal.valueOf(q.toDouble()) |
|
|
|
else -> BigDecimal.ZERO |
|
|
|
} |
|
|
|
StockTakeCreateLotOption( |
|
|
|
inventoryLotLineId = lineId, |
|
|
|
itemId = itemId, |
|
|
|
itemCode = row["itemCode"]?.toString(), |
|
|
|
itemName = row["itemName"]?.toString(), |
|
|
|
lotId = lotId, |
|
|
|
lotNo = row["lotNo"]?.toString(), |
|
|
|
expiryDate = row["expiryDate"]?.toString(), |
|
|
|
warehouseId = warehouseId, |
|
|
|
warehouseCode = row["warehouseCode"]?.toString(), |
|
|
|
stockTakeSection = row["stockTakeSection"]?.toString()?.trim(), |
|
|
|
storeId = row["storeId"]?.toString(), |
|
|
|
availableQty = qty, |
|
|
|
busy = (lotId to warehouseId) in busy, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
return mapCreateLotOptionRows(jdbcDao.queryForList(sql, args), busy) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
|