使用项目计划 API 对计划实体执行操作

适用于:与 ERP、Project Operations Core 集成的 Project Operations。

计划实体

项目计划 API 提供对计划实体执行创建、更新和删除操作的能力。 这些实体通过 Web 项目中的计划引擎进行管理。 早期 Dynamics 365 Project Operations 版本中限制对计划实体执行创建、更新和删除操作。

下表提供了项目计划实体的完整列表。

实体名称 实体逻辑名称
项目 msdyn_project
项目任务 msdyn_projecttask
项目任务依赖关系 msdyn_projecttaskdependency
资源分派 msdyn_资源分配
项目 Bucket msdyn_projectbucket
项目团队成员 msdyn_projectteam
项目清单 msdyn_projectchecklist
项目标签 msdyn_projectlabel
要标记的项目任务 msdyn_projecttasktolabel
项目冲刺(sprint) msdyn_projectsprint

OperationSet

OperationSet 是一种工作单位模式,当必须在交易内处理多个计划影响请求时,可以使用该模式。

项目计划 API

下面是当前项目计划 API 的列表。

API Description
msdyn_CreateProjectV1 此 API 用于创建项目。 项目和默认项目桶会立即创建。 项目创建也可以通过使用标准 Dataverse API 向项目表添加行来完成。 此过程不会为项目创建默认桶,但可能具有更好的性能。
msdyn_CreateTeamMemberV1 此 API 用于创建项目团队成员。 系统会立即创建团队成员记录。 团队成员创建也可以通过使用标准 Dataverse API 向项目团队成员表添加行来完成。
msdyn_CreateOperationSetV1 此 API 用于计划必须在交易中执行的多个请求。
msdyn_PssCreateV1 此 API 用于创建实体。 该实体可以是支持创建操作的任何项目计划实体。
msdyn_PssCreateV2 此 API 用于创建实体。 它的工作方式类似于 msdyn_PssCreateV1,但可以在一个操作中创建多个实体。
msdyn_PssUpdateV1 此 API 用于更新实体。 该实体可以是支持更新操作的任何项目计划实体。
msdyn_PssUpdateV2 此 API 用于更新实体。 它的工作方式类似于 msdyn_PssUpdateV1,但可以在一个操作中更新多个实体。
msdyn_PssDeleteV1 此 API 用于删除实体。 该实体可以是支持删除操作的任何项目计划实体。
msdyn_PssDeleteV2 此 API 用于删除实体。 它的工作方式类似于 msdyn_PssDeleteV1,但可以在一个操作中删除多个实体。
msdyn_ExecuteOperationSetV1 此 API 用于执行给定操作集内的所有操作。
msdyn_PssUpdateResourceAssignmentV1 此 API 用于更新资源分配计划的工作分布。

将项目计划 API 与 OperationSet 一起使用

由于会同时为 CreateProjectV1CreateTeamMemberV1 立即创建记录,这些 API 不能直接在 OperationSet 中使用。 但是,可以使用它们创建所需记录,创建 OperationSet,然后在 OperationSet 中使用预先创建的记录。

支持的操作

计划实体 创建​​ 更新 Delete 重要考虑因素
项目任务 可以 可以 可以 EffortCompletedEffortRemaining 字段可以在 Project for the Web 中进行编辑,但不能在 Project Operations 中进行编辑。
项目任务依赖关系 不会更新项目任务依赖关系记录。 而是可以删除旧记录,并可以创建新记录。
资源分派 可以 是* 可以 不支持对以下字段执行操作:BookableResourceIDEffortEffortCompletedEffortRemainingPlannedWork
项目 Bucket 可以 可以 可以 默认桶使用 CreateProjectV1 API 创建。 更新版 16 中添加了对创建和删除项目桶的支持。
项目团队成员 可以 可以 可以 对于创建操作,请使用 CreateTeamMemberV1 API。
项目 可以 可以 不支持以下对字段执行操作:StateCodeBulkGenerationStatusGlobalRevisionTokenCalendarIDEffortEffortCompletedEffortRemainingProgressFinishTaskEarliestStartDuration
项目清单 可以 可以 可以
项目标签 可以 标签名称可以更改。 此功能只能用于 Project for the Web。 标签将在您首次打开项目时创建。
要标记的项目任务 可以 可以 此功能只能用于 Project for the Web。
项目冲刺(sprint) 可以 可以 可以 开始字段的日期必须早于完成字段。 同一项目的冲刺不能相互重叠。 此功能只能用于 Project for the Web。
项目目标 可以 可以 不支持具有以下字段的操作:DescriptionPlainText、TaskDisplayOrder
项目任务与目标关系 可以 可以 不支持具有以下字段的操作:TaskDisplayOrder

* 资源分配记录不会更新。 而是可以删除旧记录,并可以创建新记录。 会提供单独的 API 来更新资源分配信息。

ID 属性是可选的。 如果提供了 ID 属性,则系统会尝试使用它,如果无法使用它,则会引发异常。 如果未提供此属性,系统将生成它。

限制和已知问题

下面的列表显示限制和已知问题:

  • 项目计划 API 仅供具有 Microsoft Project 许可证的用户使用。 以下用户不能使用它们:

    • 应用程序用户
    • 系统用户
    • 集成用户
    • 没有所需许可证的其他用户
  • 每个 OperationSet 最多只能有 200 项操作。

  • 每个用户最多只能有 10 个公开的 OperationSets

  • 每个更新资源分配分布操作计为一次操作。

  • 每个更新分布的列表最多可以包含 100 个时间片段。

  • OperationSet 故障状态和故障日志当前不可用。

  • 每个项目最多有 400 个冲刺。

  • 项目和任务的限制和边界

错误处理

  • 若要查看操作集生成的错误,请转到设置>计划集成>操作集
  • 若要查看项目计划服务生成的错误,请转到设置>计划集成>PSS 错误日志

编辑资源分配分布

与更新实体的所有其他项目计划 API 不同,资源分配分布 API 仅负责单个实体 msydn_resourceassignment 上的单个字段 msdyn_plannedwork 的更新。

给定计划模式为:

  • 固定单位
  • 项目日历为星期一、星期二、星期四和星期五下午 9:00 到 5:00(太平洋时间)。 (星期三没有工作。
  • 资源日历为星期一到星期五上午 9:00 到下午 1:00(太平洋时间)。

此分配为期一周,每天四小时,因为资源日历从上午 9:00 到下午 1:00(太平洋时间),即每天四小时。

  Task 开始日期 结束日期 数量 2022 年 6 月 13 日 2022 年 6 月 14 日 2022 年 6 月 15 日 2022 年 6 月 16 日 2022 年 6 月 17 日
9-1 工作人员 T1 2022 年 6 月 13 日 2022 年 6 月 17 日 20 4 4 4 4 4

例如,如果您希望工作人员本周每天只工作三个小时,为其他任务留出一小时。

UpdatedContours 示例有效负载

[{

"minutes":900.0,

"start":"2022-06-13T00:00:00-07:00",

"end":"2022-06-18T00:00:00-07:00"

}]

这是更新分布计划 API 运行后的分配。

  Task 开始日期 结束日期 数量 2022 年 6 月 13 日 2022 年 6 月 14 日 2022 年 6 月 15 日 2022 年 6 月 16 日 2022 年 6 月 17 日
9-1 工作人员 T1 2022 年 6 月 13 日 2022 年 6 月 17 日 15 3 3 3 3 3

示例应用场景

在此方案中,您将创建一个项目、一个团队成员、四个任务和两个资源分派。 接下来,您将更新一个任务、更新项目、更新资源分配信息、删除一个任务、删除一个资源分派以及创建任务依赖项。

Entity project = CreateProject();
project.Id = CallCreateProjectAction(project);
var projectReference = project.ToEntityReference();

var teamMember = new Entity("msdyn_projectteam", Guid.NewGuid());
teamMember["msdyn_name"] = $"TM {DateTime.Now.ToShortTimeString()}";
teamMember["msdyn_project"] = projectReference;
var createTeamMemberResponse = CallCreateTeamMemberAction(teamMember);

var description = $"My demo {DateTime.Now.ToShortTimeString()}";
var operationSetId = CallCreateOperationSetAction(project.Id, description);

var task1 = GetTask("1WW", projectReference);
var task2 = GetTask("2XX", projectReference, task1.ToEntityReference());
var task3 = GetTask("3YY", projectReference);
var task4 = GetTask("4ZZ", projectReference);

var assignment1 = GetResourceAssignment("R1", teamMember, task2, project);
var assignment2 = GetResourceAssignment("R2", teamMember, task3, project);

var task1Response = CallPssCreateAction(task1, operationSetId);
var task2Response = CallPssCreateAction(task2, operationSetId);
var task3Response = CallPssCreateAction(task3, operationSetId);
var task4Response = CallPssCreateAction(task4, operationSetId);

var assignment1Response = CallPssCreateAction(assignment1, operationSetId);
var assignment2Response = CallPssCreateAction(assignment2, operationSetId);

task2["msdyn_subject"] = "Updated Task";
var task2UpdateResponse = CallPssUpdateAction(task2, operationSetId);

project["msdyn_subject"] = $"Proj update {DateTime.Now.ToShortTimeString()}";
var projectUpdateResponse = CallPssUpdateAction(project, operationSetId);

List<UpdatedContour> updatedContours = new List<UpdatedContour>(); 
UpdatedContour updatedContour = new UpdatedContour(); 
updatedContour.Start = DateTime.UtcNow.Date; 
updatedContour.End = DateTime.UtcNow.Date.AddDays(1); 
updatedContour.Minutes = 120; 
updatedContours.Add(updatedContour); 

String serializedUpdate = JsonConvert.SerializeObject(updatedContours); 
var updateContoursResponse = CallPssUpdateContourAction(assignment1.Id, serializedUpdate, operationSetId); 

var task4DeleteResponse = CallPssDeleteAction(task4.Id.ToString(), task4.LogicalName, operationSetId);

var assignment2DeleteResponse = CallPssDeleteAction(assignment2.Id.ToString(), assignment2.LogicalName, operationSetId);

var dependency1 = GetTaskDependency(project, task2, task3);
var dependency1Response = CallPssCreateAction(dependency1, operationSetId);

CallExecuteOperationSetAction(operationSetId);
Console.WriteLine("Done....");

其他示例

#region Call actions --- Sample code ----

/// <summary>
/// Calls the action to create an operationSet
/// </summary>
/// <param name="projectId">project id for the operations to be included in this operationSet</param>
/// <param name="description">description of this operationSet</param>
/// <returns>operationSet id</returns>
private string CallCreateOperationSetAction(Guid projectId, string description)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_CreateOperationSetV1");
    operationSetRequest["ProjectId"] = projectId.ToString();
    operationSetRequest["Description"] = description;
    OrganizationResponse response = organizationService.Execute(operationSetRequest);
    return response["OperationSetId"].ToString();
}

/// <summary>
/// Calls the action to create an entity
/// </summary>
/// <param name="entity">Scheduling entity</param>
/// <param name="operationSetId">operationSet id</param>
/// <returns>OperationSetResponse</returns>

private OperationSetResponse CallPssCreateAction(Entity entity, string operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_PssCreateV1");
    operationSetRequest["Entity"] = entity;
    operationSetRequest["OperationSetId"] = operationSetId;
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}

/// <summary>
/// Calls the action to update an entity
/// </summary>
/// <param name="entity">Scheduling entity</param>
/// <param name="operationSetId">operationSet Id</param>
/// <returns>OperationSetResponse</returns>
private OperationSetResponse CallPssUpdateAction(Entity entity, string operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_PssUpdateV1");
    operationSetRequest["Entity"] = entity;
    operationSetRequest["OperationSetId"] = operationSetId;
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}

/// <summary>
/// Calls the action to update an entity
/// </summary>
/// <param name="recordId">Id of the record to be deleted</param>
/// <param name="entityLogicalName">Entity logical name of the record</param>
/// <param name="operationSetId">OperationSet Id</param>
/// <returns>OperationSetResponse</returns>
private OperationSetResponse CallPssDeleteAction(string recordId, string entityLogicalName, string operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_PssDeleteV1");
    operationSetRequest["RecordId"] = recordId;
    operationSetRequest["EntityLogicalName"] = entityLogicalName;
    operationSetRequest["OperationSetId"] = operationSetId;
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}

/// <summary> 
/// Calls the action to update a Resource Assignment contour
/// </summary> 
/// <param name="resourceAssignmentId">Id of the resource assignment to be updated</param> 
/// <param name="serializedUpdates">JSON formatted contour updates</param>
/// <param name="operationSetId">operationSet id</param> 
/// <returns>OperationSetResponse</returns> 
private OperationSetResponse CallPssUpdateContourAction(string resourceAssignmentId, string serializedUpdates string operationSetId) 
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_PssUpdateResourceAssignmentContourV1"); 
    operationSetRequest["ResourceAssignmentId"] = resourceAssignmentId; 
    operationSetRequest["UpdatedContours"] = serializedUpdates; 
    operationSetRequest["OperationSetId"] = operationSetId; 
    return GetOperationSetResponseFromOrgResponse(OrganizationService.Execute(operationSetRequest)); 
} 

/// <summary>
/// Calls the action to execute requests in an operationSet
/// </summary>
/// <param name="operationSetId">operationSet id</param>
/// <returns>OperationSetResponse</returns>
private OperationSetResponse CallExecuteOperationSetAction(string operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_ExecuteOperationSetV1");
    operationSetRequest["OperationSetId"] = operationSetId;
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}

/// <summary>
/// This can be used to abandon an operationSet that is no longer needed
/// </summary>
/// <param name="operationSetId">operationSet id</param>
/// <returns>OperationSetResponse</returns>
protected OperationSetResponse CallAbandonOperationSetAction(Guid operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_AbandonOperationSetV1");
    operationSetRequest["OperationSetId"] = operationSetId.ToString();
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}


/// <summary>
/// Calls the action to create a new project
/// </summary>
/// <param name="project">Project</param>
/// <returns>project Id</returns>
private Guid CallCreateProjectAction(Entity project)
{
    OrganizationRequest createProjectRequest = new OrganizationRequest("msdyn_CreateProjectV1");
    createProjectRequest["Project"] = project;
    OrganizationResponse response = organizationService.Execute(createProjectRequest);
    var projectId = Guid.Parse((string)response["ProjectId"]);
    return projectId;
}

/// <summary>
/// Calls the action to create a new project team member
/// </summary>
/// <param name="teamMember">Project team member</param>
/// <returns>project team member Id</returns>
private string CallCreateTeamMemberAction(Entity teamMember)
{
    OrganizationRequest request = new OrganizationRequest("msdyn_CreateTeamMemberV1");
    request["TeamMember"] = teamMember;
    OrganizationResponse response = organizationService.Execute(request);
    return (string)response["TeamMemberId"];
}

private OperationSetResponse GetOperationSetResponseFromOrgResponse(OrganizationResponse orgResponse)
{
    return JsonConvert.DeserializeObject<OperationSetResponse>((string)orgResponse.Results["OperationSetResponse"]);
}

private EntityCollection GetDefaultBucket(EntityReference projectReference)
{
    var columnsToFetch = new ColumnSet("msdyn_project", "msdyn_name");
    var getDefaultBucket = new QueryExpression("msdyn_projectbucket")
    {
        ColumnSet = columnsToFetch,
        Criteria =
        {
            Conditions =
            {
                new ConditionExpression("msdyn_project", ConditionOperator.Equal, projectReference.Id),
                new ConditionExpression("msdyn_name", ConditionOperator.Equal, "Bucket 1")
            }
        }
    };

    return organizationService.RetrieveMultiple(getDefaultBucket);
}

private Entity GetBucket(EntityReference projectReference)
{
    var bucketCollection = GetDefaultBucket(projectReference);
    if (bucketCollection.Entities.Count > 0)
    {
        return bucketCollection[0].ToEntity<Entity>();
    }

    throw new Exception($"Please open project with id {projectReference.Id} in the Dynamics UI and navigate to the Tasks tab");
}

private Entity CreateProject()
{
    var project = new Entity("msdyn_project", Guid.NewGuid());
    project["msdyn_subject"] = $"Proj {DateTime.Now.ToShortTimeString()}";

    return project;
}



private Entity GetTask(string name, EntityReference projectReference, EntityReference parentReference = null)
{
    var task = new Entity("msdyn_projecttask", Guid.NewGuid());
    task["msdyn_project"] = projectReference;
    task["msdyn_subject"] = name;
    task["msdyn_effort"] = 4d;
    task["msdyn_scheduledstart"] = DateTime.Today;
    task["msdyn_scheduledend"] = DateTime.Today.AddDays(5);
    task["msdyn_start"] = DateTime.Now.AddDays(1);
    task["msdyn_projectbucket"] = GetBucket(projectReference).ToEntityReference();
    task["msdyn_LinkStatus"] = new OptionSetValue(192350000);

    //Custom field handling
    /*
    task["new_custom1"] = "Just my test";
    task["new_age"] = 98;
    task["new_amount"] = 591.34m;
    task["new_isready"] = new OptionSetValue(100000000);
    */

    if (parentReference == null)
    {
        task["msdyn_outlinelevel"] = 1;
    }
    else
    {
        task["msdyn_parenttask"] = parentReference;
    }

    return task;
}

private Entity GetResourceAssignment(string name, Entity teamMember, Entity task, Entity project)
{
    var assignment = new Entity("msdyn_resourceassignment", Guid.NewGuid());
    assignment["msdyn_projectteamid"] = teamMember.ToEntityReference();
    assignment["msdyn_taskid"] = task.ToEntityReference();
    assignment["msdyn_projectid"] = project.ToEntityReference();
    assignment["msdyn_name"] = name;
   
    return assignment;
}

protected Entity GetTaskDependency(Entity project, Entity predecessor, Entity successor)
{
    var taskDependency = new Entity("msdyn_projecttaskdependency", Guid.NewGuid());
    taskDependency["msdyn_project"] = project.ToEntityReference();
    taskDependency["msdyn_predecessortask"] = predecessor.ToEntityReference();
    taskDependency["msdyn_successortask"] = successor.ToEntityReference();
    taskDependency["msdyn_linktype"] = new OptionSetValue(192350000);

    return taskDependency;
}

#endregion


#region OperationSetResponse DataContract --- Sample code ----

[DataContract]
public class OperationSetResponse
{
[DataMember(Name = "operationSetId")]
public Guid OperationSetId { get; set; }

[DataMember(Name = "operationSetDetailId")]
public Guid OperationSetDetailId { get; set; }

[DataMember(Name = "operationType")]
public string OperationType { get; set; }

[DataMember(Name = "recordId")]
public string RecordId { get; set; }

[DataMember(Name = "correlationId")]
public string CorrelationId { get; set; }
}

#endregion

#region UpdatedContour DataContract --- Sample code ---- 

[DataContract] 
public class UpdatedContour 
{ 
[DataMember(Name = "start")] 
public DateTime Start { get; set; } 

[DataMember(Name = "end")] 
public DateTime End { get; set; } 

[DataMember(Name = "minutes")] 
public decimal Minutes { get; set; } 
} 

#endregion