| @@ -266,6 +266,7 @@ open class ItemsService( | |||||
| return jdbcDao.queryForList(sql.toString(), args); | 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>> { | open fun getPickOrderItemsByPage(args: Map<String, Any>): List<Map<String, Any>> { | ||||
| try { | try { | ||||
| println("=== Debug: getPickOrderItemsByPage in ItemsService ===") | println("=== Debug: getPickOrderItemsByPage in ItemsService ===") | ||||
| @@ -274,10 +275,12 @@ open class ItemsService( | |||||
| val sql = StringBuilder( | val sql = StringBuilder( | ||||
| """ | """ | ||||
| SELECT | SELECT | ||||
| CONCAT(po.id, '-', pol.itemId) as id, | |||||
| CONCAT(po.id, '-', pol.id) as id, | |||||
| po.id as pickOrderId, | po.id as pickOrderId, | ||||
| po.code as pickOrderCode, | po.code as pickOrderCode, | ||||
| pol.id as pickOrderLineId, | |||||
| pol.itemId, | pol.itemId, | ||||
| pol.uomId, | |||||
| i.code as itemCode, | i.code as itemCode, | ||||
| i.name as itemName, | i.name as itemName, | ||||
| pol.qty as requiredQty, | pol.qty as requiredQty, | ||||
| @@ -296,15 +299,16 @@ open class ItemsService( | |||||
| LEFT JOIN ( | LEFT JOIN ( | ||||
| SELECT | SELECT | ||||
| il.itemId, | il.itemId, | ||||
| iu.uomId as uomId, | |||||
| SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty | SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty | ||||
| FROM inventory_lot_line ill | FROM inventory_lot_line ill | ||||
| JOIN inventory_lot il ON il.id = ill.inventoryLotId | 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' | WHERE ill.status = 'available' | ||||
| AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 | AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 | ||||
| AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) | 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 | WHERE po.deleted = false | ||||
| AND po.consoCode IS NULL | 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>> { | open fun getItemsWithDetailsByPage(args: Map<String, Any>): List<Map<String, Any>> { | ||||
| val sql = StringBuilder( | 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")) { | if (args.containsKey("name")) { | ||||
| sql.append(" AND i.name LIKE :name ") | sql.append(" AND i.name LIKE :name ") | ||||
| } | } | ||||
| if (args.containsKey("code")) { | if (args.containsKey("code")) { | ||||
| sql.append(" AND i.code LIKE :code ") | sql.append(" AND i.code LIKE :code ") | ||||
| } | } | ||||
| if (args.containsKey("description")) { | if (args.containsKey("description")) { | ||||
| sql.append(" AND i.description LIKE :description ") | sql.append(" AND i.description LIKE :description ") | ||||
| } | } | ||||
| if (args.containsKey("type")) { | if (args.containsKey("type")) { | ||||
| sql.append(" AND i.type = :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>> { | open fun getItemOrderOrderedItems(search: String?, type: String? = null): List<Map<String, Any>> { | ||||
| @@ -971,4 +985,78 @@ open class ItemsService( | |||||
| }?.takeIf { it.isNotBlank() } | }?.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>> { | fun allConsumables(): List<Map<String, Any>> { | ||||
| return itemsService.allConsumables() | 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") | // @GetMapping("/getRecordByPage") | ||||
| // fun getAllItemsByPage(@RequestBody filterRequest: HttpServletRequest): RecordsRes<Map<String, Any>> { | // fun getAllItemsByPage(@RequestBody filterRequest: HttpServletRequest): RecordsRes<Map<String, Any>> { | ||||
| // val pageSize = filterRequest.getParameter("pageSize").toString().toInt(); // Default to 10 if not provided | // val pageSize = filterRequest.getParameter("pageSize").toString().toInt(); // Default to 10 if not provided | ||||
| @@ -96,6 +96,7 @@ SELECT | |||||
| i.code as itemCode, | i.code as itemCode, | ||||
| i.name as itemName, | i.name as itemName, | ||||
| i.item_Order as itemOrder, | i.item_Order as itemOrder, | ||||
| pol.uomId as uomId, | |||||
| uc.code as uomCode, | uc.code as uomCode, | ||||
| uc.udfudesc as uomDesc, | uc.udfudesc as uomDesc, | ||||
| uc.udfShortDesc as uomShortDesc, | uc.udfShortDesc as uomShortDesc, | ||||
| @@ -261,6 +262,7 @@ ORDER BY | |||||
| "id" to firstLineRow["itemId"], | "id" to firstLineRow["itemId"], | ||||
| "code" to firstLineRow["itemCode"], | "code" to firstLineRow["itemCode"], | ||||
| "name" to firstLineRow["itemName"], | "name" to firstLineRow["itemName"], | ||||
| "uomId" to firstLineRow["uomId"], | |||||
| "uomCode" to firstLineRow["uomCode"], | "uomCode" to firstLineRow["uomCode"], | ||||
| "uomDesc" to firstLineRow["uomDesc"], | "uomDesc" to firstLineRow["uomDesc"], | ||||
| "uomShortDesc" to firstLineRow["uomShortDesc"], | "uomShortDesc" to firstLineRow["uomShortDesc"], | ||||
| @@ -251,6 +251,7 @@ open class PickOrderWorkbenchService( | |||||
| "id" to pol.item?.id, | "id" to pol.item?.id, | ||||
| "code" to pol.item?.code, | "code" to pol.item?.code, | ||||
| "name" to pol.item?.name, | "name" to pol.item?.name, | ||||
| "uomId" to pol.uom?.id, | |||||
| "uomCode" to pol.uom?.code, | "uomCode" to pol.uom?.code, | ||||
| "uomDesc" to pol.uom?.udfudesc, | "uomDesc" to pol.uom?.udfudesc, | ||||
| ), | ), | ||||
| @@ -47,6 +47,7 @@ interface InventoryRepository: AbstractRepository<Inventory, Long> { | |||||
| ): Page<InventoryInfo> | ): 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; | * Inventory search page only: no baseUnit/uomId match required; | ||||
| * one row per item = latest inventory id (no larger id for same item). | * one row per item = latest inventory id (no larger id for same item). | ||||
| */ | */ | ||||
| @@ -72,6 +73,7 @@ interface InventoryRepository: AbstractRepository<Inventory, Long> { | |||||
| pageable: Pageable, | pageable: Pageable, | ||||
| ): Page<InventoryInfo> | ): Page<InventoryInfo> | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.0 | 2026-07-17 */ | |||||
| @Query( | @Query( | ||||
| """ | """ | ||||
| SELECT i FROM Inventory i | SELECT i FROM Inventory i | ||||
| @@ -50,6 +50,9 @@ interface InventoryLotLineInfo { | |||||
| @get:Value("#{target.stockUom?.uom?.udfudesc}") | @get:Value("#{target.stockUom?.uom?.udfudesc}") | ||||
| val uom: String? | val uom: String? | ||||
| @get:Value("#{target.stockUom?.uom?.id}") | |||||
| val uomId: Long? | |||||
| @get:Value("#{target.inventoryLot.expiryDate}") | @get:Value("#{target.inventoryLot.expiryDate}") | ||||
| val expiryDate: LocalDate | 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). | * DO workbench label modal: pickable qty = in - out (aligned with workbench scan-pick). | ||||
| */ | */ | ||||
| open fun analyzeQrCodeWorkbench(request: QrCodeAnalysisRequest): QrCodeAnalysisResponse { | 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): | * DO workbench label modal fallback (no stockInLineId/no-lot row): | ||||
| * list available lots by item only, using pickable qty = in - out. | * 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 | val source = inventoryLotLineRepository | ||||
| .findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) | .findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) | ||||
| .asSequence() | .asSequence() | ||||
| .filter { !it.deleted && it.inventoryLot?.item != null } | .filter { !it.deleted && it.inventoryLot?.item != null } | ||||
| .filter { uomId == null || it.stockUom?.uom?.id == uomId } | |||||
| .toList() | .toList() | ||||
| val item = source.firstOrNull()?.inventoryLot?.item | val item = source.firstOrNull()?.inventoryLot?.item | ||||
| ?: inventoryLotLineRepository | |||||
| .findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) | |||||
| .firstOrNull { !it.deleted }?.inventoryLot?.item | |||||
| if (item == null) { | if (item == null) { | ||||
| return WorkbenchItemLotsResponse( | return WorkbenchItemLotsResponse( | ||||
| itemId = itemId, | itemId = itemId, | ||||
| @@ -484,6 +491,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit | |||||
| val lot = lotLine.inventoryLot ?: return@mapNotNull null | val lot = lotLine.inventoryLot ?: return@mapNotNull null | ||||
| val lotNo = lot.stockInLine?.lotNo ?: return@mapNotNull null | val lotNo = lot.stockInLine?.lotNo ?: return@mapNotNull null | ||||
| val uomDesc = lotLine.stockUom?.uom?.udfudesc ?: 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 inQty = lotLine.inQty ?: BigDecimal.ZERO | ||||
| val outQty = lotLine.outQty ?: BigDecimal.ZERO | val outQty = lotLine.outQty ?: BigDecimal.ZERO | ||||
| val remainingQty = inQty.minus(outQty) | val remainingQty = inQty.minus(outQty) | ||||
| @@ -494,6 +502,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit | |||||
| stockInLineId = lot.stockInLine?.id, | stockInLineId = lot.stockInLine?.id, | ||||
| availableQty = remainingQty, | availableQty = remainingQty, | ||||
| uom = uomDesc, | uom = uomDesc, | ||||
| uomId = lotUomId, | |||||
| warehouseCode = lotLine.warehouse?.code, | warehouseCode = lotLine.warehouse?.code, | ||||
| warehouseName = lotLine.warehouse?.name, | warehouseName = lotLine.warehouse?.name, | ||||
| ) | ) | ||||
| @@ -521,14 +530,17 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit | |||||
| val item = scannedInventoryLotLine.inventoryLot?.item | val item = scannedInventoryLotLine.inventoryLot?.item | ||||
| ?: throw IllegalStateException("Item not found for lot line id=${scannedInventoryLotLine.id}") | ?: throw IllegalStateException("Item not found for lot line id=${scannedInventoryLotLine.id}") | ||||
| val filterUomId = request.uomId | |||||
| val sameItemLots = inventoryLotLineRepository | val sameItemLots = inventoryLotLineRepository | ||||
| .findAllByInventoryLotItemIdAndStatus(request.itemId, InventoryLotLineStatus.AVAILABLE) | .findAllByInventoryLotItemIdAndStatus(request.itemId, InventoryLotLineStatus.AVAILABLE) | ||||
| .asSequence() | .asSequence() | ||||
| .filter { it.id != scannedInventoryLotLine.id } | .filter { it.id != scannedInventoryLotLine.id } | ||||
| .filter { filterUomId == null || it.stockUom?.uom?.id == filterUomId } | |||||
| .mapNotNull { lotLine -> | .mapNotNull { lotLine -> | ||||
| val lot = lotLine.inventoryLot ?: return@mapNotNull null | val lot = lotLine.inventoryLot ?: return@mapNotNull null | ||||
| val lotNo = lot.stockInLine?.lotNo ?: return@mapNotNull null | val lotNo = lot.stockInLine?.lotNo ?: return@mapNotNull null | ||||
| val uomDesc = lotLine.stockUom?.uom?.udfudesc ?: return@mapNotNull null | val uomDesc = lotLine.stockUom?.uom?.udfudesc ?: return@mapNotNull null | ||||
| val lotUomId = lotLine.stockUom?.uom?.id | |||||
| val whCode = lotLine.warehouse?.code | val whCode = lotLine.warehouse?.code | ||||
| val whName = lotLine.warehouse?.name | val whName = lotLine.warehouse?.name | ||||
| @@ -545,6 +557,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit | |||||
| stockInLineId = lot.stockInLine?.id, | stockInLineId = lot.stockInLine?.id, | ||||
| availableQty = remainingQty, | availableQty = remainingQty, | ||||
| uom = uomDesc, | uom = uomDesc, | ||||
| uomId = lotUomId, | |||||
| warehouseCode = whCode, | warehouseCode = whCode, | ||||
| warehouseName = whName, | warehouseName = whName, | ||||
| ) | ) | ||||
| @@ -569,6 +582,8 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit | |||||
| ?: throw IllegalStateException("inventoryLotLineId missing on scanned lot line"), | ?: throw IllegalStateException("inventoryLotLineId missing on scanned lot line"), | ||||
| warehouseCode = scannedInventoryLotLine.warehouse?.code, | warehouseCode = scannedInventoryLotLine.warehouse?.code, | ||||
| warehouseName = scannedInventoryLotLine.warehouse?.name, | warehouseName = scannedInventoryLotLine.warehouse?.name, | ||||
| uom = scannedInventoryLotLine.stockUom?.uom?.udfudesc, | |||||
| uomId = scannedInventoryLotLine.stockUom?.uom?.id, | |||||
| ), | ), | ||||
| sameItemLots = sameItemLots, | 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; | * 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( | open fun searchInventoriesByLatestInventory( | ||||
| request: SearchInventoryRequest, | request: SearchInventoryRequest, | ||||
| @@ -1034,9 +1034,6 @@ open class StockInLineService( | |||||
| if (request.status == StockInLineStatus.RECEIVED.status) { | if (request.status == StockInLineStatus.RECEIVED.status) { | ||||
| val _tPutAwayTotal = System.nanoTime() | val _tPutAwayTotal = System.nanoTime() | ||||
| // Putaway | // Putaway | ||||
| var savedInventoryLotLine: InventoryLotLine? = null | |||||
| // savedInventoryLotLine = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) | |||||
| val _tSaveLotLines = System.nanoTime() | val _tSaveLotLines = System.nanoTime() | ||||
| val savedInventoryLotLines = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) | val savedInventoryLotLines = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) | ||||
| _logStep("saveInventoryLotLineWhenStockIn", _tSaveLotLines) | _logStep("saveInventoryLotLineWhenStockIn", _tSaveLotLines) | ||||
| @@ -1045,6 +1042,11 @@ open class StockInLineService( | |||||
| val inventoryLotLines = stockInLine.inventoryLot?.let { it.id?.let { _id -> inventoryLotLineRepository.findAllByInventoryLotId(_id) } } ?: listOf() | val inventoryLotLines = stockInLine.inventoryLot?.let { it.id?.let { _id -> inventoryLotLineRepository.findAllByInventoryLotId(_id) } } ?: listOf() | ||||
| _logStep("inventoryLotLineRepository.findAllByInventoryLotId", _tFindAllLotLines) | _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 = 本次上架的庫存數量) | // ✅ 每次上架都寫一筆 stock_ledger(inQty = 本次上架的庫存數量) | ||||
| val _tDeltaCompute = System.nanoTime() | val _tDeltaCompute = System.nanoTime() | ||||
| val putAwayDeltaStockQty = (request.inventoryLotLines ?: listOf()).sumOf { line -> | val putAwayDeltaStockQty = (request.inventoryLotLines ?: listOf()).sumOf { line -> | ||||
| @@ -1102,7 +1104,6 @@ open class StockInLineService( | |||||
| else | else | ||||
| StockInLineStatus.PARTIALLY_COMPLETE.status | StockInLineStatus.PARTIALLY_COMPLETE.status | ||||
| } | } | ||||
| // this.inventoryLotLine = savedInventoryLotLine | |||||
| } | } | ||||
| // Update JO Status | // Update JO Status | ||||
| if (stockInLine.jobOrder != null) { //TODO Improve | 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]) | @Transactional(rollbackFor = [Exception::class]) | ||||
| open fun scanPick(request: WorkbenchScanPickRequest): MessageResponse { | open fun scanPick(request: WorkbenchScanPickRequest): MessageResponse { | ||||
| val lotNo = request.lotNo.trim() | 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 | sol.inventoryLotLine = scannedIll | ||||
| val required = pol.qty ?: BigDecimal.ZERO | val required = pol.qty ?: BigDecimal.ZERO | ||||
| @@ -96,6 +96,7 @@ open class SuggestedPickLotService( | |||||
| return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) | 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 { | open fun suggestionForPickOrderLines(request: SuggestedPickLotForPolRequest): SuggestedPickLotResponse { | ||||
| val pols = request.pickOrderLines | val pols = request.pickOrderLines | ||||
| val itemIds = pols.mapNotNull { it.item?.id } | val itemIds = pols.mapNotNull { it.item?.id } | ||||
| @@ -123,7 +124,9 @@ open class SuggestedPickLotService( | |||||
| pols.forEach { line -> | pols.forEach { line -> | ||||
| val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | ||||
| val polUomId = line.uom?.id | |||||
| val lotLines = availableInventoryLotLines[line.item?.id].orEmpty() | 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 ratio = one // (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) | ||||
| val pickOrder = line.pickOrder | val pickOrder = line.pickOrder | ||||
| val isDoPickOrder = pickOrder?.type?.value == "do" || pickOrder?.type?.value == "delivery_order" | val isDoPickOrder = pickOrder?.type?.value == "do" || pickOrder?.type?.value == "delivery_order" | ||||
| @@ -275,6 +278,7 @@ open class SuggestedPickLotService( | |||||
| } | } | ||||
| return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) | 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 { | open fun suggestionForPickOrderLinesForJobOrder(request: SuggestedPickLotForPolRequest): SuggestedPickLotResponse { | ||||
| val pols = request.pickOrderLines | val pols = request.pickOrderLines | ||||
| val itemIds = pols.mapNotNull { it.item?.id } | val itemIds = pols.mapNotNull { it.item?.id } | ||||
| @@ -302,8 +306,10 @@ open class SuggestedPickLotService( | |||||
| pols.forEach { line -> | pols.forEach { line -> | ||||
| val itemId = line.item?.id | val itemId = line.item?.id | ||||
| val isWipItem = itemId != null && wipItemIdSet.contains(itemId) | val isWipItem = itemId != null && wipItemIdSet.contains(itemId) | ||||
| val polUomId = line.uom?.id | |||||
| val lotInfos = availableInventoryLotLines[itemId].orEmpty() | val lotInfos = availableInventoryLotLines[itemId].orEmpty() | ||||
| .filter { polUomId == null || it.uomId == polUomId } | |||||
| val lotIds = lotInfos.mapNotNull { it.id } | val lotIds = lotInfos.mapNotNull { it.id } | ||||
| // 仅一次查询,避免后续重复 findById | // 仅一次查询,避免后续重复 findById | ||||
| val lotEntities = if (lotIds.isNotEmpty()) inventoryLotLineRepository.findAllById(lotIds) else emptyList() | val lotEntities = if (lotIds.isNotEmpty()) inventoryLotLineRepository.findAllById(lotIds) else emptyList() | ||||
| @@ -580,6 +586,7 @@ open class SuggestedPickLotService( | |||||
| return null | return null | ||||
| } | } | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 */ | |||||
| @Transactional(rollbackFor = [Exception::class]) | @Transactional(rollbackFor = [Exception::class]) | ||||
| open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { | open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { | ||||
| try { | try { | ||||
| @@ -1290,11 +1297,15 @@ private fun generateOptimalSuggestionsForAllPickOrders( | |||||
| var lotRemainingQty = availableQty | 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 -> | remainingPickOrderLines.removeAll { pickOrderLine -> | ||||
| val lineId = pickOrderLine.id | val lineId = pickOrderLine.id | ||||
| val lineRemainingQty = remainingQtyPerLine[lineId] ?: zero | 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 | if (lineRemainingQty <= zero || lotRemainingQty <= zero) return@removeAll false | ||||
| val assignQty = minOf(lotRemainingQty, lineRemainingQty) | val assignQty = minOf(lotRemainingQty, lineRemainingQty) | ||||
| @@ -1394,10 +1405,12 @@ private fun updateInventoryTableAfterResuggest(pickOrder: PickOrder) { | |||||
| if (excessQtyInSalesUnits <= zero) return@forEach | 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 | val availableLots = inventoryLotLineService | ||||
| .allInventoryLotLinesByItemIdIn(listOf(itemId)) | .allInventoryLotLinesByItemIdIn(listOf(itemId)) | ||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | ||||
| .filter { polUomId == null || it.uomId == polUomId } | |||||
| .sortedBy { it.expiryDate } | .sortedBy { it.expiryDate } | ||||
| var remainingQtyInBaseUnits = excessQtyInBaseUnits | var remainingQtyInBaseUnits = excessQtyInBaseUnits | ||||
| @@ -1550,9 +1563,11 @@ private fun generateCorrectSuggestionsWithExistingHolds(pickOrder: PickOrder): L | |||||
| println("Available lots: ${availableLots.size}, Unavailable lots: ${unavailableLots.size}") | println("Available lots: ${availableLots.size}, Unavailable lots: ${unavailableLots.size}") | ||||
| println("Total unavailable hold qty to redistribute: $totalUnavailableHoldQty") | 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 -> | availableLots.forEach { lotInfo -> | ||||
| if (remainingQtyInSalesUnits <= zero) return@forEach | if (remainingQtyInSalesUnits <= zero) return@forEach | ||||
| if (polUomId != null && lotInfo.uomId != polUomId) return@forEach | |||||
| val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) } | val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) } | ||||
| ?: return@forEach | ?: return@forEach | ||||
| @@ -1620,12 +1635,14 @@ private fun generateCorrectSuggestionsWithOriginalHolds( | |||||
| val salesUnit = itemUomService.findSalesUnitByItemId(itemId) | val salesUnit = itemUomService.findSalesUnitByItemId(itemId) | ||||
| val ratio = one | 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 | val availableLots = inventoryLotLineService | ||||
| .allInventoryLotLinesByItemIdIn(listOf(itemId)) | .allInventoryLotLinesByItemIdIn(listOf(itemId)) | ||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | ||||
| .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } | .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } | ||||
| .filter { polUomId == null || it.uomId == polUomId } | |||||
| .sortedBy { it.expiryDate } | .sortedBy { it.expiryDate } | ||||
| // Calculate total quantity that needs to be redistributed | // 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: | * Build suggestions with no-hold rule: | ||||
| * available = inQty - outQty (ignore holdQty). | * 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). | * 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 | 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) | val desired = buildDesiredAllocations(lots, remaining, lineId) | ||||
| created += syncSuggestedPickLotsForLine(line, desired) | 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]. | * 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. | * 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.status == InventoryLotLineStatus.AVAILABLE } | ||||
| .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } | .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } | ||||
| .filter { !isWarehouseExcluded(it, excludeSet) } | .filter { !isWarehouseExcluded(it, excludeSet) } | ||||
| .filter { matchesPolUom(it, lineFresh) } | |||||
| .filter { lot -> | .filter { lot -> | ||||
| if (storeKey == null) true | if (storeKey == null) true | ||||
| else normalizeStoreId(lot.warehouse?.store_id) == storeKey | 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): | * 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. | * 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). | * 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.status == InventoryLotLineStatus.AVAILABLE } | ||||
| .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } | .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } | ||||
| .filter { !isWarehouseExcluded(it, excludeSet) } | .filter { !isWarehouseExcluded(it, excludeSet) } | ||||
| .filter { matchesPolUom(it, line) } | |||||
| .filter { lot -> | .filter { lot -> | ||||
| if (excludeInventoryLotLineId == null) true else lot.id != excludeInventoryLotLineId | 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: | * Workbench explicit-qty split support: | ||||
| * Set suggestions for this POL to an exact [targetQty] (independent of POL.qty). | * Set suggestions for this POL to an exact [targetQty] (independent of POL.qty). | ||||
| * - If [targetQty] <= 0: soft-delete all active suggestions for the line. | * - 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.status == InventoryLotLineStatus.AVAILABLE } | ||||
| .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } | .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } | ||||
| .filter { !isWarehouseExcluded(it, excludeSet) } | .filter { !isWarehouseExcluded(it, excludeSet) } | ||||
| .filter { matchesPolUom(it, line) } | |||||
| .sortedBy { it.inventoryLot?.expiryDate } | .sortedBy { it.inventoryLot?.expiryDate } | ||||
| val desired = buildDesiredAllocations(lots, targetQty, pickOrderLineId) | val desired = buildDesiredAllocations(lots, targetQty, pickOrderLineId) | ||||
| @@ -21,7 +21,10 @@ class InventoryController( | |||||
| return inventoryService.allInventoriesByPage(request); | 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") | @GetMapping("/searchLatest/getRecordByPage") | ||||
| fun searchInventoriesByLatestInventory( | fun searchInventoriesByLatestInventory( | ||||
| @ModelAttribute request: SearchInventoryRequest, | @ModelAttribute request: SearchInventoryRequest, | ||||
| @@ -72,18 +72,35 @@ class InventoryLotLineController ( | |||||
| val stockInLine = stockInLineRepository.findById(stockInLineId).orElseThrow { | val stockInLine = stockInLineRepository.findById(stockInLineId).orElseThrow { | ||||
| ResponseStatusException(HttpStatus.NOT_FOUND, "stockInLineId $stockInLineId not found") | 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 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( | 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) | remainingQty = (inventoryLotLine.inQty ?: zero) | ||||
| .minus(inventoryLotLine.outQty ?: zero) | .minus(inventoryLotLine.outQty ?: zero) | ||||
| .minus(inventoryLotLine.holdQty ?: 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) | 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") | @PostMapping("/workbench/analyze-qr-code") | ||||
| fun analyzeQrCodeWorkbench(@RequestBody request: QrCodeAnalysisRequest): QrCodeAnalysisResponse { | fun analyzeQrCodeWorkbench(@RequestBody request: QrCodeAnalysisRequest): QrCodeAnalysisResponse { | ||||
| return inventoryLotLineService.analyzeQrCodeWorkbench(request) | 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}") | @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 | import java.math.BigDecimal | ||||
| data class LotLineInfo( | data class LotLineInfo( | ||||
| val inventoryLotLineId: Long, | val inventoryLotLineId: Long, | ||||
| val itemId: Long, | val itemId: Long, | ||||
| @@ -9,7 +10,9 @@ data class LotLineInfo( | |||||
| val itemName: String, | val itemName: String, | ||||
| val lotNo: String, | val lotNo: String, | ||||
| val remainingQty: BigDecimal, | 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( | data class UpdateInventoryLotLineStatusRequest( | ||||
| val inventoryLotLineId: Long, | val inventoryLotLineId: Long, | ||||
| @@ -18,6 +21,8 @@ data class UpdateInventoryLotLineStatusRequest( | |||||
| data class QrCodeAnalysisRequest( | data class QrCodeAnalysisRequest( | ||||
| val itemId: Long, | val itemId: Long, | ||||
| val stockInLineId: Long, | val stockInLineId: Long, | ||||
| /** When set, sameItemLots only include lots with this UomConversion id. */ | |||||
| val uomId: Long? = null, | |||||
| ) | ) | ||||
| data class ScannedLotInfo( | data class ScannedLotInfo( | ||||
| @@ -25,7 +30,9 @@ data class ScannedLotInfo( | |||||
| val lotNo: String, | val lotNo: String, | ||||
| val inventoryLotLineId: Long, | val inventoryLotLineId: Long, | ||||
| val warehouseCode: String? = null, | val warehouseCode: String? = null, | ||||
| val warehouseName: String? = null | |||||
| val warehouseName: String? = null, | |||||
| val uom: String? = null, | |||||
| val uomId: Long? = null, | |||||
| ) | ) | ||||
| data class SameItemLotInfo( | data class SameItemLotInfo( | ||||
| @@ -34,6 +41,7 @@ data class SameItemLotInfo( | |||||
| val stockInLineId: Long? = null, | val stockInLineId: Long? = null, | ||||
| val availableQty: BigDecimal, | val availableQty: BigDecimal, | ||||
| val uom: String, | val uom: String, | ||||
| val uomId: Long? = null, | |||||
| val warehouseCode: String? = null, | val warehouseCode: String? = null, | ||||
| val warehouseName: String? = null | val warehouseName: String? = null | ||||
| ) | ) | ||||
| @@ -51,4 +59,4 @@ data class WorkbenchItemLotsResponse( | |||||
| val itemCode: String, | val itemCode: String, | ||||
| val itemName: String, | val itemName: String, | ||||
| val sameItemLots: List<SameItemLotInfo> | val sameItemLots: List<SameItemLotInfo> | ||||
| ) | |||||
| ) | |||||