diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt index 45c1ed6..d3ac33b 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt @@ -5,6 +5,7 @@ import com.ffii.fpsms.modules.master.entity.projections.WarehouseCombo import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository import java.io.Serializable @@ -22,6 +23,22 @@ interface WarehouseRepository : AbstractRepository { fun findAllByIdIn(ids: List): List; fun findAllByCodeAndDeletedIsFalse(code: String): List + /** FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.1 | 2026-09-10 */ + @Query( + """ + SELECT w FROM Warehouse w + WHERE w.deleted = false + AND (:storeId IS NULL OR :storeId = '' OR w.store_id = :storeId) + AND (:warehouse IS NULL OR :warehouse = '' OR w.warehouse = :warehouse) + AND (:area IS NULL OR :area = '' OR w.area = :area) + """ + ) + fun findByLocationFilters( + @Param("storeId") storeId: String?, + @Param("warehouse") warehouse: String?, + @Param("area") area: String?, + ): List + @Query( """ SELECT COUNT(w) FROM Warehouse w diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/DoUserPickAuditReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/DoUserPickAuditReportService.kt index d469358..737aa00 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/DoUserPickAuditReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/DoUserPickAuditReportService.kt @@ -8,7 +8,7 @@ class DoUserPickAuditReportService( private val jdbcDao: JdbcDao, ) { /** - * FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.2 | 2026-08-05 + * FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.3 | 2026-09-10 * DO workbench user-audit rows (Just issue only). * issueTypes may combine: 跳過提料(使用「已完成」)+多提+少提+擅自換批/揀錯批. * 少提/多提 use POL-level SUM(stock_out_line.qty) vs pol.qty (multi-lot split OK if total matches). @@ -55,10 +55,7 @@ class DoUserPickAuditReportService( "AND dop.ticketNo LIKE CONCAT('%', :ticketNo, '%')" } else "" - val itemSql = if (!itemCode.isNullOrBlank()) { - args["itemCode"] = itemCode.trim() - "AND i.code LIKE CONCAT('%', :itemCode, '%')" - } else "" + val itemSql = buildMultiValueLikeClause(itemCode, "i.code", "itemCode", args) val storeSql = if (!storeId.isNullOrBlank()) { args["storeId"] = storeId.trim().uppercase().replace("/", "").replace(" ", "") @@ -201,4 +198,22 @@ class DoUserPickAuditReportService( .map { (it["handler"]?.toString() ?: "").trim() } .filter { it.isNotBlank() } } + + /** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.3 | 2026-09-10 */ + private fun buildMultiValueLikeClause( + paramValue: String?, + columnName: String, + paramPrefix: String, + args: MutableMap, + ): String { + if (paramValue.isNullOrBlank()) return "" + val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() } + if (values.isEmpty()) return "" + val conditions = values.mapIndexed { index, value -> + val paramName = "${paramPrefix}_$index" + args[paramName] = "%$value%" + "$columnName LIKE :$paramName" + } + return "AND (${conditions.joinToString(" OR ")})" + } } diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt index 8a9985e..dbb798d 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt @@ -23,6 +23,7 @@ open class M18BomShopSyncReportService( private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") private val versionFromHeaderCode = Regex("V(\\d+)$", RegexOption.IGNORE_CASE) + /** FP-MTMS Version Checklist | Functions Ref. No. 83 | v1.0.0 | 2026-09-10 */ open fun searchBomShopSyncHistoryReport( syncDateStart: String?, syncDateEnd: String?, @@ -31,15 +32,26 @@ open class M18BomShopSyncReportService( ): Map { val start = parseDateStart(syncDateStart) val end = parseDateEnd(syncDateEnd) - val itemFilter = finishedItemCode?.trim()?.takeIf { it.isNotEmpty() } + val itemTokens = finishedItemCode + ?.split(",") + ?.map { it.trim() } + ?.filter { it.isNotEmpty() } + .orEmpty() + val sqlItemFilter = itemTokens.singleOrNull() val statusFilter = syncStatus?.trim()?.lowercase()?.takeIf { it.isNotEmpty() && it != "all" } val logs = m18BomShopSyncLogRepository.searchForReport( syncDateStart = start, syncDateEnd = end, - finishedItemCode = itemFilter, + finishedItemCode = sqlItemFilter, syncStatus = statusFilter, - ) + ).let { fetched -> + if (itemTokens.size <= 1) fetched + else fetched.filter { log -> + val code = log.finishedItemCode.orEmpty() + itemTokens.any { token -> code.contains(token, ignoreCase = true) } + } + } val syncRows = mutableListOf>() val materialRows = mutableListOf>() 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 7b5ff3e..c854c08 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 @@ -3,6 +3,7 @@ package com.ffii.fpsms.modules.stock.entity import com.ffii.core.support.AbstractRepository import com.ffii.fpsms.modules.stock.entity.projection.CurrentInventoryItemInfo import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo +import com.ffii.fpsms.modules.stock.entity.projection.InventoryStockUomSearchRow import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.Modifying @@ -56,8 +57,116 @@ WHERE ill.id = :id fun findInventoryLotLineInfoByInventoryLotItemIdIn(ids: List): List - @Query("select ill from InventoryLotLine ill where :id is null or ill.inventoryLot.item.id = :id order by ill.id desc") - fun findInventoryLotLineInfoByItemId(id: Long?, pageable: Pageable): Page + @Query( + """ + SELECT ill FROM InventoryLotLine ill + WHERE (:id IS NULL OR ill.inventoryLot.item.id = :id) + AND (:uomId IS NULL OR ill.stockUom.uom.id = :uomId) + ORDER BY ill.id DESC + """ + ) + /** FP-MTMS Version Checklist | Functions Ref. No. 80 | v1.0.0 | 2026-09-10 */ + fun findInventoryLotLineInfoByItemId( + @Param("id") id: Long?, + @Param("uomId") uomId: Long?, + pageable: Pageable, + ): Page + + @Query( + """ + SELECT DISTINCT ill.inventoryLot.item.id + FROM InventoryLotLine ill + WHERE ill.deleted = false + AND ill.status = :status + AND ill.warehouse.id IN :warehouseIds + """ + ) + fun findDistinctItemIdsByWarehouseIdsAndStatus( + @Param("warehouseIds") warehouseIds: List, + @Param("status") status: InventoryLotLineStatus, + ): List + + @Query( + """ + SELECT ill FROM InventoryLotLine ill + WHERE (:id IS NULL OR ill.inventoryLot.item.id = :id) + AND ill.warehouse.id IN :warehouseIds + AND (:uomId IS NULL OR ill.stockUom.uom.id = :uomId) + ORDER BY ill.id DESC + """ + ) + /** FP-MTMS Version Checklist | Functions Ref. No. 80 | v1.0.0 | 2026-09-10 */ + fun findInventoryLotLineInfoByItemIdAndWarehouseIdIn( + @Param("id") id: Long?, + @Param("warehouseIds") warehouseIds: List, + @Param("uomId") uomId: Long?, + pageable: Pageable, + ): Page + + @Query( + value = """ + SELECT + MAX(CAST(ill.id AS long)) AS id, + CAST(item.id AS long) 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, + CAST(uom.id AS long) 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 InventoryLotLine ill + JOIN ill.inventoryLot il + JOIN il.item item + JOIN ill.stockUom su + JOIN su.uom uom + WHERE ill.deleted = false + AND il.deleted = false + AND item.deleted = false + AND ill.status = :status + AND (:code IS NULL OR :code = '' OR LOWER(item.code) LIKE LOWER(CONCAT('%', :code, '%'))) + AND (:name IS NULL OR :name = '' OR LOWER(item.name) LIKE LOWER(CONCAT('%', :name, '%'))) + AND (:type IS NULL OR :type = '' OR item.type = :type) + AND (:hasWarehouseFilter = false OR ill.warehouse.id IN :warehouseIds) + AND (:lotNo IS NULL OR :lotNo = '' OR il.lotNo = :lotNo) + GROUP BY item.id, item.code, item.name, item.type, uom.id, uom.code, uom.udfudesc, uom.udfShortDesc + ORDER BY item.code ASC, uom.udfudesc ASC + """, + countQuery = """ + SELECT COUNT(DISTINCT CONCAT(CAST(item.id AS string), ':', CAST(uom.id AS string))) + FROM InventoryLotLine ill + JOIN ill.inventoryLot il + JOIN il.item item + JOIN ill.stockUom su + JOIN su.uom uom + WHERE ill.deleted = false + AND il.deleted = false + AND item.deleted = false + AND ill.status = :status + AND (:code IS NULL OR :code = '' OR LOWER(item.code) LIKE LOWER(CONCAT('%', :code, '%'))) + AND (:name IS NULL OR :name = '' OR LOWER(item.name) LIKE LOWER(CONCAT('%', :name, '%'))) + AND (:type IS NULL OR :type = '' OR item.type = :type) + AND (:hasWarehouseFilter = false OR ill.warehouse.id IN :warehouseIds) + AND (:lotNo IS NULL OR :lotNo = '' OR il.lotNo = :lotNo) + """, + ) + /** FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.1 | 2026-09-10 */ + fun searchInventoryGroupedByStockUom( + @Param("code") code: String, + @Param("name") name: String, + @Param("type") type: String, + @Param("status") status: InventoryLotLineStatus, + @Param("hasWarehouseFilter") hasWarehouseFilter: Boolean, + @Param("warehouseIds") warehouseIds: List, + @Param("lotNo") lotNo: String?, + pageable: Pageable, + ): Page @Query(""" SELECT ill FROM InventoryLotLine ill 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 new file mode 100644 index 0000000..e48f4af --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRow.kt @@ -0,0 +1,30 @@ +package com.ffii.fpsms.modules.stock.entity.projection + +import java.math.BigDecimal +import java.time.LocalDateTime + +/** + * Inventory search page: one row per item + lot-line stock UoM. + */ +interface InventoryStockUomSearchRow { + val id: Long? + val itemId: Long? + val itemCode: String? + val itemName: String? + val itemType: String? + val onHandQty: BigDecimal? + val onHoldQty: BigDecimal? + val unavailableQty: BigDecimal? + val availableQty: BigDecimal? + val uomId: Long? + val uomCode: String? + val uomUdfudesc: String? + val uomShortDesc: String? + val qtyPerSmallestUnit: BigDecimal? + val baseUom: String? + val price: BigDecimal? + val currencyName: String? + val status: String? + val latestMarketUnitPrice: Double? + val latestMupUpdatedDate: LocalDateTime? +} diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt index 78494b9..c47f2fd 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt @@ -76,9 +76,15 @@ open class InventoryLotLineService( return inventoryLotLineRepository.findInventoryLotLineInfoByInventoryLotItemIdIn(itemIds) } + /** FP-MTMS Version Checklist | Functions Ref. No. 80 | v1.0.0 | 2026-09-10 */ open fun allInventoryLotLinesByPage(request: SearchInventoryLotLineInfoRequest): RecordsRes { val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10); + val warehouseIds = resolveWarehouseIds(request.storeId, request.warehouse, request.area) + if (warehouseIds != null && warehouseIds.isEmpty()) { + return RecordsRes(emptyList(), 0) + } + val response = if (request.stockIssueBadItem == true) { inventoryLotLineRepository.findStockIssueBadItemLotLinesByItemId( request.itemId, @@ -86,8 +92,15 @@ open class InventoryLotLineService( listOf(InventoryLotLineStatus.AVAILABLE, InventoryLotLineStatus.UNAVAILABLE), pageable, ) + } else if (warehouseIds != null) { + inventoryLotLineRepository.findInventoryLotLineInfoByItemIdAndWarehouseIdIn( + request.itemId, + warehouseIds, + request.uomId, + pageable, + ) } else { - inventoryLotLineRepository.findInventoryLotLineInfoByItemId(request.itemId, pageable) + inventoryLotLineRepository.findInventoryLotLineInfoByItemId(request.itemId, request.uomId, pageable) } val records = response.content @@ -95,6 +108,28 @@ open class InventoryLotLineService( return RecordsRes(records, total.toInt()); } + /** + * FP-MTMS Version Checklist | Functions Ref. No. 80 | v1.0.0 | 2026-09-10 + * Returns null when no location filter is set. + */ + private fun resolveWarehouseIds( + storeId: String?, + warehouse: String?, + area: String?, + ): List? { + val trimmedStoreId = storeId?.trim()?.takeIf { it.isNotEmpty() } + val trimmedWarehouse = warehouse?.trim()?.takeIf { it.isNotEmpty() } + val trimmedArea = area?.trim()?.takeIf { it.isNotEmpty() } + if (trimmedStoreId == null && trimmedWarehouse == null && trimmedArea == null) { + return null + } + return warehouseRepository.findByLocationFilters( + trimmedStoreId, + trimmedWarehouse, + trimmedArea, + ).mapNotNull { it.id } + } + open fun searchStockIssueBadItemLotLines( request: SearchStockIssueBadItemLotLineRequest, ): RecordsRes { 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 d909d74..da108a7 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,6 +17,7 @@ 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.InventoryStockUomSearchRow import com.ffii.fpsms.modules.stock.web.model.SearchInventoryRequest import org.apache.poi.ss.usermodel.Sheet import org.apache.poi.ss.usermodel.Workbook @@ -89,43 +90,56 @@ 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 (latest id). Shared getRecordByPage is unchanged. + * FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.1 | 2026-09-10 + * Inventory search page: one row per item + lot-line stock UoM. */ open fun searchInventoriesByLatestInventory( request: SearchInventoryRequest, - ): RecordsRes { + ): RecordsRes { val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10) 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.findLatestInventoryInfoByItemCodeContainsAndItemNameContainsAndItemTypeAndItemIdInAndDeletedIsFalse( - code = code, - name = name, - type = type, - itemIds = itemIds, - pageable = pageable, - ) - } else { - inventoryRepository.findLatestInventoryInfoByItemCodeContainsAndItemNameContainsAndItemTypeAndDeletedIsFalse( - code = code, - name = name, - type = type, - pageable = pageable, - ) + 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, + ) + return RecordsRes(response.content, response.totalElements.toInt()) } + /** FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.1 | 2026-09-10 */ + private fun resolveWarehouseIds( + storeId: String?, + warehouse: String?, + area: String?, + ): List? { + val trimmedStoreId = storeId?.trim()?.takeIf { it.isNotEmpty() } + val trimmedWarehouse = warehouse?.trim()?.takeIf { it.isNotEmpty() } + val trimmedArea = area?.trim()?.takeIf { it.isNotEmpty() } + if (trimmedStoreId == null && trimmedWarehouse == null && trimmedArea == null) { + return null + } + return warehouseRepository.findByLocationFilters( + trimmedStoreId, + trimmedWarehouse, + trimmedArea, + ).mapNotNull { it.id } + } + open fun allInventoriesByItems(items: List): List{ return inventoryRepository.findInventoryInfoByItemInAndDeletedIsFalse(items); } 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 20cf216..c868176 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 @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.stock.web import com.ffii.core.response.RecordsRes import com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo +import com.ffii.fpsms.modules.stock.entity.projection.InventoryStockUomSearchRow import com.ffii.fpsms.modules.stock.service.InventoryService import com.ffii.fpsms.modules.stock.web.model.SearchInventoryRequest import org.springframework.web.bind.annotation.* @@ -22,13 +23,13 @@ class InventoryController( } /** - * 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. + * FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.1 | 2026-09-10 + * Inventory search page: one row per item + lot-line stock UoM, with optional location filters. */ @GetMapping("/searchLatest/getRecordByPage") fun searchInventoriesByLatestInventory( @ModelAttribute request: SearchInventoryRequest, - ): RecordsRes { + ): RecordsRes { return inventoryService.searchInventoriesByLatestInventory(request) } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryLotLineInfoRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryLotLineInfoRequest.kt index 53d7327..edf1b75 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryLotLineInfoRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryLotLineInfoRequest.kt @@ -2,8 +2,15 @@ package com.ffii.fpsms.modules.stock.web.model data class SearchInventoryLotLineInfoRequest( val itemId: Long? = null, + val uomId: Long? = null, val pageSize: Int?, val pageNum: Int?, /** When true: non-expired lots with inQty > outQty; includes available and unavailable status. */ val stockIssueBadItem: Boolean? = false, + /** Floor / store, e.g. 2F. Used by inventory location search. */ + val storeId: String? = null, + /** Warehouse zone, e.g. W201. */ + val warehouse: String? = null, + /** Area, e.g. #R. */ + val area: String? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryRequest.kt index c40dcb4..7e3cbc2 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryRequest.kt @@ -5,6 +5,12 @@ data class SearchInventoryRequest( val name: String, val type: String, val lotNo: String? = null, + /** Floor / store, e.g. 2F. Used by inventory location search. */ + val storeId: String? = null, + /** Warehouse zone, e.g. W201. */ + val warehouse: String? = null, + /** Area, e.g. #R. */ + val area: String? = null, val pageNum: Int?, val pageSize: Int? )