diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt index c36fdbb..eaa58d8 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt @@ -107,4 +107,10 @@ data class ReleasedDoPickOrderListItem( data class AssignByDoPickOrderIdRequest( val userId: Long, val doPickOrderId: Long +) + +/** Workbench: assign a `delivery_order_pick_order` ticket + its linked pick orders. */ +data class AssignByDeliveryOrderPickOrderIdRequest( + val userId: Long, + val deliveryOrderPickOrderId: Long, ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt index 75983ab..3ccd286 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt @@ -12,5 +12,9 @@ interface SuggestPickLotRepository : AbstractRepository fun findAllByPickOrderLineId(pickOrderLineId: Long): List + fun findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId: Long): List + + fun findAllByPickOrderLineIdInAndDeletedFalse(pickOrderLineIds: List): List + fun findFirstByStockOutLineId(stockOutLineId: Long): SuggestedPickLot? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt index c9bfda7..1a4d916 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt @@ -115,9 +115,6 @@ open class InventoryLotLineService( val stockUom = request.stockUomId?.let { itemUomRespository.findById(it).getOrNull() } val status = request.status?.let { _status -> InventoryLotLineStatus.entries.find { it.value == _status } } - println("status: ${request.status}") - println("status123: ${status?.value}") - inventoryLotLine.apply { this.inventoryLot = inventoryLot this.warehouse = warehouse 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 b047c0f..29a8c7f 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 @@ -46,6 +46,9 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository import java.time.LocalTime import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository +import com.ffii.fpsms.modules.stock.entity.InventoryLotLine +import jakarta.persistence.EntityManager +import jakarta.persistence.PersistenceContext import java.util.UUID @Service open class StockOutLineService( @@ -75,6 +78,10 @@ private val inventoryLotLineService: InventoryLotLineService, private val pickExecutionIssueRepository: PickExecutionIssueRepository, private val itemUomService: ItemUomService, ): AbstractBaseEntityService(jdbcDao, stockOutLineRepository) { + + @PersistenceContext + private lateinit var entityManager: EntityManager + private fun isEndStatus(status: String?): Boolean { val s = status?.trim()?.lowercase() ?: return false return s == "completed" || s == "rejected" || s == "partially_completed" @@ -104,6 +111,22 @@ private val inventoryLotLineService: InventoryLotLineService, } } } + + /** When every POL on this pick order is COMPLETED or PARTIALLY_COMPLETE, mark pick order completed and cascade DO completion. */ + private fun refreshPickOrderHeaderIfAllLinesCompleted(pickOrderId: Long) { + val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) ?: return + val allLines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId) + val allCompleted = allLines.all { + it.status == PickOrderLineStatus.COMPLETED || it.status == PickOrderLineStatus.PARTIALLY_COMPLETE + } + if (allCompleted && allLines.isNotEmpty()) { + pickOrder.status = PickOrderStatus.COMPLETED + pickOrderRepository.save(pickOrder) + completeDoForPickOrder(pickOrderId) + completeDoIfAllPickOrdersCompleted(pickOrderId) + } + } + @Throws(IOException::class) @Transactional open fun findAllByStockOutId(stockOutId: Long): List { @@ -378,7 +401,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { // }) // } @Transactional - fun checkIsStockOutLineCompleted(pickOrderLineId: Long) { + fun checkIsStockOutLineCompleted(pickOrderLineId: Long, quiet: Boolean = false) { val allStockOutLines = stockOutLineRepository .findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) @@ -409,7 +432,9 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { acc + (issue.issueQty ?: BigDecimal.ZERO) } } catch (e: Exception) { - println(" Error fetching issues for pickOrderLineId $pickOrderLineId: ${e.message}") + if (!quiet) { + println(" Error fetching issues for pickOrderLineId $pickOrderLineId: ${e.message}") + } BigDecimal.ZERO } @@ -430,14 +455,16 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { // ✅ 现在的规则:这三类状态都算“已结束” !(isComplete || isRejected || isPartiallyComplete) } - - println("Unfinished lines: ${unfinishedLine.size}") - if (unfinishedLine.isNotEmpty()) { - unfinishedLine.forEach { sol -> - println(" - StockOutLine ${sol.id}: status=${sol.status}, qty=${sol.qty}") + + if (!quiet) { + println("Unfinished lines: ${unfinishedLine.size}") + if (unfinishedLine.isNotEmpty()) { + unfinishedLine.forEach { sol -> + println(" - StockOutLine ${sol.id}: status=${sol.status}, qty=${sol.qty}") + } } } - + if (unfinishedLine.isEmpty()) { // set pick order line status to complete val pol = pickOrderLineRepository.findById(pickOrderLineId).orElseThrow() @@ -447,11 +474,28 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { this.status = PickOrderLineStatus.COMPLETED } ) - println("✅ Updated pick order line $pickOrderLineId from $previousStatus to COMPLETED") - } else { + if (!quiet) { + println("✅ Updated pick order line $pickOrderLineId from $previousStatus to COMPLETED") + } + } else if (!quiet) { println("⏳ Pick order line $pickOrderLineId not completed yet - has ${unfinishedLine.size} unfinished stock out lines") } } + + /** Batch pick: same qty rules as [InventoryLotLineService.updateInventoryLotLineQuantities] pick, without double findById / extra service layers. */ + private fun applyPickToInventoryLotLineInBatch(ill: InventoryLotLine, submitQty: BigDecimal) { + val zero = BigDecimal.ZERO + val newHold = (ill.holdQty ?: zero).minus(submitQty) + val newOut = (ill.outQty ?: zero).plus(submitQty) + if (newHold < zero || newOut < zero) { + throw IllegalArgumentException("Invalid pick quantities for lotLine ${ill.id}: holdQty=$newHold, outQty=$newOut") + } + val prevStatus = ill.status + ill.holdQty = newHold + ill.outQty = newOut + ill.status = inventoryLotLineService.deriveInventoryLotLineStatus(prevStatus, ill.inQty, ill.outQty, ill.holdQty) + inventoryLotLineRepository.save(ill) + } private fun completeDoIfAllPickOrdersCompleted(pickOrderId: Long) { // 1) 先用 line 关联找 do_pick_order_id val lines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(pickOrderId) @@ -634,60 +678,66 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { println("Updating StockOutLine ID: ${request.id}") println("Current status: ${stockOutLine.status}") println("New status: ${request.status}") + val deferAggregate = request.deferAggregatePickOrderEffects == true val savedStockOutLine = applyStockOutLineDelta( - stockOutLineId = request.id, + stockOutLine = stockOutLine, deltaQty = BigDecimal((request.qty ?: 0.0).toString()), newStatus = request.status, skipInventoryWrite = request.skipInventoryWrite == true, - skipLedgerWrite = request.skipLedgerWrite == true + skipLedgerWrite = request.skipLedgerWrite == true, + skipTryCompletePickOrderLine = deferAggregate, + deferPersistenceFlush = deferAggregate ) println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}") - try { - val item = savedStockOutLine.item - val inventoryLotLine = savedStockOutLine.inventoryLotLine - val reqDeltaQty = request.qty ?: 0.0 - - // 只在状态为 completed 或 partially_completed,且数量增加时创建 BagLotLine - val isCompletedOrPartiallyCompleted = request.status == "completed" || - request.status == "partially_completed" || - request.status == "PARTIALLY_COMPLETE" - - if (item?.isBag == true && - inventoryLotLine != null && - isCompletedOrPartiallyCompleted && - reqDeltaQty > 0) { - - println(" Item isBag=true, creating BagLotLine...") - - val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!) - - if (bag != null) { - val lotNo = inventoryLotLine.inventoryLot?.lotNo - if (lotNo != null) { - val createBagLotLineRequest = CreateBagLotLineRequest( - bagId = bag.id!!, - lotId = inventoryLotLine.inventoryLot?.id ?: 0L, - itemId = item.id!!, - lotNo = lotNo, - stockQty = reqDeltaQty.toInt(), - date = LocalDate.now(), - time = LocalTime.now(), - stockOutLineId = savedStockOutLine.id - ) - - bagService.createBagLotLinesByBagId(createBagLotLineRequest) - println(" ✓ BagLotLine created successfully for item ${item.code}") + if (!deferAggregate) { + try { + val item = savedStockOutLine.item + val inventoryLotLine = savedStockOutLine.inventoryLotLine + val reqDeltaQty = request.qty ?: 0.0 + + // 只在状态为 completed 或 partially_completed,且数量增加时创建 BagLotLine + val isCompletedOrPartiallyCompleted = request.status == "completed" || + request.status == "partially_completed" || + request.status == "PARTIALLY_COMPLETE" + + if (item?.isBag == true && + inventoryLotLine != null && + isCompletedOrPartiallyCompleted && + reqDeltaQty > 0 + ) { + + println(" Item isBag=true, creating BagLotLine...") + + val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!) + + if (bag != null) { + val lotNo = inventoryLotLine.inventoryLot?.lotNo + if (lotNo != null) { + val createBagLotLineRequest = CreateBagLotLineRequest( + bagId = bag.id!!, + lotId = inventoryLotLine.inventoryLot?.id ?: 0L, + itemId = item.id!!, + lotNo = lotNo, + stockQty = reqDeltaQty.toInt(), + date = LocalDate.now(), + time = LocalTime.now(), + stockOutLineId = savedStockOutLine.id + ) + + bagService.createBagLotLinesByBagId(createBagLotLineRequest) + println(" ✓ BagLotLine created successfully for item ${item.code}") + } else { + println(" Warning: lotNo is null, skipping BagLotLine creation") + } } else { - println(" Warning: lotNo is null, skipping BagLotLine creation") + println(" Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation") } - } else { - println(" Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation") } + } catch (e: Exception) { + println(" Error creating BagLotLine: ${e.message}") + e.printStackTrace() + // 不中断主流程,只记录错误 } - } catch (e: Exception) { - println(" Error creating BagLotLine: ${e.message}") - e.printStackTrace() - // 不中断主流程,只记录错误 } // 3. 如果被拒绝,触发特殊处理 if (request.status == "rejected" || request.status == "REJECTED") { @@ -695,26 +745,18 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { handleLotRejectionFromStockOutLine(savedStockOutLine) } - // 4. 自动刷 pickOrderLine 状态 - val pickOrderLine = savedStockOutLine.pickOrderLine - if (pickOrderLine != null) { - checkIsStockOutLineCompleted(pickOrderLine.id) - // 5. 自动刷 pickOrder 状态 - val pickOrder = pickOrderLine.pickOrder - if (pickOrder != null && pickOrder.id != null) { - // ✅ 修复:使用 repository 查询所有 lines,避免懒加载问题 - val allLines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrder.id!!) - val allCompleted = allLines.all { it.status == PickOrderLineStatus.COMPLETED || it.status == PickOrderLineStatus.PARTIALLY_COMPLETE } - if (allCompleted && allLines.isNotEmpty()) { - pickOrder.status = PickOrderStatus.COMPLETED - pickOrderRepository.save(pickOrder) - completeDoForPickOrder(pickOrder.id!!) - completeDoIfAllPickOrdersCompleted(pickOrder.id!!) - } + // 4–5. 自动刷 pickOrderLine / pickOrder 状态(批次提交在结尾统一处理) + if (!deferAggregate) { + val pickOrderLine = savedStockOutLine.pickOrderLine + if (pickOrderLine != null) { + checkIsStockOutLineCompleted(pickOrderLine.id) + pickOrderLine.pickOrder?.id?.let { refreshPickOrderHeaderIfAllLinesCompleted(it) } } } - - val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) + + val mappedSavedStockOutLine = + if (deferAggregate) null + else stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) return MessageResponse( id = savedStockOutLine.id, name = savedStockOutLine.inventoryLotLine?.inventoryLot?.lotNo?: "", @@ -1213,140 +1255,120 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { // 先前預載的 inventories 從未使用(save 已註解),已移除以避免批量提交無故失敗。 val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId } println("Loading ${lotLineIds.size} lot lines...") - val lotLines = if (lotLineIds.isNotEmpty()) { - inventoryLotLineRepository.findAllById(lotLineIds).associateBy { it.id } + val lotLinesById: MutableMap = if (lotLineIds.isNotEmpty()) { + inventoryLotLineRepository.findAllById(lotLineIds).associateByTo(mutableMapOf()) { it.id!! } } else { - emptyMap() + mutableMapOf() } - // 2) Bulk load all stock out lines to get current quantities + // 2) Bulk load all stock out lines(批次內就地更新) val stockOutLineIds = request.lines.map { it.stockOutLineId } println("Loading ${stockOutLineIds.size} stock out lines...") - val stockOutLines = stockOutLineRepository.findAllById(stockOutLineIds).associateBy { it.id } + val stockOutLinesById = + stockOutLineRepository.findAllById(stockOutLineIds).associateByTo(mutableMapOf()) { it.id!! } - // 3) Process each request line + // 3) Process each request line(直接 applyStockOutLineDelta + 內聯扣庫存,避免 updateStatus 與雙重查詢) request.lines.forEach { line: QrPickSubmitLineRequest -> val lineTrace = "$traceId|SOL=${line.stockOutLineId}" try { - println("[$lineTrace] Processing line, noLot=${line.noLot}") - + val solEntity = stockOutLinesById[line.stockOutLineId] + ?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found") + if (line.noLot) { - // noLot branch - updateStatus(UpdateStockOutLineStatusRequest( - id = line.stockOutLineId, - status = "completed", - qty = 0.0 - )) + val updated = applyStockOutLineDelta( + stockOutLine = solEntity, + deltaQty = BigDecimal.ZERO, + newStatus = "completed", + skipInventoryWrite = true, + skipLedgerWrite = true, + skipTryCompletePickOrderLine = true, + deferPersistenceFlush = true + ) + stockOutLinesById[line.stockOutLineId] = updated processedIds += line.stockOutLineId - println("[$lineTrace] noLot processed (status->completed, qty=0)") return@forEach } - // 修复:从数据库获取当前实际数量 - val stockOutLine = stockOutLines[line.stockOutLineId] - ?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found") - val currentStatus = stockOutLine.status?.trim()?.lowercase() + val currentStatus = solEntity.status?.trim()?.lowercase() if (currentStatus == "completed" || currentStatus == "complete") { - println("[$lineTrace] Skip because current status is already completed") return@forEach } - - val currentActual = (stockOutLine.qty ?: 0.0).toBigDecimal() - val targetActual = line.actualPickQty ?: BigDecimal.ZERO - val required = line.requiredQty ?: BigDecimal.ZERO - - println("[$lineTrace] currentActual=$currentActual, targetActual=$targetActual, required=$required") - - // 计算增量(前端发送的是目标累计值) - val submitQty = targetActual - currentActual - - println("[$lineTrace] submitQty(increment)=$submitQty") - - // 使用前端发送的状态,否则根据数量自动判断 - val newStatus = line.stockOutLineStatus - ?: if (targetActual >= required) "completed" else "partially_completed" - - if (submitQty <= BigDecimal.ZERO) { - println("[$lineTrace] submitQty<=0, only update status, skip inventory+ledger") - - updateStatus( - UpdateStockOutLineStatusRequest( - id = line.stockOutLineId, - status = newStatus, // 例如前端传来的 "completed" - qty = 0.0, // 不改变现有 qty - skipLedgerWrite = true, - skipInventoryWrite = true - ) - ) - - // 直接跳过后面的库存扣减逻辑 - return@forEach + + val currentActual = (solEntity.qty ?: 0.0).toBigDecimal() + val targetActual = line.actualPickQty ?: BigDecimal.ZERO + val required = line.requiredQty ?: BigDecimal.ZERO + val submitQty = targetActual - currentActual + val newStatus = line.stockOutLineStatus + ?: if (targetActual >= required) "completed" else "partially_completed" + + if (submitQty <= BigDecimal.ZERO) { + val updated = applyStockOutLineDelta( + stockOutLine = solEntity, + deltaQty = BigDecimal.ZERO, + newStatus = newStatus, + skipInventoryWrite = true, + skipLedgerWrite = true, + skipTryCompletePickOrderLine = true, + deferPersistenceFlush = true + ) + stockOutLinesById[line.stockOutLineId] = updated + return@forEach + } + + val savedSol = applyStockOutLineDelta( + stockOutLine = solEntity, + deltaQty = submitQty, + newStatus = newStatus, + skipInventoryWrite = true, + skipLedgerWrite = true, + skipTryCompletePickOrderLine = true, + deferPersistenceFlush = true + ) + stockOutLinesById[line.stockOutLineId] = savedSol + + val actualInventoryLotLineId = + line.inventoryLotLineId ?: savedSol.inventoryLotLine?.id + + if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { + val ill = lotLinesById[actualInventoryLotLineId] + ?: inventoryLotLineRepository.findById(actualInventoryLotLineId).orElseThrow().also { + lotLinesById[actualInventoryLotLineId] = it } - - // 只有 submitQty > 0 时,才真正增加 qty 并触发库存扣减 - updateStatus( - UpdateStockOutLineStatusRequest( - id = line.stockOutLineId, - status = newStatus, - qty = submitQty.toDouble(), - skipLedgerWrite = true, - skipInventoryWrite = true - ) + val item = savedSol.item + val inventoryBeforeUpdate = item?.id?.let { itemId -> + itemUomService.findInventoryForItemBaseUom(itemId) + } + val onHandQtyBeforeUpdate = + (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + applyPickToInventoryLotLineInBatch(ill, submitQty) + createStockLedgerForPickDelta( + stockOutLine = savedSol, + deltaQty = submitQty, + onHandQtyBeforeUpdate = onHandQtyBeforeUpdate, + traceTag = lineTrace, + flushAfterSave = false ) - println("[$lineTrace] stock_out_line qty/status updated with delta=$submitQty (inventory+ledger deferred)") + } else if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId == null) { + val item = savedSol.item + val inventoryBeforeUpdate = item?.id?.let { itemId -> + itemUomService.findInventoryForItemBaseUom(itemId) + } + val onHandQtyBeforeUpdate = + (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() + createStockLedgerForPickDelta( + stockOutLine = savedSol, + deltaQty = submitQty, + onHandQtyBeforeUpdate = onHandQtyBeforeUpdate, + traceTag = lineTrace, + flushAfterSave = false + ) + } - // Inventory updates - 修复:使用增量数量 - // ✅ 修复:如果 inventoryLotLineId 为 null,从 stock_out_line 中获取 - val actualInventoryLotLineId = line.inventoryLotLineId - ?: stockOutLine.inventoryLotLine?.id - - // 在 newBatchSubmit 方法中,修改这部分代码(大约在 1169-1185 行) -if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { - println("[$lineTrace] Updating inventory lot line $actualInventoryLotLineId with qty=$submitQty") - - // ✅ 修复:在更新 inventory_lot_line 之前获取 inventory 的当前 onHandQty - val item = stockOutLine.item - val inventoryBeforeUpdate = item?.id?.let { itemId -> - itemUomService.findInventoryForItemBaseUom(itemId) - } - val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() - - println("[$lineTrace] Inventory before update: onHandQty=$onHandQtyBeforeUpdate") - - inventoryLotLineService.updateInventoryLotLineQuantities( - UpdateInventoryLotLineQuantitiesRequest( - inventoryLotLineId = actualInventoryLotLineId, - qty = submitQty, - operation = "pick" - ) - ) - - if (submitQty > BigDecimal.ZERO) { - // ✅ 修复:传入更新前的 onHandQty,让 createStockLedgerForPickDelta 使用它 - createStockLedgerForPickDelta(line.stockOutLineId, submitQty, onHandQtyBeforeUpdate, lineTrace) - } -} else if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId == null) { - // ✅ 修复:即使没有 inventoryLotLineId,也应该获取 inventory.onHandQty - val item = stockOutLine.item - val inventoryBeforeUpdate = item?.id?.let { itemId -> - itemUomService.findInventoryForItemBaseUom(itemId) - } - val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() - - println("[$lineTrace] Warning: No inventoryLotLineId, still trying ledger creation") - createStockLedgerForPickDelta(line.stockOutLineId, submitQty, onHandQtyBeforeUpdate, lineTrace) -} try { - val stockOutLine = stockOutLines[line.stockOutLineId] - val item = stockOutLine?.item - val inventoryLotLine = line.inventoryLotLineId?.let { lotLines[it] } - + val item = savedSol.item + val inventoryLotLine = line.inventoryLotLineId?.let { lid -> lotLinesById[lid] } if (item?.isBag == true && inventoryLotLine != null && submitQty > BigDecimal.ZERO) { - println(" Item isBag=true, creating BagLotLine...") - - // 根据 itemId 查找对应的 Bag val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!) - if (bag != null) { val lotNo = inventoryLotLine.inventoryLot?.lotNo if (lotNo != null) { @@ -1354,29 +1376,21 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { bagId = bag.id!!, lotId = inventoryLotLine.inventoryLot?.id ?: 0L, itemId = item.id!!, - stockOutLineId = stockOutLine.id , + stockOutLineId = savedSol.id, lotNo = lotNo, - stockQty = submitQty.toInt(), // 转换为 Int + stockQty = submitQty.toInt(), date = LocalDate.now(), time = LocalTime.now() ) - bagService.createBagLotLinesByBagId(createBagLotLineRequest) - println(" ✓ BagLotLine created successfully for item ${item.code}") - } else { - println(" Warning: lotNo is null, skipping BagLotLine creation") } - } else { - println(" Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation") } } } catch (e: Exception) { println(" Error creating BagLotLine: ${e.message}") e.printStackTrace() - // 不中断主流程,只记录错误 } processedIds += line.stockOutLineId - println("[$lineTrace] Line processed successfully") } catch (e: Exception) { println("[$lineTrace] Error processing line: ${e.message}") e.printStackTrace() @@ -1384,39 +1398,39 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { } } - // 4) 移除:不需要保存 lotLines,因为它们没有被修改 - // inventoryLotLineRepository.saveAll(lotLines.values.toList()) + entityManager.flush() - // ✅ 修复:批处理完成后,检查所有受影响的 pick order lines 是否应该标记为完成 - val affectedPickOrderIds = request.lines - .mapNotNull { line -> - stockOutLines[line.stockOutLineId]?.pickOrderLine?.pickOrder?.id - } - .distinct() + // 批次內已 defer:此處只處理本批有碰到的 POL,再刷新所屬 pick order 表頭,最後每個 conso 只掃一次 + val polIdsTouched = request.lines.mapNotNull { line -> + stockOutLinesById[line.stockOutLineId]?.pickOrderLine?.id + }.distinct() - val allPickOrderLineIdsToCheck = if (affectedPickOrderIds.isNotEmpty()) { - affectedPickOrderIds.flatMap { pickOrderId -> - pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId).mapNotNull { it.id } - }.distinct() - } else { - emptyList() + println("=== Checking ${polIdsTouched.size} pick order lines touched by batch ===") + polIdsTouched.forEach { pickOrderLineId -> + try { + checkIsStockOutLineCompleted(pickOrderLineId, quiet = true) + } catch (e: Exception) { + println("Error checking pick order line $pickOrderLineId: ${e.message}") + } } - println("=== Checking ${allPickOrderLineIdsToCheck.size} pick order lines (all lines of affected pick orders) after batch submit ===") - allPickOrderLineIdsToCheck.forEach { pickOrderLineId -> + val pickOrderIdsTouched = request.lines.mapNotNull { line -> + stockOutLinesById[line.stockOutLineId]?.pickOrderLine?.pickOrder?.id + }.distinct() + + pickOrderIdsTouched.forEach { pickOrderId -> try { - checkIsStockOutLineCompleted(pickOrderLineId) + refreshPickOrderHeaderIfAllLinesCompleted(pickOrderId) } catch (e: Exception) { - println("Error checking pick order line $pickOrderLineId: ${e.message}") + println("Error refreshing pick order header for pickOrderId=$pickOrderId: ${e.message}") } } - val affectedConsoCodes = affectedPickOrderIds - .mapNotNull { pickOrderId -> - val po = pickOrderRepository.findById(pickOrderId).orElse(null) - po?.consoCode - } - .filter { !it.isNullOrBlank() } - .distinct() + + val affectedConsoCodes = pickOrderIdsTouched.mapNotNull { pickOrderId -> + pickOrderRepository.findById(pickOrderId).orElse(null)?.consoCode + } + .filter { !it.isNullOrBlank() } + .distinct() println("=== Checking completion by consoCode for ${affectedConsoCodes.size} affected consoCodes after batch submit ===") affectedConsoCodes.forEach { consoCode -> @@ -1559,63 +1573,72 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { private fun createStockLedgerForPickDelta( - stockOutLineId: Long, + stockOutLine: StockOutLine, deltaQty: BigDecimal, - onHandQtyBeforeUpdate: Double? = null, // ✅ 新增参数:更新前的 onHandQty - traceTag: String? = null + onHandQtyBeforeUpdate: Double? = null, + traceTag: String? = null, + flushAfterSave: Boolean = true ) { - val tracePrefix = traceTag?.let { "[$it] " } ?: "[SOL=$stockOutLineId] " + val solId = stockOutLine.id + val tracePrefix = traceTag?.let { "[$it] " } ?: "[SOL=$solId] " if (deltaQty <= BigDecimal.ZERO) { - println("${tracePrefix}Skip ledger creation: deltaQty <= 0 (deltaQty=$deltaQty)") - return - } - - val sol = stockOutLineRepository.findById(stockOutLineId).orElse(null) - if (sol == null) { - println("${tracePrefix}Skip ledger creation: stockOutLine not found") + if (flushAfterSave) { + println("${tracePrefix}Skip ledger creation: deltaQty <= 0 (deltaQty=$deltaQty)") + } return } - val item = sol.item + + val item = stockOutLine.item if (item == null) { - println("${tracePrefix}Skip ledger creation: stockOutLine.item is null") + if (flushAfterSave) { + println("${tracePrefix}Skip ledger creation: stockOutLine.item is null") + } return } - + val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!) if (inventory == null) { - println("${tracePrefix}Skip ledger creation: inventory not found by itemId=${item.id}") + if (flushAfterSave) { + println("${tracePrefix}Skip ledger creation: inventory not found by itemId=${item.id}") + } return } - + val previousBalance = resolvePreviousBalance( itemId = item.id!!, inventory = inventory, onHandQtyBeforeUpdate = onHandQtyBeforeUpdate ) - + val newBalance = previousBalance - deltaQty.toDouble() - - println("${tracePrefix}Creating ledger: previousBalance=$previousBalance, deltaQty=$deltaQty, newBalance=$newBalance, inventoryId=${inventory.id}") - if (onHandQtyBeforeUpdate != null) { - println("${tracePrefix}Using onHandQtyBeforeUpdate: $onHandQtyBeforeUpdate") + + if (flushAfterSave) { + println("${tracePrefix}Creating ledger: previousBalance=$previousBalance, deltaQty=$deltaQty, newBalance=$newBalance, inventoryId=${inventory.id}") + if (onHandQtyBeforeUpdate != null) { + println("${tracePrefix}Using onHandQtyBeforeUpdate: $onHandQtyBeforeUpdate") + } } - + val ledger = StockLedger().apply { - this.stockOutLine = sol + this.stockOutLine = stockOutLine this.inventory = inventory this.inQty = null - this.outQty = deltaQty.toDouble() + this.outQty = deltaQty.toDouble() this.balance = newBalance - this.type = "NOR" + this.type = "NOR" this.itemId = item.id this.itemCode = item.code this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id ?: inventory.uom?.id this.date = LocalDate.now() } - - stockLedgerRepository.saveAndFlush(ledger) - println("${tracePrefix}Ledger created successfully for stockOutLineId=$stockOutLineId") + + if (flushAfterSave) { + stockLedgerRepository.saveAndFlush(ledger) + println("${tracePrefix}Ledger created successfully for stockOutLineId=$solId") + } else { + stockLedgerRepository.save(ledger) + } } private fun resolvePreviousBalance( @@ -1886,20 +1909,20 @@ open fun batchScan(request: com.ffii.fpsms.modules.stock.web.model.BatchScanRequ } @Transactional fun applyStockOutLineDelta( - stockOutLineId: Long, + stockOutLine: StockOutLine, deltaQty: BigDecimal, newStatus: String?, typeOverride: String? = null, skipInventoryWrite: Boolean = false, skipLedgerWrite: Boolean = false, operator: String? = null, - eventTime: LocalDateTime = LocalDateTime.now() + eventTime: LocalDateTime = LocalDateTime.now(), + skipTryCompletePickOrderLine: Boolean = false, + deferPersistenceFlush: Boolean = false ): StockOutLine { require(deltaQty >= BigDecimal.ZERO) { "deltaQty cannot be negative" } - val sol = stockOutLineRepository.findById(stockOutLineId).orElseThrow { - IllegalArgumentException("StockOutLine not found: $stockOutLineId") - } + val sol = stockOutLine // 1) update stock_out_line qty/status/time val currentQty = BigDecimal(sol.qty?.toString() ?: "0") @@ -1923,7 +1946,9 @@ fun applyStockOutLineDelta( if (!operator.isNullOrBlank()) { sol.modifiedBy = operator } - val savedSol = stockOutLineRepository.saveAndFlush(sol) + val savedSol = + if (deferPersistenceFlush) stockOutLineRepository.save(sol) + else stockOutLineRepository.saveAndFlush(sol) // Nothing to post if no delta if (deltaQty == BigDecimal.ZERO || !isPickEnd) { @@ -1955,7 +1980,8 @@ fun applyStockOutLineDelta( if (!operator.isNullOrBlank()) { latestLotLine.modifiedBy = operator } - inventoryLotLineRepository.saveAndFlush(latestLotLine) + if (deferPersistenceFlush) inventoryLotLineRepository.save(latestLotLine) + else inventoryLotLineRepository.saveAndFlush(latestLotLine) } } else { val lotUpdateResult = inventoryLotLineService.updateInventoryLotLineQuantities( @@ -2017,11 +2043,12 @@ fun applyStockOutLineDelta( this.modifiedBy = operator } } - stockLedgerRepository.saveAndFlush(ledger) + if (deferPersistenceFlush) stockLedgerRepository.save(ledger) + else stockLedgerRepository.saveAndFlush(ledger) } - // 4) existing side-effects keep same behavior - if (isEndStatus(savedSol.status)) { + // 4) existing side-effects keep same behavior (batch submit defers to end of newBatchSubmit) + if (!skipTryCompletePickOrderLine && isEndStatus(savedSol.status)) { savedSol.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) } } 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 3cd7d97..3a25122 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 @@ -2317,28 +2317,27 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch( -fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes { +open fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes { val startTime = System.currentTimeMillis() - - // 添加调试日志 - println("Search request received: itemCode=${request.itemCode}, itemName=${request.itemName}, type=${request.type}, startDate=${request.startDate}, endDate=${request.endDate}") - - // 验证:itemCode 或 itemName 至少一个不为 null 或空字符串 + + println( + "Search request received: itemCode=${request.itemCode}, itemName=${request.itemName}, " + + "type=${request.type}, startDate=${request.startDate}, endDate=${request.endDate}" + ) + val itemCode = request.itemCode?.trim()?.takeIf { it.isNotEmpty() } val itemName = request.itemName?.trim()?.takeIf { it.isNotEmpty() } - + if (itemCode == null && itemName == null) { println("Search validation failed: both itemCode and itemName are null/empty") return RecordsRes(emptyList(), 0) } - - // request.startDate 和 request.endDate 已经是 LocalDate? 类型,不需要转换 + val startDate = request.startDate val endDate = request.endDate - + println("Processed params: itemCode=$itemCode, itemName=$itemName, startDate=$startDate, endDate=$endDate") - - // 使用 Repository 查询(更简单、更快) + val total = stockLedgerRepository.countStockTransactions( itemCode = itemCode, itemName = itemName, @@ -2346,20 +2345,17 @@ fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes< startDate = startDate, endDate = endDate ) - + println("Total count: $total") - - // 如果 pageSize 是默认值(100)或未设置,使用 total 作为 pageSize + val actualPageSize = if (request.pageSize == 100) { total.toInt().coerceAtLeast(1) } else { request.pageSize } - - // 计算 offset + val offset = request.pageNum * actualPageSize - - // 查询所有符合条件的记录 + val ledgers = stockLedgerRepository.findStockTransactions( itemCode = itemCode, itemName = itemName, @@ -2367,13 +2363,13 @@ fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes< startDate = startDate, endDate = endDate ) - + println("Found ${ledgers.size} ledgers") - + val transactions = ledgers.map { ledger -> val stockInLine = ledger.stockInLine val stockOutLine = ledger.stockOutLine - + StockTransactionResponse( id = stockInLine?.id ?: stockOutLine?.id ?: 0L, transactionType = if (ledger.inQty != null && ledger.inQty!! > 0) "IN" else "OUT", @@ -2400,20 +2396,18 @@ fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes< remarks = stockInLine?.remarks ) } - - // 按 date 排序(从旧到新),如果 date 为 null 则使用 transactionDate 的日期部分 + val sortedTransactions = transactions.sortedWith( compareBy( { it.date ?: it.transactionDate?.toLocalDate() }, { it.transactionDate } ) ) - - // 应用分页 + val paginatedTransactions = sortedTransactions.drop(offset).take(actualPageSize) val totalTime = System.currentTimeMillis() - startTime println("Total time (Repository query): ${totalTime}ms, count: ${paginatedTransactions.size}, total: $total") - + return RecordsRes(paginatedTransactions, total.toInt()) } } 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 65fd96f..b8aa0eb 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 @@ -61,7 +61,9 @@ data class UpdateStockOutLineStatusRequest( val qty: Double? = null, val remarks: String? = null, val skipLedgerWrite: Boolean? = false, - val skipInventoryWrite: Boolean? = false + val skipInventoryWrite: Boolean? = false, + /** When true (batch submit path): skip per-line POL/PO rollup, conso completion scan, duplicate BagLotLine; caller runs these once at end. */ + val deferAggregatePickOrderEffects: Boolean? = false ) data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( val pickOrderLineId: Long,