From 221e51f8185302c1ec2a9576e0fa71b1f24aa187 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 20 Jan 2026 09:52:31 +0800 Subject: [PATCH] update batch scan --- .../stock/entity/StockLedgerRepository.kt | 11 +- .../stock/service/StockOutLineService.kt | 292 ++++++++++++++++++ .../stock/web/StockOutLineController.kt | 4 + .../stock/web/model/SaveStockOutRequest.kt | 13 + 4 files changed, 319 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt index 04e4bc7..4c9b8e5 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt +++ b/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 { @@ -49,4 +49,13 @@ interface StockLedgerRepository: AbstractRepository { @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? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 065749f..d3c76bc 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/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() + val processedIds = mutableListOf() + val createdIds = mutableListOf() + + 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() + 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, 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") + ) + ) + } +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt index 3e93837..5f1b4f0 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt +++ b/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) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt index 62013a4..495237e 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt +++ b/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 ) +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 +)