From 588eb7f0a894c7effb65992e5980b16ec22864f3 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 27 Apr 2026 17:28:30 +0800 Subject: [PATCH] fix stock take efficient and jo picking dashboard efficient --- .../jobOrder/service/JoPickOrderService.kt | 102 ++++++---- .../entity/PickExecutionIssueRepository.kt | 8 + .../stock/entity/StockTakeRecordRepository.kt | 21 +++ .../stock/service/StockTakeRecordService.kt | 175 ++++++++++++++---- .../stock/web/StockTakeRecordController.kt | 22 +++ .../stock/web/model/StockTakeRecordReponse.kt | 6 + .../20260427_01_Enson/01_alter_stock_take.sql | 5 + 7 files changed, 259 insertions(+), 80 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20260427_01_Enson/01_alter_stock_take.sql diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt index 815dcf4..8a9d264 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt @@ -2906,35 +2906,65 @@ open fun getMaterialPickStatus(date: String?): List { // Get all joPickOrders val joPickOrders = joPickOrderRepository.findAll() - - // Filter by date if provided (filter by jobOrder.planStart date) - val filteredJoPickOrders = if (filterDate != null) { - joPickOrders.filter { joPickOrder -> - val jobOrder = joPickOrder.jobOrderId?.let { - jobOrderRepository.findById(it).orElse(null) - } - jobOrder?.planStart?.toLocalDate() == filterDate - } + if (joPickOrders.isEmpty()) return emptyList() + + // Batch load related Job Orders once to avoid repeated findById calls + val jobOrderIds = joPickOrders.mapNotNull { it.jobOrderId }.distinct() + val jobOrdersById = if (jobOrderIds.isEmpty()) { + emptyMap() } else { - joPickOrders + jobOrderRepository.findAllById(jobOrderIds).associateBy { it.id } } - - // Group by jobOrderId - val groupedByJobOrder = filteredJoPickOrders + + // Filter by date/status in-memory using preloaded Job Orders + val groupedByJobOrder = joPickOrders .filter { joPickOrder -> - val jobOrder = joPickOrder.jobOrderId?.let { - jobOrderRepository.findById(it).orElse(null) - } - jobOrder?.status != JobOrderStatus.COMPLETED + val jobOrder = joPickOrder.jobOrderId?.let { jobOrdersById[it] } ?: return@filter false + val matchesDate = filterDate == null || jobOrder.planStart?.toLocalDate() == filterDate + val notCompleted = jobOrder.status != JobOrderStatus.COMPLETED + matchesDate && notCompleted } .groupBy { it.jobOrderId } + + val allPickOrderIds = groupedByJobOrder.values + .flatten() + .mapNotNull { it.pickOrderId } + .distinct() + + val pickOrdersById = if (allPickOrderIds.isEmpty()) { + emptyMap() + } else { + pickOrderRepository.findAllById(allPickOrderIds).associateBy { it.id } + } + + val allPickOrderLines = if (allPickOrderIds.isEmpty()) { + emptyList() + } else { + pickOrderLineRepository.findAllByPickOrderIdInAndDeletedFalse(allPickOrderIds) + } + val pickOrderLinesByPickOrderId = allPickOrderLines.groupBy { it.pickOrder?.id } + + val pickOrderLineIds = allPickOrderLines.mapNotNull { it.id }.distinct() + val allStockOutLines = if (pickOrderLineIds.isEmpty()) { + emptyList() + } else { + stockOutLineRepository.findAllByPickOrderLineIdInAndDeletedFalse(pickOrderLineIds) + } + val stockOutLinesByPickOrderLineId = allStockOutLines.groupBy { it.pickOrderLine?.id } + + val allIssues = if (allPickOrderIds.isEmpty()) { + emptyList() + } else { + pickExecutionIssueRepository.findAllByPickOrderIdInAndDeletedFalse(allPickOrderIds) + } + val issuesByPickOrderId = allIssues.groupBy { it.pickOrderId } + // Map each job order group to a single MaterialPickStatusItem return groupedByJobOrder.mapNotNull { (jobOrderId, joPickOrdersForJob) -> if (jobOrderId == null) return@mapNotNull null - val jobOrder = jobOrderRepository.findById(jobOrderId).orElse(null) - ?: return@mapNotNull null + val jobOrder = jobOrdersById[jobOrderId] ?: return@mapNotNull null // Get BOM item (finished good/semi-finished product), not BOM Material item val bomItem = jobOrder.bom?.item @@ -2944,31 +2974,29 @@ open fun getMaterialPickStatus(date: String?): List { val pickOrderIds = joPickOrdersForJob.mapNotNull { it.pickOrderId }.distinct() // Aggregate data from all pick orders for this job order - val allPickOrderLines = pickOrderIds.flatMap { poId -> - pickOrderLineRepository.findByPickOrderId(poId) + val allPickOrderLinesForJob = pickOrderIds.flatMap { poId -> + pickOrderLinesByPickOrderId[poId].orEmpty() } // Get all stock out lines for all pick orders of this job order - val allStockOutLines = allPickOrderLines.flatMap { pol -> - stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!) - .mapNotNull { solInfo -> - stockOutLineRepository.findById(solInfo.id).orElse(null) - } + val allStockOutLinesForJob = allPickOrderLinesForJob.flatMap { pol -> + val polId = pol.id ?: return@flatMap emptyList() + stockOutLinesByPickOrderLineId[polId].orEmpty() } // ✅ 修复:startTime = 第一个 item 开始提料的时间 // 只考虑已经开始的 items(status 不是 pending),取最早的 startTime - val pickStartTime = allStockOutLines + val pickStartTime = allStockOutLinesForJob .filter { it.status != null && it.status != "pending" } .mapNotNull { it.startTime } .minOrNull() // Count total items to pick (number of distinct pick order lines) - val numberOfItemsToPick = allPickOrderLines.size + val numberOfItemsToPick = allPickOrderLinesForJob.size // 已结束行数:有至少一条 completed,或整行全是 rejected 都算「已结束」 - val finishedItemsCount = allPickOrderLines.count { pol -> - val stockOutLinesForPol = allStockOutLines.filter { + val finishedItemsCount = allPickOrderLinesForJob.count { pol -> + val stockOutLinesForPol = allStockOutLinesForJob.filter { it.pickOrderLine?.id == pol.id } stockOutLinesForPol.any { it.status == "completed" } || @@ -2977,13 +3005,13 @@ open fun getMaterialPickStatus(date: String?): List { // 只有当所有 items 都已结束(完成或全部拒绝)时,才返回 endTime val pickEndTime = if (finishedItemsCount == numberOfItemsToPick && numberOfItemsToPick > 0) { - val completedEndTime = allStockOutLines + val completedEndTime = allStockOutLinesForJob .filter { it.status == "completed" } .mapNotNull { it.endTime } .maxOrNull() // 若没有任何 completed 的 endTime(例如全部 rejected),用 pick order 的 completeDate 作为结束时间 completedEndTime ?: pickOrderIds.mapNotNull { poId -> - pickOrderRepository.findById(poId).orElse(null)?.completeDate + pickOrdersById[poId]?.completeDate }.maxOrNull() } else { null @@ -2994,7 +3022,7 @@ open fun getMaterialPickStatus(date: String?): List { // Count total items with issues from all pick orders val numberOfItemsWithIssue = pickOrderIds.sumOf { poId -> - pickExecutionIssueRepository.findByPickOrderIdAndDeletedFalse(poId).size + issuesByPickOrderId[poId]?.size ?: 0 } // Get job order quantity and UOM @@ -3002,9 +3030,7 @@ open fun getMaterialPickStatus(date: String?): List { val uom = jobOrder.bom?.uom?.code // Determine pick status - check if all pick orders are completed - val pickOrders = pickOrderIds.mapNotNull { poId -> - pickOrderRepository.findById(poId).orElse(null) - } + val pickOrders = pickOrderIds.mapNotNull { poId -> pickOrdersById[poId] } val pickStatus = when { pickOrders.isEmpty() -> null pickOrders.all { it.status == com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.COMPLETED } -> "completed" @@ -3013,9 +3039,7 @@ open fun getMaterialPickStatus(date: String?): List { } // Use the first pick order code (or combine if multiple) - val pickOrderCode = pickOrderIds.firstOrNull()?.let { poId -> - pickOrderRepository.findById(poId).orElse(null)?.code - } + val pickOrderCode = pickOrderIds.firstOrNull()?.let { poId -> pickOrdersById[poId]?.code } MaterialPickStatusItem( id = jobOrderId, // Use jobOrderId as id diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt index 3422587..5669fec 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt @@ -12,6 +12,14 @@ import org.springframework.transaction.annotation.Transactional @Repository interface PickExecutionIssueRepository : JpaRepository { fun findByPickOrderIdAndDeletedFalse(pickOrderId: Long): List + @Query( + """ + SELECT p FROM PickExecutionIssue p + WHERE p.deleted = false + AND p.pickOrderId IN :pickOrderIds + """ + ) + fun findAllByPickOrderIdInAndDeletedFalse(@Param("pickOrderIds") pickOrderIds: List): List fun findByPickOrderLineIdAndDeletedFalse(pickOrderLineId: Long): List fun findByLotIdAndDeletedFalse(lotId: Long): List fun findByPickOrderLineIdAndLotIdAndDeletedFalse( diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt index 4f78083..bd20279 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt @@ -12,7 +12,28 @@ interface StockTakeRecordRepository : AbstractRepository fun findAllByStockTakeIdAndDeletedIsFalse(stockTakeId: Long): List; fun findAllByStockTakeIdInAndDeletedIsFalse(stockTakeIds: Collection): List; fun findAllByStockTakeRoundIdAndDeletedIsFalse(stockTakeRoundId: Long): List; + fun findAllByStockTakeRoundIdInAndDeletedIsFalse(stockTakeRoundIds: Collection): List; fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeRecord?; + fun findAllByStockTakeIdAndWarehouseIdInAndDeletedIsFalse( + stockTakeId: Long, + warehouseIds: Collection + ): List + + @Query( + """ + SELECT r + FROM StockTakeRecord r + WHERE r.deleted = false + AND r.stockTake.id = :stockTakeId + AND r.stockTakeSection = :stockTakeSection + AND r.approverStockTakeQty IS NULL + AND (r.pickerFirstStockTakeQty IS NOT NULL OR r.pickerSecondStockTakeQty IS NOT NULL) + """ + ) + fun findPendingApproverRecordsByStockTakeAndSection( + @Param("stockTakeId") stockTakeId: Long, + @Param("stockTakeSection") stockTakeSection: String + ): List @Query(""" SELECT sl FROM StockLedger sl 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 3a25122..eca5189 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 @@ -616,7 +616,7 @@ open class StockTakeRecordService( val warehouse = ill.warehouse val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) + //.subtract(ill.holdQty ?: BigDecimal.ZERO) val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) { stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)] @@ -770,7 +770,7 @@ open class StockTakeRecordService( val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) + //.subtract(ill.holdQty ?: BigDecimal.ZERO) availableQty.compareTo(BigDecimal.ZERO) > 0 || recordKeySet.contains(Pair(lotId, whId)) } @@ -873,7 +873,7 @@ open class StockTakeRecordService( val warehouse = ill.warehouse val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) + //.subtract(ill.holdQty ?: BigDecimal.ZERO) InventoryLotDetailResponse( id = ill.id ?: 0L, @@ -972,7 +972,7 @@ open class StockTakeRecordService( val warehouse = ill.warehouse val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) + // .subtract(ill.holdQty ?: BigDecimal.ZERO) val inventoryLotLineId = ill.id val stockTakeLine = if (effectiveStockTakeId != null && inventoryLotLineId != null) { @@ -1062,7 +1062,7 @@ open class StockTakeRecordService( val warehouse = ill.warehouse val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) + // .subtract(ill.holdQty ?: BigDecimal.ZERO) val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) { stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)] @@ -1161,7 +1161,7 @@ open class StockTakeRecordService( // 2. 计算 availableQty val availableQty = (inventoryLotLine.inQty ?: BigDecimal.ZERO) .subtract(inventoryLotLine.outQty ?: BigDecimal.ZERO) - .subtract(inventoryLotLine.holdQty ?: BigDecimal.ZERO) + //.subtract(inventoryLotLine.holdQty ?: BigDecimal.ZERO) // 3. 新建、更新預建記錄之第一次盤點、或第二次盤點 val stockTakeRecord: StockTakeRecord = if (request.stockTakeRecordId != null) { @@ -1303,12 +1303,8 @@ open class StockTakeRecordService( println("Found ${inventoryLotLines.size} inventory lot lines") // 4. 使用 stockTakeId 获取已创建的记录,建立映射以排除它们 - val existingRecordsMap = stockTakeRecordRepository.findAll() - .filter { - !it.deleted && - it.stockTake?.id == request.stockTakeId && - it.warehouse?.id in warehouseIds - } + val existingRecordsMap = stockTakeRecordRepository + .findAllByStockTakeIdAndWarehouseIdInAndDeletedIsFalse(request.stockTakeId, warehouseIds) .associateBy { Pair(it.inventoryLotId ?: 0L, it.warehouse?.id ?: 0L) } @@ -1350,7 +1346,7 @@ open class StockTakeRecordService( // 计算 availableQty val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) + // .subtract(ill.holdQty ?: BigDecimal.ZERO) // 使用 availableQty 作为 qty,badQty 为 0 val qty = availableQty @@ -1398,12 +1394,6 @@ open class StockTakeRecordService( } } if (successCount > 0) { - val existingRecordsCount = stockTakeRecordRepository.findAll() - .filter { - !it.deleted && - it.stockTake?.id == request.stockTakeId - } - .count() checkAndUpdateStockTakeStatus(request.stockTakeId, request.stockTakeSection) } println("batchSaveStockTakeRecords completed: success=$successCount, errors=$errorCount") @@ -1585,15 +1575,11 @@ open fun batchSaveApproverStockTakeRecords( ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") - val stockTakeRecords = stockTakeRecordRepository.findAll() - .filter { - !it.deleted && - it.stockTake?.id == request.stockTakeId && - it.stockTakeSection == request.stockTakeSection && - // 只处理已经有 picker 盘点记录的行(第一或第二次盘点任意一个非 null 即视为已盘点) - (it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) && - it.approverStockTakeQty == null // 只处理未审批的记录 - } + val stockTakeRecords = stockTakeRecordRepository + .findPendingApproverRecordsByStockTakeAndSection( + stockTakeId = request.stockTakeId, + stockTakeSection = request.stockTakeSection + ) println("Found ${stockTakeRecords.size} records to process") @@ -1711,14 +1697,12 @@ open fun batchSaveApproverStockTakeRecordsAll( val roundStockTakeIds: Set = resolveRoundStockTakeIds(stockTake) - var stockTakeRecords = stockTakeRecordRepository.findAll() + var stockTakeRecords = stockTakeRecordRepository + .findAllByStockTakeIdInAndDeletedIsFalse(roundStockTakeIds) .filter { - !it.deleted && - it.stockTake?.id != null && - it.stockTake!!.id!! in roundStockTakeIds && - // 只处理已经有 picker 盘点记录的行(第一或第二次盘点任意一个非 null 即视为已盘点) - (it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) && - it.approverStockTakeQty == null + // 只处理已经有 picker 盘点记录的行(第一或第二次盘点任意一个非 null 即视为已盘点) + (it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) && + it.approverStockTakeQty == null } val sectionParts = request.stockTakeSections ?.split(",") @@ -1855,6 +1839,111 @@ if (itemParts.isNotEmpty()) { ) } +open fun batchSaveApproverStockTakeRecordsByIds( + request: BatchSaveApproverStockTakeByIdsRequest +): BatchSaveApproverStockTakeRecordResponse { + println("batchSaveApproverStockTakeRecordsByIds called for stockTakeId: ${request.stockTakeId}, ids=${request.recordIds.size}") + if (request.recordIds.isEmpty()) { + return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No record IDs provided")) + } + + val user = userRepository.findById(request.approverId).orElse(null) + val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) + ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") + + val idSet = request.recordIds.toSet() + val stockTakeRecords = stockTakeRecordRepository.findAllById(request.recordIds) + .filter { + !it.deleted && + (it.id in idSet) && + (it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) && + it.approverStockTakeQty == null + } + println("Found ${stockTakeRecords.size} records to process by IDs") + if (stockTakeRecords.isEmpty()) { + return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No records found matching criteria")) + } + + var successCount = 0 + var errorCount = 0 + val errors = mutableListOf() + val processedStockTakes = mutableSetOf>() + stockTakeRecords.forEach { record -> + try { + val qty: BigDecimal + val badQty: BigDecimal + + if (record.pickerSecondStockTakeQty != null && record.pickerSecondStockTakeQty!! > BigDecimal.ZERO) { + qty = record.pickerSecondStockTakeQty!! + badQty = record.pickerSecondBadQty ?: BigDecimal.ZERO + } else { + qty = record.pickerFirstStockTakeQty ?: BigDecimal.ZERO + badQty = record.pickerFirstBadQty ?: BigDecimal.ZERO + } + + val bookQty = record.bookQty ?: BigDecimal.ZERO + val varianceQty = qty.subtract(bookQty) + + record.apply { + this.approverId = request.approverId + this.approverName = user?.name + this.approverStockTakeQty = qty + this.approverBadQty = badQty + this.varianceQty = varianceQty + this.status = "completed" + this.approverTime = java.time.LocalDateTime.now() + this.lastSelect = if ( + record.pickerSecondStockTakeQty != null && + record.pickerSecondStockTakeQty!! > BigDecimal.ZERO + ) 2 else 1 + if (this.stockTakeEndTime == null) { + this.stockTakeEndTime = java.time.LocalDateTime.now() + } + } + + stockTakeRecordRepository.save(record) + + if (varianceQty != BigDecimal.ZERO) { + try { + applyVarianceAdjustment(record.stockTake ?: stockTake, record, qty, varianceQty, request.approverId) + } catch (e: Exception) { + logger.error("Failed to apply variance adjustment for record ${record.id}", e) + errorCount++ + errors.add("Record ${record.id}: ${e.message}") + return@forEach + } + } else { + completeStockTakeLineForApproverNoVariance(record.stockTake ?: stockTake, record, qty) + } + + val stId = record.stockTake?.id + val section = record.stockTakeSection + if (stId != null && section != null) { + processedStockTakes.add(Pair(stId, section)) + } + successCount++ + } catch (e: Exception) { + errorCount++ + val errorMsg = "Error processing record ${record.id}: ${e.message}" + errors.add(errorMsg) + logger.error(errorMsg, e) + } + } + + if (successCount > 0) { + processedStockTakes.forEach { (stId, section) -> + checkAndUpdateStockTakeStatus(stId, section) + } + } + + println("batchSaveApproverStockTakeRecordsByIds completed: success=$successCount, errors=$errorCount") + return BatchSaveApproverStockTakeRecordResponse( + successCount = successCount, + errorCount = errorCount, + errors = errors + ) +} + /** * stockTakeRecord 上存的是 inventory_lot.id(批次),不是 inventory_lot_line.id;用倉庫 + 批次找唯一庫存行。 */ @@ -1980,10 +2069,11 @@ private fun applyVarianceAdjustment( // 避免同一批多筆盤虧時每筆都用同一個 inventory.onHandQty 導致 balance 錯誤。 val itemIdForLedger = inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found for stock take ledger") - val latestLedger = stockLedgerRepository.findLatestByItemId(itemIdForLedger).firstOrNull() + val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger) val previousBalance = latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() val newBalance = previousBalance - qtyToRemove.toDouble() + val stockLedger = StockLedger().apply { this.inventory = inventory this.itemId = inventoryLot.item?.id @@ -1996,7 +2086,8 @@ private fun applyVarianceAdjustment( this.uomId = latestLine.stockUom?.uom?.id ?: inventory.uom?.id this.date = LocalDate.now() } - stockLedgerRepository.saveAndFlush(stockLedger) + + stockLedgerRepository.save(stockLedger) val newOutQty = (latestLine.outQty ?: zero).add(qtyToRemove) val updateRequest = SaveInventoryLotLineRequest( @@ -2058,10 +2149,11 @@ private fun applyVarianceAdjustment( val itemIdForLedger = inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found for stock take ledger (in)") - val latestLedger = stockLedgerRepository.findLatestByItemId(itemIdForLedger).firstOrNull() + val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger) val previousBalance = latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() val newBalance = previousBalance + plusQty.toDouble() + val stockLedger = StockLedger().apply { this.inventory = inventory this.itemId = inventoryLot.item?.id @@ -2074,7 +2166,8 @@ private fun applyVarianceAdjustment( this.uomId = latestLine.stockUom?.uom?.id ?: inventory.uom?.id this.date = LocalDate.now() } - stockLedgerRepository.saveAndFlush(stockLedger) + + stockLedgerRepository.save(stockLedger) } } open fun updateStockTakeRecordStatusToNotMatch(stockTakeRecordId: Long): StockTakeRecord { @@ -2146,7 +2239,7 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch( val warehouse = ill.warehouse val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) + // .subtract(ill.holdQty ?: BigDecimal.ZERO) val inventoryLotLineId = ill.id val stockTakeLine = if (effectiveStockTakeId != null && inventoryLotLineId != null) { @@ -2238,7 +2331,7 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch( val warehouse = ill.warehouse val availableQty = (ill.inQty ?: BigDecimal.ZERO) .subtract(ill.outQty ?: BigDecimal.ZERO) - .subtract(ill.holdQty ?: BigDecimal.ZERO) + //.subtract(ill.holdQty ?: BigDecimal.ZERO) val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) { stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)] diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt index 61cc04d..66cd811 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt @@ -300,6 +300,28 @@ class StockTakeRecordController( )) } } + @PostMapping("/batchSaveApproverStockTakeRecordsByIds") + fun batchSaveApproverStockTakeRecordsByIds( + @RequestBody request: BatchSaveApproverStockTakeByIdsRequest + ): ResponseEntity { + return try { + val result = stockOutRecordService.batchSaveApproverStockTakeRecordsByIds(request) + logger.info("Batch approver save by ids completed: success=${result.successCount}, errors=${result.errorCount}") + ResponseEntity.ok(result) + } catch (e: IllegalArgumentException) { + logger.warn("Validation error: ${e.message}") + ResponseEntity.badRequest().body(mapOf( + "error" to "VALIDATION_ERROR", + "message" to (e.message ?: "Validation failed") + )) + } catch (e: Exception) { + logger.error("Error batch saving approver stock take records by ids", e) + ResponseEntity.status(500).body(mapOf( + "error" to "INTERNAL_ERROR", + "message" to (e.message ?: "Failed to batch save approver stock take records by ids") + )) + } + } @PostMapping("/updateStockTakeRecordStatusToNotMatch") fun updateStockTakeRecordStatusToNotMatch( @RequestParam stockTakeRecordId: 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 b973363..4c25691 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 @@ -143,6 +143,12 @@ data class BatchSaveApproverStockTakeAllRequest( val stockTakeSections: String? = null, ) +data class BatchSaveApproverStockTakeByIdsRequest( + val stockTakeId: Long, + val approverId: Long, + val recordIds: List, +) + data class BatchSaveApproverStockTakeRecordResponse( val successCount: Int, val errorCount: Int, diff --git a/src/main/resources/db/changelog/changes/20260427_01_Enson/01_alter_stock_take.sql b/src/main/resources/db/changelog/changes/20260427_01_Enson/01_alter_stock_take.sql new file mode 100644 index 0000000..04962e8 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260427_01_Enson/01_alter_stock_take.sql @@ -0,0 +1,5 @@ +--liquibase formatted sql + +--changeset Enson:20260427-01 +CREATE INDEX idx_ledger_item_deleted_date_id +ON stock_ledger (itemId, deleted, date, id); \ No newline at end of file