在 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
) 上实现一些关键方法:
覆写 canSubmitToWorkflow()
方法: 这个方法决定了一个记录是否满足提交到工作流的条件。在这里,我们设定只有当 WorkflowStatus
字段为 Draft
时才允许提交。
public boolean canSubmitToWorkflow( str _workflowType = '' )
{
boolean ret = super ( _workflowType) ;
// 只有草稿状态的文档才能提交
ret = this.WorkflowStatus == Demo_WFDocumentStatus:: Draft ;
return ret;
}
(可选) 添加状态更新辅助方法: 为了方便起见,可以添加一个静态方法,允许通过记录 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 前端配置并激活工作流:
导航到你在工作流类别中指定的模块(本例中是车队管理)。
找到工作流配置相关的菜单项 (通常在 “设置” > “工作流” 下)。
点击 “新建”,从列表中选择你创建的工作流类型。
系统可能会提示输入凭据,然后会打开工作流编辑器。
在编辑器中:
从左侧工具箱将你创建的 “审批” 元素拖拽到画布上。
将 “开始” 节点连接到 “审批” 元素。
将 “审批” 元素连接到 “结束” 节点。
配置审批元素的属性(例如,分配给谁、完成策略等)。
解决编辑器底部显示的所有警告或错误。
点击 “保存并关闭”。
在弹出的对话框中选择 “激活新版本”。
现在,当你创建或查看 Demo_WFDocument
表的记录时,窗体的菜单栏上应该会出现工作流相关的按钮了!