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 452bd0a..ceac69d 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 @@ -410,8 +410,13 @@ open class ItemsService( "i.AverageUnitPrice as averageUnitPrice, " + "i.LatestMarketUnitPrice as latestMarketUnitPrice, " + "DATE_FORMAT(i.latestMupUpdatedDate, '%Y-%m-%d %H:%i') as latestMupUpdatedDate, " + + "uc_s.id as uomId, " + + "uc_s.code as uom, " + + "uc_s.udfudesc as uomDesc, " + "uc_p.udfudesc as purchaseUnit " + "FROM items i " + + "LEFT JOIN item_uom iu_s ON iu_s.itemId = i.id AND iu_s.deleted = false AND iu_s.stockUnit = true " + + "LEFT JOIN uom_conversion uc_s ON uc_s.id = iu_s.uomId " + "LEFT JOIN item_uom iu_p ON iu_p.itemId = i.id AND iu_p.deleted = false AND iu_p.purchaseUnit = true " + "LEFT JOIN uom_conversion uc_p ON uc_p.id = iu_p.uomId " + "WHERE i.deleted = false" diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt index c854c08..75b60c7 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt @@ -156,7 +156,10 @@ WHERE ill.id = :id AND (:lotNo IS NULL OR :lotNo = '' OR il.lotNo = :lotNo) """, ) - /** FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.1 | 2026-09-10 */ + /** + * FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.1 | 2026-09-10 + * Lot UOMs only. Location Search uses [com.ffii.fpsms.modules.stock.service.InventoryService.searchInventoriesByLatestInventory]. + */ fun searchInventoryGroupedByStockUom( @Param("code") code: String, @Param("name") name: String, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryInfoDto.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryInfoDto.kt new file mode 100644 index 0000000..781e0cc --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryInfoDto.kt @@ -0,0 +1,28 @@ +package com.ffii.fpsms.modules.stock.entity.projection + +import java.math.BigDecimal +import java.time.LocalDateTime + +/** JDBC row for Item Search: inventory buckets plus current master stock UOM when missing. */ +data class InventoryInfoDto( + override val id: Long? = null, + override val itemId: Long? = null, + override val itemCode: String? = null, + override val itemName: String? = null, + override val itemType: String? = null, + override val onHandQty: BigDecimal? = null, + override val onHoldQty: BigDecimal? = null, + override val unavailableQty: BigDecimal? = null, + override val availableQty: BigDecimal? = null, + override val stockUomId: Long? = null, + override val uomCode: String? = null, + override val uomUdfudesc: String? = null, + override val uomShortDesc: String? = null, + override val qtyPerSmallestUnit: BigDecimal? = null, + override val baseUom: String? = null, + override val price: BigDecimal? = null, + override val currencyName: String? = null, + override val status: String? = null, + override val latestMarketUnitPrice: Double? = null, + override val latestMupUpdatedDate: LocalDateTime? = null, +) : InventoryInfo diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRow.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRow.kt index e48f4af..9346003 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRow.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRow.kt @@ -4,7 +4,8 @@ import java.math.BigDecimal import java.time.LocalDateTime /** - * Inventory search page: one row per item + lot-line stock UoM. + * Inventory search page: one row per item + UoM + * (lot-line / bucket UOMs, plus current master stockUnit when it has no rows). */ interface InventoryStockUomSearchRow { val id: Long? diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRowDto.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRowDto.kt new file mode 100644 index 0000000..7218b54 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRowDto.kt @@ -0,0 +1,28 @@ +package com.ffii.fpsms.modules.stock.entity.projection + +import java.math.BigDecimal +import java.time.LocalDateTime + +/** JDBC row for inventory search: lot UOMs plus current master stock UOM when missing. */ +data class InventoryStockUomSearchRowDto( + override val id: Long? = null, + override val itemId: Long? = null, + override val itemCode: String? = null, + override val itemName: String? = null, + override val itemType: String? = null, + override val onHandQty: BigDecimal? = null, + override val onHoldQty: BigDecimal? = null, + override val unavailableQty: BigDecimal? = null, + override val availableQty: BigDecimal? = null, + override val uomId: Long? = null, + override val uomCode: String? = null, + override val uomUdfudesc: String? = null, + override val uomShortDesc: String? = null, + override val qtyPerSmallestUnit: BigDecimal? = null, + override val baseUom: String? = null, + override val price: BigDecimal? = null, + override val currencyName: String? = null, + override val status: String? = null, + override val latestMarketUnitPrice: Double? = null, + override val latestMupUpdatedDate: LocalDateTime? = null, +) : InventoryStockUomSearchRow 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 dd0fea9..71744df 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 @@ -17,15 +17,17 @@ import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderType import com.ffii.fpsms.modules.stock.entity.* import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus import com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo +import com.ffii.fpsms.modules.stock.entity.projection.InventoryInfoDto import com.ffii.fpsms.modules.stock.entity.projection.InventoryStockUomSearchRow +import com.ffii.fpsms.modules.stock.entity.projection.InventoryStockUomSearchRowDto import com.ffii.fpsms.modules.stock.web.model.SearchInventoryRequest import org.apache.poi.ss.usermodel.Sheet import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.core.io.support.PathMatchingResourcePatternResolver -import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service import java.math.BigDecimal +import java.sql.Timestamp import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId @@ -58,67 +60,80 @@ open class InventoryService( return inventoryRepository.findInventoryInfoByDeletedIsFalse(); } - open fun allInventoriesByPage(request: SearchInventoryRequest): RecordsRes{ - val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10) + /** + * Item Search: one row per (itemId, stockUomId) bucket, plus current master stockUnit + * as a 0-qty row when that UoM has no inventory bucket. + */ + open fun allInventoriesByPage(request: SearchInventoryRequest): RecordsRes { + val pageSize = (request.pageSize ?: 10).coerceAtLeast(1) + val pageNum = (request.pageNum ?: 0).coerceAtLeast(0) + val code = request.code?.trim().orEmpty() + val name = request.name?.trim().orEmpty() + val type = request.type?.trim().orEmpty() val lotNo = request.lotNo?.trim()?.takeIf { it.isNotEmpty() } - - val response = if (lotNo != null) { - val itemIds = inventoryLotLineRepository.findDistinctItemIdsByLotNo(lotNo) - if (itemIds.isEmpty()) { - return RecordsRes(emptyList(), 0) - } - inventoryRepository.findInventoryInfoByItemCodeContainsAndItemNameContainsAndItemTypeAndItemIdInAndDeletedIsFalse( - code = request.code, - name = request.name, - type = request.type, - itemIds = itemIds, - pageable = pageable - ) + val itemIds = if (lotNo != null) { + inventoryLotLineRepository.findDistinctItemIdsByLotNo(lotNo) } else { - inventoryRepository.findInventoryInfoByItemCodeContainsAndItemNameContainsAndItemTypeAndDeletedIsFalse( - code = request.code, - name = request.name, - type = request.type, - pageable = pageable - ) + emptyList() + } + if (lotNo != null && itemIds.isEmpty()) { + return RecordsRes(emptyList(), 0) } - val records = response.content - val total = response.totalElements - - return RecordsRes(records, total.toInt()); + val args = mutableMapOf( + "code" to code, + "name" to name, + "type" to type, + "hasItemIdFilter" to if (lotNo != null) 1 else 0, + "itemIds" to itemIds.ifEmpty { listOf(-1L) }, + ) + val unionSql = buildItemSearchUnionSql() + val total = jdbcDao.queryForInt("SELECT COUNT(*) FROM ($unionSql) t", args) + args["limit"] = pageSize + args["offset"] = pageNum * pageSize + val rows = jdbcDao.queryForList( + "$unionSql ORDER BY itemCode ASC, uomUdfudesc ASC LIMIT :limit OFFSET :offset", + args, + ) + return RecordsRes(rows.map { mapItemSearchRow(it) }, total) } /** - * Location Search: one row per item + lot-line stock UoM, optionally filtered by warehouse. - * Item Search uses [allInventoriesByPage] (inventory buckets, no location). + * Location Search: one row per item + lot-line UoM, plus current master stockUnit + * as a 0-qty row when that UoM has no matching lots. */ open fun searchInventoriesByLatestInventory( request: SearchInventoryRequest, ): RecordsRes { - val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10) + val pageSize = (request.pageSize ?: 10).coerceAtLeast(1) + val pageNum = (request.pageNum ?: 0).coerceAtLeast(0) val code = request.code?.trim().orEmpty() val name = request.name?.trim().orEmpty() val type = request.type?.trim().orEmpty() - val lotNo = request.lotNo?.trim()?.takeIf { it.isNotEmpty() } + val lotNo = request.lotNo?.trim().orEmpty() val warehouseIds = resolveWarehouseIds(request.storeId, request.warehouse, request.area) if (warehouseIds != null && warehouseIds.isEmpty()) { return RecordsRes(emptyList(), 0) } - val hasWarehouseFilter = warehouseIds != null - val response = inventoryLotLineRepository.searchInventoryGroupedByStockUom( - code = code, - name = name, - type = type, - status = InventoryLotLineStatus.AVAILABLE, - hasWarehouseFilter = hasWarehouseFilter, - warehouseIds = warehouseIds ?: listOf(-1L), - lotNo = lotNo, - pageable = pageable, + val args = mutableMapOf( + "code" to code, + "name" to name, + "type" to type, + "lotNo" to lotNo, + "status" to InventoryLotLineStatus.AVAILABLE.value, + "hasWarehouseFilter" to if (warehouseIds != null) 1 else 0, + "warehouseIds" to (warehouseIds ?: listOf(-1L)), ) - - return RecordsRes(response.content, response.totalElements.toInt()) + val unionSql = buildLocationSearchUnionSql() + val total = jdbcDao.queryForInt("SELECT COUNT(*) FROM ($unionSql) t", args) + args["limit"] = pageSize + args["offset"] = pageNum * pageSize + val rows = jdbcDao.queryForList( + "$unionSql ORDER BY itemCode ASC, uomUdfudesc ASC LIMIT :limit OFFSET :offset", + args, + ) + return RecordsRes(rows.map { mapLocationSearchRow(it) }, total) } /** FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.1 | 2026-09-10 */ @@ -140,6 +155,260 @@ open class InventoryService( ).mapNotNull { it.id } } + private fun buildItemSearchUnionSql(): String { + val itemFilter = """ + AND (:code = '' OR item.code LIKE CONCAT('%', :code, '%')) + AND (:name = '' OR item.name LIKE CONCAT('%', :name, '%')) + AND (:type = '' OR item.type = :type) + AND (:hasItemIdFilter = 0 OR item.id IN (:itemIds)) + """.trimIndent() + return """ + SELECT + inv.id AS id, + item.id AS itemId, + item.code AS itemCode, + item.name AS itemName, + item.type AS itemType, + COALESCE(inv.onHandQty, 0) AS onHandQty, + COALESCE(inv.onHoldQty, 0) AS onHoldQty, + COALESCE(inv.unavailableQty, 0) AS unavailableQty, + (COALESCE(inv.onHandQty, 0) - COALESCE(inv.unavailableQty, 0)) AS availableQty, + uc.id AS stockUomId, + uc.code AS uomCode, + uc.udfudesc AS uomUdfudesc, + uc.udfShortDesc AS uomShortDesc, + (COALESCE(inv.onHandQty, 0) - COALESCE(inv.unavailableQty, 0)) AS qtyPerSmallestUnit, + uc_base.udfudesc AS baseUom, + inv.price AS price, + cur.name AS currencyName, + inv.status AS status, + item.latestMarketUnitPrice AS latestMarketUnitPrice, + item.latestMupUpdatedDate AS latestMupUpdatedDate + FROM inventory inv + INNER JOIN items item ON item.id = inv.itemId AND item.deleted = 0 + LEFT JOIN uom_conversion uc ON uc.id = inv.stockUomId + LEFT JOIN uom_conversion uc_base ON uc_base.id = inv.uomId + LEFT JOIN currency cur ON cur.id = inv.currencyId + WHERE inv.deleted = 0 + $itemFilter + + UNION ALL + + SELECT + 0 AS id, + item.id AS itemId, + item.code AS itemCode, + item.name AS itemName, + item.type AS itemType, + 0 AS onHandQty, + 0 AS onHoldQty, + 0 AS unavailableQty, + 0 AS availableQty, + uc.id AS stockUomId, + uc.code AS uomCode, + uc.udfudesc AS uomUdfudesc, + uc.udfShortDesc AS uomShortDesc, + 0 AS qtyPerSmallestUnit, + uc.udfudesc AS baseUom, + NULL AS price, + NULL AS currencyName, + 'active' AS status, + item.latestMarketUnitPrice AS latestMarketUnitPrice, + item.latestMupUpdatedDate AS latestMupUpdatedDate + FROM items item + INNER JOIN item_uom iu + ON iu.itemId = item.id + AND iu.stockUnit = 1 + AND iu.deleted = 0 + AND iu.id = ( + SELECT MIN(iu2.id) + FROM item_uom iu2 + WHERE iu2.itemId = item.id + AND iu2.stockUnit = 1 + AND iu2.deleted = 0 + ) + INNER JOIN uom_conversion uc ON uc.id = iu.uomId + WHERE item.deleted = 0 + $itemFilter + AND EXISTS ( + SELECT 1 + FROM inventory inv2 + WHERE inv2.itemId = item.id + AND inv2.deleted = 0 + ) + AND NOT EXISTS ( + SELECT 1 + FROM inventory inv2 + WHERE inv2.itemId = item.id + AND inv2.deleted = 0 + AND inv2.stockUomId = uc.id + ) + """.trimIndent() + } + + private fun buildLocationSearchUnionSql(): String { + val itemFilter = """ + AND (:code = '' OR LOWER(item.code) LIKE LOWER(CONCAT('%', :code, '%'))) + AND (:name = '' OR LOWER(item.name) LIKE LOWER(CONCAT('%', :name, '%'))) + AND (:type = '' OR item.type = :type) + """.trimIndent() + val lotFilter = """ + ill.deleted = 0 + AND il.deleted = 0 + AND item.deleted = 0 + AND ill.status = :status + AND (:hasWarehouseFilter = 0 OR ill.warehouseId IN (:warehouseIds)) + AND (:lotNo = '' OR il.lotNo = :lotNo) + """.trimIndent() + val lotExistsFilter = """ + ill2.deleted = 0 + AND il2.deleted = 0 + AND ill2.status = :status + AND (:hasWarehouseFilter = 0 OR ill2.warehouseId IN (:warehouseIds)) + AND (:lotNo = '' OR il2.lotNo = :lotNo) + """.trimIndent() + return """ + SELECT + MAX(ill.id) AS id, + item.id AS itemId, + item.code AS itemCode, + item.name AS itemName, + item.type AS itemType, + SUM(COALESCE(ill.inQty, 0)) AS onHandQty, + SUM(COALESCE(ill.holdQty, 0)) AS onHoldQty, + SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) AS availableQty, + uom.id AS uomId, + uom.code AS uomCode, + uom.udfudesc AS uomUdfudesc, + uom.udfShortDesc AS uomShortDesc, + SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) AS qtyPerSmallestUnit, + MAX(item.latestMarketUnitPrice) AS latestMarketUnitPrice, + MAX(item.latestMupUpdatedDate) AS latestMupUpdatedDate + FROM inventory_lot_line ill + INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId + INNER JOIN items item ON item.id = il.itemId + INNER JOIN item_uom su ON su.id = ill.stockItemUomId + INNER JOIN uom_conversion uom ON uom.id = su.uomId + WHERE $lotFilter + $itemFilter + GROUP BY item.id, item.code, item.name, item.type, uom.id, uom.code, uom.udfudesc, uom.udfShortDesc + + UNION ALL + + SELECT + 0 AS id, + item.id AS itemId, + item.code AS itemCode, + item.name AS itemName, + item.type AS itemType, + 0 AS onHandQty, + 0 AS onHoldQty, + 0 AS availableQty, + uom.id AS uomId, + uom.code AS uomCode, + uom.udfudesc AS uomUdfudesc, + uom.udfShortDesc AS uomShortDesc, + 0 AS qtyPerSmallestUnit, + item.latestMarketUnitPrice AS latestMarketUnitPrice, + item.latestMupUpdatedDate AS latestMupUpdatedDate + FROM items item + INNER JOIN item_uom iu + ON iu.itemId = item.id + AND iu.stockUnit = 1 + AND iu.deleted = 0 + AND iu.id = ( + SELECT MIN(iu2.id) + FROM item_uom iu2 + WHERE iu2.itemId = item.id + AND iu2.stockUnit = 1 + AND iu2.deleted = 0 + ) + INNER JOIN uom_conversion uom ON uom.id = iu.uomId + WHERE item.deleted = 0 + $itemFilter + AND EXISTS ( + SELECT 1 + FROM inventory_lot_line ill2 + INNER JOIN inventory_lot il2 ON il2.id = ill2.inventoryLotId + WHERE il2.itemId = item.id + AND $lotExistsFilter + ) + AND NOT EXISTS ( + SELECT 1 + FROM inventory_lot_line ill2 + INNER JOIN inventory_lot il2 ON il2.id = ill2.inventoryLotId + INNER JOIN item_uom su2 ON su2.id = ill2.stockItemUomId + WHERE il2.itemId = item.id + AND su2.uomId = uom.id + AND $lotExistsFilter + ) + """.trimIndent() + } + + private fun mapItemSearchRow(row: Map): InventoryInfoDto { + val available = toBigDecimal(row["availableQty"]) + return InventoryInfoDto( + id = toLong(row["id"]), + itemId = toLong(row["itemId"]), + itemCode = row["itemCode"]?.toString(), + itemName = row["itemName"]?.toString(), + itemType = row["itemType"]?.toString(), + onHandQty = toBigDecimal(row["onHandQty"]), + onHoldQty = toBigDecimal(row["onHoldQty"]), + unavailableQty = toBigDecimal(row["unavailableQty"]), + availableQty = available, + stockUomId = toLong(row["stockUomId"]), + uomCode = row["uomCode"]?.toString(), + uomUdfudesc = row["uomUdfudesc"]?.toString(), + uomShortDesc = row["uomShortDesc"]?.toString(), + qtyPerSmallestUnit = available, + baseUom = row["baseUom"]?.toString(), + price = toBigDecimal(row["price"]), + currencyName = row["currencyName"]?.toString(), + status = row["status"]?.toString(), + latestMarketUnitPrice = (row["latestMarketUnitPrice"] as? Number)?.toDouble(), + latestMupUpdatedDate = toLocalDateTime(row["latestMupUpdatedDate"]), + ) + } + + private fun mapLocationSearchRow(row: Map): InventoryStockUomSearchRowDto { + val available = toBigDecimal(row["availableQty"]) + return InventoryStockUomSearchRowDto( + id = toLong(row["id"]), + itemId = toLong(row["itemId"]), + itemCode = row["itemCode"]?.toString(), + itemName = row["itemName"]?.toString(), + itemType = row["itemType"]?.toString(), + onHandQty = toBigDecimal(row["onHandQty"]), + onHoldQty = toBigDecimal(row["onHoldQty"]), + unavailableQty = BigDecimal.ZERO, + availableQty = available, + uomId = toLong(row["uomId"]), + uomCode = row["uomCode"]?.toString(), + uomUdfudesc = row["uomUdfudesc"]?.toString(), + uomShortDesc = row["uomShortDesc"]?.toString(), + qtyPerSmallestUnit = available, + latestMarketUnitPrice = (row["latestMarketUnitPrice"] as? Number)?.toDouble(), + latestMupUpdatedDate = toLocalDateTime(row["latestMupUpdatedDate"]), + ) + } + + private fun toLong(value: Any?): Long? = (value as? Number)?.toLong() + + private fun toBigDecimal(value: Any?): BigDecimal? = when (value) { + null -> null + is BigDecimal -> value + is Number -> BigDecimal(value.toString()) + else -> null + } + + private fun toLocalDateTime(value: Any?): LocalDateTime? = when (value) { + is LocalDateTime -> value + is Timestamp -> value.toLocalDateTime() + is java.sql.Date -> value.toLocalDate().atStartOfDay() + else -> null + } + open fun allInventoriesByItems(items: List): List{ return inventoryRepository.findInventoryInfoByItemInAndDeletedIsFalse(items); } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt index 6b192dc..1e9ace1 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt @@ -25,7 +25,7 @@ open class StockAdjustmentService( private val stockAdjustmentRecordRepository: StockAdjustmentRecordRepository ) { - /** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.7 | 2026-09-08 */ + /** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.8 | 2026-09-16 */ @Transactional open fun submit(request: StockAdjustmentRequest): MessageResponse { val originalById = request.originalLines.filter { it.id > 0 }.associateBy { it.id } @@ -55,7 +55,8 @@ open class StockAdjustmentService( productLotNo = current.productlotNo?.takeIf { it.isNotBlank() }, dnNo = current.dnNo?.takeIf { it.isNotBlank() }, type = stockType, - warehouseId = current.warehouseId + warehouseId = current.warehouseId, + uomId = current.uomId, ) ) saveAdjustmentRecordForStockIn(stockInLine, current.remarks) 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 c46be31..7283ead 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 @@ -28,6 +28,7 @@ import com.ffii.core.utils.PdfUtils; import com.ffii.core.utils.ZebraPrinterUtil import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus +import com.ffii.fpsms.modules.master.entity.ItemUom import com.ffii.fpsms.modules.master.entity.ItemUomRespository import com.ffii.fpsms.modules.master.entity.WarehouseRepository import com.ffii.fpsms.modules.master.service.ItemUomService @@ -1496,12 +1497,7 @@ open class StockInLineService( savedStockInLine.inventoryLot = savedInventoryLot // Step 4: Create a row in inventory_lot_line table - val stockItemUom = itemUomRepository.findFirstByItemIdAndStockUnitIsTrueAndDeletedIsFalseOrderByIdAsc( - request.itemId - ) ?: throw IllegalArgumentException( - "Stock UOM not found for item: id=${request.itemId}, itemNo=${request.itemNo}. " + - "Ensure at least one item_uom row has stockUnit=1, deleted=0 for this item." - ) + val stockItemUom = resolveItemUomForNewLot(request.itemId, request.itemNo, request.uomId) val warehouse = if (request.warehouseId != null) { warehouseRepository.findById(request.warehouseId).orElseThrow() @@ -1531,6 +1527,20 @@ open class StockInLineService( return savedStockInLine } + private fun resolveItemUomForNewLot(itemId: Long, itemNo: String, uomId: Long?): ItemUom { + if (uomId != null) { + return itemUomRepository.findFirstByItemIdAndUomIdAndDeletedIsFalse(itemId, uomId) + ?: throw IllegalArgumentException( + "Item UOM not found for item: id=$itemId, itemNo=$itemNo, uomId=$uomId." + ) + } + return itemUomRepository.findFirstByItemIdAndStockUnitIsTrueAndDeletedIsFalseOrderByIdAsc(itemId) + ?: throw IllegalArgumentException( + "Stock UOM not found for item: id=$itemId, itemNo=$itemNo. " + + "Ensure at least one item_uom row has stockUnit=1, deleted=0 for this item." + ) + } + @Transactional open fun createStockInForExistingInventoryLotLine( request: StockInRequest, 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 9205a8f..f63b22b 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 @@ -17,15 +17,15 @@ class InventoryController( return inventoryService.allInventories() } - /** Full inventory search: one row per (itemId, stockUomId) bucket. */ + /** Full inventory search: one row per (itemId, stockUomId) bucket, plus current stockUnit when missing. */ @GetMapping("/getRecordByPage") fun allInventoriesByPage(@ModelAttribute request: SearchInventoryRequest): RecordsRes { return inventoryService.allInventoriesByPage(request) } /** - * Location Search: one row per item + lot-line stock UoM, with optional location filters. - * Item Search uses GET /inventory/getRecordByPage (inventory buckets). + * Location Search: one row per item + lot-line stock UoM, plus current master stockUnit when missing. + * Item Search uses GET /inventory/getRecordByPage (inventory buckets + current stockUnit). */ @GetMapping("/searchLatest/getRecordByPage") fun searchInventoriesByLatestInventory( diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt index 570985c..b2b4dbe 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt @@ -15,6 +15,7 @@ data class StockAdjustmentLineRequest( val expiryDate: String, val warehouseId: Long, val uom: String? = null, + val uomId: Long? = null, val remarks: String? = null ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt index 5a935dd..2f617e0 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt @@ -13,5 +13,6 @@ data class StockInRequest( val productLotNo: String? = null, val dnNo: String? = null, val type: String? = null, - val warehouseId: Long? = null + val warehouseId: Long? = null, + val uomId: Long? = null, ) \ No newline at end of file