|
|
|
@@ -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<InventoryInfo>{ |
|
|
|
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<InventoryInfo> { |
|
|
|
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<InventoryInfo>(records, total.toInt()); |
|
|
|
val args = mutableMapOf<String, Any>( |
|
|
|
"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<InventoryStockUomSearchRow> { |
|
|
|
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<String, Any>( |
|
|
|
"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<String, Any>): 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<String, Any>): 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<Items>): List<InventoryInfo>{ |
|
|
|
return inventoryRepository.findInventoryInfoByItemInAndDeletedIsFalse(items); |
|
|
|
} |
|
|
|
|