使用專案排程 API 以對排程實體執行作業

適用於: 專案營運與 ERP、專案營運核心整合。

排程實體

專案排程 API 提供建立、更新及刪除 排程實體的能力。 Project for the Web 中的排程引擎負責管理這些實體。 早期的 Dynamics 365 專案操作版本限制了 排程實體的建立、更新與刪除操作。

下表提供專案排程實體的完整清單。

實體名稱 實體邏輯名稱
專案 msdyn_project
專案任務 msdyn_projecttask
專案工作相依性 msdyn_projecttaskdependency
資源指派 msdyn_resourceassignment
專案貯體 msdyn_projectbucket
項目團隊成員 msdyn_projectteam
專案檢查清單 msdyn_projectchecklist
專案標籤 msdyn_projectlabel
專案工作與標籤 msdyn_projecttasktolabel
專案短期衝刺 msdyn_projectsprint

OperationSet

當你需要處理交易中多個影響排程的請求時,使用 OperationSet 作為工作單元。

專案排程 API

以下列表顯示目前的專案排程 API。

API 描述
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 更新資源分配計畫的工作輪廓。

使用 OperationSet 的專案排程 API

因為 CreateProjectV1 和 CreateTeamMemberV1 都會立即建立紀錄,所以你無法直接在 OperationSet 中使用這些 API。 不過,你可以利用這些 API 建立所需的紀錄,建立 OperationSet,然後在 OperationSet 中使用預先建立的紀錄。

支援的作業

排程實體 Create Update Delete 重要考慮
項目任務 是的 是的 是的 你可以在 Project for the Web 中編輯 EffortCompleted 和 EffortRemaining 欄位,但在 Project Operations 中無法編輯這些欄位。
專案工作相依性 是的 No 是的 不會更新專案工作相依性記錄。 你可以刪除舊紀錄並建立新紀錄。
資源指派 是的 是* 是的 不支援對下列欄位的作業:BookableResourceID、投入量、EffortCompleted、EffortRemaining 和 PlannedWork。
專案貯體 是的 是的 是的 預設貯體是使用 CreateProjectV1 API 所建立。 更新版本 16 中已新增對建立和刪除專案貯體的支援。
專案團隊成員 是的 是的 是的 如果是建立作業,請使用 CreateTeamMemberV1 API。
專案 是的 是的 No 不支援對下列欄位的作業:StateCode、BulkGenerationStatus、GlobalRevisionToken、CalendarID、投入量、EffortCompleted、EffortRemaining、進度、完成、TaskEarliestStart 和期間。
專案檢查清單 是的 是的 是的
專案標籤 No 是的 No 你可以更改唱片公司名稱。 此功能僅適用於 Project for the Web。 標籤是在你第一次開啟專案時建立的。
專案工作與標籤 是的 No 是的 此功能僅適用於 Project for the Web。
專案短期衝刺 是的 是的 是的 起始欄位的日期必須早於結束欄位。 同一個專案的短期衝刺不能相互重疊。 此功能僅適用於 Project for the Web。
專案目標 是的 是的 是的 不支援對下列欄位的作業:DescriptionPlainText、TaskDisplayOrder
專案工作與目標 是的 No 是的 不支援對下列欄位的作業:TaskDisplayOrder

* 不會更新資源指派記錄。 你可以刪除舊紀錄並建立新紀錄。 提供單獨的 API 來更新資源分配輪廓。

識別碼屬性可選用。 如果你提供 ID 屬性,系統會嘗試使用它,若無法使用就會拋出例外。 如果你沒有提供 ID 屬性,系統會自動產生它。

限制與已知問題

以下列表顯示限制與已知問題:

  • 只有 持有 Microsoft Project 授權的使用者 才能使用 Project Schedule API。 以下使用者無法使用這些 API:

    • 應用程式使用者
    • 系統使用者
    • 整合使用者
    • 其他沒有必要授權的使用者
  • 每個 操作集 最多只能有 200 個操作。

  • 每位使用者最多只能有 10 個開啟的 操作集。

  • 每個更新資源指派輪廓操作都算作一個操作。

  • 每個更新的等高線清單最多可包含 100 個時間切片。

  • OperationSet 失敗狀態和失敗記錄目前未提供。

  • 每個專案最多可包含 400 個短衝。

  • 專案和工作的限制與界限。

錯誤處理

  • 若要檢閱作業集所產生的錯誤,請移至設定>排程整合>作業集。
  • 若要檢閱專案排程服務所產生的錯誤,請移至設定>排程整合>PSS 錯誤記錄。

編輯資源指派分佈

與其他所有能更新實體的專案排程 API 不同,資源指派輪廓 API 僅更新單一欄位 msdyn_plannedwork,即單一實體 msydn_resourceassignment。

指定的排程模式為:

  • 固定單位
  • 專案行事曆為週一、週二、週四及週五太平洋時間上午9:00至下午5:00。 (星期三沒事。)
  • 資源行事曆是星期一至星期五上午 9:00 到下午 1:00 (太平洋時間)。

此分配為期一周,每天四小時,因為資源行事曆是從上午 9:00 到下午 1:00 (太平洋時間),即每天四個小時。

  任務 開始日期 結束日期 數量 6/13/2022 6/14/2022 2022 年 6 月 15 日 6/16/2022 6/17/2022
9-1 工作人員 T1 6/13/2022 6/17/2022 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 執行後進行的。

  任務 開始日期 結束日期 數量 6/13/2022 6/14/2022 2022 年 6 月 15 日 6/16/2022 6/17/2022
9-1 工作人員 T1 6/13/2022 6/17/2022 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