CRM 2013:如何安排并发约会(使用约会和 RecurringAppointmentMaster 实体)?

CRM 2013 : How can I Schedule Concurrent Appointments (using Appointment & RecurringAppointmentMaster entities)?

我们有一个插件使用 BookRequest & RescheduleRequest 方法来安排 Appointment & RecurringAppointmentMaster 实体。 最近我的任务是实现在给定时间段内安排多个应用程序的能力。 因此,在对此进行研究时,我发现一些帖子提到了资源容量(以工作时间为单位)以及在约会中将 ActivityParty 的 Effort 字段设置为 1.0。 我想,太好了,这会很容易。

所以我更改了插件来存储努力:

            activityParty = new Entity("activityparty");
            activityParty["partyid"] = new EntityReference("systemuser", apptCaregiverId);
            activityParty["effort"] = (Double)1.0;

但是当我 运行 代码时,BookRequest 返回了这个让我困惑的错误:ErrorCode.DifferentEffort

我搜索了 ErrorCode.DifferentEffort, 2139095040, BookRequest,没找到有用的东西。

这个错误到底是什么意思? 为什么在 CRM 中安排并发约会如此困难?

这是我辛苦学到的东西,所以也许它会让其他人免于受挫。

我查看了数据库并注意到与约会相关的活动方实体都将工作量字段设置为 2139095040,我想知道该数字是从哪里来的?在与 CRM 无关的 post 中,我发现 2139095040 表示 'positive infinity'。

我重新访问了 posts,他们在那里谈论将工作量设置为 1.0 并意识到他们都指的是 ServiceAppointment 实体(而不是 Appointment),然后我终于偶然发现错误代码列表 Scheduling Error Codes

DifferentEffort = The required capacity of this service does not match the capacity of resource {resource name}.

不完全是事实,但无论如何。 真正的问题是约会实体不引用服务,因此 non-existent 服务的容量是 ‘Positive Infinity’ (2139095040)。 将 Effort 设置为等于 2139095040 以外的任何值都会引发此错误。 这里并不是真的检查资源的容量,只是说non-existent服务容量必须=2139095040

无论如何,为了解决这个问题,我删除了设置 effort = 1.0 的逻辑,当 BookRequestRescheduleRequest returns ErrorCode.ResourceBusy 时,我检查了 Capacity 与# appts 安排在那个时间段 & 如果有剩余容量,我会使用创建或更新强制它超额预订。

    private Guid BookAppointment(Entity appointment, bool setState, out List<string> errors)
    {
        Guid apptId = Guid.Empty;

        try
        {
            BookRequest request = new BookRequest
            {
                Target = appointment
            };
            BookResponse booked = (BookResponse)this.orgService.Execute(request);
            apptId = ParseValidationResult(booked.ValidationResult, setState, appointment, true, out errors);
        }
        catch (Exception ex)
        {
            errors = new List<string> { ex.GetBaseException().Message };
        }

        return apptId;
    }

    private Guid RescheduleAppointment(Entity appointment, out List<string> errors)
    {   // used to reschedule non-recurring appt or appt in recurrence
        Guid apptId = Guid.Empty;
        try
        {
            RescheduleRequest request = new RescheduleRequest
            {
                Target = appointment
            };
            RescheduleResponse rescheduled = (RescheduleResponse)this.orgService.Execute(request);
            apptId = ParseValidationResult(rescheduled.ValidationResult, false, appointment, false, out errors);
        }
        catch (Exception ex)
        {
            errors = new List<string> { ex.GetBaseException().Message };
        }

        return apptId;
    }

    private Guid ParseValidationResult(ValidationResult result, bool setState, Entity appointment, Boolean addNew, out List<string> errors)
    {
        Guid apptId = result.ActivityId;
        errors = new List<string>();
        if (result.ValidationSuccess == true)
        {
            if (setState == true)
            {
                SetStateRequest state = new SetStateRequest();
                state.State = new OptionSetValue(3);   // Scheduled
                state.Status = new OptionSetValue(5);  // Busy
                state.EntityMoniker = new EntityReference("appointment", apptId);
                SetStateResponse stateSet = (SetStateResponse)this.orgService.Execute(state);
            }
        }
        else
        {
            String error;
            String errortxt;
            Boolean overbookAppt = true;
            foreach (var errorInfo in result.TraceInfo.ErrorInfoList)
            {
                bool unavailable = false;
                if (errorInfo.ErrorCode == "ErrorCode.ResourceNonBusinessHours")
                {
                    errortxt = "{0} is being scheduled outside work hours";
                }
                else if (errorInfo.ErrorCode == "ErrorCode.ResourceBusy")
                {
                    errortxt = "{0} is unavailable at this time";
                    unavailable = true;
                }
                else
                {
                    errortxt = "failed to schedule {0}, error code = " + errorInfo.ErrorCode;
                }
                Dictionary<Guid, String> providers;
                Dictionary<Guid, String> resources;
                DateTime start = DateTime.Now, end = DateTime.Now;
                Guid[] resourceIds = errorInfo.ResourceList.Where(r => r.EntityName == "equipment").Select(r => r.Id).ToList().ToArray();
                if (unavailable == true)
                {
                    if (appointment.LogicalName == "recurringappointmentmaster")
                    {
                        start = (DateTime)appointment["starttime"];
                        end = (DateTime)appointment["endtime"];
                    }
                    else
                    {
                        start = (DateTime)appointment["scheduledstart"];
                        end = (DateTime)appointment["scheduledend"];
                    }
                    Dictionary<Guid, Boolean> availability = GetAvailabilityOfResources(resourceIds, start, end);
                    resourceIds = availability.Where(a => a.Value == false).Select(t => t.Key).ToArray();   // get ids of all unavailable resources
                    if (resourceIds.Count() == 0)
                    {   // all resources still have capacity left at this timeslot - overbook appt timeslot
                        overbookAppt = true;
                    }   // otherwise at least some resources are booked up in this timeslot - return error
                }
                if (errortxt.Contains("{0}"))
                {   // include resource name in error msg
                    if (resourceIds.Count() > 0)
                    {
                        LoadProviderAndResourceInfo(resourceIds, out providers, out resources);
                        foreach (var resource in providers)
                        {
                            error = String.Format(errortxt, resource.Value);
                            errors.Add(error);
                        }
                        foreach (var resource in resources)
                        {
                            error = String.Format(errortxt, resource.Value);
                            errors.Add(error);
                        }
                    }
                }
                else
                {   // no place for name in msg - just store it
                    errors.Add(errortxt);
                    break;
                }
            }
            if (overbookAppt == true && errors.Count() == 0)
            {   // all resources still have capacity left at this timeslot & no other errors have been returned - create appt anyway
                if (addNew)
                {
                    appointment.Attributes.Remove("owner"); // Create message does not like when owner field is specified
                    apptId = this.orgService.Create(appointment);
                    if (setState == true)
                    {
                        SetStateRequest state = new SetStateRequest();
                        state.State = new OptionSetValue(3);   // Scheduled
                        state.Status = new OptionSetValue(5);  // Busy
                        state.EntityMoniker = new EntityReference("appointment", apptId);
                        SetStateResponse stateSet = (SetStateResponse)this.orgService.Execute(state);
                    }
                }
                else
                {
                    this.orgService.Update(appointment);
                }
            }
        }

        return apptId;
    }

    private Dictionary<Guid, Boolean> GetAvailabilityOfResources(Guid[] resourceIds, DateTime start, DateTime end)
    {
        Dictionary<Guid, Boolean> availability = new Dictionary<Guid, Boolean>();
        QueryMultipleSchedulesRequest scheduleRequest = new QueryMultipleSchedulesRequest();
        scheduleRequest.ResourceIds = resourceIds;
        scheduleRequest.Start = start;
        scheduleRequest.End = end;
        // TimeCode.Unavailable - gets appointments
        // TimeCode.Filter - gets resource capacity
        scheduleRequest.TimeCodes = new TimeCode[] { TimeCode.Unavailable, TimeCode.Filter };

        QueryMultipleSchedulesResponse scheduleResponse = (QueryMultipleSchedulesResponse)this.orgService.Execute(scheduleRequest);
        int index = 0;
        TimeInfo[][] timeInfo = new TimeInfo[scheduleResponse.TimeInfos.Count()][];
        foreach (var schedule in scheduleResponse.TimeInfos)
        {
            TimeInfo resourceCapacity = schedule.Where(s => s.SubCode == SubCode.ResourceCapacity).FirstOrDefault();
            Int32 capacity = (resourceCapacity != null) ? (Int32)resourceCapacity.Effort : 1;
            Int32 numAppts = schedule.Where(s => s.SubCode == SubCode.Appointment).Count();
            // resource is available if capacity is more than # appts in timeslot
            availability.Add(resourceIds[index++], (capacity > numAppts) ? true : false);
        }

        return availability;
    }

这确实是比设置 Effort = 1 更好的解决方案,因为现在我们不需要 one-time on-demand 工作流来更新现有数据。

如果您需要这样做,我希望这可以帮助您节省一些时间。