| @@ -93,6 +93,8 @@ class DoPickOrder { | |||
| @Column(name = "handler_name", length = 100) | |||
| var handlerName: String? = null | |||
| @Column(name = "release_type", length = 100) | |||
| var releaseType: String? = null | |||
| // Default constructor for Hibernate | |||
| constructor() | |||
| @@ -119,6 +121,7 @@ class DoPickOrder { | |||
| pickOrderCode: String? = null, | |||
| deliveryOrderCode: String? = null, | |||
| loadingSequence: Int? = null, | |||
| releaseType: String? = null | |||
| ) { | |||
| this.storeId = storeId | |||
| this.ticketNo = ticketNo | |||
| @@ -141,5 +144,6 @@ class DoPickOrder { | |||
| this.pickOrderCode = pickOrderCode | |||
| this.deliveryOrderCode = deliveryOrderCode | |||
| this.loadingSequence = loadingSequence | |||
| this.releaseType = releaseType | |||
| } | |||
| } | |||
| @@ -94,6 +94,8 @@ class DoPickOrderRecord { | |||
| var deleted: Boolean = false | |||
| @Column(name = "handler_name", length = 100) | |||
| var handlerName: String? = null | |||
| @Column(name = "release_type", length = 100) | |||
| var releaseType: String? = null | |||
| // Default constructor for Hibernate | |||
| constructor() | |||
| @@ -39,5 +39,10 @@ fun findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||
| @Param("startDate") startDate: LocalDate, | |||
| @Param("endDate") endDate: LocalDate | |||
| ): List<DoPickOrder> | |||
| fun findByShopIdAndStoreIdAndReleaseTypeAndTicketStatusAndDeletedFalse( | |||
| shopId: Long?, | |||
| storeId: String, | |||
| releaseType: String, | |||
| ticketStatus: DoPickOrderStatus | |||
| ): List<DoPickOrder> | |||
| } | |||
| @@ -18,9 +18,9 @@ class DoPickOrderQueryService( | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository | |||
| ) { | |||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | |||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?, releaseType: String): StoreLaneSummary { | |||
| val targetDate = requiredDate ?: LocalDate.now() | |||
| println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate") | |||
| println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate, releaseType=$releaseType") | |||
| val actualStoreId = when (storeId) { | |||
| "2/F" -> "2/F" | |||
| @@ -35,6 +35,13 @@ class DoPickOrderQueryService( | |||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | |||
| ) | |||
| // 根据 releaseType 过滤 activeRecords | |||
| val filteredActiveRecordsByReleaseType = when (releaseType.lowercase()) { | |||
| "batch" -> activeRecords.filter { it.releaseType == "batch" } | |||
| "single" -> activeRecords.filter { it.releaseType == "single" } | |||
| else -> activeRecords // "all" 或其他值,不过滤 | |||
| } | |||
| // Query completed records from do_pick_order_record table | |||
| val completedRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||
| actualStoreId, | |||
| @@ -42,11 +49,20 @@ class DoPickOrderQueryService( | |||
| listOf(DoPickOrderStatus.completed) | |||
| ) | |||
| // 根据 releaseType 过滤 completedRecords | |||
| val filteredCompletedRecordsByReleaseType = when (releaseType.lowercase()) { | |||
| "batch" -> completedRecords.filter { it.releaseType == "batch" } | |||
| "single" -> completedRecords.filter { it.releaseType == "single" } | |||
| else -> completedRecords // "all" 或其他值,不过滤 | |||
| } | |||
| println("🔍 DEBUG: Found ${activeRecords.size} active records for date $targetDate") | |||
| println("🔍 DEBUG: After releaseType filter: ${filteredActiveRecordsByReleaseType.size} active records") | |||
| println("🔍 DEBUG: Found ${completedRecords.size} completed records for date $targetDate") | |||
| println("🔍 DEBUG: After releaseType filter: ${filteredCompletedRecordsByReleaseType.size} completed records") | |||
| // Filter active records (check for non-issue lines) | |||
| val filteredActiveRecords = activeRecords.filter { doPickOrder -> | |||
| val filteredActiveRecords = filteredActiveRecordsByReleaseType.filter { doPickOrder -> | |||
| val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | |||
| if (!hasNonIssueLines) { | |||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | |||
| @@ -55,7 +71,7 @@ class DoPickOrderQueryService( | |||
| } | |||
| // For completed records, check if they have non-issue lines in the record table | |||
| val filteredCompletedRecords = completedRecords.filter { record -> | |||
| val filteredCompletedRecords = filteredCompletedRecordsByReleaseType.filter { record -> | |||
| val hasNonIssueLines = checkDoPickOrderRecordHasNonIssueLines(record.id!!) | |||
| if (!hasNonIssueLines) { | |||
| println("🔍 DEBUG: Filtering out DoPickOrderRecord ${record.id} - all lines are issues") | |||
| @@ -91,7 +91,7 @@ open class DoPickOrderService( | |||
| return RecordsRes<DeliveryOrderInfo>(records, total.toInt()); | |||
| } | |||
| open fun getNextTicketNumber(datePrefix: String, storeId: String): String { | |||
| println("🔍 DEBUG: Getting next ticket number for date prefix: $datePrefix, store: $storeId") | |||
| println(" DEBUG: Getting next ticket number for date prefix: $datePrefix, store: $storeId") | |||
| try { | |||
| val sanitizedStoreId = storeId.replace("/", "") | |||
| val shortDatePrefix = if (datePrefix.length == 8) { | |||
| @@ -102,14 +102,14 @@ open class DoPickOrderService( | |||
| // 修改搜索模式为新格式 | |||
| val searchPattern = "TI-${shortDatePrefix}-${sanitizedStoreId}-" // T-20250915-4F- | |||
| val todayTickets = doPickOrderRepository.findByTicketNoStartingWith(searchPattern) | |||
| println("🔍 DEBUG: Found ${todayTickets.size} existing tickets with prefix $searchPattern") | |||
| println(" DEBUG: Found ${todayTickets.size} existing tickets with prefix $searchPattern") | |||
| todayTickets.forEach { ticket -> | |||
| println("🔍 DEBUG: Existing ticket: ${ticket.ticketNo}, Status: ${ticket.ticketStatus}") | |||
| println(" DEBUG: Existing ticket: ${ticket.ticketNo}, Status: ${ticket.ticketStatus}") | |||
| } | |||
| val nextNumber = (todayTickets.size + 1).toString().padStart(3, '0') | |||
| // 修改生成格式 | |||
| val ticketNumber = "TI-${datePrefix}-${sanitizedStoreId}-${nextNumber}" // T-20250915-4F-001 | |||
| println("🔍 DEBUG: Generated ticket number: $ticketNumber") | |||
| println(" DEBUG: Generated ticket number: $ticketNumber") | |||
| return ticketNumber | |||
| } catch (e: Exception) { | |||
| println("❌ ERROR in getNextTicketNumber: ${e.message}") | |||
| @@ -297,41 +297,49 @@ open class DoPickOrderService( | |||
| } | |||
| return doPickOrderRepository.saveAll(doPickOrders) | |||
| } | |||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | |||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?, releaseType: String): StoreLaneSummary { | |||
| val targetDate = requiredDate ?: LocalDate.now() | |||
| println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate") | |||
| println(" DEBUG: Getting summary for store=$storeId, date=$targetDate") | |||
| val actualStoreId = when (storeId) { | |||
| "2/F" -> "2/F" | |||
| "4/F" -> "4/F" | |||
| else -> storeId | |||
| } | |||
| val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||
| actualStoreId, | |||
| targetDate, | |||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | |||
| ) | |||
| val filteredByReleaseType = when (releaseType.lowercase()) { | |||
| "batch" -> allRecords.filter { it.releaseType == "batch" } | |||
| "single" -> allRecords.filter { it.releaseType == "single" } | |||
| else -> allRecords // "all" 或其他值,不过滤 | |||
| } | |||
| // 添加 finishedRecords 查询 | |||
| val finishedRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||
| actualStoreId, | |||
| targetDate, | |||
| listOf(DoPickOrderStatus.completed) | |||
| ) | |||
| val filteredFinishedRecords = when (releaseType.lowercase()) { | |||
| "batch" -> finishedRecords.filter { it.releaseType == "batch" } | |||
| "single" -> finishedRecords.filter { it.releaseType == "single" } | |||
| else -> finishedRecords // "all" 或其他值,不过滤 | |||
| } | |||
| println(" DEBUG: Found ${allRecords.size} records for date $targetDate") | |||
| println(" DEBUG: Found ${finishedRecords.size} finished records for date $targetDate") | |||
| println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate") | |||
| println("🔍 DEBUG: Found ${finishedRecords.size} finished records for date $targetDate") | |||
| val filteredRecords = allRecords.filter { doPickOrder -> | |||
| val filteredRecords = filteredByReleaseType.filter { doPickOrder -> | |||
| val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | |||
| if (!hasNonIssueLines) { | |||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | |||
| println(" DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | |||
| } | |||
| hasNonIssueLines | |||
| } | |||
| println("🔍 DEBUG: After filtering, ${filteredRecords.size} records remain") | |||
| println(" DEBUG: After filtering, ${filteredRecords.size} records remain") | |||
| val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||
| .mapValues { (key, list) -> | |||
| @@ -341,9 +349,9 @@ open class DoPickOrderService( | |||
| (record.truckDepartureTime == truckDepartureTime) && | |||
| (record.truckLanceCode == truckLanceCode) | |||
| } | |||
| println("🔍 DEBUG: Group key - truckDepartureTime: $truckDepartureTime, truckLanceCode: $truckLanceCode") | |||
| println("🔍 DEBUG: Found ${list.size} active records in this group") | |||
| println("🔍 DEBUG: Found $matchingFinishedCount finished records matching this group") | |||
| println(" DEBUG: Group key - truckDepartureTime: $truckDepartureTime, truckLanceCode: $truckLanceCode") | |||
| println(" DEBUG: Found ${list.size} active records in this group") | |||
| println(" DEBUG: Found $matchingFinishedCount finished records matching this group") | |||
| LaneBtn( | |||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||
| unassigned = list.count { it.handledBy == null }, | |||
| @@ -403,7 +411,7 @@ open class DoPickOrderService( | |||
| // 3. 只有当所有 lines 都是 "issue" 状态时才过滤掉 | |||
| val hasNonIssueLines = nonIssueLines > 0 | |||
| println("🔍 DEBUG: DoPickOrder $doPickOrderId - Total lines: $totalLines, Non-issue lines: $nonIssueLines, Has non-issue lines: $hasNonIssueLines") | |||
| println(" DEBUG: DoPickOrder $doPickOrderId - Total lines: $totalLines, Non-issue lines: $nonIssueLines, Has non-issue lines: $hasNonIssueLines") | |||
| return hasNonIssueLines | |||
| @@ -426,7 +434,7 @@ open class DoPickOrderService( | |||
| "4/F" -> "4/F" // 保持原格式 | |||
| else -> request.storeId | |||
| } | |||
| println("🔍 DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'") | |||
| println(" DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'") | |||
| val candidates = doPickOrderRepository | |||
| .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | |||
| @@ -453,7 +461,7 @@ open class DoPickOrderService( | |||
| // 关键修改:获取这个 do_pick_order 下的所有 pick orders 并分配给用户 | |||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(firstOrder.id!!) | |||
| println("🔍 DEBUG: Found ${doPickOrderLines.size} pick orders in do_pick_order ${firstOrder.id}") | |||
| println(" DEBUG: Found ${doPickOrderLines.size} pick orders in do_pick_order ${firstOrder.id}") | |||
| doPickOrderLines.forEach { line -> | |||
| if (line.pickOrderId != null) { | |||
| @@ -462,7 +470,7 @@ open class DoPickOrderService( | |||
| pickOrder.assignTo = user | |||
| pickOrder.status = PickOrderStatus.RELEASED | |||
| pickOrderRepository.save(pickOrder) | |||
| println("🔍 DEBUG: Assigned pick order ${line.pickOrderId} to user ${request.userId}") | |||
| println(" DEBUG: Assigned pick order ${line.pickOrderId} to user ${request.userId}") | |||
| } else { | |||
| println("⚠️ WARNING: Pick order ${line.pickOrderId} not found") | |||
| } | |||
| @@ -481,7 +489,7 @@ open class DoPickOrderService( | |||
| } | |||
| if (records.isNotEmpty()) { | |||
| doPickOrderRecordRepository.saveAll(records) | |||
| println("🔍 DEBUG: Updated ${records.size} do_pick_order_record for pick order ${line.pickOrderId}") | |||
| println(" DEBUG: Updated ${records.size} do_pick_order_record for pick order ${line.pickOrderId}") | |||
| } | |||
| } | |||
| } | |||
| @@ -157,7 +157,7 @@ class DoReleaseCoordinatorService( | |||
| ) | |||
| SELECT | |||
| dpo2.id, | |||
| CONCAT('TI-', | |||
| CONCAT('TI-B-', | |||
| DATE_FORMAT(dpo2.RequiredDeliveryDate, '%Y%m%d'), | |||
| '-', | |||
| REPLACE(COALESCE(dpo2.store_id, ts.preferred_floor, '2F'), '/', ''), | |||
| @@ -212,6 +212,14 @@ class DoReleaseCoordinatorService( | |||
| AND w.store_id IN ('2F', '4F') | |||
| WHERE dol.deleted = 0 | |||
| AND dol.deliveryOrderId IN (${ids.joinToString(",")}) | |||
| AND dol.deliveryOrderId NOT IN ( | |||
| SELECT DISTINCT dpol.do_order_id | |||
| FROM fpsmsdb.do_pick_order_line dpol | |||
| INNER JOIN fpsmsdb.do_pick_order dpo ON dpo.id = dpol.do_pick_order_id | |||
| WHERE dpo.release_type = 'single' | |||
| AND dpo.deleted = 0 | |||
| AND dpol.deleted = 0 | |||
| ) | |||
| GROUP BY dol.deliveryOrderId, w.store_id | |||
| ), | |||
| DoFloorSummary AS ( | |||
| @@ -507,7 +515,9 @@ class DoReleaseCoordinatorService( | |||
| truckLanceCode = first.truckLanceCode, | |||
| shopCode = first.shopCode, | |||
| shopName = first.shopName, | |||
| requiredDeliveryDate = first.estimatedArrivalDate | |||
| requiredDeliveryDate = first.estimatedArrivalDate, | |||
| releaseType = "batch" | |||
| ) | |||
| // 直接使用 doPickOrderRepository.save() 而不是 doPickOrderService.save() | |||
| @@ -616,4 +626,206 @@ class DoReleaseCoordinatorService( | |||
| ) | |||
| ) | |||
| } | |||
| fun startBatchReleaseAsyncSingle(doId: Long, userId: Long): MessageResponse { | |||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId) | |||
| ?: return MessageResponse( | |||
| id = null, code = "NOT_FOUND", name = null, type = null, | |||
| message = "Delivery Order not found", errorPosition = null, entity = null | |||
| ) | |||
| executor.submit { | |||
| try { | |||
| println("📦 Starting single release for DO $doId") | |||
| // 调用 releaseDeliveryOrderWithoutTicket 创建 pick order | |||
| val result = deliveryOrderService.releaseDeliveryOrderWithoutTicket( | |||
| ReleaseDoRequest(id = doId, userId = userId) | |||
| ) | |||
| // 确定 storeId | |||
| val storeId = when (result.preferredFloor) { | |||
| "2F" -> "2/F" | |||
| "4F" -> "4/F" | |||
| else -> "2/F" | |||
| } | |||
| // 查找是否已有相同 shop、storeId 和 releaseType='single' 的 do_pick_order | |||
| val existingDoPickOrder = doPickOrderRepository | |||
| .findByShopIdAndStoreIdAndReleaseTypeAndTicketStatusAndDeletedFalse( | |||
| result.shopId, | |||
| storeId, | |||
| "single", | |||
| DoPickOrderStatus.pending | |||
| ) | |||
| .firstOrNull { | |||
| it.requiredDeliveryDate == result.estimatedArrivalDate && | |||
| it.truckDepartureTime == result.truckDepartureTime && | |||
| it.truckLanceCode == result.truckLanceCode | |||
| } | |||
| if (existingDoPickOrder != null) { | |||
| // 如果已存在,创建 do_pick_order_line 关联到已存在的 do_pick_order | |||
| val existingLine = doPickOrderLineRepository | |||
| .findByPickOrderIdAndDeletedFalse(result.pickOrderId) | |||
| .firstOrNull { it.doPickOrderId == existingDoPickOrder.id } | |||
| if (existingLine == null) { | |||
| val line = DoPickOrderLine().apply { | |||
| doPickOrderId = existingDoPickOrder.id | |||
| pickOrderId = result.pickOrderId | |||
| doOrderId = result.deliveryOrderId | |||
| pickOrderCode = result.pickOrderCode | |||
| deliveryOrderCode = result.deliveryOrderCode | |||
| status = "pending" | |||
| } | |||
| doPickOrderLineRepository.save(line) | |||
| println("🔍 DEBUG: Created DoPickOrderLine for existing DoPickOrder ${existingDoPickOrder.id}") | |||
| } | |||
| } else { | |||
| // 如果不存在,创建新的 do_pick_order | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| ticketStatus = DoPickOrderStatus.pending, | |||
| truckId = result.truckId, | |||
| truckDepartureTime = result.truckDepartureTime, | |||
| shopId = result.shopId, | |||
| handledBy = null, | |||
| loadingSequence = result.loadingSequence ?: 999, | |||
| ticketReleaseTime = null, | |||
| truckLanceCode = result.truckLanceCode, | |||
| shopCode = result.shopCode, | |||
| shopName = result.shopName, | |||
| requiredDeliveryDate = result.estimatedArrivalDate, | |||
| releaseType = "single" // 设置为 single | |||
| ) | |||
| val saved = doPickOrderRepository.save(doPickOrder) | |||
| // 创建 do_pick_order_line | |||
| val line = DoPickOrderLine().apply { | |||
| doPickOrderId = saved.id | |||
| pickOrderId = result.pickOrderId | |||
| doOrderId = result.deliveryOrderId | |||
| pickOrderCode = result.pickOrderCode | |||
| deliveryOrderCode = result.deliveryOrderCode | |||
| status = "pending" | |||
| } | |||
| doPickOrderLineRepository.save(line) | |||
| println("🔍 DEBUG: Created new DoPickOrder with releaseType=single") | |||
| } | |||
| // 更新 ticket numbers(只更新 single 类型的) | |||
| updateSingleTicketNumbers() | |||
| } catch (e: Exception) { | |||
| println("❌ Single release exception: ${e.message}") | |||
| e.printStackTrace() | |||
| } | |||
| } | |||
| return MessageResponse( | |||
| id = null, code = "STARTED", name = null, type = null, | |||
| message = "Single release started", errorPosition = null, | |||
| entity = mapOf("doId" to doId) | |||
| ) | |||
| } | |||
| private fun updateSingleTicketNumbers() { | |||
| try { | |||
| // 1. 查找所有 TEMP- 开头的 single release type 订单 | |||
| val tempTickets = doPickOrderRepository.findAll() | |||
| .filter { | |||
| it.ticketNo?.startsWith("TEMP-") == true && | |||
| it.releaseType == "single" && | |||
| !it.deleted | |||
| } | |||
| if (tempTickets.isEmpty()) { | |||
| println("🔍 No single release type tickets to update") | |||
| return | |||
| } | |||
| println("🔍 DEBUG: Found ${tempTickets.size} single release type tickets to update") | |||
| // 2. 按日期和 storeId 分组 | |||
| val grouped = tempTickets.groupBy { ticket -> | |||
| val date = ticket.requiredDeliveryDate | |||
| val storeId = ticket.storeId?.replace("/", "") ?: "2F" | |||
| Pair(date, storeId) | |||
| } | |||
| // 3. 为每个组生成 ticket numbers | |||
| grouped.forEach { (dateStorePair, tickets) -> | |||
| val (date, storeId) = dateStorePair | |||
| if (date == null) { | |||
| println("⚠️ WARNING: Skipping tickets with null requiredDeliveryDate") | |||
| return@forEach | |||
| } | |||
| // 格式化日期为 YYYYMMDD | |||
| val datePrefix = date.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")) | |||
| // 4. 查询该组已存在的 ticket numbers(包括已生成的,不只是 TEMP-) | |||
| val existingTicketPattern = "TI-S-$datePrefix-$storeId-" | |||
| val existingTickets = doPickOrderRepository.findAll() | |||
| .filter { | |||
| it.ticketNo?.startsWith(existingTicketPattern) == true && | |||
| it.releaseType == "single" && | |||
| !it.deleted && | |||
| it.requiredDeliveryDate == date && | |||
| it.storeId?.replace("/", "") == storeId | |||
| } | |||
| .mapNotNull { it.ticketNo } | |||
| // 提取已存在的序号 | |||
| val existingNumbers = existingTickets.mapNotNull { ticketNo -> | |||
| val parts = ticketNo.split("-") | |||
| if (parts.size >= 4) { | |||
| parts.lastOrNull()?.toIntOrNull() | |||
| } else null | |||
| }.toMutableSet() | |||
| println("🔍 DEBUG: Group ($date, $storeId) - Existing ticket numbers: $existingTickets, Existing numbers: $existingNumbers") | |||
| // 5. 在组内按 truckDepartureTime, truckLanceCode, loadingSequence, doOrderId 排序 | |||
| val sortedTickets = tickets.sortedWith( | |||
| compareBy<DoPickOrder>( | |||
| { it.truckDepartureTime ?: java.time.LocalTime.of(23, 59, 59) }, | |||
| { it.truckLanceCode ?: "ZZ" }, | |||
| { it.loadingSequence ?: 999 }, | |||
| { it.doOrderId ?: 0L } | |||
| ) | |||
| ) | |||
| // 6. 为每个订单生成序号并更新 ticket_no(跳过已存在的序号) | |||
| var nextNumber = 1 | |||
| sortedTickets.forEach { ticket -> | |||
| // 找到下一个可用的序号 | |||
| while (existingNumbers.contains(nextNumber)) { | |||
| nextNumber++ | |||
| } | |||
| val sequenceNumber = nextNumber.toString().padStart(3, '0') | |||
| val newTicketNo = "TI-S-$datePrefix-$storeId-$sequenceNumber" | |||
| ticket.ticketNo = newTicketNo | |||
| doPickOrderRepository.save(ticket) | |||
| // 将新生成的序号添加到已存在集合中,避免同一批次内重复 | |||
| existingNumbers.add(nextNumber) | |||
| nextNumber++ | |||
| println("🔍 DEBUG: Updated ticket ${ticket.id} to $newTicketNo") | |||
| } | |||
| } | |||
| println("✅ Updated ${tempTickets.size} single release type ticket numbers") | |||
| } catch (e: Exception) { | |||
| println("❌ Error updating single ticket numbers: ${e.message}") | |||
| e.printStackTrace() | |||
| } | |||
| } | |||
| } | |||
| @@ -69,27 +69,32 @@ class DoPickOrderController( | |||
| @GetMapping("/summary-by-store") | |||
| fun getSummaryByStore( | |||
| @RequestParam storeId: String, | |||
| @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate? | |||
| @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?, | |||
| @RequestParam releaseType: String? | |||
| ): StoreLaneSummary { | |||
| return doPickOrderQueryService.getSummaryByStore(storeId, requiredDate) | |||
| return doPickOrderQueryService.getSummaryByStore(storeId, requiredDate, releaseType?: "ALL") | |||
| } | |||
| @PostMapping("/assign-by-lane") | |||
| fun assignByLane(@RequestBody request: AssignByLaneRequest): MessageResponse { | |||
| return doPickOrderAssignmentService.assignByLane(request) // 使用新的 Service | |||
| } | |||
| @PostMapping("/batch-release/async") | |||
| fun startBatchReleaseAsync( | |||
| @RequestBody ids: List<Long>, | |||
| @RequestParam(defaultValue = "1") userId: Long | |||
| ): MessageResponse { | |||
| return doReleaseCoordinatorService.startBatchReleaseAsync(ids, userId) | |||
| } | |||
| @GetMapping("/batch-release/progress/{jobId}") | |||
| fun getBatchReleaseProgress(@PathVariable jobId: String): MessageResponse { | |||
| return doReleaseCoordinatorService.getBatchReleaseProgress(jobId) | |||
| } | |||
| @PostMapping("/batch-release/async") | |||
| fun startBatchReleaseAsync( | |||
| @RequestBody ids: List<Long>, | |||
| @RequestParam(defaultValue = "1") userId: Long | |||
| ): MessageResponse { | |||
| return doReleaseCoordinatorService.startBatchReleaseAsync(ids, userId) | |||
| } | |||
| @PostMapping("/batch-release/async-single") | |||
| fun startBatchReleaseAsyncSingle( @RequestBody doId: Long, @RequestParam(defaultValue = "1") userId: Long | |||
| ): MessageResponse { | |||
| return doReleaseCoordinatorService.startBatchReleaseAsyncSingle(doId, userId) | |||
| } | |||
| @GetMapping("/batch-release/progress/{jobId}") | |||
| fun getBatchReleaseProgress(@PathVariable jobId: String): MessageResponse { | |||
| return doReleaseCoordinatorService.getBatchReleaseProgress(jobId) | |||
| } | |||
| @GetMapping("/ticket-release-table") | |||
| fun getTicketReleaseTable( | |||
| @@ -58,6 +58,8 @@ data class JobOrderInfoWithTypeName( | |||
| val item: JobOrderItemInfo, | |||
| val stockInLineId: Long?, | |||
| val stockInLineStatus: String?, | |||
| val sufficientCount: Int?, | |||
| val insufficientCount: Int?, | |||
| val silHandlerId: Long?, | |||
| val planStart: LocalDateTime?, | |||
| val status: String, | |||
| @@ -39,6 +39,11 @@ import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus | |||
| import com.ffii.fpsms.modules.productProcess.entity.ProductProcessLineRepository | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutRepository | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderProcessRepository | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderLotsHierarchicalResponse | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoResponse | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderBasicInfoResponse | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderLineWithLotsResponse | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.LotDetailResponse | |||
| @Service | |||
| open class JoPickOrderService( | |||
| private val joPickOrderRepository: JoPickOrderRepository, | |||
| @@ -215,7 +220,7 @@ open class JoPickOrderService( | |||
| } | |||
| return joPickOrderRecordRepository.saveAll(joPickOrderRecords) | |||
| } | |||
| open fun getAllJobOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | |||
| open fun getAllJobOrderLotsWithDetailsHierarchical(userId: Long): JobOrderLotsHierarchicalResponse { | |||
| println("=== Debug: getAllJobOrderLotsWithDetailsHierarchical ===") | |||
| println("today: ${LocalDate.now()}") | |||
| println("userId filter: $userId") | |||
| @@ -224,25 +229,38 @@ open class JoPickOrderService( | |||
| val user = userService.find(userId).orElse(null) | |||
| if (user == null) { | |||
| println("❌ User not found: $userId") | |||
| return emptyMap() | |||
| // ✅ 修复:返回 JobOrderLotsHierarchicalResponse | |||
| return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = PickOrderInfoResponse( | |||
| id = null, | |||
| code = null, | |||
| consoCode = null, | |||
| targetDate = null, | |||
| type = null, | |||
| status = null, | |||
| assignTo = null, | |||
| jobOrder = JobOrderBasicInfoResponse(0, "", "") | |||
| ), | |||
| pickOrderLines = emptyList() | |||
| ) | |||
| } | |||
| val statusList = listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED) | |||
| // Get all pick orders assigned to user with PENDING or RELEASED status that have joId | |||
| val allAssignedPickOrders = pickOrderRepository.findAllByAssignToIdAndStatusIn( | |||
| userId, | |||
| statusList | |||
| ).filter { it.jobOrder != null } // Only pick orders with joId | |||
| println("🔍 DEBUG: Found ${allAssignedPickOrders.size} job order pick orders assigned to user $userId") | |||
| // Filter based on assignment and status | |||
| val filteredPickOrders = if (allAssignedPickOrders.isNotEmpty()) { | |||
| // Check if there are any RELEASED orders assigned to this user (active work) | |||
| val assignedReleasedOrders = allAssignedPickOrders.filter { | |||
| val assignedReleasedOrders = allAssignedPickOrders.filter { | |||
| it.status == PickOrderStatus.RELEASED && it.assignTo?.id == userId | |||
| } | |||
| if (assignedReleasedOrders.isNotEmpty()) { | |||
| // If there are assigned RELEASED orders, show only those | |||
| println("🔍 DEBUG: Found ${assignedReleasedOrders.size} assigned RELEASED job orders, showing only those") | |||
| @@ -251,7 +269,8 @@ open class JoPickOrderService( | |||
| // If no assigned RELEASED orders, show only the latest COMPLETED order | |||
| val completedOrders = allAssignedPickOrders.filter { it.status == PickOrderStatus.COMPLETED } | |||
| if (completedOrders.isNotEmpty()) { | |||
| val latestCompleted = completedOrders.maxByOrNull { it.completeDate ?: it.modified ?: LocalDateTime.MIN } | |||
| val latestCompleted = | |||
| completedOrders.maxByOrNull { it.completeDate ?: it.modified ?: LocalDateTime.MIN } | |||
| println("🔍 DEBUG: No assigned RELEASED job orders, showing latest completed order: ${latestCompleted?.code}") | |||
| listOfNotNull(latestCompleted) | |||
| } else { | |||
| @@ -262,42 +281,68 @@ open class JoPickOrderService( | |||
| } else { | |||
| emptyList() | |||
| } | |||
| val pickOrderIds = filteredPickOrders.map { it.id!! } | |||
| println(" Job Order Pick order IDs to fetch: $pickOrderIds") | |||
| if (pickOrderIds.isEmpty()) { | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = PickOrderInfoResponse( | |||
| id = null, | |||
| code = null, | |||
| consoCode = null, | |||
| targetDate = null, | |||
| type = null, | |||
| status = null, | |||
| assignTo = null, | |||
| jobOrder = JobOrderBasicInfoResponse(0, "", "") | |||
| ), | |||
| pickOrderLines = emptyList() | |||
| ) | |||
| } | |||
| // 使用 Repository 获取数据 | |||
| try { | |||
| val pickOrders = pickOrderRepository.findAllByIdIn(pickOrderIds) | |||
| .filter { it.deleted == false && it.assignTo?.id == userId } | |||
| if (pickOrders.isEmpty()) { | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = PickOrderInfoResponse( | |||
| id = null, | |||
| code = null, | |||
| consoCode = null, | |||
| targetDate = null, | |||
| type = null, | |||
| status = null, | |||
| assignTo = null, | |||
| jobOrder = JobOrderBasicInfoResponse(0, "", "") | |||
| ), | |||
| pickOrderLines = emptyList() | |||
| ) | |||
| } | |||
| val pickOrder = pickOrders.first() // 取第一个(应该只有一个) | |||
| val jobOrder = pickOrder.jobOrder ?: return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| val jobOrder = pickOrder.jobOrder ?: return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = PickOrderInfoResponse( | |||
| id = null, | |||
| code = null, | |||
| consoCode = null, | |||
| targetDate = null, | |||
| type = null, | |||
| status = null, | |||
| assignTo = null, | |||
| jobOrder = JobOrderBasicInfoResponse(0, "", "") | |||
| ), | |||
| pickOrderLines = emptyList() | |||
| ) | |||
| // 获取 pick order lines | |||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!) | |||
| .filter { it.deleted == false } | |||
| // 获取所有 pick order line IDs | |||
| val pickOrderLineIds = pickOrderLines.map { it.id!! } | |||
| // 获取 suggested pick lots | |||
| val suggestedPickLots = if (pickOrderLineIds.isNotEmpty()) { | |||
| suggestPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||
| @@ -305,10 +350,10 @@ open class JoPickOrderService( | |||
| } else { | |||
| emptyList() | |||
| } | |||
| // 获取所有 inventory lot line IDs | |||
| val inventoryLotLineIds = suggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | |||
| // 获取 inventory lot lines | |||
| val inventoryLotLines = if (inventoryLotLineIds.isNotEmpty()) { | |||
| inventoryLotLineRepository.findAllByIdIn(inventoryLotLineIds) | |||
| @@ -316,7 +361,7 @@ open class JoPickOrderService( | |||
| } else { | |||
| emptyList() | |||
| } | |||
| // 获取 inventory lots | |||
| val inventoryLotIds = inventoryLotLines.mapNotNull { it.inventoryLot?.id }.distinct() | |||
| val inventoryLots = if (inventoryLotIds.isNotEmpty()) { | |||
| @@ -325,7 +370,7 @@ open class JoPickOrderService( | |||
| } else { | |||
| emptyList() | |||
| } | |||
| // 获取 stock out lines | |||
| val stockOutLines = if (pickOrderLineIds.isNotEmpty() && inventoryLotLineIds.isNotEmpty()) { | |||
| pickOrderLineIds.flatMap { polId -> | |||
| @@ -342,13 +387,13 @@ open class JoPickOrderService( | |||
| } | |||
| // 获取 jo_pick_order 记录 | |||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!) | |||
| // 构建 pick order info | |||
| val pickOrderInfo = mapOf( | |||
| "id" to pickOrder.id, | |||
| "code" to pickOrder.code, | |||
| "consoCode" to pickOrder.consoCode, | |||
| "targetDate" to pickOrder.targetDate?.let { | |||
| "targetDate" to pickOrder.targetDate?.let { | |||
| "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}" | |||
| }, | |||
| "type" to pickOrder.type?.value, | |||
| @@ -360,49 +405,35 @@ open class JoPickOrderService( | |||
| "name" to "Job Order ${jobOrder.code}" | |||
| ) | |||
| ) | |||
| // 构建 pick order lines with lots | |||
| val pickOrderLinesResult = pickOrderLines.map { pol -> | |||
| val item = pol.item | |||
| val uom = pol.uom | |||
| val lineId = pol.id!! | |||
| val suggestions = suggestedPickLots.filter { it.pickOrderLine?.id == lineId } | |||
| val stockoutsForLine = stockOutLinesByPickOrderLine[lineId].orEmpty() | |||
| // 获取该 line 的 suggested pick lots | |||
| val lineSuggestedLots = suggestedPickLots.filter { it.pickOrderLine?.id == pol.id } | |||
| // 构建 lots 数据 | |||
| val lots = lineSuggestedLots.mapNotNull { spl -> | |||
| val ill = spl.suggestedLotLine | |||
| if (ill == null || ill.deleted == true) return@mapNotNull null | |||
| val il = ill.inventoryLot | |||
| if (il == null || il.deleted == true) return@mapNotNull null | |||
| val warehouse = ill.warehouse | |||
| // 获取对应的 stock out line | |||
| val sol = stockOutLines.firstOrNull { | |||
| it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id | |||
| val sol = stockOutLines.firstOrNull { | |||
| it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id | |||
| } | |||
| // 获取对应的 jo_pick_order | |||
| val jpo = joPickOrders.firstOrNull { it.itemId == item?.id } | |||
| // 计算 available quantity | |||
| val availableQty = if (sol?.status == "rejected") { | |||
| null | |||
| } else { | |||
| (ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty ?: BigDecimal.ZERO) | |||
| (ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty | |||
| ?: BigDecimal.ZERO) | |||
| } | |||
| // 计算 total picked by all pick orders | |||
| val totalPickedByAllPickOrders = stockOutLines | |||
| .filter { it.inventoryLotLine?.id == ill.id && it.deleted == false } | |||
| .filter { it.status in listOf("pending", "checked", "partially_completed", "completed") } | |||
| .sumOf { it.qty?.toBigDecimal() ?: BigDecimal.ZERO } | |||
| // 计算 lot availability | |||
| val lotAvailability = when { | |||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | |||
| sol?.status == "rejected" -> "rejected" | |||
| @@ -410,72 +441,97 @@ open class JoPickOrderService( | |||
| ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | |||
| else -> "available" | |||
| } | |||
| // 计算 processing status | |||
| val processingStatus = when (sol?.status) { | |||
| "completed" -> "completed" | |||
| "rejected" -> "rejected" | |||
| "created" -> "pending" | |||
| else -> "pending" | |||
| } | |||
| mapOf( | |||
| "lotId" to ill.id, | |||
| "lotNo" to il.lotNo, | |||
| "expiryDate" to il.expiryDate?.let { | |||
| LotDetailResponse( | |||
| lotId = ill.id, | |||
| lotNo = il.lotNo, | |||
| expiryDate = il.expiryDate?.let { | |||
| "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}" | |||
| }, | |||
| "location" to warehouse?.name, | |||
| "availableQty" to availableQty?.toDouble(), | |||
| "requiredQty" to (spl.qty?.toDouble() ?: 0.0), | |||
| "actualPickQty" to (sol?.qty ?: 0.0), | |||
| "processingStatus" to processingStatus, | |||
| "lotAvailability" to lotAvailability, | |||
| "pickOrderId" to pickOrder.id, | |||
| "pickOrderCode" to pickOrder.code, | |||
| "pickOrderConsoCode" to pickOrder.consoCode, | |||
| "pickOrderLineId" to pol.id, | |||
| "stockOutLineId" to sol?.id, | |||
| "suggestedPickLotId" to spl.id, | |||
| "stockOutLineQty" to (sol?.qty ?: 0.0), | |||
| "stockOutLineStatus" to sol?.status, | |||
| "routerIndex" to warehouse?.order, | |||
| "routerArea" to warehouse?.code, | |||
| "routerRoute" to warehouse?.code, | |||
| "uomShortDesc" to uom?.udfShortDesc, | |||
| "matchStatus" to jpo?.matchStatus?.value, | |||
| "matchBy" to jpo?.matchBy, | |||
| "matchQty" to jpo?.matchQty | |||
| location = warehouse?.name, | |||
| availableQty = availableQty?.toDouble(), | |||
| requiredQty = spl.qty?.toDouble() ?: 0.0, | |||
| actualPickQty = sol?.qty ?: 0.0, | |||
| processingStatus = processingStatus, | |||
| lotAvailability = lotAvailability, | |||
| pickOrderId = pickOrder.id, | |||
| pickOrderCode = pickOrder.code, | |||
| pickOrderConsoCode = pickOrder.consoCode, | |||
| pickOrderLineId = pol.id, | |||
| stockOutLineId = sol?.id, | |||
| suggestedPickLotId = spl.id, | |||
| stockOutLineQty = sol?.qty ?: 0.0, | |||
| stockOutLineStatus = sol?.status, | |||
| routerIndex = warehouse?.order, | |||
| routerArea = warehouse?.code, | |||
| routerRoute = warehouse?.code, | |||
| uomShortDesc = uom?.udfShortDesc, | |||
| matchStatus = jpo?.matchStatus?.value, | |||
| matchBy = jpo?.matchBy, | |||
| matchQty = jpo?.matchQty?.toDouble() | |||
| ) | |||
| } | |||
| mapOf( | |||
| "id" to pol.id, | |||
| "itemId" to item?.id, | |||
| "itemCode" to item?.code, | |||
| "itemName" to item?.name, | |||
| "requiredQty" to pol.qty?.toDouble(), | |||
| "uomCode" to uom?.code, | |||
| "uomDesc" to uom?.udfudesc, | |||
| "lots" to lots | |||
| PickOrderLineWithLotsResponse( | |||
| id = pol.id!!, | |||
| itemId = item?.id, | |||
| itemCode = item?.code, | |||
| itemName = item?.name, | |||
| requiredQty = pol.qty?.toDouble(), | |||
| uomCode = uom?.code, | |||
| uomDesc = uom?.udfudesc, | |||
| status = pol.status?.value, | |||
| lots = lots | |||
| ) | |||
| } | |||
| return mapOf( | |||
| "pickOrder" to pickOrderInfo as Any?, | |||
| "pickOrderLines" to pickOrderLinesResult as Any? | |||
| // ✅ 修复第469行:返回 JobOrderLotsHierarchicalResponse | |||
| return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = PickOrderInfoResponse( | |||
| id = pickOrder.id, | |||
| code = pickOrder.code, | |||
| consoCode = pickOrder.consoCode, | |||
| targetDate = pickOrder.targetDate?.let { | |||
| "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}" | |||
| }, | |||
| type = pickOrder.type?.value, | |||
| status = pickOrder.status?.value, | |||
| assignTo = pickOrder.assignTo?.id, | |||
| jobOrder = JobOrderBasicInfoResponse( | |||
| id = jobOrder.id!!, | |||
| code = jobOrder.code ?: "", // ✅ 修复第482行:处理空值 | |||
| name = "Job Order ${jobOrder.code ?: ""}" | |||
| ) | |||
| ), | |||
| pickOrderLines = pickOrderLinesResult // ✅ 修复第486行:使用转换后的结果 | |||
| ) | |||
| } catch (e: Exception) { | |||
| println("❌ Error executing Job Order hierarchical query: ${e.message}") | |||
| e.printStackTrace() | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| // ✅ 修复 catch 块:返回 JobOrderLotsHierarchicalResponse 而不是 Map | |||
| return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = PickOrderInfoResponse( | |||
| id = null, | |||
| code = null, | |||
| consoCode = null, | |||
| targetDate = null, | |||
| type = null, | |||
| status = null, | |||
| assignTo = null, | |||
| jobOrder = JobOrderBasicInfoResponse(0, "", "") | |||
| ), | |||
| pickOrderLines = emptyList() | |||
| ) | |||
| } | |||
| } | |||
| // Get completed job order pick orders (for second tab) | |||
| // Fix the getCompletedJobOrderLotsHierarchical method | |||
| open fun getCompletedJobOrderLotsHierarchical(userId: Long): Map<String, Any?> { | |||
| @@ -1725,7 +1781,7 @@ open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> { | |||
| emptyList() | |||
| } | |||
| } | |||
| open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String, Any?> { | |||
| open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLotsHierarchicalResponse { | |||
| println("=== getJobOrderLotsHierarchicalByPickOrderId ===") | |||
| println("pickOrderId: $pickOrderId") | |||
| @@ -1733,17 +1789,36 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||
| val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) | |||
| if (pickOrder == null || pickOrder.deleted == true) { | |||
| println("❌ Pick order $pickOrderId not found or deleted") | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = PickOrderInfoResponse( | |||
| id = null, | |||
| code = null, | |||
| consoCode = null, | |||
| targetDate = null, | |||
| type = null, | |||
| status = null, | |||
| assignTo = null, | |||
| jobOrder = JobOrderBasicInfoResponse(0, "", "") | |||
| ), | |||
| pickOrderLines = emptyList() | |||
| ) | |||
| } | |||
| val jobOrder = pickOrder.jobOrder ?: return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| val jobOrder = pickOrder.jobOrder ?: return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = PickOrderInfoResponse( | |||
| id = null, | |||
| code = null, | |||
| consoCode = null, | |||
| targetDate = null, | |||
| type = null, | |||
| status = null, | |||
| assignTo = null, | |||
| jobOrder = JobOrderBasicInfoResponse(0, "", "") | |||
| ), | |||
| pickOrderLines = emptyList() | |||
| ) | |||
| // ✅ 添加数据获取逻辑(从原始 Map 版本复制) | |||
| // 获取 pick order lines | |||
| val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!) | |||
| .filter { it.deleted == false } | |||
| @@ -1799,20 +1874,20 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!) | |||
| // 构建 pick order info | |||
| val pickOrderInfo = mapOf( | |||
| "id" to pickOrder.id, | |||
| "code" to pickOrder.code, | |||
| "consoCode" to pickOrder.consoCode, | |||
| "targetDate" to pickOrder.targetDate?.let { | |||
| val pickOrderInfo = PickOrderInfoResponse( | |||
| id = pickOrder.id, | |||
| code = pickOrder.code, | |||
| consoCode = pickOrder.consoCode, | |||
| targetDate = pickOrder.targetDate?.let { | |||
| "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}" | |||
| }, | |||
| "type" to pickOrder.type?.value, | |||
| "status" to pickOrder.status?.value, | |||
| "assignTo" to pickOrder.assignTo?.id, | |||
| "jobOrder" to mapOf( | |||
| "id" to jobOrder.id, | |||
| "code" to jobOrder.code, | |||
| "name" to "Job Order ${jobOrder.code}" | |||
| type = pickOrder.type?.value, | |||
| status = pickOrder.status?.value, | |||
| assignTo = pickOrder.assignTo?.id, | |||
| jobOrder = JobOrderBasicInfoResponse( | |||
| id = jobOrder.id!!, | |||
| code = jobOrder.code ?: "", | |||
| name = "Job Order ${jobOrder.code}" | |||
| ) | |||
| ) | |||
| @@ -1821,9 +1896,6 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||
| val item = pol.item | |||
| val uom = pol.uom | |||
| val lineId = pol.id!! | |||
| val suggestions = suggestedPickLots.filter { it.pickOrderLine?.id == lineId } | |||
| val stockoutsForLine = stockOutLinesByPickOrderLine[lineId].orEmpty() | |||
| // 获取该 line 的 suggested pick lots | |||
| val lineSuggestedLots = suggestedPickLots.filter { it.pickOrderLine?.id == pol.id } | |||
| // 构建 lots 数据 | |||
| @@ -1835,29 +1907,17 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||
| if (il == null || il.deleted == true) return@mapNotNull null | |||
| val warehouse = ill.warehouse | |||
| // 获取对应的 stock out line | |||
| val sol = stockOutLines.firstOrNull { | |||
| it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id | |||
| } | |||
| // 获取对应的 jo_pick_order | |||
| val jpo = joPickOrders.firstOrNull { it.itemId == item?.id } | |||
| // 计算 available quantity | |||
| val availableQty = if (sol?.status == "rejected") { | |||
| null | |||
| } else { | |||
| (ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty ?: BigDecimal.ZERO) | |||
| } | |||
| // 计算 total picked by all pick orders | |||
| val totalPickedByAllPickOrders = stockOutLines | |||
| .filter { it.inventoryLotLine?.id == ill.id && it.deleted == false } | |||
| .filter { it.status in listOf("pending", "checked", "partially_completed", "completed") } | |||
| .sumOf { it.qty?.toBigDecimal() ?: BigDecimal.ZERO } | |||
| // 计算 lot availability | |||
| val lotAvailability = when { | |||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | |||
| sol?.status == "rejected" -> "rejected" | |||
| @@ -1866,7 +1926,6 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||
| else -> "available" | |||
| } | |||
| // 计算 processing status | |||
| val processingStatus = when (sol?.status) { | |||
| "completed" -> "completed" | |||
| "rejected" -> "rejected" | |||
| @@ -1874,59 +1933,69 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): Map<String | |||
| else -> "pending" | |||
| } | |||
| mapOf( | |||
| "lotId" to ill.id, | |||
| "lotNo" to il.lotNo, | |||
| "expiryDate" to il.expiryDate?.let { | |||
| LotDetailResponse( | |||
| lotId = ill.id, | |||
| lotNo = il.lotNo, | |||
| expiryDate = il.expiryDate?.let { | |||
| "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}" | |||
| }, | |||
| "location" to warehouse?.name, | |||
| "availableQty" to availableQty?.toDouble(), | |||
| "requiredQty" to (spl.qty?.toDouble() ?: 0.0), | |||
| "actualPickQty" to (sol?.qty ?: 0.0), | |||
| "processingStatus" to processingStatus, | |||
| "lotAvailability" to lotAvailability, | |||
| "pickOrderId" to pickOrder.id, | |||
| "pickOrderCode" to pickOrder.code, | |||
| "pickOrderConsoCode" to pickOrder.consoCode, | |||
| "pickOrderLineId" to pol.id, | |||
| "stockOutLineId" to sol?.id, | |||
| "suggestedPickLotId" to spl.id, | |||
| "stockOutLineQty" to (sol?.qty ?: 0.0), | |||
| "stockOutLineStatus" to sol?.status, | |||
| "routerIndex" to warehouse?.order, | |||
| "routerArea" to warehouse?.code, | |||
| "routerRoute" to warehouse?.code, | |||
| "uomShortDesc" to uom?.udfShortDesc, | |||
| "matchStatus" to jpo?.matchStatus?.value, | |||
| "matchBy" to jpo?.matchBy, | |||
| "matchQty" to jpo?.matchQty | |||
| location = warehouse?.name, | |||
| availableQty = availableQty?.toDouble(), | |||
| requiredQty = spl.qty?.toDouble() ?: 0.0, | |||
| actualPickQty = sol?.qty ?: 0.0, | |||
| processingStatus = processingStatus, | |||
| lotAvailability = lotAvailability, | |||
| pickOrderId = pickOrder.id, | |||
| pickOrderCode = pickOrder.code, | |||
| pickOrderConsoCode = pickOrder.consoCode, | |||
| pickOrderLineId = pol.id, | |||
| stockOutLineId = sol?.id, | |||
| suggestedPickLotId = spl.id, | |||
| stockOutLineQty = sol?.qty ?: 0.0, | |||
| stockOutLineStatus = sol?.status, | |||
| routerIndex = warehouse?.order, | |||
| routerArea = warehouse?.code, | |||
| routerRoute = warehouse?.code, | |||
| uomShortDesc = uom?.udfShortDesc, | |||
| matchStatus = jpo?.matchStatus?.value, | |||
| matchBy = jpo?.matchBy, | |||
| matchQty = jpo?.matchQty?.toDouble() | |||
| ) | |||
| } | |||
| mapOf( | |||
| "id" to pol.id, | |||
| "itemId" to item?.id, | |||
| "itemCode" to item?.code, | |||
| "itemName" to item?.name, | |||
| "requiredQty" to pol.qty?.toDouble(), | |||
| "uomCode" to uom?.code, | |||
| "uomDesc" to uom?.udfudesc, | |||
| "lots" to lots | |||
| PickOrderLineWithLotsResponse( | |||
| id = pol.id!!, | |||
| itemId = item?.id, | |||
| itemCode = item?.code, | |||
| itemName = item?.name, | |||
| requiredQty = pol.qty?.toDouble(), | |||
| uomCode = uom?.code, | |||
| uomDesc = uom?.udfudesc, | |||
| status = pol.status?.value, | |||
| lots = lots | |||
| ) | |||
| } | |||
| return mapOf( | |||
| "pickOrder" to pickOrderInfo as Any?, | |||
| "pickOrderLines" to pickOrderLinesResult as Any? | |||
| return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = pickOrderInfo, | |||
| pickOrderLines = pickOrderLinesResult | |||
| ) | |||
| } catch (e: Exception) { | |||
| println("❌ Error in getJobOrderLotsHierarchicalByPickOrderId: ${e.message}") | |||
| e.printStackTrace() | |||
| return mapOf( | |||
| "pickOrder" to null as Any?, | |||
| "pickOrderLines" to emptyList<Map<String, Any>>() as Any? | |||
| return JobOrderLotsHierarchicalResponse( | |||
| pickOrder = PickOrderInfoResponse( | |||
| id = null, | |||
| code = null, | |||
| consoCode = null, | |||
| targetDate = null, | |||
| type = null, | |||
| status = null, | |||
| assignTo = null, | |||
| jobOrder = JobOrderBasicInfoResponse(0, "", "") | |||
| ), | |||
| pickOrderLines = emptyList() | |||
| ) | |||
| } | |||
| } | |||
| @@ -56,7 +56,7 @@ import java.io.FileNotFoundException | |||
| import java.io.IOException | |||
| import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderInfoWithTypeName | |||
| import com.ffii.fpsms.modules.jobOrder.entity.projections.JobTypeResponse | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | |||
| @Service | |||
| open class JobOrderService( | |||
| @@ -73,7 +73,8 @@ open class JobOrderService( | |||
| val stockOutRepository: StockOutRepository, | |||
| val stockOutLineRepository: StockOutLIneRepository, | |||
| private val printerService: PrinterService, | |||
| val jobTypeRepository: JobTypeRepository | |||
| val jobTypeRepository: JobTypeRepository, | |||
| val inventoryRepository: InventoryRepository | |||
| ) { | |||
| open fun allJobOrdersByPage(request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfo> { | |||
| @@ -116,6 +117,27 @@ open class JobOrderService( | |||
| emptyMap() | |||
| } | |||
| // 获取所有 JobOrder IDs,用于批量加载 jobms | |||
| val jobOrderIds = response.content.map { it.id }.distinct() | |||
| val jobOrdersMap = if (jobOrderIds.isNotEmpty()) { | |||
| jobOrderRepository.findAllById(jobOrderIds).associateBy { it.id } | |||
| } else { | |||
| emptyMap() | |||
| } | |||
| // 获取所有涉及的 itemIds,用于批量加载 inventory | |||
| val allItemIds = jobOrdersMap.values | |||
| .flatMap { it.jobms } | |||
| .mapNotNull { it.item?.id } | |||
| .distinct() | |||
| val inventoriesMap = if (allItemIds.isNotEmpty()) { | |||
| inventoryRepository.findInventoryInfoByItemIdInAndDeletedIsFalse(allItemIds) | |||
| .associateBy { it.itemId } | |||
| } else { | |||
| emptyMap() | |||
| } | |||
| val planStartFrom = request.planStart | |||
| val planStartTo = request.planStartTo | |||
| val records = response.content | |||
| @@ -124,6 +146,15 @@ open class JobOrderService( | |||
| (planStartTo == null || (it.planStart != null && (planStartTo.isEqual(it.planStart) || planStartTo.isAfter(it.planStart)))) | |||
| } | |||
| .map { info -> | |||
| val jobOrder = jobOrdersMap[info.id] | |||
| // 计算 sufficientCount 和 insufficientCount | |||
| val (sufficientCount, insufficientCount) = if (jobOrder != null) { | |||
| calculateStockCounts(jobOrder, inventoriesMap) | |||
| } else { | |||
| Pair(null, null) | |||
| } | |||
| JobOrderInfoWithTypeName( | |||
| id = info.id, | |||
| code = info.code, | |||
| @@ -134,6 +165,8 @@ open class JobOrderService( | |||
| item = info.item, | |||
| stockInLineId = info.stockInLineId, | |||
| stockInLineStatus = info.stockInLineStatus, | |||
| sufficientCount = sufficientCount, | |||
| insufficientCount = insufficientCount, | |||
| silHandlerId = info.silHandlerId, | |||
| planStart = info.planStart, | |||
| status = info.status, | |||
| @@ -152,6 +185,55 @@ open class JobOrderService( | |||
| val total = response.totalElements | |||
| return RecordsRes<JobOrderInfoWithTypeName>(records, total.toInt()); | |||
| } | |||
| // 添加辅助方法计算库存统计 | |||
| private fun calculateStockCounts( | |||
| jobOrder: JobOrder, | |||
| inventoriesMap: Map<Long?, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo> | |||
| ): Pair<Int, Int> { | |||
| // 过滤掉 consumables 和 CMB 类型的物料 | |||
| val nonConsumablesJobms = jobOrder.jobms.filter { jobm -> | |||
| val itemType = jobm.item?.type?.lowercase() | |||
| itemType != "consumables" && itemType != "cmb" | |||
| } | |||
| if (nonConsumablesJobms.isEmpty()) { | |||
| return Pair(0, 0) | |||
| } | |||
| var sufficientCount = 0 | |||
| var insufficientCount = 0 | |||
| nonConsumablesJobms.forEach { jobm -> | |||
| val itemId = jobm.item?.id | |||
| val reqQty = jobm.reqQty ?: BigDecimal.ZERO | |||
| if (itemId != null) { | |||
| val inventory = inventoriesMap[itemId] | |||
| val availableQty = if (inventory != null) { | |||
| // 使用 availableQty,如果没有则计算:onHandQty - onHoldQty - unavailableQty | |||
| inventory.availableQty ?: ( | |||
| (inventory.onHandQty ?: BigDecimal.ZERO) - | |||
| (inventory.onHoldQty ?: BigDecimal.ZERO) - | |||
| (inventory.unavailableQty ?: BigDecimal.ZERO) | |||
| ) | |||
| } else { | |||
| BigDecimal.ZERO | |||
| } | |||
| if (availableQty >= reqQty) { | |||
| sufficientCount++ | |||
| } else { | |||
| insufficientCount++ | |||
| } | |||
| } else { | |||
| // 如果没有 itemId,视为不足 | |||
| insufficientCount++ | |||
| } | |||
| } | |||
| return Pair(sufficientCount, insufficientCount) | |||
| } | |||
| open fun jobOrderDetail(id: Long): JobOrderDetail { | |||
| val sqlResult = jobOrderRepository.findJobOrderDetailById(id) ?: throw NoSuchElementException(); | |||
| @@ -308,13 +390,26 @@ open class JobOrderService( | |||
| // 添加 suggested pick lots 创建逻辑 | |||
| val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!) | |||
| println("DEBUG: pickOrderLines=${lines.map { it.id to it.item?.code }}") | |||
| if (lines.isNotEmpty()) { | |||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLines( | |||
| val suggestions = suggestedPickLotService.suggestionForPickOrderLinesForJobOrder( | |||
| SuggestedPickLotForPolRequest(pickOrderLines = lines) | |||
| ) | |||
| println("DEBUG: suggestions.size=${suggestions.suggestedList.size}") | |||
| suggestions.suggestedList.forEach { s -> | |||
| println( | |||
| "DEBUG: suggestion polId=${s.pickOrderLine?.id}, lotLineId=${s.suggestedLotLine?.id}, " + | |||
| "lotNo=${s.suggestedLotLine?.inventoryLot?.lotNo}, qty=${s.qty}, pickSuggested=${s.pickSuggested}" | |||
| ) | |||
| } | |||
| val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | |||
| println("DEBUG: saved suggestions size=${saveSuggestedPickLots.size}") | |||
| saveSuggestedPickLots.forEach { s -> | |||
| println( | |||
| "DEBUG: saved polId=${s.pickOrderLine?.id}, lotLineId=${s.suggestedLotLine?.id}, " + | |||
| "lotNo=${s.suggestedLotLine?.inventoryLot?.lotNo}, qty=${s.qty}" | |||
| ) | |||
| } | |||
| // Hold inventory quantities | |||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | |||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | |||
| @@ -100,7 +100,7 @@ class JobOrderController( | |||
| return jo | |||
| } | |||
| @GetMapping("/all-lots-hierarchical/{userId}") | |||
| fun getAllJobOrderLotsHierarchical(@PathVariable userId: Long): Map<String, Any?> { | |||
| fun getAllJobOrderLotsHierarchical(@PathVariable userId: Long): JobOrderLotsHierarchicalResponse { | |||
| return joPickOrderService.getAllJobOrderLotsWithDetailsHierarchical(userId) | |||
| } | |||
| @@ -235,7 +235,7 @@ fun recordSecondScanIssue( | |||
| } | |||
| @GetMapping("/all-lots-hierarchical-by-pick-order/{pickOrderId}") | |||
| fun getJobOrderLotsHierarchicalByPickOrderId(@PathVariable pickOrderId: Long): Map<String, Any?> { | |||
| fun getJobOrderLotsHierarchicalByPickOrderId(@PathVariable pickOrderId: Long): JobOrderLotsHierarchicalResponse { | |||
| return joPickOrderService.getJobOrderLotsHierarchicalByPickOrderId(pickOrderId) | |||
| } | |||
| @PostMapping("/update-jo-pick-order-handled-by") | |||
| @@ -51,18 +51,72 @@ data class AllJoPickOrderResponse( | |||
| val finishedPickOLineCount: Int, | |||
| ) | |||
| data class JobOrderLotsHierarchicalResponse( | |||
| val id: Long, | |||
| val lotId: Long?, | |||
| val lotCode: String?, | |||
| val lotName: String?, | |||
| val lotQty: BigDecimal?, | |||
| val lotUomId: Long?, | |||
| val lotUomName: String?, | |||
| ) | |||
| data class UpdateJoPickOrderHandledByRequest( | |||
| val pickOrderId: Long, | |||
| val itemId: Long, | |||
| val userId: Long | |||
| ) | |||
| ) | |||
| data class JobOrderLotsHierarchicalResponse( | |||
| val pickOrder: PickOrderInfoResponse, | |||
| val pickOrderLines: List<PickOrderLineWithLotsResponse> | |||
| ) | |||
| data class PickOrderInfoResponse( | |||
| val id: Long?, | |||
| val code: String?, | |||
| val consoCode: String?, | |||
| val targetDate: String?, | |||
| val type: String?, | |||
| val status: String?, | |||
| val assignTo: Long?, | |||
| val jobOrder: JobOrderBasicInfoResponse | |||
| ) | |||
| data class JobOrderBasicInfoResponse( | |||
| val id: Long, | |||
| val code: String, | |||
| val name: String | |||
| ) | |||
| data class PickOrderLineWithLotsResponse( | |||
| val id: Long, | |||
| val itemId: Long?, | |||
| val itemCode: String?, | |||
| val itemName: String?, | |||
| val requiredQty: Double?, | |||
| val uomCode: String?, | |||
| val uomDesc: String?, | |||
| val status: String?, | |||
| val lots: List<LotDetailResponse> | |||
| ) | |||
| data class LotDetailResponse( | |||
| val lotId: Long?, | |||
| val lotNo: String?, | |||
| val expiryDate: String?, | |||
| val location: String?, | |||
| val availableQty: Double?, | |||
| val requiredQty: Double?, | |||
| val actualPickQty: Double?, | |||
| val processingStatus: String?, | |||
| val lotAvailability: String?, | |||
| val pickOrderId: Long?, | |||
| val pickOrderCode: String?, | |||
| val pickOrderConsoCode: String?, | |||
| val pickOrderLineId: Long?, | |||
| val stockOutLineId: Long?, | |||
| val suggestedPickLotId: Long?, | |||
| val stockOutLineQty: Double?, | |||
| val stockOutLineStatus: String?, | |||
| val routerIndex: Int?, | |||
| val routerArea: String?, | |||
| val routerRoute: String?, | |||
| val uomShortDesc: String?, | |||
| val matchStatus: String?, | |||
| val matchBy: Long?, | |||
| val matchQty: Double? | |||
| ) | |||
| @@ -14,6 +14,11 @@ open class ProductionProcessIssue : BaseEntity<Long>() { // 修复:改为正 | |||
| @ManyToOne(fetch = FetchType.LAZY) | |||
| @JoinColumn(name = "productprocessid", nullable = false) | |||
| open var productProcess: ProductProcess? = null | |||
| @Column(name = "productProcessLineId") | |||
| open var productProcessLineId: Long? = null | |||
| @ManyToOne(fetch = FetchType.LAZY) | |||
| @JoinColumn(name = "operatorId") | |||
| @@ -36,4 +41,6 @@ open class ProductionProcessIssue : BaseEntity<Long>() { // 修复:改为正 | |||
| @Column(name = "totalTime") | |||
| open var totalTime: Int? = null // 分钟 | |||
| @Column(name = "status") | |||
| open var status: String? = null | |||
| } | |||
| @@ -6,4 +6,5 @@ import org.springframework.stereotype.Repository | |||
| @Repository | |||
| interface ProductionProcessIssueRepository : JpaRepository<ProductionProcessIssue, Long> { | |||
| fun findByProductProcess_Id(productProcessId: Long): List<ProductionProcessIssue> | |||
| fun findByProductProcessLineId(productProcessLineId: Long):List<ProductionProcessIssue> | |||
| } | |||
| @@ -28,6 +28,7 @@ import com.ffii.fpsms.modules.master.entity.Process | |||
| import com.ffii.fpsms.modules.master.entity.Equipment | |||
| import com.ffii.fpsms.modules.master.entity.BomProcess | |||
| import com.ffii.fpsms.modules.master.entity.Bom | |||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderBomMaterialRepository | |||
| import com.ffii.fpsms.modules.master.service.ProductionScheduleService | |||
| @@ -39,8 +40,10 @@ import com.ffii.fpsms.modules.stock.service.StockInLineService | |||
| import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest | |||
| import com.ffii.fpsms.modules.master.entity.BomProcessMaterialRepository | |||
| import com.ffii.fpsms.modules.master.entity.BomMaterialRepository | |||
| import com.ffii.fpsms.modules.productProcess.entity.ProductionProcessIssue | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JobTypeRepository | |||
| import java.time.ZoneOffset | |||
| @Service | |||
| @Transactional | |||
| open class ProductProcessService( | |||
| @@ -532,11 +535,20 @@ open class ProductProcessService( | |||
| planEndDate.atStartOfDay(), | |||
| "detailed" | |||
| ) | |||
| scheduleLine?.itemPriority?.toString() ?: "0" | |||
| if (scheduleLine != null) { | |||
| // 计算 targetMinStock | |||
| val targetMinStock = scheduleLine.lastMonthAvgSales * 2 | |||
| // 调整后的生产数量(与 generateDetailedScheduleByDay 逻辑一致) | |||
| val adjustedProdQty = scheduleLine.prodQty * 2 | |||
| // 计算 difference(缺口) | |||
| val difference = -(targetMinStock + adjustedProdQty - scheduleLine.estCloseBal) | |||
| difference.toString() | |||
| } else { | |||
| "0" | |||
| } | |||
| } else { | |||
| "0" | |||
| } | |||
| } | |||
| fun calculateColourScore(value: Int?): String { | |||
| return when (value) { | |||
| 0 -> "淺" | |||
| @@ -899,6 +911,9 @@ open class ProductProcessService( | |||
| } | |||
| open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse { | |||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | |||
| val bomProcess = bomProcessRepository.findById(productProcessLine?.bomProcess?.id?:0L).orElse(null) | |||
| val productProcessIssue = productionProcessIssueRepository.findByProductProcessLineId(productProcessLineId).filter{it.status == "Paused"}.firstOrNull() | |||
| return JobOrderProcessLineDetailResponse( | |||
| id = productProcessLine.id?:0, | |||
| productProcessId = productProcessLine.productProcess ?.id?:0, | |||
| @@ -906,7 +921,11 @@ open class ProductProcessService( | |||
| operatorId = productProcessLine.operator?.id?:0, | |||
| operatorName = productProcessLine.operator?.name?:"", | |||
| handlerId = productProcessLine.handler?.id?:0, | |||
| durationInMinutes = productProcessLine.bomProcess?.durationInMinute?:0, | |||
| durationInMinutes = bomProcess?.durationInMinute?:0, | |||
| productProcessIssueId = productProcessIssue?.id?:0, | |||
| productProcessIssueStatus = productProcessIssue?.status?:"", | |||
| seqNo = productProcessLine.seqNo?:0, | |||
| name = productProcessLine.name?:"", | |||
| description = productProcessLine.description?:"", | |||
| @@ -1233,5 +1252,70 @@ open class ProductProcessService( | |||
| errorPosition = null, | |||
| ) | |||
| } | |||
| open fun SaveProductProcessIssueTime(request: SaveProductProcessIssueTimeRequest): MessageResponse { | |||
| println("📋 Service: Saving ProductProcess Issue Time: ${request.productProcessLineId}") | |||
| val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null) | |||
| val productProcess = productProcessRepository.findById(productProcessLine.productProcess?.id?:0L).orElse(null) | |||
| //val productProcessLines=productProcessLineRepository.findByProductProcess_Id(productProcessId) | |||
| val startTime=productProcessLine?.startTime | |||
| println("📋 Service: Start Time: $startTime") | |||
| val stopTime=LocalDateTime.now() | |||
| println("📋 Service: Stop Time: $stopTime") | |||
| val operatorId=productProcessLine.operator?.id | |||
| val Operator=userRepository.findById(operatorId).orElse(null) | |||
| val reason = request.reason | |||
| val productProcessIssue = ProductionProcessIssue().apply { | |||
| this.productProcess = productProcess | |||
| this.productProcessLineId = productProcessLine.id | |||
| this.operator = productProcessLine.operator | |||
| this.operatorName = Operator.name | |||
| this.reason = reason | |||
| this.status = "Paused" | |||
| //this.productProcess = operator.name | |||
| //this.startTime = startTime | |||
| this.stopTime = stopTime | |||
| } | |||
| productionProcessIssueRepository.save(productProcessIssue) | |||
| productProcessLine.status = "Paused" | |||
| productProcessLineRepository.save(productProcessLine) | |||
| return MessageResponse( | |||
| id = 0, | |||
| code = "200", | |||
| name = "ProductProcess IssueTime Updated", | |||
| type = "success", | |||
| message = "ProductProcess IssueTime Updated", | |||
| errorPosition = null, | |||
| ) | |||
| } | |||
| open fun SaveProductProcessResumeTime(productProcessIssueId: Long): MessageResponse { | |||
| println("📋 Service: Saving ProductProcess Resume Time: $productProcessIssueId") | |||
| val productProcessLineIssue = productionProcessIssueRepository.findById(productProcessIssueId).orElse(null) | |||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineIssue.productProcessLineId?:0L).orElse(null) | |||
| val resumeTime = LocalDateTime.now() | |||
| println("📋 Service: Resume Time: $resumeTime") | |||
| productProcessLineIssue?.resumeTime = resumeTime | |||
| println("📋 Service: Resume Time: $resumeTime") | |||
| val totalTime = resumeTime.toEpochSecond(ZoneOffset.UTC) - (productProcessLineIssue.stopTime?.toEpochSecond(ZoneOffset.UTC) ?: 0L) | |||
| productProcessLineIssue?.totalTime = totalTime.toInt() | |||
| productProcessLineIssue?.status = "Resumed" | |||
| productionProcessIssueRepository.save(productProcessLineIssue) | |||
| productProcessLine.status = "InProgress" | |||
| productProcessLineRepository.save(productProcessLine) | |||
| return MessageResponse( | |||
| id = 0, | |||
| code = "200", | |||
| name = "ProductProcess ResumeTime Updated", | |||
| type = "success", | |||
| message = "ProductProcess ResumeTime Updated", | |||
| errorPosition = null, | |||
| ) | |||
| } | |||
| } | |||
| @@ -197,4 +197,12 @@ class ProductProcessController( | |||
| fun completeProductProcessLine(@PathVariable lineId: Long): MessageResponse { | |||
| return productProcessService.CompleteProductProcessLine(lineId) | |||
| } | |||
| @PostMapping("/Demo/ProcessLine/issue") | |||
| fun issueProductProcessLine(@RequestBody request: SaveProductProcessIssueTimeRequest): MessageResponse { | |||
| return productProcessService.SaveProductProcessIssueTime(request) | |||
| } | |||
| @PostMapping("/Demo/ProcessLine/resume/{productProcessIssueId}") | |||
| fun resumeProductProcessLine(@PathVariable productProcessIssueId: Long): MessageResponse { | |||
| return productProcessService.SaveProductProcessResumeTime(productProcessIssueId) | |||
| } | |||
| } | |||
| @@ -132,7 +132,8 @@ data class JobOrderProcessLineDetailResponse( | |||
| val equipmentId: Long?, | |||
| val startTime: LocalDateTime?, | |||
| val endTime: LocalDateTime?, | |||
| val productProcessIssueId: Long?, | |||
| val productProcessIssueStatus: String?, | |||
| val status: String, | |||
| val outputFromProcessQty: Int?, | |||
| val outputFromProcessUom: String?, | |||
| @@ -190,4 +191,12 @@ data class NewUpdateProductProcessLineOperatorIdOrEquipmentIdAndEquipmentDetailR | |||
| val EquipmentTypeSubTypeEquipmentNo: String, | |||
| val staffNo: String?, | |||
| val Name: String?, | |||
| ) | |||
| data class SaveProductProcessIssueTimeRequest( | |||
| val productProcessLineId: Long, | |||
| val reason: String | |||
| ) | |||
| data class SaveProductProcessResumeTimeRequest( | |||
| val productProcessLineId: Long, | |||
| val resumeTime: LocalDateTime? | |||
| ) | |||
| @@ -235,6 +235,148 @@ open class SuggestedPickLotService( | |||
| } | |||
| return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) | |||
| } | |||
| open fun suggestionForPickOrderLinesForJobOrder(request: SuggestedPickLotForPolRequest): SuggestedPickLotResponse { | |||
| val pols = request.pickOrderLines | |||
| val itemIds = pols.mapNotNull { it.item?.id } | |||
| val zero = BigDecimal.ZERO | |||
| val one = BigDecimal.ONE | |||
| val today = LocalDate.now() | |||
| val suggestedList: MutableList<SuggestedPickLot> = mutableListOf() | |||
| val holdQtyMap: MutableMap<Long?, BigDecimal?> = request.holdQtyMap | |||
| // get current inventory lot line qty & grouped by item id | |||
| val availableInventoryLotLines = inventoryLotLineService | |||
| .allInventoryLotLinesByItemIdIn(itemIds) | |||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | |||
| .filter { (it.inQty ?: zero).minus(it.outQty ?: zero).minus(it.holdQty ?: zero) > zero } | |||
| .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today)} | |||
| .sortedBy { it.expiryDate } | |||
| .groupBy { it.item?.id } | |||
| // loop for suggest pick lot line | |||
| pols.forEach { line -> | |||
| val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | |||
| val lotLines = availableInventoryLotLines[line.item?.id].orEmpty() | |||
| val ratio = one // (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) | |||
| // FIX: Calculate remaining quantity needed (not the full required quantity) | |||
| val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(line.id!!) | |||
| val totalPickedQty = stockOutLines | |||
| .filter { | |||
| it.status == "completed" || | |||
| it.status == "partially_completed" || | |||
| (it.status == "rejected" && (it.qty ?: zero) > zero) // 包含已 picked 的 rejected | |||
| } | |||
| .sumOf { it.qty ?: zero } | |||
| val requiredQty = line.qty ?: zero | |||
| val remainingQty = requiredQty.minus(totalPickedQty) | |||
| println("=== SUGGESTION DEBUG for Pick Order Line ${line.id} ===") | |||
| println("Required qty: $requiredQty") | |||
| println("Total picked qty: $totalPickedQty") | |||
| println("Remaining qty needed: $remainingQty") | |||
| println("Stock out lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}") | |||
| // FIX: Use remainingQty instead of line.qty | |||
| var remainingQtyToAllocate = remainingQty | |||
| println("remaining1 $remainingQtyToAllocate (sales units)") | |||
| val updatedLotLines = mutableListOf<InventoryLotLineInfo>() | |||
| lotLines.forEachIndexed { index, lotLine -> | |||
| if (remainingQtyToAllocate <= zero) return@forEachIndexed | |||
| println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}") | |||
| val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } | |||
| // 修复:计算可用数量,转换为销售单位 | |||
| val availableQtyInBaseUnits = calculateRemainingQtyForInfo(lotLine) | |||
| val holdQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero | |||
| val availableQtyInSalesUnits = availableQtyInBaseUnits | |||
| .minus(holdQtyInBaseUnits) | |||
| .divide(ratio, 2, RoundingMode.HALF_UP) | |||
| println("holdQtyMap[lotLine.id] ?: zero ${holdQtyMap[lotLine.id] ?: zero}") | |||
| if (availableQtyInSalesUnits <= zero) { | |||
| updatedLotLines += lotLine | |||
| return@forEachIndexed | |||
| } | |||
| println("$index : ${lotLine.id}") | |||
| // val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } | |||
| val originalHoldQty = inventoryLotLine?.holdQty | |||
| // 修复:在销售单位中计算分配数量 | |||
| val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQtyToAllocate) | |||
| remainingQtyToAllocate = remainingQtyToAllocate.minus(assignQtyInSalesUnits) | |||
| val newHoldQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero | |||
| // 修复:将销售单位转换为基础单位来更新 holdQty | |||
| val assignQtyInBaseUnits = assignQtyInSalesUnits.multiply(ratio) | |||
| holdQtyMap[lotLine.id] = (holdQtyMap[lotLine.id] ?: zero).plus(assignQtyInBaseUnits) | |||
| suggestedList += SuggestedPickLot().apply { | |||
| type = SuggestedPickLotType.PICK_ORDER | |||
| suggestedLotLine = inventoryLotLine | |||
| pickOrderLine = line | |||
| qty = assignQtyInSalesUnits // 保存销售单位 | |||
| } | |||
| } | |||
| // 修复:计算现有 suggestions 中 pending/checked 状态满足的数量 | |||
| var existingSatisfiedQty = BigDecimal.ZERO | |||
| // 查询现有的 suggestions 用于这个 pick order line | |||
| val existingSuggestions = suggestedPickLotRepository.findAllByPickOrderLineId(line.id!!) | |||
| existingSuggestions.forEach { existingSugg -> | |||
| if (existingSugg.suggestedLotLine?.id != null) { | |||
| val stockOutLines = stockOutLIneRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||
| line.id!!, existingSugg.suggestedLotLine?.id!! | |||
| ) | |||
| val canCountAsSatisfied = stockOutLines.isEmpty() || stockOutLines.any { | |||
| it.status == "pending" || it.status == "checked" || it.status == "partially_completed" | |||
| } | |||
| if (canCountAsSatisfied) { | |||
| existingSatisfiedQty = existingSatisfiedQty.plus(existingSugg.qty ?: BigDecimal.ZERO) | |||
| } | |||
| } | |||
| } | |||
| // 调整 remainingQtyToAllocate,减去已经通过现有 suggestions 满足的数量 | |||
| remainingQtyToAllocate = remainingQtyToAllocate.minus(existingSatisfiedQty) | |||
| println("Existing satisfied qty: $existingSatisfiedQty") | |||
| println("Adjusted remaining qty: $remainingQtyToAllocate") | |||
| // if still have remainingQty | |||
| println("remaining2 $remainingQtyToAllocate (sales units)") | |||
| // if still have remainingQty | |||
| println("remaining2 $remainingQtyToAllocate (sales units)") | |||
| if (remainingQtyToAllocate > zero) { | |||
| suggestedList += SuggestedPickLot().apply { | |||
| type = SuggestedPickLotType.PICK_ORDER | |||
| suggestedLotLine = null | |||
| pickOrderLine = line | |||
| qty = remainingQtyToAllocate // 保存销售单位 | |||
| } | |||
| try { | |||
| /* | |||
| val pickOrder = line.pickOrder | |||
| if (pickOrder != null) { | |||
| createInsufficientStockIssue( | |||
| pickOrder = pickOrder, | |||
| pickOrderLine = line, | |||
| insufficientQty = remainingQtyToAllocate | |||
| ) | |||
| } | |||
| */ | |||
| } catch (e: Exception) { | |||
| println("❌ Error creating insufficient stock issue: ${e.message}") | |||
| e.printStackTrace() | |||
| } | |||
| } | |||
| } | |||
| return SuggestedPickLotResponse(holdQtyMap = holdQtyMap, suggestedList = suggestedList) | |||
| } | |||
| // Convertion | |||
| open fun convertRequestsToEntities(request: List<SaveSuggestedPickLotRequest>): List<SuggestedPickLot> { | |||
| @@ -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`; | |||