2 коммитов

Автор SHA1 Сообщение Дата
  kelvin.yau 86b98b804c Merge branch 'fix負數倉' of https://git.2fi-solutions.com/derek/FPSMS-backend into fix負數倉 1 месяц назад
  kelvin.yau b36c68666a Fix DO stockQty and Location Search to use per-stock-UOM buckets. 1 месяц назад
6 измененных файлов: 121 добавлений и 25 удалений
  1. +28
    -16
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  2. +3
    -3
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  3. +26
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLocationSearchQty.kt
  4. +8
    -6
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt
  5. +25
    -0
      src/test/kotlin/com/ffii/fpsms/modules/deliveryOrder/service/DoLineStockQtyBucketTest.kt
  6. +31
    -0
      src/test/kotlin/com/ffii/fpsms/modules/stock/service/InventoryLocationSearchQtyTest.kt

+ 28
- 16
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Просмотреть файл

@@ -43,6 +43,7 @@ import com.ffii.fpsms.modules.pickOrder.entity.TruckRepository
import java.time.LocalTime import java.time.LocalTime
import com.ffii.fpsms.modules.pickOrder.entity.Truck import com.ffii.fpsms.modules.pickOrder.entity.Truck
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.stock.entity.StockOutRepository import com.ffii.fpsms.modules.stock.entity.StockOutRepository
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.entity.StockOut import com.ffii.fpsms.modules.stock.entity.StockOut
@@ -91,7 +92,6 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.models.DeliveryOrderLineInfo
import com.ffii.fpsms.modules.master.entity.Items import com.ffii.fpsms.modules.master.entity.Items
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo
import java.util.Locale import java.util.Locale
import org.slf4j.Logger import org.slf4j.Logger
@@ -110,6 +110,7 @@ open class DeliveryOrderService(
private val pickOrderRepository: PickOrderRepository, private val pickOrderRepository: PickOrderRepository,
private val suggestedPickLotService: SuggestedPickLotService, private val suggestedPickLotService: SuggestedPickLotService,
private val inventoryLotLineRepository: InventoryLotLineRepository, private val inventoryLotLineRepository: InventoryLotLineRepository,
private val inventoryRepository: InventoryRepository,
private val stockOutRepository: StockOutRepository, private val stockOutRepository: StockOutRepository,
private val stockOutLineRepository: StockOutLIneRepository, private val stockOutLineRepository: StockOutLIneRepository,
private val pickOrderLineRepository: PickOrderLineRepository, private val pickOrderLineRepository: PickOrderLineRepository,
@@ -481,14 +482,18 @@ open class DeliveryOrderService(
return deliveryOrderRepository.findByIdAndDeletedIsFalse(id); return deliveryOrderRepository.findByIdAndDeletedIsFalse(id);
} }
*/ */
private fun computeAvailableStockQtyForItem(itemId: Long): BigDecimal {
val availableLots = inventoryLotLineRepository
.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
return availableLots.sumOf { lot ->
(lot.inQty ?: BigDecimal.ZERO)
.minus(lot.outQty ?: BigDecimal.ZERO)
.minus(lot.holdQty ?: BigDecimal.ZERO)
}
/**
* DO line stock figure: one inventory bucket `(itemId, stockUomId)` where
* [stockUomId] is the line's UOM (`uom_conversion.id`).
* `available = onHand − unavailable` (hold not subtracted), same as Item Search.
* Must not sum lot lines across all stock UOMs of the item.
*/
private fun computeAvailableStockQtyForItem(itemId: Long, stockUomId: Long?): BigDecimal {
if (stockUomId == null) return BigDecimal.ZERO
val bucket = inventoryRepository.findByItemIdAndStockUomIdAndDeletedIsFalse(itemId, stockUomId)
?: return BigDecimal.ZERO
return (bucket.onHandQty ?: BigDecimal.ZERO)
.minus(bucket.unavailableQty ?: BigDecimal.ZERO)
} }


/** /**
@@ -603,11 +608,13 @@ open class DeliveryOrderService(
null null
} }


val itemIds = deliveryOrder.deliveryOrderLines.filter { it.deleted != true }.mapNotNull { it.item?.id }.distinct()
val stockQtyByItemId = itemIds.associateWith { itemId ->
computeAvailableStockQtyForItem(itemId)
}
val fallbackQtyByItemId = deliveryOrder.deliveryOrderLines.filter { it.deleted != true }.mapNotNull { line ->
val activeLines = deliveryOrder.deliveryOrderLines.filter { it.deleted != true }
val stockQtyByBucket = activeLines.mapNotNull { line ->
val itemId = line.item?.id ?: return@mapNotNull null
val stockUomId = line.uom?.id ?: return@mapNotNull null
(itemId to stockUomId) to computeAvailableStockQtyForItem(itemId, stockUomId)
}.toMap()
val fallbackQtyByItemId = activeLines.mapNotNull { line ->
val itemId = line.item?.id ?: return@mapNotNull null val itemId = line.item?.id ?: return@mapNotNull null
itemId to (line.qty ?: BigDecimal.ZERO) itemId to (line.qty ?: BigDecimal.ZERO)
}.toMap() }.toMap()
@@ -632,9 +639,14 @@ open class DeliveryOrderService(
isExtra = deliveryOrder.isExtra, isExtra = deliveryOrder.isExtra,
handlerName = handlerName, handlerName = handlerName,
truckLaneCode = truckLaneCode, truckLaneCode = truckLaneCode,
deliveryOrderLines = deliveryOrder.deliveryOrderLines.filter { it.deleted != true }.map { line ->
deliveryOrderLines = activeLines.map { line ->
val itemId = line.item?.id val itemId = line.item?.id
val stockQty = itemId?.let { stockQtyByItemId[it] } ?: BigDecimal.ZERO
val stockUomId = line.uom?.id
val stockQty = if (itemId != null && stockUomId != null) {
stockQtyByBucket[itemId to stockUomId] ?: BigDecimal.ZERO
} else {
BigDecimal.ZERO
}
val requiredQty = line.qty ?: BigDecimal.ZERO val requiredQty = line.qty ?: BigDecimal.ZERO
val availableStatus = val availableStatus =
if (stockQty >= requiredQty) "available" else "insufficient" if (stockQty >= requiredQty) "available" else "insufficient"


+ 3
- 3
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt Просмотреть файл

@@ -111,14 +111,14 @@ WHERE ill.id = :id
item.code AS itemCode, item.code AS itemCode,
item.name AS itemName, item.name AS itemName,
item.type AS itemType, item.type AS itemType,
SUM(COALESCE(ill.inQty, 0)) AS onHandQty,
SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS onHandQty,
SUM(COALESCE(ill.holdQty, 0)) AS onHoldQty, SUM(COALESCE(ill.holdQty, 0)) AS onHoldQty,
SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) AS availableQty,
SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS availableQty,
CAST(uom.id AS long) AS uomId, CAST(uom.id AS long) AS uomId,
uom.code AS uomCode, uom.code AS uomCode,
uom.udfudesc AS uomUdfudesc, uom.udfudesc AS uomUdfudesc,
uom.udfShortDesc AS uomShortDesc, uom.udfShortDesc AS uomShortDesc,
SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) AS qtyPerSmallestUnit,
SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS qtyPerSmallestUnit,
MAX(item.latestMarketUnitPrice) AS latestMarketUnitPrice, MAX(item.latestMarketUnitPrice) AS latestMarketUnitPrice,
MAX(item.latestMupUpdatedDate) AS latestMupUpdatedDate MAX(item.latestMupUpdatedDate) AS latestMupUpdatedDate
FROM InventoryLotLine ill FROM InventoryLotLine ill


+ 26
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLocationSearchQty.kt Просмотреть файл

@@ -0,0 +1,26 @@
package com.ffii.fpsms.modules.stock.service

import java.math.BigDecimal

/**
* Location Search qty rules aligned with inventory **bucket** display
* (`available = onHand − unavailable`; hold is separate, not subtracted from available).
*
* Location Search only aggregates `status = available` lot lines at the filtered location,
* so unavailable at that slice is 0 and available collapses to physical remain `in − out`.
*/
object InventoryLocationSearchQty {
fun onHand(inQty: BigDecimal?, outQty: BigDecimal?): BigDecimal {
val inn = inQty ?: BigDecimal.ZERO
val out = outQty ?: BigDecimal.ZERO
return inn.subtract(out)
}

/** Bucket-aligned available for available-status lines: physical remain (hold not subtracted). */
fun available(inQty: BigDecimal?, outQty: BigDecimal?): BigDecimal = onHand(inQty, outQty)

/** SQL fragments used by [InventoryService.buildLocationSearchUnionSql]. */
const val SQL_ON_HAND = "SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0))"
const val SQL_AVAILABLE = "SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0))"
const val SQL_ON_HOLD = "SUM(COALESCE(ill.holdQty, 0))"
}

+ 8
- 6
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt Просмотреть файл

@@ -101,8 +101,10 @@ open class InventoryService(
} }


/** /**
* 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.
* Location Search: one row per item + lot-line stock UoM (bucket key), plus current
* master stockUnit as a 0-qty row when that UoM has no matching lots.
* Qty formula matches inventory bucket display: available = onHand − unavailable
* (hold shown separately; not subtracted). See [InventoryLocationSearchQty].
*/ */
open fun searchInventoriesByLatestInventory( open fun searchInventoriesByLatestInventory(
request: SearchInventoryRequest, request: SearchInventoryRequest,
@@ -276,14 +278,14 @@ open class InventoryService(
item.code AS itemCode, item.code AS itemCode,
item.name AS itemName, item.name AS itemName,
item.type AS itemType, 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,
${InventoryLocationSearchQty.SQL_ON_HAND} AS onHandQty,
${InventoryLocationSearchQty.SQL_ON_HOLD} AS onHoldQty,
${InventoryLocationSearchQty.SQL_AVAILABLE} AS availableQty,
uom.id AS uomId, uom.id AS uomId,
uom.code AS uomCode, uom.code AS uomCode,
uom.udfudesc AS uomUdfudesc, uom.udfudesc AS uomUdfudesc,
uom.udfShortDesc AS uomShortDesc, uom.udfShortDesc AS uomShortDesc,
SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) AS qtyPerSmallestUnit,
${InventoryLocationSearchQty.SQL_AVAILABLE} AS qtyPerSmallestUnit,
MAX(item.latestMarketUnitPrice) AS latestMarketUnitPrice, MAX(item.latestMarketUnitPrice) AS latestMarketUnitPrice,
MAX(item.latestMupUpdatedDate) AS latestMupUpdatedDate MAX(item.latestMupUpdatedDate) AS latestMupUpdatedDate
FROM inventory_lot_line ill FROM inventory_lot_line ill


+ 25
- 0
src/test/kotlin/com/ffii/fpsms/modules/deliveryOrder/service/DoLineStockQtyBucketTest.kt Просмотреть файл

@@ -0,0 +1,25 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.math.BigDecimal

/**
* Documents DO detail stockQty rule after bucket fix:
* available = onHand − unavailable for one (itemId, stockUomId); never sum sibling UOMs.
*/
class DoLineStockQtyBucketTest {

private fun bucketAvailable(onHand: BigDecimal?, unavailable: BigDecimal?): BigDecimal =
(onHand ?: BigDecimal.ZERO).minus(unavailable ?: BigDecimal.ZERO)

@Test
fun two_buckets_must_not_be_added() {
// MA0879-style: 箱 avail 12 + 個 avail 760 — DO line in 箱 must show 12, not 772
val box = bucketAvailable(BigDecimal("15"), BigDecimal("3"))
val each = bucketAvailable(BigDecimal("880"), BigDecimal("120"))
assertEquals(BigDecimal("12"), box)
assertEquals(BigDecimal("760"), each)
assertEquals(BigDecimal("772"), box.add(each)) // old bug total — must not be returned for either line
}
}

+ 31
- 0
src/test/kotlin/com/ffii/fpsms/modules/stock/service/InventoryLocationSearchQtyTest.kt Просмотреть файл

@@ -0,0 +1,31 @@
package com.ffii.fpsms.modules.stock.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.math.BigDecimal

class InventoryLocationSearchQtyTest {

@Test
fun onHand_and_available_are_physical_remain_not_minus_hold() {
// in=10, out=3 → remain 7; hold must not shrink Location Search available
assertEquals(BigDecimal("7"), InventoryLocationSearchQty.onHand(BigDecimal("10"), BigDecimal("3")))
assertEquals(BigDecimal("7"), InventoryLocationSearchQty.available(BigDecimal("10"), BigDecimal("3")))
}

@Test
fun sql_fragments_match_bucket_not_pickable_formula() {
assertEquals(
"SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0))",
InventoryLocationSearchQty.SQL_ON_HAND,
)
assertEquals(
"SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0))",
InventoryLocationSearchQty.SQL_AVAILABLE,
)
assertFalse(InventoryLocationSearchQty.SQL_AVAILABLE.contains("holdQty"))
assertTrue(InventoryLocationSearchQty.SQL_ON_HOLD.contains("holdQty"))
}
}

Загрузка…
Отмена
Сохранить