Przeglądaj źródła

14,13,9,15,19,16,17,20-22

fix負數倉
CANCERYS\kw093 6 dni temu
rodzic
commit
fbe1fae555
12 zmienionych plików z 239 dodań i 448 usunięć
  1. +29
    -33
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  2. +14
    -10
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PSService.kt
  3. +13
    -2
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt
  4. +61
    -13
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  5. +0
    -66
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  6. +59
    -87
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  7. +6
    -46
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt
  8. +3
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt
  9. +3
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  10. +1
    -80
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  11. +49
    -24
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  12. +1
    -87
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt

+ 29
- 33
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt Wyświetl plik

@@ -166,7 +166,12 @@ open class JobOrderService(
val inventoriesMap = if (allItemIds.isNotEmpty()) {
inventoryRepository.findInventoryInfoByItemIdInAndDeletedIsFalse(allItemIds)
.associateBy { it.itemId }
.mapNotNull { info ->
val itemId = info.itemId ?: return@mapNotNull null
val stockUomId = info.stockUomId ?: return@mapNotNull null
(itemId to stockUomId) to info
}
.toMap()
} else {
emptyMap()
}
@@ -228,7 +233,7 @@ open class JobOrderService(
private fun calculateStockCounts(
jobOrder: JobOrder,
inventoriesMap: Map<Long?, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo>
inventoriesMap: Map<Pair<Long, Long>, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo>
): Pair<Int, Int> {
// 过滤掉 consumables 和 CMB 类型的物料
val nonConsumablesJobms = jobOrder.jobms.filter { jobm ->
@@ -243,36 +248,22 @@ open class JobOrderService(
var sufficientCount = 0
var insufficientCount = 0
//println("=== JobOrderService.calculateStockCounts for JobOrder: ${jobOrder.code} ===")
nonConsumablesJobms.forEach { jobm ->
val itemId = jobm.item?.id
val itemCode = jobm.item?.code ?: "N/A"
val itemName = jobm.item?.name ?: "N/A"
if (itemId != null) {
val inventory = inventoriesMap[itemId]
val availableQty = if (inventory != null) {
inventory.availableQty ?: (
(inventory.onHandQty ?: BigDecimal.ZERO) -
(inventory.onHoldQty ?: BigDecimal.ZERO) -
(inventory.unavailableQty ?: BigDecimal.ZERO)
)
} else {
BigDecimal.ZERO
}
val jobmUomId = jobm.uom?.id
val inventory = resolveInventoryForJobm(itemId, jobmUomId, inventoriesMap)
val availableQty = inventory?.availableQty
?: (inventory?.let {
(it.onHandQty ?: BigDecimal.ZERO) - (it.unavailableQty ?: BigDecimal.ZERO)
} ?: BigDecimal.ZERO)
// ✅ 获取 reqQty 和 availableQty 的单位信息
val reqQty = jobm.reqQty ?: BigDecimal.ZERO
val reqUomId = jobm.uom?.id ?: 0L
val reqUomName = jobm.uom?.udfudesc ?: "N/A"
// ✅ 修复:使用 itemUomService 获取 stockUomId(与 ProductProcessService 保持一致)
val stockUnitItemUom = itemUomService.findStockUnitByItemId(itemId)
val stockUomId = stockUnitItemUom?.uom?.id ?: 0L
val stockUomName = stockUnitItemUom?.uom?.udfudesc ?: "N/A"
val availableUomId = inventory?.stockUomId ?: stockUnitItemUom?.uom?.id ?: 0L
// ✅ 转换为 base unit 进行比较(与 ProductProcessService 保持一致)
val baseReqQtyResult = if (reqUomId > 0 && reqQty > BigDecimal.ZERO) {
try {
itemUomService.convertUomByItem(
@@ -291,13 +282,13 @@ open class JobOrderService(
null
}
val baseAvailableQtyResult = if (stockUomId > 0 && availableQty > BigDecimal.ZERO) {
val baseAvailableQtyResult = if (availableUomId > 0 && availableQty > BigDecimal.ZERO) {
try {
itemUomService.convertUomByItem(
ConvertUomByItemRequest(
itemId = itemId,
qty = availableQty,
uomId = stockUomId,
uomId = availableUomId,
targetUnit = "baseUnit"
)
)
@@ -311,27 +302,32 @@ open class JobOrderService(
val baseReqQty = baseReqQtyResult?.newQty ?: BigDecimal.ZERO
val baseAvailableQty = baseAvailableQtyResult?.newQty ?: BigDecimal.ZERO
val baseUomName = baseReqQtyResult?.udfudesc ?: baseAvailableQtyResult?.udfudesc ?: "N/A"
// ✅ 使用 base unit 进行比较
if (baseAvailableQty >= baseReqQty) {
sufficientCount++
//println("✅ SUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)")
} else {
insufficientCount++
//println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)")
}
} else {
// 如果没有 itemId,视为不足
insufficientCount++
//println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId")
}
}
//println("=== Result: sufficient=$sufficientCount, insufficient=$insufficientCount ===")
return Pair(sufficientCount, insufficientCount)
}

private fun resolveInventoryForJobm(
itemId: Long,
jobmUomId: Long?,
inventoriesMap: Map<Pair<Long, Long>, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo>,
): com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo? {
if (jobmUomId != null) {
inventoriesMap[itemId to jobmUomId]?.let { return it }
}
val stockUnitUomId = itemUomService.findStockUnitByItemId(itemId)?.uom?.id ?: return null
return inventoriesMap[itemId to stockUnitUomId]
}

open fun jobOrderDetail(id: Long): JobOrderDetail {
val sqlResult = jobOrderRepository.findJobOrderDetailById(id) ?: throw NoSuchElementException();



+ 14
- 10
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PSService.kt Wyświetl plik

@@ -168,8 +168,8 @@ open class PSService(

/** Set or clear coffee_or_tea for itemCode + systemType (coffee / tea / lemon). */
/**
* Recalculate [inventory.onHandQty] / hold / unavailable from [inventory_lot_line] for FG BOM items.
* Same aggregation as pick-issue manual inventory sync.
* Recalculate [inventory.onHandQty] / unavailable from [inventory_lot_line] for FG BOM items.
* Per (itemId, stockUomId) bucket. Does not write onHoldQty.
*/
fun refreshInventoryOnHandForFgBomItems(): Int {
val sql = """
@@ -182,24 +182,28 @@ open class PSService(
LEFT JOIN (
SELECT
il.itemId,
iu.uomId AS stockUomId,
SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS totalOnHandQty,
SUM(COALESCE(ill.holdQty, 0)) AS totalOnHoldQty,
SUM(CASE
WHEN ill.status = 'unavailable'
THEN COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)
WHEN LOWER(ill.status) = 'unavailable'
THEN COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)
ELSE 0
END) AS totalUnavailableQty
FROM inventory_lot_line ill
INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId AND il.deleted = 0
WHERE ill.deleted = 0
GROUP BY il.itemId
INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId AND IFNULL(il.deleted, 0) = 0
INNER JOIN item_uom iu ON iu.id = ill.stockItemUomId AND IFNULL(iu.deleted, 0) = 0
WHERE IFNULL(ill.deleted, 0) = 0
GROUP BY il.itemId, iu.uomId
) calc ON calc.itemId = i.itemId
AND (
i.stockUomId = calc.stockUomId
OR (i.stockUomId IS NULL AND i.uomId = calc.stockUomId)
)
SET
i.onHandQty = COALESCE(calc.totalOnHandQty, 0),
i.onHoldQty = COALESCE(calc.totalOnHoldQty, 0),
i.unavailableQty = COALESCE(calc.totalUnavailableQty, 0),
i.status = IF(
COALESCE(calc.totalOnHandQty, 0) - COALESCE(calc.totalOnHoldQty, 0) - COALESCE(calc.totalUnavailableQty, 0) > 0,
COALESCE(calc.totalOnHandQty, 0) - COALESCE(calc.totalUnavailableQty, 0) > 0,
'available',
'unavailable'
),


+ 13
- 2
src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt Wyświetl plik

@@ -27,14 +27,25 @@ interface ProductionScheduleLineRepository : AbstractRepository<ProductionSchedu
bmi.code,
bmi.name,
bmi.`type`,
coalesce(i.onHandQty, 0) - coalesce(i.onHoldQty, 0) - coalesce(i.unavailableQty, 0) as availableQty,
coalesce(i.onHandQty, 0) - coalesce(i.unavailableQty, 0) as availableQty,
ceil(coalesce(bm.qty, 0) * pp.proportion) as demandQty
from production_schedule_line psl
left join prod_prop pp on pp.pslId = psl.id
left join bom b on b.itemId = psl.itemId
left join bom_material bm on bm.bomId = b.id
left join items bmi on bmi.id = bm.itemId
left join inventory i on i.itemId = bmi.id
left join inventory i on i.id = (
select i2.id
from inventory i2
where i2.itemId = bmi.id
and ifnull(i2.deleted, 0) = 0
and (
i2.stockUomId = coalesce(bm.stockUnit, bm.uomId)
or (i2.stockUomId is null and i2.uomId = coalesce(bm.stockUnit, bm.uomId))
)
order by case when i2.stockUomId is null then 1 else 0 end, i2.id
limit 1
)
where psl.id = :id and bmi.id is not null
group by psl.id, bm.id, i.id, pp.proportion
""")


+ 61
- 13
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt Wyświetl plik

@@ -171,29 +171,29 @@ open class ProductionScheduleService(
val proportion = BigDecimal.ONE // BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP)
val demandQty = bm.qty?.times(proportion) ?: zero
val saleUnit = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
val materialStockUomId = bm.stockUnit?.toLong()
?: bm.uom?.id
?: bm.item?.id?.let { itemUomService.findStockUnitByItemId(it)?.uom?.id }

RoughProdScheduleLineBomMaterialInfoByFg(
id = bm.id,
code = bm.item?.code,
name = bm.item?.name,
type = bm.item?.type,
availableQty = bm.item?.inventories?.sumOf {
(it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero)
},
availableQty = availableQtyWithoutHold(bm.item?.inventories, materialStockUomId, zero),
demandQty = demandQty,
uomName = saleUnit?.uom?.udfudesc
)
}

val saleUnit = line.item.id?.let { itemUomService.findSalesUnitByItemId(it) }
val fgStockUomId = line.item.id?.let { itemUomService.findStockUnitByItemId(it)?.uom?.id }
RoughProdScheduleLineInfoByFg(
id = line.id,
code = line.item.code,
name = line.item.name,
type = line.item.type,
availableQty = line.item.inventories.sumOf {
(it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero)
},
availableQty = availableQtyWithoutHold(line.item.inventories, fgStockUomId, zero),
prodQty = BigDecimal(line.prodQty),
lastMonthAvgSales = BigDecimal(line.lastMonthAvgSales),
estCloseBal = BigDecimal(line.estCloseBal),
@@ -258,15 +258,16 @@ open class ProductionScheduleService(
val proportion = BigDecimal.ONE //BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP)
val demandQty = bm.qty?.times(proportion) ?: zero
val saleUnit = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
val materialStockUomId = bm.stockUnit?.toLong()
?: bm.uom?.id
?: bm.item?.id?.let { itemUomService.findStockUnitByItemId(it)?.uom?.id }

RoughProdScheduleLineInfoByBomByDate(
id = bm.item?.id,
code = bm.item?.code,
name = bm.item?.name,
type = bm.item?.type,
availableQty = bm.item?.inventories?.sumOf {
(it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero)
} ?: zero,
availableQty = availableQtyWithoutHold(bm.item?.inventories, materialStockUomId, zero),
demandQty = demandQty,
assignDate = line.assignDate,
uomName = saleUnit?.uom?.udfudesc
@@ -724,7 +725,21 @@ open class ProductionScheduleService(
FROM
bom
LEFT JOIN items ON bom.itemId = items.id
LEFT JOIN inventory ON items.id = inventory.itemId
LEFT JOIN item_uom iu_stock ON iu_stock.itemId = items.id
AND iu_stock.stockUnit = 1
AND IFNULL(iu_stock.deleted, 0) = 0
LEFT JOIN inventory ON inventory.id = (
SELECT i2.id
FROM inventory i2
WHERE i2.itemId = items.id
AND IFNULL(i2.deleted, 0) = 0
AND (
i2.stockUomId = iu_stock.uomId
OR (i2.stockUomId IS NULL AND i2.uomId = iu_stock.uomId)
)
ORDER BY CASE WHEN i2.stockUomId IS NULL THEN 1 ELSE 0 END, i2.id
LIMIT 1
)
left join item_fake_onhand on items.code = item_fake_onhand.itemCode
WHERE bom.deleted = 0 and bom.description = 'FG' and bom.status = 'active'
-- and bom.itemId != 16771
@@ -1821,7 +1836,18 @@ open class ProductionScheduleService(
left join bom on it.id = bom.itemId
left join bom_material bm on bom.id = bm.bomId
left join items itm on bm.itemId = itm.id
left join inventory iv on itm.id = iv.itemId
left join inventory iv on iv.id = (
select i2.id
from inventory i2
where i2.itemId = itm.id
and ifnull(i2.deleted, 0) = 0
and (
i2.stockUomId = ius.uomId
or (i2.stockUomId is null and i2.uomId = ius.uomId)
)
order by case when i2.stockUomId is null then 1 else 0 end, i2.id
limit 1
)
left join item_uom ius on itm.id = ius.itemId and ius.stockUnit = 1
left join uom_conversion ucs on ius.uomId = ucs.id
@@ -1893,7 +1919,18 @@ open class ProductionScheduleService(
LEFT JOIN item_uom ium18 ON ium18.itemId = itm.id AND ium18.uomId = lsu.uomIdM18 AND ium18.deleted = 0
LEFT JOIN uom_conversion uomM18 ON uomM18.id = ium18.uomId
JOIN item_uom itsm ON itm.id = itsm.itemId and itsm.stockUnit = 1
LEFT JOIN inventory iv ON itm.id = iv.itemId
LEFT JOIN inventory iv ON iv.id = (
SELECT i2.id
FROM inventory i2
WHERE i2.itemId = itm.id
AND IFNULL(i2.deleted, 0) = 0
AND (
i2.stockUomId = itsm.uomId
OR (i2.stockUomId IS NULL AND i2.uomId = itsm.uomId)
)
ORDER BY CASE WHEN i2.stockUomId IS NULL THEN 1 ELSE 0 END, i2.id
LIMIT 1
)
WHERE DATE(ps.produceAt) >= DATE_ADD(:fromDate, INTERVAL 1 DAY)
AND DATE(ps.produceAt) < DATE_ADD(:fromDate, INTERVAL 8 DAY)
AND ps.id = (
@@ -1968,5 +2005,16 @@ open class ProductionScheduleService(
// Optional: log the action (if you have logging setup)
// logger.info("Cleared all production schedules with produceAt >= today")
}

private fun availableQtyWithoutHold(
inventories: List<Inventory>?,
stockUomId: Long?,
zero: BigDecimal,
): BigDecimal {
if (inventories.isNullOrEmpty() || stockUomId == null) return zero
val bucket = inventories.firstOrNull { inv ->
inv.stockUom?.id == stockUomId || (inv.stockUom == null && inv.uom?.id == stockUomId)
} ?: return zero
return (bucket.onHandQty ?: zero) - (bucket.unavailableQty ?: zero)
}
}

+ 0
- 66
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Wyświetl plik

@@ -2313,71 +2313,6 @@ private fun updateLotLineAfterIssue(lotLineId: Long, qty: BigDecimal, isMissItem
lotLine.modifiedBy = "system"
// 修复:使用 saveAndFlush 确保立即提交到数据库,触发触发器
inventoryLotLineRepository.saveAndFlush(lotLine)
updateInventoryAfterLotLineChange(lotLine)
}
}
private fun updateInventoryAfterLotLineChange(lotLine: InventoryLotLine) {
try {
val item = lotLine.inventoryLot?.item ?: return
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return
// 使用 SQL 查询计算所有相关 lot lines 的总和
val sql = """
SELECT
SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) as totalOnHandQty,
SUM(COALESCE(ill.holdQty, 0)) as totalOnHoldQty,
SUM(CASE
WHEN ill.status = 'unavailable'
THEN COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)
ELSE 0
END) as totalUnavailableQty
FROM inventory_lot_line ill
INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId
WHERE il.itemId = :itemId
AND ill.deleted = 0
""".trimIndent()
val result = jdbcDao.queryForMap(sql, mapOf("itemId" to item.id!!)).orElse(null)
if (result != null) {
val totalOnHandQty = (result["totalOnHandQty"] as? Number)?.let {
BigDecimal(it.toString())
} ?: BigDecimal.ZERO
val totalOnHoldQty = (result["totalOnHoldQty"] as? Number)?.let {
BigDecimal(it.toString())
} ?: BigDecimal.ZERO
val totalUnavailableQty = (result["totalUnavailableQty"] as? Number)?.let {
BigDecimal(it.toString())
} ?: BigDecimal.ZERO
// 更新 inventory
inventory.onHandQty = totalOnHandQty
inventory.onHoldQty = totalOnHoldQty
inventory.unavailableQty = totalUnavailableQty
inventory.status = if (totalOnHandQty.subtract(totalOnHoldQty).subtract(totalUnavailableQty) > BigDecimal.ZERO) {
"available"
} else {
"unavailable"
}
inventory.modified = LocalDateTime.now()
inventory.modifiedBy = "system"
inventoryRepository.saveAndFlush(inventory)
println("=== MANUALLY UPDATED INVENTORY ===")
println("Item ID: ${item.id}")
println("Total OnHandQty: ${totalOnHandQty}")
println("Total OnHoldQty: ${totalOnHoldQty}")
println("Total UnavailableQty: ${totalUnavailableQty}")
println("Status: ${inventory.status}")
println("==================================")
}
} catch (e: Exception) {
println("Error updating inventory manually: ${e.message}")
e.printStackTrace()
}
}
/** FP-MTMS Version Checklist | Functions Ref. No. 46 | v1.0.1 | 2026-08-05 */
@@ -2768,7 +2703,6 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
lotLine.modified = LocalDateTime.now()
lotLine.modifiedBy = "system"
inventoryLotLineRepository.saveAndFlush(lotLine)
updateInventoryAfterLotLineChange(lotLine)
println("✅ Reset lot ${request.lotId}: issueQty=0, status=AVAILABLE")
}
val rejectedLines = stockOutLineRepository


+ 59
- 87
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt Wyświetl plik

@@ -31,6 +31,7 @@ import com.ffii.fpsms.modules.stock.entity.StockOutRepository
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.stock.entity.projection.CurrentInventoryItemInfo
import com.ffii.fpsms.modules.stock.service.InventoryLotLineService
import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo
import com.ffii.fpsms.modules.stock.service.InventoryService
import com.ffii.fpsms.modules.stock.service.StockOutLineService
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService
@@ -215,28 +216,17 @@ open class PickOrderService(
val today = LocalDate.now()
val zero = BigDecimal.ZERO

val inventories = if (itemIds.isNotEmpty()) {
inventoryLotLineService
.allInventoryLotLinesByItemIdIn(itemIds)
.filter { it.status == InventoryLotLineStatus.AVAILABLE.value }
.filter { (it.inQty ?: zero).minus(it.outQty ?: zero).minus(it.holdQty ?: zero) > zero }
.filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) }
.groupBy { it.item?.id }
} else {
emptyMap()
}
val inventories = pickableLotLinesByItemAndStockUom(itemIds, today, zero)

// Build response (removed suggestions)
val pickOrderInfos = fullPickOrders.map { po ->
val pickOrderLineInfos = po.pickOrderLines.map { pol ->
val inventory = pol.item?.id?.let { inventories[it] }

GetPickOrderLineInfo(
id = pol.id,
itemId = pol.item?.id,
itemCode = pol.item?.code,
itemName = pol.item?.name,
availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) },
availableQty = pickLineAvailableQty(pol.item?.id, pol.uom?.id, inventories, zero),
requiredQty = pol.qty,
uomCode = pol.uom?.code,
uomDesc = pol.uom?.udfudesc,
@@ -660,7 +650,7 @@ open class PickOrderService(
// Get Inventory Data
val requiredItems = pos
.flatMap { it.pickOrderLines }
.groupBy { it.item?.id }
.groupBy { it.item?.id to it.uom?.id }
.map { (key, value) ->
key to object : CurrentInventoryItemInfo {
override val id: Long? = value[0].item?.id
@@ -671,42 +661,25 @@ open class PickOrderService(
override val requiredQty: BigDecimal = value.sumOf { it.qty ?: zero }

}
} // itemId - requiredQty
} // itemId+uom - requiredQty

val itemIds = requiredItems.mapNotNull { it.first }
// val inventories = inventoryLotLineRepository.findCurrentInventoryByItems(itemIds)
// val inventories = inventoryService.allInventoriesByItemIds(itemIds)
val inventories = inventoryLotLineService
.allInventoryLotLinesByItemIdIn(itemIds)
.filter { it.status == InventoryLotLineStatus.AVAILABLE.value }
.filter { (it.inQty ?: zero).minus(it.outQty ?: zero).minus(it.holdQty ?: zero) > zero }
.filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) }
.sortedBy { it.expiryDate }
.groupBy { it.item?.id }
val itemIds = requiredItems.mapNotNull { it.first.first }
val inventories = pickableLotLinesByItemAndStockUom(itemIds, today, zero)

// Pick Orders
val releasePickOrderInfos = pos
.map { po ->
val releasePickOrderLineInfos = po.pickOrderLines.map { pol ->
// if (pol.item?.id != null && pol.item!!.id!! > 0) {
val inventory = pol.item?.id?.let { inventories[it] }
val itemUom = pol.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
// val inventory = inventories.find { it.itemId == pol.item?.id }

// Return
ReleasePickOrderLineInfo(
id = pol.id,
itemId = pol.item?.id,
itemCode = pol.item?.code,
itemName = pol.item?.name,
// availableQty = inventory?.availableQty,
availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) },
// availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) * (itemUom?.ratioN ?: one) * (itemUom?.ratioD ?: one) },
availableQty = pickLineAvailableQty(pol.item?.id, pol.uom?.id, inventories, zero),
requiredQty = pol.qty,
uomCode = pol.uom?.code,
uomDesc = pol.uom?.udfudesc,
)
// }
}

// Return
@@ -725,16 +698,12 @@ open class PickOrderService(
// val inventory = inventories
// .find { it.itemId == item.first }

val inventory = item.first?.let { inventories[it] }
val itemUom = item.first?.let { itemUomService.findSalesUnitByItemId(it) }
val inventory = item.first.let { (itemId, uomId) ->
if (itemId != null && uomId != null) inventories[itemId to uomId] else null
}

item.second.let {
// it.availableQty = inventory?.availableQty

it.availableQty = inventory?.sumOf { i -> i.availableQty ?: zero }
// it.availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) * (itemUom?.ratioN ?: one) * (itemUom?.ratioD ?: one) }

// return
it.availableQty = inventory?.sumOf { i -> (i.inQty ?: zero) - (i.outQty ?: zero) }
it
}
}
@@ -759,7 +728,7 @@ open class PickOrderService(
// Get Inventory Data
val requiredItems = pos
.flatMap { it.pickOrderLines }
.groupBy { it.item?.id }
.groupBy { it.item?.id to it.uom?.id }
.map { (key, value) ->
key to object : CurrentInventoryItemInfo {
override val id: Long? = value[0].item?.id
@@ -769,16 +738,10 @@ open class PickOrderService(
override var availableQty: BigDecimal? = zero
override val requiredQty: BigDecimal = value.sumOf { it.qty ?: zero }
}
} // itemId - requiredQty
}

val itemIds = requiredItems.mapNotNull { it.first }
val inventories = inventoryLotLineService
.allInventoryLotLinesByItemIdIn(itemIds)
.filter { it.status == InventoryLotLineStatus.AVAILABLE.value }
.filter { (it.inQty ?: zero).minus(it.outQty ?: zero).minus(it.holdQty ?: zero) > zero }
.filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) }
.sortedBy { it.expiryDate }
.groupBy { it.item?.id }
val itemIds = requiredItems.mapNotNull { it.first.first }
val inventories = pickableLotLinesByItemAndStockUom(itemIds, today, zero)

val suggestions =
suggestedPickLotService.suggestionForPickOrders(SuggestedPickLotForPoRequest(pickOrders = pos))
@@ -793,24 +756,10 @@ open class PickOrderService(
emptyMap()
}

// Pre-calculate available quantities for each item ONCE
val itemAvailableQtyMap = mutableMapOf<Long, BigDecimal>()
itemIds.forEach { itemId ->
val inventory = inventories[itemId]
val totalAvailableQty = inventory?.sumOf { i ->
val inQty = i.inQty ?: zero
val outQty = i.outQty ?: zero
val holdQty = i.holdQty ?: zero
inQty.minus(outQty).minus(holdQty)
} ?: zero
itemAvailableQtyMap[itemId] = totalAvailableQty
}

val releasePickOrderLineInfos = pos
.map { po ->
val releasePickOrderLineInfos = po.pickOrderLines.map { pol ->
val itemId = pol.item?.id
val availableQty = itemId?.let { itemAvailableQtyMap[it] } ?: zero
val availableQty = pickLineAvailableQty(pol.item?.id, pol.uom?.id, inventories, zero)
// 取得這一行的所有 stock_out_line
val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList()
@@ -871,16 +820,11 @@ open class PickOrderService(
}
// Items
val currentInventoryInfos = requiredItems.map { item ->
val inventory = item.first?.let { inventories[it] }
val itemUom = item.first?.let { itemUomService.findSalesUnitByItemId(it) }
val inventory = item.first.let { (itemId, uomId) ->
if (itemId != null && uomId != null) inventories[itemId to uomId] else null
}
item.second.let {
val convertedAvailableQty = inventory?.sumOf { i ->
val baseQty = (i.availableQty ?: zero)
baseQty
}

it.availableQty = convertedAvailableQty
it.availableQty = inventory?.sumOf { i -> (i.inQty ?: zero) - (i.outQty ?: zero) }
it
}
}
@@ -2344,19 +2288,16 @@ open fun checkAndCompletePickOrderByConsoCode(consoCode: String): MessageRespons
-- Calculate available quantity from inventory
COALESCE((
SELECT SUM(
COALESCE(inv.onHandQty, 0)
- COALESCE(inv.onHoldQty, 0)
- COALESCE(inv.unavailableQty, 0)
)
SELECT COALESCE(inv.onHandQty, 0) - COALESCE(inv.unavailableQty, 0)
FROM fpsmsdb.inventory inv
JOIN fpsmsdb.item_uom iu
ON iu.itemId = inv.itemId
AND iu.uomId = inv.uomId
AND iu.baseUnit = 1
AND iu.deleted = false
WHERE inv.itemId = i.id
AND inv.deleted = false
AND (
inv.stockUomId = pol.uomId
OR (inv.stockUomId IS NULL AND inv.uomId = pol.uomId)
)
ORDER BY CASE WHEN inv.stockUomId IS NULL THEN 1 ELSE 0 END, inv.id
LIMIT 1
), 0) as availableQty,
-- Check if all stock out lines for this pick order line are completed
@@ -5201,5 +5142,36 @@ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, A
)
}

private fun pickableLotLinesByItemAndStockUom(
itemIds: List<Long>,
today: LocalDate,
zero: BigDecimal,
): Map<Pair<Long, Long>, List<InventoryLotLineInfo>> {
if (itemIds.isEmpty()) return emptyMap()
return inventoryLotLineService
.allInventoryLotLinesByItemIdIn(itemIds)
.filter { it.status == InventoryLotLineStatus.AVAILABLE.value }
.filter { ((it.inQty ?: zero) - (it.outQty ?: zero)) > zero }
.filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) }
.mapNotNull { line ->
val itemId = line.item?.id ?: return@mapNotNull null
val stockUomId = line.uomId ?: return@mapNotNull null
(itemId to stockUomId) to line
}
.groupBy({ it.first }, { it.second })
}

private fun pickLineAvailableQty(
itemId: Long?,
polUomId: Long?,
lotsByBucket: Map<Pair<Long, Long>, List<InventoryLotLineInfo>>,
zero: BigDecimal,
): BigDecimal {
if (itemId == null || polUomId == null) return zero
return lotsByBucket[itemId to polUomId]
?.sumOf { (it.inQty ?: zero) - (it.outQty ?: zero) }
?: zero
}

}


+ 6
- 46
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt Wyświetl plik

@@ -35,7 +35,6 @@ import java.util.Optional
import kotlin.jvm.optionals.getOrNull
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineQuantitiesRequest
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisRequest
import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisResponse
import com.ffii.fpsms.modules.stock.web.model.ScannedLotInfo
@@ -58,7 +57,6 @@ open class InventoryLotLineService(
private val warehouseRepository: WarehouseRepository,
private val itemUomRespository: ItemUomRespository,
private val stockInLineRepository: StockInLineRepository,
private val inventoryRepository: InventoryRepository,
private val printerService: PrinterService,
@Lazy
private val jobOrderService: JobOrderService
@@ -150,11 +148,15 @@ open class InventoryLotLineService(
}
}
open fun saveInventoryLotLine(request: SaveInventoryLotLineRequest): InventoryLotLine {
val inventoryLotLine =
request.id?.let { inventoryLotLineRepository.findById(it).getOrNull() } ?: InventoryLotLine()
val existing = request.id?.let { inventoryLotLineRepository.findById(it).getOrNull() }
val inventoryLotLine = existing ?: InventoryLotLine()
val inventoryLot = request.inventoryLotId?.let { inventoryLotRepository.findById(it).getOrNull() }
val warehouse = request.warehouseId?.let { warehouseRepository.findById(it).getOrNull() }
val stockUom = request.stockUomId?.let { itemUomRespository.findById(it).getOrNull() }
?: existing?.stockUom
if (stockUom == null) {
throw IllegalArgumentException("stockItemUomId is required for inventory_lot_line")
}
val status = request.status?.let { _status -> InventoryLotLineStatus.entries.find { it.value == _status } }

inventoryLotLine.apply {
@@ -191,9 +193,6 @@ open class InventoryLotLineService(

val updatedLotLine = saveInventoryLotLine(updateRequest)

// ADD THIS: Update inventory table after lot line status change
updateInventoryTable(updatedLotLine)

return MessageResponse(
id = updatedLotLine.id,
name = updatedLotLine.id.toString(),
@@ -204,45 +203,6 @@ open class InventoryLotLineService(
)
}

// ADD THIS: New method to update inventory table
private fun updateInventoryTable(inventoryLotLine: InventoryLotLine) {
try {
// Get the item ID from the inventory lot
val itemId = inventoryLotLine.inventoryLot?.item?.id
if (itemId == null) {
println("Cannot update inventory table: itemId is null for lot line ${inventoryLotLine.id}")
return
}

// Calculate onHoldQty (sum of holdQty from available lots only)
val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
.sumOf { it.holdQty ?: BigDecimal.ZERO }

// Calculate unavailableQty (sum of inQty from unavailable lots only)
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE)
.sumOf {
val inQty = it.inQty ?: BigDecimal.ZERO
val outQty = it.outQty ?: BigDecimal.ZERO
val remainingQty = inQty.minus(outQty)
remainingQty
}

// Update the inventory table
val inventory = inventoryRepository.findByItemId(itemId).orElse(null)
if (inventory != null) {
inventory.onHoldQty = onHoldQty
inventory.unavailableQty = unavailableQty
inventoryRepository.save(inventory)

println("Updated inventory for item $itemId: onHoldQty=$onHoldQty, unavailableQty=$unavailableQty")
} else {
println("Inventory not found for item $itemId")
}
} catch (e: Exception) {
println("Error updating inventory table for lot line ${inventoryLotLine.id}: ${e.message}")
e.printStackTrace()
}
}
@Throws(IOException::class)
@Transactional
open fun exportStockInLineQrcode(request: LotLineToQrcode): Map<String, Any> {


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt Wyświetl plik

@@ -338,6 +338,9 @@ open class InventoryService(
val warehouse = warehouseRepository.findAll().find { it.code == request.warehouseCode }!!
// val salesUnit = itemUomService.findSalesUnitByItemId(itemId = inventoryLot.item!!.id!!)
val stockUnit = itemUomService.findStockUnitByItemId(itemId = inventoryLot.item!!.id!!)
?: throw IllegalArgumentException(
"stockItemUomId is required: no stockUnit item_uom for itemId=${inventoryLot.item!!.id}",
)
// ChangeList #8: lot-line stockItemUomId must be set so trigger writes inventory.stockUomId.
InventoryLotLine().apply {
this.inventoryLot = inventoryLot


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Wyświetl plik

@@ -538,6 +538,9 @@ open class StockInLineService(
this.inQty = convertedBaseQty
this.status = InventoryLotLineStatus.AVAILABLE
this.stockUom = stockItemUom
?: throw IllegalArgumentException(
"stockItemUomId is required: no stockUnit item_uom for itemId=${request.itemId}",
)
}
saveLines.add(inventoryLotLine)
}


+ 1
- 80
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Wyświetl plik

@@ -43,7 +43,6 @@ import com.ffii.fpsms.modules.pickOrder.service.PickOrderService
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.common.SecurityUtils
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository
import java.time.LocalTime
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
@@ -78,7 +77,6 @@ private val inventoryLotLineService: InventoryLotLineService,
private val bagService: BagService,
private val pickOrderService: PickOrderService,
private val stockLedgerRepository: StockLedgerRepository,
private val inventoryRepository: InventoryRepository,
private val pickExecutionIssueRepository: PickExecutionIssueRepository,
private val itemUomService: ItemUomService,
private val inventoryBucketResolver: InventoryBucketResolver,
@@ -266,13 +264,6 @@ fun handleQc(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): Li
this.holdQty = this.holdQty!!.minus((request.qty.div(ratio)).toBigDecimal())
}
inventoryLotLineRepository.save(targetLotLineEntry)
// update inventory
val inventory = inventoryRepository.findByItemId(request.itemId).orElseThrow()
val inventoryEntry = inventory.apply {
this.onHandQty = this.onHandQty!!.minus((request.qty.div(ratio)).toBigDecimal())
this.onHoldQty = this.onHoldQty!!.minus((request.qty.div(ratio)).toBigDecimal())
}
inventoryRepository.save(inventoryEntry)
return listOf(stockOutLine, newStockOutLine)
}
@Transactional
@@ -817,10 +808,6 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
val savedLotLine = inventoryLotLineRepository.save(inventoryLotLine)
println("Saved lot line: ${savedLotLine.id}, status: ${savedLotLine.status}, holdQty: ${savedLotLine.holdQty}")
// Step 3: Update inventory table
println("Updating inventory table...")
updateInventoryTableAfterLotRejection(inventoryLotLine)
// Step 4: Trigger resuggest
val pickOrderLine = stockOutLine.pickOrderLine
if (pickOrderLine?.pickOrder?.id != null) {
@@ -839,51 +826,6 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
}
}

// ADD THIS: Update inventory table after lot rejection
private fun updateInventoryTableAfterLotRejection(inventoryLotLine: InventoryLotLine) {
try {
println("=== UPDATING INVENTORY TABLE ===")
val itemId = inventoryLotLine.inventoryLot?.item?.id
println("Item ID: $itemId")
if (itemId != null) {
// Calculate onHoldQty (sum of holdQty from available lots only)
val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
.sumOf { it.holdQty ?: BigDecimal.ZERO }
// Calculate unavailableQty (sum of inQty from unavailable lots only)
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId,InventoryLotLineStatus.UNAVAILABLE)
.sumOf {
val inQty = it.inQty ?: BigDecimal.ZERO
val outQty = it.outQty ?: BigDecimal.ZERO
val remainingQty = inQty.minus(outQty)
remainingQty
}
println("Calculated onHoldQty: $onHoldQty")
println("Calculated unavailableQty: $unavailableQty")
// Update the inventory table
val inventory = inventoryRepository.findByItemId(itemId).orElse(null)
if (inventory != null) {
println("Found inventory record: ${inventory.id}")
println("Current inventory - onHoldQty: ${inventory.onHoldQty}, unavailableQty: ${inventory.unavailableQty}")
inventory.onHoldQty = onHoldQty
inventory.unavailableQty = unavailableQty
val savedInventory = inventoryRepository.save(inventory)
println("Updated inventory - onHoldQty: ${savedInventory.onHoldQty}, unavailableQty: ${savedInventory.unavailableQty}")
} else {
println("No inventory record found for item $itemId")
}
}
} catch (e: Exception) {
println("Error updating inventory table after lot rejection: ${e.message}")
e.printStackTrace()
}
}

@Transactional(rollbackFor = [Exception::class])
open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
val startTime = System.currentTimeMillis()
@@ -955,14 +897,6 @@ open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
this.holdQty = (this.holdQty ?: zero) - baseQty
}
inventoryLotLineRepository.save(lotLine)

// 更新 inventory
val inv = inventoryRepository.findByItemId(lotLine.inventoryLot?.item?.id!!).orElseThrow()
inv.apply {
this.onHandQty = (this.onHandQty ?: zero) - baseQty
this.onHoldQty = (this.onHoldQty ?: zero) - baseQty
}
inventoryRepository.save(inv)
}

processedIds += line.stockOutLineId
@@ -2156,7 +2090,7 @@ fun applyStockOutLineDelta(
val postingType = (typeOverride ?: savedSol.type ?: "").trim().lowercase()
val isIssuePosting = postingType == "miss" || postingType == "bad" || postingType == "expiry"

// 2) inventory_lot_line + inventory
// 2) inventory_lot_line; inventory onHand/unavailable is owned by the lot-line trigger
if (!skipInventoryWrite) {
val lotLine = savedSol.inventoryLotLine
if (lotLine != null) {
@@ -2196,19 +2130,6 @@ fun applyStockOutLineDelta(
}
}
}

val itemId = savedSol.item?.id
if (itemId != null) {
val inv = inventoryBucketResolver.findInventoryBucket(itemId, savedSol.inventoryLotLine)
if (inv != null) {
val zero = BigDecimal.ZERO
inv.onHandQty = (inv.onHandQty ?: zero).minus(deltaQty)
if (!isIssuePosting) {
inv.onHoldQty = (inv.onHoldQty ?: zero).minus(deltaQty)
}
inventoryRepository.save(inv)
}
}
}

// 3) stock_ledger (same ledger shape as createStockLedgerForStockOut / completeStockOutAfterLotLineSave)


+ 49
- 24
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Wyświetl plik

@@ -65,7 +65,8 @@ open class StockTakeRecordService(
val stockLedgerRepository: StockLedgerRepository,
val inventoryRepository: InventoryRepository,
val uomConversionRepository: UomConversionRepository,
val itemsRepository: ItemsRepository
val itemsRepository: ItemsRepository,
val inventoryBucketResolver: InventoryBucketResolver,

) {
private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java)
@@ -2125,7 +2126,7 @@ open fun batchSaveApproverStockTakeRecordsByIds(
"batchSaveApproverStockTakeRecordsByIds cache build completed: lotLinePairs={}, lots={}, inventories={}, stockTakeLines={}, cacheBuildMs={}",
adjustmentCache.inventoryLotLineByWarehouseLot.size,
adjustmentCache.inventoryLotById.size,
adjustmentCache.inventoryByItemId.size,
adjustmentCache.inventoryByItemAndStockUom.size,
adjustmentCache.stockTakeLineByRecordId.size,
elapsedMs(cacheBuildStartNs)
)
@@ -2213,7 +2214,7 @@ open fun batchSaveApproverStockTakeRecordsByIds(
val runtimeCache = BatchAdjustmentRuntimeCache(
stockOutByStockTakeId = mutableMapOf(),
stockInByStockTakeId = mutableMapOf(),
runningLedgerBalanceByItemId = mutableMapOf()
runningLedgerBalanceByBucket = mutableMapOf()
)
val adjustmentContext = StockTakeAdjustmentBatchContext()
var varianceCount = 0
@@ -2278,7 +2279,7 @@ open fun batchSaveApproverStockTakeRecordsByIds(
"batchSaveApproverStockTakeRecordsByIds runtime cache stats: stockOutHeads={}, stockInHeads={}, ledgerItems={}, batchedStockTakeLines={}, batchedOutLines={}, batchedInLines={}, batchedInventoryLotLines={}, batchedLedgers={}",
runtimeCache.stockOutByStockTakeId.size,
runtimeCache.stockInByStockTakeId.size,
runtimeCache.runningLedgerBalanceByItemId.size,
runtimeCache.runningLedgerBalanceByBucket.size,
adjustmentContext.stockTakeLineByRecordId.size,
adjustmentContext.stockOutLines.size,
adjustmentContext.stockInLines.size,
@@ -2398,9 +2399,7 @@ private fun applyVarianceAdjustment(
?: throw IllegalArgumentException("Inventory lot not found")

val itemId = inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found")
val inventory = cache?.inventoryByItemId?.get(itemId)
?: inventoryRepository.findByItemId(itemId).orElse(null)
?: throw IllegalArgumentException("Inventory not found for item")
val inventory = resolveInventoryForStockTake(itemId, inventoryLotLine, cache)

// 1. 更新開輪預建的 StockTakeLine,或舊資料無預建時新建
val stockTakeLine = stockTakeRecord.id?.let { rid ->
@@ -2481,11 +2480,9 @@ private fun applyVarianceAdjustment(
// 避免同一批多筆盤虧時每筆都用同一個 inventory.onHandQty 導致 balance 錯誤。
val itemIdForLedger = inventoryLot.item?.id
?: throw IllegalArgumentException("Item ID not found for stock take ledger")
val previousBalance = runtimeCache?.runningLedgerBalanceByItemId?.get(itemIdForLedger)
?: run {
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger)
latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
}
val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory)
val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey)
?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val newBalance = previousBalance - qtyToRemove.toDouble()

// FP-MTMS Version Checklist | Functions Ref. No. 46 | v1.0.1 | 2026-08-05
@@ -2519,7 +2516,7 @@ private fun applyVarianceAdjustment(
} else {
stockLedgerRepository.save(stockLedger)
}
runtimeCache?.runningLedgerBalanceByItemId?.put(itemIdForLedger, newBalance)
runtimeCache?.runningLedgerBalanceByBucket?.put(ledgerKey, newBalance)

val newOutQty = (latestLine.outQty ?: zero).add(qtyToRemove)
latestLine.outQty = newOutQty
@@ -2619,11 +2616,9 @@ private fun applyVarianceAdjustment(

val itemIdForLedger = inventoryLot.item?.id
?: throw IllegalArgumentException("Item ID not found for stock take ledger (in)")
val previousBalance = runtimeCache?.runningLedgerBalanceByItemId?.get(itemIdForLedger)
?: run {
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger)
latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
}
val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory)
val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey)
?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val newBalance = previousBalance + plusQty.toDouble()

// latestLine.inQty already includes plusQty above — snapshot live lot line after move.
@@ -2656,21 +2651,46 @@ private fun applyVarianceAdjustment(
} else {
stockLedgerRepository.save(stockLedger)
}
runtimeCache?.runningLedgerBalanceByItemId?.put(itemIdForLedger, newBalance)
runtimeCache?.runningLedgerBalanceByBucket?.put(ledgerKey, newBalance)
}
}

private fun resolveInventoryForStockTake(
itemId: Long,
lotLine: InventoryLotLine,
cache: BatchAdjustmentCache?,
): com.ffii.fpsms.modules.stock.entity.Inventory {
val stockUomId = inventoryBucketResolver.resolveStockUomId(lotLine)
if (stockUomId != null) {
cache?.inventoryByItemAndStockUom?.get(itemId to stockUomId)?.let { return it }
inventoryBucketResolver.findInventoryBucket(itemId, stockUomId)?.let { return it }
}
inventoryBucketResolver.findInventoryBucket(itemId, lotLine)?.let { return it }
throw IllegalArgumentException("Inventory not found for item $itemId stock UOM $stockUomId")
}

private fun stockTakeLedgerBucketKey(
itemId: Long,
lotLine: InventoryLotLine,
inventory: com.ffii.fpsms.modules.stock.entity.Inventory,
): Pair<Long, Long> {
val stockUomId = inventoryBucketResolver.resolveStockUomId(lotLine)
?: inventory.stockUom?.id
?: 0L
return itemId to stockUomId
}

private data class BatchAdjustmentCache(
val inventoryLotLineByWarehouseLot: Map<Pair<Long, Long>, InventoryLotLine>,
val inventoryLotById: Map<Long, InventoryLot>,
val inventoryByItemId: Map<Long, com.ffii.fpsms.modules.stock.entity.Inventory>,
val inventoryByItemAndStockUom: Map<Pair<Long, Long>, com.ffii.fpsms.modules.stock.entity.Inventory>,
val stockTakeLineByRecordId: Map<Long, StockTakeLine>
)

private data class BatchAdjustmentRuntimeCache(
val stockOutByStockTakeId: MutableMap<Long, StockOut>,
val stockInByStockTakeId: MutableMap<Long, StockIn>,
val runningLedgerBalanceByItemId: MutableMap<Long, Double>
val runningLedgerBalanceByBucket: MutableMap<Pair<Long, Long>, Double>
)

private data class StockTakeAdjustmentBatchContext(
@@ -2725,10 +2745,15 @@ private fun buildBatchAdjustmentCache(records: List<StockTakeRecord>): BatchAdju
emptyMap()
}
val itemIds = inventoryLotById.values.mapNotNull { it.item?.id }.distinct()
val inventoryByItemId =
val inventoryByItemAndStockUom =
if (itemIds.isNotEmpty()) {
inventoryRepository.findAllByItemIdInAndDeletedIsFalse(itemIds)
.groupBy { it.item?.id ?: 0L }
.mapNotNull { inv ->
val itemId = inv.item?.id ?: return@mapNotNull null
val stockUomId = inv.stockUom?.id ?: inv.uom?.id ?: return@mapNotNull null
(itemId to stockUomId) to inv
}
.groupBy({ it.first }, { it.second })
.mapValues { (_, list) -> list.minByOrNull { it.id ?: Long.MAX_VALUE }!! }
} else {
emptyMap()
@@ -2745,7 +2770,7 @@ private fun buildBatchAdjustmentCache(records: List<StockTakeRecord>): BatchAdju
return BatchAdjustmentCache(
inventoryLotLineByWarehouseLot = inventoryLotLineByWarehouseLot,
inventoryLotById = inventoryLotById,
inventoryByItemId = inventoryByItemId,
inventoryByItemAndStockUom = inventoryByItemAndStockUom,
stockTakeLineByRecordId = stockTakeLineByRecordId
)
}


+ 1
- 87
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Wyświetl plik

@@ -28,8 +28,7 @@ import kotlin.jvm.optionals.getOrNull
import org.springframework.transaction.annotation.Transactional
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import java.math.RoundingMode
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import java.math.RoundingMode
import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus
import com.ffii.fpsms.modules.stock.web.model.PickAnotherLotRequest
import com.ffii.fpsms.modules.stock.service.InventoryLotLineService
@@ -54,7 +53,6 @@ open class SuggestedPickLotService(
val itemUomService: ItemUomService,
val pickExecutionIssueRepository: PickExecutionIssueRepository, // 添加逗号
val pickOrderRepository: PickOrderRepository,
val inventoryRepository: InventoryRepository,
val failInventoryLotLineRepository: FailInventoryLotLineRepository,
val stockOutRepository: StockOutRepository,
val itemRepository: ItemsRepository,
@@ -1073,38 +1071,6 @@ val basePickOrders = (listOf(pickOrder) + allCompetingPickOrders).distinctBy { i
}
}
// ✅ 优化:批量更新 inventory table(收集所有 item IDs,只更新一次)
val allItemIdsToUpdate = allPickOrdersToResuggest
.flatMap { it.pickOrderLines }
.mapNotNull { it.item?.id }
.distinct()
if (allItemIdsToUpdate.isNotEmpty()) {
println("=== Batch updating inventory table for ${allItemIdsToUpdate.size} items ===")
allItemIdsToUpdate.forEach { itemId ->
// Calculate onHoldQty for ALL pick orders that use this item
// ✅ FIX: Use enum directly, not .value
val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
.sumOf { it.holdQty ?: BigDecimal.ZERO }
// ✅ FIX: Use enum directly, not .value
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE)
.sumOf {
val inQty = it.inQty ?: BigDecimal.ZERO
val outQty = it.outQty ?: BigDecimal.ZERO
val remainingQty = inQty.minus(outQty)
remainingQty
}
val inventory = inventoryRepository.findByItemId(itemId).orElse(null)
if (inventory != null) {
inventory.onHoldQty = onHoldQty
inventory.unavailableQty = unavailableQty
inventoryRepository.save(inventory)
}
}
println("✅ Batch updated inventory table for ${allItemIdsToUpdate.size} items")
}
println("=== RESUGGEST DEBUG END ===")
return MessageResponse(
@@ -1345,40 +1311,6 @@ private fun generateOptimalSuggestionsForAllPickOrders(
return suggestions
}

private fun updateInventoryTableAfterResuggest(pickOrder: PickOrder) {
try {
// Get all item IDs from the pick order
val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct()
itemIds.forEach { itemId ->
// FIX: Calculate onHoldQty for ALL pick orders that use this item, not just the current one
val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
.sumOf { it.holdQty ?: BigDecimal.ZERO }
// FIX: Use enum method instead of string method
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE)
.sumOf {
val inQty = it.inQty ?: BigDecimal.ZERO
val outQty = it.outQty ?: BigDecimal.ZERO
val remainingQty = inQty.minus(outQty)
remainingQty
}
// Update the inventory table
val inventory = inventoryRepository.findByItemId(itemId).orElse(null)
if (inventory != null) {
inventory.onHoldQty = onHoldQty
inventory.unavailableQty = unavailableQty
inventoryRepository.save(inventory)
println("Updated inventory for item $itemId: onHoldQty=$onHoldQty, unavailableQty=$unavailableQty")
}
}
} catch (e: Exception) {
println("Error updating inventory table after resuggest: ${e.message}")
e.printStackTrace()
}
}
private fun findAllSuggestionsForPickOrder(pickOrderId: Long): List<SuggestedPickLot> {
val pickOrderLines = pickOrderRepository.findById(pickOrderId)
.orElseThrow()
@@ -1802,27 +1734,9 @@ private fun handleMissingItem(inventoryLotLine: InventoryLotLine, failQty: BigDe
// 处理损坏物品的情况
private fun handleBrokenItem(inventoryLotLine: InventoryLotLine, failQty: BigDecimal) {
try {
// 1. 将库存批次行标记为不可用
inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE
// 2. 更新库存表的不可用数量
val itemId = inventoryLotLine.inventoryLot?.item?.id
if (itemId != null) {
val inventory = inventoryRepository.findByItemId(itemId).orElse(null)
if (inventory != null) {
val currentUnavailableQty = inventory.unavailableQty ?: BigDecimal.ZERO
inventory.unavailableQty = currentUnavailableQty.plus(failQty)
inventoryRepository.save(inventory)
println("Broken item handled: Updated unavailableQty by $failQty for item $itemId")
}
}
// 3. 保存库存批次行的状态变更
inventoryLotLineRepository.save(inventoryLotLine)
println("Broken item handled: Marked lot line ${inventoryLotLine.id} as unavailable")
} catch (e: Exception) {
println("Error handling broken item: ${e.message}")
throw e


Ładowanie…
Anuluj
Zapisz