| @@ -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<String, Any>): List<Map<String, Any>> { | |||
| 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<String, Any>): List<Map<String, Any>> { | |||
| 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<Map<String, Any>> { | |||
| @@ -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<String, Any?> { | |||
| 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<Long, Map<String, Any?>>() | |||
| fun putUom(row: Map<String, Any?>?) { | |||
| 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(), | |||
| ) | |||
| } | |||
| } | |||
| @@ -45,6 +45,15 @@ class ItemsController( | |||
| fun allConsumables(): List<Map<String, Any>> { | |||
| 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<String, Any?> { | |||
| return itemsService.getAvailablePickUoms(itemId) | |||
| } | |||
| // @GetMapping("/getRecordByPage") | |||
| // fun getAllItemsByPage(@RequestBody filterRequest: HttpServletRequest): RecordsRes<Map<String, Any>> { | |||
| // val pageSize = filterRequest.getParameter("pageSize").toString().toInt(); // Default to 10 if not provided | |||
| @@ -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"], | |||
| @@ -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, | |||
| ), | |||
| @@ -47,6 +47,7 @@ interface InventoryRepository: AbstractRepository<Inventory, Long> { | |||
| ): Page<InventoryInfo> | |||
| /** | |||
| * 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<Inventory, Long> { | |||
| pageable: Pageable, | |||
| ): Page<InventoryInfo> | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.0 | 2026-07-17 */ | |||
| @Query( | |||
| """ | |||
| SELECT i FROM Inventory i | |||
| @@ -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 | |||
| } | |||
| @@ -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, | |||
| ) | |||
| @@ -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, | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -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, | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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<SameItemLotInfo> | |||
| ) | |||
| ) | |||