From 4f7dc6687dd7f7529f5aaace447fbe8c04f815ba Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sun, 29 Mar 2026 18:42:53 +0800 Subject: [PATCH] update expiry lot handle in jo/do and show qty will submit and search invenotry by itemid and uomid --- .../modules/master/service/ItemUomService.kt | 23 +++++++- .../entity/InventoryLotLineRepository.kt | 3 + .../stock/entity/InventoryRepository.kt | 3 + .../stock/service/StockOutLineService.kt | 57 +++++++++---------- .../stock/service/StockTakeRecordService.kt | 2 +- 5 files changed, 56 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt index b5061d7..2b50394 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt @@ -12,13 +12,17 @@ import kotlin.jvm.optionals.getOrNull import org.slf4j.Logger import org.slf4j.LoggerFactory import com.ffii.fpsms.modules.master.entity.ItemsRepository +import com.ffii.fpsms.modules.stock.entity.Inventory +import com.ffii.fpsms.modules.stock.entity.InventoryRepository + @Service open class ItemUomService( val uomConversionService: UomConversionService, val itemUomRespository: ItemUomRespository, val currencyService: CurrencyService, - val itemsRepository: ItemsRepository + val itemsRepository: ItemsRepository, + private val inventoryRepository: InventoryRepository, ) { val logger = org.slf4j.LoggerFactory.getLogger(this::class.java) @@ -65,6 +69,23 @@ open class ItemUomService( return itemUomRespository.findByItemIdAndBaseUnitIsTrueAndDeletedIsFalse(itemId) } + /** + * 同一 [itemId] 可能對應多筆 [Inventory](不同 uom)。 + * 優先取 `inventory.uomId` 與 `item_uom` 中 `baseUnit = true` 之列的 `uomId` 一致的那一筆; + * 若無 base 設定或無匹配列,則回退為 `findFirstByItemIdAndDeletedIsFalseOrderByIdAsc`。 + */ + open fun findInventoryForItemBaseUom(itemId: Long): Inventory? { + val base = findBaseUnitByItemId(itemId) + val uomId = base?.uom?.id + if (uomId != null) { + val match = inventoryRepository.findFirstByItemIdAndUomIdAndDeletedIsFalseOrderByIdAsc(itemId, uomId) + if (match != null) { + return match + } + } + return inventoryRepository.findFirstByItemIdAndDeletedIsFalseOrderByIdAsc(itemId) + } + open fun findPickingUnitByItemId(itemId: Long): ItemUom? { return itemUomRespository.findByItemIdAndPickingUnitIsTrueAndDeletedIsFalse(itemId) } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt index 48a3420..dfd9434 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt @@ -73,6 +73,9 @@ interface InventoryLotLineRepository : AbstractRepository): List + @Query("SELECT ill FROM InventoryLotLine ill WHERE ill.warehouse.id IN :warehouseIds AND ill.deleted = false AND ill.status = 'available'") + fun findAllByWarehouseIdInAndDeletedIsFalseAndStatusIsAvailable(@Param("warehouseIds") warehouseIds: List): List + @Query(""" SELECT ill FROM InventoryLotLine ill WHERE ill.deleted = false diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt index 8b907a8..6e0f5f9 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt @@ -45,4 +45,7 @@ interface InventoryRepository: AbstractRepository { fun findAllByItemIdAndDeletedIsFalse(itemId: Long): List fun findFirstByItemIdAndDeletedIsFalseOrderByIdAsc(itemId: Long): Inventory? + + /** 與 item_uom 中 baseUnit=true 之 uomId 對齊時使用,避免同一 item 多筆 inventory 時 NonUniqueResult */ + fun findFirstByItemIdAndUomIdAndDeletedIsFalseOrderByIdAsc(itemId: Long, uomId: Long): Inventory? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 34f9c09..1f7d16f 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 @@ -38,6 +38,7 @@ import com.ffii.fpsms.modules.common.CodeGenerator import org.springframework.context.annotation.Lazy import com.ffii.fpsms.modules.bag.service.BagService import com.ffii.fpsms.modules.pickOrder.service.PickOrderService +import com.ffii.fpsms.modules.master.service.ItemUomService import com.ffii.fpsms.modules.common.SecurityUtils import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository import com.ffii.fpsms.modules.stock.entity.InventoryRepository @@ -71,7 +72,8 @@ private val inventoryLotLineService: InventoryLotLineService, private val pickOrderService: PickOrderService, private val stockLedgerRepository: StockLedgerRepository, private val inventoryRepository: InventoryRepository, - private val pickExecutionIssueRepository: PickExecutionIssueRepository + private val pickExecutionIssueRepository: PickExecutionIssueRepository, + private val itemUomService: ItemUomService, ): AbstractBaseEntityService(jdbcDao, stockOutLineRepository) { private fun isEndStatus(status: String?): Boolean { val s = status?.trim()?.lowercase() ?: return false @@ -1190,7 +1192,10 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { val processedIds = mutableListOf() try { - // 1) Bulk load all lot lines and inventories + // 1) Bulk load lot lines(下方會用 lotLines 取批次/袋裝等) + // 注意:勿呼叫 inventoryRepository.findByItemId —— 若歷史資料同一 item 有多筆 inventory, + // Spring Data 的 Optional 單筆查詢會拋 NonUniqueResultException(「unique result: 2」)。 + // 先前預載的 inventories 從未使用(save 已註解),已移除以避免批量提交無故失敗。 val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId } println("Loading ${lotLineIds.size} lot lines...") val lotLines = if (lotLineIds.isNotEmpty()) { @@ -1199,12 +1204,6 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { emptyMap() } - val itemIds = lotLines.values.mapNotNull { it.inventoryLot?.item?.id } - println("Loading ${itemIds.size} inventories...") - val inventories = itemIds.mapNotNull { itemId -> - inventoryRepository.findByItemId(itemId).orElse(null) - }.associateBy { it.item?.id } - // 2) Bulk load all stock out lines to get current quantities val stockOutLineIds = request.lines.map { it.stockOutLineId } println("Loading ${stockOutLineIds.size} stock out lines...") @@ -1292,8 +1291,8 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { // ✅ 修复:在更新 inventory_lot_line 之前获取 inventory 的当前 onHandQty val item = stockOutLine.item - val inventoryBeforeUpdate = item?.let { - inventoryRepository.findByItemId(it.id!!).orElse(null) + val inventoryBeforeUpdate = item?.id?.let { itemId -> + itemUomService.findInventoryForItemBaseUom(itemId) } val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() @@ -1314,8 +1313,8 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { } else if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId == null) { // ✅ 修复:即使没有 inventoryLotLineId,也应该获取 inventory.onHandQty val item = stockOutLine.item - val inventoryBeforeUpdate = item?.let { - inventoryRepository.findByItemId(it.id!!).orElse(null) + val inventoryBeforeUpdate = item?.id?.let { itemId -> + itemUomService.findInventoryForItemBaseUom(itemId) } val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() @@ -1370,9 +1369,8 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { } } - // 4) 移除:不需要保存 lotLines 和 inventories,因为它们没有被修改 + // 4) 移除:不需要保存 lotLines,因为它们没有被修改 // inventoryLotLineRepository.saveAll(lotLines.values.toList()) - // inventoryRepository.saveAll(inventories.values.toList()) // ✅ 修复:批处理完成后,检查所有受影响的 pick order lines 是否应该标记为完成 val affectedPickOrderIds = request.lines @@ -1394,7 +1392,7 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { try { checkIsStockOutLineCompleted(pickOrderLineId) } catch (e: Exception) { - println(" ✗ Error checking pick order line $pickOrderLineId: ${e.message}") + println("Error checking pick order line $pickOrderLineId: ${e.message}") } } val affectedConsoCodes = affectedPickOrderIds @@ -1405,16 +1403,16 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { .filter { !it.isNullOrBlank() } .distinct() -println("=== Checking completion by consoCode for ${affectedConsoCodes.size} affected consoCodes after batch submit ===") -affectedConsoCodes.forEach { consoCode -> - try { - val result = pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode) - println(" -> checkAndCompletePickOrderByConsoCode($consoCode) result: code=${result.code}, message=${result.message}") - } catch (e: Exception) { - println(" ✗ Error checking completion for consoCode=$consoCode: ${e.message}") - e.printStackTrace() - } -} + println("=== Checking completion by consoCode for ${affectedConsoCodes.size} affected consoCodes after batch submit ===") + affectedConsoCodes.forEach { consoCode -> + try { + val result = pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode) + println(" -> checkAndCompletePickOrderByConsoCode($consoCode) result: code=${result.code}, message=${result.message}") + } catch (e: Exception) { + println("Error checking completion for consoCode=$consoCode: ${e.message}") + e.printStackTrace() + } + } val msg = if (errors.isEmpty()) { "Batch submit success (${processedIds.size} lines)." } else { @@ -1461,7 +1459,7 @@ affectedConsoCodes.forEach { consoCode -> @Transactional private fun createStockLedgerForStockOut(stockOutLine: StockOutLine) { val item = stockOutLine.item ?: return - val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return + val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!) ?: return val outQty = stockOutLine.qty?.toDouble() ?: 0.0 // Use latest ledger balance (same pattern as createStockLedgerForStockIn) so balance is correct when multiple actions run in one transaction @@ -1560,8 +1558,7 @@ affectedConsoCodes.forEach { consoCode -> return } - val inventory = inventoryRepository.findAllByItemIdAndDeletedIsFalse(item.id!!) - .firstOrNull() + val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!) if (inventory == null) { println("${tracePrefix}Skip ledger creation: inventory not found by itemId=${item.id}") return @@ -1948,7 +1945,7 @@ fun applyStockOutLineDelta( val itemId = savedSol.item?.id if (itemId != null) { - val inv = inventoryRepository.findByItemId(itemId).orElse(null) + val inv = itemUomService.findInventoryForItemBaseUom(itemId) if (inv != null) { val zero = BigDecimal.ZERO inv.onHandQty = (inv.onHandQty ?: zero).minus(deltaQty) @@ -1963,7 +1960,7 @@ fun applyStockOutLineDelta( // 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 + val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!) ?: return savedSol val previousBalance = resolvePreviousBalance( itemId = item.id!!, 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 0b3d296..af2ebbf 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 @@ -615,7 +615,7 @@ class StockTakeRecordService( val warehouseIds = warehouses.mapNotNull { it.id } println("Found ${warehouses.size} warehouses for section $stockTakeSection") - val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds) + val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalseAndStatusIsAvailable(warehouseIds) println("Found ${inventoryLotLines.size} inventory lot lines") val stockTakeRecordsMap = if (stockTakeId != null) {