Quellcode durchsuchen

update Job order and release type

master
CANCERYS\kw093 vor 5 Tagen
Ursprung
Commit
a5f9da8ecf
21 geänderte Dateien mit 985 neuen und 243 gelöschten Zeilen
  1. +4
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrder.kt
  2. +2
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecord.kt
  3. +6
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRepository.kt
  4. +20
    -4
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt
  5. +30
    -22
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt
  6. +214
    -2
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
  7. +19
    -14
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt
  8. +2
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt
  9. +246
    -177
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  10. +100
    -5
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  11. +2
    -2
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt
  12. +64
    -10
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt
  13. +7
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductionProcessIssue.kt
  14. +1
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductionProcessIssueRepository.kt
  15. +89
    -5
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  16. +8
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  17. +10
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  18. +142
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  19. +8
    -0
      src/main/resources/db/changelog/changes/20251203_01_Enson/01_add_dopickordercolumn.sql
  20. +6
    -0
      src/main/resources/db/changelog/changes/20251209_01_Enson/01_add_dopickordercolumn.sql
  21. +5
    -0
      src/main/resources/db/changelog/changes/20251210_01_Enson/01_add_column.sql

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrder.kt Datei anzeigen

@@ -93,6 +93,8 @@ class DoPickOrder {
@Column(name = "handler_name", length = 100)
var handlerName: String? = null
@Column(name = "release_type", length = 100)
var releaseType: String? = null
// Default constructor for Hibernate
constructor()
@@ -119,6 +121,7 @@ class DoPickOrder {
pickOrderCode: String? = null,
deliveryOrderCode: String? = null,
loadingSequence: Int? = null,
releaseType: String? = null
) {
this.storeId = storeId
this.ticketNo = ticketNo
@@ -141,5 +144,6 @@ class DoPickOrder {
this.pickOrderCode = pickOrderCode
this.deliveryOrderCode = deliveryOrderCode
this.loadingSequence = loadingSequence
this.releaseType = releaseType
}
}

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecord.kt Datei anzeigen

@@ -94,6 +94,8 @@ class DoPickOrderRecord {
var deleted: Boolean = false
@Column(name = "handler_name", length = 100)
var handlerName: String? = null
@Column(name = "release_type", length = 100)
var releaseType: String? = null
// Default constructor for Hibernate
constructor()


+ 6
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRepository.kt Datei anzeigen

@@ -39,5 +39,10 @@ fun findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn(
@Param("startDate") startDate: LocalDate,
@Param("endDate") endDate: LocalDate
): List<DoPickOrder>

fun findByShopIdAndStoreIdAndReleaseTypeAndTicketStatusAndDeletedFalse(
shopId: Long?,
storeId: String,
releaseType: String,
ticketStatus: DoPickOrderStatus
): List<DoPickOrder>
}

+ 20
- 4
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt Datei anzeigen

@@ -18,9 +18,9 @@ class DoPickOrderQueryService(
private val doPickOrderRecordRepository: DoPickOrderRecordRepository
) {
fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary {
fun getSummaryByStore(storeId: String, requiredDate: LocalDate?, releaseType: String): StoreLaneSummary {
val targetDate = requiredDate ?: LocalDate.now()
println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate")
println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate, releaseType=$releaseType")
val actualStoreId = when (storeId) {
"2/F" -> "2/F"
@@ -35,6 +35,13 @@ class DoPickOrderQueryService(
listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed)
)
// 根据 releaseType 过滤 activeRecords
val filteredActiveRecordsByReleaseType = when (releaseType.lowercase()) {
"batch" -> activeRecords.filter { it.releaseType == "batch" }
"single" -> activeRecords.filter { it.releaseType == "single" }
else -> activeRecords // "all" 或其他值,不过滤
}
// Query completed records from do_pick_order_record table
val completedRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn(
actualStoreId,
@@ -42,11 +49,20 @@ class DoPickOrderQueryService(
listOf(DoPickOrderStatus.completed)
)
// 根据 releaseType 过滤 completedRecords
val filteredCompletedRecordsByReleaseType = when (releaseType.lowercase()) {
"batch" -> completedRecords.filter { it.releaseType == "batch" }
"single" -> completedRecords.filter { it.releaseType == "single" }
else -> completedRecords // "all" 或其他值,不过滤
}
println("🔍 DEBUG: Found ${activeRecords.size} active records for date $targetDate")
println("🔍 DEBUG: After releaseType filter: ${filteredActiveRecordsByReleaseType.size} active records")
println("🔍 DEBUG: Found ${completedRecords.size} completed records for date $targetDate")
println("🔍 DEBUG: After releaseType filter: ${filteredCompletedRecordsByReleaseType.size} completed records")
// Filter active records (check for non-issue lines)
val filteredActiveRecords = activeRecords.filter { doPickOrder ->
val filteredActiveRecords = filteredActiveRecordsByReleaseType.filter { doPickOrder ->
val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!)
if (!hasNonIssueLines) {
println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues")
@@ -55,7 +71,7 @@ class DoPickOrderQueryService(
}
// For completed records, check if they have non-issue lines in the record table
val filteredCompletedRecords = completedRecords.filter { record ->
val filteredCompletedRecords = filteredCompletedRecordsByReleaseType.filter { record ->
val hasNonIssueLines = checkDoPickOrderRecordHasNonIssueLines(record.id!!)
if (!hasNonIssueLines) {
println("🔍 DEBUG: Filtering out DoPickOrderRecord ${record.id} - all lines are issues")


+ 30
- 22
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt Datei anzeigen

@@ -91,7 +91,7 @@ open class DoPickOrderService(
return RecordsRes<DeliveryOrderInfo>(records, total.toInt());
}
open fun getNextTicketNumber(datePrefix: String, storeId: String): String {
println("🔍 DEBUG: Getting next ticket number for date prefix: $datePrefix, store: $storeId")
println(" DEBUG: Getting next ticket number for date prefix: $datePrefix, store: $storeId")
try {
val sanitizedStoreId = storeId.replace("/", "")
val shortDatePrefix = if (datePrefix.length == 8) {
@@ -102,14 +102,14 @@ open class DoPickOrderService(
// 修改搜索模式为新格式
val searchPattern = "TI-${shortDatePrefix}-${sanitizedStoreId}-" // T-20250915-4F-
val todayTickets = doPickOrderRepository.findByTicketNoStartingWith(searchPattern)
println("🔍 DEBUG: Found ${todayTickets.size} existing tickets with prefix $searchPattern")
println(" DEBUG: Found ${todayTickets.size} existing tickets with prefix $searchPattern")
todayTickets.forEach { ticket ->
println("🔍 DEBUG: Existing ticket: ${ticket.ticketNo}, Status: ${ticket.ticketStatus}")
println(" DEBUG: Existing ticket: ${ticket.ticketNo}, Status: ${ticket.ticketStatus}")
}
val nextNumber = (todayTickets.size + 1).toString().padStart(3, '0')
// 修改生成格式
val ticketNumber = "TI-${datePrefix}-${sanitizedStoreId}-${nextNumber}" // T-20250915-4F-001
println("🔍 DEBUG: Generated ticket number: $ticketNumber")
println(" DEBUG: Generated ticket number: $ticketNumber")
return ticketNumber
} catch (e: Exception) {
println("❌ ERROR in getNextTicketNumber: ${e.message}")
@@ -297,41 +297,49 @@ open class DoPickOrderService(
}
return doPickOrderRepository.saveAll(doPickOrders)
}
fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary {
fun getSummaryByStore(storeId: String, requiredDate: LocalDate?, releaseType: String): StoreLaneSummary {
val targetDate = requiredDate ?: LocalDate.now()
println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate")
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)
)

val filteredByReleaseType = when (releaseType.lowercase()) {
"batch" -> allRecords.filter { it.releaseType == "batch" }
"single" -> allRecords.filter { it.releaseType == "single" }
else -> allRecords // "all" 或其他值,不过滤
}
// 添加 finishedRecords 查询
val finishedRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn(
actualStoreId,
targetDate,
listOf(DoPickOrderStatus.completed)
)
val filteredFinishedRecords = when (releaseType.lowercase()) {
"batch" -> finishedRecords.filter { it.releaseType == "batch" }
"single" -> finishedRecords.filter { it.releaseType == "single" }
else -> finishedRecords // "all" 或其他值,不过滤
}
println(" DEBUG: Found ${allRecords.size} records for date $targetDate")
println(" DEBUG: Found ${finishedRecords.size} finished records for date $targetDate")
println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate")
println("🔍 DEBUG: Found ${finishedRecords.size} finished records for date $targetDate")
val filteredRecords = allRecords.filter { doPickOrder ->
val filteredRecords = filteredByReleaseType.filter { doPickOrder ->
val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!)
if (!hasNonIssueLines) {
println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues")
println(" DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues")
}
hasNonIssueLines
}
println("🔍 DEBUG: After filtering, ${filteredRecords.size} records remain")
println(" DEBUG: After filtering, ${filteredRecords.size} records remain")
val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode }
.mapValues { (key, list) ->
@@ -341,9 +349,9 @@ open class DoPickOrderService(
(record.truckDepartureTime == truckDepartureTime) &&
(record.truckLanceCode == truckLanceCode)
}
println("🔍 DEBUG: Group key - truckDepartureTime: $truckDepartureTime, truckLanceCode: $truckLanceCode")
println("🔍 DEBUG: Found ${list.size} active records in this group")
println("🔍 DEBUG: Found $matchingFinishedCount finished records matching this group")
println(" DEBUG: Group key - truckDepartureTime: $truckDepartureTime, truckLanceCode: $truckLanceCode")
println(" DEBUG: Found ${list.size} active records in this group")
println(" DEBUG: Found $matchingFinishedCount finished records matching this group")
LaneBtn(
truckLanceCode = list.first().truckLanceCode ?: "",
unassigned = list.count { it.handledBy == null },
@@ -403,7 +411,7 @@ open class DoPickOrderService(
// 3. 只有当所有 lines 都是 "issue" 状态时才过滤掉
val hasNonIssueLines = nonIssueLines > 0
println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total lines: $totalLines, Non-issue lines: $nonIssueLines, Has non-issue lines: $hasNonIssueLines")
println(" DEBUG: DoPickOrder $doPickOrderId - Total lines: $totalLines, Non-issue lines: $nonIssueLines, Has non-issue lines: $hasNonIssueLines")
return hasNonIssueLines
@@ -426,7 +434,7 @@ open class DoPickOrderService(
"4/F" -> "4/F" // 保持原格式
else -> request.storeId
}
println("🔍 DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'")
println(" DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'")
val candidates = doPickOrderRepository
.findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc(
@@ -453,7 +461,7 @@ open class DoPickOrderService(
// 关键修改:获取这个 do_pick_order 下的所有 pick orders 并分配给用户
val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(firstOrder.id!!)
println("🔍 DEBUG: Found ${doPickOrderLines.size} pick orders in do_pick_order ${firstOrder.id}")
println(" DEBUG: Found ${doPickOrderLines.size} pick orders in do_pick_order ${firstOrder.id}")
doPickOrderLines.forEach { line ->
if (line.pickOrderId != null) {
@@ -462,7 +470,7 @@ open class DoPickOrderService(
pickOrder.assignTo = user
pickOrder.status = PickOrderStatus.RELEASED
pickOrderRepository.save(pickOrder)
println("🔍 DEBUG: Assigned pick order ${line.pickOrderId} to user ${request.userId}")
println(" DEBUG: Assigned pick order ${line.pickOrderId} to user ${request.userId}")
} else {
println("⚠️ WARNING: Pick order ${line.pickOrderId} not found")
}
@@ -481,7 +489,7 @@ open class DoPickOrderService(
}
if (records.isNotEmpty()) {
doPickOrderRecordRepository.saveAll(records)
println("🔍 DEBUG: Updated ${records.size} do_pick_order_record for pick order ${line.pickOrderId}")
println(" DEBUG: Updated ${records.size} do_pick_order_record for pick order ${line.pickOrderId}")
}
}
}


+ 214
- 2
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt Datei anzeigen

@@ -157,7 +157,7 @@ class DoReleaseCoordinatorService(
)
SELECT
dpo2.id,
CONCAT('TI-',
CONCAT('TI-B-',
DATE_FORMAT(dpo2.RequiredDeliveryDate, '%Y%m%d'),
'-',
REPLACE(COALESCE(dpo2.store_id, ts.preferred_floor, '2F'), '/', ''),
@@ -212,6 +212,14 @@ class DoReleaseCoordinatorService(
AND w.store_id IN ('2F', '4F')
WHERE dol.deleted = 0
AND dol.deliveryOrderId IN (${ids.joinToString(",")})
AND dol.deliveryOrderId NOT IN (
SELECT DISTINCT dpol.do_order_id
FROM fpsmsdb.do_pick_order_line dpol
INNER JOIN fpsmsdb.do_pick_order dpo ON dpo.id = dpol.do_pick_order_id
WHERE dpo.release_type = 'single'
AND dpo.deleted = 0
AND dpol.deleted = 0
)
GROUP BY dol.deliveryOrderId, w.store_id
),
DoFloorSummary AS (
@@ -507,7 +515,9 @@ class DoReleaseCoordinatorService(
truckLanceCode = first.truckLanceCode,
shopCode = first.shopCode,
shopName = first.shopName,
requiredDeliveryDate = first.estimatedArrivalDate
requiredDeliveryDate = first.estimatedArrivalDate,
releaseType = "batch"

)
// 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save()
@@ -616,4 +626,206 @@ class DoReleaseCoordinatorService(
)
)
}

fun startBatchReleaseAsyncSingle(doId: Long, userId: Long): MessageResponse {
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId)
?: return MessageResponse(
id = null, code = "NOT_FOUND", name = null, type = null,
message = "Delivery Order not found", errorPosition = null, entity = null
)
executor.submit {
try {
println("📦 Starting single release for DO $doId")
// 调用 releaseDeliveryOrderWithoutTicket 创建 pick order
val result = deliveryOrderService.releaseDeliveryOrderWithoutTicket(
ReleaseDoRequest(id = doId, userId = userId)
)
// 确定 storeId
val storeId = when (result.preferredFloor) {
"2F" -> "2/F"
"4F" -> "4/F"
else -> "2/F"
}
// 查找是否已有相同 shop、storeId 和 releaseType='single' 的 do_pick_order
val existingDoPickOrder = doPickOrderRepository
.findByShopIdAndStoreIdAndReleaseTypeAndTicketStatusAndDeletedFalse(
result.shopId,
storeId,
"single",
DoPickOrderStatus.pending
)
.firstOrNull {
it.requiredDeliveryDate == result.estimatedArrivalDate &&
it.truckDepartureTime == result.truckDepartureTime &&
it.truckLanceCode == result.truckLanceCode
}
if (existingDoPickOrder != null) {
// 如果已存在,创建 do_pick_order_line 关联到已存在的 do_pick_order
val existingLine = doPickOrderLineRepository
.findByPickOrderIdAndDeletedFalse(result.pickOrderId)
.firstOrNull { it.doPickOrderId == existingDoPickOrder.id }
if (existingLine == null) {
val line = DoPickOrderLine().apply {
doPickOrderId = existingDoPickOrder.id
pickOrderId = result.pickOrderId
doOrderId = result.deliveryOrderId
pickOrderCode = result.pickOrderCode
deliveryOrderCode = result.deliveryOrderCode
status = "pending"
}
doPickOrderLineRepository.save(line)
println("🔍 DEBUG: Created DoPickOrderLine for existing DoPickOrder ${existingDoPickOrder.id}")
}
} else {
// 如果不存在,创建新的 do_pick_order
val doPickOrder = DoPickOrder(
storeId = storeId,
ticketNo = "TEMP-${System.currentTimeMillis()}",
ticketStatus = DoPickOrderStatus.pending,
truckId = result.truckId,
truckDepartureTime = result.truckDepartureTime,
shopId = result.shopId,
handledBy = null,
loadingSequence = result.loadingSequence ?: 999,
ticketReleaseTime = null,
truckLanceCode = result.truckLanceCode,
shopCode = result.shopCode,
shopName = result.shopName,
requiredDeliveryDate = result.estimatedArrivalDate,
releaseType = "single" // 设置为 single
)
val saved = doPickOrderRepository.save(doPickOrder)
// 创建 do_pick_order_line
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 new DoPickOrder with releaseType=single")
}
// 更新 ticket numbers(只更新 single 类型的)
updateSingleTicketNumbers()
} catch (e: Exception) {
println("❌ Single release exception: ${e.message}")
e.printStackTrace()
}
}
return MessageResponse(
id = null, code = "STARTED", name = null, type = null,
message = "Single release started", errorPosition = null,
entity = mapOf("doId" to doId)
)
}
private fun updateSingleTicketNumbers() {
try {
// 1. 查找所有 TEMP- 开头的 single release type 订单
val tempTickets = doPickOrderRepository.findAll()
.filter {
it.ticketNo?.startsWith("TEMP-") == true &&
it.releaseType == "single" &&
!it.deleted
}
if (tempTickets.isEmpty()) {
println("🔍 No single release type tickets to update")
return
}
println("🔍 DEBUG: Found ${tempTickets.size} single release type tickets to update")
// 2. 按日期和 storeId 分组
val grouped = tempTickets.groupBy { ticket ->
val date = ticket.requiredDeliveryDate
val storeId = ticket.storeId?.replace("/", "") ?: "2F"
Pair(date, storeId)
}
// 3. 为每个组生成 ticket numbers
grouped.forEach { (dateStorePair, tickets) ->
val (date, storeId) = dateStorePair
if (date == null) {
println("⚠️ WARNING: Skipping tickets with null requiredDeliveryDate")
return@forEach
}
// 格式化日期为 YYYYMMDD
val datePrefix = date.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
// 4. 查询该组已存在的 ticket numbers(包括已生成的,不只是 TEMP-)
val existingTicketPattern = "TI-S-$datePrefix-$storeId-"
val existingTickets = doPickOrderRepository.findAll()
.filter {
it.ticketNo?.startsWith(existingTicketPattern) == true &&
it.releaseType == "single" &&
!it.deleted &&
it.requiredDeliveryDate == date &&
it.storeId?.replace("/", "") == storeId
}
.mapNotNull { it.ticketNo }
// 提取已存在的序号
val existingNumbers = existingTickets.mapNotNull { ticketNo ->
val parts = ticketNo.split("-")
if (parts.size >= 4) {
parts.lastOrNull()?.toIntOrNull()
} else null
}.toMutableSet()
println("🔍 DEBUG: Group ($date, $storeId) - Existing ticket numbers: $existingTickets, Existing numbers: $existingNumbers")
// 5. 在组内按 truckDepartureTime, truckLanceCode, loadingSequence, doOrderId 排序
val sortedTickets = tickets.sortedWith(
compareBy<DoPickOrder>(
{ it.truckDepartureTime ?: java.time.LocalTime.of(23, 59, 59) },
{ it.truckLanceCode ?: "ZZ" },
{ it.loadingSequence ?: 999 },
{ it.doOrderId ?: 0L }
)
)
// 6. 为每个订单生成序号并更新 ticket_no(跳过已存在的序号)
var nextNumber = 1
sortedTickets.forEach { ticket ->
// 找到下一个可用的序号
while (existingNumbers.contains(nextNumber)) {
nextNumber++
}
val sequenceNumber = nextNumber.toString().padStart(3, '0')
val newTicketNo = "TI-S-$datePrefix-$storeId-$sequenceNumber"
ticket.ticketNo = newTicketNo
doPickOrderRepository.save(ticket)
// 将新生成的序号添加到已存在集合中,避免同一批次内重复
existingNumbers.add(nextNumber)
nextNumber++
println("🔍 DEBUG: Updated ticket ${ticket.id} to $newTicketNo")
}
}
println("✅ Updated ${tempTickets.size} single release type ticket numbers")
} catch (e: Exception) {
println("❌ Error updating single ticket numbers: ${e.message}")
e.printStackTrace()
}
}
}

+ 19
- 14
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt Datei anzeigen

@@ -69,27 +69,32 @@ class DoPickOrderController(
@GetMapping("/summary-by-store")
fun getSummaryByStore(
@RequestParam storeId: String,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?,
@RequestParam releaseType: String?
): StoreLaneSummary {
return doPickOrderQueryService.getSummaryByStore(storeId, requiredDate)
return doPickOrderQueryService.getSummaryByStore(storeId, requiredDate, releaseType?: "ALL")
}
@PostMapping("/assign-by-lane")
fun assignByLane(@RequestBody request: AssignByLaneRequest): MessageResponse {
return doPickOrderAssignmentService.assignByLane(request) // 使用新的 Service
}
@PostMapping("/batch-release/async")
fun startBatchReleaseAsync(
@RequestBody ids: List<Long>,
@RequestParam(defaultValue = "1") userId: Long
): MessageResponse {
return doReleaseCoordinatorService.startBatchReleaseAsync(ids, userId)
}

@GetMapping("/batch-release/progress/{jobId}")
fun getBatchReleaseProgress(@PathVariable jobId: String): MessageResponse {
return doReleaseCoordinatorService.getBatchReleaseProgress(jobId)
}
@PostMapping("/batch-release/async")
fun startBatchReleaseAsync(
@RequestBody ids: List<Long>,
@RequestParam(defaultValue = "1") userId: Long
): MessageResponse {
return doReleaseCoordinatorService.startBatchReleaseAsync(ids, userId)
}
@PostMapping("/batch-release/async-single")
fun startBatchReleaseAsyncSingle( @RequestBody doId: Long, @RequestParam(defaultValue = "1") userId: Long
): MessageResponse {
return doReleaseCoordinatorService.startBatchReleaseAsyncSingle(doId, userId)
}
@GetMapping("/batch-release/progress/{jobId}")
fun getBatchReleaseProgress(@PathVariable jobId: String): MessageResponse {
return doReleaseCoordinatorService.getBatchReleaseProgress(jobId)
}

@GetMapping("/ticket-release-table")
fun getTicketReleaseTable(


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt Datei anzeigen

@@ -58,6 +58,8 @@ data class JobOrderInfoWithTypeName(
val item: JobOrderItemInfo,
val stockInLineId: Long?,
val stockInLineStatus: String?,
val sufficientCount: Int?,
val insufficientCount: Int?,
val silHandlerId: Long?,
val planStart: LocalDateTime?,
val status: String,


+ 246
- 177
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt Datei anzeigen

@@ -39,6 +39,11 @@ import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus
import com.ffii.fpsms.modules.productProcess.entity.ProductProcessLineRepository
import com.ffii.fpsms.modules.stock.entity.StockOutRepository
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderProcessRepository
import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderLotsHierarchicalResponse
import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoResponse
import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderBasicInfoResponse
import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderLineWithLotsResponse
import com.ffii.fpsms.modules.jobOrder.web.model.LotDetailResponse
@Service
open class JoPickOrderService(
private val joPickOrderRepository: JoPickOrderRepository,
@@ -215,7 +220,7 @@ open class JoPickOrderService(
}
return joPickOrderRecordRepository.saveAll(joPickOrderRecords)
}
open fun getAllJobOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> {
open fun getAllJobOrderLotsWithDetailsHierarchical(userId: Long): JobOrderLotsHierarchicalResponse {
println("=== Debug: getAllJobOrderLotsWithDetailsHierarchical ===")
println("today: ${LocalDate.now()}")
println("userId filter: $userId")
@@ -224,25 +229,38 @@ open class JoPickOrderService(
val user = userService.find(userId).orElse(null)
if (user == null) {
println("❌ User not found: $userId")
return emptyMap()
// ✅ 修复:返回 JobOrderLotsHierarchicalResponse
return JobOrderLotsHierarchicalResponse(
pickOrder = PickOrderInfoResponse(
id = null,
code = null,
consoCode = null,
targetDate = null,
type = null,
status = null,
assignTo = null,
jobOrder = JobOrderBasicInfoResponse(0, "", "")
),
pickOrderLines = emptyList()
)
}
val statusList = listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED)
// Get all pick orders assigned to user with PENDING or RELEASED status that have joId
val allAssignedPickOrders = pickOrderRepository.findAllByAssignToIdAndStatusIn(
userId,
statusList
).filter { it.jobOrder != null } // Only pick orders with joId
println("🔍 DEBUG: Found ${allAssignedPickOrders.size} job order pick orders assigned to user $userId")
// 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 {
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 job orders, showing only those")
@@ -251,7 +269,8 @@ open class JoPickOrderService(
// 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 }
val latestCompleted =
completedOrders.maxByOrNull { it.completeDate ?: it.modified ?: LocalDateTime.MIN }
println("🔍 DEBUG: No assigned RELEASED job orders, showing latest completed order: ${latestCompleted?.code}")
listOfNotNull(latestCompleted)
} else {
@@ -262,42 +281,68 @@ open class JoPickOrderService(
} else {
emptyList()
}
val pickOrderIds = filteredPickOrders.map { it.id!! }
println(" Job Order Pick order IDs to fetch: $pickOrderIds")
if (pickOrderIds.isEmpty()) {
return mapOf(
"pickOrder" to null as Any?,
"pickOrderLines" to emptyList<Map<String, Any>>() as Any?
return JobOrderLotsHierarchicalResponse(
pickOrder = PickOrderInfoResponse(
id = null,
code = null,
consoCode = null,
targetDate = null,
type = null,
status = null,
assignTo = null,
jobOrder = JobOrderBasicInfoResponse(0, "", "")
),
pickOrderLines = emptyList()
)
}
// 使用 Repository 获取数据
try {
val pickOrders = pickOrderRepository.findAllByIdIn(pickOrderIds)
.filter { it.deleted == false && it.assignTo?.id == userId }
if (pickOrders.isEmpty()) {
return mapOf(
"pickOrder" to null as Any?,
"pickOrderLines" to emptyList<Map<String, Any>>() as Any?
return JobOrderLotsHierarchicalResponse(
pickOrder = PickOrderInfoResponse(
id = null,
code = null,
consoCode = null,
targetDate = null,
type = null,
status = null,
assignTo = null,
jobOrder = JobOrderBasicInfoResponse(0, "", "")
),
pickOrderLines = emptyList()
)
}
val pickOrder = pickOrders.first() // 取第一个(应该只有一个)
val jobOrder = pickOrder.jobOrder ?: return mapOf(
"pickOrder" to null as Any?,
"pickOrderLines" to emptyList<Map<String, Any>>() as Any?
val jobOrder = pickOrder.jobOrder ?: return JobOrderLotsHierarchicalResponse(
pickOrder = PickOrderInfoResponse(
id = null,
code = null,
consoCode = null,
targetDate = null,
type = null,
status = null,
assignTo = null,
jobOrder = JobOrderBasicInfoResponse(0, "", "")
),
pickOrderLines = emptyList()
)
// 获取 pick order lines
val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!)
.filter { it.deleted == false }
// 获取所有 pick order line IDs
val pickOrderLineIds = pickOrderLines.map { it.id!! }
// 获取 suggested pick lots
val suggestedPickLots = if (pickOrderLineIds.isNotEmpty()) {
suggestPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds)
@@ -305,10 +350,10 @@ open class JoPickOrderService(
} else {
emptyList()
}
// 获取所有 inventory lot line IDs
val inventoryLotLineIds = suggestedPickLots.mapNotNull { it.suggestedLotLine?.id }
// 获取 inventory lot lines
val inventoryLotLines = if (inventoryLotLineIds.isNotEmpty()) {
inventoryLotLineRepository.findAllByIdIn(inventoryLotLineIds)
@@ -316,7 +361,7 @@ open class JoPickOrderService(
} else {
emptyList()
}
// 获取 inventory lots
val inventoryLotIds = inventoryLotLines.mapNotNull { it.inventoryLot?.id }.distinct()
val inventoryLots = if (inventoryLotIds.isNotEmpty()) {
@@ -325,7 +370,7 @@ open class JoPickOrderService(
} else {
emptyList()
}
// 获取 stock out lines
val stockOutLines = if (pickOrderLineIds.isNotEmpty() && inventoryLotLineIds.isNotEmpty()) {
pickOrderLineIds.flatMap { polId ->
@@ -342,13 +387,13 @@ open class JoPickOrderService(
}
// 获取 jo_pick_order 记录
val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!)
// 构建 pick order info
val pickOrderInfo = mapOf(
"id" to pickOrder.id,
"code" to pickOrder.code,
"consoCode" to pickOrder.consoCode,
"targetDate" to pickOrder.targetDate?.let {
"targetDate" to pickOrder.targetDate?.let {
"${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}"
},
"type" to pickOrder.type?.value,
@@ -360,49 +405,35 @@ open class JoPickOrderService(
"name" to "Job Order ${jobOrder.code}"
)
)
// 构建 pick order lines with lots
val pickOrderLinesResult = pickOrderLines.map { pol ->
val item = pol.item
val uom = pol.uom
val lineId = pol.id!!
val suggestions = suggestedPickLots.filter { it.pickOrderLine?.id == lineId }
val stockoutsForLine = stockOutLinesByPickOrderLine[lineId].orEmpty()
// 获取该 line 的 suggested pick lots
val lineSuggestedLots = suggestedPickLots.filter { it.pickOrderLine?.id == pol.id }

// 构建 lots 数据
val lots = lineSuggestedLots.mapNotNull { spl ->
val ill = spl.suggestedLotLine
if (ill == null || ill.deleted == true) return@mapNotNull null
val il = ill.inventoryLot
if (il == null || il.deleted == true) return@mapNotNull null
val warehouse = ill.warehouse
// 获取对应的 stock out line
val sol = stockOutLines.firstOrNull {
it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id
val sol = stockOutLines.firstOrNull {
it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id
}
// 获取对应的 jo_pick_order
val jpo = joPickOrders.firstOrNull { it.itemId == item?.id }
// 计算 available quantity

val availableQty = if (sol?.status == "rejected") {
null
} else {
(ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty ?: BigDecimal.ZERO)
(ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty
?: BigDecimal.ZERO)
}
// 计算 total picked by all pick orders
val totalPickedByAllPickOrders = stockOutLines
.filter { it.inventoryLotLine?.id == ill.id && it.deleted == false }
.filter { it.status in listOf("pending", "checked", "partially_completed", "completed") }
.sumOf { it.qty?.toBigDecimal() ?: BigDecimal.ZERO }
// 计算 lot availability

val lotAvailability = when {
il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired"
sol?.status == "rejected" -> "rejected"
@@ -410,72 +441,97 @@ open class JoPickOrderService(
ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable"
else -> "available"
}
// 计算 processing status

val processingStatus = when (sol?.status) {
"completed" -> "completed"
"rejected" -> "rejected"
"created" -> "pending"
else -> "pending"
}
mapOf(
"lotId" to ill.id,
"lotNo" to il.lotNo,
"expiryDate" to il.expiryDate?.let {
LotDetailResponse(
lotId = ill.id,
lotNo = il.lotNo,
expiryDate = il.expiryDate?.let {
"${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}"
},
"location" to warehouse?.name,
"availableQty" to availableQty?.toDouble(),
"requiredQty" to (spl.qty?.toDouble() ?: 0.0),
"actualPickQty" to (sol?.qty ?: 0.0),
"processingStatus" to processingStatus,
"lotAvailability" to lotAvailability,
"pickOrderId" to pickOrder.id,
"pickOrderCode" to pickOrder.code,
"pickOrderConsoCode" to pickOrder.consoCode,
"pickOrderLineId" to pol.id,
"stockOutLineId" to sol?.id,
"suggestedPickLotId" to spl.id,
"stockOutLineQty" to (sol?.qty ?: 0.0),
"stockOutLineStatus" to sol?.status,
"routerIndex" to warehouse?.order,
"routerArea" to warehouse?.code,
"routerRoute" to warehouse?.code,
"uomShortDesc" to uom?.udfShortDesc,
"matchStatus" to jpo?.matchStatus?.value,
"matchBy" to jpo?.matchBy,
"matchQty" to jpo?.matchQty
location = warehouse?.name,
availableQty = availableQty?.toDouble(),
requiredQty = spl.qty?.toDouble() ?: 0.0,
actualPickQty = sol?.qty ?: 0.0,
processingStatus = processingStatus,
lotAvailability = lotAvailability,
pickOrderId = pickOrder.id,
pickOrderCode = pickOrder.code,
pickOrderConsoCode = pickOrder.consoCode,
pickOrderLineId = pol.id,
stockOutLineId = sol?.id,
suggestedPickLotId = spl.id,
stockOutLineQty = sol?.qty ?: 0.0,
stockOutLineStatus = sol?.status,
routerIndex = warehouse?.order,
routerArea = warehouse?.code,
routerRoute = warehouse?.code,
uomShortDesc = uom?.udfShortDesc,
matchStatus = jpo?.matchStatus?.value,
matchBy = jpo?.matchBy,
matchQty = jpo?.matchQty?.toDouble()
)
}
mapOf(
"id" to pol.id,
"itemId" to item?.id,
"itemCode" to item?.code,
"itemName" to item?.name,
"requiredQty" to pol.qty?.toDouble(),
"uomCode" to uom?.code,
"uomDesc" to uom?.udfudesc,
"lots" to lots

PickOrderLineWithLotsResponse(
id = pol.id!!,
itemId = item?.id,
itemCode = item?.code,
itemName = item?.name,
requiredQty = pol.qty?.toDouble(),
uomCode = uom?.code,
uomDesc = uom?.udfudesc,
status = pol.status?.value,
lots = lots
)
}
return mapOf(
"pickOrder" to pickOrderInfo as Any?,
"pickOrderLines" to pickOrderLinesResult as Any?

// ✅ 修复第469行:返回 JobOrderLotsHierarchicalResponse
return JobOrderLotsHierarchicalResponse(
pickOrder = PickOrderInfoResponse(
id = pickOrder.id,
code = pickOrder.code,
consoCode = pickOrder.consoCode,
targetDate = pickOrder.targetDate?.let {
"${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}"
},
type = pickOrder.type?.value,
status = pickOrder.status?.value,
assignTo = pickOrder.assignTo?.id,
jobOrder = JobOrderBasicInfoResponse(
id = jobOrder.id!!,
code = jobOrder.code ?: "", // ✅ 修复第482行:处理空值
name = "Job Order ${jobOrder.code ?: ""}"
)
),
pickOrderLines = pickOrderLinesResult // ✅ 修复第486行:使用转换后的结果
)
} catch (e: Exception) {
println("❌ Error executing Job Order hierarchical query: ${e.message}")
e.printStackTrace()
return mapOf(
"pickOrder" to null as Any?,
"pickOrderLines" to emptyList<Map<String, Any>>() as Any?
// ✅ 修复 catch 块:返回 JobOrderLotsHierarchicalResponse 而不是 Map
return JobOrderLotsHierarchicalResponse(
pickOrder = PickOrderInfoResponse(
id = null,
code = null,
consoCode = null,
targetDate = null,
type = null,
status = null,
assignTo = null,
jobOrder = JobOrderBasicInfoResponse(0, "", "")
),
pickOrderLines = emptyList()
)
}
}

// Get completed job order pick orders (for second tab)
// Fix the getCompletedJobOrderLotsHierarchical method
open fun getCompletedJobOrderLotsHierarchical(userId: Long): Map<String, Any?> {
@@ -1725,7 +1781,7 @@ open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> {
emptyList()
}
}
open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String, Any?> {
open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLotsHierarchicalResponse {
println("=== getJobOrderLotsHierarchicalByPickOrderId ===")
println("pickOrderId: $pickOrderId")
@@ -1733,17 +1789,36 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String
val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null)
if (pickOrder == null || pickOrder.deleted == true) {
println("❌ Pick order $pickOrderId not found or deleted")
return mapOf(
"pickOrder" to null as Any?,
"pickOrderLines" to emptyList<Map<String, Any>>() as Any?
return JobOrderLotsHierarchicalResponse(
pickOrder = PickOrderInfoResponse(
id = null,
code = null,
consoCode = null,
targetDate = null,
type = null,
status = null,
assignTo = null,
jobOrder = JobOrderBasicInfoResponse(0, "", "")
),
pickOrderLines = emptyList()
)
}
val jobOrder = pickOrder.jobOrder ?: return mapOf(
"pickOrder" to null as Any?,
"pickOrderLines" to emptyList<Map<String, Any>>() as Any?
val jobOrder = pickOrder.jobOrder ?: return JobOrderLotsHierarchicalResponse(
pickOrder = PickOrderInfoResponse(
id = null,
code = null,
consoCode = null,
targetDate = null,
type = null,
status = null,
assignTo = null,
jobOrder = JobOrderBasicInfoResponse(0, "", "")
),
pickOrderLines = emptyList()
)
// ✅ 添加数据获取逻辑(从原始 Map 版本复制)
// 获取 pick order lines
val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!)
.filter { it.deleted == false }
@@ -1799,20 +1874,20 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String
val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!)
// 构建 pick order info
val pickOrderInfo = mapOf(
"id" to pickOrder.id,
"code" to pickOrder.code,
"consoCode" to pickOrder.consoCode,
"targetDate" to pickOrder.targetDate?.let {
val pickOrderInfo = PickOrderInfoResponse(
id = pickOrder.id,
code = pickOrder.code,
consoCode = pickOrder.consoCode,
targetDate = pickOrder.targetDate?.let {
"${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}"
},
"type" to pickOrder.type?.value,
"status" to pickOrder.status?.value,
"assignTo" to pickOrder.assignTo?.id,
"jobOrder" to mapOf(
"id" to jobOrder.id,
"code" to jobOrder.code,
"name" to "Job Order ${jobOrder.code}"
type = pickOrder.type?.value,
status = pickOrder.status?.value,
assignTo = pickOrder.assignTo?.id,
jobOrder = JobOrderBasicInfoResponse(
id = jobOrder.id!!,
code = jobOrder.code ?: "",
name = "Job Order ${jobOrder.code}"
)
)
@@ -1821,9 +1896,6 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String
val item = pol.item
val uom = pol.uom
val lineId = pol.id!!
val suggestions = suggestedPickLots.filter { it.pickOrderLine?.id == lineId }
val stockoutsForLine = stockOutLinesByPickOrderLine[lineId].orEmpty()
// 获取该 line 的 suggested pick lots
val lineSuggestedLots = suggestedPickLots.filter { it.pickOrderLine?.id == pol.id }
// 构建 lots 数据
@@ -1835,29 +1907,17 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String
if (il == null || il.deleted == true) return@mapNotNull null
val warehouse = ill.warehouse
// 获取对应的 stock out line
val sol = stockOutLines.firstOrNull {
it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id
}
// 获取对应的 jo_pick_order
val jpo = joPickOrders.firstOrNull { it.itemId == item?.id }
// 计算 available quantity
val availableQty = if (sol?.status == "rejected") {
null
} else {
(ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty ?: BigDecimal.ZERO)
}
// 计算 total picked by all pick orders
val totalPickedByAllPickOrders = stockOutLines
.filter { it.inventoryLotLine?.id == ill.id && it.deleted == false }
.filter { it.status in listOf("pending", "checked", "partially_completed", "completed") }
.sumOf { it.qty?.toBigDecimal() ?: BigDecimal.ZERO }
// 计算 lot availability
val lotAvailability = when {
il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired"
sol?.status == "rejected" -> "rejected"
@@ -1866,7 +1926,6 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String
else -> "available"
}
// 计算 processing status
val processingStatus = when (sol?.status) {
"completed" -> "completed"
"rejected" -> "rejected"
@@ -1874,59 +1933,69 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String
else -> "pending"
}
mapOf(
"lotId" to ill.id,
"lotNo" to il.lotNo,
"expiryDate" to il.expiryDate?.let {
LotDetailResponse(
lotId = ill.id,
lotNo = il.lotNo,
expiryDate = il.expiryDate?.let {
"${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}"
},
"location" to warehouse?.name,
"availableQty" to availableQty?.toDouble(),
"requiredQty" to (spl.qty?.toDouble() ?: 0.0),
"actualPickQty" to (sol?.qty ?: 0.0),
"processingStatus" to processingStatus,
"lotAvailability" to lotAvailability,
"pickOrderId" to pickOrder.id,
"pickOrderCode" to pickOrder.code,
"pickOrderConsoCode" to pickOrder.consoCode,
"pickOrderLineId" to pol.id,
"stockOutLineId" to sol?.id,
"suggestedPickLotId" to spl.id,
"stockOutLineQty" to (sol?.qty ?: 0.0),
"stockOutLineStatus" to sol?.status,
"routerIndex" to warehouse?.order,
"routerArea" to warehouse?.code,
"routerRoute" to warehouse?.code,
"uomShortDesc" to uom?.udfShortDesc,
"matchStatus" to jpo?.matchStatus?.value,
"matchBy" to jpo?.matchBy,
"matchQty" to jpo?.matchQty
location = warehouse?.name,
availableQty = availableQty?.toDouble(),
requiredQty = spl.qty?.toDouble() ?: 0.0,
actualPickQty = sol?.qty ?: 0.0,
processingStatus = processingStatus,
lotAvailability = lotAvailability,
pickOrderId = pickOrder.id,
pickOrderCode = pickOrder.code,
pickOrderConsoCode = pickOrder.consoCode,
pickOrderLineId = pol.id,
stockOutLineId = sol?.id,
suggestedPickLotId = spl.id,
stockOutLineQty = sol?.qty ?: 0.0,
stockOutLineStatus = sol?.status,
routerIndex = warehouse?.order,
routerArea = warehouse?.code,
routerRoute = warehouse?.code,
uomShortDesc = uom?.udfShortDesc,
matchStatus = jpo?.matchStatus?.value,
matchBy = jpo?.matchBy,
matchQty = jpo?.matchQty?.toDouble()
)
}
mapOf(
"id" to pol.id,
"itemId" to item?.id,
"itemCode" to item?.code,
"itemName" to item?.name,
"requiredQty" to pol.qty?.toDouble(),
"uomCode" to uom?.code,
"uomDesc" to uom?.udfudesc,
"lots" to lots
PickOrderLineWithLotsResponse(
id = pol.id!!,
itemId = item?.id,
itemCode = item?.code,
itemName = item?.name,
requiredQty = pol.qty?.toDouble(),
uomCode = uom?.code,
uomDesc = uom?.udfudesc,
status = pol.status?.value,
lots = lots
)
}
return mapOf(
"pickOrder" to pickOrderInfo as Any?,
"pickOrderLines" to pickOrderLinesResult as Any?
return JobOrderLotsHierarchicalResponse(
pickOrder = pickOrderInfo,
pickOrderLines = pickOrderLinesResult
)
} catch (e: Exception) {
println("❌ Error in getJobOrderLotsHierarchicalByPickOrderId: ${e.message}")
e.printStackTrace()
return mapOf(
"pickOrder" to null as Any?,
"pickOrderLines" to emptyList<Map<String, Any>>() as Any?
return JobOrderLotsHierarchicalResponse(
pickOrder = PickOrderInfoResponse(
id = null,
code = null,
consoCode = null,
targetDate = null,
type = null,
status = null,
assignTo = null,
jobOrder = JobOrderBasicInfoResponse(0, "", "")
),
pickOrderLines = emptyList()
)
}
}


+ 100
- 5
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt Datei anzeigen

@@ -56,7 +56,7 @@ import java.io.FileNotFoundException
import java.io.IOException
import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderInfoWithTypeName
import com.ffii.fpsms.modules.jobOrder.entity.projections.JobTypeResponse
import com.ffii.fpsms.modules.stock.entity.InventoryRepository

@Service
open class JobOrderService(
@@ -73,7 +73,8 @@ open class JobOrderService(
val stockOutRepository: StockOutRepository,
val stockOutLineRepository: StockOutLIneRepository,
private val printerService: PrinterService,
val jobTypeRepository: JobTypeRepository
val jobTypeRepository: JobTypeRepository,
val inventoryRepository: InventoryRepository
) {

open fun allJobOrdersByPage(request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfo> {
@@ -116,6 +117,27 @@ open class JobOrderService(
emptyMap()
}
// 获取所有 JobOrder IDs,用于批量加载 jobms
val jobOrderIds = response.content.map { it.id }.distinct()
val jobOrdersMap = if (jobOrderIds.isNotEmpty()) {
jobOrderRepository.findAllById(jobOrderIds).associateBy { it.id }
} else {
emptyMap()
}
// 获取所有涉及的 itemIds,用于批量加载 inventory
val allItemIds = jobOrdersMap.values
.flatMap { it.jobms }
.mapNotNull { it.item?.id }
.distinct()
val inventoriesMap = if (allItemIds.isNotEmpty()) {
inventoryRepository.findInventoryInfoByItemIdInAndDeletedIsFalse(allItemIds)
.associateBy { it.itemId }
} else {
emptyMap()
}
val planStartFrom = request.planStart
val planStartTo = request.planStartTo
val records = response.content
@@ -124,6 +146,15 @@ open class JobOrderService(
(planStartTo == null || (it.planStart != null && (planStartTo.isEqual(it.planStart) || planStartTo.isAfter(it.planStart))))
}
.map { info ->
val jobOrder = jobOrdersMap[info.id]
// 计算 sufficientCount 和 insufficientCount
val (sufficientCount, insufficientCount) = if (jobOrder != null) {
calculateStockCounts(jobOrder, inventoriesMap)
} else {
Pair(null, null)
}
JobOrderInfoWithTypeName(
id = info.id,
code = info.code,
@@ -134,6 +165,8 @@ open class JobOrderService(
item = info.item,
stockInLineId = info.stockInLineId,
stockInLineStatus = info.stockInLineStatus,
sufficientCount = sufficientCount,
insufficientCount = insufficientCount,
silHandlerId = info.silHandlerId,
planStart = info.planStart,
status = info.status,
@@ -152,6 +185,55 @@ open class JobOrderService(
val total = response.totalElements
return RecordsRes<JobOrderInfoWithTypeName>(records, total.toInt());
}
// 添加辅助方法计算库存统计
private fun calculateStockCounts(
jobOrder: JobOrder,
inventoriesMap: Map<Long?, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo>
): Pair<Int, Int> {
// 过滤掉 consumables 和 CMB 类型的物料
val nonConsumablesJobms = jobOrder.jobms.filter { jobm ->
val itemType = jobm.item?.type?.lowercase()
itemType != "consumables" && itemType != "cmb"
}
if (nonConsumablesJobms.isEmpty()) {
return Pair(0, 0)
}
var sufficientCount = 0
var insufficientCount = 0
nonConsumablesJobms.forEach { jobm ->
val itemId = jobm.item?.id
val reqQty = jobm.reqQty ?: BigDecimal.ZERO
if (itemId != null) {
val inventory = inventoriesMap[itemId]
val availableQty = if (inventory != null) {
// 使用 availableQty,如果没有则计算:onHandQty - onHoldQty - unavailableQty
inventory.availableQty ?: (
(inventory.onHandQty ?: BigDecimal.ZERO) -
(inventory.onHoldQty ?: BigDecimal.ZERO) -
(inventory.unavailableQty ?: BigDecimal.ZERO)
)
} else {
BigDecimal.ZERO
}
if (availableQty >= reqQty) {
sufficientCount++
} else {
insufficientCount++
}
} else {
// 如果没有 itemId,视为不足
insufficientCount++
}
}
return Pair(sufficientCount, insufficientCount)
}
open fun jobOrderDetail(id: Long): JobOrderDetail {
val sqlResult = jobOrderRepository.findJobOrderDetailById(id) ?: throw NoSuchElementException();

@@ -308,13 +390,26 @@ open class JobOrderService(

// 添加 suggested pick lots 创建逻辑
val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!)
println("DEBUG: pickOrderLines=${lines.map { it.id to it.item?.code }}")
if (lines.isNotEmpty()) {
val suggestions = suggestedPickLotService.suggestionForPickOrderLines(
val suggestions = suggestedPickLotService.suggestionForPickOrderLinesForJobOrder(
SuggestedPickLotForPolRequest(pickOrderLines = lines)
)

println("DEBUG: suggestions.size=${suggestions.suggestedList.size}")
suggestions.suggestedList.forEach { s ->
println(
"DEBUG: suggestion polId=${s.pickOrderLine?.id}, lotLineId=${s.suggestedLotLine?.id}, " +
"lotNo=${s.suggestedLotLine?.inventoryLot?.lotNo}, qty=${s.qty}, pickSuggested=${s.pickSuggested}"
)
}
val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList)

println("DEBUG: saved suggestions size=${saveSuggestedPickLots.size}")
saveSuggestedPickLots.forEach { s ->
println(
"DEBUG: saved polId=${s.pickOrderLine?.id}, lotLineId=${s.suggestedLotLine?.id}, " +
"lotNo=${s.suggestedLotLine?.inventoryLot?.lotNo}, qty=${s.qty}"
)
}
// Hold inventory quantities
val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(
saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id }


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt Datei anzeigen

@@ -100,7 +100,7 @@ class JobOrderController(
return jo
}
@GetMapping("/all-lots-hierarchical/{userId}")
fun getAllJobOrderLotsHierarchical(@PathVariable userId: Long): Map<String, Any?> {
fun getAllJobOrderLotsHierarchical(@PathVariable userId: Long): JobOrderLotsHierarchicalResponse {
return joPickOrderService.getAllJobOrderLotsWithDetailsHierarchical(userId)
}

@@ -235,7 +235,7 @@ fun recordSecondScanIssue(
}

@GetMapping("/all-lots-hierarchical-by-pick-order/{pickOrderId}")
fun getJobOrderLotsHierarchicalByPickOrderId(@PathVariable pickOrderId: Long): Map<String, Any?> {
fun getJobOrderLotsHierarchicalByPickOrderId(@PathVariable pickOrderId: Long): JobOrderLotsHierarchicalResponse {
return joPickOrderService.getJobOrderLotsHierarchicalByPickOrderId(pickOrderId)
}
@PostMapping("/update-jo-pick-order-handled-by")


+ 64
- 10
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt Datei anzeigen

@@ -51,18 +51,72 @@ data class AllJoPickOrderResponse(
val finishedPickOLineCount: Int,

)
data class JobOrderLotsHierarchicalResponse(
val id: Long,
val lotId: Long?,
val lotCode: String?,
val lotName: String?,
val lotQty: BigDecimal?,
val lotUomId: Long?,
val lotUomName: String?,
)


data class UpdateJoPickOrderHandledByRequest(
val pickOrderId: Long,
val itemId: Long,
val userId: Long
)
)
data class JobOrderLotsHierarchicalResponse(
val pickOrder: PickOrderInfoResponse,
val pickOrderLines: List<PickOrderLineWithLotsResponse>
)

data class PickOrderInfoResponse(
val id: Long?,
val code: String?,
val consoCode: String?,
val targetDate: String?,
val type: String?,
val status: String?,
val assignTo: Long?,
val jobOrder: JobOrderBasicInfoResponse
)

data class JobOrderBasicInfoResponse(
val id: Long,
val code: String,
val name: String
)

data class PickOrderLineWithLotsResponse(
val id: Long,
val itemId: Long?,
val itemCode: String?,
val itemName: String?,
val requiredQty: Double?,
val uomCode: String?,
val uomDesc: String?,
val status: String?,
val lots: List<LotDetailResponse>
)

data class LotDetailResponse(
val lotId: Long?,
val lotNo: String?,
val expiryDate: String?,
val location: String?,
val availableQty: Double?,
val requiredQty: Double?,
val actualPickQty: Double?,
val processingStatus: String?,
val lotAvailability: String?,
val pickOrderId: Long?,
val pickOrderCode: String?,
val pickOrderConsoCode: String?,
val pickOrderLineId: Long?,
val stockOutLineId: Long?,
val suggestedPickLotId: Long?,
val stockOutLineQty: Double?,
val stockOutLineStatus: String?,
val routerIndex: Int?,
val routerArea: String?,
val routerRoute: String?,
val uomShortDesc: String?,
val matchStatus: String?,
val matchBy: Long?,
val matchQty: Double?
)



+ 7
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductionProcessIssue.kt Datei anzeigen

@@ -14,6 +14,11 @@ open class ProductionProcessIssue : BaseEntity<Long>() { // 修复:改为正
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "productprocessid", nullable = false)
open var productProcess: ProductProcess? = null


@Column(name = "productProcessLineId")
open var productProcessLineId: Long? = null

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "operatorId")
@@ -36,4 +41,6 @@ open class ProductionProcessIssue : BaseEntity<Long>() { // 修复:改为正
@Column(name = "totalTime")
open var totalTime: Int? = null // 分钟
@Column(name = "status")
open var status: String? = null
}

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductionProcessIssueRepository.kt Datei anzeigen

@@ -6,4 +6,5 @@ import org.springframework.stereotype.Repository
@Repository
interface ProductionProcessIssueRepository : JpaRepository<ProductionProcessIssue, Long> {
fun findByProductProcess_Id(productProcessId: Long): List<ProductionProcessIssue>
fun findByProductProcessLineId(productProcessLineId: Long):List<ProductionProcessIssue>
}

+ 89
- 5
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Datei anzeigen

@@ -28,6 +28,7 @@ import com.ffii.fpsms.modules.master.entity.Process
import com.ffii.fpsms.modules.master.entity.Equipment
import com.ffii.fpsms.modules.master.entity.BomProcess
import com.ffii.fpsms.modules.master.entity.Bom

import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderBomMaterialRepository
import com.ffii.fpsms.modules.master.service.ProductionScheduleService
@@ -39,8 +40,10 @@ import com.ffii.fpsms.modules.stock.service.StockInLineService
import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest
import com.ffii.fpsms.modules.master.entity.BomProcessMaterialRepository
import com.ffii.fpsms.modules.master.entity.BomMaterialRepository
import com.ffii.fpsms.modules.productProcess.entity.ProductionProcessIssue
import com.ffii.fpsms.modules.jobOrder.entity.JobTypeRepository

import java.time.ZoneOffset
@Service
@Transactional
open class ProductProcessService(
@@ -532,11 +535,20 @@ open class ProductProcessService(
planEndDate.atStartOfDay(),
"detailed"
)
scheduleLine?.itemPriority?.toString() ?: "0"
if (scheduleLine != null) {
// 计算 targetMinStock
val targetMinStock = scheduleLine.lastMonthAvgSales * 2
// 调整后的生产数量(与 generateDetailedScheduleByDay 逻辑一致)
val adjustedProdQty = scheduleLine.prodQty * 2
// 计算 difference(缺口)
val difference = -(targetMinStock + adjustedProdQty - scheduleLine.estCloseBal)
difference.toString()
} else {
"0"
}
} else {
"0"
}
}
fun calculateColourScore(value: Int?): String {
return when (value) {
0 -> "淺"
@@ -899,6 +911,9 @@ open class ProductProcessService(
}
open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse {
val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null)
val bomProcess = bomProcessRepository.findById(productProcessLine?.bomProcess?.id?:0L).orElse(null)
val productProcessIssue = productionProcessIssueRepository.findByProductProcessLineId(productProcessLineId).filter{it.status == "Paused"}.firstOrNull()

return JobOrderProcessLineDetailResponse(
id = productProcessLine.id?:0,
productProcessId = productProcessLine.productProcess ?.id?:0,
@@ -906,7 +921,11 @@ open class ProductProcessService(
operatorId = productProcessLine.operator?.id?:0,
operatorName = productProcessLine.operator?.name?:"",
handlerId = productProcessLine.handler?.id?:0,
durationInMinutes = productProcessLine.bomProcess?.durationInMinute?:0,
durationInMinutes = bomProcess?.durationInMinute?:0,

productProcessIssueId = productProcessIssue?.id?:0,
productProcessIssueStatus = productProcessIssue?.status?:"",
seqNo = productProcessLine.seqNo?:0,
name = productProcessLine.name?:"",
description = productProcessLine.description?:"",
@@ -1233,5 +1252,70 @@ open class ProductProcessService(
errorPosition = null,
)
}
open fun SaveProductProcessIssueTime(request: SaveProductProcessIssueTimeRequest): MessageResponse {
println("📋 Service: Saving ProductProcess Issue Time: ${request.productProcessLineId}")
val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null)
val productProcess = productProcessRepository.findById(productProcessLine.productProcess?.id?:0L).orElse(null)
//val productProcessLines=productProcessLineRepository.findByProductProcess_Id(productProcessId)
val startTime=productProcessLine?.startTime
println("📋 Service: Start Time: $startTime")
val stopTime=LocalDateTime.now()
println("📋 Service: Stop Time: $stopTime")
val operatorId=productProcessLine.operator?.id
val Operator=userRepository.findById(operatorId).orElse(null)
val reason = request.reason
val productProcessIssue = ProductionProcessIssue().apply {
this.productProcess = productProcess
this.productProcessLineId = productProcessLine.id
this.operator = productProcessLine.operator
this.operatorName = Operator.name
this.reason = reason
this.status = "Paused"
//this.productProcess = operator.name
//this.startTime = startTime
this.stopTime = stopTime
}
productionProcessIssueRepository.save(productProcessIssue)
productProcessLine.status = "Paused"
productProcessLineRepository.save(productProcessLine)
return MessageResponse(
id = 0,
code = "200",
name = "ProductProcess IssueTime Updated",
type = "success",
message = "ProductProcess IssueTime Updated",
errorPosition = null,
)
}


open fun SaveProductProcessResumeTime(productProcessIssueId: Long): MessageResponse {
println("📋 Service: Saving ProductProcess Resume Time: $productProcessIssueId")
val productProcessLineIssue = productionProcessIssueRepository.findById(productProcessIssueId).orElse(null)
val productProcessLine = productProcessLineRepository.findById(productProcessLineIssue.productProcessLineId?:0L).orElse(null)
val resumeTime = LocalDateTime.now()
println("📋 Service: Resume Time: $resumeTime")
productProcessLineIssue?.resumeTime = resumeTime
println("📋 Service: Resume Time: $resumeTime")
val totalTime = resumeTime.toEpochSecond(ZoneOffset.UTC) - (productProcessLineIssue.stopTime?.toEpochSecond(ZoneOffset.UTC) ?: 0L)
productProcessLineIssue?.totalTime = totalTime.toInt()
productProcessLineIssue?.status = "Resumed"
productionProcessIssueRepository.save(productProcessLineIssue)
productProcessLine.status = "InProgress"
productProcessLineRepository.save(productProcessLine)

return MessageResponse(
id = 0,
code = "200",
name = "ProductProcess ResumeTime Updated",
type = "success",
message = "ProductProcess ResumeTime Updated",
errorPosition = null,
)

}
}


+ 8
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt Datei anzeigen

@@ -197,4 +197,12 @@ class ProductProcessController(
fun completeProductProcessLine(@PathVariable lineId: Long): MessageResponse {
return productProcessService.CompleteProductProcessLine(lineId)
}
@PostMapping("/Demo/ProcessLine/issue")
fun issueProductProcessLine(@RequestBody request: SaveProductProcessIssueTimeRequest): MessageResponse {
return productProcessService.SaveProductProcessIssueTime(request)
}
@PostMapping("/Demo/ProcessLine/resume/{productProcessIssueId}")
fun resumeProductProcessLine(@PathVariable productProcessIssueId: Long): MessageResponse {
return productProcessService.SaveProductProcessResumeTime(productProcessIssueId)
}
}

+ 10
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt Datei anzeigen

@@ -132,7 +132,8 @@ data class JobOrderProcessLineDetailResponse(
val equipmentId: Long?,
val startTime: LocalDateTime?,
val endTime: LocalDateTime?,

val productProcessIssueId: Long?,
val productProcessIssueStatus: String?,
val status: String,
val outputFromProcessQty: Int?,
val outputFromProcessUom: String?,
@@ -190,4 +191,12 @@ data class NewUpdateProductProcessLineOperatorIdOrEquipmentIdAndEquipmentDetailR
val EquipmentTypeSubTypeEquipmentNo: String,
val staffNo: String?,
val Name: String?,
)
data class SaveProductProcessIssueTimeRequest(
val productProcessLineId: Long,
val reason: String
)
data class SaveProductProcessResumeTimeRequest(
val productProcessLineId: Long,
val resumeTime: LocalDateTime?
)

+ 142
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Datei anzeigen

@@ -235,6 +235,148 @@ open class SuggestedPickLotService(
}
return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList)
}
open fun suggestionForPickOrderLinesForJobOrder(request: SuggestedPickLotForPolRequest): SuggestedPickLotResponse {
val pols = request.pickOrderLines
val itemIds = pols.mapNotNull { it.item?.id }
val zero = BigDecimal.ZERO
val one = BigDecimal.ONE
val today = LocalDate.now()
val suggestedList: MutableList<SuggestedPickLot> = mutableListOf()
val holdQtyMap: MutableMap<Long?, BigDecimal?> = request.holdQtyMap
// get current inventory lot line qty & grouped by item id
val availableInventoryLotLines = inventoryLotLineService
.allInventoryLotLinesByItemIdIn(itemIds)
.filter { it.status == InventoryLotLineStatus.AVAILABLE.value }
.filter { (it.inQty ?: zero).minus(it.outQty ?: zero).minus(it.holdQty ?: zero) > zero }
.filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today)}
.sortedBy { it.expiryDate }
.groupBy { it.item?.id }
// loop for suggest pick lot line
pols.forEach { line ->
val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
val lotLines = availableInventoryLotLines[line.item?.id].orEmpty()
val ratio = one // (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP)
// FIX: Calculate remaining quantity needed (not the full required quantity)
val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(line.id!!)
val totalPickedQty = stockOutLines
.filter {
it.status == "completed" ||
it.status == "partially_completed" ||
(it.status == "rejected" && (it.qty ?: zero) > zero) // 包含已 picked 的 rejected
}
.sumOf { it.qty ?: zero }
val requiredQty = line.qty ?: zero
val remainingQty = requiredQty.minus(totalPickedQty)
println("=== SUGGESTION DEBUG for Pick Order Line ${line.id} ===")
println("Required qty: $requiredQty")
println("Total picked qty: $totalPickedQty")
println("Remaining qty needed: $remainingQty")
println("Stock out lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}")
// FIX: Use remainingQty instead of line.qty
var remainingQtyToAllocate = remainingQty
println("remaining1 $remainingQtyToAllocate (sales units)")
val updatedLotLines = mutableListOf<InventoryLotLineInfo>()
lotLines.forEachIndexed { index, lotLine ->
if (remainingQtyToAllocate <= zero) return@forEachIndexed
println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}")
val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() }
// 修复:计算可用数量,转换为销售单位
val availableQtyInBaseUnits = calculateRemainingQtyForInfo(lotLine)
val holdQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero
val availableQtyInSalesUnits = availableQtyInBaseUnits
.minus(holdQtyInBaseUnits)
.divide(ratio, 2, RoundingMode.HALF_UP)
println("holdQtyMap[lotLine.id] ?: zero ${holdQtyMap[lotLine.id] ?: zero}")
if (availableQtyInSalesUnits <= zero) {
updatedLotLines += lotLine
return@forEachIndexed
}
println("$index : ${lotLine.id}")
// val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() }
val originalHoldQty = inventoryLotLine?.holdQty
// 修复:在销售单位中计算分配数量
val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQtyToAllocate)
remainingQtyToAllocate = remainingQtyToAllocate.minus(assignQtyInSalesUnits)
val newHoldQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero
// 修复:将销售单位转换为基础单位来更新 holdQty
val assignQtyInBaseUnits = assignQtyInSalesUnits.multiply(ratio)
holdQtyMap[lotLine.id] = (holdQtyMap[lotLine.id] ?: zero).plus(assignQtyInBaseUnits)
suggestedList += SuggestedPickLot().apply {
type = SuggestedPickLotType.PICK_ORDER
suggestedLotLine = inventoryLotLine
pickOrderLine = line
qty = assignQtyInSalesUnits // 保存销售单位
}
}
// 修复:计算现有 suggestions 中 pending/checked 状态满足的数量
var existingSatisfiedQty = BigDecimal.ZERO

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

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

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

// if still have remainingQty
println("remaining2 $remainingQtyToAllocate (sales units)")
if (remainingQtyToAllocate > zero) {
suggestedList += SuggestedPickLot().apply {
type = SuggestedPickLotType.PICK_ORDER
suggestedLotLine = null
pickOrderLine = line
qty = remainingQtyToAllocate // 保存销售单位
}
try {
/*
val pickOrder = line.pickOrder
if (pickOrder != null) {
createInsufficientStockIssue(
pickOrder = pickOrder,
pickOrderLine = line,
insufficientQty = remainingQtyToAllocate
)
}
*/
} catch (e: Exception) {
println("❌ Error creating insufficient stock issue: ${e.message}")
e.printStackTrace()
}
}
}
return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList)
}

// Convertion
open fun convertRequestsToEntities(request: List<SaveSuggestedPickLotRequest>): List<SuggestedPickLot> {


+ 8
- 0
src/main/resources/db/changelog/changes/20251203_01_Enson/01_add_dopickordercolumn.sql Datei anzeigen

@@ -0,0 +1,8 @@
-- liquibase formatted sql
-- changeset KelvinY:add_company_to_items

ALTER TABLE `fpsmsdb`.`do_pick_order`
ADD COLUMN `release_type` VARCHAR(100) NULL after `ticket_status`;

ALTER TABLE `fpsmsdb`.`do_pick_order_record`
ADD COLUMN `release_type` VARCHAR(100) NULL after `ticket_status`;

+ 6
- 0
src/main/resources/db/changelog/changes/20251209_01_Enson/01_add_dopickordercolumn.sql Datei anzeigen

@@ -0,0 +1,6 @@
-- liquibase formatted sql
-- changeset KelvinY:add_company_to_items

ALTER TABLE `fpsmsdb`.`productionprocessissue`
ADD COLUMN `productProcessLineId` Int NULL after `productprocessid`;


+ 5
- 0
src/main/resources/db/changelog/changes/20251210_01_Enson/01_add_column.sql Datei anzeigen

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset Enson:add_column

ALTER TABLE `fpsmsdb`.`productionprocessissue`
ADD COLUMN `status` VARCHAR(255) NULL after `totalTime`;

Laden…
Abbrechen
Speichern