From 2fe4480a1137a9e0e643f0acffc66f6afe8d7e02 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 24 Mar 2026 21:37:29 +0800 Subject: [PATCH] update --- .../service/PickExecutionIssueService.kt | 326 ++++-------------- .../stock/service/StockOutLineService.kt | 207 ++++++++--- .../stock/service/StockTakeRecordService.kt | 3 + .../stock/web/model/SaveStockOutRequest.kt | 3 +- .../stock/web/model/StockTakeRecordReponse.kt | 3 + 5 files changed, 236 insertions(+), 306 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 a78fc7c..432675b 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 @@ -12,7 +12,6 @@ import com.ffii.fpsms.modules.stock.entity.InventoryLotLine import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository import com.ffii.fpsms.modules.stock.entity.InventoryRepository import com.ffii.fpsms.modules.stock.service.StockOutLineService -import org.springframework.context.annotation.Lazy import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService @@ -32,6 +31,7 @@ import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus +import com.ffii.fpsms.modules.stock.web.model.StockOutRequest import com.ffii.fpsms.modules.stock.web.model.StockOutStatus import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus @@ -59,7 +59,7 @@ open class PickExecutionIssueService( private val pickOrderRepository: PickOrderRepository, private val jdbcDao: JdbcDao, private val stockOutRepository: StockOutRepository, - @Lazy private val stockOutLineService: StockOutLineService, + private val stockOutLineService: StockOutLineService, private val pickOrderLineRepository: PickOrderLineRepository, private val doPickOrderService: DoPickOrderService, private val joPickOrderRepository: JoPickOrderRepository, @@ -1673,15 +1673,6 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse { } val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L - val stockOut = createIssueStockOutHeader("MISS_ITEM", issue.issueRemark, handler) - - val pickOrderLine = issue.pickOrderLineId?.let { - pickOrderLineRepository.findById(it).orElse(null) - } - - val lotLine = issue.lotId?.let { - inventoryLotLineRepository.findById(it).orElse(null) - } val item = itemsRepository.findById(issue.itemId).orElse(null) ?: return MessageResponse( @@ -1693,50 +1684,18 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse { errorPosition = null ) - // ✅ 修复:在创建和保存 stockOutLine 之前获取 inventory 的当前 onHandQty - // 必须在任何可能触发数据库触发器更新 inventory 的操作之前获取 - val inventoryBeforeUpdate = inventoryRepository.findByItemId(issue.itemId).orElse(null) - val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() - - println("=== submitMissItem: Before update ===") - println("Item ID: ${issue.itemId}") - println("Issue Qty: ${issueQty}") - println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") - println("=====================================") - - // 修改:使用 issueQty 创建 stock_out_line - val stockOutLine = StockOutLine().apply { - this.stockOut = stockOut - this.inventoryLotLine = lotLine - this.item = item - this.qty = issueQty.toDouble() // 使用 issueQty 而不是 missQty - this.status = StockOutLineStatus.COMPLETE.status - this.pickOrderLine = pickOrderLine - this.type = "Miss" - } - val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - - // 修改:使用 issueQty 更新 inventory_lot_line - // 注意:这会触发数据库触发器更新 inventory,所以必须在获取 onHandQtyBeforeUpdate 之后调用 - if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = true) // 使用 issueQty - } - + val lotLineId = issue.lotId + ?: throw IllegalArgumentException("Issue ${issue.id} has no lot line id") + val savedStockOutLine = stockOutLineService.createStockOut( + StockOutRequest( + inventoryLotLineId = lotLineId, + qty = issueQty.toDouble(), + type = "Miss" + ) + ) markIssueHandled(issue, handler) - - // ✅ 修复:使用更新前的 onHandQty 计算 balance - // balance = 更新前的 onHandQty - 本次出库数量 - val balance = onHandQtyBeforeUpdate - issueQty.toDouble() - - println("=== submitMissItem: Creating stock ledger ===") - println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") - println("Issue Qty (OutQty): ${issueQty.toDouble()}") - println("Calculated Balance: ${balance}") - println("=============================================") - - createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Miss") return MessageResponse( - id = stockOut.id, + id = savedStockOutLine.stockOut?.id, name = "Success", code = "SUCCESS", type = "stock_issue", @@ -1781,15 +1740,6 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { } val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L - val stockOut = createIssueStockOutHeader("BAD_ITEM", issue.issueRemark, handler) - - val pickOrderLine = issue.pickOrderLineId?.let { - pickOrderLineRepository.findById(it).orElse(null) - } - - val lotLine = issue.lotId?.let { - inventoryLotLineRepository.findById(it).orElse(null) - } val item = itemsRepository.findById(issue.itemId).orElse(null) ?: return MessageResponse( @@ -1801,27 +1751,15 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { errorPosition = null ) - // ✅ 修复:在创建和保存 stockOutLine 之前获取 inventory 的当前 onHandQty - val inventoryBeforeUpdate = inventoryRepository.findByItemId(issue.itemId).orElse(null) - val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() - - println("=== submitBadItem: Before update ===") - println("Item ID: ${issue.itemId}") - println("Issue Qty: ${issueQty}") - println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") - println("====================================") - - // 修改:使用 issueQty 创建 stock_out_line - val stockOutLine = StockOutLine().apply { - this.stockOut = stockOut - this.inventoryLotLine = lotLine - this.item = item - this.qty = issueQty.toDouble() // 使用 issueQty 而不是 badItemQty - this.status = StockOutLineStatus.COMPLETE.status - this.pickOrderLine = pickOrderLine - this.type = "Bad" - } - val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + val lotLineId = issue.lotId + ?: throw IllegalArgumentException("Issue ${issue.id} has no lot line id") + val savedStockOutLine = stockOutLineService.createStockOut( + StockOutRequest( + inventoryLotLineId = lotLineId, + qty = issueQty.toDouble(), + type = "Bad" + ) + ) // ✅ 修复:Bad item 的 issueQty 应该重置为 0,而不是累加/减去 // 先重置 issueQty 为 0 @@ -1834,25 +1772,9 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { } } - // 修改:使用 issueQty 更新 inventory_lot_line(更新 outQty) - if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = false) // 使用 issueQty - } - markIssueHandled(issue, handler) - - // ✅ 修复:使用更新前的 onHandQty 计算 balance - val balance = onHandQtyBeforeUpdate - issueQty.toDouble() - - println("=== submitBadItem: Creating stock ledger ===") - println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") - println("Issue Qty (OutQty): ${issueQty.toDouble()}") - println("Calculated Balance: ${balance}") - println("===========================================") - - createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Bad") return MessageResponse( - id = stockOut.id, + id = savedStockOutLine.stockOut?.id, name = "Success", code = "SUCCESS", type = "stock_issue", @@ -1874,6 +1796,8 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse { @Transactional(rollbackFor = [Exception::class]) open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse { try { + println("=== submitExpiryItem START ===") + println("Request lotLineId=${request.lotLineId}, handler=${request.handler}") val lotLine = inventoryLotLineRepository.findById(request.lotLineId).orElse(null) ?: return MessageResponse( id = null, @@ -1901,6 +1825,7 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse { val inQty = lotLine.inQty ?: BigDecimal.ZERO val outQty = lotLine.outQty ?: BigDecimal.ZERO val remainingQty = inQty.subtract(outQty) + println("LotLine=${lotLine.id}, itemId=${lot?.item?.id}, inQty=$inQty, outQty=$outQty, remainingQty=$remainingQty") if (remainingQty <= BigDecimal.ZERO) { return MessageResponse( @@ -1914,7 +1839,6 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse { } val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L - val stockOut = createIssueStockOutHeader("EXPIRY_ITEM", "Expired item removal", handler) val item = lot.item ?: return MessageResponse( @@ -1926,23 +1850,16 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse { errorPosition = null ) - val stockOutLine = StockOutLine().apply { - this.stockOut = stockOut - this.inventoryLotLine = lotLine - this.item = item - this.qty = remainingQty.toDouble() - this.status = StockOutLineStatus.COMPLETE.status - this.type = "Expiry" - } - val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - - - - updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty, isMissItem = false) - // Create stock ledger entry - createStockLedgerForStockOut(savedStockOutLine) + val savedStockOutLine = stockOutLineService.createStockOut( + StockOutRequest( + inventoryLotLineId = lotLine.id!!, + qty = remainingQty.toDouble(), + type = "Expiry" + ) + ) + println("submitExpiryItem createStockOut SUCCESS: stockOutLineId=${savedStockOutLine.id}, qty=$remainingQty") return MessageResponse( - id = stockOut.id, + id = savedStockOutLine.stockOut?.id, name = "Success", code = "SUCCESS", type = "stock_issue", @@ -1950,6 +1867,8 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse { errorPosition = null ) } catch (e: Exception) { + println("submitExpiryItem ERROR: ${e.message}") + e.printStackTrace() return MessageResponse( id = null, name = "Error", @@ -1980,46 +1899,20 @@ open fun batchSubmitMissItem(request: BatchSubmitIssueRequest): MessageResponse } val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L - val stockOut = createIssueStockOutHeader("MISS_ITEM", "Batch miss item submission", handler) issues.forEach { issue -> val issueQty = issue.issueQty ?: BigDecimal.ZERO - val pickOrderLine = issue.pickOrderLineId?.let { - pickOrderLineRepository.findById(it).orElse(null) - } - - val lotLine = issue.lotId?.let { - inventoryLotLineRepository.findById(it).orElse(null) - } - - val item = itemsRepository.findById(issue.itemId).orElse(null) - - // 修改:使用 issueQty - val stockOutLine = StockOutLine().apply { - this.stockOut = stockOut - this.inventoryLotLine = lotLine - this.item = item - this.qty = issueQty.toDouble() // 使用 issueQty - this.status = StockOutLineStatus.COMPLETE.status - this.pickOrderLine = pickOrderLine - this.type = "Miss" - } - val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - - // ✅ 修复:在更新 lot line 之前获取 inventory 的当前 onHandQty,用于计算 balance - val inventoryBeforeUpdate = item?.let { inventoryRepository.findByItemId(it.id!!).orElse(null) } - val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() - - // 修改:使用 issueQty - if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = true) // 使用 issueQty - } - + val lotLineId = issue.lotId + ?: throw IllegalArgumentException("Issue ${issue.id} has no lot line id") + stockOutLineService.createStockOut( + StockOutRequest( + inventoryLotLineId = lotLineId, + qty = issueQty.toDouble(), + type = "Miss" + ) + ) markIssueHandled(issue, handler) - // ✅ 修复:使用更新前的 onHandQty 计算 balance,避免触发器更新后的错误计算 - val balance = onHandQtyBeforeUpdate - issueQty.toDouble() - createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Miss") } return MessageResponse( @@ -2061,37 +1954,19 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse { } val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L - val stockOut = createIssueStockOutHeader("BAD_ITEM", "Batch bad item submission", handler) issues.forEach { issue -> val issueQty = issue.issueQty ?: BigDecimal.ZERO - val pickOrderLine = issue.pickOrderLineId?.let { - pickOrderLineRepository.findById(it).orElse(null) - } - - val lotLine = issue.lotId?.let { - inventoryLotLineRepository.findById(it).orElse(null) - } - - val item = itemsRepository.findById(issue.itemId).orElse(null) - - // 修改:使用 issueQty - val stockOutLine = StockOutLine().apply { - this.stockOut = stockOut - this.inventoryLotLine = lotLine - this.item = item - this.qty = issueQty.toDouble() // 使用 issueQty - this.status = StockOutLineStatus.COMPLETE.status - this.pickOrderLine = pickOrderLine - this.type = "Bad" - } - val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - - // ✅ 修复:在更新 lot line 之前获取 inventory 的当前 onHandQty,用于计算 balance - val inventoryBeforeUpdate = item?.let { inventoryRepository.findByItemId(it.id!!).orElse(null) } - val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() - + val lotLineId = issue.lotId + ?: throw IllegalArgumentException("Issue ${issue.id} has no lot line id") + stockOutLineService.createStockOut( + StockOutRequest( + inventoryLotLineId = lotLineId, + qty = issueQty.toDouble(), + type = "Bad" + ) + ) // ✅ 修复:Bad item 的 issueQty 应该重置为 0,而不是累加/减去 // 先重置 issueQty 为 0 if (issue.lotId != null) { @@ -2103,15 +1978,7 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse { } } - // 修改:使用 issueQty - if (issue.lotId != null) { - updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = false) // 使用 issueQty - } - markIssueHandled(issue, handler) - // ✅ 修复:使用更新前的 onHandQty 计算 balance,避免触发器更新后的错误计算 - val balance = onHandQtyBeforeUpdate - issueQty.toDouble() - createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Bad") } return MessageResponse( @@ -2158,32 +2025,16 @@ open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageRespon val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L - // Create single StockOut header for all submissions - val stockOut = createIssueStockOutHeader("EXPIRY_ITEM", "Batch expiry item removal", handler) - lotLines.forEach { lotLine -> val remainingQty = (lotLine.inQty ?: BigDecimal.ZERO).subtract(lotLine.outQty ?: BigDecimal.ZERO) - // Get item from inventoryLot - val item = lotLine.inventoryLot?.item - - // Create StockOutLine - val stockOutLine = StockOutLine().apply { - this.stockOut = stockOut - this.inventoryLotLine = lotLine - this.item = item - this.qty = remainingQty.toDouble() - this.status = StockOutLineStatus.COMPLETE.status - this.type = "Expiry" - } - val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - - // Create stock ledger entry - - - // Update InventoryLotLine - updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty, isMissItem = false) - createStockLedgerForStockOut(savedStockOutLine) + stockOutLineService.createStockOut( + StockOutRequest( + inventoryLotLineId = lotLine.id!!, + qty = remainingQty.toDouble(), + type = "Expiry" + ) + ) } return MessageResponse( @@ -2746,20 +2597,8 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") println("==========================================") - val stockOut = createIssueStockOutHeader( - if (isMissItem) "MISS_ITEM" else "BAD_ITEM", - firstIssue.issueRemark, - handler - ) - println("Created stock out header: id=${stockOut.id}, type=${if (isMissItem) "MISS_ITEM" else "BAD_ITEM"}") - - val pickOrderLine = firstIssue.pickOrderLineId?.let { - pickOrderLineRepository.findById(it).orElse(null) - } - println("Pick order line: id=${pickOrderLine?.id}") - - val lotLine = request.lotId.let { - inventoryLotLineRepository.findById(it).orElse(null) + val lotLine = request.lotId.let { + inventoryLotLineRepository.findById(it).orElse(null) } println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}") @@ -2774,18 +2613,14 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse ) println("Item: id=${item.id}, code=${item.code}") - // Create stock_out_line with custom quantity - val stockOutLine = StockOutLine().apply { - this.stockOut = stockOut - this.inventoryLotLine = lotLine - this.item = item - this.qty = submitQty.toDouble() // Use custom quantity - this.status = StockOutLineStatus.COMPLETE.status - this.pickOrderLine = pickOrderLine - this.type = if (isMissItem) "Miss" else "Bad" - } - val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - println("Created stock out line: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}") + val savedStockOutLine = stockOutLineService.createStockOut( + StockOutRequest( + inventoryLotLineId = request.lotId, + qty = submitQty.toDouble(), + type = if (isMissItem) "Miss" else "Bad" + ) + ) + println("Created stock out line via createStockOut: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}") if (!isMissItem && request.lotId != null) { val rejectedLines = stockOutLineRepository @@ -2804,32 +2639,15 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse } } - // Update inventory_lot_line with custom quantity - pass isMissItem flag - if (request.lotId != null) { - println("Updating lot line after issue: lotId=${request.lotId}, submitQty=$submitQty, isMissItem=$isMissItem") - updateLotLineAfterIssue(request.lotId, submitQty, isMissItem) - } - // Mark all issues as handled issues.forEach { issue -> println(" Marking issue ${issue.id} as handled by handler $handler") markIssueHandled(issue, handler) } - // ✅ 修复:使用更新前的 onHandQty 计算 balance - val balance = onHandQtyBeforeUpdate - submitQty.toDouble() - - println("=== submitIssueWithQty: Creating stock ledger ===") - println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}") - println("Submit Qty (OutQty): ${submitQty.toDouble()}") - println("Calculated Balance: ${balance}") - println("===================================================") - - createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad") - println("=== submitIssueWithQty: SUCCESS ===") return MessageResponse( - id = stockOut.id, + id = savedStockOutLine.stockOut?.id, name = "Success", code = "SUCCESS", type = "stock_issue", 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 9b80cf3..8c76b89 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 @@ -626,7 +626,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { } open fun updateStatus(request: UpdateStockOutLineStatusRequest): MessageResponse { try { - // 1. 查当前 stockOutLine + // 1. 查当前 stockOutLine(用于日志) val stockOutLine = stockOutLineRepository.findById(request.id).orElseThrow { IllegalArgumentException("StockOutLine not found with ID: ${request.id}") } @@ -634,54 +634,18 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { println("Updating StockOutLine ID: ${request.id}") println("Current status: ${stockOutLine.status}") println("New status: ${request.status}") - if (request.status == "checked") { - stockOutLine.startTime = LocalDateTime.now() - } - if (request.status == "completed") { - // ✅ 修复:如果 startTime 为 null,也设置它(处理直接完成的情况) - if (stockOutLine.startTime == null) { - stockOutLine.startTime = LocalDateTime.now() - } - stockOutLine.endTime = LocalDateTime.now() - } - // 2. 更新自身 status/qty - stockOutLine.status = request.status - if (request.qty != null) { - val currentQty = stockOutLine.qty?.toDouble() ?: 0.0 - val newQty = currentQty + request.qty - stockOutLine.qty = (newQty) - } - val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) - - val statusLower = request.status.trim().lowercase() - val isPickEnd = statusLower == "completed" || statusLower == "partially_completed" - val deltaQty = request.qty - val skipLedgerWrite = request.skipLedgerWrite == true - - // 手工拣货增量扣减时补写出库 ledger(DO/JO 也需要写) - // 批量提交流程会显式传 skipLedgerWrite=true,避免重复写入。 - if (!skipLedgerWrite && isPickEnd && deltaQty != null && deltaQty > 0 && savedStockOutLine.id != null) { - val itemId = savedStockOutLine.item?.id - - if (itemId != null) { - val invBefore = inventoryRepository.findByItemId(itemId).orElse(null) - val onHandBefore = invBefore?.onHandQty?.toDouble() ?: 0.0 - - createStockLedgerForPickDelta( - stockOutLineId = savedStockOutLine.id!!, - deltaQty = BigDecimal(deltaQty.toString()), - onHandQtyBeforeUpdate = onHandBefore - ) - } - } + val savedStockOutLine = applyStockOutLineDelta( + stockOutLineId = request.id, + deltaQty = BigDecimal((request.qty ?: 0.0).toString()), + newStatus = request.status, + skipInventoryWrite = request.skipInventoryWrite == true, + skipLedgerWrite = request.skipLedgerWrite == true + ) println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}") - // If this stock out line is in end status, try completing its pick order line - if (isEndStatus(savedStockOutLine.status)) { - savedStockOutLine.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) } - } try { val item = savedStockOutLine.item val inventoryLotLine = savedStockOutLine.inventoryLotLine + val reqDeltaQty = request.qty ?: 0.0 // 只在状态为 completed 或 partially_completed,且数量增加时创建 BagLotLine val isCompletedOrPartiallyCompleted = request.status == "completed" || @@ -691,8 +655,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { if (item?.isBag == true && inventoryLotLine != null && isCompletedOrPartiallyCompleted && - request.qty != null && - request.qty > 0) { + reqDeltaQty > 0) { println(" Item isBag=true, creating BagLotLine...") @@ -706,7 +669,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { lotId = inventoryLotLine.inventoryLot?.id ?: 0L, itemId = item.id!!, lotNo = lotNo, - stockQty = request.qty.toInt(), + stockQty = reqDeltaQty.toInt(), date = LocalDate.now(), time = LocalTime.now(), stockOutLineId = savedStockOutLine.id @@ -733,7 +696,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { } // 4. 自动刷 pickOrderLine 状态 - val pickOrderLine = stockOutLine.pickOrderLine + val pickOrderLine = savedStockOutLine.pickOrderLine if (pickOrderLine != null) { checkIsStockOutLineCompleted(pickOrderLine.id) // 5. 自动刷 pickOrder 状态 @@ -1263,6 +1226,11 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { // 修复:从数据库获取当前实际数量 val stockOutLine = stockOutLines[line.stockOutLineId] ?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found") + val currentStatus = stockOutLine.status?.trim()?.lowercase() + if (currentStatus == "completed" || currentStatus == "complete") { + println(" Skipping already completed stockOutLineId=${line.stockOutLineId}") + return@forEach + } val currentActual = (stockOutLine.qty ?: 0.0).toBigDecimal() val targetActual = line.actualPickQty ?: BigDecimal.ZERO @@ -1287,7 +1255,8 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { id = line.stockOutLineId, status = newStatus, // 例如前端传来的 "completed" qty = 0.0, // 不改变现有 qty - skipLedgerWrite = true + skipLedgerWrite = true, + skipInventoryWrite = true ) ) @@ -1301,7 +1270,8 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { id = line.stockOutLineId, status = newStatus, qty = submitQty.toDouble(), - skipLedgerWrite = true + skipLedgerWrite = true, + skipInventoryWrite = true ) ) @@ -1862,4 +1832,139 @@ open fun batchScan(request: com.ffii.fpsms.modules.stock.web.model.BatchScanRequ ) } } +@Transactional +fun applyStockOutLineDelta( + stockOutLineId: Long, + deltaQty: BigDecimal, + newStatus: String?, + typeOverride: String? = null, + skipInventoryWrite: Boolean = false, + skipLedgerWrite: Boolean = false, + operator: String? = null, + eventTime: LocalDateTime = LocalDateTime.now() +): StockOutLine { + require(deltaQty >= BigDecimal.ZERO) { "deltaQty cannot be negative" } + + val sol = stockOutLineRepository.findById(stockOutLineId).orElseThrow { + IllegalArgumentException("StockOutLine not found: $stockOutLineId") + } + + // 1) update stock_out_line qty/status/time + val currentQty = BigDecimal(sol.qty?.toString() ?: "0") + val newQty = currentQty + deltaQty + sol.qty = newQty.toDouble() + + if (!newStatus.isNullOrBlank()) { + sol.status = newStatus + } + val statusLower = sol.status?.trim()?.lowercase() ?: "" + val isPickEnd = statusLower == "completed" || statusLower == "partially_completed" + + if (statusLower == "checked" && sol.startTime == null) { + sol.startTime = eventTime + } + if (statusLower == "completed") { + if (sol.startTime == null) sol.startTime = eventTime + sol.endTime = eventTime + } + + if (!operator.isNullOrBlank()) { + sol.modifiedBy = operator + } + val savedSol = stockOutLineRepository.saveAndFlush(sol) + + // Nothing to post if no delta + if (deltaQty == BigDecimal.ZERO || !isPickEnd) { + return savedSol + } + + val postingType = (typeOverride ?: savedSol.type ?: "").trim().lowercase() + val isIssuePosting = postingType == "miss" || postingType == "bad" || postingType == "expiry" + + // 2) inventory_lot_line + inventory + if (!skipInventoryWrite) { + val lotLine = savedSol.inventoryLotLine + if (lotLine != null) { + if (isIssuePosting) { + val latestLotLine = inventoryLotLineRepository.findById(lotLine.id!!).orElse(null) + if (latestLotLine != null) { + val currentHoldQty = latestLotLine.holdQty ?: BigDecimal.ZERO + val currentOutQty = latestLotLine.outQty ?: BigDecimal.ZERO + latestLotLine.holdQty = currentHoldQty.subtract(deltaQty).coerceAtLeast(BigDecimal.ZERO) + latestLotLine.outQty = currentOutQty.add(deltaQty) + latestLotLine.modified = eventTime + if (!operator.isNullOrBlank()) { + latestLotLine.modifiedBy = operator + } + inventoryLotLineRepository.saveAndFlush(latestLotLine) + } + } else { + val lotUpdateResult = inventoryLotLineService.updateInventoryLotLineQuantities( + UpdateInventoryLotLineQuantitiesRequest( + inventoryLotLineId = lotLine.id!!, + qty = deltaQty, + operation = "pick" + ) + ) + if (lotUpdateResult.code != "SUCCESS") { + throw IllegalStateException( + "Failed to update inventory lot line ${lotLine.id} with pick operation: ${lotUpdateResult.message}" + ) + } + } + } + + val itemId = savedSol.item?.id + if (itemId != null) { + val inv = inventoryRepository.findByItemId(itemId).orElse(null) + if (inv != null) { + val zero = BigDecimal.ZERO + inv.onHandQty = (inv.onHandQty ?: zero).minus(deltaQty) + if (!isIssuePosting) { + inv.onHoldQty = (inv.onHoldQty ?: zero).minus(deltaQty) + } + inventoryRepository.save(inv) + } + } + } + + // 3) stock_ledger (shared source with createStockOut/createStockLedgerForStockOut style) + if (!skipLedgerWrite) { + val item = savedSol.item ?: return savedSol + val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return savedSol + + // unified balance source: latest ledger first, fallback inventory.onHand + val latestLedger = stockLedgerRepository.findLatestByItemId(item.id!!).firstOrNull() + val previousBalance = latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + val outQty = deltaQty.toDouble() + val newBalance = previousBalance - outQty + + val ledger = StockLedger().apply { + this.stockOutLine = savedSol + this.inventory = inventory + this.inQty = null + this.outQty = outQty + this.balance = newBalance + this.type = typeOverride ?: savedSol.type ?: "NOR" + this.itemId = item.id + this.itemCode = item.code + this.uomId = + itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id + ?: inventory.uom?.id + this.date = eventTime.toLocalDate() + if (!operator.isNullOrBlank()) { + this.createdBy = operator + this.modifiedBy = operator + } + } + stockLedgerRepository.saveAndFlush(ledger) + } + + // 4) existing side-effects keep same behavior + if (isEndStatus(savedSol.status)) { + savedSol.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) } + } + + return savedSol +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt index 3db01ba..f25a7d9 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt @@ -363,6 +363,9 @@ class StockTakeRecordService( approverBadQty = stockTakeRecord?.approverBadQty, finalQty = stockTakeLine?.finalQty, bookQty = stockTakeRecord?.bookQty, + stockTakeSection = stockTakeRecord?.stockTakeSection ?: warehouse?.stockTakeSection, + stockTakeSectionDescription = warehouse?.stockTakeSectionDescription, + stockTakerName = stockTakeRecord?.stockTakerName, ) } 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 0c6fbc0..65fd96f 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 @@ -60,7 +60,8 @@ data class UpdateStockOutLineStatusRequest( val status: String, val qty: Double? = null, val remarks: String? = null, - val skipLedgerWrite: Boolean? = false + val skipLedgerWrite: Boolean? = false, + val skipInventoryWrite: Boolean? = false ) data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( val pickOrderLineId: Long, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt index 99b039b..c549ecc 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt @@ -61,6 +61,9 @@ data class InventoryLotDetailResponse( val approverBadQty: BigDecimal? = null, val finalQty: BigDecimal? = null, val bookQty: BigDecimal? = null, + val stockTakeSection: String? = null, + val stockTakeSectionDescription: String? = null, + val stockTakerName: String? = null, ) data class InventoryLotLineListRequest( val warehouseCode: String