ソースを参照

if have open item round, select new round by location if have overlap lcoation will tell those item will skip

stockTakeUpdate
CANCERYS\kw093 1週間前
親
コミット
055c247013
4個のファイルの変更、234行の追加、63行の削除
  1. +21
    -20
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt
  2. +189
    -43
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  3. +11
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt
  4. +13
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt

+ 21
- 20
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt ファイルの表示

@@ -21,10 +21,7 @@ interface StockTakeRecordRepository : AbstractRepository<StockTakeRecord, Long>

fun findAllByLotIdInAndDeletedIsFalse(lotIds: Collection<Long>): List<StockTakeRecord>

/**
* Open-round busy lot keys without loading full stock_take / stocktakerecord entities.
* Matches records by stockTakeRoundId or stockTakeId against open headers.
*/
/** Busy lot keys for open rounds — caller must pass non-empty roundIds. */
@Query(
value = """
SELECT DISTINCT r.lotId AS lotId, r.warehouseId AS warehouseId
@@ -32,25 +29,29 @@ interface StockTakeRecordRepository : AbstractRepository<StockTakeRecord, Long>
WHERE r.deleted = 0
AND r.lotId IS NOT NULL
AND r.warehouseId IS NOT NULL
AND (
EXISTS (
SELECT 1 FROM stock_take st
WHERE st.deleted = 0
AND st.status IN ('pending', 'stockTaking', 'approving')
AND r.stockTakeRoundId IS NOT NULL
AND r.stockTakeRoundId = st.stockTakeRoundId
)
OR EXISTS (
SELECT 1 FROM stock_take st
WHERE st.deleted = 0
AND st.status IN ('pending', 'stockTaking', 'approving')
AND r.stockTakeId = st.id
)
)
AND r.stockTakeRoundId IN (:roundIds)
""",
nativeQuery = true,
)
fun findBusyLotWarehouseKeysFromOpenRounds(): List<BusyLotWarehouseKeyRow>
fun findBusyLotWarehouseKeysByRoundIds(
@Param("roundIds") roundIds: Collection<Long>,
): List<BusyLotWarehouseKeyRow>

/** Busy lot keys for open headers — caller must pass non-empty stockTakeIds. */
@Query(
value = """
SELECT DISTINCT r.lotId AS lotId, r.warehouseId AS warehouseId
FROM stocktakerecord r
WHERE r.deleted = 0
AND r.lotId IS NOT NULL
AND r.warehouseId IS NOT NULL
AND r.stockTakeId IN (:stockTakeIds)
""",
nativeQuery = true,
)
fun findBusyLotWarehouseKeysByStockTakeIds(
@Param("stockTakeIds") stockTakeIds: Collection<Long>,
): List<BusyLotWarehouseKeyRow>

@Query(
"""


+ 189
- 43
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt ファイルの表示

@@ -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)
}

/**


+ 11
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt ファイルの表示

@@ -202,6 +202,17 @@ class StockTakeRecordController(
return stockOutRecordService.busyStockTakeSections()
}

/**
* 建立區域輪預覽:所選區域與 open「依貨品」輪重疊的貨品
*(本輪會略過這些 lot,不會盤到)。
*/
@GetMapping("/sectionCreateItemOverlaps")
fun sectionCreateItemOverlaps(
@RequestParam sections: List<String>?,
): List<SectionCreateItemOverlap> {
return stockOutRecordService.previewSectionCreateItemOverlaps(sections.orEmpty())
}

/** 建立 item 輪:搜尋有可用 lot 的貨品(可依來源 PP/PF/其他) */
@GetMapping("/searchItemsForCreate")
fun searchItemsForCreate(


+ 13
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt ファイルの表示

@@ -61,6 +61,19 @@ data class StockTakeCreateItemOption(
val name: String?,
)

/**
* 建立區域輪預覽:所選區域內已有 open「依貨品」輪佔用的貨品
*(本輪 placeholder 會略過對應 lot)。
*/
data class SectionCreateItemOverlap(
val itemId: Long,
val itemCode: String?,
val itemName: String?,
val stockTakeSection: String?,
val stockTakeRoundId: Long,
val stockTakeRoundName: String?,
)

data class AllPickedStockTakeListReponse(
val id: Long,
val stockTakeSession: String,


読み込み中…
キャンセル
保存