From 2dee12d5c872568901b2871c73f85dba42795397 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 23 Mar 2026 15:59:57 +0800 Subject: [PATCH] update --- .../service/PickExecutionIssueService.kt | 6 + .../fpsms/modules/stock/entity/StockLedger.kt | 5 + .../stock/entity/StockTakeLineRepository.kt | 4 + .../stock/entity/StockTakeRecordRepository.kt | 1 + .../stock/service/StockInLineService.kt | 1 + .../stock/service/StockOutLineService.kt | 4 + .../stock/service/StockTakeRecordService.kt | 188 ++++++++++-------- .../stock/web/model/StockTakeRecordReponse.kt | 1 + .../20260323_02_Enson/01_alter_stock_take.sql | 8 + 9 files changed, 133 insertions(+), 85 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20260323_02_Enson/01_alter_stock_take.sql 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 2eb5b77..a78fc7c 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 @@ -40,6 +40,7 @@ import com.ffii.fpsms.modules.common.SecurityUtils import com.ffii.fpsms.modules.stock.entity.StockOut 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.entity.StockLedger @@ -64,6 +65,7 @@ open class PickExecutionIssueService( private val joPickOrderRepository: JoPickOrderRepository, private val joPickOrderRecordRepository: JoPickOrderRecordRepository, private val itemsRepository: ItemsRepository, + private val itemUomRespository: ItemUomRespository, private val stockLedgerRepository: StockLedgerRepository, private val deliveryOrderRepository: DeliveryOrderRepository, private val suggestedPickLotRepository: SuggestPickLotRepository, @@ -2357,6 +2359,8 @@ private fun createStockLedgerForStockOut( this.type = ledgerTypeToUse this.itemId = item.id this.itemCode = item.code + this.uomId = inventory.uom?.id + ?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id this.date = LocalDate.now() } @@ -2874,6 +2878,8 @@ private fun createStockLedgerForStockOutWithBalance( this.type = ledgerTypeToUse this.itemId = item.id this.itemCode = item.code + this.uomId = inventory.uom?.id + ?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id this.date = LocalDate.now() } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedger.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedger.kt index bbd3790..5f868a8 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedger.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedger.kt @@ -32,6 +32,11 @@ open class StockLedger: BaseEntity() { @Column(name = "balance") open var balance: Double? = null + + /** 物料庫存單位對應之 uom_conversion.id(與 item_uom / inventory 之 stock UOM 一致) */ + @Column(name = "uomId") + open var uomId: Long? = null + @Column(name = "type") open var type: String? = null @Column(name = "itemId") diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt index b1321fc..496875b 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt @@ -8,4 +8,8 @@ import java.io.Serializable interface StockTakeLineRepository : AbstractRepository { fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeLine?; fun findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId: Serializable, stockTakeId: Serializable): StockTakeLine?; + fun findAllByStockTakeIdInAndInventoryLotLineIdInAndDeletedIsFalse( + stockTakeIds: Collection, + inventoryLotLineIds: Collection + ): List } \ No newline at end of file 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 4b23a13..36397a6 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 @@ -10,6 +10,7 @@ import java.time.LocalDate @Repository interface StockTakeRecordRepository : AbstractRepository { fun findAllByStockTakeIdAndDeletedIsFalse(stockTakeId: Long): List; + fun findAllByStockTakeIdInAndDeletedIsFalse(stockTakeIds: Collection): List; fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeRecord?; @Query(""" diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index 255c094..e76092a 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -1162,6 +1162,7 @@ open class StockInLineService( this.type = stockInLine.type this.itemId = item.id this.itemCode = item.code + this.uomId = inventory.uom?.id ?: itemUomService.findStockUnitByItemId(item.id!!)?.uom?.id this.date = LocalDate.now() } 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 6ba9fa8..799eb81 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 @@ -1509,6 +1509,8 @@ affectedConsoCodes.forEach { consoCode -> this.type = stockOutLine.type this.itemId = item.id this.itemCode = item.code + this.uomId = inventory.uom?.id + ?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id this.date = LocalDate.now() } @@ -1605,6 +1607,8 @@ affectedConsoCodes.forEach { consoCode -> this.type = "NOR" this.itemId = item.id this.itemCode = item.code + this.uomId = inventory.uom?.id + ?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id this.date = LocalDate.now() } 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 9ab9a6a..2d93d08 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 @@ -204,6 +204,7 @@ class StockTakeRecordService( currentStockTakeItemNumber = 0, totalInventoryLotNumber = totalInventoryLotNumber, stockTakeId = latestStockTake?.id ?: 0, + stockTakeRoundId = latestStockTake?.stockTakeRoundId ?: latestStockTake?.id, stockTakerName = stockTakerName, TotalItemNumber = totalItemNumber, approverName = approverName, @@ -250,19 +251,28 @@ class StockTakeRecordService( emptySet() } - // 4. 如果有 stockTakeId,则预先把这一轮相关的 stockTakeRecord 查出来建 map(跟 section 版类似) - val stockTakeRecordsMap = if (stockTakeId != null) { - val allStockTakeRecords = stockTakeRecordRepository.findAll() - .filter { - !it.deleted && - it.stockTake?.id != null && - it.stockTake!!.id!! in roundStockTakeIds && - it.warehouse?.id in warehouseIds + // 4. 如果有 stockTakeId,则预先把这一轮相关的 stockTakeRecord 查出来建 map(避免全表扫描 + N^2) + val roundStockTakeRecords = if (stockTakeId != null && roundStockTakeIds.isNotEmpty()) { + stockTakeRecordRepository.findAllByStockTakeIdInAndDeletedIsFalse(roundStockTakeIds) + .filter { it.warehouse?.id in warehouseIds } + } else { + emptyList() + } + val stockTakeRecordsMap = roundStockTakeRecords + .groupBy { Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L) } + .mapValues { (_, records) -> records.maxByOrNull { it.id ?: 0L }!! } + + // 4.1 预先拉取本轮 stockTakeLine,避免每行调用 repository 形成 N+1 + val stockTakeLineMap = if (stockTakeId != null && roundStockTakeRecords.isNotEmpty()) { + val lineIds = inventoryLotLines.mapNotNull { it.id }.toSet() + val roundIds = roundStockTakeIds + stockTakeLineRepository.findAllByStockTakeIdInAndInventoryLotLineIdInAndDeletedIsFalse(roundIds, lineIds) + .associateBy { + Pair( + it.inventoryLotLine?.id ?: 0L, + it.stockTake?.id ?: 0L + ) } - - allStockTakeRecords.associateBy { - Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L) - } } else { emptyMap() } @@ -284,10 +294,7 @@ class StockTakeRecordService( val inventoryLotLineId = ill.id val stockTakeLine = if (stockTakeRecord != null && stockTakeRecord.stockTake?.id != null && inventoryLotLineId != null) { - stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse( - inventoryLotLineId, - stockTakeRecord.stockTake!!.id!! - ) + stockTakeLineMap[Pair(inventoryLotLineId, stockTakeRecord.stockTake!!.id!!)] } else { null } @@ -357,36 +364,70 @@ class StockTakeRecordService( return RecordsRes(paginatedResult, filteredResults.size) } open fun AllApproverStockTakeList(): List { - // 1. 获取所有不同的 stockTakeSection(从 warehouse 表) + // 1. 获取 section 与仓库基础数据 val allWarehouses = warehouseRepository.findAllByDeletedIsFalse() val distinctSections = allWarehouses .mapNotNull { it.stockTakeSection } .distinct() - .filter { !it.isBlank() } + .filter { it.isNotBlank() } + if (distinctSections.isEmpty()) return emptyList() - if (distinctSections.isEmpty()) { - return emptyList() - } - - // 2. 批量获取所有相关的数据(优化性能 - 只查询一次) - // 2.1 按 section 分组 warehouse val warehousesBySection = allWarehouses - .filter { it.stockTakeSection != null && !it.stockTakeSection!!.isBlank() } + .filter { !it.stockTakeSection.isNullOrBlank() } .groupBy { it.stockTakeSection!! } + val sectionByWarehouseId = allWarehouses + .mapNotNull { w -> w.id?.let { id -> id to (w.stockTakeSection ?: "") } } + .toMap() + val allWarehouseIds = allWarehouses.mapNotNull { it.id } - // 2.2 批量获取所有 stock_take 记录(只获取一次),按 stockTakeSection 分组 + // 2. 批量加载核心数据(一次拿齐,避免循环中 repository 查询) val allStockTakes = stockTakeRepository.findAll() - .filter { !it.deleted } - .groupBy { it.stockTakeSection } - - // 2.3 批量获取所有 stocktakeRecord(只获取一次) - val allStockTakeRecords = stockTakeRecordRepository.findAll() - .filter { !it.deleted } + .filter { !it.deleted && !it.stockTakeSection.isNullOrBlank() } + .groupBy { it.stockTakeSection!! } + val latestStockTakeBySection = allStockTakes.mapValues { (_, takes) -> + takes.maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } + } + val latestStockTakeIds = latestStockTakeBySection.values.mapNotNull { it?.id }.toSet() + val allStockTakeRecords = if (latestStockTakeIds.isEmpty()) { + emptyList() + } else { + stockTakeRecordRepository.findAllByStockTakeIdInAndDeletedIsFalse(latestStockTakeIds) + } + val allInventoryLotLines = if (allWarehouseIds.isEmpty()) { + emptyList() + } else { + inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(allWarehouseIds) + } + val inventoryLotLinesBySection = allInventoryLotLines.groupBy { ill -> + sectionByWarehouseId[ill.warehouse?.id] ?: "" + } - // 2.4 按 warehouseId 分组 stocktakeRecord,便于快速查找 - val recordsByWarehouseId = allStockTakeRecords - .filter { it.warehouse?.id != null } - .groupBy { it.warehouse!!.id } + // 3. 预计算:同一 stockTake 下已盘点 key、人员名称、notMatch + val recordKeyByStockTakeId = allStockTakeRecords + .groupBy { it.stockTake?.id ?: -1L } + .mapValues { (_, records) -> + records.mapNotNull { r -> + val lotId = r.inventoryLotId + val whId = r.warehouse?.id + if (lotId != null && whId != null) Pair(lotId, whId) else null + }.toSet() + } + val latestStockTakerNameByStockTakeAndSection = allStockTakeRecords + .filter { !it.stockTakeSection.isNullOrBlank() && it.stockTake?.id != null && it.stockTakerName != null } + .groupBy { Pair(it.stockTake!!.id!!, it.stockTakeSection!!) } + .mapValues { (_, records) -> + records.maxByOrNull { it.stockTakeStartTime ?: LocalDateTime.MIN }?.stockTakerName + } + val latestApproverNameByStockTakeAndSection = allStockTakeRecords + .filter { !it.stockTakeSection.isNullOrBlank() && it.stockTake?.id != null && it.approverName != null } + .groupBy { Pair(it.stockTake!!.id!!, it.stockTakeSection!!) } + .mapValues { (_, records) -> + records.maxByOrNull { it.stockTakeStartTime ?: LocalDateTime.MIN }?.approverName + } + val hasNotMatchByStockTakeAndSection = allStockTakeRecords + .filter { it.stockTake?.id != null && !it.stockTakeSection.isNullOrBlank() && it.status == "notMatch" } + .map { Pair(it.stockTake!!.id!!, it.stockTakeSection!!) } + .toSet() val result = mutableListOf() var idCounter = 1L @@ -427,52 +468,15 @@ class StockTakeRecordService( } else { null } - val stockTakerName = if (latestStockTake != null) { - // 从该 stock take 的 records 中获取最新的 stockTakerName - val recordsForStockTake = allStockTakeRecords - .filter { - it.stockTake?.id == latestStockTake.id && - it.stockTakeSection == stockTakeSection && - it.stockTakerName != null - } - .sortedByDescending { it.stockTakeStartTime ?: LocalDateTime.MIN } - - recordsForStockTake.firstOrNull()?.stockTakerName - } else { - null - } - val approverName = if (latestStockTake != null) { - // 从该 stock take 的 records 中获取最新的 approverName - val recordsForStockTake = allStockTakeRecords - .filter { - it.stockTake?.id == latestStockTake.id && - it.stockTakeSection == stockTakeSection && - it.approverName != null - } - .sortedByDescending { it.stockTakeStartTime ?: LocalDateTime.MIN } - - recordsForStockTake.firstOrNull()?.approverName - } else { - null - } - val reStockTakeTrueFalse = if (latestStockTake != null) { - // 检查该 stock take 下该 section 的记录中是否有 notMatch 状态 - val hasNotMatch = allStockTakeRecords.any { - !it.deleted && - it.stockTake?.id == latestStockTake.id && - it.stockTakeSection == stockTakeSection && - it.status == "notMatch" - } - hasNotMatch - } else { - false - } + val stockTakerName = latestStockTake?.id?.let { latestStockTakerNameByStockTakeAndSection[Pair(it, stockTakeSection)] } + val approverName = latestStockTake?.id?.let { latestApproverNameByStockTakeAndSection[Pair(it, stockTakeSection)] } + val reStockTakeTrueFalse = latestStockTake?.id?.let { hasNotMatchByStockTakeAndSection.contains(Pair(it, stockTakeSection)) } ?: false // 9. 计算 TotalItemNumber / TotalInventoryLotNumber(只统计“需要盘点的行”): // 规则与前端 Picker / Approver 明细一致: // - availableQty > 0,或 // - 在 latestStockTake 下已经有 stockTakeRecord(即本轮参与盘点的 lot) - val inventoryLotLinesForSection = - inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) + val inventoryLotLinesForSection = inventoryLotLinesBySection[stockTakeSection] ?: emptyList() + val recordedKeysForLatest = latestStockTake?.id?.let { recordKeyByStockTakeId[it] } ?: emptySet() val relevantInventoryLotLines = inventoryLotLinesForSection.filter { ill -> val inventoryLot = ill.inventoryLot @@ -485,12 +489,7 @@ class StockTakeRecordService( val holdQty = ill.holdQty ?: BigDecimal.ZERO val availableQty = inQty.subtract(outQty).subtract(holdQty) - val hasRecordForLatest = allStockTakeRecords.any { record -> - !record.deleted && - record.stockTake?.id == latestStockTake.id && - record.inventoryLotId == inventoryLot.id && - record.warehouse?.id == warehouse.id - } + val hasRecordForLatest = recordedKeysForLatest.contains(Pair(inventoryLot.id, warehouse.id)) availableQty.compareTo(BigDecimal.ZERO) > 0 || hasRecordForLatest } } @@ -515,6 +514,7 @@ class StockTakeRecordService( currentStockTakeItemNumber = 0, // 临时设为 0,测试性能 totalInventoryLotNumber = totalInventoryLotNumber, // 临时设为 0,测试性能 stockTakeId = latestStockTake?.id ?: 0, + stockTakeRoundId = latestStockTake?.stockTakeRoundId ?: latestStockTake?.id, stockTakerName = stockTakerName, approverName = approverName, TotalItemNumber = totalItemNumber, @@ -528,7 +528,23 @@ class StockTakeRecordService( ) } - return result.sortedBy { it.stockTakeSession } + // 10. 以 stockTakeRoundId 聚合为“每轮一张卡”;旧资料 roundId 为空时退回 stockTakeId + val groupedByRound = result.groupBy { it.stockTakeRoundId ?: it.stockTakeId } + val roundCards = groupedByRound.values.mapIndexed { idx, cardsInRound -> + val representative = cardsInRound.maxByOrNull { it.startTime ?: LocalDateTime.MIN } ?: cardsInRound.first() + val totalItems = cardsInRound.sumOf { it.TotalItemNumber } + val totalLots = cardsInRound.sumOf { it.totalInventoryLotNumber } + + representative.copy( + id = (idx + 1).toLong(), + stockTakeSession = "", + TotalItemNumber = totalItems, + totalInventoryLotNumber = totalLots, + stockTakeSectionDescription = null + ) + } + + return roundCards.sortedByDescending { it.planStartDate ?: LocalDate.MIN } } open fun getInventoryLotDetailsByWarehouseCode(warehouseCode: String): List { @@ -1429,6 +1445,7 @@ private fun applyVarianceAdjustment( this.stockOutLine = stockOutLine this.balance = newBalance this.type = "TKE" + this.uomId = inventoryLotLine.stockUom?.uom?.id ?: inventory.uom?.id this.date = LocalDate.now() } stockLedgerRepository.save(stockLedger) @@ -1509,6 +1526,7 @@ private fun applyVarianceAdjustment( this.stockInLine = stockInLine this.balance = newBalance this.type = "TKE" + this.uomId = inventoryLotLine.stockUom?.uom?.id ?: inventory.uom?.id this.date = LocalDate.now() } stockLedgerRepository.save(stockLedger) 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 02c4dc0..e9aaf78 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 @@ -19,6 +19,7 @@ data class AllPickedStockTakeListReponse( @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") val endTime: LocalDateTime?, val stockTakeId: Long, + val stockTakeRoundId: Long?, val stockTakerName: String?, val approverName: String?, val TotalItemNumber: Int, diff --git a/src/main/resources/db/changelog/changes/20260323_02_Enson/01_alter_stock_take.sql b/src/main/resources/db/changelog/changes/20260323_02_Enson/01_alter_stock_take.sql new file mode 100644 index 0000000..da174dd --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260323_02_Enson/01_alter_stock_take.sql @@ -0,0 +1,8 @@ +-- liquibase formatted sql +-- changeset Enson:alter_do_pick_order_record_box_number + +ALTER TABLE `fpsmsdb`.`do_pick_order_record` +ADD COLUMN `BoxNumber` INT NULL AFTER `deleted`; + +ALTER TABLE `fpsmsdb`.`stock_ledger` +ADD COLUMN `uomId` INT NULL AFTER `balance`;