Browse Source

update

master
CANCERYS\kw093 1 month ago
parent
commit
3bc7e3c691
12 changed files with 1761 additions and 736 deletions
  1. +3
    -5
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderLineRecord.kt
  2. +510
    -304
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  3. +169
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderAssignmentService.kt
  4. +102
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderCompletionService.kt
  5. +108
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt
  6. +213
    -79
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt
  7. +184
    -43
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
  8. +12
    -5
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt
  9. +17
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt
  10. +368
    -299
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  11. +1
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt
  12. +74
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt

+ 3
- 5
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderLineRecord.kt View File

@@ -21,15 +21,13 @@ open class DoPickOrderLineRecord: BaseEntity<Long>() {
@Column(name = "do_pick_order_id", length = 100)
open var doPickOrderId: Long? = null

@JoinColumn(name = "pick_order_id")
@Column(name = "pick_order_id") // ✅ 正确:普通列
open var pickOrderId: Long? = null

@JoinColumn(name = "do_order_id")
@Column(name = "do_order_id") // ✅ 正确:普通列
open var doOrderId: Long? = null

@JoinColumn(name = "pick_order_code")
@Column(name = "pick_order_code") // ✅ 正确:普通列
open var pickOrderCode: String? = null

@Column(name = "delivery_order_code")


+ 510
- 304
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt View File

@@ -1,6 +1,7 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.core.utils.PdfUtils
import org.springframework.context.annotation.Lazy
import com.ffii.core.utils.ZebraPrinterUtil
import com.ffii.fpsms.m18.entity.M18DataLogRepository
import com.ffii.fpsms.m18.service.M18DataLogService
@@ -64,12 +65,15 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintDNLabelsRequest
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository
import com.ffii.fpsms.modules.stock.service.InventoryLotService

import net.sf.jasperreports.engine.JasperPrintManager
import net.sf.jasperreports.engine.JRPrintPage
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService // ✅ 添加这行

import com.ffii.fpsms.modules.deliveryOrder.web.models.*
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssue // ✅ 添加
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository // ✅ 添加
import com.ffii.fpsms.modules.pickOrder.entity.IssueCategory // ✅ 添加
import com.ffii.fpsms.modules.pickOrder.entity.HandleStatus
@Service
open class DeliveryOrderService(
private val deliveryOrderRepository: DeliveryOrderRepository,
@@ -80,7 +84,7 @@ open class DeliveryOrderService(
private val userService: UserService,
private val userRepository: UserRepository,
private val pickOrderService: PickOrderService,
private val doPickOrderService: DoPickOrderService,
@Lazy private val doPickOrderService: DoPickOrderService,
private val truckRepository: TruckRepository,
private val pickOrderRepository: PickOrderRepository,
private val suggestedPickLotService: SuggestedPickLotService,
@@ -95,6 +99,7 @@ open class DeliveryOrderService(
private val suggestedPickLotRepository: SuggestPickLotRepository,
private val inventoryLotRepository: InventoryLotRepository,
private val jdbcDao: JdbcDao,
private val pickExecutionIssueRepository: PickExecutionIssueRepository,
) {

open fun findByM18DataLogId(m18DataLogId: Long): DeliveryOrder? {
@@ -274,6 +279,7 @@ open class DeliveryOrderService(
open fun searchCodeAndShopName(code: String?, shopName: String?): List<DeliveryOrderInfo> {
return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndDeletedIsFalse(code, shopName);
}

open fun getWarehouseOrderByItemId(itemId: Long): Int? {
val inventoryLots = inventoryLotService.findByItemId(itemId)
if (inventoryLots.isNotEmpty()) {
@@ -297,7 +303,7 @@ open class DeliveryOrderService(
}
return null
}
// ✅ 新增方法2:获取 warehouse 的 code 字段(用于显示路由)
open fun getWarehouseCodeByItemId(itemId: Long): String? {
val inventoryLots = inventoryLotService.findByItemId(itemId)
@@ -402,12 +408,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}")
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id)
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}")
deliveryOrder.apply {
status = DeliveryOrderStatus.PENDING
}
@@ -429,57 +435,51 @@ open class DeliveryOrderService(

val createdPickOrder = pickOrderService.create(po)
println("🔍 DEBUG: Created pick order - ID: ${createdPickOrder.id}")
val consoCode = pickOrderService.assignConsoCode()
val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null)
if (pickOrderEntity != null) {
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}")
// ✅ Debug: Check 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}")
}


val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!)
println("🔍 DEBUG: Loaded ${lines.size} pick order lines from DB")
if (lines.isEmpty()) {
println("⚠️ No pick order lines found; suggestions will be empty")
}
// ✅ Create suggested pick lots and hold inventory (like normal release)
println("🔍 DEBUG: About to call suggestionForPickOrderLines for pick order ${pickOrderEntity.id}")
val suggestions = suggestedPickLotService.suggestionForPickOrderLines(
SuggestedPickLotForPolRequest(pickOrderLines = lines)
)
println("🔍 DEBUG: Got ${suggestions.suggestedList.size} suggested pick lots")
if (suggestions.suggestedList.isEmpty()) {
println("⚠️ WARNING: No suggested pick lots generated - this might be due to no inventory available")
}

val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList)
println("🔍 DEBUG: Saved ${saveSuggestedPickLots.size} suggested pick lots")
val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null }
if (insufficientCount > 0) {
println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)")
}

// ✅ Hold inventory quantities
val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(
saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id }
)
saveSuggestedPickLots.forEach { lot ->
if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) {
val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine)
if (lineIndex >= 0) {
inventoryLotLines[lineIndex].holdQty =
inventoryLotLines[lineIndex].holdQty =
(inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO)
}
}
}
inventoryLotLineRepository.saveAll(inventoryLotLines)
// ✅ Create stock out record and pre-create stock out lines
val stockOut = StockOut().apply {
this.type = "job"
@@ -488,17 +488,18 @@ open class DeliveryOrderService(
this.handler = request.userId
}
val savedStockOut = stockOutRepository.saveAndFlush(stockOut)
// ✅ Pre-create stock out lines for suggested lots
saveSuggestedPickLots.forEach { lot ->
val polId = lot.pickOrderLine?.id
val illId = lot.suggestedLotLine?.id
if (polId != null && illId != null) {
val existingLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId)
val existingLines =
stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId)
if (existingLines.isEmpty()) {
val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null)
val inventoryLotLine = inventoryLotLineRepository.findById(illId).orElse(null)
if (pickOrderLine != null && inventoryLotLine != null) {
val line = StockOutLine().apply {
this.stockOut = savedStockOut
@@ -516,332 +517,537 @@ open class DeliveryOrderService(
}

// ✅ CREATE do_pick_order_record entries
// 第 471-555 行附近 - 修复创建逻辑
// 第 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")
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")
if (trucks.size <= 1) {
// 如果只有一个或没有 truck,直接返回
return@let trucks.firstOrNull()
}
// ✅ 分析 DO order lines 中的 items 分布
val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct()
println("🔍 DEBUG: Analyzing ${itemIds.size} unique items in DO order lines")
// 使用 SQL 查询统计每个楼层的库存数量
val inventoryQuery = """
SELECT w.store_id as floor, COUNT(*) as inventory_count
FROM inventory_lot_line ill
JOIN inventory_lot il ON il.id = ill.inventoryLotId
JOIN warehouse w ON w.id = ill.warehouseId
WHERE il.itemId IN (${itemIds.joinToString(",")})
AND ill.deleted = false
AND il.deleted = false
AND w.deleted = false
AND ill.inQty > ill.outQty + COALESCE(ill.holdQty, 0)
GROUP BY w.store_id
""".trimIndent()
val inventoryResults = jdbcDao.queryForList(inventoryQuery)
val floorInventoryCount = mutableMapOf<String, Int>()
inventoryResults.forEach { row: Map<String, Any> ->
val floor = row["floor"] as? String ?: "Other"
val count = (row["inventory_count"] as? Number)?.toInt() ?: 0
floorInventoryCount[floor] = count
}
println("🔍 DEBUG: Floor inventory distribution: $floorInventoryCount")
val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
val datePrefix = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))

// 决定使用哪个楼层
val preferredFloor = when {
floorInventoryCount["2F"] ?: 0 > floorInventoryCount["4F"] ?: 0 -> "2F"
floorInventoryCount["4F"] ?: 0 > floorInventoryCount["2F"] ?: 0 -> "4F"
else -> "2F" // 默认使用 2F
}
println("🔍 DEBUG: Preferred floor based on inventory: $preferredFloor")
val preferredStoreId = when (preferredFloor) {
"2F" -> 2
"4F" -> 4
else -> 2
}
val selectedTruck = if (trucks.size > 1) {
// Multiple trucks: prefer matching preferredStoreId, then earliest departure
trucks.find { it.storeId == preferredStoreId }
?: trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
} else {
trucks.firstOrNull()
}
println("🔍 DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}, DepartureTime=${selectedTruck?.departureTime}")
selectedTruck
}
println("🔍 DEBUG: Target date: $targetDate, Date prefix: $datePrefix")
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
}

// ✅ 根据 truck 的 Store_id 字段确定 storeId
val storeId = when (truck?.storeId) {
4 -> "4/F"
2 -> "2/F"
else -> "2/F" // 默认值
}
val loadingSequence = truck?.loadingSequence ?: 999
// ✅ 每个 pick order 只创建一条 DoPickOrder
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
)

println("🔍 DEBUG: Creating DoPickOrder - Store: $storeId, Ticket: , Truck: ${truck?.id}")
val savedDoPickOrder = doPickOrderService.save(doPickOrder)
println("🔍 DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}")

return MessageResponse(
id = deliveryOrder.id,
code = deliveryOrder.code,
name = deliveryOrder.shop?.name,
type = null,
message = null,
errorPosition = null,
entity = mapOf("status" to deliveryOrder.status?.value)
)

// ... existing code ...
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
} else {
"2F" // 只要有任何 item 不在 4F,就算 2F
}

println("🔍 DEBUG: Preferred floor: $preferredFloor (All items on 4F: ${preferredFloor == "4F"})")

// ✅ 查找 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")

val preferredStoreId = when (preferredFloor) {
"2F" -> 2
"4F" -> 4
else -> 2
}

// 优先选择匹配的 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: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}")
selectedTruck
}

// ✅ 检查 truck 和 preferredFloor 是否匹配
val truckStoreId = truck?.storeId
val expectedStoreId = when (preferredFloor) {
"2F" -> 2
"4F" -> 4
else -> 2
}

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) // 或返回错误响应
}

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}")
truck
}
return MessageResponse(
id = deliveryOrder.id,
code = deliveryOrder.code,
name = deliveryOrder.shop?.name,
type = null,
message = null,
errorPosition = null,
entity = mapOf("status" to deliveryOrder.status?.value)
)
}
open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String {
try {
val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId)
val pickOrderLineIds = pickOrderLines.mapNotNull { it.id }
val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds)

val lotNumbers = suggestedPickLots
.filter { it.pickOrderLine?.item?.id == itemId }
.mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo }
.distinct()

return lotNumbers.joinToString(", ")
} catch (e: Exception) {
println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}")
return ""

open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String {
try {
val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId)
val pickOrderLineIds = pickOrderLines.mapNotNull { it.id }
val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds)

val lotNumbers = suggestedPickLots
.filter { it.pickOrderLine?.item?.id == itemId }
.mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo }
.distinct()

return lotNumbers.joinToString(", ")
} catch (e: Exception) {
println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}")
return ""
}
}
}


//Delivery Note
@Throws(IOException::class)
@Transactional
open fun exportDeliveryNote(request: ExportDeliveryNoteRequest): Map<String, Any> {
val DELIVERYNOTE_PDF = "DeliveryNote/DeliveryNotePDF.jrxml"
val resource = ClassPathResource(DELIVERYNOTE_PDF)
if(!resource.exists()){
throw FileNotFoundException("Report file not fount: $DELIVERYNOTE_PDF")
}
val inputStream = resource.inputStream
val deliveryNote = JasperCompileManager.compileReport(inputStream)
val deliveryNoteInfo = deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList()
//Delivery Note
@Throws(IOException::class)
@Transactional
open fun exportDeliveryNote(request: ExportDeliveryNoteRequest): Map<String, Any> {
val DELIVERYNOTE_PDF = "DeliveryNote/DeliveryNotePDF.jrxml"
val resource = ClassPathResource(DELIVERYNOTE_PDF)
if (!resource.exists()) {
throw FileNotFoundException("Report file not fount: $DELIVERYNOTE_PDF")
}
val inputStream = resource.inputStream
val deliveryNote = JasperCompileManager.compileReport(inputStream)
val deliveryNoteInfo =
deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList()

val fields = mutableListOf<MutableMap<String, Any>>()
val params = mutableMapOf<String, Any>()
val fields = mutableListOf<MutableMap<String, Any>>()
val params = mutableMapOf<String, Any>()

val deliveryOrderEntity = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.deliveryOrderIds)
val selectedTruckNo = deliveryOrderEntity?.shop?.id?.let { shopId ->
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
trucks.firstOrNull()?.truckLanceCode
} ?: ""
val deliveryOrderEntity = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.deliveryOrderIds)
val selectedTruckNo = deliveryOrderEntity?.shop?.id?.let { shopId ->
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
trucks.firstOrNull()?.truckLanceCode
} ?: ""

val selectedPickOrder = pickOrderRepository.findById(request.pickOrderIds).orElse(null)
val selectedPickOrder = pickOrderRepository.findById(request.pickOrderIds).orElse(null)


for (info in deliveryNoteInfo) {
val sortedLines = info.deliveryOrderLines.sortedBy { line ->
line.itemId?.let { itemId ->
getWarehouseOrderByItemId(itemId) // ✅ 改用 warehouse order
} ?: Int.MAX_VALUE
}
for (info in deliveryNoteInfo) {
val sortedLines = info.deliveryOrderLines.sortedBy { line ->
line.itemId?.let { itemId ->
getWarehouseOrderByItemId(itemId) // ✅ 改用 warehouse order
} ?: Int.MAX_VALUE
}

sortedLines.forEachIndexed { index, line ->
sortedLines.forEachIndexed { index, line ->

val field = mutableMapOf<String, Any>()
val field = mutableMapOf<String, Any>()

field["sequenceNumber"] = (index + 1).toString()
field["itemNo"] = line.itemNo
field["itemName"] = line.itemName ?:""
field["uom"] = line.uom ?:""
field["qty"] = line.qty.toString()
field["shortName"] = line.uomShortDesc ?:""
field["sequenceNumber"] = (index + 1).toString()
field["itemNo"] = line.itemNo
field["itemName"] = line.itemName ?: ""
field["uom"] = line.uom ?: ""
field["qty"] = line.qty.toString()
field["shortName"] = line.uomShortDesc ?: ""

val route = line.itemId?.let { itemId ->
getWarehouseCodeByItemId(itemId) // ✅ 使用新方法
} ?: ""
field["route"] = route
val route = line.itemId?.let { itemId ->
getWarehouseCodeByItemId(itemId) // ✅ 使用新方法
} ?: ""
field["route"] = route

val lotNo = line.itemId?.let { itemId ->
getLotNumbersForPickOrderByItemId(itemId, request.pickOrderIds)
} ?: ""
field["lotNo"] = lotNo
val lotNo = line.itemId?.let { itemId ->
getLotNumbersForPickOrderByItemId(itemId, request.pickOrderIds)
} ?: ""
field["lotNo"] = lotNo

fields.add(field)
fields.add(field)
}
}
}

if(request.isDraft){
params["dnTitle"] = "送貨單(初稿)"
params["colQty"] = "所需數量"
params["totalCartonTitle"] = ""
}
else{
params["dnTitle"] = "送貨單"
params["colQty"] = "數量"
params["totalCartonTitle"] = "總箱數:"
}
if (request.isDraft) {
params["dnTitle"] = "送貨單(初稿)"
params["colQty"] = "所需數量"
params["totalCartonTitle"] = ""
} else {
params["dnTitle"] = "送貨單"
params["colQty"] = "數量"
params["totalCartonTitle"] = "總箱數:"
}

params["numOfCarton"] = request.numOfCarton.toString()
if(params["numOfCarton"] == "0"){
params["numOfCarton"] = ""
}
params["numOfCarton"] = request.numOfCarton.toString()
if (params["numOfCarton"] == "0") {
params["numOfCarton"] = ""
}

params["deliveryOrderCode"] = deliveryNoteInfo[0].code
params["shopName"] = deliveryNoteInfo[0].shopName ?: ""
params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: ""
params["deliveryDate"] = deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
params["truckNo"] = selectedTruckNo
params["ShopPurchaseOrderNo"] = deliveryNoteInfo[0].code
params["FGPickOrderNo"] = selectedPickOrder?.code ?: ""
params["deliveryOrderCode"] = deliveryNoteInfo[0].code
params["shopName"] = deliveryNoteInfo[0].shopName ?: ""
params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: ""
params["deliveryDate"] =
deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
params["truckNo"] = selectedTruckNo
params["ShopPurchaseOrderNo"] = deliveryNoteInfo[0].code
params["FGPickOrderNo"] = selectedPickOrder?.code ?: ""


return mapOf(
"report" to PdfUtils.fillReport(deliveryNote, fields, params),
"filename" to deliveryNoteInfo[0].code
)
}
"report" to PdfUtils.fillReport(deliveryNote, fields, params),
"filename" to deliveryNoteInfo[0].code
)
}


//Print Delivery Note
@Transactional
open fun printDeliveryNote(request: PrintDeliveryNoteRequest){
//val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")

val pdf = exportDeliveryNote(
ExportDeliveryNoteRequest(
deliveryOrderIds = request.deliveryOrderId,
numOfCarton = request.numOfCarton,
isDraft = request.isDraft,
pickOrderIds = request.pickOrderId
//Print Delivery Note
@Transactional
open fun printDeliveryNote(request: PrintDeliveryNoteRequest) {
//val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")

val pdf = exportDeliveryNote(
ExportDeliveryNoteRequest(
deliveryOrderIds = request.deliveryOrderId,
numOfCarton = request.numOfCarton,
isDraft = request.isDraft,
pickOrderIds = request.pickOrderId
)
)
)

val jasperPrint = pdf["report"] as JasperPrint
val jasperPrint = pdf["report"] as JasperPrint

val tempPdfFile = File.createTempFile("print_job_",".pdf")
val tempPdfFile = File.createTempFile("print_job_", ".pdf")

try{
JasperExportManager.exportReportToPdfFile(jasperPrint,tempPdfFile.absolutePath)
try {
JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath)

//val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty
//printer.ip?.let { ip -> printer.port?.let { port ->
// ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED)
//}}
} finally {
//tempPdfFile.delete()
}

//val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty
//printer.ip?.let { ip -> printer.port?.let { port ->
// ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED)
//}}
} finally {
//tempPdfFile.delete()
}

}
//Carton Labels
open fun exportDNLabels(request: ExportDNLabelsRequest): Map<String, Any> {
val DNLABELS_PDF = "DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml"
val resource = ClassPathResource(DNLABELS_PDF)
if (!resource.exists()) {
throw FileNotFoundException("Label file not found: $DNLABELS_PDF")
}
val inputStream = resource.inputStream
val cartonLabel = JasperCompileManager.compileReport(inputStream)
val cartonLabelInfo =
deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList()
val params = mutableMapOf<String, Any>()
val fields = mutableListOf<MutableMap<String, Any>>()
for (info in cartonLabelInfo) {
val field = mutableMapOf<String, Any>()
}

//Carton Labels
open fun exportDNLabels(request: ExportDNLabelsRequest): Map<String, Any>{
val DNLABELS_PDF = "DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml"
val resource = ClassPathResource(DNLABELS_PDF)
if(!resource.exists()){
throw FileNotFoundException("Label file not found: $DNLABELS_PDF")
}
val inputStream = resource.inputStream
val cartonLabel = JasperCompileManager.compileReport(inputStream)
val cartonLabelInfo = deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList()
val params = mutableMapOf<String, Any>()
val fields = mutableListOf<MutableMap<String ,Any>>()
for (info in cartonLabelInfo) {
val field = mutableMapOf<String, Any>()
}
params["shopPurchaseOrderNo"] = cartonLabelInfo[0].code
params["deliveryOrderCode"] = cartonLabelInfo[0].code
params["shopAddress"] = cartonLabelInfo[0].shopAddress ?: ""
params["shopName"] = cartonLabelInfo[0].shopName ?: ""

params["shopPurchaseOrderNo"] = cartonLabelInfo[0].code
params["deliveryOrderCode"] = cartonLabelInfo[0].code
params["shopAddress"] = cartonLabelInfo[0].shopAddress?: ""
params["shopName"] = cartonLabelInfo[0].shopName?: ""
for (cartonNumber in 1..request.numOfCarton) {
val field = mutableMapOf<String, Any>()
fields.add(field)
}

return mapOf(
"report" to PdfUtils.fillReport(cartonLabel, fields, params),
"filename" to "${cartonLabelInfo[0].code}_carton_labels"
)

for(cartonNumber in 1..request.numOfCarton){
val field = mutableMapOf<String, Any>()
fields.add(field)
}

return mapOf(
"report" to PdfUtils.fillReport(cartonLabel, fields, params),
"filename" to "${cartonLabelInfo[0].code}_carton_labels"
)

}
//Print Carton Labels
@Transactional
open fun printDNLabels(request: PrintDNLabelsRequest) {
val printer =
printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")

val pdf = exportDNLabels(
ExportDNLabelsRequest(
deliveryOrderIds = request.deliveryOrderId,
numOfCarton = request.numOfCarton
)
)

val jasperPrint = pdf["report"] as JasperPrint

val tempPdfFile = File.createTempFile("print_job_", ".pdf")

try {
JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath)

//Print Carton Labels
@Transactional
open fun printDNLabels(request: PrintDNLabelsRequest){
val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")

val pdf = exportDNLabels(
ExportDNLabelsRequest(
deliveryOrderIds = request.deliveryOrderId,
numOfCarton = request.numOfCarton
)
)
val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty
printer.ip?.let { ip ->
printer.port?.let { port ->
ZebraPrinterUtil.printPdfToZebra(
tempPdfFile,
ip,
port,
printQty,
ZebraPrinterUtil.PrintDirection.ROTATED
)
}
}

val jasperPrint = pdf["report"] as JasperPrint
println("Test PDF saved to: ${tempPdfFile.absolutePath}")

val tempPdfFile = File.createTempFile("print_job_",".pdf")
} finally {
//tempPdfFile.delete()
}

try{
JasperExportManager.exportReportToPdfFile(jasperPrint,tempPdfFile.absolutePath)
}
@Transactional(rollbackFor = [Exception::class])
open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult {
println("🔍 DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}")

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

val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty
printer.ip?.let { ip -> printer.port?.let { port ->
ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, ZebraPrinterUtil.PrintDirection.ROTATED)
}}
// ✅ 检查状态,跳过已完成或已发布的DO
if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) {
throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release")
}

println("Test PDF saved to: ${tempPdfFile.absolutePath}")
// ✅ 更新状态为released (使用RECEIVING表示已发布)
deliveryOrder.apply {
status = DeliveryOrderStatus.RECEIVING // 使用RECEIVING表示已发布状态
}
deliveryOrderRepository.save(deliveryOrder)

// 创建 pick order
val pols = deliveryOrder.deliveryOrderLines.map {
SavePickOrderLineRequest(
itemId = it.item?.id,
qty = it.qty ?: BigDecimal.ZERO,
uomId = it.uom?.id,
)
}
val po = SavePickOrderRequest(
doId = deliveryOrder.id,
type = PickOrderType.DELIVERY_ORDER,
targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(),
pickOrderLine = pols
)

val createdPickOrder = pickOrderService.create(po)
val consoCode = pickOrderService.assignConsoCode()
val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null)

if (pickOrderEntity != null) {
pickOrderEntity.consoCode = consoCode
pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED
pickOrderRepository.saveAndFlush(pickOrderEntity)

// 创建 suggestions 和 hold inventory
val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!)
val suggestions = suggestedPickLotService.suggestionForPickOrderLines(
SuggestedPickLotForPolRequest(pickOrderLines = lines)
)
val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList)
val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null }
if (insufficientCount > 0) {
println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)")
}
val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(
saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id }
)

} finally {
//tempPdfFile.delete()
saveSuggestedPickLots.forEach { lot ->
if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) {
val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine)
if (lineIndex >= 0) {
inventoryLotLines[lineIndex].holdQty =
(inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO)
}
}
}
inventoryLotLineRepository.saveAll(inventoryLotLines)

// 创建 stock out
val stockOut = StockOut().apply {
this.type = "job"
this.consoPickOrderCode = consoCode
this.status = StockOutStatus.PENDING.status
this.handler = request.userId
}
val savedStockOut = stockOutRepository.saveAndFlush(stockOut)

saveSuggestedPickLots.forEach { lot ->
val polId = lot.pickOrderLine?.id
val illId = lot.suggestedLotLine?.id
if (polId != null && illId != null) {
val existingLines =
stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId)
if (existingLines.isEmpty()) {
val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null)
val inventoryLotLine = inventoryLotLineRepository.findById(illId).orElse(null)

if (pickOrderLine != null && inventoryLotLine != null) {
val line = StockOutLine().apply {
this.stockOut = savedStockOut
this.pickOrderLine = pickOrderLine
this.inventoryLotLine = inventoryLotLine
this.item = pickOrderLine.item
this.status = StockOutLineStatus.PENDING.status
this.qty = 0.0
}
stockOutLineRepository.save(line)
}
}
}
}
}

// 分析楼层分布
val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
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 preferredFloor = if ((floorItemCount["4F"] ?: 0) == itemIds.size && (floorItemCount["2F"] ?: 0) == 0) {
"4F"
} else {
"2F"
}

// ✅ 查找匹配 preferred floor 的 truck
val truck = deliveryOrder.shop?.id?.let { shopId ->
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
val preferredStoreId = when (preferredFloor) {
"2F" -> 2
"4F" -> 4
else -> 2
}
// 只选择 store_id 匹配的 truck
val matchedTrucks = trucks.filter { it.storeId == preferredStoreId }
if (matchedTrucks.isEmpty()) {
null // 没有匹配的 truck
} else {
matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
}
}

// ✅ 如果没有匹配的 truck,抛出异常跳过
if (truck == null) {
val errorMsg = "No matching truck found for preferredFloor ($preferredFloor). Skipping DO ${deliveryOrder.id}."
println("⚠️ $errorMsg")
throw IllegalStateException(errorMsg)
}

println("✅ DEBUG: Matched truck - ID=${truck.id}, Store=${truck.storeId}, Floor=$preferredFloor")

return ReleaseDoResult(
deliveryOrderId = deliveryOrder.id!!,
deliveryOrderCode = deliveryOrder.code,
pickOrderId = createdPickOrder.id!!,
pickOrderCode = pickOrderEntity?.code,
shopId = deliveryOrder.shop?.id,
shopCode = deliveryOrder.shop?.code,
shopName = deliveryOrder.shop?.name,
estimatedArrivalDate = targetDate,
preferredFloor = preferredFloor,
truckId = truck.id,
truckDepartureTime = truck.departureTime,
truckLanceCode = truck.truckLanceCode,
loadingSequence = truck.loadingSequence // ✅ 直接使用 truck 的值
)
}

@Transactional
open fun syncDeliveryOrderStatusFromDoPickOrder(): Int {
val sql = """
UPDATE fpsmsdb.delivery_order do
INNER JOIN fpsmsdb.do_pick_order dpo ON dpo.do_order_id = do.id
SET do.status = 'completed'
WHERE dpo.ticket_status = 'completed'
AND do.status != 'completed'
AND do.deleted = 0
AND dpo.deleted = 0
""".trimIndent()
return jdbcDao.executeUpdate(sql, emptyMap<String, Any>())
}
}
}

+ 169
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderAssignmentService.kt View File

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

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByLaneRequest
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import com.ffii.fpsms.modules.user.entity.UserRepository
import org.springframework.stereotype.Service
import java.time.LocalDateTime

@Service
class DoPickOrderAssignmentService(
private val doPickOrderRepository: DoPickOrderRepository,
private val doPickOrderLineRepository: DoPickOrderLineRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
private val pickOrderRepository: PickOrderRepository,
private val userRepository: UserRepository,
private val jdbcDao: JdbcDao // ✅ 添加 JdbcDao
) {
fun assignByLane(request: AssignByLaneRequest): MessageResponse {
val user = userRepository.findById(request.userId).orElse(null)
?: return MessageResponse(
id = null, code = "USER_NOT_FOUND", name = null, type = null,
message = "User not found", errorPosition = null, entity = null
)
// ✅ 转换 storeId 格式
val actualStoreId = when (request.storeId) {
"2/F" -> "2/F"
"4/F" -> "4/F"
else -> request.storeId
}
println("🔍 DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'")
// ✅ 获取所有候选记录
val allCandidates = doPickOrderRepository
.findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc(
actualStoreId,
DoPickOrderStatus.pending
)
.filter { it.truckLanceCode == request.truckLanceCode }
println("🔍 DEBUG: Found ${allCandidates.size} candidate do_pick_orders for lane ${request.truckLanceCode}")
// ✅ 过滤掉所有 pick orders 都是 issue 的记录
val filteredCandidates = allCandidates.filter { doPickOrder ->
val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!)
if (!hasNonIssueLines) {
println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues")
}
hasNonIssueLines
}
println("🔍 DEBUG: After filtering, ${filteredCandidates.size} do_pick_orders remain")
if (filteredCandidates.isEmpty()) {
return MessageResponse(
id = null, code = "NO_ORDERS", name = null, type = null,
message = "No available pick order(s) for this lane (all have stock issues).",
errorPosition = null, entity = null
)
}
val firstOrder = filteredCandidates.first()
// ✅ 更新 do_pick_order
firstOrder.handledBy = request.userId
firstOrder.handlerName = user.name
firstOrder.ticketStatus = DoPickOrderStatus.released
firstOrder.ticketReleaseTime = LocalDateTime.now()
doPickOrderRepository.save(firstOrder)
// ✅ 获取这个 do_pick_order 下的所有 pick orders 并分配给用户
val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(firstOrder.id!!)
println("🔍 DEBUG: Found ${doPickOrderLines.size} pick orders in do_pick_order ${firstOrder.id}")
doPickOrderLines.forEach { line ->
if (line.pickOrderId != null) {
val pickOrder = pickOrderRepository.findById(line.pickOrderId!!).orElse(null)
if (pickOrder != null) {
pickOrder.assignTo = user
pickOrder.status = PickOrderStatus.RELEASED
pickOrderRepository.save(pickOrder)
println("🔍 DEBUG: Assigned pick order ${line.pickOrderId} to user ${request.userId}")
} else {
println("⚠️ WARNING: Pick order ${line.pickOrderId} not found")
}
}
}
// ✅ 同步更新 do_pick_order_record(如果有的话)
doPickOrderLines.forEach { line ->
if (line.pickOrderId != null) {
val records = doPickOrderRecordRepository.findByPickOrderId(line.pickOrderId!!)
records.forEach { record ->
record.handledBy = request.userId
record.handlerName = user.name
record.ticketStatus = DoPickOrderStatus.released
record.ticketReleaseTime = LocalDateTime.now()
}
if (records.isNotEmpty()) {
doPickOrderRecordRepository.saveAll(records)
println("🔍 DEBUG: Updated ${records.size} do_pick_order_record for pick order ${line.pickOrderId}")
}
}
}
return MessageResponse(
id = firstOrder.id,
code = "SUCCESS",
name = null,
type = null,
message = "Assigned ${doPickOrderLines.size} pick order(s) from lane ${request.truckLanceCode}",
errorPosition = null,
entity = mapOf(
"doPickOrderId" to firstOrder.id,
"ticketNo" to firstOrder.ticketNo,
"numberOfPickOrders" to doPickOrderLines.size,
"pickOrderIds" to doPickOrderLines.mapNotNull { it.pickOrderId }
)
)
}
// ✅ 添加过滤方法(和 DoPickOrderQueryService 中相同的逻辑)
private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean {
return try {
val totalLinesSql = """
SELECT COUNT(*) as total_lines
FROM fpsmsdb.do_pick_order_line dpol
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
""".trimIndent()
val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId))
val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0
if (totalLines == 0) {
return true // 没有 lines,不算过滤
}
val nonIssueLinesSql = """
SELECT COUNT(*) as non_issue_lines
FROM fpsmsdb.do_pick_order_line dpol
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
AND (dpol.status IS NULL OR dpol.status != 'issue')
""".trimIndent()
val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId))
val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0
val hasNonIssueLines = nonIssueLines > 0
println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total lines: $totalLines, Non-issue lines: $nonIssueLines, Has non-issue lines: $hasNonIssueLines")
hasNonIssueLines
} catch (e: Exception) {
println("❌ Error checking non-issue lines for do_pick_order $doPickOrderId: ${e.message}")
true // 出错时不过滤
}
}
}

+ 102
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderCompletionService.kt View File

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

import com.ffii.fpsms.modules.deliveryOrder.entity.*
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime

@Service
open class DoPickOrderCompletionService(
private val doPickOrderRepository: DoPickOrderRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
private val doPickOrderLineRepository: DoPickOrderLineRepository,
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
private val deliveryOrderRepository: DeliveryOrderRepository
) {
@Transactional
fun completeDoPickOrdersForPickOrder(pickOrderId: Long): List<DoPickOrder> {
val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId)
doPickOrders.forEach {
it.ticketStatus = DoPickOrderStatus.completed
it.ticketCompleteDateTime = LocalDateTime.now()
}
val savedDoPickOrders = doPickOrderRepository.saveAll(doPickOrders)
// ✅ 同步更新相关的delivery_order状态
savedDoPickOrders.forEach { doPickOrder ->
if (doPickOrder.doOrderId != null) {
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doPickOrder.doOrderId!!)
if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) {
deliveryOrder.status = DeliveryOrderStatus.COMPLETED
deliveryOrderRepository.save(deliveryOrder)
println("✅ Updated delivery order ${doPickOrder.doOrderId} status to completed")
}
}
}
return savedDoPickOrders
}
@Transactional
fun removeDoPickOrdersForPickOrder(pickOrderId: Long): Int {
val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId)
var deletedCount = 0
doPickOrders.forEach { doPickOrder ->
// ✅ 第一步:复制 do_pick_order 到 do_pick_order_record
val doPickOrderRecord = DoPickOrderRecord(
storeId = doPickOrder.storeId ?: "",
ticketNo = doPickOrder.ticketNo ?: "",
ticketStatus = DoPickOrderStatus.completed,
truckId = doPickOrder.truckId,
pickOrderId = doPickOrder.pickOrderId,
truckDepartureTime = doPickOrder.truckDepartureTime,
shopId = doPickOrder.shopId,
handledBy = doPickOrder.handledBy,
handlerName = doPickOrder.handlerName,
doOrderId = doPickOrder.doOrderId,
pickOrderCode = doPickOrder.pickOrderCode,
deliveryOrderCode = doPickOrder.deliveryOrderCode,
loadingSequence = doPickOrder.loadingSequence,
ticketReleaseTime = doPickOrder.ticketReleaseTime,
ticketCompleteDateTime = LocalDateTime.now(),
truckLanceCode = doPickOrder.truckLanceCode,
shopCode = doPickOrder.shopCode,
shopName = doPickOrder.shopName,
requiredDeliveryDate = doPickOrder.requiredDeliveryDate
)
val savedRecord = doPickOrderRecordRepository.save(doPickOrderRecord)
println("✅ Copied do_pick_order ${doPickOrder.id} to do_pick_order_record")
// ✅ 第二步:复制 do_pick_order_line 到 do_pick_order_line_record
val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!)
doPickOrderLines.forEach { line ->
val doPickOrderLineRecord = DoPickOrderLineRecord()
doPickOrderLineRecord.doPickOrderId = savedRecord.id
doPickOrderLineRecord.pickOrderId = line.pickOrderId
doPickOrderLineRecord.doOrderId = line.doOrderId
doPickOrderLineRecord.pickOrderCode = line.pickOrderCode
doPickOrderLineRecord.deliveryOrderCode = line.deliveryOrderCode
doPickOrderLineRecord.status = line.status
doPickOrderLineRecordRepository.save(doPickOrderLineRecord)
println("✅ Copied do_pick_order_line ${line.id} to do_pick_order_line_record")
}
// ✅ 第三步:删除原始的 do_pick_order_line 记录
if (doPickOrderLines.isNotEmpty()) {
doPickOrderLineRepository.deleteAll(doPickOrderLines)
println("✅ Deleted ${doPickOrderLines.size} do_pick_order_line records")
}
// ✅ 第四步:删除原始的 do_pick_order 记录
doPickOrderRepository.delete(doPickOrder)
deletedCount++
println("✅ Deleted do_pick_order ${doPickOrder.id}")
}
return deletedCount
}
}

+ 108
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt View File

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

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneBtn
import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneRow
import com.ffii.fpsms.modules.deliveryOrder.web.models.StoreLaneSummary
import org.springframework.stereotype.Service
import java.time.LocalDate

@Service
class DoPickOrderQueryService(
private val doPickOrderRepository: DoPickOrderRepository,
private val jdbcDao: JdbcDao
) {
fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary {
val targetDate = requiredDate ?: LocalDate.now()
println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate")
val actualStoreId = when (storeId) {
"2/F" -> "2/F"
"4/F" -> "4/F"
else -> storeId
}
val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn(
actualStoreId,
targetDate,
listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed)
)
println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate")
val filteredRecords = allRecords.filter { doPickOrder ->
val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!)
if (!hasNonIssueLines) {
println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues")
}
hasNonIssueLines
}
println("🔍 DEBUG: After filtering, ${filteredRecords.size} records remain")
val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode }
.mapValues { (_, list) ->
LaneBtn(
truckLanceCode = list.first().truckLanceCode ?: "",
unassigned = list.count { it.handledBy == null },
total = list.size
)
}
val timeGroups = grouped.entries
.groupBy { it.key.first }
.mapValues { (_, entries) ->
entries.map { it.value }
.sortedByDescending { it.unassigned }
.take(3)
}
.filterValues { lanes -> lanes.any { it.unassigned > 0 } }
.toSortedMap(compareBy { it })
.entries.take(4)
.map { (time, lanes) ->
LaneRow(
truckDepartureTime = time?.toString() ?: "",
lanes = lanes
)
}
return StoreLaneSummary(storeId = storeId, rows = timeGroups)
}
private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean {
return try {
val totalLinesSql = """
SELECT COUNT(*) as total_lines
FROM fpsmsdb.do_pick_order_line dpol
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
""".trimIndent()
val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId))
val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0
if (totalLines == 0) {
return true
}
val nonIssueLinesSql = """
SELECT COUNT(*) as non_issue_lines
FROM fpsmsdb.do_pick_order_line dpol
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
AND (dpol.status IS NULL OR dpol.status != 'issue')
""".trimIndent()
val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId))
val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0
nonIssueLines > 0
} catch (e: Exception) {
println("❌ Error checking non-issue lines: ${e.message}")
true
}
}
}

+ 213
- 79
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt View File

@@ -3,7 +3,6 @@ package com.ffii.fpsms.modules.deliveryOrder.service
import com.ffii.fpsms.m18.entity.M18DataLogRepository
import com.ffii.fpsms.m18.service.M18DataLogService
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrder
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.models.DeliveryOrderInfo
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.web.models.SaveDeliveryOrderRequest
@@ -42,15 +41,22 @@ import java.time.LocalDate
import java.time.format.DateTimeFormatter
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.pickOrder.entity.Truck
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord
import org.springframework.context.annotation.Lazy
@Service
class DoPickOrderService(
open class DoPickOrderService(
private val doPickOrderRepository: DoPickOrderRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
private val userRepository: UserRepository,
private val pickOrderRepository: PickOrderRepository,
private val jdbcDao: JdbcDao, // ✅ 添加这行
private val truckRepository: TruckRepository

private val truckRepository: TruckRepository,
private val doPickOrderLineRepository: DoPickOrderLineRepository,
@Lazy private val deliveryOrderRepository: DeliveryOrderRepository,
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository
) {
fun findReleasedDoPickOrders(): List<DoPickOrder> {
return doPickOrderRepository.findByTicketStatusIn(
@@ -141,20 +147,86 @@ class DoPickOrderService(
val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId)
doPickOrders.forEach {
it.ticketStatus = DoPickOrderStatus.completed
it.ticketCompleteDateTime = LocalDateTime.now() // ✅ 设置完成时间
it.ticketCompleteDateTime = LocalDateTime.now()
}
return doPickOrderRepository.saveAll(doPickOrders)
val savedDoPickOrders = doPickOrderRepository.saveAll(doPickOrders)
// ✅ 同步更新相关的delivery_order状态
savedDoPickOrders.forEach { doPickOrder ->
if (doPickOrder.doOrderId != null) {
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doPickOrder.doOrderId!!)
if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) {
deliveryOrder.status = DeliveryOrderStatus.COMPLETED
deliveryOrderRepository.save(deliveryOrder)
println("✅ Updated delivery order ${doPickOrder.doOrderId} status to completed")
}
}
}
return savedDoPickOrders
}

// ✅ New method to remove do_pick_order records when auto-assigning by store
// ✅ 修改方法:先复制记录到record表,再删除原记录
@Transactional
fun removeDoPickOrdersForPickOrder(pickOrderId: Long): Int {
val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId)
if (doPickOrders.isNotEmpty()) {
// ✅ 物理删除记录
doPickOrderRepository.deleteAll(doPickOrders)
return doPickOrders.size
var deletedCount = 0
doPickOrders.forEach { doPickOrder ->
// ✅ 第一步:复制 do_pick_order 到 do_pick_order_record
val doPickOrderRecord = DoPickOrderRecord(
storeId = doPickOrder.storeId?: "",
ticketNo = doPickOrder.ticketNo?: "",
ticketStatus = DoPickOrderStatus.completed, // 设置为completed状态
truckId = doPickOrder.truckId,
pickOrderId = doPickOrder.pickOrderId,
truckDepartureTime = doPickOrder.truckDepartureTime,
shopId = doPickOrder.shopId,
handledBy = doPickOrder.handledBy,
handlerName = doPickOrder.handlerName,
doOrderId = doPickOrder.doOrderId,
pickOrderCode = doPickOrder.pickOrderCode,
deliveryOrderCode = doPickOrder.deliveryOrderCode,
loadingSequence = doPickOrder.loadingSequence,
ticketReleaseTime = doPickOrder.ticketReleaseTime,
ticketCompleteDateTime = LocalDateTime.now(), // 设置完成时间
truckLanceCode = doPickOrder.truckLanceCode,
shopCode = doPickOrder.shopCode,
shopName = doPickOrder.shopName,
requiredDeliveryDate = doPickOrder.requiredDeliveryDate
)
val savedRecord = doPickOrderRecordRepository.save(doPickOrderRecord)
println("✅ Copied do_pick_order ${doPickOrder.id} to do_pick_order_record")
// ✅ 第二步:复制 do_pick_order_line 到 do_pick_order_line_record
// ✅ 第二步:复制 do_pick_order_line 到 do_pick_order_line_record
val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!)
doPickOrderLines.forEach { line ->
val doPickOrderLineRecord = DoPickOrderLineRecord() // ✅ 使用默认构造函数
doPickOrderLineRecord.doPickOrderId = savedRecord.id // ✅ 设置属性
doPickOrderLineRecord.pickOrderId = line.pickOrderId
doPickOrderLineRecord.doOrderId = line.doOrderId
doPickOrderLineRecord.pickOrderCode = line.pickOrderCode
doPickOrderLineRecord.deliveryOrderCode = line.deliveryOrderCode
doPickOrderLineRecord.status = line.status
doPickOrderLineRecordRepository.save(doPickOrderLineRecord)
println("✅ Copied do_pick_order_line ${line.id} to do_pick_order_line_record")
}
// ✅ 第三步:删除原始的 do_pick_order_line 记录
if (doPickOrderLines.isNotEmpty()) {
doPickOrderLineRepository.deleteAll(doPickOrderLines)
println("✅ Deleted ${doPickOrderLines.size} do_pick_order_line records")
}
// ✅ 第四步:删除原始的 do_pick_order 记录
doPickOrderRepository.delete(doPickOrder)
deletedCount++
println("✅ Deleted do_pick_order ${doPickOrder.id}")
}
return 0
return deletedCount
}

fun saveRecord(record: DoPickOrderRecord): DoPickOrderRecord {
@@ -197,31 +269,48 @@ class DoPickOrderService(
}
return doPickOrderRepository.saveAll(doPickOrders)
}

fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary {
// ✅ 使用传入的日期,如果没有传入则使用今天
val targetDate = requiredDate ?: LocalDate.now()

println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate")

// ✅ 修改:按日期查询订单
// ✅ 修复格式转换:保持原始格式
val actualStoreId = when (storeId) {
"2/F" -> "2/F" // ✅ 保持原格式
"4/F" -> "4/F" // ✅ 保持原格式
else -> storeId
}
println("🔍 DEBUG: Using storeId: '$actualStoreId'")
// ✅ 直接查询 do_pick_order 表
val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn(
storeId,
actualStoreId,
targetDate,
listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed)
)

println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate")

val grouped = allRecords.groupBy { it.truckDepartureTime to it.truckLanceCode }
// ✅ 过滤掉所有 do_pick_order_line 都是 "issue" 状态的 shop
val filteredRecords = allRecords.filter { doPickOrder ->
val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!)
if (!hasNonIssueLines) {
println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues")
}
hasNonIssueLines
}
println("🔍 DEBUG: After filtering, ${filteredRecords.size} records remain")
// ✅ 使用过滤后的记录
val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode }
.mapValues { (_, list) ->
LaneBtn(
truckLanceCode = list.first().truckLanceCode ?: "",
unassigned = list.count { it.handledBy == null }, // 未分配的订单数
total = list.size // 总订单数(包括已分配和未分配)
unassigned = list.count { it.handledBy == null },
total = list.size
)
}

val timeGroups = grouped.entries
.groupBy { it.key.first }
.mapValues { (_, entries) ->
@@ -238,91 +327,136 @@ class DoPickOrderService(
lanes = lanes
)
}
return StoreLaneSummary(storeId = storeId, rows = timeGroups)
}

private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean {
try {
// 1. 获取该 do_pick_order 的所有 do_pick_order_line 数量
val totalLinesSql = """
SELECT COUNT(*) as total_lines
FROM fpsmsdb.do_pick_order_line dpol
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
""".trimIndent()
val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId))
val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0
if (totalLines == 0) {
return true // 没有 lines,不算过滤
}
// 2. 获取非 "issue" 状态的 lines 数量
val nonIssueLinesSql = """
SELECT COUNT(*) as non_issue_lines
FROM fpsmsdb.do_pick_order_line dpol
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
AND (dpol.status IS NULL OR dpol.status != 'issue')
""".trimIndent()
val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId))
val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0
// 3. 只有当所有 lines 都是 "issue" 状态时才过滤掉
val hasNonIssueLines = nonIssueLines > 0
println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total lines: $totalLines, Non-issue lines: $nonIssueLines, Has non-issue lines: $hasNonIssueLines")
return hasNonIssueLines
} catch (e: Exception) {
println("❌ Error checking non-issue lines for do_pick_order $doPickOrderId: ${e.message}")
return true // 出错时不过滤
}
}
// ✅ 修复:把 assignByLane 移到类里面
fun assignByLane(request: AssignByLaneRequest): MessageResponse {
val existingOrders = doPickOrderRepository.findByHandledByAndTicketStatusIn(
request.userId,
listOf(DoPickOrderStatus.released, DoPickOrderStatus.pending)
)

if (existingOrders.isNotEmpty()) {
return MessageResponse(
id = null, code = "USER_BUSY", name = null, type = null,
message = "User already has an active pick order. Please complete it first.",
errorPosition = null, entity = null
val user = userRepository.findById(request.userId).orElse(null)
?: return MessageResponse(
id = null, code = "USER_NOT_FOUND", name = null, type = null,
message = "User not found", errorPosition = null, entity = null
)
// ✅ 转换 storeId 格式:'2/F' -> '2F/F', '4/F' -> '4F/F'
val actualStoreId = when (request.storeId) {
"2/F" -> "2/F" // ✅ 保持原格式
"4/F" -> "4/F" // ✅ 保持原格式
else -> request.storeId
}

println("🔍 DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'")
val candidates = doPickOrderRepository
.findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc(
request.storeId,
actualStoreId,
DoPickOrderStatus.pending
)
.filter {
it.handledBy == null &&
it.truckLanceCode == request.truckLanceCode &&
(request.truckDepartureTime == null ||
it.truckDepartureTime?.toString() == request.truckDepartureTime)
}

.filter { it.truckLanceCode == request.truckLanceCode }
if (candidates.isEmpty()) {
return MessageResponse(
id = null, code = "NO_ORDERS", name = null, type = null,
message = "No available orders for lane ${request.truckLanceCode}",
errorPosition = null, entity = null
message = "No available pick order(s) for this lane.", errorPosition = null, entity = null
)
}

val firstOrder = candidates.first()
val user = userRepository.findById(request.userId).orElse(null)
val handlerName = user?.name ?: "Unknown"

// ✅ 更新 do_pick_order - 保持原有的卡车信息
// ✅ 更新 do_pick_order
firstOrder.handledBy = request.userId
firstOrder.handlerName = handlerName
firstOrder.handlerName = user.name
firstOrder.ticketStatus = DoPickOrderStatus.released
firstOrder.ticketReleaseTime = LocalDateTime.now()

// ✅ 重要:不要修改 truckDepartureTime 和 truckLanceCode
// 这些信息应该保持用户选择的值

doPickOrderRepository.save(firstOrder)

// ✅ 同步更新 pick_order 表
if (firstOrder.pickOrderId != null) {
val pickOrder = pickOrderRepository.findById(firstOrder.pickOrderId!!).orElse(null)
if (pickOrder != null) {
val user = userRepository.findById(request.userId).orElse(null)
pickOrder.assignTo = user
pickOrder.status = PickOrderStatus.RELEASED
pickOrderRepository.save(pickOrder)
// ✅ 关键修改:获取这个 do_pick_order 下的所有 pick orders 并分配给用户
val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(firstOrder.id!!)
println("🔍 DEBUG: Found ${doPickOrderLines.size} pick orders in do_pick_order ${firstOrder.id}")
doPickOrderLines.forEach { line ->
if (line.pickOrderId != null) {
val pickOrder = pickOrderRepository.findById(line.pickOrderId!!).orElse(null)
if (pickOrder != null) {
pickOrder.assignTo = user
pickOrder.status = PickOrderStatus.RELEASED
pickOrderRepository.save(pickOrder)
println("🔍 DEBUG: Assigned pick order ${line.pickOrderId} to user ${request.userId}")
} else {
println("⚠️ WARNING: Pick order ${line.pickOrderId} not found")
}
}
}

// 同步更新 record
val records = doPickOrderRecordRepository.findByPickOrderId(firstOrder.pickOrderId!!)
records.forEach {
it.handledBy = request.userId
it.handlerName = handlerName
it.ticketStatus = DoPickOrderStatus.released
it.ticketReleaseTime = LocalDateTime.now()
// ✅ 同步更新 do_pick_order_record(如果有的话)
doPickOrderLines.forEach { line ->
if (line.pickOrderId != null) {
val records = doPickOrderRecordRepository.findByPickOrderId(line.pickOrderId!!)
records.forEach { record ->
record.handledBy = request.userId
record.handlerName = user.name
record.ticketStatus = DoPickOrderStatus.released
record.ticketReleaseTime = LocalDateTime.now()
}
if (records.isNotEmpty()) {
doPickOrderRecordRepository.saveAll(records)
println("🔍 DEBUG: Updated ${records.size} do_pick_order_record for pick order ${line.pickOrderId}")
}
}
}
doPickOrderRecordRepository.saveAll(records)

return MessageResponse(
id = firstOrder.pickOrderId,
id = firstOrder.id,
code = "SUCCESS",
name = null,
type = null,
message = "Assigned pick order from lane ${request.truckLanceCode}",
message = "Assigned ${doPickOrderLines.size} pick order(s) from lane ${request.truckLanceCode}",
errorPosition = null,
entity = mapOf(
"pickOrderId" to firstOrder.pickOrderId,
"ticketNo" to firstOrder.ticketNo
"doPickOrderId" to firstOrder.id,
"ticketNo" to firstOrder.ticketNo,
"numberOfPickOrders" to doPickOrderLines.size,
"pickOrderIds" to doPickOrderLines.mapNotNull { it.pickOrderId }
)
)
}


+ 184
- 43
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt View File

@@ -10,7 +10,14 @@ import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.min
import com.ffii.core.support.JdbcDao

import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoResult
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrder
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLine
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
data class BatchReleaseJobStatus(
val jobId: String,
val total: Int,
@@ -24,7 +31,10 @@ data class BatchReleaseJobStatus(
@Service
class DoReleaseCoordinatorService(
private val deliveryOrderService: DeliveryOrderService,
private val jdbcDao: JdbcDao
private val jdbcDao: JdbcDao,
private val doPickOrderLineRepository: DoPickOrderLineRepository,
private val deliveryOrderRepository: DeliveryOrderRepository,
private val doPickOrderRepository: DoPickOrderRepository
) {
private val poolSize = Runtime.getRuntime().availableProcessors()
private val executor = Executors.newFixedThreadPool(min(poolSize, 4))
@@ -135,7 +145,13 @@ class DoReleaseCoordinatorService(
) AS loading_sequence
FROM fpsmsdb.delivery_order do
LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id
WHERE do.deleted = 0
WHERE do.id IN (
SELECT DISTINCT do_order_id
FROM fpsmsdb.do_pick_order
WHERE ticket_no LIKE 'TEMP-%'
AND deleted = 0
)
AND do.deleted = 0
)
SELECT
dpo2.id,
@@ -224,7 +240,7 @@ class DoReleaseCoordinatorService(
FROM DoFloorSummary
),
TruckSelection AS (
SELECT
SELECT
do.id AS delivery_order_id,
do.shopId,
do.estimatedArrivalDate,
@@ -296,7 +312,7 @@ class DoReleaseCoordinatorService(
) AS loading_sequence
FROM fpsmsdb.delivery_order do
LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id
WHERE do.id IN (${ids.joinToString(",")})
WHERE do.id IN (${ids.joinToString(",")})
AND do.deleted = 0
)
SELECT delivery_order_id AS id
@@ -314,8 +330,8 @@ class DoReleaseCoordinatorService(
println("🔍 DEBUG: SQL length: ${sql.length} characters") // ✅ 添加这行
println("🔍 DEBUG: SQL first 500 chars: ${sql.take(500)}") // ✅ 添加这行
val results = jdbcDao.queryForList(sql)
val results = jdbcDao.queryForList(sql)
println("🔍 DEBUG: Results type: ${results.javaClass.name}") // ✅ 添加这行
println("🔍 DEBUG: Results size: ${results.size}") // ✅ 添加这行
@@ -337,13 +353,13 @@ class DoReleaseCoordinatorService(
} else {
sortedIds
}
} catch (e: Exception) {
} catch (e: Exception) {
println("❌ ERROR: ${e.message}")
println("❌ ERROR Stack Trace:") // ✅ 添加这行
e.printStackTrace()
return ids
e.printStackTrace()
return ids
}
}
}
fun startBatchReleaseAsync(ids: List<Long>, userId: Long): MessageResponse {
if (ids.isEmpty()) {
return MessageResponse(id = null, code = "NO_IDS", name = null, type = null,
@@ -356,55 +372,72 @@ class DoReleaseCoordinatorService(
executor.submit {
try {
println("📦 Starting serial batch release for ${ids.size} orders")
val sortedIds = getOrderedDeliveryOrderIds(ids) // ✅ 使用本地方法
println("📦 Starting batch release for ${ids.size} orders")
val sortedIds = getOrderedDeliveryOrderIds(ids)
println("🔍 DEBUG: Got ${sortedIds.size} sorted orders")
sortedIds.forEachIndexed { index, id ->
val releaseResults = mutableListOf<ReleaseDoResult>()
// 第一步:发布所有 DO(创建 pick orders,但不创建 DoPickOrder)
sortedIds.forEach { id ->
try {
val res = deliveryOrderService.releaseDeliveryOrder(
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(id)
if (deliveryOrder?.status == DeliveryOrderStatus.COMPLETED ||
deliveryOrder?.status == DeliveryOrderStatus.RECEIVING) {
println("⏭️ DO $id is already ${deliveryOrder.status?.value}, skipping")
return@forEach
}
val result = deliveryOrderService.releaseDeliveryOrderWithoutTicket(
ReleaseDoRequest(id = id, userId = userId)
)
val code = res.code ?: "OK"
println("🔍 DO $id -> code='$code', msg='${res.message}'")
val isSuccess = code in setOf("SUCCESS", "OK", "PARTIAL_SUCCESS") ||
code.matches(Regex("TO[A-Z]{2}\\d{2}PO\\d+")) ||
(res.message == null && code.isNotEmpty())
if (isSuccess) {
status.success.incrementAndGet()
} else {
synchronized(status.failed) {
status.failed.add(id to "Code: $code, Msg: ${res.message}")
}
println("⚠️ DO $id marked as failed: code='$code'")
}
if ((index + 1) % 50 == 0) {
println("📊 Progress: ${index + 1}/${sortedIds.size} (Success: ${status.success.get()}, Failed: ${status.failed.size})")
}
releaseResults.add(result)
status.success.incrementAndGet()
println("🔍 DO $id -> Success")
} catch (e: Exception) {
synchronized(status.failed) {
status.failed.add(id to (e.message ?: "Exception"))
}
println("❌ DO $id exception: ${e.javaClass.simpleName} - ${e.message}")
println("❌ DO $id skipped: ${e.message}")
}
}
// 第二步:按日期、楼层、店铺分组(与 SQL 逻辑一致)
val sortedResults = releaseResults.sortedWith(compareBy(
{ it.estimatedArrivalDate },
{ it.preferredFloor },
{ it.truckDepartureTime },
{ it.truckLanceCode },
{ it.loadingSequence },
{ it.shopId }
))

// ✅ 然后按正确的顺序分组(保持排序后的顺序)
val grouped = sortedResults.groupBy {
Triple(it.estimatedArrivalDate, it.preferredFloor, it.shopId)
}

println("🔍 DEBUG: Grouped into ${grouped.size} DoPickOrders")
// 第三步:为每组创建一个 DoPickOrder 和多条 DoPickOrderLine
grouped.forEach { (key, group) ->
try {
createMergedDoPickOrder(group)
println("🔍 DEBUG: Created DoPickOrder for ${group.size} DOs")
} catch (e: Exception) {
println("❌ Error creating DoPickOrder: ${e.message}")
e.printStackTrace()
}
}
// 第四步:更新 ticket numbers
if (status.success.get() > 0) {
println("🎫 Updating ticket numbers...")
updateBatchTicketNumbers()
}
println("✅ Batch completed: ${status.success.get()} success, ${status.failed.size} failed")
if (status.failed.isNotEmpty()) {
println("🔴 Failed examples:")
status.failed.take(10).forEach { (id, msg) ->
println(" DO $id: $msg")
}
}
} catch (e: Exception) {
println("❌ Batch release exception: ${e.javaClass.simpleName} - ${e.message}")
e.printStackTrace()
@@ -416,10 +449,118 @@ class DoReleaseCoordinatorService(
return MessageResponse(
id = null, code = "STARTED", name = null, type = null,
message = "Batch release started (serial mode)", errorPosition = null,
message = "Batch release started", errorPosition = null,
entity = mapOf("jobId" to jobId, "total" to ids.size)
)
}
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
)
// ✅ 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save()
val saved = doPickOrderRepository.save(doPickOrder)
println("🔍 DEBUG: Saved DoPickOrder - ID: ${saved.id}, Ticket: ${saved.ticketNo}")
// 创建多条 DoPickOrderLine(每个 DO 一条)
results.forEach { result ->
val existingLines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(result.pickOrderId)
if (existingLines.isNotEmpty()) {
println("⚠️ WARNING: pick_order ${result.pickOrderId} already in do_pick_order_line, skipping")
return@forEach // 跳过这个
}
// ✅ 先创建 DoPickOrderLine,然后检查库存问题
val line = DoPickOrderLine().apply {
doPickOrderId = saved.id
pickOrderId = result.pickOrderId
doOrderId = result.deliveryOrderId
pickOrderCode = result.pickOrderCode
deliveryOrderCode = result.deliveryOrderCode
status = "pending" // 初始状态
}
doPickOrderLineRepository.save(line)
println("🔍 DEBUG: Created DoPickOrderLine for pick order ${result.pickOrderId}")
}
// ✅ 现在检查整个 DoPickOrder 是否有库存问题
val hasStockIssues = checkPickOrderHasStockIssues(saved.id!!)
if (hasStockIssues) {
// 更新所有相关的 DoPickOrderLine 状态为 "issue"
val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(saved.id!!)
doPickOrderLines.forEach { line ->
line.status = "issue"
}
doPickOrderLineRepository.saveAll(doPickOrderLines)
println("🔍 DEBUG: Updated ${doPickOrderLines.size} DoPickOrderLine records to 'issue' status")
}
println("🔍 DEBUG: Created ${results.size} DoPickOrderLine records")
}
private fun checkPickOrderHasStockIssues(doPickOrderId: Long): Boolean {
try {
// 1. 获取 do_pick_order 的所有 pick orders 数量
val totalPickOrdersSql = """
SELECT COUNT(*) as total_pick_orders
FROM fpsmsdb.do_pick_order_line dpol
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
""".trimIndent()
val totalPickOrdersResult = jdbcDao.queryForList(totalPickOrdersSql, mapOf("doPickOrderId" to doPickOrderId))
val totalPickOrders = (totalPickOrdersResult.firstOrNull()?.get("total_pick_orders") as? Number)?.toInt() ?: 0
if (totalPickOrders == 0) {
return false // 没有 pick orders,不算 issue
}
// 2. 获取有库存问题的 pick orders 数量(通过检查 pick_execution_issue 表)
val issuePickOrdersSql = """
SELECT COUNT(DISTINCT dpol.pick_order_id) as issue_pick_orders
FROM fpsmsdb.do_pick_order_line dpol
INNER JOIN fpsmsdb.pick_execution_issue pei ON pei.pick_order_id = dpol.pick_order_id
WHERE dpol.do_pick_order_id = :doPickOrderId
AND pei.deleted = 0
AND dpol.deleted = 0
""".trimIndent()
val issuePickOrdersResult = jdbcDao.queryForList(issuePickOrdersSql, mapOf("doPickOrderId" to doPickOrderId))
val issuePickOrders = (issuePickOrdersResult.firstOrNull()?.get("issue_pick_orders") as? Number)?.toInt() ?: 0
// 3. 只有当所有 pick orders 都有问题时才算 issue
val hasAllPickOrdersIssues = (totalPickOrders > 0) && (issuePickOrders == totalPickOrders)
println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total pick orders: $totalPickOrders, Issue pick orders: $issuePickOrders, All pick orders have issues: $hasAllPickOrdersIssues")
return hasAllPickOrdersIssues
} catch (e: Exception) {
println("❌ Error checking stock issues for do pick order $doPickOrderId: ${e.message}")
return false
}
}
fun getBatchReleaseProgress(jobId: String): MessageResponse {
val s = jobs[jobId] ?: return MessageResponse(
id = null, code = "NOT_FOUND", name = null, type = null,


+ 12
- 5
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt View File

@@ -39,12 +39,18 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByStoreRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.*
import org.springframework.format.annotation.DateTimeFormat
import com.ffii.fpsms.modules.deliveryOrder.service.DoReleaseCoordinatorService
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderQueryService
import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderAssignmentService
@RestController
@RequestMapping("/doPickOrder")
class DoPickOrderController(
private val doPickOrderService: DoPickOrderService,
private val doPickOrderRepository: DoPickOrderRepository,
private val doReleaseCoordinatorService: DoReleaseCoordinatorService
private val doReleaseCoordinatorService: DoReleaseCoordinatorService,
private val doPickOrderLineRepository: DoPickOrderLineRepository,
private val doPickOrderQueryService: DoPickOrderQueryService,
private val doPickOrderAssignmentService: DoPickOrderAssignmentService
) {
@PostMapping("/assign-by-store")
fun assignPickOrderByStore(@RequestBody request: AssignByStoreRequest): MessageResponse {
@@ -65,12 +71,13 @@ class DoPickOrderController(
@RequestParam storeId: String,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?
): StoreLaneSummary {
return doPickOrderService.getSummaryByStore(storeId, requiredDate)
return doPickOrderQueryService.getSummaryByStore(storeId, requiredDate)
}
@PostMapping("/assign-by-lane")
fun assignByLane(@RequestBody request: AssignByLaneRequest): MessageResponse {
return doPickOrderService.assignByLane(request)
}
fun assignByLane(@RequestBody request: AssignByLaneRequest): MessageResponse {
return doPickOrderAssignmentService.assignByLane(request) // ✅ 使用新的 Service
}
@PostMapping("/batch-release/async")
fun startBatchReleaseAsync(
@RequestBody ids: List<Long>,


+ 17
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt View File

@@ -1,6 +1,23 @@
package com.ffii.fpsms.modules.deliveryOrder.web.models
import java.time.LocalDate
import java.time.LocalTime

data class ReleaseDoRequest(
val id: Long,
val userId: Long
)
data class ReleaseDoResult(
val deliveryOrderId: Long,
val deliveryOrderCode: String?,
val pickOrderId: Long,
val pickOrderCode: String?,
val shopId: Long?,
val shopCode: String?,
val shopName: String?,
val estimatedArrivalDate: LocalDate?,
val preferredFloor: String,
val truckId: Long?,
val truckDepartureTime: LocalTime?,
val truckLanceCode: String?,
val loadingSequence: Int?
)

+ 368
- 299
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt View File

@@ -60,7 +60,11 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository
import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository
import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus

import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLine
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
@Service
open class PickOrderService(
private val jdbcDao: JdbcDao,
@@ -81,12 +85,13 @@ open class PickOrderService(
private val deliveryOrderRepository: DeliveryOrderRepository,
private val truckRepository: TruckRepository,
private val doPickOrderService: DoPickOrderService,
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
private val doPickOrderRepository: DoPickOrderRepository,
private val userRepository: UserRepository,
private val joPickOrderRepository: JoPickOrderRepository, // ✅ 添加这行
private val joPickOrderRecordRepository: JoPickOrderRecordRepository,
private val doPickOrderLineRepository: DoPickOrderLineRepository,

) : AbstractBaseEntityService<PickOrder, Long, PickOrderRepository>(jdbcDao, pickOrderRepository) {
@@ -1220,13 +1225,87 @@ logger.info("Precreated $precreated stock out lines for suggested lots on releas
pickOrderRepository.save(pickOrder)
println("✅ Updated pick order ${pickOrder.code} to COMPLETED status")
val removedCount = doPickOrderService.removeDoPickOrdersForPickOrder(pickOrderId)
println("✅ Removed $removedCount do_pick_order records for completed pick order ${pickOrderId}")
// ✅ Update do_pick_order_record status to completed (don't remove)
doPickOrderService.completeDoPickOrderRecordsForPickOrder(pickOrderId)
println("✅ Updated do_pick_order_record status to COMPLETED for pick order ${pickOrderId}")
// ✅ 修改:通过 do_pick_order_line 查询(因为 do_pick_order.pick_order_id 可能为 null)
val doPickOrderLines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(pickOrderId)
val doPickOrderIds = doPickOrderLines.mapNotNull { it.doPickOrderId }.distinct()

println("🔍 DEBUG: Found ${doPickOrderLines.size} do_pick_order_line records for pick order $pickOrderId")
println("🔍 DEBUG: Unique do_pick_order IDs: $doPickOrderIds")

if (doPickOrderIds.isEmpty()) {
println("ℹ️ INFO: No do_pick_order records found - skipping record copying")
} else {
var copied = 0
var deleted = 0

doPickOrderIds.forEach { doPickOrderId ->
val dpo = doPickOrderRepository.findById(doPickOrderId).orElse(null)
if (dpo == null) {
println("⚠️ WARNING: do_pick_order $doPickOrderId not found, skipping")
return@forEach
}
println("🔍 Processing do_pick_order ID: ${dpo.id}, ticket: ${dpo.ticketNo}")
// 2) 先复制 do_pick_order -> do_pick_order_record
val dpoRecord = DoPickOrderRecord(
storeId = dpo.storeId ?: "",
ticketNo = dpo.ticketNo ?: "",
ticketStatus = DoPickOrderStatus.completed,
truckId = dpo.truckId,

truckDepartureTime = dpo.truckDepartureTime,
shopId = dpo.shopId,
handledBy = dpo.handledBy,
handlerName = dpo.handlerName,
doOrderId = dpo.doOrderId,
pickOrderCode = dpo.pickOrderCode,
deliveryOrderCode = dpo.deliveryOrderCode,
loadingSequence = dpo.loadingSequence,
ticketReleaseTime = dpo.ticketReleaseTime,
ticketCompleteDateTime = java.time.LocalDateTime.now(),
truckLanceCode = dpo.truckLanceCode,
shopCode = dpo.shopCode,
shopName = dpo.shopName,
requiredDeliveryDate = dpo.requiredDeliveryDate
)
val savedHeader = doPickOrderRecordRepository.save(dpoRecord)

// 3) 复制行 do_pick_order_line -> do_pick_order_line_record
val lines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(dpo.id!!)
val lineRecords = lines.map { l: DoPickOrderLine ->
DoPickOrderLineRecord().apply {
this.doPickOrderId = savedHeader.id
this.pickOrderId = l.pickOrderId
this.doOrderId = l.doOrderId
this.pickOrderCode = l.pickOrderCode
this.deliveryOrderCode = l.deliveryOrderCode
this.status = l.status
}
}
if (lineRecords.isNotEmpty()) {
doPickOrderLineRecordRepository.saveAll(lineRecords)
}
copied++

// 4) 删除原行、原表
if (lines.isNotEmpty()) doPickOrderLineRepository.deleteAll(lines)
doPickOrderRepository.delete(dpo)
deleted++

// 5) 同步更新 delivery_order 状态为 completed
dpo.doOrderId?.let { doId ->
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId)
if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) {
deliveryOrder.status = DeliveryOrderStatus.COMPLETED
deliveryOrderRepository.save(deliveryOrder)
println("✅ Updated delivery order $doId to COMPLETED")
}
}
}

println("✅ Copied $copied do_pick_order to record and deleted $deleted original(s)")
}
// ✅ 添加:直接使用 Repository 处理 JO pick order(避免循环依赖)
if (pickOrder.jobOrder != null) {
val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrderId)
@@ -2771,38 +2850,52 @@ open fun autoAssignAndReleasePickOrderByStoreAndTicket(storeId: String, ticketNo
try {
println("🔍 Starting getFgPickOrdersByUserId with userId: $userId")
// ✅ 完全修复:使用 shop 表的正确字段
// ✅ 修复:从 do_pick_order_line 获取 pick order 信息
val sql = """
SELECT
dpo.id as doPickOrderId,
dpo.pick_order_id as pickOrderId,
dpo.do_order_id as deliveryOrderId,
dpo.store_id as storeId,
dpo.ticket_no as ticketNo,
dpo.TruckLanceCode as truckLanceCode,
dpo.truck_departure_time as DepartureTime,
dpo.ShopCode as shopCode,
dpo.ShopName as shopName,
dpo.delivery_order_code as deliveryNo,
dpo.pick_order_code as pickOrderCode,
po.consoCode as pickOrderConsoCode,
po.targetDate as pickOrderTargetDate,
po.status as pickOrderStatus,
do.orderDate as deliveryDate,
s.id as shopId,
s.name as shopNameFromShop,
CONCAT_WS(', ', s.addr1, s.addr2, s.addr3, s.addr4, s.district) as shopAddress,
(SELECT COUNT(*) FROM pick_order_line pol WHERE pol.poId = po.id AND pol.deleted = false) as numberOfCartons
-- ✅ 从 do_pick_order_line 获取所有关联的 pick orders 和 delivery orders
GROUP_CONCAT(DISTINCT dpol.pick_order_id ORDER BY dpol.pick_order_id) as pickOrderIds,
GROUP_CONCAT(DISTINCT dpol.pick_order_code ORDER BY dpol.pick_order_id SEPARATOR ', ') as pickOrderCodes,
GROUP_CONCAT(DISTINCT dpol.do_order_id ORDER BY dpol.do_order_id) as deliveryOrderIds,
GROUP_CONCAT(DISTINCT dpol.delivery_order_code ORDER BY dpol.do_order_id SEPARATOR ', ') as deliveryNos,
-- ✅ 获取第一个 pick order 的详细信息(用于兼容性)
(SELECT po2.consoCode FROM pick_order po2 WHERE po2.id = MIN(dpol.pick_order_id) LIMIT 1) as pickOrderConsoCode,
(SELECT po2.targetDate FROM pick_order po2 WHERE po2.id = MIN(dpol.pick_order_id) LIMIT 1) as pickOrderTargetDate,
(SELECT po2.status FROM pick_order po2 WHERE po2.id = MIN(dpol.pick_order_id) LIMIT 1) as pickOrderStatus,
(SELECT do2.orderDate FROM delivery_order do2 WHERE do2.id = MIN(dpol.do_order_id) LIMIT 1) as deliveryDate,
COUNT(DISTINCT dpol.pick_order_id) as numberOfPickOrders,
(SELECT SUM(pol_count.line_count)
FROM (
SELECT po3.id, COUNT(*) as line_count
FROM pick_order po3
JOIN pick_order_line pol3 ON pol3.poId = po3.id AND pol3.deleted = false
WHERE po3.id IN (SELECT dpol2.pick_order_id FROM do_pick_order_line dpol2 WHERE dpol2.do_pick_order_id = dpo.id AND dpol2.deleted = 0)
GROUP BY po3.id
) pol_count
) as numberOfCartons
FROM do_pick_order dpo
JOIN pick_order po ON po.id = dpo.pick_order_id
LEFT JOIN delivery_order do ON do.id = dpo.do_order_id
INNER JOIN do_pick_order_line dpol ON dpol.do_pick_order_id = dpo.id AND dpol.deleted = 0
-- ✅ JOIN pick_order 以检查用户分配
INNER JOIN pick_order po ON po.id = dpol.pick_order_id
LEFT JOIN shop s ON s.id = dpo.shop_id
WHERE po.assignTo = :userId
AND po.type = 'do'
AND po.status IN ('assigned', 'released', 'picking')
AND po.deleted = false
AND dpo.deleted = false
ORDER BY po.targetDate DESC, po.code ASC
GROUP BY dpo.id, dpo.store_id, dpo.ticket_no, dpo.TruckLanceCode, dpo.truck_departure_time,
dpo.ShopCode, dpo.ShopName, s.id, s.name, s.addr1, s.addr2, s.addr3, s.addr4, s.district
ORDER BY MIN(po.targetDate) DESC, MIN(po.code) ASC
""".trimIndent()
println("🔍 Executing SQL for FG pick orders by userId: $sql")
@@ -2817,27 +2910,48 @@ open fun autoAssignAndReleasePickOrderByStoreAndTicket(storeId: String, ticketNo
println("🔍 Found ${results.size} active FG pick orders for user: $userId")
// ✅ 添加调试信息
results.forEachIndexed { index, row ->
println("🔍 DEBUG: Result $index:")
println(" - doPickOrderId: ${row["doPickOrderId"]}")
println(" - pickOrderIds: ${row["pickOrderIds"]}")
println(" - numberOfPickOrders: ${row["numberOfPickOrders"]}")
println(" - ticketNo: ${row["ticketNo"]}")
}
val formattedResults = results.map { row ->
// ✅ 解析 pick order IDs 列表
val pickOrderIdsStr = row["pickOrderIds"] as? String ?: ""
val pickOrderIds = if (pickOrderIdsStr.isNotEmpty()) {
pickOrderIdsStr.split(",").mapNotNull { it.toLongOrNull() }
} else {
emptyList()
}
mapOf(
"pickOrderId" to (row["pickOrderId"] ?: 0L),
"pickOrderCode" to (row["pickOrderCode"] ?: ""),
"doPickOrderId" to (row["doPickOrderId"] ?: 0L),
"pickOrderIds" to pickOrderIds, // ✅ 返回所有 pick order IDs
"pickOrderId" to (pickOrderIds.firstOrNull() ?: 0L), // ✅ 兼容:返回第一个
"pickOrderCodes" to (row["pickOrderCodes"] ?: ""),
"pickOrderCode" to ((row["pickOrderCodes"] as? String)?.split(", ")?.firstOrNull() ?: ""), // ✅ 兼容
"deliveryOrderIds" to (row["deliveryOrderIds"] as? String ?: "").split(",").mapNotNull { it.toLongOrNull() },
"deliveryNos" to (row["deliveryNos"] ?: ""),
"pickOrderConsoCode" to (row["pickOrderConsoCode"] ?: ""),
"pickOrderTargetDate" to (row["pickOrderTargetDate"]?.toString() ?: ""),
"pickOrderStatus" to (row["pickOrderStatus"] ?: ""),
"deliveryOrderId" to (row["deliveryOrderId"] ?: 0L),
"deliveryNo" to (row["deliveryNo"] ?: ""),
"deliveryDate" to (row["deliveryDate"]?.toString() ?: ""),
"shopId" to (row["shopId"] ?: 0L),
"shopCode" to (row["shopCode"] ?: ""),
"shopName" to (row["shopName"] ?: row["shopNameFromShop"] ?: ""),
"shopAddress" to (row["shopAddress"] ?: ""),
"shopPoNo" to "",
"numberOfPickOrders" to (row["numberOfPickOrders"] ?: 0), // ✅ 新增:pick order 数量
"numberOfCartons" to (row["numberOfCartons"] ?: 0),
"truckLanceCode" to (row["truckLanceCode"] ?: ""),
"DepartureTime" to (row["DepartureTime"]?.toString() ?: ""),
"ticketNo" to (row["ticketNo"] ?: ""),
"storeId" to (row["storeId"] ?: ""),
"qrCodeData" to (row["pickOrderId"] ?: 0L)
"qrCodeData" to (row["doPickOrderId"] ?: 0L) // ✅ 改为 doPickOrderId
)
}
@@ -3363,303 +3477,258 @@ ORDER BY

// Fix the method signature and return types
open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> {
println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical ===")
println("today: ${LocalDate.now()}")
println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===")
println("userId filter: $userId")
// Get all pick order IDs assigned to the user (both RELEASED and PENDING with doId)
val user = userService.find(userId).orElse(null)
if (user == null) {
println("❌ User not found: $userId")
return emptyMap()
}
val statusList = listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED,
//PickOrderStatus.COMPLETED
)
// Get all pick orders assigned to user with PENDING or RELEASED status that have doId
val allAssignedPickOrders = pickOrderRepository.findAllByAssignToAndStatusIn(
user,
statusList
).filter { it.deliveryOrder != null } // Only pick orders with doId
println("🔍 DEBUG: Found ${allAssignedPickOrders.size} pick orders assigned to user $userId")

// ✅ NEW LOGIC: Filter based on assignment and status
val filteredPickOrders = if (allAssignedPickOrders.isNotEmpty()) {
// Check if there are any RELEASED orders assigned to this user (active work)
val assignedReleasedOrders = allAssignedPickOrders.filter {
it.status == PickOrderStatus.RELEASED && it.assignTo?.id == userId
}
if (assignedReleasedOrders.isNotEmpty()) {
// ✅ If there are assigned RELEASED orders, show only those
println("🔍 DEBUG: Found ${assignedReleasedOrders.size} assigned RELEASED orders, showing only those")
assignedReleasedOrders
} else {
// ✅ If no assigned RELEASED orders, show only the latest COMPLETED order
val completedOrders = allAssignedPickOrders.filter { it.status == PickOrderStatus.COMPLETED }
if (completedOrders.isNotEmpty()) {
val latestCompleted = completedOrders.maxByOrNull { it.completeDate ?: it.modified ?: LocalDateTime.MIN }
println("🔍 DEBUG: No assigned RELEASED orders, showing latest completed order: ${latestCompleted?.code}")
listOfNotNull(latestCompleted)
} else {
println("🔍 DEBUG: No orders found")
emptyList()
}
}
} else {
emptyList()
}
val pickOrderIds = filteredPickOrders.map { it.id!! }
println("🎯 Pick order IDs to fetch: $pickOrderIds")
// ✅ Step 1: 获取 do_pick_order 基本信息
val doPickOrderSql = """
SELECT DISTINCT
dpo.id as do_pick_order_id,
dpo.ticket_no,
dpo.store_id,
dpo.TruckLanceCode,
dpo.truck_departure_time,
dpo.ShopCode,
dpo.ShopName
FROM fpsmsdb.do_pick_order dpo
INNER JOIN fpsmsdb.do_pick_order_line dpol ON dpol.do_pick_order_id = dpo.id AND dpol.deleted = 0
INNER JOIN fpsmsdb.pick_order po ON po.id = dpol.pick_order_id
WHERE po.assignTo = :userId
AND po.type = 'do'
AND po.status IN ('assigned', 'released', 'picking')
AND po.deleted = false
AND dpo.deleted = false
LIMIT 1
""".trimIndent()
if (pickOrderIds.isEmpty()) {
val doPickOrderInfo = jdbcDao.queryForMap(doPickOrderSql, mapOf("userId" to userId)).orElse(null)
if (doPickOrderInfo == null) {
println("❌ No do_pick_order found for user $userId")
return mapOf(
"pickOrder" to null as Any?,
"pickOrderLines" to emptyList<Map<String, Any>>() as Any?
"fgInfo" to null,
"pickOrders" to emptyList<Any>()
)
}
// Use the same SQL query but transform the results into hierarchical structure
val pickOrderIdsStr = pickOrderIds.joinToString(",")
val doPickOrderId = (doPickOrderInfo["do_pick_order_id"] as? Number)?.toLong()
println("🔍 Found do_pick_order ID: $doPickOrderId")
val sql = """
SELECT
-- Pick Order Information
po.id as pickOrderId,
po.code as pickOrderCode,
po.consoCode as pickOrderConsoCode,
DATE_FORMAT(po.targetDate, '%Y-%m-%d') as pickOrderTargetDate,
po.type as pickOrderType,
po.status as pickOrderStatus,
po.assignTo as pickOrderAssignTo,
-- Pick Order Line Information
pol.id as pickOrderLineId,
pol.qty as pickOrderLineRequiredQty,
pol.status as pickOrderLineStatus,
-- Item Information
i.id as itemId,
i.code as itemCode,
i.name as itemName,
uc.code as uomCode,
uc.udfudesc as uomDesc,
uc.udfShortDesc as uomShortDesc,
-- Lot Information
ill.id as lotId,
il.lotNo,
DATE_FORMAT(il.expiryDate, '%Y-%m-%d') as expiryDate,
w.name as location,
COALESCE(uc.udfudesc, 'N/A') as stockUnit,
-- ✅ 修改:直接使用 warehouse 作为路由信息
w.`order` as routerIndex,
w.code as routerRoute,
w.code as routerArea,
-- ✅ FIXED: Set quantities to NULL for rejected lots
CASE
WHEN sol.status = 'rejected' THEN NULL
ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0))
END as availableQty,
-- Required quantity for this lot
COALESCE(spl.qty, 0) as requiredQty,
-- Actual picked quantity
COALESCE(sol.qty, 0) as actualPickQty,
-- Suggested pick lot information
spl.id as suggestedPickLotId,
ill.status as lotStatus,
-- Stock out line information
sol.id as stockOutLineId,
sol.status as stockOutLineStatus,
COALESCE(sol.qty, 0) as stockOutLineQty,
-- Additional detailed fields
COALESCE(ill.inQty, 0) as inQty,
COALESCE(ill.outQty, 0) as outQty,
COALESCE(ill.holdQty, 0) as holdQty,
COALESCE(spl.suggestedLotLineId, sol.inventoryLotLineId) as debugSuggestedLotLineId,
ill.inventoryLotId as debugInventoryLotId,
// ✅ Step 2: 获取该 do_pick_order 下的所有 pick orders
val pickOrdersSql = """
SELECT DISTINCT
dpol.pick_order_id,
dpol.pick_order_code,
dpol.do_order_id,
dpol.delivery_order_code,
po.consoCode,
po.status,
DATE_FORMAT(po.targetDate, '%Y-%m-%d') as targetDate
FROM fpsmsdb.do_pick_order_line dpol
INNER JOIN fpsmsdb.pick_order po ON po.id = dpol.pick_order_id
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
AND po.deleted = false
ORDER BY dpol.pick_order_id
""".trimIndent()
val pickOrdersInfo = jdbcDao.queryForList(pickOrdersSql, mapOf("doPickOrderId" to doPickOrderId))
println("🔍 Found ${pickOrdersInfo.size} pick orders")
// ✅ Step 3: 为每个 pick order 获取 lines 和 lots(包括 null stock 的)
val pickOrders = pickOrdersInfo.map { poInfo ->
val pickOrderId = (poInfo["pick_order_id"] as? Number)?.toLong()
-- Calculate total picked quantity by ALL pick orders for this lot
COALESCE((
SELECT SUM(sol_all.qty)
FROM fpsmsdb.stock_out_line sol_all
WHERE sol_all.inventoryLotLineId = ill.id
AND sol_all.deleted = false
AND sol_all.status IN ('pending', 'checked', 'partially_completed', 'completed')
), 0) as totalPickedByAllPickOrders,
// ✅ 查询该 pick order 的所有 lines 和 lots
val linesSql = """
SELECT
po.id as pickOrderId,
po.code as pickOrderCode,
po.consoCode as pickOrderConsoCode,
DATE_FORMAT(po.targetDate, '%Y-%m-%d') as pickOrderTargetDate,
po.type as pickOrderType,
po.status as pickOrderStatus,
po.assignTo as pickOrderAssignTo,
pol.id as pickOrderLineId,
pol.qty as pickOrderLineRequiredQty,
pol.status as pickOrderLineStatus,
i.id as itemId,
i.code as itemCode,
i.name as itemName,
uc.code as uomCode,
uc.udfudesc as uomDesc,
uc.udfShortDesc as uomShortDesc,
ill.id as lotId,
il.lotNo,
DATE_FORMAT(il.expiryDate, '%Y-%m-%d') as expiryDate,
w.name as location,
COALESCE(uc.udfudesc, 'N/A') as stockUnit,
w.`order` as routerIndex,
w.code as routerRoute,
CASE
WHEN sol.status = 'rejected' THEN NULL
ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0))
END as availableQty,
COALESCE(spl.qty, 0) as requiredQty,
COALESCE(sol.qty, 0) as actualPickQty,
spl.id as suggestedPickLotId,
ill.status as lotStatus,
sol.id as stockOutLineId,
sol.status as stockOutLineStatus,
COALESCE(sol.qty, 0) as stockOutLineQty,
COALESCE(ill.inQty, 0) as inQty,
COALESCE(ill.outQty, 0) as outQty,
COALESCE(ill.holdQty, 0) as holdQty,
CASE
WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired'
WHEN sol.status = 'rejected' THEN 'rejected'
WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock'
WHEN ill.status = 'unavailable' THEN 'status_unavailable'
ELSE 'available'
END as lotAvailability,
CASE
WHEN sol.status = 'completed' THEN 'completed'
WHEN sol.status = 'rejected' THEN 'rejected'
WHEN sol.status = 'created' THEN 'pending'
ELSE 'pending'
END as processingStatus
FROM fpsmsdb.pick_order po
JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false
JOIN fpsmsdb.items i ON i.id = pol.itemId
LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId
-- ✅ 关键修改:使用 LEFT JOIN 来包含没有 lot 的 pick order lines
LEFT JOIN (
SELECT spl.pickOrderLineId, spl.suggestedLotLineId AS lotLineId
FROM fpsmsdb.suggested_pick_lot spl
UNION
SELECT sol.pickOrderLineId, sol.inventoryLotLineId
FROM fpsmsdb.stock_out_line sol
WHERE sol.deleted = false
) ll ON ll.pickOrderLineId = pol.id
LEFT JOIN fpsmsdb.suggested_pick_lot spl
ON spl.pickOrderLineId = pol.id AND spl.suggestedLotLineId = ll.lotLineId
LEFT JOIN fpsmsdb.stock_out_line sol
ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ll.lotLineId AND sol.deleted = false
LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = ll.lotLineId AND ill.deleted = false
LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId AND il.deleted = false
LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
WHERE po.id = :pickOrderId
AND po.deleted = false
ORDER BY
COALESCE(w.`order`, 999999) ASC,
pol.id ASC,
il.lotNo ASC
""".trimIndent()
-- Calculate remaining available quantity correctly
(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as remainingAfterAllPickOrders,
val linesResults = jdbcDao.queryForList(linesSql, mapOf("pickOrderId" to pickOrderId))
println("🔍 Pick order $pickOrderId has ${linesResults.size} line-lot records")
-- Lot availability status
CASE
WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired'
WHEN sol.status = 'rejected' THEN 'rejected'
WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock'
WHEN ill.status = 'unavailable' THEN 'status_unavailable'
ELSE 'available'
END as lotAvailability,
// ✅ 按 pickOrderLineId 分组
val lineGroups = linesResults.groupBy { (it["pickOrderLineId"] as? Number)?.toLong() }
-- Processing status
CASE
WHEN sol.status = 'completed' THEN 'completed'
WHEN sol.status = 'rejected' THEN 'rejected'
WHEN sol.status = 'created' THEN 'pending'
ELSE 'pending'
END as processingStatus
val pickOrderLines = lineGroups.map { (lineId, lineRows) ->
val firstLineRow = lineRows.firstOrNull()
if (firstLineRow == null) {
null
} else {
// ✅ 构建 lots 列表(如果没有 lot,返回空数组)
val lots = if (lineRows.any { it["lotId"] != null }) {
lineRows.filter { it["lotId"] != null }.map { lotRow ->
mapOf(
"id" to lotRow["lotId"],
"lotNo" to lotRow["lotNo"],
"expiryDate" to lotRow["expiryDate"],
"location" to lotRow["location"],
"stockUnit" to lotRow["stockUnit"],
"availableQty" to lotRow["availableQty"],
"requiredQty" to lotRow["requiredQty"],
"actualPickQty" to lotRow["actualPickQty"],
"inQty" to lotRow["inQty"],
"outQty" to lotRow["outQty"],
"holdQty" to lotRow["holdQty"],
"lotStatus" to lotRow["lotStatus"],
"lotAvailability" to lotRow["lotAvailability"],
"processingStatus" to lotRow["processingStatus"],
"suggestedPickLotId" to lotRow["suggestedPickLotId"],
"stockOutLineId" to lotRow["stockOutLineId"],
"stockOutLineStatus" to lotRow["stockOutLineStatus"],
"stockOutLineQty" to lotRow["stockOutLineQty"],
"router" to mapOf(
"id" to null,
"index" to lotRow["routerIndex"],
"route" to lotRow["routerRoute"],
"area" to lotRow["routerRoute"],
"itemCode" to lotRow["itemId"],
"itemName" to lotRow["itemName"],
"uomId" to lotRow["uomCode"],
"noofCarton" to lotRow["requiredQty"]
)
)
}
} else {
emptyList() // ✅ 返回空数组而不是 null
}
mapOf(
"id" to lineId,
"requiredQty" to firstLineRow["pickOrderLineRequiredQty"],
"status" to firstLineRow["pickOrderLineStatus"],
"item" to mapOf(
"id" to firstLineRow["itemId"],
"code" to firstLineRow["itemCode"],
"name" to firstLineRow["itemName"],
"uomCode" to firstLineRow["uomCode"],
"uomDesc" to firstLineRow["uomDesc"],
"uomShortDesc" to firstLineRow["uomShortDesc"],
),
"lots" to lots // ✅ 即使是空数组也返回
)
}
}.filterNotNull()
FROM fpsmsdb.pick_order po
JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id
JOIN fpsmsdb.items i ON i.id = pol.itemId
LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId

-- Base lot links: all lot lines referenced by either suggestions or stock out lines for this POL
LEFT JOIN (
SELECT spl.pickOrderLineId AS pickOrderLineId, spl.suggestedLotLineId AS lotLineId
FROM fpsmsdb.suggested_pick_lot spl
UNION
SELECT sol.pickOrderLineId, sol.inventoryLotLineId
FROM fpsmsdb.stock_out_line sol
WHERE sol.deleted = false
) ll ON ll.pickOrderLineId = pol.id

-- Re-bind spl/sol strictly on the same lot line
LEFT JOIN fpsmsdb.suggested_pick_lot spl
ON spl.pickOrderLineId = pol.id AND spl.suggestedLotLineId = ll.lotLineId
LEFT JOIN fpsmsdb.stock_out_line sol
ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ll.lotLineId AND sol.deleted = false

LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = ll.lotLineId
-- ✅ 删除 router 相关的 JOIN
-- LEFT JOIN router r ON r.inventoryLotId = ill.inventoryLotId AND r.deleted = false
-- LEFT JOIN router_order ro ON r.router_id = ro.id
LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId
LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId -- ✅ 保留 warehouse JOIN

WHERE po.deleted = false
AND po.id IN ($pickOrderIdsStr)
AND pol.deleted = false
AND po.status IN ('PENDING', 'RELEASED', 'COMPLETED')
AND po.assignTo = :userId
AND ill.deleted = false
AND il.deleted = false
AND ll.lotLineId IS NOT NULL
AND (spl.pickOrderLineId IS NOT NULL OR sol.pickOrderLineId IS NOT NULL)
ORDER BY
CASE WHEN sol.status = 'rejected' THEN 0 ELSE 1 END,
COALESCE(w.`order`, 999999) ASC, -- ✅ 使用 warehouse.order 排序
po.code ASC,
i.code ASC,
il.expiryDate ASC,
il.lotNo ASC
""".trimIndent()
println("�� Executing SQL for hierarchical structure: $sql")
println("�� With parameters: userId = $userId, pickOrderIds = $pickOrderIdsStr")
val results = jdbcDao.queryForList(sql, mapOf("userId" to userId))
println("✅ Total result count: ${results.size}")
// Filter out lots with null availableQty (rejected lots)
// val filteredResults = results.filter { row ->
//val availableQty = row["availableQty"]
// availableQty != null
// }
val filteredResults = results
println("✅ Filtered result count: ${filteredResults.size}")
// ✅ Transform flat results into hierarchical structure
if (filteredResults.isEmpty()) {
return mapOf(
"pickOrder" to null as Any?,
"pickOrderLines" to emptyList<Map<String, Any>>() as Any?
mapOf(
"pickOrderId" to pickOrderId,
"pickOrderCode" to poInfo["pick_order_code"],
"doOrderId" to poInfo["do_order_id"],
"deliveryOrderCode" to poInfo["delivery_order_code"],
"consoCode" to poInfo["consoCode"],
"status" to poInfo["status"],
"targetDate" to poInfo["targetDate"],
"pickOrderLines" to pickOrderLines
)
}
// Get pick order info from first row (all rows have same pick order info)
val firstRow = filteredResults.first()
val pickOrderInfo = mapOf(
"id" to firstRow["pickOrderId"],
"code" to firstRow["pickOrderCode"],
"consoCode" to firstRow["pickOrderConsoCode"],
"targetDate" to firstRow["pickOrderTargetDate"],
"type" to firstRow["pickOrderType"],
"status" to firstRow["pickOrderStatus"],
"assignTo" to firstRow["pickOrderAssignTo"]
// ✅ 构建 FG 信息
val fgInfo = mapOf(
"doPickOrderId" to doPickOrderId,
"ticketNo" to doPickOrderInfo["ticket_no"],
"storeId" to doPickOrderInfo["store_id"],
"shopCode" to doPickOrderInfo["ShopCode"],
"shopName" to doPickOrderInfo["ShopName"],
"truckLanceCode" to doPickOrderInfo["TruckLanceCode"],
"departureTime" to doPickOrderInfo["truck_departure_time"]
)
// Group by pick order line ID to create hierarchical structure
val pickOrderLinesMap = filteredResults
.groupBy { it["pickOrderLineId"] as Number }
.map { (pickOrderLineId, lots) ->
val firstLot = lots.first()
// Item information (same for all lots of this line)
val itemInfo = mapOf(
"id" to firstLot["itemId"],
"code" to firstLot["itemCode"],
"name" to firstLot["itemName"],
"uomCode" to firstLot["uomCode"],
"uomDesc" to firstLot["uomDesc"]
)
// Transform lots for this pick order line
val lotsInfo = lots.map { lot ->
mapOf(
"id" to lot["lotId"],
"lotNo" to lot["lotNo"],
"expiryDate" to lot["expiryDate"],
"location" to lot["location"],
"stockUnit" to lot["stockUnit"],
"availableQty" to lot["availableQty"],
"requiredQty" to lot["requiredQty"],
"actualPickQty" to lot["actualPickQty"],
"inQty" to lot["inQty"],
"outQty" to lot["outQty"],
"holdQty" to lot["holdQty"],
"lotStatus" to lot["lotStatus"],
"lotAvailability" to lot["lotAvailability"],
"processingStatus" to lot["processingStatus"],
"suggestedPickLotId" to lot["suggestedPickLotId"],
"stockOutLineId" to lot["stockOutLineId"],
"stockOutLineStatus" to lot["stockOutLineStatus"],
"stockOutLineQty" to lot["stockOutLineQty"],
"router" to mapOf(
"id" to lot["routerId"],
"index" to lot["routerIndex"],
"route" to lot["routerRoute"],
"area" to lot["routerArea"],
"itemCode" to firstLot["itemId"],
"itemName" to firstLot["itemName"],
"uomId" to firstLot["uomShortDesc"],
"noofCarton" to lot["requiredQty"] // Use required qty as carton count
)
)
}
// Pick order line with item and lots
mapOf(
"id" to pickOrderLineId,
"requiredQty" to firstLot["pickOrderLineRequiredQty"],
"status" to firstLot["pickOrderLineStatus"],
"item" to itemInfo,
"lots" to lotsInfo
)
}
return mapOf(
"pickOrder" to pickOrderInfo as Any?,
"pickOrderLines" to pickOrderLinesMap as Any?
"fgInfo" to fgInfo,
"pickOrders" to pickOrders
)
}



+ 1
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt View File

@@ -301,4 +301,5 @@ fun getCompletedDoPickOrders(
fun getLotDetailsByPickOrderId(@PathVariable pickOrderId: Long): List<Map<String, Any>> {
return pickOrderService.getLotDetailsByPickOrderId(pickOrderId);
}

}

+ 74
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt View File

@@ -209,6 +209,19 @@ existingSuggestions.forEach { existingSugg ->
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()
}
}
}
return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList)
@@ -1480,7 +1493,67 @@ open fun updateSuggestedLotLineId(suggestedPickLotId: Long, newLotLineId: Long):
}
}

// ... existing code ...
private fun createInsufficientStockIssue(
pickOrder: PickOrder,
pickOrderLine: PickOrderLine,
insufficientQty: BigDecimal
) {
try {
// ✅ 检查是否已存在相同的 issue(避免重复创建)
val existingIssues = pickExecutionIssueRepository
.findByPickOrderLineIdAndDeletedFalse(pickOrderLine.id ?: 0L)
.filter {
it.issueCategory == IssueCategory.resuggest_issue &&
it.handleStatus == HandleStatus.pending
}
if (existingIssues.isNotEmpty()) {
println("⏭️ Issue already exists for pick order line ${pickOrderLine.id}, skipping")
return
}
val issue = PickExecutionIssue(
id = null,
pickOrderId = pickOrder.id!!,
pickOrderCode = pickOrder.code ?: "",
pickOrderCreateDate = pickOrder.created?.toLocalDate(),
pickExecutionDate = LocalDate.now(),
doPickOrderId = if (pickOrder.type?.value == "do") pickOrder.deliveryOrder?.id else null,
joPickOrderId = pickOrder.jobOrder?.id,
pickOrderLineId = pickOrderLine.id!!,
issueNo = generateIssueNo(),
issueCategory = IssueCategory.resuggest_issue,
itemId = pickOrderLine.item?.id ?: 0L,
itemCode = pickOrderLine.item?.code,
itemDescription = pickOrderLine.item?.name,
lotId = null,
lotNo = null,
storeLocation = null,
requiredQty = pickOrderLine.qty,
actualPickQty = BigDecimal.ZERO,
missQty = insufficientQty,
badItemQty = BigDecimal.ZERO,
issueRemark = "No inventory available for suggestion (auto-created by system)",
pickerName = null,
handleStatus = HandleStatus.pending,
handleDate = null,
handledBy = null,
created = LocalDateTime.now(),
createdBy = "SYSTEM",
version = 0,
modified = LocalDateTime.now(),
modifiedBy = "SYSTEM",
deleted = false
)
pickExecutionIssueRepository.save(issue)
println("✅ Auto-created issue ${issue.issueNo} for insufficient stock (line ${pickOrderLine.id}, qty ${insufficientQty})")
} catch (e: Exception) {
println("❌ Error creating insufficient stock issue: ${e.message}")
e.printStackTrace()
}
}

}



Loading…
Cancel
Save