Ver a proveniência

update suggest do order logic

master
CANCERYS\kw093 há 2 semanas
ascendente
cometimento
c2d9f2f816
3 ficheiros alterados com 222 adições e 380 eliminações
  1. +84
    -162
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  2. +79
    -131
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
  3. +59
    -87
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt

+ 84
- 162
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Ver ficheiro

@@ -494,12 +494,12 @@ open class DeliveryOrderService(

@Transactional(rollbackFor = [Exception::class])
open fun releaseDeliveryOrder(request: ReleaseDoRequest): MessageResponse {
println("�� DEBUG: Starting releaseDeliveryOrder for DO ID: ${request.id}, User ID: ${request.userId}")
println(" DEBUG: Starting releaseDeliveryOrder for DO ID: ${request.id}, User ID: ${request.userId}")

val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id)
?: throw NoSuchElementException("Delivery Order not found")

println("�� DEBUG: Found delivery order - ID: ${deliveryOrder.id}, Shop: ${deliveryOrder.shop?.code}, Status: ${deliveryOrder.status}")
println(" DEBUG: Found delivery order - ID: ${deliveryOrder.id}, Shop: ${deliveryOrder.shop?.code}, Status: ${deliveryOrder.status}")

deliveryOrder.apply {
status = DeliveryOrderStatus.PENDING
@@ -530,10 +530,10 @@ open class DeliveryOrderService(
pickOrderEntity.consoCode = consoCode
pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED
pickOrderRepository.saveAndFlush(pickOrderEntity)
println("�� DEBUG: Assigned consoCode $consoCode to pick order ${createdPickOrder.id}")
println(" DEBUG: Assigned consoCode $consoCode to pick order ${createdPickOrder.id}")

// Debug: Check pick order lines
println("�� DEBUG: Pick order has ${pickOrderEntity.pickOrderLines?.size ?: 0} pick order lines")
println(" DEBUG: Pick order has ${pickOrderEntity.pickOrderLines?.size ?: 0} pick order lines")
pickOrderEntity.pickOrderLines?.forEach { line ->
println(" DEBUG: Pick order line - Item ID: ${line.item?.id}, Qty: ${line.qty}")
}
@@ -605,125 +605,98 @@ open class DeliveryOrderService(
}

// CREATE do_pick_order_record entries
// 第 471-555 行附近 - 修复创建逻辑

// CREATE do_pick_order_record entries
val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
val datePrefix = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))

println(" DEBUG: Target date: $targetDate, Date prefix: $datePrefix")
// 新逻辑:根据 supplier code 决定楼层
// 如果 supplier code 是 "P06B",使用 4F,否则使用 2F
val supplierCode = deliveryOrder.supplier?.code
val preferredFloor = if (supplierCode == "P06B") {
"4F"
} else {
"2F"
}

println(" DEBUG: Supplier code: $supplierCode, Preferred floor: $preferredFloor")

// 查找 truck
val truck = deliveryOrder.shop?.id?.let { shopId ->
println(" DEBUG: Looking for truck with shop ID: $shopId")
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
println(" DEBUG: Found ${trucks.size} trucks for shop $shopId")

// 移除提前返回,总是分析 items 分布
// 分析 DO order lines 中的 items 分布
// 分析 items 来确定 storeId
val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct()
val inventoryQuery = """
SELECT
w.store_id as floor,
COUNT(DISTINCT il.itemId) as item_count
FROM inventory_lot il
INNER JOIN inventory i ON i.itemId = il.itemId AND i.deleted = 0 AND i.onHandQty > 0
INNER JOIN inventory_lot_line ill ON ill.inventoryLotId = il.id AND ill.deleted = 0
INNER JOIN warehouse w ON w.id = ill.warehouseId AND w.deleted = 0 AND w.store_id IN ('2F', '4F')
WHERE il.itemId IN (${itemIds.joinToString(",")}) AND il.deleted = 0
GROUP BY w.store_id
""".trimIndent()

val inventoryResults = jdbcDao.queryForList(inventoryQuery)
val floorItemCount = mutableMapOf<String, Int>()
inventoryResults.forEach { row ->
floorItemCount[row["floor"] as? String ?: "Other"] = (row["item_count"] as? Number)?.toInt() ?: 0
val preferredStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
}

println(" DEBUG: Floor item count distribution: $floorItemCount")
println(" DEBUG: Total items: ${itemIds.size}, Items on 4F: ${floorItemCount["4F"] ?: 0}")

// 新逻辑:只有所有 items 都在 4F,才算 4F,否则算 2F
val preferredFloor = if ((floorItemCount["4F"] ?: 0) == itemIds.size && (floorItemCount["2F"] ?: 0) == 0) {
"4F" // 所有 items 都在 4F
// 优先选择匹配的 truck
val selectedTruck = if (trucks.size > 1) {
trucks.find { it.storeId == preferredStoreId }
?: trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
} else {
"2F" // 只要有任何 item 不在 4F,就算 2F
trucks.firstOrNull()
}

println(" DEBUG: Preferred floor: $preferredFloor (All items on 4F: ${preferredFloor == "4F"})")
println(" DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}")
selectedTruck
}

// 查找 truck
val truck = deliveryOrder.shop?.id?.let { shopId ->
println(" DEBUG: Looking for truck with shop ID: $shopId")
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
println(" DEBUG: Found ${trucks.size} trucks for shop $shopId")
// 检查 truck 和 preferredFloor 是否匹配
val truckStoreId = truck?.storeId
val expectedStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
}

if (truck == null || truckStoreId != expectedStoreId) {
val errorMsg =
"Items preferredFloor ($preferredFloor) does not match available truck (Truck Store: $truckStoreId). Skipping DO ${deliveryOrder.id}."
println("⚠️ $errorMsg")
throw IllegalStateException(errorMsg)
}

val preferredStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
}
// 优先选择匹配的 truck
val selectedTruck = if (trucks.size > 1) {
trucks.find { it.storeId == preferredStoreId }
?: trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
} else {
trucks.firstOrNull()
}
println(" DEBUG: Truck matches preferred floor - Truck Store: $truckStoreId, Preferred: $preferredFloor")

println(" DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}")
selectedTruck
}
// storeId 基于 preferredFloor
val storeId = "$preferredFloor/F"
val loadingSequence = truck.loadingSequence ?: 999

// 检查 truck 和 preferredFloor 是否匹配
val truckStoreId = truck?.storeId
val expectedStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
}
println(" DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${truck.id}")

if (truck == null || truckStoreId != expectedStoreId) {
val errorMsg =
"Items preferredFloor ($preferredFloor) does not match available truck (Truck Store: $truckStoreId). Skipping DO ${deliveryOrder.id}."
println("⚠️ $errorMsg")
throw IllegalStateException(errorMsg) // 或返回错误响应
}
val doPickOrder = DoPickOrder(
storeId = storeId,
ticketNo = "TEMP-${System.currentTimeMillis()}",
ticketStatus = DoPickOrderStatus.pending,
truckId = truck.id,
pickOrderId = createdPickOrder.id,
doOrderId = deliveryOrder.id,
ticketReleaseTime = null,
shopId = deliveryOrder.shop?.id,
handlerName = null,
handledBy = null,
ticketCompleteDateTime = null,
truckDepartureTime = truck.departureTime,
truckLanceCode = truck.truckLanceCode,
shopCode = deliveryOrder.shop?.code,
shopName = deliveryOrder.shop?.name,
requiredDeliveryDate = targetDate,
createdBy = null,
modifiedBy = null,
pickOrderCode = createdPickOrder.code,
deliveryOrderCode = deliveryOrder.code,
loadingSequence = loadingSequence,
releaseType = null
)

println(" DEBUG: Truck matches preferred floor - Truck Store: $truckStoreId, Preferred: $preferredFloor")

// storeId 基于 items 的 preferredFloor
val storeId = "$preferredFloor/F"
val loadingSequence = truck.loadingSequence ?: 999

println(" DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${truck.id}")

val doPickOrder = DoPickOrder(
storeId = storeId,
ticketNo = "TEMP-${System.currentTimeMillis()}",
ticketStatus = DoPickOrderStatus.pending,
truckId = truck.id,
doOrderId = deliveryOrder.id,
pickOrderId = createdPickOrder.id,
truckDepartureTime = truck.departureTime,
shopId = deliveryOrder.shop?.id,
handledBy = null,
pickOrderCode = createdPickOrder.code,
deliveryOrderCode = deliveryOrder.code,
loadingSequence = loadingSequence,
ticketReleaseTime = null,
truckLanceCode = truck.truckLanceCode,
shopCode = deliveryOrder.shop?.code,
shopName = deliveryOrder.shop?.name,
requiredDeliveryDate = targetDate
)
val savedDoPickOrder = doPickOrderService.save(doPickOrder)
println(" DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}")

val savedDoPickOrder = doPickOrderService.save(doPickOrder)
println(" DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}")
truck
}
return MessageResponse(
id = deliveryOrder.id,
code = deliveryOrder.code,
@@ -1273,63 +1246,20 @@ open class DeliveryOrderService(
}
}

// 分析楼层分布
// 新逻辑:根据 supplier code 决定楼层
// 如果 supplier code 是 "P06B",使用 4F,否则使用 2F
val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct()

val itemsStoreIdQuery = """
SELECT
i.store_id,
COUNT(DISTINCT i.id) as item_count
FROM items i
WHERE i.id IN (${itemIds.joinToString(",")})
AND i.deleted = 0
GROUP BY i.store_id
""".trimIndent()

val itemsStoreIdResults = jdbcDao.queryForList(itemsStoreIdQuery)
val storeIdItemCount = mutableMapOf<String, Int>()
var totalItemsWithStoreId = 0 // 统计有 store_id 的商品总数

itemsStoreIdResults.forEach { row ->
val rawStoreId = row["store_id"] as? String
if (rawStoreId != null) { // 只统计非 NULL 的 store_id
val normalizedStoreId = when (rawStoreId) {
"3F" -> "4F"
else -> rawStoreId
}
val count = (row["item_count"] as? Number)?.toInt() ?: 0
storeIdItemCount[normalizedStoreId] =
(storeIdItemCount[normalizedStoreId] ?: 0) + count
totalItemsWithStoreId += count
}
}

val count2F = storeIdItemCount["2F"] ?: 0
val count4F = storeIdItemCount["4F"] ?: 0


val preferredFloor = when {
totalItemsWithStoreId == 0 -> {
println("⚠️ WARNING: All items have NULL store_id, defaulting to 2F")
"2F"
}

count4F > count2F -> "4F"
count2F > count4F -> "2F"
count4F == totalItemsWithStoreId && count2F == 0 -> "4F"
else -> "2F" // 默认 2F
val supplierCode = deliveryOrder.supplier?.code
val preferredFloor = if (supplierCode == "P06B") {
"4F"
} else {
"2F"
}

println(" DEBUG: Floor calculation for DO ${deliveryOrder.id}")
println(" - Total items: ${itemIds.size}")
println(" - Items with store_id: $totalItemsWithStoreId")
println(" - Items without store_id: ${itemIds.size - totalItemsWithStoreId}")
println(" - 2F items: $count2F")
println(" - 4F items: $count4F")
println(" - Supplier code: $supplierCode")
println(" - Preferred floor: $preferredFloor")


val truck = deliveryOrder.shop?.id?.let { shopId ->
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
val preferredStoreId = when (preferredFloor) {
@@ -1339,19 +1269,16 @@ open class DeliveryOrderService(
else -> "2F"
}


val matchedTrucks = trucks.filter { it.storeId == preferredStoreId }

if (matchedTrucks.isEmpty()) {
null
} else {

if (preferredStoreId == "4F" && matchedTrucks.size > 1) {
deliveryOrder.estimatedArrivalDate?.let { estimatedArrivalDate ->
val targetDate = estimatedArrivalDate.toLocalDate()
val dayOfWeek = targetDate.dayOfWeek


val dayAbbr = when (dayOfWeek) {
java.time.DayOfWeek.MONDAY -> "Mon"
java.time.DayOfWeek.TUESDAY -> "Tue"
@@ -1365,7 +1292,6 @@ open class DeliveryOrderService(
println(" DEBUG: DO ${deliveryOrder.id} - Target date: $targetDate ($dayAbbr), Shop: $shopId")
println(" DEBUG: Found ${matchedTrucks.size} matched 4F trucks")


val dayMatchedTrucks = matchedTrucks.filter {
it.truckLanceCode?.contains(dayAbbr, ignoreCase = true) == true
}
@@ -1376,21 +1302,17 @@ open class DeliveryOrderService(
}

if (dayMatchedTrucks.isNotEmpty()) {

val selected = dayMatchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
println("✅ DEBUG: Selected truck matching $dayAbbr - ID=${selected?.id}, Code=${selected?.truckLanceCode}")
selected
} else {

println("⚠️ WARNING: No truck matching $dayAbbr, using first available truck")
matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
}
} ?: run {

matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
}
} else {

matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
}
}
@@ -1419,7 +1341,7 @@ open class DeliveryOrderService(
truckId = truck.id,
truckDepartureTime = truck.departureTime,
truckLanceCode = truck.truckLanceCode,
loadingSequence = truck.loadingSequence // 直接使用 truck 的值
loadingSequence = truck.loadingSequence
)
}



+ 79
- 131
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt Ver ficheiro

@@ -77,48 +77,20 @@ class DoReleaseCoordinatorService(
val updateSql = """
UPDATE fpsmsdb.do_pick_order dpo
INNER JOIN (
WITH DoStoreIdCounts AS (
SELECT
dol.deliveryOrderId,
CASE
WHEN i.store_id = '3F' THEN '4F'
ELSE i.store_id
END AS store_id,
COUNT(DISTINCT dol.itemId) AS item_count
FROM fpsmsdb.delivery_order_line dol
INNER JOIN fpsmsdb.items i ON i.id = dol.itemId
AND i.deleted = 0
WHERE dol.deleted = 0
GROUP BY dol.deliveryOrderId,
CASE
WHEN i.store_id = '3F' THEN '4F'
ELSE i.store_id
END
),
DoStoreIdSummary AS (
WITH PreferredFloor AS (
SELECT
do.id AS deliveryOrderId,
COALESCE(SUM(CASE WHEN dsc.store_id = '2F' THEN dsc.item_count ELSE 0 END), 0) AS count_2f,
COALESCE(SUM(CASE WHEN dsc.store_id = '4F' THEN dsc.item_count ELSE 0 END), 0) AS count_4f
FROM fpsmsdb.delivery_order do
LEFT JOIN DoStoreIdCounts dsc ON dsc.deliveryOrderId = do.id
WHERE do.deleted = 0
GROUP BY do.id
),
PreferredFloor AS (
SELECT
deliveryOrderId,
CASE
WHEN count_2f > count_4f THEN '2F'
WHEN count_4f > count_2f THEN '4F'
WHEN s.code = 'P06B' THEN '4F'
ELSE '2F'
END AS preferred_floor,
CASE
WHEN count_2f > count_4f THEN 2
WHEN count_4f > count_2f THEN 4
WHEN s.code = 'P06B' THEN 4
ELSE 2
END AS preferred_store_id
FROM DoStoreIdSummary
FROM fpsmsdb.delivery_order do
LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0
WHERE do.deleted = 0
),
TruckSelection AS (
SELECT
@@ -261,68 +233,27 @@ class DoReleaseCoordinatorService(
e.printStackTrace()
}
}

private fun getOrderedDeliveryOrderIds(ids: List<Long>): List<Long> {
try {
println(" DEBUG: Getting ordered IDs for ${ids.size} orders")
println(" DEBUG: First 5 IDs: ${ids.take(5)}")
val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate")
val sql = """
WITH DoFloorCounts AS (
SELECT
dol.deliveryOrderId,
w.store_id,
COUNT(DISTINCT dol.itemId) AS item_count
FROM fpsmsdb.delivery_order_line dol
INNER JOIN fpsmsdb.inventory i ON i.itemId = dol.itemId
AND i.deleted = 0
AND i.onHandQty > 0
INNER JOIN fpsmsdb.inventory_lot il ON il.itemId = i.itemId
AND il.deleted = 0
INNER JOIN fpsmsdb.inventory_lot_line ill ON ill.inventoryLotId = il.id
AND ill.deleted = 0
INNER JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
AND w.deleted = 0
AND w.store_id IN ('2F', '4F')
WHERE dol.deleted = 0
AND dol.deliveryOrderId IN (${ids.joinToString(",")})
AND dol.deliveryOrderId NOT IN (
SELECT DISTINCT dpol.do_order_id
FROM fpsmsdb.do_pick_order_line dpol
INNER JOIN fpsmsdb.do_pick_order dpo ON dpo.id = dpol.do_pick_order_id
WHERE dpo.release_type = 'single'
AND dpo.deleted = 0
AND dpol.deleted = 0
)
GROUP BY dol.deliveryOrderId, w.store_id
),
DoFloorSummary AS (
WITH PreferredFloor AS (
SELECT
do.id AS deliveryOrderId,
COALESCE(SUM(CASE WHEN dfc.store_id = '2F' THEN dfc.item_count ELSE 0 END), 0) AS count_2f,
COALESCE(SUM(CASE WHEN dfc.store_id = '4F' THEN dfc.item_count ELSE 0 END), 0) AS count_4f
FROM fpsmsdb.delivery_order do
LEFT JOIN DoFloorCounts dfc ON dfc.deliveryOrderId = do.id
WHERE do.id IN (${ids.joinToString(",")})
AND do.deleted = 0
GROUP BY do.id
),
PreferredFloor AS (
SELECT
deliveryOrderId,
count_2f,
count_4f,
CASE
WHEN count_2f > count_4f THEN '2F'
WHEN count_4f > count_2f THEN '4F'
WHEN s.code = 'P06B' THEN '4F'
ELSE '2F'
END AS preferred_floor,
CASE
WHEN count_2f > count_4f THEN 2
WHEN count_4f > count_2f THEN 4
WHEN s.code = 'P06B' THEN 4
ELSE 2
END AS preferred_store_id
FROM DoFloorSummary
FROM fpsmsdb.delivery_order do
LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0
WHERE do.id IN (${ids.joinToString(",")})
AND do.deleted = 0
),
TruckSelection AS (
SELECT
@@ -624,37 +555,45 @@ class DoReleaseCoordinatorService(
)
}

private fun createMergedDoPickOrder(results: List<ReleaseDoResult>) {
val first = results.first()
val storeId = when (first.preferredFloor) {
"2F" -> "2/F"
"4F" -> "4/F"
else -> "2/F"
}

val doPickOrder = DoPickOrder(
storeId = storeId,
ticketNo = "TEMP-${System.currentTimeMillis()}",
ticketStatus = DoPickOrderStatus.pending,
truckId = first.truckId,
truckDepartureTime = first.truckDepartureTime,
shopId = first.shopId,
handledBy = null,
loadingSequence = first.loadingSequence ?: 999,
ticketReleaseTime = null,
truckLanceCode = first.truckLanceCode,
shopCode = first.shopCode,
shopName = first.shopName,
requiredDeliveryDate = first.estimatedArrivalDate,
releaseType = "batch"
// 替换第 627-653 行
private fun createMergedDoPickOrder(results: List<ReleaseDoResult>) {
val first = results.first()
val storeId = when (first.preferredFloor) {
"2F" -> "2/F"
"4F" -> "4/F"
else -> "2/F"
}

)
// 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save()
val saved = doPickOrderRepository.save(doPickOrder)
println(" DEBUG: Saved DoPickOrder - ID: ${saved.id}, Ticket: ${saved.ticketNo}")
val doPickOrder = DoPickOrder(
storeId = storeId,
ticketNo = "TEMP-${System.currentTimeMillis()}",
ticketStatus = DoPickOrderStatus.pending,
truckId = first.truckId,
pickOrderId = null,
doOrderId = null,
ticketReleaseTime = null,
shopId = first.shopId,
handlerName = null,
handledBy = null,
ticketCompleteDateTime = null,
truckDepartureTime = first.truckDepartureTime,
truckLanceCode = first.truckLanceCode,
shopCode = first.shopCode,
shopName = first.shopName,
requiredDeliveryDate = first.estimatedArrivalDate,
createdBy = null,
modifiedBy = null,
pickOrderCode = null,
deliveryOrderCode = null,
loadingSequence = first.loadingSequence,
releaseType = "batch"
)
// 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save()
val saved = doPickOrderRepository.save(doPickOrder)
println(" DEBUG: Saved DoPickOrder - ID: ${saved.id}, Ticket: ${saved.ticketNo}")
// 创建多条 DoPickOrderLine(每个 DO 一条)
results.forEach { result ->
@@ -813,25 +752,34 @@ class DoReleaseCoordinatorService(
doPickOrderLineRepository.save(line)
println(" DEBUG: Created DoPickOrderLine for existing DoPickOrder ${existingDoPickOrder.id}")
}
} else {
// 如果不存在,创建新的 do_pick_order
val doPickOrder = DoPickOrder(
storeId = storeId,
ticketNo = "TEMP-${System.currentTimeMillis()}",
ticketStatus = DoPickOrderStatus.pending,
truckId = result.truckId,
truckDepartureTime = result.truckDepartureTime,
shopId = result.shopId,
handledBy = null,
loadingSequence = result.loadingSequence ?: 999,
ticketReleaseTime = null,
truckLanceCode = result.truckLanceCode,
shopCode = result.shopCode,
shopName = result.shopName,
requiredDeliveryDate = result.estimatedArrivalDate,
releaseType = "single" // 设置为 single
)
val saved = doPickOrderRepository.save(doPickOrder)
// 替换第 748-764 行
} else {
// 如果不存在,创建新的 do_pick_order
val doPickOrder = DoPickOrder(
storeId = storeId,
ticketNo = "TEMP-${System.currentTimeMillis()}",
ticketStatus = DoPickOrderStatus.pending,
truckId = result.truckId,
pickOrderId = null,
doOrderId = null,
ticketReleaseTime = null,
shopId = result.shopId,
handlerName = null,
handledBy = null,
ticketCompleteDateTime = null,
truckDepartureTime = result.truckDepartureTime,
truckLanceCode = result.truckLanceCode,
shopCode = result.shopCode,
shopName = result.shopName,
requiredDeliveryDate = result.estimatedArrivalDate,
createdBy = null,
modifiedBy = null,
pickOrderCode = null,
deliveryOrderCode = null,
loadingSequence = result.loadingSequence,
releaseType = "single"
)
val saved = doPickOrderRepository.save(doPickOrder)
// 创建 do_pick_order_line
val line = DoPickOrderLine().apply {


+ 59
- 87
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Ver ficheiro

@@ -99,8 +99,8 @@ open class SuggestedPickLotService(
val suggestedList: MutableList<SuggestedPickLot> = mutableListOf()
val holdQtyMap: MutableMap<Long?, BigDecimal?> = request.holdQtyMap
// get current inventory lot line qty & grouped by item id
val excludedWarehouseCodes = listOf("2F-W202-01-00")
val availableInventoryLotLines = inventoryLotLineService
.allInventoryLotLinesByItemIdIn(itemIds)
.filter { it.status == InventoryLotLineStatus.AVAILABLE.value }
@@ -111,6 +111,7 @@ open class SuggestedPickLotService(
// loop for suggest pick lot line
pols.forEach { line ->
val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
val lotLines = availableInventoryLotLines[line.item?.id].orEmpty()
val ratio = one // (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP)
@@ -256,126 +257,97 @@ open class SuggestedPickLotService(
// loop for suggest pick lot line
pols.forEach { line ->
val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
val ratio = one
// === 新增:如果是 DO pick order,则按 DO 的 preferredFloor 过滤 lot ===
// preferredFloor 规则:supplier.code == "P06B" -> 4F,否则 2F
val pickOrder = line.pickOrder
val isDoPickOrder = pickOrder?.type?.value == "do" || pickOrder?.type?.value == "delivery_order"
val doPreferredFloor: String? = if (isDoPickOrder) {
val supplierCode = pickOrder?.deliveryOrder?.supplier?.code
if (supplierCode == "P06B") "4F" else "2F"
} else {
null
}
val lotLines = availableInventoryLotLines[line.item?.id].orEmpty()
val ratio = one // (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP)
// FIX: Calculate remaining quantity needed (not the full required quantity)
// 计算 remaining qty
val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(line.id!!)
val totalPickedQty = stockOutLines
.filter {
it.status == "completed" ||
it.status == "partially_completed" ||
(it.status == "rejected" && (it.qty ?: zero) > zero) // 包含已 picked 的 rejected
}
.sumOf { it.qty ?: zero }
.filter {
it.status == "completed" ||
it.status == "partially_completed" ||
(it.status == "rejected" && (it.qty ?: zero) > zero)
}
.sumOf { it.qty ?: zero }
val requiredQty = line.qty ?: zero
val remainingQty = requiredQty.minus(totalPickedQty)
println("=== SUGGESTION DEBUG for Pick Order Line ${line.id} ===")
println("Required qty: $requiredQty")
println("Total picked qty: $totalPickedQty")
println("Remaining qty needed: $remainingQty")
println("Stock out lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}")
// FIX: Use remainingQty instead of line.qty
var remainingQtyToAllocate = remainingQty
println("remaining1 $remainingQtyToAllocate (sales units)")
val updatedLotLines = mutableListOf<InventoryLotLineInfo>()
lotLines.forEachIndexed { index, lotLine ->
lotLines.forEachIndexed { _, lotLine ->
if (remainingQtyToAllocate <= zero) return@forEachIndexed
println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}")
// 拿 entity 是因为 projection 的 warehouse 没有 store_id 字段
val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() }
// 修复:计算可用数量,转换为销售单位
?: return@forEachIndexed
val warehouseStoreId = inventoryLotLine.warehouse?.store_id
val warehouseCode = inventoryLotLine.warehouse?.code
// 规则 1:排除指定 warehouse code
if (warehouseCode != null && excludedWarehouseCodes.contains(warehouseCode)) {
return@forEachIndexed
}
// 规则 2:原有逻辑:跳过 3F
if (warehouseStoreId == "3F") {
return@forEachIndexed
}
// 规则 3:新增逻辑:DO 订单必须匹配楼层,否则不建议
// 例:doPreferredFloor=2F,但 lot 在 4F => 跳过
if (doPreferredFloor != null && warehouseStoreId != doPreferredFloor) {
return@forEachIndexed
}
val availableQtyInBaseUnits = calculateRemainingQtyForInfo(lotLine)
val holdQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero
val availableQtyInSalesUnits = availableQtyInBaseUnits
.minus(holdQtyInBaseUnits)
.divide(ratio, 2, RoundingMode.HALF_UP)
println("holdQtyMap[lotLine.id] ?: zero ${holdQtyMap[lotLine.id] ?: zero}")
if (availableQtyInSalesUnits <= zero) {
updatedLotLines += lotLine
return@forEachIndexed
}
println("$index : ${lotLine.id}")
// val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() }
val originalHoldQty = inventoryLotLine?.holdQty
if (availableQtyInSalesUnits <= zero) return@forEachIndexed
// 修复:在销售单位中计算分配数量
val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQtyToAllocate)
remainingQtyToAllocate = remainingQtyToAllocate.minus(assignQtyInSalesUnits)
val newHoldQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero
// 修复:将销售单位转换为基础单位来更新 holdQty
val assignQtyInBaseUnits = assignQtyInSalesUnits.multiply(ratio)
holdQtyMap[lotLine.id] = (holdQtyMap[lotLine.id] ?: zero).plus(assignQtyInBaseUnits)
suggestedList += SuggestedPickLot().apply {
type = SuggestedPickLotType.PICK_ORDER
suggestedLotLine = inventoryLotLine
pickOrderLine = line
qty = assignQtyInSalesUnits // 保存销售单位
qty = assignQtyInSalesUnits
}
}
// 修复:计算现有 suggestions 中 pending/checked 状态满足的数量
var existingSatisfiedQty = BigDecimal.ZERO

// 查询现有的 suggestions 用于这个 pick order line
val existingSuggestions = suggestedPickLotRepository.findAllByPickOrderLineId(line.id!!)
existingSuggestions.forEach { existingSugg ->
if (existingSugg.suggestedLotLine?.id != null) {
val stockOutLines = stockOutLIneRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
line.id!!, existingSugg.suggestedLotLine?.id!!
)
val canCountAsSatisfied = stockOutLines.isEmpty() || stockOutLines.any {
it.status == "pending" || it.status == "checked" || it.status == "partially_completed"
}
if (canCountAsSatisfied) {
existingSatisfiedQty = existingSatisfiedQty.plus(existingSugg.qty ?: BigDecimal.ZERO)
}
}
}

// 调整 remainingQtyToAllocate,减去已经通过现有 suggestions 满足的数量
remainingQtyToAllocate = remainingQtyToAllocate.minus(existingSatisfiedQty)
println("Existing satisfied qty: $existingSatisfiedQty")
println("Adjusted remaining qty: $remainingQtyToAllocate")

// if still have remainingQty
println("remaining2 $remainingQtyToAllocate (sales units)")

// if still have remainingQty
println("remaining2 $remainingQtyToAllocate (sales units)")
// 如果仍有剩余 -> 给 null lot(你举例的场景会走到这里)
if (remainingQtyToAllocate > zero) {
suggestedList += SuggestedPickLot().apply {
type = SuggestedPickLotType.PICK_ORDER
suggestedLotLine = null
pickOrderLine = line
qty = remainingQtyToAllocate // 保存销售单位
}
try {
/*
val pickOrder = line.pickOrder
if (pickOrder != null) {
createInsufficientStockIssue(
pickOrder = pickOrder,
pickOrderLine = line,
insufficientQty = remainingQtyToAllocate
)
}
*/
} catch (e: Exception) {
println("❌ Error creating insufficient stock issue: ${e.message}")
e.printStackTrace()
qty = remainingQtyToAllocate
}
}
}
return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList)
return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList)
}

// Convertion


Carregando…
Cancelar
Guardar