From e0f898eeb024d14bcb2a349a78240c913d210a5e Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Fri, 23 Oct 2026 16:21:11 +0800 Subject: [PATCH] update ui --- .../stock/service/StockTakeRecordService.kt | 332 +++++++++++++++++- .../modules/stock/service/StockTakeService.kt | 83 +++-- .../modules/stock/web/StockTakeController.kt | 2 + .../stock/web/StockTakeRecordController.kt | 19 + .../CreateStockTakeForSectionsRequest.kt | 2 + .../stock/web/model/StockTakeRecordReponse.kt | 24 ++ 6 files changed, 437 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt index 597458d7..4b83d0db 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt @@ -47,9 +47,11 @@ import com.ffii.fpsms.modules.stock.entity.StockLedger import com.ffii.fpsms.modules.master.entity.UomConversionRepository import java.time.LocalDate import com.ffii.fpsms.modules.stock.entity.InventoryLotLine +import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus import java.math.RoundingMode import com.ffii.fpsms.modules.stock.entity.StockTake import com.ffii.fpsms.modules.master.entity.ItemsRepository +import com.ffii.core.support.JdbcDao @Service open class StockTakeRecordService( val stockTakeRepository: StockTakeRepository, @@ -69,8 +71,65 @@ open class StockTakeRecordService( val uomConversionRepository: UomConversionRepository, val itemsRepository: ItemsRepository, @Lazy private val stockInLineService: StockInLineService, - + private val jdbcDao: JdbcDao, ) { + companion object { + /** 與庫存批次現況報告相同:TRF 往回找 root PO */ + private const val CREATE_LOT_ROOT_ORIGIN_CTE = """ +seed_lots AS ( + SELECT DISTINCT + il.id AS lotId, + il.stockInLineId AS silId + 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, '')) <> '' + __SEED_ITEM_FILTER__ +), +lot_trace AS ( + SELECT + sl.lotId AS lotId, + sl.silId AS silId, + 0 AS depth + FROM seed_lots sl + WHERE sl.silId 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 val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java) /** 建立盤點輪次時預建 stock_take_record 用的佔位盤點員(取任一有效 user id)。 */ @@ -97,6 +156,7 @@ open class StockTakeRecordService( val roundId = stockTake.stockTakeRoundId ?: stockTake.id ?: throw IllegalStateException("stockTake id is null") + val busyLotKeys = busyOpenLotWarehouseKeys() val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalseAndStatusIsAvailable(warehouseIds) .filter { ill -> @@ -110,6 +170,12 @@ open class StockTakeRecordService( val inventoryLot = ill.inventoryLot ?: continue val warehouse = ill.warehouse ?: continue val item = inventoryLot.item ?: continue + val lotId = inventoryLot.id ?: continue + val warehouseId = warehouse.id ?: continue + // Option B:區域輪可開,但略過已在其他 open 輪次的 lot + if ((lotId to warehouseId) in busyLotKeys) { + continue + } val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) //.subtract(ill.holdQty ?: BigDecimal.ZERO) @@ -161,6 +227,80 @@ open class StockTakeRecordService( } } + /** + * 依明確的 inventory_lot_line id 預建盤點列(item 輪勾選 lot);仍略過 busy/qty≤0。 + */ + open fun createPlaceholderStockTakeRecordsForLotLines( + stockTake: StockTake, + placeholderStockTakerId: Long, + inventoryLotLineIds: Collection, + ) { + val section = stockTake.stockTakeSection ?: return + val wanted = inventoryLotLineIds.filter { it > 0L }.distinct() + if (wanted.isEmpty()) return + val roundId = stockTake.stockTakeRoundId ?: stockTake.id + ?: throw IllegalStateException("stockTake id is null") + val busyLotKeys = busyOpenLotWarehouseKeys() + val inventoryLotLines = inventoryLotLineRepository.findAllById(wanted).filter { ill -> + !ill.deleted && + ill.status == InventoryLotLineStatus.AVAILABLE && + section.equals(ill.warehouse?.stockTakeSection?.trim(), ignoreCase = true) + } + if (inventoryLotLines.isEmpty()) return + + val toSave = ArrayList(inventoryLotLines.size) + val illPerRecord = ArrayList(inventoryLotLines.size) + for (ill in inventoryLotLines) { + val inventoryLot = ill.inventoryLot ?: continue + val warehouse = ill.warehouse ?: continue + val item = inventoryLot.item ?: continue + val lotId = inventoryLot.id ?: continue + val warehouseId = warehouse.id ?: continue + if ((lotId to warehouseId) in busyLotKeys) continue + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + if (availableQty.compareTo(BigDecimal.ZERO) <= 0) continue + + toSave.add( + StockTakeRecord().apply { + this.itemId = item.id + this.lotId = inventoryLot.id + this.warehouse = warehouse + this.stockTake = stockTake + this.stockTakeRoundId = roundId + this.stockTakeSection = section + this.inventoryLotId = inventoryLot.id + this.stockTakerId = placeholderStockTakerId + this.stockTakerName = null + this.bookQty = availableQty + this.uom = ill.stockUom?.uom?.udfudesc + this.date = LocalDate.now() + this.status = "pending" + this.itemCode = item.code + this.itemName = item.name + this.lotNo = inventoryLot.lotNo + this.expiredDate = inventoryLot.expiryDate + } + ) + illPerRecord.add(ill) + } + stockTakeRecordRepository.saveAll(toSave) + if (toSave.isNotEmpty()) { + val lines = toSave.zip(illPerRecord).map { (record, ill) -> + StockTakeLine().apply { + this.stockTake = stockTake + this.inventoryLotLine = ill + this.initialQty = record.bookQty + this.finalQty = null + this.uom = ill.stockUom?.uom + this.status = StockTakeLineStatus.PENDING + this.stockTakeRecord = record + } + } + stockTakeLineRepository.saveAll(lines) + } + } + /** * 同一輪多 section 的 stock_take:優先用 [StockTake.stockTakeRoundId];舊資料為 null 時退回以 planStart 相同辨識一輪。 */ @@ -1186,15 +1326,199 @@ open class StockTakeRecordService( ) } + /** + * 僅「依區域」open 輪次佔用的區域(整區不可再開區域輪)。 + * Item 輪次不佔整區,改以 [busyOpenLotWarehouseKeys] 在建立時排除 lot。 + */ open fun busyStockTakeSections(): List { - return stockTakeRepository.findAll() - .filter { !it.deleted && it.status?.isOpen() == true } - .mapNotNull { it.stockTakeSection?.trim() } + return stockTakeRepository.findOpenStockTakeRows() + .filter { row -> + val mode = row.getStockTakeRoundMode()?.trim()?.lowercase().orEmpty() + mode.isEmpty() || mode == StockTakeRoundMode.SECTION + } + .mapNotNull { it.getStockTakeSection()?.trim() } .filter { it.isNotBlank() } .distinct() .sorted() } + /** 所有 open 輪次中尚未刪除的盤點列 (lotId, warehouseId),供建立時避免重疊。 */ + open fun busyOpenLotWarehouseKeys(): Set> { + val openHeaders = stockTakeRepository.findAll() + .filter { !it.deleted && it.status?.isOpen() == true } + if (openHeaders.isEmpty()) return emptySet() + val roundIds = openHeaders.mapNotNull { it.stockTakeRoundId?.takeIf { id -> id > 0L } }.distinct() + val headerIds = openHeaders.mapNotNull { it.id } + val records = buildList { + if (roundIds.isNotEmpty()) { + addAll(stockTakeRecordRepository.findAllByStockTakeRoundIdInAndDeletedIsFalse(roundIds)) + } + if (headerIds.isNotEmpty()) { + addAll(stockTakeRecordRepository.findAllByStockTakeIdInAndDeletedIsFalse(headerIds)) + } + } + return records.mapNotNull { rec -> + val lotId = rec.lotId ?: return@mapNotNull null + val warehouseId = rec.warehouse?.id ?: return@mapNotNull null + lotId to warehouseId + }.toSet() + } + + /** PP/PF = root PO 前綴;other = 非 PP/PF(與庫存批次現況報告一致) */ + private fun buildCreateLotOriginFilterSql(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 -> "" + } + } + + /** + * 搜尋「有可用 lot」的貨品(建立 item 輪左側列表)。 + * @param lotOrigin All / PP / PF / other + */ + open fun searchItemsForStockTakeCreate( + code: String?, + name: String?, + lotOrigin: String?, + limit: Int = 1000, + ): List { + val codeTrim = code?.trim().orEmpty() + val nameTrim = name?.trim().orEmpty() + val originTrim = lotOrigin?.trim().orEmpty() + val hasOrigin = originTrim.isNotEmpty() && !originTrim.equals("All", ignoreCase = true) + // 來源已選 PP/PF/其他 → 可只依來源列出;來源=全部則仍需編號或名稱 + if (codeTrim.isEmpty() && nameTrim.isEmpty() && !hasOrigin) return emptyList() + + val args = mutableMapOf() + val codeSql = if (codeTrim.isNotEmpty()) { + args["itemCode"] = "%$codeTrim%" + "AND it.code LIKE :itemCode" + } else "" + val nameSql = if (nameTrim.isNotEmpty()) { + 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 = """ +WITH RECURSIVE +$cte +SELECT DISTINCT + it.id AS id, + it.code AS code, + it.name AS name +FROM seed_lots sl +INNER JOIN inventory_lot il ON il.id = sl.lotId AND il.deleted = 0 +INNER JOIN items it ON it.id = il.itemId AND it.deleted = 0 +LEFT JOIN lot_root_origin lro ON lro.lotId = sl.lotId +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 +WHERE 1=1 + $originSql +ORDER BY it.code +LIMIT :rowLimit +""".trimIndent() + + return jdbcDao.queryForList(sql, args).mapNotNull { row -> + val id = (row["id"] as? Number)?.toLong() ?: return@mapNotNull null + StockTakeCreateItemOption( + id = id, + code = row["code"]?.toString(), + name = row["name"]?.toString(), + ) + } + } + + /** + * 依貨品查可選 lot(建立 item 輪用);已在 open 輪次的 lot 標 busy。 + * @param lotOrigin All / PP / PF / other + */ + open fun availableLotsForStockTakeCreate( + itemIds: List, + lotOrigin: String? = null, + ): List { + val wanted = itemIds.filter { it > 0L }.distinct() + if (wanted.isEmpty()) return emptyList() + val busy = busyOpenLotWarehouseKeys() + + val args = mutableMapOf() + val inClause = wanted.mapIndexed { index, id -> + val key = "itemId_$index" + 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 sql = """ +WITH RECURSIVE +$cte +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 +INNER JOIN seed_lots sl ON sl.lotId = il.id +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 +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) + $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, + ) + } + } + /** * 排程將過期 lot line 標成 UNAVAILABLE 後呼叫:對仍 open 的盤點輪次, * soft-delete 對應且尚未 completed 的 [StockTakeRecord]/[StockTakeLine], diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt index a3b5489d..59a81802 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt @@ -325,40 +325,72 @@ open class StockTakeService( fun createStockTakeForItems( itemIds: List, + inventoryLotLineIds: List? = null, stockTakeRoundName: String? = null, planStart: LocalDateTime? = null, ): Map { log.info("--------- Start - Create Stock Take for Items -------") - val wanted = itemIds.filter { it > 0L }.distinct() - if (wanted.isEmpty()) { - return mapOf("_" to "Error: No items selected") - } - val lines = inventoryLotLineRepository.findAllByItemIdIn(wanted).filter { ill -> - !ill.deleted && - ill.status == InventoryLotLineStatus.AVAILABLE && - ill.warehouse?.stockTakeSection?.trim()?.isNotBlank() == true - }.filter { ill -> - val availableQty = (ill.inQty ?: BigDecimal.ZERO) - .subtract(ill.outQty ?: BigDecimal.ZERO) - availableQty.compareTo(BigDecimal.ZERO) > 0 + val busyLotKeys = stockTakeRecordService.busyOpenLotWarehouseKeys() + val busySections = stockTakeRecordService.busyStockTakeSections() + .map { it.lowercase() } + .toSet() + + val lineIds = inventoryLotLineIds.orEmpty().filter { it > 0L }.distinct() + val lines = if (lineIds.isNotEmpty()) { + inventoryLotLineRepository.findAllById(lineIds).filter { ill -> + !ill.deleted && + ill.status == InventoryLotLineStatus.AVAILABLE && + ill.warehouse?.stockTakeSection?.trim()?.isNotBlank() == true + }.filter { ill -> + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + availableQty.compareTo(BigDecimal.ZERO) > 0 + }.filter { ill -> + val lotId = ill.inventoryLot?.id + val warehouseId = ill.warehouse?.id + lotId != null && warehouseId != null && (lotId to warehouseId) !in busyLotKeys + } + } else { + val wanted = itemIds.filter { it > 0L }.distinct() + if (wanted.isEmpty()) { + return mapOf("_" to "Error: No items selected") + } + inventoryLotLineRepository.findAllByItemIdIn(wanted).filter { ill -> + !ill.deleted && + ill.status == InventoryLotLineStatus.AVAILABLE && + ill.warehouse?.stockTakeSection?.trim()?.isNotBlank() == true + }.filter { ill -> + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + availableQty.compareTo(BigDecimal.ZERO) > 0 + }.filter { ill -> + val lotId = ill.inventoryLot?.id + val warehouseId = ill.warehouse?.id + val section = ill.warehouse?.stockTakeSection?.trim()?.lowercase().orEmpty() + // 整區 section 輪 open → 該區全部不可進 item 輪;其餘以 lot 重疊排除 + section.isNotEmpty() && + section !in busySections && + lotId != null && + warehouseId != null && + (lotId to warehouseId) !in busyLotKeys + } } if (lines.isEmpty()) { return mapOf("_" to "Error: No available lots for selected items") } + val itemIdFilter = lines.mapNotNull { it.inventoryLot?.item?.id }.toSet() val bySection = lines.groupBy { it.warehouse!!.stockTakeSection!!.trim() } val batchPlanStart = planStart ?: LocalDateTime.now() val batchPlanEnd = batchPlanStart.plusDays(1) - val busySections = stockTakeRecordService.busyStockTakeSections() - .map { it.lowercase() } - .toSet() val roundId = stockTakeRepository.findMaxStockTakeRoundId() + 1 val roundName = stockTakeRoundName?.trim()?.takeIf { it.isNotBlank() } var placeholderStockTakerId: Long? = null val result = mutableMapOf() bySection.forEach { (section, sectionLines) -> + // 僅「依區域」open 整區不可再建;item 輪佔用改由 placeholder 略過 lot if (busySections.contains(section.lowercase())) { result[section] = "Skipped: in progress" - log.info("Skipped item stock take for section $section: already in an open round") + log.info("Skipped item stock take for section $section: section round in progress") return@forEach } try { @@ -381,11 +413,20 @@ open class StockTakeService( if (placeholderStockTakerId == null) { placeholderStockTakerId = stockTakeRecordService.resolvePlaceholderStockTakerId() } - stockTakeRecordService.createPlaceholderStockTakeRecordsForStockTake( - savedStockTake, - placeholderStockTakerId!!, - wanted.toSet(), - ) + // 有明確 lot line 時只建這些 lot(再經 busy 過濾);否則依 item 集合建 + if (lineIds.isNotEmpty()) { + stockTakeRecordService.createPlaceholderStockTakeRecordsForLotLines( + savedStockTake, + placeholderStockTakerId!!, + sectionLines.mapNotNull { it.id }, + ) + } else { + stockTakeRecordService.createPlaceholderStockTakeRecordsForStockTake( + savedStockTake, + placeholderStockTakerId!!, + itemIdFilter, + ) + } result[section] = "Created: ${savedStockTake.code} (${sectionLines.size} lots)" log.info("Created item stock take for section $section: ${savedStockTake.code}, roundId=$roundId") } catch (e: Exception) { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt index dae4ec44..4f3f3279 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt @@ -71,8 +71,10 @@ class StockTakeController( @RequestBody body: CreateStockTakeForItemsRequest, ): ResponseEntity> { val itemIds = body.itemIds.orEmpty().filter { it > 0L }.distinct() + val lotLineIds = body.inventoryLotLineIds.orEmpty().filter { it > 0L }.distinct() val result = stockTakeService.createStockTakeForItems( itemIds = itemIds, + inventoryLotLineIds = lotLineIds.takeIf { it.isNotEmpty() }, stockTakeRoundName = body.stockTakeRoundName, planStart = body.planStart, ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt index 27ccf41c..bce65f5c 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt @@ -202,6 +202,25 @@ class StockTakeRecordController( return stockOutRecordService.busyStockTakeSections() } + /** 建立 item 輪:搜尋有可用 lot 的貨品(可依來源 PP/PF/其他) */ + @GetMapping("/searchItemsForCreate") + fun searchItemsForCreate( + @RequestParam(required = false) code: String?, + @RequestParam(required = false) name: String?, + @RequestParam(required = false) lotOrigin: String?, + ): List { + return stockOutRecordService.searchItemsForStockTakeCreate(code, name, lotOrigin) + } + + /** 建立 item 輪:依貨品列出可選 lot(含 busy 標記;可依來源) */ + @GetMapping("/availableLotsForCreate") + fun availableLotsForCreate( + @RequestParam itemIds: List?, + @RequestParam(required = false) lotOrigin: String?, + ): List { + return stockOutRecordService.availableLotsForStockTakeCreate(itemIds.orEmpty(), lotOrigin) + } + @PostMapping("/closeRound") fun closeStockTakeRound( @RequestBody body: CloseStockTakeRoundRequest diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt index e20b711e..327bfa95 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt @@ -18,6 +18,8 @@ data class CloseStockTakeRoundRequest( @JsonIgnoreProperties(ignoreUnknown = true) data class CreateStockTakeForItemsRequest( val itemIds: List? = null, + /** 優先:依勾選的 inventory_lot_line;有值時忽略僅用 itemIds 帶全部 lot 的舊行為 */ + val inventoryLotLineIds: List? = null, val stockTakeRoundName: String? = null, val planStart: LocalDateTime? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt index c3dc0888..86c9634f 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt @@ -37,6 +37,30 @@ data class StockTakeRoundCardsRes( val completedYearMonths: List, ) +/** 建立 item 盤點輪時可選的 lot 明細 */ +data class StockTakeCreateLotOption( + val inventoryLotLineId: Long, + val itemId: Long, + val itemCode: String?, + val itemName: String?, + val lotId: Long, + val lotNo: String?, + val expiryDate: String?, + val warehouseId: Long, + val warehouseCode: String?, + val stockTakeSection: String?, + val storeId: String?, + val availableQty: BigDecimal, + val busy: Boolean, +) + +/** 建立 item 盤點輪時可選貨品(僅有可用 lot 者) */ +data class StockTakeCreateItemOption( + val id: Long, + val code: String?, + val name: String?, +) + data class AllPickedStockTakeListReponse( val id: Long, val stockTakeSession: String,