From 92b2e35117a1d4b125f1cf185f29cf238cc59d8f Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 27 Apr 2026 14:15:22 +0800 Subject: [PATCH] update expiry lot batch handle --- .../service/PickExecutionIssueService.kt | 58 ++++----- .../entity/InventoryLotLineRepository.kt | 14 +++ .../stock/service/StockOutLineService.kt | 115 ++++++++++++++++++ .../stock/web/model/SaveStockOutRequest.kt | 16 +++ 4 files changed, 172 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt index 0fe2094..c38d793 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt @@ -42,13 +42,15 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine import com.ffii.fpsms.modules.master.entity.ItemsRepository import com.ffii.fpsms.modules.master.entity.ItemUomRespository import com.ffii.fpsms.modules.common.CodeGenerator - +import com.ffii.fpsms.modules.stock.web.model.BatchStockOutResult import com.ffii.fpsms.modules.stock.entity.StockLedger import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository import org.springframework.beans.factory.annotation.Value +import com.ffii.fpsms.modules.stock.web.model.BatchStockOutRequest +import com.ffii.fpsms.modules.stock.web.model.BatchStockOutLineRequest @Service open class PickExecutionIssueService( private val pickExecutionIssueRepository: PickExecutionIssueRepository, @@ -2202,54 +2204,48 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse { open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageResponse { try { val lotLines = inventoryLotLineRepository.findAllById(request.lotLineIds) - .filter { + .filter { val lot = it.inventoryLot val today = LocalDate.now() - lot?.expiryDate != null && lot.expiryDate!!.isBefore(today) && + lot?.expiryDate != null && + lot.expiryDate!!.isBefore(today) && (it.inQty ?: BigDecimal.ZERO) != (it.outQty ?: BigDecimal.ZERO) } - + if (lotLines.isEmpty()) { return MessageResponse( - id = null, - name = "Error", - code = "EMPTY", - type = "stock_issue", - message = "No valid expiry items to submit", - errorPosition = null + id = null, name = "Error", code = "EMPTY", type = "stock_issue", + message = "No valid expiry items to submit", errorPosition = null ) } - + val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L - - lotLines.forEach { lotLine -> - val remainingQty = (lotLine.inQty ?: BigDecimal.ZERO).subtract(lotLine.outQty ?: BigDecimal.ZERO) - - stockOutLineService.createStockOut( - StockOutRequest( - inventoryLotLineId = lotLine.id!!, - qty = remainingQty.toDouble(), - type = "Expiry" + val batchReq = BatchStockOutRequest( + type = "Expiry", + handler = handler, + lines = lotLines.map { ll -> + val remaining = (ll.inQty ?: BigDecimal.ZERO).subtract(ll.outQty ?: BigDecimal.ZERO) + BatchStockOutLineRequest( + inventoryLotLineId = ll.id!!, + qty = remaining.toDouble() ) - ) - } - + } + ) + + val result = stockOutLineService.createStockOutBatch(batchReq) + return MessageResponse( - id = null, + id = result.stockOutId, name = "Success", code = "SUCCESS", type = "stock_issue", - message = "Successfully submitted ${lotLines.size} expiry item(s)", + message = "Successfully submitted ${result.processedCount} expiry item(s), skipped ${result.skippedCount}", errorPosition = null ) } catch (e: Exception) { return MessageResponse( - id = null, - name = "Error", - code = "ERROR", - type = "stock_issue", - message = "Failed to submit expiry items: ${e.message}", - errorPosition = null + id = null, name = "Error", code = "ERROR", type = "stock_issue", + message = "Failed to submit expiry items: ${e.message}", errorPosition = null ) } } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt index 6c453cf..d82a0e5 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt @@ -262,4 +262,18 @@ WHERE ill.id = :id """ ) fun countByStatusAndDeletedIsFalse(@Param("status") status: InventoryLotLineStatus): Long + + @EntityGraph( + type = EntityGraph.EntityGraphType.FETCH, + attributePaths = ["inventoryLot", "inventoryLot.item", "warehouse"] +) +@Query(""" + SELECT ill + FROM InventoryLotLine ill + WHERE ill.id IN :ids + AND ill.deleted = false +""") +fun findAllByIdInAndDeletedFalseWithRefs( + @Param("ids") ids: Collection +): List } \ 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 5ca3c88..f1c43b9 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 @@ -2223,4 +2223,119 @@ fun applyStockOutLineDelta( return savedSol } +@Transactional(rollbackFor = [Exception::class]) +open fun createStockOutBatch(request: BatchStockOutRequest): BatchStockOutResult { + if (request.lines.isEmpty()) return BatchStockOutResult(null, 0, 0) + + val currentUserId = request.handler ?: SecurityUtils.getUser().orElseThrow().id ?: 0L + val lotLineIds = request.lines.map { it.inventoryLotLineId }.distinct() + + // 1) 一次 preload lotLine + item + warehouse + val lotLines = inventoryLotLineRepository + .findAllByIdInAndDeletedFalseWithRefs(lotLineIds) + + val lotLineMap = lotLines.associateBy { it.id!! } + + // 2) 构造 header(每批一个,避免每笔新建) + val stockOutHeader = StockOut().apply { + this.type = request.type + this.completeDate = LocalDateTime.now() + this.handler = currentUserId + this.status = StockOutStatus.COMPLETE.status + } + val savedHeader = stockOutRepository.save(stockOutHeader) + + val now = LocalDateTime.now() + val today = LocalDate.now() + + val lotLinesToUpdate = mutableListOf() + val stockOutLinesToInsert = mutableListOf() + val ledgersToInsert = mutableListOf() + var skipped = 0 + + // 3) 内存校验 + 组装对象(不在循环里 findById / saveAndFlush) + request.lines.forEach { lineReq -> + val lotLine = lotLineMap[lineReq.inventoryLotLineId] + if (lotLine == null) { + skipped++ + return@forEach + } + + val qtyBd = BigDecimal.valueOf(lineReq.qty) + if (qtyBd <= BigDecimal.ZERO) { + skipped++ + return@forEach + } + + // 可用量校验(按你现有规则) + val inQty = lotLine.inQty ?: BigDecimal.ZERO + val outQty = lotLine.outQty ?: BigDecimal.ZERO + val holdQty = lotLine.holdQty ?: BigDecimal.ZERO + val available = inQty.subtract(outQty) // Expiry 通常吃剩余量,可视业务改成 -holdQty + if (qtyBd > available) { + skipped++ + return@forEach + } + + // 更新 lot line + lotLine.outQty = outQty.add(qtyBd) + lotLine.holdQty = holdQty.subtract(qtyBd).coerceAtLeast(BigDecimal.ZERO) + lotLine.status = inventoryLotLineService.deriveInventoryLotLineStatus( + lotLine.status, lotLine.inQty, lotLine.outQty, lotLine.holdQty + ) + lotLinesToUpdate += lotLine + + val item = lotLine.inventoryLot?.item ?: run { + skipped++ + return@forEach + } + + // 组装 stock_out_line + val sol = StockOutLine().apply { + this.item = item + this.qty = lineReq.qty + this.stockOut = savedHeader + this.inventoryLotLine = lotLine + this.status = StockOutLineStatus.COMPLETE.status + this.pickTime = now + this.handledBy = currentUserId + this.type = request.type + } + stockOutLinesToInsert += sol + } + + // 4) 批量写(关键) + inventoryLotLineRepository.saveAll(lotLinesToUpdate) + val savedLines = stockOutLineRepository.saveAll(stockOutLinesToInsert) + + // 5) 批量组装 ledger(避免每笔 createStockLedgerForStockOut) + savedLines.forEach { sol -> + val item = sol.item ?: return@forEach + val inv = itemUomService.findInventoryForItemBaseUom(item.id!!) ?: return@forEach + val delta = BigDecimal.valueOf(sol.qty ?: 0.0) + val prevBalance = inv.onHandQty?.toDouble() ?: 0.0 +val newBalance = prevBalance - delta.toDouble() + + ledgersToInsert += StockLedger().apply { + this.stockOutLine = sol + this.inventory = inv + this.inQty = null + this.outQty = delta.toDouble() + this.balance = newBalance + this.type = request.type + this.itemId = item.id + this.itemCode = item.code + this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id + ?: inv.uom?.id + this.date = today + } + } + stockLedgerRepository.saveAll(ledgersToInsert) + + return BatchStockOutResult( + stockOutId = savedHeader.id, + processedCount = savedLines.size, + skippedCount = skipped + ) +} } \ 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 b8aa0eb..2841f08 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 @@ -102,3 +102,19 @@ data class BatchScanRequest( val userId: Long, val lines: List ) +data class BatchStockOutRequest( + val type: String, // "Expiry" / "Miss" / "Bad" + val handler: Long?, + val lines: List +) + +data class BatchStockOutLineRequest( + val inventoryLotLineId: Long, + val qty: Double +) + +data class BatchStockOutResult( + val stockOutId: Long?, + val processedCount: Int, + val skippedCount: Int = 0 +) \ No newline at end of file