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