diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt index ce85564..8e5a578 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt @@ -266,6 +266,7 @@ open class ItemsService( return jdbcDao.queryForList(sql.toString(), args); } + /** FP-MTMS Version Checklist | Functions Ref. No. 35 | v1.0.1 | 2026-07-22 */ open fun getPickOrderItemsByPage(args: Map): List> { try { println("=== Debug: getPickOrderItemsByPage in ItemsService ===") @@ -274,10 +275,12 @@ open class ItemsService( val sql = StringBuilder( """ SELECT - CONCAT(po.id, '-', pol.itemId) as id, + CONCAT(po.id, '-', pol.id) as id, po.id as pickOrderId, po.code as pickOrderCode, + pol.id as pickOrderLineId, pol.itemId, + pol.uomId, i.code as itemCode, i.name as itemName, pol.qty as requiredQty, @@ -296,15 +299,16 @@ open class ItemsService( LEFT JOIN ( SELECT il.itemId, + iu.uomId as uomId, SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty FROM inventory_lot_line ill JOIN inventory_lot il ON il.id = ill.inventoryLotId - LEFT JOIN item_uom iu_ratio ON iu_ratio.itemId = il.itemId AND iu_ratio.deleted = false AND iu_ratio.salesUnit = true + JOIN item_uom iu ON iu.id = ill.stockItemUomId AND iu.deleted = false WHERE ill.status = 'available' AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) - GROUP BY il.itemId - ) inv ON inv.itemId = pol.itemId + GROUP BY il.itemId, iu.uomId + ) inv ON inv.itemId = pol.itemId AND inv.uomId = pol.uomId WHERE po.deleted = false AND po.consoCode IS NULL """ @@ -752,56 +756,66 @@ open class ItemsService( ) } + /** + * FP-MTMS Version Checklist | Functions Ref. No. 35 | v1.0.1 | 2026-07-22 + * Pick-order create search: one row per item + available lot UOM + * (available qty summed per UOM). Items with no available stock are omitted. + */ open fun getItemsWithDetailsByPage(args: Map): List> { val sql = StringBuilder( - "select" - + " i.id, " - + " i.code, " - + " i.name, " - + " i.type, " - + " i.description, " - + " uc.id as uomId, " - + " uc.code as uom, " - + " uc.udfudesc as uomDesc, " - + " COALESCE(inv.availableQty, 0) as currentStockBalance " - + " from items i " - + " left join item_uom iu on iu.itemId = i.id and iu.deleted = false and iu.salesUnit = true " - + " left join uom_conversion uc on uc.id = iu.uomId " - + " left join (" - + " SELECT " - + " il.itemId, " - + " SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty " - + " FROM inventory_lot_line ill " - + " JOIN inventory_lot il ON il.id = ill.inventoryLotId " - + " LEFT JOIN item_uom iu_ratio ON iu_ratio.itemId = il.itemId AND iu_ratio.deleted = false AND iu_ratio.salesUnit = true " - + " WHERE ill.status = 'available' " - + " AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 " - + " AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) " - + " GROUP BY il.itemId " - + " ) inv ON inv.itemId = i.id " - + " where i.deleted = false " + """ + SELECT + i.id, + i.code, + i.name, + i.type, + i.description, + uc.id AS uomId, + uc.code AS uom, + uc.udfudesc AS uomDesc, + SUM( + COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0) + ) AS currentStockBalance + FROM items i + JOIN inventory_lot il + ON il.itemId = i.id + AND il.deleted = false + JOIN inventory_lot_line ill + ON ill.inventoryLotId = il.id + AND ill.deleted = false + JOIN item_uom iu + ON iu.id = ill.stockItemUomId + AND iu.deleted = false + JOIN uom_conversion uc + ON uc.id = iu.uomId + WHERE i.deleted = false + AND ill.status = 'available' + AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 + AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) + """.trimIndent() ) - // Add search conditions if (args.containsKey("name")) { sql.append(" AND i.name LIKE :name ") } - if (args.containsKey("code")) { sql.append(" AND i.code LIKE :code ") } - if (args.containsKey("description")) { sql.append(" AND i.description LIKE :description ") } - if (args.containsKey("type")) { sql.append(" AND i.type = :type ") } - sql.append(" ORDER BY i.name ASC ") + sql.append( + """ + GROUP BY i.id, i.code, i.name, i.type, i.description, uc.id, uc.code, uc.udfudesc + ORDER BY i.name ASC, uc.udfudesc ASC + """.trimIndent() + ) - return jdbcDao.queryForList(sql.toString(), args); + return jdbcDao.queryForList(sql.toString(), args) } open fun getItemOrderOrderedItems(search: String?, type: String? = null): List> { @@ -971,4 +985,78 @@ open class ItemsService( }?.takeIf { it.isNotBlank() } } + /** + * FP-MTMS Version Checklist | Functions Ref. No. 35 | v1.0.1 | 2026-07-22 + * Pick-order create: stock UOM (default) + distinct UOMs from currently available lots. + * Lot UOM = inventory_lot_line.stockItemUom → item_uom.uom (uom_conversion id). + */ + open fun getAvailablePickUoms(itemId: Long): Map { + val args = mapOf("itemId" to itemId) + + val stockRows = jdbcDao.queryForList( + """ + SELECT + uc.id AS uomId, + uc.code AS uomCode, + uc.udfudesc AS uomDesc + FROM item_uom iu + JOIN uom_conversion uc ON uc.id = iu.uomId + WHERE iu.itemId = :itemId + AND iu.deleted = false + AND iu.stockUnit = true + LIMIT 1 + """.trimIndent(), + args, + ) + val stockUom = stockRows.firstOrNull()?.let { row -> + mapOf( + "uomId" to row["uomId"], + "uomCode" to row["uomCode"], + "uomDesc" to row["uomDesc"], + ) + } + + val lotRows = jdbcDao.queryForList( + """ + SELECT DISTINCT + uc.id AS uomId, + uc.code AS uomCode, + uc.udfudesc AS uomDesc + FROM inventory_lot_line ill + JOIN inventory_lot il ON il.id = ill.inventoryLotId + JOIN item_uom iu ON iu.id = ill.stockItemUomId AND iu.deleted = false + JOIN uom_conversion uc ON uc.id = iu.uomId + WHERE il.itemId = :itemId + AND ill.deleted = false + AND ill.status = 'available' + AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) > 0 + AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) + ORDER BY uc.udfudesc ASC + """.trimIndent(), + args, + ) + + val byId = linkedMapOf>() + fun putUom(row: Map?) { + if (row == null) return + val id = (row["uomId"] as? Number)?.toLong() ?: return + if (id <= 0L) return + if (!byId.containsKey(id)) { + byId[id] = mapOf( + "uomId" to id, + "uomCode" to row["uomCode"], + "uomDesc" to row["uomDesc"], + ) + } + } + putUom(stockUom) + lotRows.forEach { putUom(it) } + + return mapOf( + "itemId" to itemId, + "stockUom" to stockUom, + "options" to byId.values.toList(), + ) + } + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt index ad812cd..5b83ade 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt @@ -45,6 +45,15 @@ class ItemsController( fun allConsumables(): List> { return itemsService.allConsumables() } + + /** + * FP-MTMS Version Checklist | Functions Ref. No. 35 | v1.0.1 | 2026-07-22 + * Stock UOM + distinct available-lot UOMs for pick-order create dropdown. + */ + @GetMapping("/{itemId}/available-pick-uoms") + fun getAvailablePickUoms(@PathVariable itemId: Long): Map { + return itemsService.getAvailablePickUoms(itemId) + } // @GetMapping("/getRecordByPage") // fun getAllItemsByPage(@RequestBody filterRequest: HttpServletRequest): RecordsRes> { // val pageSize = filterRequest.getParameter("pageSize").toString().toInt(); // Default to 10 if not provided diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt index 491e973..c875b91 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt @@ -96,6 +96,7 @@ SELECT i.code as itemCode, i.name as itemName, i.item_Order as itemOrder, + pol.uomId as uomId, uc.code as uomCode, uc.udfudesc as uomDesc, uc.udfShortDesc as uomShortDesc, @@ -261,6 +262,7 @@ ORDER BY "id" to firstLineRow["itemId"], "code" to firstLineRow["itemCode"], "name" to firstLineRow["itemName"], + "uomId" to firstLineRow["uomId"], "uomCode" to firstLineRow["uomCode"], "uomDesc" to firstLineRow["uomDesc"], "uomShortDesc" to firstLineRow["uomShortDesc"], diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderWorkbenchService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderWorkbenchService.kt index e35f250..00250e1 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderWorkbenchService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderWorkbenchService.kt @@ -251,6 +251,7 @@ open class PickOrderWorkbenchService( "id" to pol.item?.id, "code" to pol.item?.code, "name" to pol.item?.name, + "uomId" to pol.uom?.id, "uomCode" to pol.uom?.code, "uomDesc" to pol.uom?.udfudesc, ), 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 45fb4c0..89d7754 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 @@ -47,6 +47,7 @@ interface InventoryRepository: AbstractRepository { ): Page /** + * FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.0 | 2026-07-17 * Inventory search page only: no baseUnit/uomId match required; * one row per item = latest inventory id (no larger id for same item). */ @@ -72,6 +73,7 @@ interface InventoryRepository: AbstractRepository { pageable: Pageable, ): Page + /** FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.0 | 2026-07-17 */ @Query( """ SELECT i FROM Inventory i diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryLotLineInfo.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryLotLineInfo.kt index 109251c..9039574 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryLotLineInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryLotLineInfo.kt @@ -50,6 +50,9 @@ interface InventoryLotLineInfo { @get:Value("#{target.stockUom?.uom?.udfudesc}") val uom: String? + @get:Value("#{target.stockUom?.uom?.id}") + val uomId: Long? + @get:Value("#{target.inventoryLot.expiryDate}") val expiryDate: LocalDate } 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 712ca3e..78494b9 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 @@ -454,6 +454,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit } /** + * FP-MTMS Version Checklist | Functions Ref. No. 34 | v1.0.1 | 2026-07-22 * DO workbench label modal: pickable qty = in - out (aligned with workbench scan-pick). */ open fun analyzeQrCodeWorkbench(request: QrCodeAnalysisRequest): QrCodeAnalysisResponse { @@ -461,16 +462,22 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit } /** + * FP-MTMS Version Checklist | Functions Ref. No. 34 | v1.0.1 | 2026-07-22 * DO workbench label modal fallback (no stockInLineId/no-lot row): * list available lots by item only, using pickable qty = in - out. + * When [uomId] is set, only lots whose stockUom.uom.id matches are returned. */ - open fun listWorkbenchAvailableLotsByItem(itemId: Long): WorkbenchItemLotsResponse { + open fun listWorkbenchAvailableLotsByItem(itemId: Long, uomId: Long? = null): WorkbenchItemLotsResponse { val source = inventoryLotLineRepository .findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) .asSequence() .filter { !it.deleted && it.inventoryLot?.item != null } + .filter { uomId == null || it.stockUom?.uom?.id == uomId } .toList() val item = source.firstOrNull()?.inventoryLot?.item + ?: inventoryLotLineRepository + .findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) + .firstOrNull { !it.deleted }?.inventoryLot?.item if (item == null) { return WorkbenchItemLotsResponse( itemId = itemId, @@ -484,6 +491,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit val lot = lotLine.inventoryLot ?: return@mapNotNull null val lotNo = lot.stockInLine?.lotNo ?: return@mapNotNull null val uomDesc = lotLine.stockUom?.uom?.udfudesc ?: return@mapNotNull null + val lotUomId = lotLine.stockUom?.uom?.id val inQty = lotLine.inQty ?: BigDecimal.ZERO val outQty = lotLine.outQty ?: BigDecimal.ZERO val remainingQty = inQty.minus(outQty) @@ -494,6 +502,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit stockInLineId = lot.stockInLine?.id, availableQty = remainingQty, uom = uomDesc, + uomId = lotUomId, warehouseCode = lotLine.warehouse?.code, warehouseName = lotLine.warehouse?.name, ) @@ -521,14 +530,17 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit val item = scannedInventoryLotLine.inventoryLot?.item ?: throw IllegalStateException("Item not found for lot line id=${scannedInventoryLotLine.id}") + val filterUomId = request.uomId val sameItemLots = inventoryLotLineRepository .findAllByInventoryLotItemIdAndStatus(request.itemId, InventoryLotLineStatus.AVAILABLE) .asSequence() .filter { it.id != scannedInventoryLotLine.id } + .filter { filterUomId == null || it.stockUom?.uom?.id == filterUomId } .mapNotNull { lotLine -> val lot = lotLine.inventoryLot ?: return@mapNotNull null val lotNo = lot.stockInLine?.lotNo ?: return@mapNotNull null val uomDesc = lotLine.stockUom?.uom?.udfudesc ?: return@mapNotNull null + val lotUomId = lotLine.stockUom?.uom?.id val whCode = lotLine.warehouse?.code val whName = lotLine.warehouse?.name @@ -545,6 +557,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit stockInLineId = lot.stockInLine?.id, availableQty = remainingQty, uom = uomDesc, + uomId = lotUomId, warehouseCode = whCode, warehouseName = whName, ) @@ -569,6 +582,8 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit ?: throw IllegalStateException("inventoryLotLineId missing on scanned lot line"), warehouseCode = scannedInventoryLotLine.warehouse?.code, warehouseName = scannedInventoryLotLine.warehouse?.name, + uom = scannedInventoryLotLine.stockUom?.uom?.udfudesc, + uomId = scannedInventoryLotLine.stockUom?.uom?.id, ), sameItemLots = sameItemLots, ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt index eb803fe..d909d74 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt @@ -89,8 +89,9 @@ open class InventoryService( } /** + * FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.0 | 2026-07-17 * Inventory search page only: no baseUnit/uomId filter; - * one inventory row per item (MAX id). Shared getRecordByPage is unchanged. + * one inventory row per item (latest id). Shared getRecordByPage is unchanged. */ open fun searchInventoriesByLatestInventory( request: SearchInventoryRequest, 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 a2c925e..b3fd967 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 @@ -1034,9 +1034,6 @@ open class StockInLineService( if (request.status == StockInLineStatus.RECEIVED.status) { val _tPutAwayTotal = System.nanoTime() // Putaway - var savedInventoryLotLine: InventoryLotLine? = null - -// savedInventoryLotLine = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) val _tSaveLotLines = System.nanoTime() val savedInventoryLotLines = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) _logStep("saveInventoryLotLineWhenStockIn", _tSaveLotLines) @@ -1045,6 +1042,11 @@ open class StockInLineService( val inventoryLotLines = stockInLine.inventoryLot?.let { it.id?.let { _id -> inventoryLotLineRepository.findAllByInventoryLotId(_id) } } ?: listOf() _logStep("inventoryLotLineRepository.findAllByInventoryLotId", _tFindAllLotLines) + // NOR putaway can create multiple ILL (per warehouse); SIL only has one FK — keep latest from this putaway. + (savedInventoryLotLines.lastOrNull() ?: inventoryLotLines.lastOrNull())?.let { primaryLotLine -> + stockInLine.inventoryLotLine = primaryLotLine + } + // ✅ 每次上架都寫一筆 stock_ledger(inQty = 本次上架的庫存數量) val _tDeltaCompute = System.nanoTime() val putAwayDeltaStockQty = (request.inventoryLotLines ?: listOf()).sumOf { line -> @@ -1102,7 +1104,6 @@ open class StockInLineService( else StockInLineStatus.PARTIALLY_COMPLETE.status } - // this.inventoryLotLine = savedInventoryLotLine } // Update JO Status if (stockInLine.jobOrder != null) { //TODO Improve diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt index 013fd73..752022a 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt @@ -197,6 +197,9 @@ open class StockOutLineWorkbenchService( ) } + /** + * FP-MTMS Version Checklist | Functions Ref. No. 33 | v1.0.1 | 2026-07-22 + */ @Transactional(rollbackFor = [Exception::class]) open fun scanPick(request: WorkbenchScanPickRequest): MessageResponse { val lotNo = request.lotNo.trim() @@ -315,6 +318,15 @@ open class StockOutLineWorkbenchService( ) } + val polUomId = pol.uom?.id + val lotUomId = scannedIll.stockUom?.uom?.id + if (polUomId != null && lotUomId != null && polUomId != lotUomId) { + return MessageResponse( + id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick", + message = "Scanned lot UOM does not match pick line UOM", errorPosition = null, entity = null + ) + } + sol.inventoryLotLine = scannedIll val required = pol.qty ?: BigDecimal.ZERO diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index 48692d9..507b100 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt @@ -96,6 +96,7 @@ open class SuggestedPickLotService( return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) } + /** FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 */ open fun suggestionForPickOrderLines(request: SuggestedPickLotForPolRequest): SuggestedPickLotResponse { val pols = request.pickOrderLines val itemIds = pols.mapNotNull { it.item?.id } @@ -123,7 +124,9 @@ open class SuggestedPickLotService( pols.forEach { line -> val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } + val polUomId = line.uom?.id val lotLines = availableInventoryLotLines[line.item?.id].orEmpty() + .filter { polUomId == null || it.uomId == polUomId } val ratio = one // (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) val pickOrder = line.pickOrder val isDoPickOrder = pickOrder?.type?.value == "do" || pickOrder?.type?.value == "delivery_order" @@ -275,6 +278,7 @@ open class SuggestedPickLotService( } return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) } + /** FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 */ open fun suggestionForPickOrderLinesForJobOrder(request: SuggestedPickLotForPolRequest): SuggestedPickLotResponse { val pols = request.pickOrderLines val itemIds = pols.mapNotNull { it.item?.id } @@ -302,8 +306,10 @@ open class SuggestedPickLotService( pols.forEach { line -> val itemId = line.item?.id val isWipItem = itemId != null && wipItemIdSet.contains(itemId) - + val polUomId = line.uom?.id + val lotInfos = availableInventoryLotLines[itemId].orEmpty() + .filter { polUomId == null || it.uomId == polUomId } val lotIds = lotInfos.mapNotNull { it.id } // 仅一次查询,避免后续重复 findById val lotEntities = if (lotIds.isNotEmpty()) inventoryLotLineRepository.findAllById(lotIds) else emptyList() @@ -580,6 +586,7 @@ open class SuggestedPickLotService( return null } } +/** FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 */ @Transactional(rollbackFor = [Exception::class]) open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { try { @@ -1290,11 +1297,15 @@ private fun generateOptimalSuggestionsForAllPickOrders( var lotRemainingQty = availableQty - // Allocate this lot to pick order lines in order + // Allocate this lot to pick order lines in order (same UOM only) remainingPickOrderLines.removeAll { pickOrderLine -> val lineId = pickOrderLine.id val lineRemainingQty = remainingQtyPerLine[lineId] ?: zero - + val polUomId = pickOrderLine.uom?.id + if (polUomId != null && lot.stockUom?.uom?.id != polUomId) { + return@removeAll false + } + if (lineRemainingQty <= zero || lotRemainingQty <= zero) return@removeAll false val assignQty = minOf(lotRemainingQty, lineRemainingQty) @@ -1394,10 +1405,12 @@ private fun updateInventoryTableAfterResuggest(pickOrder: PickOrder) { if (excessQtyInSalesUnits <= zero) return@forEach - // Get available inventory lots for this item (FEFO order) + val polUomId = orderLine.uom?.id + // Get available inventory lots for this item (FEFO order, same UOM) val availableLots = inventoryLotLineService .allInventoryLotLinesByItemIdIn(listOf(itemId)) .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } + .filter { polUomId == null || it.uomId == polUomId } .sortedBy { it.expiryDate } var remainingQtyInBaseUnits = excessQtyInBaseUnits @@ -1550,9 +1563,11 @@ private fun generateCorrectSuggestionsWithExistingHolds(pickOrder: PickOrder): L println("Available lots: ${availableLots.size}, Unavailable lots: ${unavailableLots.size}") println("Total unavailable hold qty to redistribute: $totalUnavailableHoldQty") - // Generate suggestions from available lots ONLY + val polUomId = orderLine.uom?.id + // Generate suggestions from available lots ONLY (same UOM as POL) availableLots.forEach { lotInfo -> if (remainingQtyInSalesUnits <= zero) return@forEach + if (polUomId != null && lotInfo.uomId != polUomId) return@forEach val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) } ?: return@forEach @@ -1620,12 +1635,14 @@ private fun generateCorrectSuggestionsWithOriginalHolds( val salesUnit = itemUomService.findSalesUnitByItemId(itemId) val ratio = one + val polUomId = orderLine.uom?.id - // Get available inventory lots for this item (FEFO order) + // Get available inventory lots for this item (FEFO order, same UOM) val availableLots = inventoryLotLineService .allInventoryLotLinesByItemIdIn(listOf(itemId)) .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } + .filter { polUomId == null || it.uomId == polUomId } .sortedBy { it.expiryDate } // Calculate total quantity that needs to be redistributed diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt index 92f0754..f5de253 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt @@ -150,6 +150,16 @@ open class SuggestedPickLotWorkbenchService( } /** + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 + * Lot stock UOM must match pick_order_line.uom (UomConversion id). + */ + private fun matchesPolUom(lot: InventoryLotLine, line: PickOrderLine): Boolean { + val polUomId = line.uom?.id ?: return false + return lot.stockUom?.uom?.id == polUomId + } + + /** + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 * Build suggestions with no-hold rule: * available = inQty - outQty (ignore holdQty). * When [desired] is a single segment, reuses one SPL row (max id) and repoints lot + qty (switch-lot without new id). @@ -207,7 +217,7 @@ open class SuggestedPickLotWorkbenchService( return@forEach } - val lots = lotsByItem[line.item?.id].orEmpty() + val lots = lotsByItem[line.item?.id].orEmpty().filter { matchesPolUom(it, line) } val desired = buildDesiredAllocations(lots, remaining, lineId) created += syncSuggestedPickLotsForLine(line, desired) } @@ -282,6 +292,7 @@ open class SuggestedPickLotWorkbenchService( } /** + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 * Same no-hold allocation as [rebuildNoHoldSuggestionsForPickOrder], but only for one [pickOrderLineId]. * Used by workbench scan-pick to avoid rebuilding suggestions for every line on the pick order. */ @@ -340,6 +351,7 @@ open class SuggestedPickLotWorkbenchService( .filter { it.status == InventoryLotLineStatus.AVAILABLE } .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } .filter { !isWarehouseExcluded(it, excludeSet) } + .filter { matchesPolUom(it, lineFresh) } .filter { lot -> if (storeKey == null) true else normalizeStoreId(lot.warehouse?.store_id) == storeKey @@ -372,6 +384,7 @@ open class SuggestedPickLotWorkbenchService( } /** + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 * Workbench explicit-qty remainder support (single-next-lot): * Create exactly ONE suggested lot row for this POL with [targetQty] pointing to the next available lot. * When [storeId] is provided, the next lot must be within the same store (warehouse.store_id). @@ -411,6 +424,7 @@ open class SuggestedPickLotWorkbenchService( .filter { it.status == InventoryLotLineStatus.AVAILABLE } .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } .filter { !isWarehouseExcluded(it, excludeSet) } + .filter { matchesPolUom(it, line) } .filter { lot -> if (excludeInventoryLotLineId == null) true else lot.id != excludeInventoryLotLineId } @@ -431,6 +445,7 @@ open class SuggestedPickLotWorkbenchService( } /** + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 * Workbench explicit-qty split support: * Set suggestions for this POL to an exact [targetQty] (independent of POL.qty). * - If [targetQty] <= 0: soft-delete all active suggestions for the line. @@ -466,6 +481,7 @@ open class SuggestedPickLotWorkbenchService( .filter { it.status == InventoryLotLineStatus.AVAILABLE } .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } .filter { !isWarehouseExcluded(it, excludeSet) } + .filter { matchesPolUom(it, line) } .sortedBy { it.inventoryLot?.expiryDate } val desired = buildDesiredAllocations(lots, targetQty, pickOrderLineId) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryController.kt index 3198dcc..20cf216 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryController.kt @@ -21,7 +21,10 @@ class InventoryController( return inventoryService.allInventoriesByPage(request); } - /** Inventory search page only: latest inventory row per item, no baseUnit/uomId filter. */ + /** + * FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.0 | 2026-07-17 + * Inventory search page only: latest inventory row per item, no baseUnit/uomId filter. + */ @GetMapping("/searchLatest/getRecordByPage") fun searchInventoriesByLatestInventory( @ModelAttribute request: SearchInventoryRequest, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt index 14c14c0..9310e05 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt @@ -72,18 +72,35 @@ class InventoryLotLineController ( val stockInLine = stockInLineRepository.findById(stockInLineId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "stockInLineId $stockInLineId not found") } - val inventoryLotLine = stockInLine.inventoryLotLine!! + // NOR putaway historically may leave inventoryLotLineId null; fall back via inventoryLot. + val inventoryLotLine = stockInLine.inventoryLotLine + ?: stockInLine.inventoryLot?.inventoryLotLines?.firstOrNull() + ?: throw ResponseStatusException( + HttpStatus.NOT_FOUND, + "No inventory lot line for stockInLineId $stockInLineId", + ) val zero = BigDecimal.ZERO + val item = inventoryLotLine.inventoryLot?.item + ?: throw ResponseStatusException( + HttpStatus.NOT_FOUND, + "Item not found for stockInLineId $stockInLineId", + ) + val uom = inventoryLotLine.stockUom?.uom return LotLineInfo( - inventoryLotLineId = inventoryLotLine.id!!, - itemId = inventoryLotLine.inventoryLot!!.item!!.id!!, - itemNo = inventoryLotLine.inventoryLot!!.item!!.code!!, - itemName = inventoryLotLine.inventoryLot!!.item!!.name!!, - lotNo = stockInLine.lotNo!!, + inventoryLotLineId = inventoryLotLine.id + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "inventoryLotLine id missing"), + itemId = item.id + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "item id missing"), + itemNo = item.code ?: "", + itemName = item.name ?: "", + lotNo = stockInLine.lotNo + ?: inventoryLotLine.inventoryLot?.lotNo + ?: "", remainingQty = (inventoryLotLine.inQty ?: zero) .minus(inventoryLotLine.outQty ?: zero) .minus(inventoryLotLine.holdQty ?: zero), - uom = inventoryLotLine.stockUom!!.uom!!.udfudesc!! + uom = uom?.udfudesc ?: uom?.code ?: "", + uomId = uom?.id, ) } @@ -182,15 +199,24 @@ class InventoryLotLineController ( return inventoryLotLineService.analyzeQrCode(request) } - /** DO workbench label modal: same-item lots use pickable qty inQty - outQty (no hold). */ + /** + * FP-MTMS Version Checklist | Functions Ref. No. 34 | v1.0.1 | 2026-07-22 + * DO workbench label modal: same-item lots use pickable qty inQty - outQty (no hold). + */ @PostMapping("/workbench/analyze-qr-code") fun analyzeQrCodeWorkbench(@RequestBody request: QrCodeAnalysisRequest): QrCodeAnalysisResponse { return inventoryLotLineService.analyzeQrCodeWorkbench(request) } - /** Workbench label modal fallback: load same-item available lots without stockInLineId. */ + /** + * FP-MTMS Version Checklist | Functions Ref. No. 34 | v1.0.1 | 2026-07-22 + * Workbench label modal fallback: load same-item available lots without stockInLineId. + */ @GetMapping("/workbench/available-lots-by-item/{itemId}") - fun listWorkbenchAvailableLotsByItem(@PathVariable itemId: Long): WorkbenchItemLotsResponse { - return inventoryLotLineService.listWorkbenchAvailableLotsByItem(itemId) + fun listWorkbenchAvailableLotsByItem( + @PathVariable itemId: Long, + @RequestParam(required = false) uomId: Long?, + ): WorkbenchItemLotsResponse { + return inventoryLotLineService.listWorkbenchAvailableLotsByItem(itemId, uomId) } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt index 0d5d450..b3cf7ae 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.stock.web.model import java.math.BigDecimal + data class LotLineInfo( val inventoryLotLineId: Long, val itemId: Long, @@ -9,7 +10,9 @@ data class LotLineInfo( val itemName: String, val lotNo: String, val remainingQty: BigDecimal, - val uom: String + val uom: String, + /** UomConversion id from inventory_lot_line.stockItemUom → item_uom.uom */ + val uomId: Long? = null, ) data class UpdateInventoryLotLineStatusRequest( val inventoryLotLineId: Long, @@ -18,6 +21,8 @@ data class UpdateInventoryLotLineStatusRequest( data class QrCodeAnalysisRequest( val itemId: Long, val stockInLineId: Long, + /** When set, sameItemLots only include lots with this UomConversion id. */ + val uomId: Long? = null, ) data class ScannedLotInfo( @@ -25,7 +30,9 @@ data class ScannedLotInfo( val lotNo: String, val inventoryLotLineId: Long, val warehouseCode: String? = null, - val warehouseName: String? = null + val warehouseName: String? = null, + val uom: String? = null, + val uomId: Long? = null, ) data class SameItemLotInfo( @@ -34,6 +41,7 @@ data class SameItemLotInfo( val stockInLineId: Long? = null, val availableQty: BigDecimal, val uom: String, + val uomId: Long? = null, val warehouseCode: String? = null, val warehouseName: String? = null ) @@ -51,4 +59,4 @@ data class WorkbenchItemLotsResponse( val itemCode: String, val itemName: String, val sameItemLots: List -) \ No newline at end of file +)