Преглед изворни кода

建議批號 UOM 檢核/不符則擋、標籤列印只顯示同 UOM

production
CANCERYS\kw093 пре 9 часа
родитељ
комит
3ebf46e304
15 измењених фајлова са 268 додато и 64 уклоњено
  1. +124
    -36
      src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt
  2. +9
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt
  3. +2
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt
  4. +1
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderWorkbenchService.kt
  5. +2
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt
  6. +3
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryLotLineInfo.kt
  7. +16
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt
  8. +2
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt
  9. +5
    -4
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  10. +12
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt
  11. +23
    -6
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  12. +17
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt
  13. +4
    -1
      src/main/java/com/ffii/fpsms/modules/stock/web/InventoryController.kt
  14. +37
    -11
      src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt
  15. +11
    -3
      src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt

+ 124
- 36
src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt Прегледај датотеку

@@ -266,6 +266,7 @@ open class ItemsService(
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>> {
try {
println("=== Debug: getPickOrderItemsByPage in ItemsService ===")
@@ -274,10 +275,12 @@ open class ItemsService(
val sql = StringBuilder(
"""
SELECT
CONCAT(po.id, '-', pol.itemId) as id,
CONCAT(po.id, '-', pol.id) as id,
po.id as pickOrderId,
po.code as pickOrderCode,
pol.id as pickOrderLineId,
pol.itemId,
pol.uomId,
i.code as itemCode,
i.name as itemName,
pol.qty as requiredQty,
@@ -296,15 +299,16 @@ open class ItemsService(
LEFT JOIN (
SELECT
il.itemId,
iu.uomId as uomId,
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
JOIN item_uom iu ON iu.id = ill.stockItemUomId AND iu.deleted = false
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 = pol.itemId
GROUP BY il.itemId, iu.uomId
) inv ON inv.itemId = pol.itemId AND inv.uomId = pol.uomId
WHERE po.deleted = false
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>> {
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")) {
sql.append(" AND i.name LIKE :name ")
}

if (args.containsKey("code")) {
sql.append(" AND i.code LIKE :code ")
}

if (args.containsKey("description")) {
sql.append(" AND i.description LIKE :description ")
}

if (args.containsKey("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>> {
@@ -971,4 +985,78 @@ open class ItemsService(
}?.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(),
)
}

}

+ 9
- 0
src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt Прегледај датотеку

@@ -45,6 +45,15 @@ class ItemsController(
fun allConsumables(): List<Map<String, Any>> {
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")
// fun getAllItemsByPage(@RequestBody filterRequest: HttpServletRequest): RecordsRes<Map<String, Any>> {
// val pageSize = filterRequest.getParameter("pageSize").toString().toInt(); // Default to 10 if not provided


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt Прегледај датотеку

@@ -96,6 +96,7 @@ SELECT
i.code as itemCode,
i.name as itemName,
i.item_Order as itemOrder,
pol.uomId as uomId,
uc.code as uomCode,
uc.udfudesc as uomDesc,
uc.udfShortDesc as uomShortDesc,
@@ -261,6 +262,7 @@ ORDER BY
"id" to firstLineRow["itemId"],
"code" to firstLineRow["itemCode"],
"name" to firstLineRow["itemName"],
"uomId" to firstLineRow["uomId"],
"uomCode" to firstLineRow["uomCode"],
"uomDesc" to firstLineRow["uomDesc"],
"uomShortDesc" to firstLineRow["uomShortDesc"],


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderWorkbenchService.kt Прегледај датотеку

@@ -251,6 +251,7 @@ open class PickOrderWorkbenchService(
"id" to pol.item?.id,
"code" to pol.item?.code,
"name" to pol.item?.name,
"uomId" to pol.uom?.id,
"uomCode" to pol.uom?.code,
"uomDesc" to pol.uom?.udfudesc,
),


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt Прегледај датотеку

@@ -47,6 +47,7 @@ interface InventoryRepository: AbstractRepository<Inventory, Long> {
): 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;
* one row per item = latest inventory id (no larger id for same item).
*/
@@ -72,6 +73,7 @@ interface InventoryRepository: AbstractRepository<Inventory, Long> {
pageable: Pageable,
): Page<InventoryInfo>

/** FP-MTMS Version Checklist | Functions Ref. No. 19 | v1.0.0 | 2026-07-17 */
@Query(
"""
SELECT i FROM Inventory i


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryLotLineInfo.kt Прегледај датотеку

@@ -50,6 +50,9 @@ interface InventoryLotLineInfo {
@get:Value("#{target.stockUom?.uom?.udfudesc}")
val uom: String?

@get:Value("#{target.stockUom?.uom?.id}")
val uomId: Long?

@get:Value("#{target.inventoryLot.expiryDate}")
val expiryDate: LocalDate
}


+ 16
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt Прегледај датотеку

@@ -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).
*/
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):
* 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
.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
.asSequence()
.filter { !it.deleted && it.inventoryLot?.item != null }
.filter { uomId == null || it.stockUom?.uom?.id == uomId }
.toList()
val item = source.firstOrNull()?.inventoryLot?.item
?: inventoryLotLineRepository
.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
.firstOrNull { !it.deleted }?.inventoryLot?.item
if (item == null) {
return WorkbenchItemLotsResponse(
itemId = itemId,
@@ -484,6 +491,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit
val lot = lotLine.inventoryLot ?: return@mapNotNull null
val lotNo = lot.stockInLine?.lotNo ?: 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 outQty = lotLine.outQty ?: BigDecimal.ZERO
val remainingQty = inQty.minus(outQty)
@@ -494,6 +502,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit
stockInLineId = lot.stockInLine?.id,
availableQty = remainingQty,
uom = uomDesc,
uomId = lotUomId,
warehouseCode = lotLine.warehouse?.code,
warehouseName = lotLine.warehouse?.name,
)
@@ -521,14 +530,17 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit
val item = scannedInventoryLotLine.inventoryLot?.item
?: throw IllegalStateException("Item not found for lot line id=${scannedInventoryLotLine.id}")

val filterUomId = request.uomId
val sameItemLots = inventoryLotLineRepository
.findAllByInventoryLotItemIdAndStatus(request.itemId, InventoryLotLineStatus.AVAILABLE)
.asSequence()
.filter { it.id != scannedInventoryLotLine.id }
.filter { filterUomId == null || it.stockUom?.uom?.id == filterUomId }
.mapNotNull { lotLine ->
val lot = lotLine.inventoryLot ?: return@mapNotNull null
val lotNo = lot.stockInLine?.lotNo ?: return@mapNotNull null
val uomDesc = lotLine.stockUom?.uom?.udfudesc ?: return@mapNotNull null
val lotUomId = lotLine.stockUom?.uom?.id
val whCode = lotLine.warehouse?.code
val whName = lotLine.warehouse?.name

@@ -545,6 +557,7 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit
stockInLineId = lot.stockInLine?.id,
availableQty = remainingQty,
uom = uomDesc,
uomId = lotUomId,
warehouseCode = whCode,
warehouseName = whName,
)
@@ -569,6 +582,8 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit
?: throw IllegalStateException("inventoryLotLineId missing on scanned lot line"),
warehouseCode = scannedInventoryLotLine.warehouse?.code,
warehouseName = scannedInventoryLotLine.warehouse?.name,
uom = scannedInventoryLotLine.stockUom?.uom?.udfudesc,
uomId = scannedInventoryLotLine.stockUom?.uom?.id,
),
sameItemLots = sameItemLots,
)


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt Прегледај датотеку

@@ -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;
* 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(
request: SearchInventoryRequest,


+ 5
- 4
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Прегледај датотеку

@@ -1034,9 +1034,6 @@ open class StockInLineService(
if (request.status == StockInLineStatus.RECEIVED.status) {
val _tPutAwayTotal = System.nanoTime()
// Putaway
var savedInventoryLotLine: InventoryLotLine? = null

// savedInventoryLotLine = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine)
val _tSaveLotLines = System.nanoTime()
val savedInventoryLotLines = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine)
_logStep("saveInventoryLotLineWhenStockIn", _tSaveLotLines)
@@ -1045,6 +1042,11 @@ open class StockInLineService(
val inventoryLotLines = stockInLine.inventoryLot?.let { it.id?.let { _id -> inventoryLotLineRepository.findAllByInventoryLotId(_id) } } ?: listOf()
_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 = 本次上架的庫存數量)
val _tDeltaCompute = System.nanoTime()
val putAwayDeltaStockQty = (request.inventoryLotLines ?: listOf()).sumOf { line ->
@@ -1102,7 +1104,6 @@ open class StockInLineService(
else
StockInLineStatus.PARTIALLY_COMPLETE.status
}
// this.inventoryLotLine = savedInventoryLotLine
}
// Update JO Status
if (stockInLine.jobOrder != null) { //TODO Improve


+ 12
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt Прегледај датотеку

@@ -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])
open fun scanPick(request: WorkbenchScanPickRequest): MessageResponse {
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

val required = pol.qty ?: BigDecimal.ZERO


+ 23
- 6
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Прегледај датотеку

@@ -96,6 +96,7 @@ open class SuggestedPickLotService(
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 {
val pols = request.pickOrderLines
val itemIds = pols.mapNotNull { it.item?.id }
@@ -123,7 +124,9 @@ open class SuggestedPickLotService(
pols.forEach { line ->
val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
val polUomId = line.uom?.id
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 pickOrder = line.pickOrder
val isDoPickOrder = pickOrder?.type?.value == "do" || pickOrder?.type?.value == "delivery_order"
@@ -275,6 +278,7 @@ open class SuggestedPickLotService(
}
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 {
val pols = request.pickOrderLines
val itemIds = pols.mapNotNull { it.item?.id }
@@ -302,8 +306,10 @@ open class SuggestedPickLotService(
pols.forEach { line ->
val itemId = line.item?.id
val isWipItem = itemId != null && wipItemIdSet.contains(itemId)
val polUomId = line.uom?.id

val lotInfos = availableInventoryLotLines[itemId].orEmpty()
.filter { polUomId == null || it.uomId == polUomId }
val lotIds = lotInfos.mapNotNull { it.id }
// 仅一次查询,避免后续重复 findById
val lotEntities = if (lotIds.isNotEmpty()) inventoryLotLineRepository.findAllById(lotIds) else emptyList()
@@ -580,6 +586,7 @@ open class SuggestedPickLotService(
return null
}
}
/** FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22 */
@Transactional(rollbackFor = [Exception::class])
open fun resuggestPickOrder(pickOrderId: Long): MessageResponse {
try {
@@ -1290,11 +1297,15 @@ private fun generateOptimalSuggestionsForAllPickOrders(
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 ->
val lineId = pickOrderLine.id
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
val assignQty = minOf(lotRemainingQty, lineRemainingQty)
@@ -1394,10 +1405,12 @@ private fun updateInventoryTableAfterResuggest(pickOrder: PickOrder) {
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
.allInventoryLotLinesByItemIdIn(listOf(itemId))
.filter { it.status == InventoryLotLineStatus.AVAILABLE.value }
.filter { polUomId == null || it.uomId == polUomId }
.sortedBy { it.expiryDate }
var remainingQtyInBaseUnits = excessQtyInBaseUnits
@@ -1550,9 +1563,11 @@ private fun generateCorrectSuggestionsWithExistingHolds(pickOrder: PickOrder): L
println("Available lots: ${availableLots.size}, Unavailable lots: ${unavailableLots.size}")
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 ->
if (remainingQtyInSalesUnits <= zero) return@forEach
if (polUomId != null && lotInfo.uomId != polUomId) return@forEach

val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) }
?: return@forEach
@@ -1620,12 +1635,14 @@ private fun generateCorrectSuggestionsWithOriginalHolds(
val salesUnit = itemUomService.findSalesUnitByItemId(itemId)
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
.allInventoryLotLinesByItemIdIn(listOf(itemId))
.filter { it.status == InventoryLotLineStatus.AVAILABLE.value }
.filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) }
.filter { polUomId == null || it.uomId == polUomId }
.sortedBy { it.expiryDate }
// Calculate total quantity that needs to be redistributed


+ 17
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt Прегледај датотеку

@@ -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:
* 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).
@@ -207,7 +217,7 @@ open class SuggestedPickLotWorkbenchService(
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)
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].
* 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.inventoryLot?.expiryDate?.isBefore(today) != true }
.filter { !isWarehouseExcluded(it, excludeSet) }
.filter { matchesPolUom(it, lineFresh) }
.filter { lot ->
if (storeKey == null) true
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):
* 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).
@@ -411,6 +424,7 @@ open class SuggestedPickLotWorkbenchService(
.filter { it.status == InventoryLotLineStatus.AVAILABLE }
.filter { it.inventoryLot?.expiryDate?.isBefore(today) != true }
.filter { !isWarehouseExcluded(it, excludeSet) }
.filter { matchesPolUom(it, line) }
.filter { lot ->
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:
* Set suggestions for this POL to an exact [targetQty] (independent of POL.qty).
* - 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.inventoryLot?.expiryDate?.isBefore(today) != true }
.filter { !isWarehouseExcluded(it, excludeSet) }
.filter { matchesPolUom(it, line) }
.sortedBy { it.inventoryLot?.expiryDate }

val desired = buildDesiredAllocations(lots, targetQty, pickOrderLineId)


+ 4
- 1
src/main/java/com/ffii/fpsms/modules/stock/web/InventoryController.kt Прегледај датотеку

@@ -21,7 +21,10 @@ class InventoryController(
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")
fun searchInventoriesByLatestInventory(
@ModelAttribute request: SearchInventoryRequest,


+ 37
- 11
src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt Прегледај датотеку

@@ -72,18 +72,35 @@ class InventoryLotLineController (
val stockInLine = stockInLineRepository.findById(stockInLineId).orElseThrow {
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 item = inventoryLotLine.inventoryLot?.item
?: throw ResponseStatusException(
HttpStatus.NOT_FOUND,
"Item not found for stockInLineId $stockInLineId",
)
val uom = inventoryLotLine.stockUom?.uom
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)
.minus(inventoryLotLine.outQty ?: 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)
}

/** 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")
fun analyzeQrCodeWorkbench(@RequestBody request: QrCodeAnalysisRequest): QrCodeAnalysisResponse {
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}")
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)
}
}

+ 11
- 3
src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt Прегледај датотеку

@@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.stock.web.model

import java.math.BigDecimal


data class LotLineInfo(
val inventoryLotLineId: Long,
val itemId: Long,
@@ -9,7 +10,9 @@ data class LotLineInfo(
val itemName: String,
val lotNo: String,
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(
val inventoryLotLineId: Long,
@@ -18,6 +21,8 @@ data class UpdateInventoryLotLineStatusRequest(
data class QrCodeAnalysisRequest(
val itemId: Long,
val stockInLineId: Long,
/** When set, sameItemLots only include lots with this UomConversion id. */
val uomId: Long? = null,
)

data class ScannedLotInfo(
@@ -25,7 +30,9 @@ data class ScannedLotInfo(
val lotNo: String,
val inventoryLotLineId: Long,
val warehouseCode: String? = null,
val warehouseName: String? = null
val warehouseName: String? = null,
val uom: String? = null,
val uomId: Long? = null,
)

data class SameItemLotInfo(
@@ -34,6 +41,7 @@ data class SameItemLotInfo(
val stockInLineId: Long? = null,
val availableQty: BigDecimal,
val uom: String,
val uomId: Long? = null,
val warehouseCode: String? = null,
val warehouseName: String? = null
)
@@ -51,4 +59,4 @@ data class WorkbenchItemLotsResponse(
val itemCode: String,
val itemName: String,
val sameItemLots: List<SameItemLotInfo>
)
)

Loading…
Откажи
Сачувај