分步指南:在 Dynamics 365 F&O 中构建自定义工作流(Step-by-Step Guide: Building a Custom Workflow in Dynamics 365 F&O)

在 Microsoft Dynamics 365 for Finance and Operations (D365 F&O) 中,工作流是定义业务流程自动化、文档审批和流转路径的核心机制。

本指南将详细演示如何为一个名为 Demo_WFDocument 的自定义文档表创建一个完整的工作流。

重要提示: 本教程假定你在与文档表相同的模型中创建工作流。如果跨模型操作,请注意,截至本文撰写时,某些步骤(特别是覆写 canSubmitToWorkflow() 方法)可能需要进行覆盖 (Overlaying) 操作。

让我们开始吧!

第 1 步:定义工作流状态

首先,我们需要一个基础枚举来定义文档在工作流中的各个状态。

  • 创建一个新的基础枚举,例如 Demo_WFDocumentStatus
  • 为其添加状态值,例如:Draft (草稿), Submitted (已提交), ChangeRequest (请求更改), Rejected (已拒绝), Approved (已批准)。

接下来,在你的文档表 (Demo_WFDocument) 上:

  • 添加一个新字段,使用刚刚创建的 Demo_WFDocumentStatus 枚举作为类型。
  • 将此字段设置为只读 (Read-Only),因为它的值将由工作流系统自动更新。
  • (可选) 在文档对应的窗体 (Form) 上添加这个新字段,以便用户可以直观地看到当前状态。

第 2 步:实现核心表逻辑

我们需要在文档表 (Demo_WFDocument) 上实现一些关键方法:

  1. 覆写 canSubmitToWorkflow() 方法: 这个方法决定了一个记录是否满足提交到工作流的条件。在这里,我们设定只有当 WorkflowStatus 字段为 Draft 时才允许提交。
public boolean canSubmitToWorkflow(str _workflowType = '')
{
    boolean ret = super(_workflowType);
 
    // 只有草稿状态的文档才能提交
    ret = this.WorkflowStatus == Demo_WFDocumentStatus::Draft;
 
    return ret;
}
  1. (可选) 添加状态更新辅助方法: 为了方便起见,可以添加一个静态方法,允许通过记录 ID 来更新工作流状态。这在实现其他业务逻辑时可能很有用。
public static void updateWorkflowStatus(RecId _documentRecId, Demo_WFDocumentStatus _status)
{
    ttsbegin;
 
    Demo_WFDocument document;
 
    update_recordset document
        setting WorkflowStatus = _status
        where document.RecId == _documentRecId;
 
    ttscommit;
}

第 3 步:创建数据查询

工作流需要一个查询 (Query) 来定义它将使用哪些表和字段。

  • 创建一个新的查询。
  • 将你的文档表 (Demo_WFDocument) 添加为数据源。
  • 关键: 将查询的 Dynamic Fields 属性设置为 Yes,以确保所有表字段都包含在内。

第 4 步:定义工作流类别

工作流类别 (Workflow Category) 决定了你创建的工作流将出现在 D365 F&O 的哪个模块下。

  • 你可以使用现有的类别,但通常建议为新功能创建一个新的类别。
  • 创建新的工作流类别,并设置其 Module 属性(例如,本例中设置为车队管理 FleetManagement)。

提示: 如果为新项目创建工作流,可能需要扩展 Module 属性背后的基础枚举,并确保存在相应的工作流配置窗体。这个过程相对简单,可以参考 这篇相关文章(注意:链接来自原文)。

第 5 步:构建工作流类型

工作流类型 (Workflow Type) 是核心,它描述了工作流的特性和可用的元素。Visual Studio 提供了一个向导来简化创建过程:

  • Category: 选择上一步创建的工作流类别。
  • Query: 选择为文档表创建的查询。
  • Document menu item: 选择文档表对应窗体的菜单项 (Menu Item)。

完成向导后,项目中会生成几个关键对象:

  • Workflow Type 本身: 主要关注其属性,特别是标签 (Label) 和帮助文本 (Help Text),确保它们清晰有意义。其他属性通常指向事件处理程序。
  • Document 类 (...Document): 通常无需修改,它实现了返回关联查询的方法。
  • EventHandler 类 (...EventHandler): 处理工作流类型级别的事件(稍后会添加代码)。
  • SubmitManager 类 (...SubmitManager): 处理文档提交操作。我们将用一个更通用的类替换它。
  • CancelMenuItem (Action Menu Item): 用于取消文档工作流,更新其标签和帮助文本。
  • SubmitMenuItem (Action Menu Item): 用于提交文档到工作流,默认调用 SubmitManager我们需要修改它指向我们新的提交管理器类。 更新其标签和帮助文本。

优化提交逻辑:使用通用的 Submission Manager

/// <summary>
/// The Demo_FAAcceptanceWorkflowSubmitManager menu item action event handler.
/// </summary>
public class Demo_FAAcceptanceWorkflowSubmitManager 
{
    private Demo_FAAcceptanceFormTable document;
    private WorkflowVersionTable versionTable;
    private WorkflowComment comment;
    private WorkflowWorkItemTable workItem;
    private SysUserId userId;
    private boolean isSubmission;
    private WorkflowTypeName workflowType;
 
    /// <summary>
    /// Main method
    /// </summary>
    /// <param name = "_args">calling arguments</param>
    public static void main(Args _args)
    {
        if (_args.record().TableId != tableNum(Demo_FAAcceptanceFormTable))
        {
            throw error('@DemoZ:DemoZ0476');
        }
 
        Demo_FAAcceptanceFormTable document = _args.record();
        FormRun caller = _args.caller() as FormRun;
        boolean isSubmission = _args.parmEnum();
        MenuItemName menuItem = _args.menuItemName();
 
        Demo_FAAcceptanceWorkflowSubmitManager manager = Demo_FAAcceptanceWorkflowSubmitManager::construct();
        manager.init(document, isSubmission, caller.getActiveWorkflowConfiguration(), caller.getActiveWorkflowWorkItem());
 
        if (manager.openSubmitDialog(menuItem))
        {
            manager.performSubmit(menuItem);
        }
 
        caller.updateWorkflowControls();
 
        FormDataSource Demo_HRMEmployeeRequest_DS;
        Demo_HRMEmployeeRequest_DS = FormDataUtil::getFormDataSource(document);
 
        if (Demo_HRMEmployeeRequest_DS)
        {
            Demo_HRMEmployeeRequest_DS.research(true);
            Demo_HRMEmployeeRequest_DS.refresh();
        }
    }
 
    /// <summary>
    /// Construct method
    /// </summary>
    /// <returns>new instance of submission manager</returns>
    public static Demo_FAAcceptanceWorkflowSubmitManager construct()
    {
        return new Demo_FAAcceptanceWorkflowSubmitManager();
    }
 
    /// <summary>
    /// parameter method for document
    /// </summary>
    /// <param name = "_document">new document value</param>
    /// <returns>current document</returns>
    public Demo_FAAcceptanceFormTable parmDocument(Demo_FAAcceptanceFormTable _document = document)
    {
        document = _document;
 
        return document;
    }
 
    /// <summary>
    /// parameter method for version
    /// </summary>
    /// <param name = "_versionTable">new version table value</param>
    /// <returns>current version table</returns>
    public WorkflowVersionTable parmVersionTable(WorkflowVersionTable _versionTable = versionTable)
    {
        versionTable = _versionTable;
 
        return versionTable;
    }
 
    /// <summary>
    /// parameter method for comment
    /// </summary>
    /// <param name = "_comment">new comment value</param>
    /// <returns>current comment value</returns>
    public WorkflowComment parmComment(WorkflowComment _comment = comment)
    {
        comment = _comment;
 
        return comment;
    }
 
    /// <summary>
    /// parameter method for work item
    /// </summary>
    /// <param name = "_workItem">new work item value</param>
    /// <returns>current work item value</returns>
    public WorkflowWorkItemTable parmWorkItem(WorkflowWorkItemTable _workItem = workItem)
    {
        workItem = _workItem;
 
        return workItem;
    }
 
    /// <summary>
    /// parameter method for user
    /// </summary>
    /// <param name = "_userId">new user value</param>
    /// <returns>current user value</returns>
    public SysUserId parmUserId(SysUserId _userId = userId)
    {
        userId = _userId;
 
        return userId;
    }
 
    /// <summary>
    /// parameter method for isSubmission flag
    /// </summary>
    /// <param name = "_isSubmission">flag value</param>
    /// <returns>current flag value</returns>
    public boolean parmIsSubmission(boolean _isSubmission = isSubmission)
    {
        isSubmission = _isSubmission;
 
        return isSubmission;
    }
 
    /// <summary>
    /// parameter method for workflow type
    /// </summary>
    /// <param name = "_workflowType">new workflow type value</param>
    /// <returns>current workflow type</returns>
    public WorkflowTypeName parmWorkflowType(WorkflowTypeName _workflowType = workflowType)
    {
        workflowType = _workflowType;
 
        return workflowType;
    }
 
    /// <summary>
    /// Opens the submit dialog and returns result
    /// </summary>
    /// <returns>true if dialog closed okay</returns>
    protected boolean openSubmitDialog(MenuItemName _menuItemName)
    {
        if (isSubmission)
        {
            return this.openSubmitDialogSubmit();
        }
        else
        {
            return this.openSubmitDialogResubmit(_menuItemName);
        }
    }
 
    /// <summary>
    /// Open submission dialog
    /// </summary>
    /// <returns>true if dialog closed okay</returns>
    private boolean openSubmitDialogSubmit()
    {
        WorkflowSubmitDialog submitDialog = WorkflowSubmitDialog::construct(this.parmVersionTable());
        submitDialog.run();
        this.parmComment(submitDialog.parmWorkflowComment());
 
        return submitDialog.parmIsClosedOK();
    }
 
    /// <summary>
    /// Open resubmit dialog
    /// </summary>
    /// <returns>true if dialog closed okay</returns>
    private boolean openSubmitDialogResubmit(MenuItemName _menuItemName)
    {
        WorkflowWorkItemActionDialog actionDialog = WorkflowWorkItemActionDialog::construct(workItem, WorkflowWorkItemActionType::Resubmit, new MenuFunction(_menuItemName, MenuItemType::Action));
        actionDialog.run();
        this.parmComment(actionDialog.parmWorkflowComment());
        this.parmUserId(actionDialog.parmTargetUser());
 
        return actionDialog.parmIsClosedOK();
    }
 
    /// <summary>
    /// initializes manager
    /// </summary>
    /// <param name = "_document">document</param>
    /// <param name = "_menuItem">calling menu item</param>
    /// <param name = "_versionTable">workflow version</param>
    /// <param name = "_workItem">workflow item</param>
    protected void init(Demo_FAAcceptanceFormTable _document, boolean _isSubmission, WorkflowVersionTable _versionTable, WorkflowWorkitemTable _workItem)
    {
        this.parmDocument(_document);
        this.parmIsSubmission(_isSubmission);
        this.parmVersionTable(_versionTable);
        this.parmWorkItem(_workItem);
        this.parmWorkflowType(this.parmVersionTable().WorkflowTable().TemplateName);
    }
 
    /// <summary>
    /// perform workflow submission
    /// </summary>
    protected void performSubmit(MenuItemName _menuItemName)
    {
        if (isSubmission)
        {
            this.performSubmitSubmit();
        }
        else
        {
            this.performSubmitResubmit(_menuItemName);
        }
    }
 
    /// <summary>
    /// perform workflow submit
    /// </summary>
    private void performSubmitSubmit()
    {
        if (this.parmWorkflowType() && Demo_FAAcceptanceFormTable::findRecId(document.RecId).WorkflowState == Demo_FAAcceptanceWorkflowState::NotSubmitted)
        {
            Workflow::activateFromWorkflowType(workflowType, document.RecId, comment, NoYes::No);
            Demo_FAAcceptanceFormTable::UpdateWorkflowState(document.RecId, Demo_FAAcceptanceWorkflowState::Submitted);
        }
    }
 
    /// <summary>
    /// perform workflow resubmit
    /// </summary>
    private void performSubmitResubmit(MenuItemName _menuItemName)
    {
        if (this.parmWorkItem())
        {
            WorkflowWorkItemActionManager::dispatchWorkItemAction(workItem, comment, userId, WorkflowWorkItemActionType::Resubmit, _menuItemName);
            Demo_FAAcceptanceFormTable::updateWorkflowState(document.RecId, Demo_FAAcceptanceWorkflowState::Submitted);
        }
    }
 
}

重要配置:

  • 修改向导生成的 SubmitManager 类代码为上述代码。
  • 修改 SubmitMenuItem 操作菜单项:
    • 将其 Object Type 设置为 Class
    • 将其 Object 设置为我们新创建的 Demo_FAAcceptanceWorkflowSubmitManagerr 类名。
    • 设置其 Enum Type Parameter 为 NoYes
    • 设置其 Enum Parameter 为 Yes (表示这是初始提交)。
  • 任何用于重新提交的操作菜单项(例如稍后创建的审批拒绝后的重新提交按钮)应将 Enum Parameter 设置为 No

第 6 步:处理工作流类型事件

在工作流类型向导生成的 EventHandler 类中,我们需要添加代码来响应关键的工作流生命周期事件:

/// <summary>
/// The Demo_FAAcceptanceWorkflowEventHandler workflow event handler.
/// </summary>
public class  Demo_FAAcceptanceWorkflowEventHandler implements WorkflowCanceledEventHandler,  
	WorkflowCompletedEventHandler,
	WorkflowStartedEventHandler
{
    public void started(WorkflowEventArgs _workflowEventArgs)
    {
        RecId documentRecId = _workflowEventArgs.parmWorkflowContext().parmRecId();
        Demo_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, Demo_FAAcceptanceWorkflowState::Submitted);
    }
 
    public void canceled(WorkflowEventArgs _workflowEventArgs)
    {
        RecId documentRecId = _workflowEventArgs.parmWorkflowContext().parmRecId();
        Demo_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, Demo_FAAcceptanceWorkflowState::NotSubmitted);
    }
 
    public void completed(WorkflowEventArgs _workflowEventArgs)
    {
        RecId documentRecId = _workflowEventArgs.parmWorkflowContext().parmRecId();
        Demo_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, Demo_FAAcceptanceWorkflowState::Completed);
    }
 
}

第 7 步:创建工作流审批元素

审批 (Approval) 是工作流中常见的元素,允许用户对文档进行批准或拒绝。同样,可以使用向导来创建:

注意: 在运行此向导之前,可能需要先编译你的项目,以确保之前创建的类(如 Document 类)可供选择。

  • Workflow document: 选择工作流类型向导创建的 ...Document 类。
  • Document preview field group: 选择文档表中用于在工作流历史记录中显示标识信息的字段组。
  • Document menu item: 再次选择文档表对应窗体的菜单项。

此向导也会生成一系列对象:

  • EventHandler 类 (...EventHandler): 处理审批特定事件(稍后添加代码)。
  • ResubmitActionMgr 类: 处理重新提交。我们将配置相应的菜单项以使用我们通用的 Demo_WFDocumentSubmitManager 类。
  • Approve (Action Menu Item): 用于批准操作,更新标签/帮助文本。
  • DelegateMenuItem (Action Menu Item): 用于委派操作,更新标签/帮助文本。
  • Reject (Action Menu Item): 用于拒绝操作,更新标签/帮助文本。
  • RequestChange (Action Menu Item): 用于请求更改操作,更新标签/帮助文本。
  • ResubmitMenuItem (Action Menu Item): 用于重新提交。我们需要修改它以使用通用的提交管理器。

配置 ResubmitMenuItem:

  • 将其 Object Type 设置为 Class
  • 将其 Object 设置为之前创建的类 SubmitManager
  • 设置其 Enum Type Parameter 为 NoYes
  • 设置其 Enum Parameter 为 No (表示这是重新提交)。
  • 删除向导生成的 ResubmitActionMgr 类。

第 8 步:处理工作流审批事件

在审批元素向导生成的 EventHandler 类中,添加处理审批结果的代码。根据原文示例,这里在取消和完成时都更新为 Rejected(请根据你的实际业务逻辑调整):

/// <summary>
/// The OXC_FAAcceptanceWorkflowApprovalEventHandler workflow outcome event handler.
/// </summary>
public final class OXC_FAAcceptanceWorkflowApprovalEventHandler implements WorkflowElementCanceledEventHandler,
	WorkflowElemChangeRequestedEventHandler,
	WorkflowElementCompletedEventHandler,
	WorkflowElementReturnedEventHandler,
	WorkflowElementStartedEventHandler,
	WorkflowElementDeniedEventHandler,
	WorkflowWorkItemsCreatedEventHandler
{
    public void started(WorkflowElementEventArgs _workflowElementEventArgs)
    {
 
    }
 
    public void canceled(WorkflowElementEventArgs _workflowElementEventArgs)
    {
        RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId();
        OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::NotSubmitted);
    }
 
    public void completed(WorkflowElementEventArgs _workflowElementEventArgs)
    {
        RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId();
        OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::Approved);
    }
 
    public void denied(WorkflowElementEventArgs _workflowElementEventArgs)
    {
        RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId();
        OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::Rejected);
    }
 
    public void changeRequested(WorkflowElementEventArgs _workflowElementEventArgs)
    {
        RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId();
        OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::ChangeRequest);
    }
 
    public void returned(WorkflowElementEventArgs _workflowElementEventArgs)
    {
        RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId();
        OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::Rejected);
    }
 
    public void created(WorkflowWorkItemsEventArgs _workflowWorkItemsEventArgs)
    {
        // TODO:  Write code to execute once work items are created.
    }
 
}

第 9 步:将审批元素添加到工作流类型

回到你的工作流类型定义,在 AOT (Application Object Tree) 中找到 Supported Element Types 节点。

  • 创建一个新的节点,引用你刚刚创建的工作流审批元素。
  • 设置其 Name 和 ElementType 属性,指向审批元素的名称。

第 10 步:在窗体上启用工作流

最后一步是在文档表对应的窗体上启用工作流支持:

  • 打开窗体设计器。
  • 选中代表主数据源 (你的 Demo_WFDocument 表) 的 Design 节点。
  • 在属性窗口中,设置以下属性:
    • Workflow Datasource: 设置为你的文档表数据源名称。
    • Workflow Enabled: 设置为 Yes
    • Workflow Type: 选择你创建的工作流类型名称。

第 11 步:配置和激活实际的工作流

完成以上所有开发步骤并成功编译项目后,你需要在 D365 F&O 前端配置并激活工作流:

  1. 导航到你在工作流类别中指定的模块(本例中是车队管理)。
  2. 找到工作流配置相关的菜单项 (通常在 “设置” > “工作流” 下)。
  3. 点击 “新建”,从列表中选择你创建的工作流类型。
  4. 系统可能会提示输入凭据,然后会打开工作流编辑器。
  5. 在编辑器中:
    • 从左侧工具箱将你创建的 “审批” 元素拖拽到画布上。
    • 将 “开始” 节点连接到 “审批” 元素。
    • 将 “审批” 元素连接到 “结束” 节点。
    • 配置审批元素的属性(例如,分配给谁、完成策略等)。
    • 解决编辑器底部显示的所有警告或错误。
  6. 点击 “保存并关闭”。
  7. 在弹出的对话框中选择 “激活新版本”。

现在,当你创建或查看 Demo_WFDocument 表的记录时,窗体的菜单栏上应该会出现工作流相关的按钮了!