| @@ -93,6 +93,8 @@ class DoPickOrder { | |||||
| @Column(name = "handler_name", length = 100) | @Column(name = "handler_name", length = 100) | ||||
| var handlerName: String? = null | var handlerName: String? = null | ||||
| @Column(name = "release_type", length = 100) | |||||
| var releaseType: String? = null | |||||
| // Default constructor for Hibernate | // Default constructor for Hibernate | ||||
| constructor() | constructor() | ||||
| @@ -119,6 +121,7 @@ class DoPickOrder { | |||||
| pickOrderCode: String? = null, | pickOrderCode: String? = null, | ||||
| deliveryOrderCode: String? = null, | deliveryOrderCode: String? = null, | ||||
| loadingSequence: Int? = null, | loadingSequence: Int? = null, | ||||
| releaseType: String? = null | |||||
| ) { | ) { | ||||
| this.storeId = storeId | this.storeId = storeId | ||||
| this.ticketNo = ticketNo | this.ticketNo = ticketNo | ||||
| @@ -141,5 +144,6 @@ class DoPickOrder { | |||||
| this.pickOrderCode = pickOrderCode | this.pickOrderCode = pickOrderCode | ||||
| this.deliveryOrderCode = deliveryOrderCode | this.deliveryOrderCode = deliveryOrderCode | ||||
| this.loadingSequence = loadingSequence | this.loadingSequence = loadingSequence | ||||
| this.releaseType = releaseType | |||||
| } | } | ||||
| } | } | ||||
| @@ -94,6 +94,8 @@ class DoPickOrderRecord { | |||||
| var deleted: Boolean = false | var deleted: Boolean = false | ||||
| @Column(name = "handler_name", length = 100) | @Column(name = "handler_name", length = 100) | ||||
| var handlerName: String? = null | var handlerName: String? = null | ||||
| @Column(name = "release_type", length = 100) | |||||
| var releaseType: String? = null | |||||
| // Default constructor for Hibernate | // Default constructor for Hibernate | ||||
| constructor() | constructor() | ||||
| @@ -39,5 +39,10 @@ fun findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||||
| @Param("startDate") startDate: LocalDate, | @Param("startDate") startDate: LocalDate, | ||||
| @Param("endDate") endDate: LocalDate | @Param("endDate") endDate: LocalDate | ||||
| ): List<DoPickOrder> | ): List<DoPickOrder> | ||||
| fun findByShopIdAndStoreIdAndReleaseTypeAndTicketStatusAndDeletedFalse( | |||||
| shopId: Long?, | |||||
| storeId: String, | |||||
| releaseType: String, | |||||
| ticketStatus: DoPickOrderStatus | |||||
| ): List<DoPickOrder> | |||||
| } | } | ||||
| @@ -18,9 +18,9 @@ class DoPickOrderQueryService( | |||||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository | 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() | 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) { | val actualStoreId = when (storeId) { | ||||
| "2/F" -> "2/F" | "2/F" -> "2/F" | ||||
| @@ -35,6 +35,13 @@ class DoPickOrderQueryService( | |||||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | 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 | // Query completed records from do_pick_order_record table | ||||
| val completedRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | val completedRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | ||||
| actualStoreId, | actualStoreId, | ||||
| @@ -42,11 +49,20 @@ class DoPickOrderQueryService( | |||||
| listOf(DoPickOrderStatus.completed) | 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: 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: 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) | // Filter active records (check for non-issue lines) | ||||
| val filteredActiveRecords = activeRecords.filter { doPickOrder -> | |||||
| val filteredActiveRecords = filteredActiveRecordsByReleaseType.filter { doPickOrder -> | |||||
| val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | ||||
| if (!hasNonIssueLines) { | if (!hasNonIssueLines) { | ||||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | 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 | // 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!!) | val hasNonIssueLines = checkDoPickOrderRecordHasNonIssueLines(record.id!!) | ||||
| if (!hasNonIssueLines) { | if (!hasNonIssueLines) { | ||||
| println("🔍 DEBUG: Filtering out DoPickOrderRecord ${record.id} - all lines are issues") | println("🔍 DEBUG: Filtering out DoPickOrderRecord ${record.id} - all lines are issues") | ||||
| @@ -91,7 +91,7 @@ open class DoPickOrderService( | |||||
| return RecordsRes<DeliveryOrderInfo>(records, total.toInt()); | return RecordsRes<DeliveryOrderInfo>(records, total.toInt()); | ||||
| } | } | ||||
| open fun getNextTicketNumber(datePrefix: String, storeId: String): String { | 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 { | try { | ||||
| val sanitizedStoreId = storeId.replace("/", "") | val sanitizedStoreId = storeId.replace("/", "") | ||||
| val shortDatePrefix = if (datePrefix.length == 8) { | val shortDatePrefix = if (datePrefix.length == 8) { | ||||
| @@ -102,14 +102,14 @@ open class DoPickOrderService( | |||||
| // 修改搜索模式为新格式 | // 修改搜索模式为新格式 | ||||
| val searchPattern = "TI-${shortDatePrefix}-${sanitizedStoreId}-" // T-20250915-4F- | val searchPattern = "TI-${shortDatePrefix}-${sanitizedStoreId}-" // T-20250915-4F- | ||||
| val todayTickets = doPickOrderRepository.findByTicketNoStartingWith(searchPattern) | 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 -> | 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 nextNumber = (todayTickets.size + 1).toString().padStart(3, '0') | ||||
| // 修改生成格式 | // 修改生成格式 | ||||
| val ticketNumber = "TI-${datePrefix}-${sanitizedStoreId}-${nextNumber}" // T-20250915-4F-001 | val ticketNumber = "TI-${datePrefix}-${sanitizedStoreId}-${nextNumber}" // T-20250915-4F-001 | ||||
| println("🔍 DEBUG: Generated ticket number: $ticketNumber") | |||||
| println(" DEBUG: Generated ticket number: $ticketNumber") | |||||
| return ticketNumber | return ticketNumber | ||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| println("❌ ERROR in getNextTicketNumber: ${e.message}") | println("❌ ERROR in getNextTicketNumber: ${e.message}") | ||||
| @@ -297,41 +297,49 @@ open class DoPickOrderService( | |||||
| } | } | ||||
| return doPickOrderRepository.saveAll(doPickOrders) | 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() | 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) { | val actualStoreId = when (storeId) { | ||||
| "2/F" -> "2/F" | "2/F" -> "2/F" | ||||
| "4/F" -> "4/F" | "4/F" -> "4/F" | ||||
| else -> storeId | else -> storeId | ||||
| } | } | ||||
| val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | ||||
| actualStoreId, | actualStoreId, | ||||
| targetDate, | targetDate, | ||||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | 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 查询 | // 添加 finishedRecords 查询 | ||||
| val finishedRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | val finishedRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | ||||
| actualStoreId, | actualStoreId, | ||||
| targetDate, | targetDate, | ||||
| listOf(DoPickOrderStatus.completed) | 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!!) | val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | ||||
| if (!hasNonIssueLines) { | 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 | 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 } | val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | ||||
| .mapValues { (key, list) -> | .mapValues { (key, list) -> | ||||
| @@ -341,9 +349,9 @@ open class DoPickOrderService( | |||||
| (record.truckDepartureTime == truckDepartureTime) && | (record.truckDepartureTime == truckDepartureTime) && | ||||
| (record.truckLanceCode == truckLanceCode) | (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( | LaneBtn( | ||||
| truckLanceCode = list.first().truckLanceCode ?: "", | truckLanceCode = list.first().truckLanceCode ?: "", | ||||
| unassigned = list.count { it.handledBy == null }, | unassigned = list.count { it.handledBy == null }, | ||||
| @@ -403,7 +411,7 @@ open class DoPickOrderService( | |||||
| // 3. 只有当所有 lines 都是 "issue" 状态时才过滤掉 | // 3. 只有当所有 lines 都是 "issue" 状态时才过滤掉 | ||||
| val hasNonIssueLines = nonIssueLines > 0 | 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 | return hasNonIssueLines | ||||
| @@ -426,7 +434,7 @@ open class DoPickOrderService( | |||||
| "4/F" -> "4/F" // 保持原格式 | "4/F" -> "4/F" // 保持原格式 | ||||
| else -> request.storeId | 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 | val candidates = doPickOrderRepository | ||||
| .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | ||||
| @@ -453,7 +461,7 @@ open class DoPickOrderService( | |||||
| // 关键修改:获取这个 do_pick_order 下的所有 pick orders 并分配给用户 | // 关键修改:获取这个 do_pick_order 下的所有 pick orders 并分配给用户 | ||||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(firstOrder.id!!) | 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 -> | doPickOrderLines.forEach { line -> | ||||
| if (line.pickOrderId != null) { | if (line.pickOrderId != null) { | ||||
| @@ -462,7 +470,7 @@ open class DoPickOrderService( | |||||
| pickOrder.assignTo = user | pickOrder.assignTo = user | ||||
| pickOrder.status = PickOrderStatus.RELEASED | pickOrder.status = PickOrderStatus.RELEASED | ||||
| pickOrderRepository.save(pickOrder) | 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 { | } else { | ||||
| println("⚠️ WARNING: Pick order ${line.pickOrderId} not found") | println("⚠️ WARNING: Pick order ${line.pickOrderId} not found") | ||||
| } | } | ||||
| @@ -481,7 +489,7 @@ open class DoPickOrderService( | |||||
| } | } | ||||
| if (records.isNotEmpty()) { | if (records.isNotEmpty()) { | ||||
| doPickOrderRecordRepository.saveAll(records) | 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}") | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -157,7 +157,7 @@ class DoReleaseCoordinatorService( | |||||
| ) | ) | ||||
| SELECT | SELECT | ||||
| dpo2.id, | dpo2.id, | ||||
| CONCAT('TI-', | |||||
| CONCAT('TI-B-', | |||||
| DATE_FORMAT(dpo2.RequiredDeliveryDate, '%Y%m%d'), | DATE_FORMAT(dpo2.RequiredDeliveryDate, '%Y%m%d'), | ||||
| '-', | '-', | ||||
| REPLACE(COALESCE(dpo2.store_id, ts.preferred_floor, '2F'), '/', ''), | REPLACE(COALESCE(dpo2.store_id, ts.preferred_floor, '2F'), '/', ''), | ||||
| @@ -212,6 +212,14 @@ class DoReleaseCoordinatorService( | |||||
| AND w.store_id IN ('2F', '4F') | AND w.store_id IN ('2F', '4F') | ||||
| WHERE dol.deleted = 0 | WHERE dol.deleted = 0 | ||||
| AND dol.deliveryOrderId IN (${ids.joinToString(",")}) | 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 | GROUP BY dol.deliveryOrderId, w.store_id | ||||
| ), | ), | ||||
| DoFloorSummary AS ( | DoFloorSummary AS ( | ||||
| @@ -507,7 +515,9 @@ class DoReleaseCoordinatorService( | |||||
| truckLanceCode = first.truckLanceCode, | truckLanceCode = first.truckLanceCode, | ||||
| shopCode = first.shopCode, | shopCode = first.shopCode, | ||||
| shopName = first.shopName, | shopName = first.shopName, | ||||
| requiredDeliveryDate = first.estimatedArrivalDate | |||||
| requiredDeliveryDate = first.estimatedArrivalDate, | |||||
| releaseType = "batch" | |||||
| ) | ) | ||||
| // 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save() | // 直接使用 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() | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -69,27 +69,32 @@ class DoPickOrderController( | |||||
| @GetMapping("/summary-by-store") | @GetMapping("/summary-by-store") | ||||
| fun getSummaryByStore( | fun getSummaryByStore( | ||||
| @RequestParam storeId: String, | @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 { | ): StoreLaneSummary { | ||||
| return doPickOrderQueryService.getSummaryByStore(storeId, requiredDate) | |||||
| return doPickOrderQueryService.getSummaryByStore(storeId, requiredDate, releaseType?: "ALL") | |||||
| } | } | ||||
| @PostMapping("/assign-by-lane") | @PostMapping("/assign-by-lane") | ||||
| fun assignByLane(@RequestBody request: AssignByLaneRequest): MessageResponse { | fun assignByLane(@RequestBody request: AssignByLaneRequest): MessageResponse { | ||||
| return doPickOrderAssignmentService.assignByLane(request) // 使用新的 Service | 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") | @GetMapping("/ticket-release-table") | ||||
| fun getTicketReleaseTable( | fun getTicketReleaseTable( | ||||
| @@ -58,6 +58,8 @@ data class JobOrderInfoWithTypeName( | |||||
| val item: JobOrderItemInfo, | val item: JobOrderItemInfo, | ||||
| val stockInLineId: Long?, | val stockInLineId: Long?, | ||||
| val stockInLineStatus: String?, | val stockInLineStatus: String?, | ||||
| val sufficientCount: Int?, | |||||
| val insufficientCount: Int?, | |||||
| val silHandlerId: Long?, | val silHandlerId: Long?, | ||||
| val planStart: LocalDateTime?, | val planStart: LocalDateTime?, | ||||
| val status: String, | val status: String, | ||||
| @@ -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.productProcess.entity.ProductProcessLineRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.StockOutRepository | import com.ffii.fpsms.modules.stock.entity.StockOutRepository | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderProcessRepository | 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 | @Service | ||||
| open class JoPickOrderService( | open class JoPickOrderService( | ||||
| private val joPickOrderRepository: JoPickOrderRepository, | private val joPickOrderRepository: JoPickOrderRepository, | ||||
| @@ -215,7 +220,7 @@ open class JoPickOrderService( | |||||
| } | } | ||||
| return joPickOrderRecordRepository.saveAll(joPickOrderRecords) | return joPickOrderRecordRepository.saveAll(joPickOrderRecords) | ||||
| } | } | ||||
| open fun getAllJobOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | |||||
| open fun getAllJobOrderLotsWithDetailsHierarchical(userId: Long): JobOrderLotsHierarchicalResponse { | |||||
| println("=== Debug: getAllJobOrderLotsWithDetailsHierarchical ===") | println("=== Debug: getAllJobOrderLotsWithDetailsHierarchical ===") | ||||
| println("today: ${LocalDate.now()}") | println("today: ${LocalDate.now()}") | ||||
| println("userId filter: $userId") | println("userId filter: $userId") | ||||
| @@ -224,25 +229,38 @@ open class JoPickOrderService( | |||||
| val user = userService.find(userId).orElse(null) | val user = userService.find(userId).orElse(null) | ||||
| if (user == null) { | if (user == null) { | ||||
| println("❌ User not found: $userId") | 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) | val statusList = listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED) | ||||
| // Get all pick orders assigned to user with PENDING or RELEASED status that have joId | // Get all pick orders assigned to user with PENDING or RELEASED status that have joId | ||||
| val allAssignedPickOrders = pickOrderRepository.findAllByAssignToIdAndStatusIn( | val allAssignedPickOrders = pickOrderRepository.findAllByAssignToIdAndStatusIn( | ||||
| userId, | userId, | ||||
| statusList | statusList | ||||
| ).filter { it.jobOrder != null } // Only pick orders with joId | ).filter { it.jobOrder != null } // Only pick orders with joId | ||||
| println("🔍 DEBUG: Found ${allAssignedPickOrders.size} job order pick orders assigned to user $userId") | println("🔍 DEBUG: Found ${allAssignedPickOrders.size} job order pick orders assigned to user $userId") | ||||
| // Filter based on assignment and status | // Filter based on assignment and status | ||||
| val filteredPickOrders = if (allAssignedPickOrders.isNotEmpty()) { | val filteredPickOrders = if (allAssignedPickOrders.isNotEmpty()) { | ||||
| // Check if there are any RELEASED orders assigned to this user (active work) | // 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 | it.status == PickOrderStatus.RELEASED && it.assignTo?.id == userId | ||||
| } | } | ||||
| if (assignedReleasedOrders.isNotEmpty()) { | if (assignedReleasedOrders.isNotEmpty()) { | ||||
| // If there are assigned RELEASED orders, show only those | // If there are assigned RELEASED orders, show only those | ||||
| println("🔍 DEBUG: Found ${assignedReleasedOrders.size} assigned RELEASED job orders, showing 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 | // If no assigned RELEASED orders, show only the latest COMPLETED order | ||||
| val completedOrders = allAssignedPickOrders.filter { it.status == PickOrderStatus.COMPLETED } | val completedOrders = allAssignedPickOrders.filter { it.status == PickOrderStatus.COMPLETED } | ||||
| if (completedOrders.isNotEmpty()) { | 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}") | println("🔍 DEBUG: No assigned RELEASED job orders, showing latest completed order: ${latestCompleted?.code}") | ||||
| listOfNotNull(latestCompleted) | listOfNotNull(latestCompleted) | ||||
| } else { | } else { | ||||
| @@ -262,42 +281,68 @@ open class JoPickOrderService( | |||||
| } else { | } else { | ||||
| emptyList() | emptyList() | ||||
| } | } | ||||
| val pickOrderIds = filteredPickOrders.map { it.id!! } | val pickOrderIds = filteredPickOrders.map { it.id!! } | ||||
| println(" Job Order Pick order IDs to fetch: $pickOrderIds") | println(" Job Order Pick order IDs to fetch: $pickOrderIds") | ||||
| if (pickOrderIds.isEmpty()) { | 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 获取数据 | // 使用 Repository 获取数据 | ||||
| try { | try { | ||||
| val pickOrders = pickOrderRepository.findAllByIdIn(pickOrderIds) | val pickOrders = pickOrderRepository.findAllByIdIn(pickOrderIds) | ||||
| .filter { it.deleted == false && it.assignTo?.id == userId } | .filter { it.deleted == false && it.assignTo?.id == userId } | ||||
| if (pickOrders.isEmpty()) { | 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 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 | // 获取 pick order lines | ||||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!) | val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!) | ||||
| .filter { it.deleted == false } | .filter { it.deleted == false } | ||||
| // 获取所有 pick order line IDs | // 获取所有 pick order line IDs | ||||
| val pickOrderLineIds = pickOrderLines.map { it.id!! } | val pickOrderLineIds = pickOrderLines.map { it.id!! } | ||||
| // 获取 suggested pick lots | // 获取 suggested pick lots | ||||
| val suggestedPickLots = if (pickOrderLineIds.isNotEmpty()) { | val suggestedPickLots = if (pickOrderLineIds.isNotEmpty()) { | ||||
| suggestPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | suggestPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | ||||
| @@ -305,10 +350,10 @@ open class JoPickOrderService( | |||||
| } else { | } else { | ||||
| emptyList() | emptyList() | ||||
| } | } | ||||
| // 获取所有 inventory lot line IDs | // 获取所有 inventory lot line IDs | ||||
| val inventoryLotLineIds = suggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | val inventoryLotLineIds = suggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | ||||
| // 获取 inventory lot lines | // 获取 inventory lot lines | ||||
| val inventoryLotLines = if (inventoryLotLineIds.isNotEmpty()) { | val inventoryLotLines = if (inventoryLotLineIds.isNotEmpty()) { | ||||
| inventoryLotLineRepository.findAllByIdIn(inventoryLotLineIds) | inventoryLotLineRepository.findAllByIdIn(inventoryLotLineIds) | ||||
| @@ -316,7 +361,7 @@ open class JoPickOrderService( | |||||
| } else { | } else { | ||||
| emptyList() | emptyList() | ||||
| } | } | ||||
| // 获取 inventory lots | // 获取 inventory lots | ||||
| val inventoryLotIds = inventoryLotLines.mapNotNull { it.inventoryLot?.id }.distinct() | val inventoryLotIds = inventoryLotLines.mapNotNull { it.inventoryLot?.id }.distinct() | ||||
| val inventoryLots = if (inventoryLotIds.isNotEmpty()) { | val inventoryLots = if (inventoryLotIds.isNotEmpty()) { | ||||
| @@ -325,7 +370,7 @@ open class JoPickOrderService( | |||||
| } else { | } else { | ||||
| emptyList() | emptyList() | ||||
| } | } | ||||
| // 获取 stock out lines | // 获取 stock out lines | ||||
| val stockOutLines = if (pickOrderLineIds.isNotEmpty() && inventoryLotLineIds.isNotEmpty()) { | val stockOutLines = if (pickOrderLineIds.isNotEmpty() && inventoryLotLineIds.isNotEmpty()) { | ||||
| pickOrderLineIds.flatMap { polId -> | pickOrderLineIds.flatMap { polId -> | ||||
| @@ -342,13 +387,13 @@ open class JoPickOrderService( | |||||
| } | } | ||||
| // 获取 jo_pick_order 记录 | // 获取 jo_pick_order 记录 | ||||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!) | val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!) | ||||
| // 构建 pick order info | // 构建 pick order info | ||||
| val pickOrderInfo = mapOf( | val pickOrderInfo = mapOf( | ||||
| "id" to pickOrder.id, | "id" to pickOrder.id, | ||||
| "code" to pickOrder.code, | "code" to pickOrder.code, | ||||
| "consoCode" to pickOrder.consoCode, | "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)}" | "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}" | ||||
| }, | }, | ||||
| "type" to pickOrder.type?.value, | "type" to pickOrder.type?.value, | ||||
| @@ -360,49 +405,35 @@ open class JoPickOrderService( | |||||
| "name" to "Job Order ${jobOrder.code}" | "name" to "Job Order ${jobOrder.code}" | ||||
| ) | ) | ||||
| ) | ) | ||||
| // 构建 pick order lines with lots | // 构建 pick order lines with lots | ||||
| val pickOrderLinesResult = pickOrderLines.map { pol -> | val pickOrderLinesResult = pickOrderLines.map { pol -> | ||||
| val item = pol.item | val item = pol.item | ||||
| val uom = pol.uom | val uom = pol.uom | ||||
| val lineId = pol.id!! | 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 } | val lineSuggestedLots = suggestedPickLots.filter { it.pickOrderLine?.id == pol.id } | ||||
| // 构建 lots 数据 | // 构建 lots 数据 | ||||
| val lots = lineSuggestedLots.mapNotNull { spl -> | val lots = lineSuggestedLots.mapNotNull { spl -> | ||||
| val ill = spl.suggestedLotLine | val ill = spl.suggestedLotLine | ||||
| if (ill == null || ill.deleted == true) return@mapNotNull null | if (ill == null || ill.deleted == true) return@mapNotNull null | ||||
| val il = ill.inventoryLot | val il = ill.inventoryLot | ||||
| if (il == null || il.deleted == true) return@mapNotNull null | if (il == null || il.deleted == true) return@mapNotNull null | ||||
| val warehouse = ill.warehouse | 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 } | val jpo = joPickOrders.firstOrNull { it.itemId == item?.id } | ||||
| // 计算 available quantity | |||||
| val availableQty = if (sol?.status == "rejected") { | val availableQty = if (sol?.status == "rejected") { | ||||
| null | null | ||||
| } else { | } 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 { | val lotAvailability = when { | ||||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | ||||
| sol?.status == "rejected" -> "rejected" | sol?.status == "rejected" -> "rejected" | ||||
| @@ -410,72 +441,97 @@ open class JoPickOrderService( | |||||
| ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | ||||
| else -> "available" | else -> "available" | ||||
| } | } | ||||
| // 计算 processing status | |||||
| val processingStatus = when (sol?.status) { | val processingStatus = when (sol?.status) { | ||||
| "completed" -> "completed" | "completed" -> "completed" | ||||
| "rejected" -> "rejected" | "rejected" -> "rejected" | ||||
| "created" -> "pending" | "created" -> "pending" | ||||
| else -> "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)}" | "${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) { | } catch (e: Exception) { | ||||
| println("❌ Error executing Job Order hierarchical query: ${e.message}") | println("❌ Error executing Job Order hierarchical query: ${e.message}") | ||||
| e.printStackTrace() | 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) | // Get completed job order pick orders (for second tab) | ||||
| // Fix the getCompletedJobOrderLotsHierarchical method | // Fix the getCompletedJobOrderLotsHierarchical method | ||||
| open fun getCompletedJobOrderLotsHierarchical(userId: Long): Map<String, Any?> { | open fun getCompletedJobOrderLotsHierarchical(userId: Long): Map<String, Any?> { | ||||
| @@ -1725,7 +1781,7 @@ open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> { | |||||
| emptyList() | emptyList() | ||||
| } | } | ||||
| } | } | ||||
| open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String, Any?> { | |||||
| open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLotsHierarchicalResponse { | |||||
| println("=== getJobOrderLotsHierarchicalByPickOrderId ===") | println("=== getJobOrderLotsHierarchicalByPickOrderId ===") | ||||
| println("pickOrderId: $pickOrderId") | println("pickOrderId: $pickOrderId") | ||||
| @@ -1733,17 +1789,36 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||||
| val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) | val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) | ||||
| if (pickOrder == null || pickOrder.deleted == true) { | if (pickOrder == null || pickOrder.deleted == true) { | ||||
| println("❌ Pick order $pickOrderId not found or deleted") | 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 | // 获取 pick order lines | ||||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!) | val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!) | ||||
| .filter { it.deleted == false } | .filter { it.deleted == false } | ||||
| @@ -1799,20 +1874,20 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!) | val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!) | ||||
| // 构建 pick order info | // 构建 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)}" | "${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 item = pol.item | ||||
| val uom = pol.uom | val uom = pol.uom | ||||
| val lineId = pol.id!! | 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 } | val lineSuggestedLots = suggestedPickLots.filter { it.pickOrderLine?.id == pol.id } | ||||
| // 构建 lots 数据 | // 构建 lots 数据 | ||||
| @@ -1835,29 +1907,17 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||||
| if (il == null || il.deleted == true) return@mapNotNull null | if (il == null || il.deleted == true) return@mapNotNull null | ||||
| val warehouse = ill.warehouse | val warehouse = ill.warehouse | ||||
| // 获取对应的 stock out line | |||||
| val sol = stockOutLines.firstOrNull { | val sol = stockOutLines.firstOrNull { | ||||
| it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id | it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id | ||||
| } | } | ||||
| // 获取对应的 jo_pick_order | |||||
| val jpo = joPickOrders.firstOrNull { it.itemId == item?.id } | val jpo = joPickOrders.firstOrNull { it.itemId == item?.id } | ||||
| // 计算 available quantity | |||||
| val availableQty = if (sol?.status == "rejected") { | val availableQty = if (sol?.status == "rejected") { | ||||
| null | null | ||||
| } else { | } 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 { | val lotAvailability = when { | ||||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | ||||
| sol?.status == "rejected" -> "rejected" | sol?.status == "rejected" -> "rejected" | ||||
| @@ -1866,7 +1926,6 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||||
| else -> "available" | else -> "available" | ||||
| } | } | ||||
| // 计算 processing status | |||||
| val processingStatus = when (sol?.status) { | val processingStatus = when (sol?.status) { | ||||
| "completed" -> "completed" | "completed" -> "completed" | ||||
| "rejected" -> "rejected" | "rejected" -> "rejected" | ||||
| @@ -1874,59 +1933,69 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||||
| else -> "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)}" | "${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) { | } catch (e: Exception) { | ||||
| println("❌ Error in getJobOrderLotsHierarchicalByPickOrderId: ${e.message}") | println("❌ Error in getJobOrderLotsHierarchicalByPickOrderId: ${e.message}") | ||||
| e.printStackTrace() | 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() | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| @@ -56,7 +56,7 @@ import java.io.FileNotFoundException | |||||
| import java.io.IOException | import java.io.IOException | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderInfoWithTypeName | import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderInfoWithTypeName | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.projections.JobTypeResponse | import com.ffii.fpsms.modules.jobOrder.entity.projections.JobTypeResponse | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | |||||
| @Service | @Service | ||||
| open class JobOrderService( | open class JobOrderService( | ||||
| @@ -73,7 +73,8 @@ open class JobOrderService( | |||||
| val stockOutRepository: StockOutRepository, | val stockOutRepository: StockOutRepository, | ||||
| val stockOutLineRepository: StockOutLIneRepository, | val stockOutLineRepository: StockOutLIneRepository, | ||||
| private val printerService: PrinterService, | private val printerService: PrinterService, | ||||
| val jobTypeRepository: JobTypeRepository | |||||
| val jobTypeRepository: JobTypeRepository, | |||||
| val inventoryRepository: InventoryRepository | |||||
| ) { | ) { | ||||
| open fun allJobOrdersByPage(request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfo> { | open fun allJobOrdersByPage(request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfo> { | ||||
| @@ -116,6 +117,27 @@ open class JobOrderService( | |||||
| emptyMap() | 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 planStartFrom = request.planStart | ||||
| val planStartTo = request.planStartTo | val planStartTo = request.planStartTo | ||||
| val records = response.content | val records = response.content | ||||
| @@ -124,6 +146,15 @@ open class JobOrderService( | |||||
| (planStartTo == null || (it.planStart != null && (planStartTo.isEqual(it.planStart) || planStartTo.isAfter(it.planStart)))) | (planStartTo == null || (it.planStart != null && (planStartTo.isEqual(it.planStart) || planStartTo.isAfter(it.planStart)))) | ||||
| } | } | ||||
| .map { info -> | .map { info -> | ||||
| val jobOrder = jobOrdersMap[info.id] | |||||
| // 计算 sufficientCount 和 insufficientCount | |||||
| val (sufficientCount, insufficientCount) = if (jobOrder != null) { | |||||
| calculateStockCounts(jobOrder, inventoriesMap) | |||||
| } else { | |||||
| Pair(null, null) | |||||
| } | |||||
| JobOrderInfoWithTypeName( | JobOrderInfoWithTypeName( | ||||
| id = info.id, | id = info.id, | ||||
| code = info.code, | code = info.code, | ||||
| @@ -134,6 +165,8 @@ open class JobOrderService( | |||||
| item = info.item, | item = info.item, | ||||
| stockInLineId = info.stockInLineId, | stockInLineId = info.stockInLineId, | ||||
| stockInLineStatus = info.stockInLineStatus, | stockInLineStatus = info.stockInLineStatus, | ||||
| sufficientCount = sufficientCount, | |||||
| insufficientCount = insufficientCount, | |||||
| silHandlerId = info.silHandlerId, | silHandlerId = info.silHandlerId, | ||||
| planStart = info.planStart, | planStart = info.planStart, | ||||
| status = info.status, | status = info.status, | ||||
| @@ -152,6 +185,55 @@ open class JobOrderService( | |||||
| val total = response.totalElements | val total = response.totalElements | ||||
| return RecordsRes<JobOrderInfoWithTypeName>(records, total.toInt()); | 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 { | open fun jobOrderDetail(id: Long): JobOrderDetail { | ||||
| val sqlResult = jobOrderRepository.findJobOrderDetailById(id) ?: throw NoSuchElementException(); | val sqlResult = jobOrderRepository.findJobOrderDetailById(id) ?: throw NoSuchElementException(); | ||||
| @@ -308,13 +390,26 @@ open class JobOrderService( | |||||
| // 添加 suggested pick lots 创建逻辑 | // 添加 suggested pick lots 创建逻辑 | ||||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | ||||
| println("DEBUG: pickOrderLines=${lines.map { it.id to it.item?.code }}") | |||||
| if (lines.isNotEmpty()) { | if (lines.isNotEmpty()) { | ||||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLines( | |||||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLinesForJobOrder( | |||||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | 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) | 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 | // Hold inventory quantities | ||||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | ||||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | ||||
| @@ -100,7 +100,7 @@ class JobOrderController( | |||||
| return jo | return jo | ||||
| } | } | ||||
| @GetMapping("/all-lots-hierarchical/{userId}") | @GetMapping("/all-lots-hierarchical/{userId}") | ||||
| fun getAllJobOrderLotsHierarchical(@PathVariable userId: Long): Map<String, Any?> { | |||||
| fun getAllJobOrderLotsHierarchical(@PathVariable userId: Long): JobOrderLotsHierarchicalResponse { | |||||
| return joPickOrderService.getAllJobOrderLotsWithDetailsHierarchical(userId) | return joPickOrderService.getAllJobOrderLotsWithDetailsHierarchical(userId) | ||||
| } | } | ||||
| @@ -235,7 +235,7 @@ fun recordSecondScanIssue( | |||||
| } | } | ||||
| @GetMapping("/all-lots-hierarchical-by-pick-order/{pickOrderId}") | @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) | return joPickOrderService.getJobOrderLotsHierarchicalByPickOrderId(pickOrderId) | ||||
| } | } | ||||
| @PostMapping("/update-jo-pick-order-handled-by") | @PostMapping("/update-jo-pick-order-handled-by") | ||||
| @@ -51,18 +51,72 @@ data class AllJoPickOrderResponse( | |||||
| val finishedPickOLineCount: Int, | 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( | data class UpdateJoPickOrderHandledByRequest( | ||||
| val pickOrderId: Long, | val pickOrderId: Long, | ||||
| val itemId: Long, | val itemId: Long, | ||||
| val userId: 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? | |||||
| ) | |||||
| @@ -14,6 +14,11 @@ open class ProductionProcessIssue : BaseEntity<Long>() { // 修复:改为正 | |||||
| @ManyToOne(fetch = FetchType.LAZY) | @ManyToOne(fetch = FetchType.LAZY) | ||||
| @JoinColumn(name = "productprocessid", nullable = false) | @JoinColumn(name = "productprocessid", nullable = false) | ||||
| open var productProcess: ProductProcess? = null | open var productProcess: ProductProcess? = null | ||||
| @Column(name = "productProcessLineId") | |||||
| open var productProcessLineId: Long? = null | |||||
| @ManyToOne(fetch = FetchType.LAZY) | @ManyToOne(fetch = FetchType.LAZY) | ||||
| @JoinColumn(name = "operatorId") | @JoinColumn(name = "operatorId") | ||||
| @@ -36,4 +41,6 @@ open class ProductionProcessIssue : BaseEntity<Long>() { // 修复:改为正 | |||||
| @Column(name = "totalTime") | @Column(name = "totalTime") | ||||
| open var totalTime: Int? = null // 分钟 | open var totalTime: Int? = null // 分钟 | ||||
| @Column(name = "status") | |||||
| open var status: String? = null | |||||
| } | } | ||||
| @@ -6,4 +6,5 @@ import org.springframework.stereotype.Repository | |||||
| @Repository | @Repository | ||||
| interface ProductionProcessIssueRepository : JpaRepository<ProductionProcessIssue, Long> { | interface ProductionProcessIssueRepository : JpaRepository<ProductionProcessIssue, Long> { | ||||
| fun findByProductProcess_Id(productProcessId: Long): List<ProductionProcessIssue> | fun findByProductProcess_Id(productProcessId: Long): List<ProductionProcessIssue> | ||||
| fun findByProductProcessLineId(productProcessLineId: Long):List<ProductionProcessIssue> | |||||
| } | } | ||||
| @@ -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.Equipment | ||||
| import com.ffii.fpsms.modules.master.entity.BomProcess | import com.ffii.fpsms.modules.master.entity.BomProcess | ||||
| import com.ffii.fpsms.modules.master.entity.Bom | import com.ffii.fpsms.modules.master.entity.Bom | ||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderBomMaterialRepository | import com.ffii.fpsms.modules.jobOrder.entity.JobOrderBomMaterialRepository | ||||
| import com.ffii.fpsms.modules.master.service.ProductionScheduleService | 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.stock.web.model.SaveStockInLineRequest | ||||
| import com.ffii.fpsms.modules.master.entity.BomProcessMaterialRepository | import com.ffii.fpsms.modules.master.entity.BomProcessMaterialRepository | ||||
| import com.ffii.fpsms.modules.master.entity.BomMaterialRepository | 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 com.ffii.fpsms.modules.jobOrder.entity.JobTypeRepository | ||||
| import java.time.ZoneOffset | |||||
| @Service | @Service | ||||
| @Transactional | @Transactional | ||||
| open class ProductProcessService( | open class ProductProcessService( | ||||
| @@ -532,11 +535,20 @@ open class ProductProcessService( | |||||
| planEndDate.atStartOfDay(), | planEndDate.atStartOfDay(), | ||||
| "detailed" | "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 { | } else { | ||||
| "0" | "0" | ||||
| } | |||||
| } | |||||
| fun calculateColourScore(value: Int?): String { | fun calculateColourScore(value: Int?): String { | ||||
| return when (value) { | return when (value) { | ||||
| 0 -> "淺" | 0 -> "淺" | ||||
| @@ -899,6 +911,9 @@ open class ProductProcessService( | |||||
| } | } | ||||
| open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse { | open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse { | ||||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | 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( | return JobOrderProcessLineDetailResponse( | ||||
| id = productProcessLine.id?:0, | id = productProcessLine.id?:0, | ||||
| productProcessId = productProcessLine.productProcess ?.id?:0, | productProcessId = productProcessLine.productProcess ?.id?:0, | ||||
| @@ -906,7 +921,11 @@ open class ProductProcessService( | |||||
| operatorId = productProcessLine.operator?.id?:0, | operatorId = productProcessLine.operator?.id?:0, | ||||
| operatorName = productProcessLine.operator?.name?:"", | operatorName = productProcessLine.operator?.name?:"", | ||||
| handlerId = productProcessLine.handler?.id?:0, | 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, | seqNo = productProcessLine.seqNo?:0, | ||||
| name = productProcessLine.name?:"", | name = productProcessLine.name?:"", | ||||
| description = productProcessLine.description?:"", | description = productProcessLine.description?:"", | ||||
| @@ -1233,5 +1252,70 @@ open class ProductProcessService( | |||||
| errorPosition = null, | 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, | |||||
| ) | |||||
| } | |||||
| } | } | ||||
| @@ -197,4 +197,12 @@ class ProductProcessController( | |||||
| fun completeProductProcessLine(@PathVariable lineId: Long): MessageResponse { | fun completeProductProcessLine(@PathVariable lineId: Long): MessageResponse { | ||||
| return productProcessService.CompleteProductProcessLine(lineId) | 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) | |||||
| } | |||||
| } | } | ||||
| @@ -132,7 +132,8 @@ data class JobOrderProcessLineDetailResponse( | |||||
| val equipmentId: Long?, | val equipmentId: Long?, | ||||
| val startTime: LocalDateTime?, | val startTime: LocalDateTime?, | ||||
| val endTime: LocalDateTime?, | val endTime: LocalDateTime?, | ||||
| val productProcessIssueId: Long?, | |||||
| val productProcessIssueStatus: String?, | |||||
| val status: String, | val status: String, | ||||
| val outputFromProcessQty: Int?, | val outputFromProcessQty: Int?, | ||||
| val outputFromProcessUom: String?, | val outputFromProcessUom: String?, | ||||
| @@ -190,4 +191,12 @@ data class NewUpdateProductProcessLineOperatorIdOrEquipmentIdAndEquipmentDetailR | |||||
| val EquipmentTypeSubTypeEquipmentNo: String, | val EquipmentTypeSubTypeEquipmentNo: String, | ||||
| val staffNo: String?, | val staffNo: String?, | ||||
| val Name: String?, | val Name: String?, | ||||
| ) | |||||
| data class SaveProductProcessIssueTimeRequest( | |||||
| val productProcessLineId: Long, | |||||
| val reason: String | |||||
| ) | |||||
| data class SaveProductProcessResumeTimeRequest( | |||||
| val productProcessLineId: Long, | |||||
| val resumeTime: LocalDateTime? | |||||
| ) | ) | ||||
| @@ -235,6 +235,148 @@ open class SuggestedPickLotService( | |||||
| } | } | ||||
| return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) | 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 | // Convertion | ||||
| open fun convertRequestsToEntities(request: List<SaveSuggestedPickLotRequest>): List<SuggestedPickLot> { | open fun convertRequestsToEntities(request: List<SaveSuggestedPickLotRequest>): List<SuggestedPickLot> { | ||||
| @@ -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`; | |||||
| @@ -0,0 +1,6 @@ | |||||
| -- liquibase formatted sql | |||||
| -- changeset KelvinY:add_company_to_items | |||||
| ALTER TABLE `fpsmsdb`.`productionprocessissue` | |||||
| ADD COLUMN `productProcessLineId` Int NULL after `productprocessid`; | |||||
| @@ -0,0 +1,5 @@ | |||||
| -- liquibase formatted sql | |||||
| -- changeset Enson:add_column | |||||
| ALTER TABLE `fpsmsdb`.`productionprocessissue` | |||||
| ADD COLUMN `status` VARCHAR(255) NULL after `totalTime`; | |||||