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

適用於:資源/非庫存型案例適用的 Project Operations、精簡部署 - 交易至開立預估發票。

排程實體

專案排程 API 提供對排程實體執行建立、更新和刪除作業的功能。 這些實體是透過 Project for the Web 中的排程引擎進行管理。 對排程實體的建立、更新及刪除作業在先前的 Dynamics 365 Project Operations 版本中受到限制。

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

實體名稱 實體邏輯名稱
Project 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

因為 CreateProjectV1CreateTeamMemberV1 都會立即建立記錄,無法直接將這些 API 用於 OperationSet。 不過,可以用於建立所需的記錄、建立 OperationSet,然後在 OperationSet 中使用這些預先建立的記錄。

支援的作業

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

* 不會更新資源指派記錄。 相反地,可以刪除舊記錄,也可以建立新記錄。 提供單獨的 API 來更新資源指派分佈。

識別碼屬性可選用。 如果已提供,則系統會嘗試使用,並在無法使用時擲回例外狀況。 如果未提供,則系統會產生此屬性。

限制和已知問題

以下是限制和已知問題的清單:

  • 只有擁有 Microsoft Project 授權的使用者才能使用專案排程 API。 無法供下列使用者使用:

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

  • 每個使用者最多只能有 10 個已開啟的 OperationSet

  • Project Operations 目前在專案中最多支援總計 500 個工作。

  • 每個更新資源指派分佈作業都計為單一作業。

  • 每個更新的分佈清單最多可以包含 100 個時間配量。

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

  • 每個專案最多有 400 個短期衝刺。

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

  • 標籤目前僅適用於 Project for the Web。

  • 初次開啟專案時,會建立標籤。

  • 每個專案最多有 10 個目標。

  • 每個工作都只能在 [專案工作與目標] 中出現一次。

錯誤處理

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

編輯資源指派分佈

與更新實體的所有其他專案排程 API 不同,資源指派分佈 API 只負責更新單一實體 msydn_resourceassignment 上的單一欄位 msdyn_plannedwork。

指定的排程模式為:

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

這項指派時間為一週,一天四小時。 這是因為資源行事曆是從上午 9:00 到下午 1:00 (太平洋時間),即每天四個小時。

  工作​​ 開始日期 結束日期 數量 6/13/2022 6/14/2022 6/15/2022 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 6/15/2022 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