Преглед на файлове

update ui

stockTakeUpdate
CANCERYS\kw093 преди 1 седмица
родител
ревизия
e0f898eeb0
променени са 6 файла, в които са добавени 437 реда и са изтрити 25 реда
  1. +328
    -4
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  2. +62
    -21
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt
  3. +2
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt
  4. +19
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt
  5. +2
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt
  6. +24
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt

+ 328
- 4
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<Long>,
) {
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<StockTakeRecord>(inventoryLotLines.size)
val illPerRecord = ArrayList<InventoryLotLine>(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<String> {
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<Pair<Long, Long>> {
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<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)
// 來源已選 PP/PF/其他 → 可只依來源列出;來源=全部則仍需編號或名稱
if (codeTrim.isEmpty() && nameTrim.isEmpty() && !hasOrigin) return emptyList()

val args = mutableMapOf<String, Any>()
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<Long>,
lotOrigin: String? = null,
): List<StockTakeCreateLotOption> {
val wanted = itemIds.filter { it > 0L }.distinct()
if (wanted.isEmpty()) return emptyList()
val busy = busyOpenLotWarehouseKeys()

val args = mutableMapOf<String, Any>()
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],


+ 62
- 21
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt Целия файл

@@ -325,40 +325,72 @@ open class StockTakeService(

fun createStockTakeForItems(
itemIds: List<Long>,
inventoryLotLineIds: List<Long>? = null,
stockTakeRoundName: String? = null,
planStart: LocalDateTime? = null,
): Map<String, String> {
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<String, String>()
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) {


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt Целия файл

@@ -71,8 +71,10 @@ class StockTakeController(
@RequestBody body: CreateStockTakeForItemsRequest,
): ResponseEntity<Map<String, String>> {
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,
)


+ 19
- 0
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<StockTakeCreateItemOption> {
return stockOutRecordService.searchItemsForStockTakeCreate(code, name, lotOrigin)
}

/** 建立 item 輪:依貨品列出可選 lot(含 busy 標記;可依來源) */
@GetMapping("/availableLotsForCreate")
fun availableLotsForCreate(
@RequestParam itemIds: List<Long>?,
@RequestParam(required = false) lotOrigin: String?,
): List<StockTakeCreateLotOption> {
return stockOutRecordService.availableLotsForStockTakeCreate(itemIds.orEmpty(), lotOrigin)
}

@PostMapping("/closeRound")
fun closeStockTakeRound(
@RequestBody body: CloseStockTakeRoundRequest


+ 2
- 0
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<Long>? = null,
/** 優先:依勾選的 inventory_lot_line;有值時忽略僅用 itemIds 帶全部 lot 的舊行為 */
val inventoryLotLineIds: List<Long>? = null,
val stockTakeRoundName: String? = null,
val planStart: LocalDateTime? = null,
)

+ 24
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt Целия файл

@@ -37,6 +37,30 @@ data class StockTakeRoundCardsRes(
val completedYearMonths: List<String>,
)

/** 建立 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,


Зареждане…
Отказ
Запис