Pārlūkot izejas kodu

Add inventory location filters and group search by item + stock UoM; support multi-value item codes on reports

production
Harry Groves pirms 1 nedēļas
vecāks
revīzija
cffd5550ed
10 mainītis faili ar 284 papildinājumiem un 38 dzēšanām
  1. +17
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt
  2. +20
    -5
      src/main/java/com/ffii/fpsms/modules/report/service/DoUserPickAuditReportService.kt
  3. +15
    -3
      src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt
  4. +111
    -2
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  5. +30
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRow.kt
  6. +36
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt
  7. +38
    -24
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt
  8. +4
    -3
      src/main/java/com/ffii/fpsms/modules/stock/web/InventoryController.kt
  9. +7
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryLotLineInfoRequest.kt
  10. +6
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryRequest.kt

+ 17
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt Parādīt failu

@@ -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<Warehouse, Long> {
fun findAllByIdIn(ids: List<Long>): List<Warehouse>;
fun findAllByCodeAndDeletedIsFalse(code: String): List<Warehouse>

/** 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<Warehouse>

@Query(
"""
SELECT COUNT(w) FROM Warehouse w


+ 20
- 5
src/main/java/com/ffii/fpsms/modules/report/service/DoUserPickAuditReportService.kt Parādīt failu

@@ -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, Any>,
): 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 ")})"
}
}

+ 15
- 3
src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt Parādīt failu

@@ -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<String, Any> {
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<Map<String, Any?>>()
val materialRows = mutableListOf<Map<String, Any?>>()


+ 111
- 2
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt Parādīt failu

@@ -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<Serializable>): List<InventoryLotLineInfo>

@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<InventoryLotLineInfo>
@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<InventoryLotLineInfo>

@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<Long>,
@Param("status") status: InventoryLotLineStatus,
): List<Long>

@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<Long>,
@Param("uomId") uomId: Long?,
pageable: Pageable,
): Page<InventoryLotLineInfo>

@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<Long>,
@Param("lotNo") lotNo: String?,
pageable: Pageable,
): Page<InventoryStockUomSearchRow>

@Query("""
SELECT ill FROM InventoryLotLine ill


+ 30
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryStockUomSearchRow.kt Parādīt failu

@@ -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?
}

+ 36
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt Parādīt failu

@@ -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<InventoryLotLineInfo> {
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<InventoryLotLineInfo>(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<Long>? {
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<InventoryLotLineInfo> {


+ 38
- 24
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt Parādīt failu

@@ -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<InventoryInfo> {
): RecordsRes<InventoryStockUomSearchRow> {
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<Long>? {
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<Items>): List<InventoryInfo>{
return inventoryRepository.findInventoryInfoByItemInAndDeletedIsFalse(items);
}


+ 4
- 3
src/main/java/com/ffii/fpsms/modules/stock/web/InventoryController.kt Parādīt failu

@@ -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<InventoryInfo> {
): RecordsRes<InventoryStockUomSearchRow> {
return inventoryService.searchInventoriesByLatestInventory(request)
}



+ 7
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryLotLineInfoRequest.kt Parādīt failu

@@ -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,
)

+ 6
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchInventoryRequest.kt Parādīt failu

@@ -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?
)

Notiek ielāde…
Atcelt
Saglabāt