kelvin.yau 2 週之前
父節點
當前提交
8f540bd516
共有 4 個文件被更改,包括 319 次插入1 次删除
  1. +10
    -1
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt
  2. +292
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  3. +4
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt
  4. +13
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt

+ 10
- 1
src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt 查看文件

@@ -5,7 +5,7 @@ import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository
import java.time.LocalDate
import java.util.Optional
@Repository
interface StockLedgerRepository: AbstractRepository<StockLedger, Long> {
@@ -49,4 +49,13 @@ interface StockLedgerRepository: AbstractRepository<StockLedger, Long> {
@Param("startDate") startDate: LocalDate?,
@Param("endDate") endDate: LocalDate?
): Long


@Query("""
SELECT sl FROM StockLedger sl
WHERE sl.itemId = :itemId
AND sl.deleted = false
ORDER BY sl.date DESC, sl.id DESC
""")
fun findLatestByItemId(@Param("itemId") itemId: Long): StockLedger?
}

+ 292
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt 查看文件

@@ -1090,6 +1090,9 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
operation = "pick"
)
)
if (submitQty > BigDecimal.ZERO) {
createStockLedgerForPickDelta(line.stockOutLineId, submitQty)
}
}
try {
val stockOutLine = stockOutLines[line.stockOutLineId]
@@ -1259,4 +1262,293 @@ private fun createStockLedgerForStockOut(stockOutLine: StockOutLine) {

return savedStockOutLine
}


private fun createStockLedgerForPickDelta(stockOutLineId: Long, deltaQty: BigDecimal) {
if (deltaQty <= BigDecimal.ZERO) return
val sol = stockOutLineRepository.findById(stockOutLineId).orElse(null) ?: return
val item = sol.item ?: return
val inventory = inventoryRepository.findAllByItemIdAndDeletedIsFalse(item.id!!)
.firstOrNull() ?: return
val latestLedger = stockLedgerRepository.findLatestByItemId(item.id!!)
val previousBalance = latestLedger?.balance
?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val newBalance = previousBalance - deltaQty.toDouble()
println(" Creating stock ledger: previousBalance=$previousBalance, deltaQty=$deltaQty, newBalance=$newBalance")
val ledger = StockLedger().apply {
this.stockOutLine = sol
this.inventory = inventory
this.inQty = null
this.outQty = deltaQty.toDouble()
this.balance = newBalance // ✅ 使用计算后的新 balance
this.type = "NOR"
this.itemId = item.id
this.itemCode = item.code
this.date = LocalDate.now()
}
stockLedgerRepository.saveAndFlush(ledger)
}

@Transactional(rollbackFor = [Exception::class])
open fun batchScan(request: com.ffii.fpsms.modules.stock.web.model.BatchScanRequest): MessageResponse {
val startTime = System.currentTimeMillis()
println("=== BATCH SCAN START ===")
println("Start time: ${java.time.LocalDateTime.now()}")
println("Request lines count: ${request.lines.size}")

if (request.lines.isEmpty()) {
return MessageResponse(
id = null,
name = "No lines",
code = "EMPTY",
type = "batch_scan",
message = "No lines to scan",
errorPosition = null
)
}

val errors = mutableListOf<String>()
val processedIds = mutableListOf<Long>()
val createdIds = mutableListOf<Long>()

try {
// 1) Bulk load all pick order lines
val pickOrderLineIds = request.lines.map { it.pickOrderLineId }.distinct()
println("Loading ${pickOrderLineIds.size} pick order lines...")
val pickOrderLines = pickOrderLineRepository.findAllById(pickOrderLineIds).associateBy { it.id }

// 2) Bulk load all inventory lot lines (if any)
val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId }.distinct()
println("Loading ${lotLineIds.size} inventory lot lines...")
val inventoryLotLines = if (lotLineIds.isNotEmpty()) {
inventoryLotLineRepository.findAllById(lotLineIds).associateBy { it.id }
} else {
emptyMap()
}

val itemIds = request.lines.map { it.itemId }.distinct()
println("Loading ${itemIds.size} items...")
val items = itemRepository.findAllById(itemIds).associateBy { it.id }

// ✅ 简化:直接使用 stockOutLineId(如果提供)来获取 StockOut
// 批量加载所有提供的 stockOutLineId 对应的 StockOutLine 和 StockOut
val providedStockOutLineIds = request.lines.mapNotNull { it.stockOutLineId }.distinct()
println("Loading ${providedStockOutLineIds.size} stock out lines by ID...")
val stockOutLinesById = if (providedStockOutLineIds.isNotEmpty()) {
stockOutLineRepository.findAllById(providedStockOutLineIds)
.associateBy { it.id }
} else {
emptyMap()
}
// 从 StockOutLine 中提取 StockOut
val stockOutsById = stockOutLinesById.values
.mapNotNull { it.stockOut?.id?.let { id -> id to it.stockOut } }
.toMap()
println("Loaded ${stockOutsById.size} stock outs from provided stockOutLineIds")

// ✅ 对于没有 stockOutLineId 的情况,使用 consoCode 或 pickOrderLineId 查找
val consoCodes = request.lines
.filter { it.stockOutLineId == null }
.map { it.pickOrderConsoCode }
.distinct()
.filter { it.isNotBlank() }
println("Loading ${consoCodes.size} stock outs by consoCode (for lines without stockOutLineId)...")
val stockOutsByConsoCode = consoCodes.mapNotNull { consoCode ->
stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null)?.let { consoCode to it }
}.toMap()

// ✅ 对于既没有 stockOutLineId 也没有 consoCode 的情况,通过 pickOrderLineId 获取
val pickOrderLineIdsNeedingStockOut = request.lines
.filter { line ->
line.stockOutLineId == null &&
(line.pickOrderConsoCode.isBlank() || !stockOutsByConsoCode.containsKey(line.pickOrderConsoCode))
}
.map { it.pickOrderLineId }
.distinct()
val stockOutsByPickOrderLineId = mutableMapOf<Long, StockOut>()
if (pickOrderLineIdsNeedingStockOut.isNotEmpty()) {
println("Batch loading ${pickOrderLineIdsNeedingStockOut.size} stock outs by pickOrderLineId...")
val sql = """
SELECT pol.id as pickOrderLineId, so.id as stockOutId
FROM stock_out so
JOIN pick_order po ON po.consoCode = so.consoPickOrderCode
JOIN pick_order_line pol ON pol.poId = po.id
WHERE pol.id IN (:pickOrderLineIds)
""".trimIndent()
val results = jdbcDao.queryForList(
sql,
mapOf("pickOrderLineIds" to pickOrderLineIdsNeedingStockOut)
)
results.forEach { row ->
val pickOrderLineId = row["pickOrderLineId"] as? Long
val stockOutId = row["stockOutId"] as? Long
if (pickOrderLineId != null && stockOutId != null) {
val stockOut = stockOutRepository.findById(stockOutId).orElse(null)
if (stockOut != null) {
stockOutsByPickOrderLineId[pickOrderLineId] = stockOut
}
}
}
}
println("Loaded ${stockOutsById.size} stock outs by ID, ${stockOutsByConsoCode.size} by consoCode, ${stockOutsByPickOrderLineId.size} by pickOrderLineId")

// 5) Check existing stock out lines for each pick order line and lot
val existingStockOutLines = mutableMapOf<Pair<Long, Long?>, StockOutLine>()
request.lines.forEach { line ->
// ✅ 如果已有 stockOutLineId,直接使用它
if (line.stockOutLineId != null) {
val existing = stockOutLinesById[line.stockOutLineId]
if (existing != null) {
existingStockOutLines[Pair(line.pickOrderLineId, line.inventoryLotLineId)] = existing
}
} else if (line.inventoryLotLineId != null) {
// 如果没有 stockOutLineId,通过 pickOrderLineId 和 inventoryLotLineId 查询
val existing = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
line.pickOrderLineId,
line.inventoryLotLineId!!
)
if (existing.isNotEmpty()) {
existingStockOutLines[Pair(line.pickOrderLineId, line.inventoryLotLineId)] = existing.first()
}
}
}
println("Found ${existingStockOutLines.size} existing stock out lines")

// 6) Process each line
request.lines.forEach { line ->
try {
println("Processing line: pickOrderLineId=${line.pickOrderLineId}, lotNo=${line.lotNo}, stockOutLineId=${line.stockOutLineId}")

val pickOrderLine = pickOrderLines[line.pickOrderLineId]
?: throw IllegalStateException("PickOrderLine ${line.pickOrderLineId} not found")

val item = items[line.itemId]
?: throw IllegalStateException("Item ${line.itemId} not found")

// ✅ 优先使用 stockOutLineId 获取 StockOut
val stockOut = when {
line.stockOutLineId != null -> {
// 从已加载的 stockOutLines 中获取
val stockOutLine = stockOutLinesById[line.stockOutLineId]
stockOutLine?.stockOut ?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found or has no StockOut")
}
line.pickOrderConsoCode.isNotBlank() && stockOutsByConsoCode.containsKey(line.pickOrderConsoCode) -> {
stockOutsByConsoCode[line.pickOrderConsoCode]!!
}
else -> {
stockOutsByPickOrderLineId[line.pickOrderLineId]
?: throw IllegalStateException("StockOut not found for pickOrderLineId: ${line.pickOrderLineId} (consoCode: '${line.pickOrderConsoCode}', stockOutLineId: ${line.stockOutLineId})")
}
}

// Check if stock out line already exists
val existingKey = Pair(line.pickOrderLineId, line.inventoryLotLineId)
val existingStockOutLine = existingStockOutLines[existingKey]

if (existingStockOutLine != null) {
// Update existing stock out line to 'checked'
println(" Updating existing stock out line ${existingStockOutLine.id}")
existingStockOutLine.status = StockOutLineStatus.CHECKED.status
existingStockOutLine.startTime = LocalDateTime.now()
stockOutLineRepository.saveAndFlush(existingStockOutLine)
processedIds += existingStockOutLine.id!!
println(" ✓ Updated stock out line ${existingStockOutLine.id}")
} else {
// Create new stock out line
println(" Creating new stock out line")
val inventoryLotLine = line.inventoryLotLineId?.let { inventoryLotLines[it] }
?: throw IllegalStateException("InventoryLotLine ${line.inventoryLotLineId} not found")

// Update pick order line status to PICKING
pickOrderLine.status = PickOrderLineStatus.PICKING
pickOrderLineRepository.saveAndFlush(pickOrderLine)

val stockOutLine = StockOutLine().apply {
this.item = item
this.qty = 0.0
this.stockOut = stockOut
this.inventoryLotLine = inventoryLotLine
this.pickOrderLine = pickOrderLine
this.status = StockOutLineStatus.CHECKED.status
this.type = "Nor"
this.startTime = LocalDateTime.now()
}

val savedStockOutLine = saveAndFlush(stockOutLine)
createStockLedgerForStockOut(savedStockOutLine)
createdIds += savedStockOutLine.id!!
processedIds += savedStockOutLine.id!!
println(" ✓ Created stock out line ${savedStockOutLine.id}")
}
} catch (e: Exception) {
println(" ✗ Error processing line pickOrderLineId=${line.pickOrderLineId}: ${e.message}")
e.printStackTrace()
errors += "pickOrderLineId=${line.pickOrderLineId}, lotNo=${line.lotNo}: ${e.message}"
}
}

val msg = if (errors.isEmpty()) {
"Batch scan success (${processedIds.size} lines processed, ${createdIds.size} created)."
} else {
"Batch scan partial success (${processedIds.size} lines processed, ${createdIds.size} created), errors: ${errors.joinToString("; ")}"
}

val totalTime = System.currentTimeMillis() - startTime
println("Processed: ${processedIds.size}/${request.lines.size} items")
println("Created: ${createdIds.size} new stock out lines")
println("Total time: ${totalTime}ms (${totalTime / 1000.0}s)")
println("Average time per item: ${if (processedIds.isNotEmpty()) totalTime / processedIds.size else 0}ms")
println("End time: ${java.time.LocalDateTime.now()}")
println("=== BATCH SCAN END ===")

return MessageResponse(
id = null,
name = "batch_scan",
code = if (errors.isEmpty()) "SUCCESS" else "PARTIAL_SUCCESS",
type = "batch_scan",
message = msg,
errorPosition = null,
entity = mapOf(
"processedIds" to processedIds,
"createdIds" to createdIds,
"errors" to errors
)
)
} catch (e: Exception) {
println("=== BATCH SCAN ERROR ===")
println("Error: ${e.message}")
e.printStackTrace()
return MessageResponse(
id = null,
name = "batch_scan",
code = "ERROR",
type = "batch_scan",
message = "Error: ${e.message}",
errorPosition = null,
entity = mapOf(
"processedIds" to processedIds,
"createdIds" to createdIds,
"errors" to listOf(e.message ?: "Unknown error")
)
)
}
}
}

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt 查看文件

@@ -81,4 +81,8 @@ class StockOutLineController(
)
}
}
@PostMapping("/batchScan")
fun batchScan(@Valid @RequestBody request: com.ffii.fpsms.modules.stock.web.model.BatchScanRequest): MessageResponse {
return stockOutLineService.batchScan(request)
}
}

+ 13
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt 查看文件

@@ -83,4 +83,17 @@ data class QrPickBatchSubmitRequest(
val userId: Long,
val lines: List<QrPickSubmitLineRequest>
)
data class BatchScanLineRequest(
val pickOrderLineId: Long,
val inventoryLotLineId: Long?, // 如果有 lot,提供 lotId;如果没有则为 null
val pickOrderConsoCode: String,
val lotNo: String?, // 用于日志和验证
val itemId: Long,
val itemCode: String,
val stockOutLineId: Long? = null // ✅ 新增:如果已有 stockOutLineId,直接使用
)

data class BatchScanRequest(
val userId: Long,
val lines: List<BatchScanLineRequest>
)

Loading…
取消
儲存