D365FO技术分享:使用X++优雅地处理Excel 新增行、合并单元格与格式化技巧

D365FO Tech Share: Handling Excel Elegantly with X++ Techniques for Adding Rows, Merging Cells, and Formatting

摘要

本文旨在分享在Dynamics 365 Finance and Operations (D365FO)环境中,如何运用X++代码,结合OfficeOpenXml库,对系统生成的Excel文件进行更灵活、更深入的控制。我们将重点探讨动态插入新行、精确复制行格式与行高、合并单元格以优化布局,以及对单元格进行赋值和应用特定样式等实用技巧。这些方法对于需要生成非标准化、高度定制化Excel报表的D365FO开发者而言非常有价值。

引言

在D365FO的开发与实施过程中,将数据导出至Excel是一项基础且频繁的需求。尽管系统提供了标准的数据导出功能,但在许多实际业务场景下,我们需要对导出的Excel文件进行超越标准功能的“二次加工”。例如,可能需要在指定位置插入小计行或明细行,按照预设模板的样式填充数据,为了报表美观和可读性合并表头或数据区域,或者对特定数据(如金额、日期、状态)应用特定的单元格格式。这时,直接通过X++代码操作Excel文档就显得尤为重要。本文将详细介绍如何使用D365FO推荐的OfficeOpenXml库来实现这些高级的Excel操作。

准备工作

开始编码前,请确保您的D365FO开发环境已就绪。OfficeOpenXml相关的程序集(如OfficeOpenXml.dll)通常在标准环境中是可用的。在您的X++代码(通常是类或方法)中,需要引入相应的命名空间:

// X++ using 语句
using OfficeOpenXml;
using OfficeOpenXml.Style;
using System.Drawing; // 如果需要处理颜色等

本文后续的代码示例假设您已经成功创建了ExcelPackage对象(例如变量 _package)和ExcelWorksheet对象(例如变量 _worksheet),并且已经通过_worksheet.Cells获取了单元格的操作句柄(通常赋给一个类似 _cells 的ExcelRange变量)。

核心功能实现

1. 动态插入新行

在向Excel写入数据或应用格式前,有时需要在工作表的特定位置插入若干空行。OfficeOpenXml 提供了 InsertRow 方法来实现此功能:

// 示例:在第 10 行(rowToInsertBefore = 10)的上方插入 5 行
int rowToInsertBefore = 10;
int numberOfRowsToInsert = 5;
_worksheet.InsertRow(rowToInsertBefore, numberOfRowsToInsert);
 
// 重要提示:插入行操作会使被插入位置及其下方的所有行号向下移动。
// 在执行插入操作后,需要特别注意更新后续操作中涉及的行号索引。

2. 复制行格式与行高

当您插入了新行,或者希望多行数据保持统一的视觉风格时,复制一个“模板行”的格式和行高是一种高效的做法。以下代码(改编自您提供的片段)展示了如何实现:它假定 row + rowCount 这一行是我们的格式模板行,然后将该行的格式(包括单元格样式和行高)应用到从 row 行开始的 rowCount 行。

W041161024848/// <summary>
/// 根据模板行,为指定范围内的行复制单元格格式、行高,并执行单元格合并。
/// </summary>
/// <param name="row">需要开始应用格式的起始行号(基于1)。</param>
/// <param name="rowCount">需要处理的总行数。</param>
/// <param name="_worksheet">目标Excel工作表对象。</param>
public void ApplyRowFormattingAndMerging(int row, int rowCount, ExcelWorksheet _worksheet)
{
    // 基本参数校验
    if (_worksheet == null || row <= 0 || rowCount <= 0)
    {
        error("为行格式化提供的参数无效。");
        return;
    }
 
    ExcelRange _cells = _worksheet.Cells; // 获取单元格集合对象
    int templateRow = row + rowCount; // 定义模板行的行号
 
    // 可选:初步检查模板行是否可能在工作表范围内
    if (templateRow > _worksheet.Dimension?.End.Row) // 使用?.安全访问Dimension
    {
        warning(strFmt("模板行 %1 可能超出了工作表范围。", templateRow));
        // 此处可根据业务逻辑决定是否继续,或采用备用模板策略
    }
 
    try
    {
        // 外层循环处理每一行
        for (int i = 0; i < rowCount; i++)
        {
            int currentRow = row + i; // 当前正在处理的行号
 
            // --- 步骤1:复制行格式 (单元格样式) ---
            int maxColumns = _worksheet.Dimension.Columns; // 获取最大列数,?? 提供备用值
            for (int col = 1; col <= maxColumns; col++)
            {
                if (_cells.get_Item(templateRow,col).Style != null)
                {
                    _cells.get_Item(currentRow,col).StyleID = _cells.get_Item(templateRow,col).StyleID;
                    _cells.get_Item(currentRow,col).Formula = _cells.get_Item(templateRow,col).Formula;
                }
            }
 
            // --- 步骤2:复制行高 ---
            if (_worksheet.Row(templateRow) != null) // 确保模板行对象有效
            {
                _worksheet.Row(currentRow).Height = _worksheet.Row(templateRow).Height;
            }
 
            // --- 可选步骤:对合并后单元格或特定单元格赋值与单独格式化 ---
            // 例如,给合并后的第一个单元格赋值并居中
            // _cells[currentRow, 3].Value = "合并区域值";
            // _cells[currentRow, 3].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
            // _cells[currentRow, 3].Style.VerticalAlignment = ExcelVerticalAlignment.Center;
 
        } // 单行处理循环结束
    }
    catch (Exception ex)
    {
        error(strFmt("在Excel行格式化与合并过程中发生错误:%1", ex.Message));
        // 强烈建议记录更详细的异常信息,或进行更完善的错误处理
    }
}

3. 合并单元格

合并单元格是整理Excel布局、突出显示表头或归类信息区的常用手段。OfficeOpenXml通过设置单元格区域(Range)的 Merge 属性为 true 来实现:

// (上述代码片段中已包含合并单元格的实现)
// 合并 currentRow 行的 C列 到 E列 (第3列到第5列)
_worksheet.Cells[currentRow, 3, currentRow, 5].Merge = true;
 
// 合并 currentRow 行的 F列 到 I列 (第6列到第9列)
_worksheet.Cells[currentRow, 6, currentRow, 9].Merge = true;
// ... 以此类推 ...
 
// 提示:合并后,通常只需要对合并区域左上角的单元格进行赋值和设置格式。
// EPPlus (OfficeOpenXml库的核心) 的行号和列号索引都是从 1 开始的。
// C=3, E=5, F=6, I=9, J=10, N=14, O=15, P=16

4. 赋值与设置特定单元格格式

除了整行复制格式外,往往还需要对单个或特定区域的单元格进行精确赋值,并应用独立的格式,比如设置数字格式、对齐方式、字体样式(加粗、颜色)、背景色或边框等。

// 示例:对第 12 行 B 列(即 B12 单元格)进行操作
int targetRow = 12;
int targetCol = 2; // B列
 
var cell = _cells[targetRow, targetCol]; // 获取单元格对象
 
// 1. 赋值
cell.Value = 12345.67;
// cell.Value = "已完成";
// cell.Value = DateTimeUtil::utcNow(); // 使用D365FO的日期时间函数
 
// 2. 设置数字或日期格式
cell.Style.Numberformat.Format = "#,##0.00_);[Red](#,##0.00)"; // 会计格式,负数红色显示
// cell.Style.Numberformat.Format = "yyyy/m/d h:mm AM/PM"; // 自定义日期时间格式
// cell.Style.Numberformat.Format = "@"; // 设置为文本格式
 
// 3. 设置对齐方式
cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; // 水平居中
cell.Style.VerticalAlignment = ExcelVerticalAlignment.Center; // 垂直居中
 
// 4. 设置字体
cell.Style.Font.Bold = true; // 加粗
cell.Style.Font.Italic = false; // 不倾斜
cell.Style.Font.Size = 11; // 字号
cell.Style.Font.Name = "等线"; // 字体名称
// cell.Style.Font.Color.SetColor(System.Drawing.Color::Blue); // 设置字体颜色为蓝色
 
// 5. 设置背景填充色
// cell.Style.Fill.PatternType = ExcelFillStyle.Solid; // 设置填充模式为纯色
// cell.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color::FromArgb(217, 217, 217)); // 浅灰色背景 (使用ARGB)
 
// 6. 设置单元格边框
// cell.Style.Border.Top.Style = ExcelBorderStyle.Thin; // 上边框细线
// cell.Style.Border.Bottom.Style = ExcelBorderStyle.Medium; // 下边框中等粗细
// cell.Style.Border.Left.Style = ExcelBorderStyle.Thin; // 左边框细线
// cell.Style.Border.Right.Style = ExcelBorderStyle.Thin; // 右边框细线
// cell.Style.Border.Top.Color.SetColor(System.Drawing.Color::Black); // 设置上边框颜色为黑色
// ... 为其他边框设置颜色 ...

总结

通过在X++代码中娴熟运用OfficeOpenXml库提供的功能,D365FO开发者能够突破标准导出功能的局限,灵活地创建出满足各种复杂业务场景、格式精美且高度定制化的Excel报表。掌握动态插入行、高效复制格式、策略性地合并单元格以及对关键数据应用特定样式等技巧,将极大地提升您交付高质量、易于最终用户使用的Excel文档的能力和效率。

分步指南:在 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 表的记录时,窗体的菜单栏上应该会出现工作流相关的按钮了!

如何在 D365FO 的现有量查询中增加字段和数据源(Add the new field or datasource in Inventory On hand form using X++ in D365FO)

我们需要为类 InventDimCtrl_Frm_OnHand 创建一个扩展,以使用方法 modifiedQueryBasedOnDatasourceName 来实现这种Inventory On hand定制。

通过这种方式,我们可以在网格中添加带有过滤选项的新字段。(不使用显示方法)。根据需求,我们可以在表单中添加来自现有数据源以及新数据源的字段。

[ExtensionOf(classStr(InventDimCtrl_Frm_OnHand))]
final class InventDimCtrl_Frm_OnHandClass_Extension
{
    public void modifyQueryBasedOnDatasourceName(
                                                Query _inventSum_DS_Query,
                                                str _inventSum_DS_Name,
                                                FormDataSource _inventDim_DS)
    {
        Query query;
        QueryBuildDataSource qbds;
        QueryBuildDataSource qbdsInventOnHandItemListView;
        QueryBuildDataSource qbdsInventTable;
        Counter loop;
        boolean qbdsExists;
        next modifyQueryBasedOnDatasourceName(_inventSum_DS_Query, _inventSum_DS_Name, _inventDim_DS);
         
        query =   _inventSum_DS_Query;
     
        qbdsInventTable = query.dataSourceTable(tableNum(InventTable));
     
        if (qbdsInventTable)
        {
            qbdsInventTable.addGroupByField(fieldNum(InventTable, BinLocation));
            qbdsInventTable.addGroupByField(fieldNum(InventTable, ItemStatus));
            qbdsInventTable.addGroupByField(fieldNum(InventTable, PackQtyString));
            qbdsInventTable.addGroupByField(fieldNum(InventTable, PartNumber));
            qbdsInventTable.addGroupByField(fieldNum(InventTable, PromotionApplicable));
            qbdsInventTable.addGroupByField(fieldNum(InventTable, PurchaseMultiple));
            qbdsInventTable.addGroupByField(fieldNum(InventTable, ManufacturerCode));
     
            for (loop = 1; loop <= _inventSum_DS_Query.dataSourceCount(); loop++)
            {
                qbds =   _inventSum_DS_Query.dataSourceNo(loop);
     
                if (qbds.table() == tableNum(InventOnHandItemListView))
                {
                    qbdsExists = true;
                    break;
                }
            }
     
            if (!qbdsExists)
            {
                qbdsInventOnHandItemListView = query.dataSourceTable(tableNum(InventTable)).addDataSource(tableNum(InventOnHandItemListView));
                qbdsInventOnHandItemListView.addLink(fieldNum(InventOnHandItemListView, ItemId), fieldNum(InventTable, ItemId));
                qbdsInventOnHandItemListView.joinMode(JoinMode::InnerJoin);
     
                qbdsInventOnHandItemListView.addRange(fieldNum(InventOnHandItemListView, LanguageId)).value(queryValue(new xInfo().language()));
            }
            else
            {
                qbdsInventOnHandItemListView = query.dataSourceTable(tableNum(InventOnHandItemListView));
                    qbdsInventOnHandItemListView.addRange(fieldNum(InventOnHandItemListView, LanguageId)).value(queryValue(new xInfo().language()));
            }
     
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, ManufacturerName));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, VendorID));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, VendorName));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, CoverageGroup));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, ProductType));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, ItemName));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, ItemDescription));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, SearchName));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, ProductDimensionGroup));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, StorageDimensionGroup));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, TrackingDimensionGroup));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, ItemModelGroupID));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, BasePurchasePrice));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, BaseSalesPrice));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, BaseCostPrice));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, InventUnit));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, SalesUnit));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, PurchUnit));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, ItemGroupId));
            qbdsInventOnHandItemListView.addGroupByField(fieldNum(InventOnHandItemListView, ProductSubType));
        }
    }
}
public void modifyQueryBasedOnDatasourceName(
                Query          _inventSum_DS_Query,
                str            _inventSum_DS_Name,
                FormDataSource _inventDim_DS)
{
    Query                   query;
    QueryBuildDataSource    qbdsInventDim;
    QueryBuildDataSource    qbdsInventSum;
    QueryBuildDataSource    qbdsInventBatch;
    QueryBuildDataSource    qbdsInventTable;
    QueryBuildRange         qbrItemId;
    str                     queryValue;
 
    next modifyQueryBasedOnDatasourceName(_inventSum_DS_Query, _inventSum_DS_Name, _inventDim_DS);
 
    query = _inventSum_DS_Query;
 
    qbdsInventSum   = query.dataSourceTable(tableNum(InventSum));
    qbdsInventTable = query.dataSourceTable(tableNum(InventTable));
    if(qbdsInventTable)
    {
        qbdsInventTable.addGroupByField(fieldNum(InventTable, NameAlias));
        qbdsInventTable.addGroupByField(fieldNum(InventTable, Demo_Size));
    }
 
}
| Tagged 

如何在D365FO使用视图在窗体上显示某一个财务维度(Financial dimensions)

在这篇文章中,我们将了解如何使用视图将财务维度(Financial dimensions)显示为表单网格中的字段,该视图将使用计算列直接从表中检索信息。这是一种快速获取信息以将其显示在列表中的方法,并且能够使用控件按维度值进行筛选。

解决方案:

如何做:

步骤 1:创建视图

我们创建一个新的视图 ( FTDDimensionSetView ),使用 DimensionAttributeValueSet 作为数据源,并添加 RecId 作为视图字段。请注意,此 RecId 将是  表中我们想要显示维度的DefaultDimension字段(本例中为CustTrans  ),这就是为什么我在视图中也将其命名为 DefaultDimension。

步骤 2:创建计算列方法

我们将对要添加到视图中的每个维度值使用一个静态方法。标准 DimensionSetEntity 实体需要对其进行概括,并且仅使用一个复杂的方法,在配置好所有维度后,使用插件加载所有维度,这些维度会自动创建,因为每个实现都有不同的维度……在我们的例子中,我们不需要额外的复杂性,因为我们已经知道并拥有我们想要显示的维度。保持简单!

这里有 3 种方法(我决定将逻辑放在一种通用方法中,所以有 4 种)。

public class FTDDimensionSetView extends common
{
    private static str getSQLStringForDimension(DimensionRefFieldName _dimensionName)
    {
        DimensionAttribute dimensionAttribute = DimensionAttribute::findByName(_dimensionName);
 
        return dimensionAttribute.DimensionValueColumnName;
    }
 
    private static str getDepartment()
    {
        return FTDDimensionSetView::getSQLStringForDimension(FTDDimensionUtils::Department);
    }
 
    private static str getCostCenter()
    {
        return FTDDimensionSetView::getSQLStringForDimension(FTDDimensionUtils::CostCenter);
    }
 
    private static str getRetailChannel()
    {
        return FTDDimensionSetView::getSQLStringForDimension(FTDDimensionUtils::RetailChannel);
    }
 
}

在这些方法中,我们将从配置它们的表 DimensionAttribute 中获取 SQL 中的列名称(对 VS 隐藏)。(FTDDimensionUtils 类中的值只是该表的名称字段的值)。

步骤 3:创建字符串计算列

现在,我们创建 3 个新的计算列,并使用属性View 方法将它们与上面创建的适当方法关联 。

如果在同步我们的视图之后,我们转到 SQL 并查看它的设计,它可以帮助我们直观地了解 计算列 实际上是什么:

步骤 4:从 CustTrans 添加新关系

现在,我们必须添加从 CustTrans  视图的关系。如果您想将财务维度添加到任何其他表格中,则只需重复执行此步骤和步骤 5(将其添加到表单)。因此,正如所有良好的发展一样,我们创建了一个非常有用、简单且可重复使用的代码片段!

第 5 步:在表单中使用它

将新视图作为新的相关数据源添加到表单,将字段添加到网格…

瞧! 请注意,我们可以通过它们进行筛选!需求匹配,客户满意

我希望这篇文章能够对你,匆忙的开发人员有所帮助,就像我之前知道它就会对我有所帮助一样。

Tax is regulated on purchase ID xxx. Purchase orders cannot be rearranged when individual purchase orders are tax regulated.

The reason why you get this error message is because you made the tax adjustment first at the individual invoice Level. Later on then you consolidated (rearranged) two or more invoices(PO partially invoiced) and AX says that it does not like that.

To get this fixed try the following:

1) Open the invoice for which you made the tax adjustment and set the tax amount back to what AX originally calculated

2) Rearrange, that is consolidate, the invoices

3) Before posting the rearranged invoices adjust the sales tax by opening the sales tax form at the vendor invoice Header or PO invoice posting.

4) Post your invoices.

如何D365FO使用视图使Display方法可以被过滤(How to create a filter on display method using view in D365FO)

基于Display的方法的表单字段不可过滤,但在某些情况下,客户希望这些字段可过滤(Filter on display method)。有两种方法可以使显示字段可过滤,我们将在本博客中仅解释最佳方法(使用最佳实践)。

在我们的案例中,供应商名称字段是一个基于Display方法的字段,并且不可过滤。

1.创建一个视图:

– metadata: 我们需要获取将替换显示字段的字段以及将用于与表单数据源创建关系的字段的表。(在我们的例子中是 VendTable 和 DirPartyTable)

– fields: 将替换显示字段的字段和我们将用于与表单数据源创建关系的字段。(在我们的例子中是 Name 和 AccountNum)

视图

2.在 FormDatasource(我们的案例 PurchLine)表上,我们需要添加一个与我们创建的 VendTableName 视图的新关系。

添加与视图的新关系

3. 在我们的表单上添加视图 VendTableName 作为数据源,并在 VendTableName FormDatasouce 属性上与主数据源(PurchLine)创建连接。

添加视图 VendTableName 作为数据源
create a join with the primary datasource

4. 最后一步是在表单网格上添加我们想要的视图数据字段。

输出结果:

| Tagged 

如何在D365FO工作流中使用审核编辑功能(how to use approval editable in d365fo workflow)

在Dynamics 365 F&O工作流开发过程中,配置采购订单工作流时,有两个审批元素可供使用,一个是“审批采购订单”,另一个是“可编辑的审批采购订单”(下文也将对其进行突出显示)。我的问题是关于第二个元素的开发,即“可编辑的审批采购订单”。根据我的测试,如果在配置过程中将此元素关联起来,系统将允许审批人编辑采购订单,然后在之后进行审批操作。

3515.editable-WF.png

可编辑的审批仅允许审批人编辑所选记录(他们正在审批的)。您可以按照以下步骤实现相同的功能:

  • 复制工作流审批工件并相应地更改其名称(例如,在我的情况下为“CaseDetailBaseApprovalEdit”),并将其与简单审批一起添加到支持的元素中。
  • 在表中创建一个方法(例如,我的情况下的“editAllowed()”)。
public boolean editAllowed()
{
    WorkflowElementTable workflowElementTable;
    WorkflowWorkItemTable workflowWorkItemTable;
 
    select * from workflowWorkItemTable
        where   workflowWorkItemTable.RefRecId      == this.RecId
            &&  workflowWorkItemTable.Status        == WorkflowWorkItemStatus::Pending
            &&  workflowWorkItemTable.UserId        == curUserId()
            &&  workflowWorkItemTable.CompanyId     == this.DataAreaId;
 
    select * from workflowElementTable
        where workflowElementTable.ElementId == workflowWorkItemTable.ElementId;
 
    return workflowElementTable.ElementName == workflowApprovalStr(CaseDetailBaseApprovalEdit)
        ||  this.CaseWFStatus == CaseWFStatus::Draft;
}
  • 覆盖表单数据源的 active() 方法(在我的情况下,我创建了 OOTB 表单数据源的 CoC)。
public int active()
{
   FormRun formRun = this.formRun() as FormRun;
   FormDataSource ds = formRun.dataSource();
   CaseDetailBase table = ds.cursor();
 
   boolean allowEdit = table.editAllowed();
 
   ds.allowEdit(allowEdit);
 
   if(allowEdit)
   {
       formRun.design().viewEditMode(ViewEditMode::Edit);
   }
   else
   {
       formRun.design().viewEditMode(ViewEditMode::View);
   }
 
   return next active();
}

如何在D365FO 中使用 Num2Str(因为每次使用的时候都会忘记)

参数 :

number :要转换为字符串的实数。

  • character:文本中所需的最少字符数。
  • decimals: 所需的小数位数。
  • separator1 : 小数分隔符
  • separator2:A 千分号分隔符

seperator1 参数的可能枚举值为:点(.),逗号 (,)

seperator2 参数的可能值为:无,点(.),逗号(,),空格( )

使用

static void Job_Num2Str(Args _args) 
{ 
    real realNum = 12345.1294567890123456777; // 19 decimals places. ; 
    info(Num2Str(realNum,0,16,1,3)); // 16 decimal places 
    info(Num2Str(realNum,0,17,1,3)); // 17 decimal places }
}

输出

12 345.1294567890123457
12 345.13

 

| Tagged 

如何在D365FO导入Excel文件(How to import EXCEL using X++ in D365 FO)

开始之前的准备

  • 命名空间
  • 参考包(reference packages)
Using System.IO;
Using OfficeOpenXml;
Using OfficeOpenXml.ExcelPackage;
Using OfficeOpenXml.ExcelRange;

除了基本的包,我们还需要参考包:Directory

How to Import Records from Excel Using X++ Code in D365FO

第一部分代码

在这部分代码中,创建了一个导入对话框,在这里我们可以选择需要导入的文件,文件上传之后会被存储在临时的空间中

How to Import Records from Excel Using X++ Code in D365FO

第二部分代码

在这部分代码中,我们从导入的Excel中获取数据

How to Import Records from Excel Using X++ Code in D365FO

完整的代码

Using System.IO;
Using OfficeOpenXml;
Using OfficeOpenXml.ExcelPackage;
Using OfficeOpenXml.ExcelRange;
class RunnableClass1
{
    /// <summary>
    /// Runs the class with the specified arguments.
    /// </summary>
    /// <param name = "_args">The specified arguments.</param>
    public static void main(Args _args)
    {
        /*---------part 1 ------------------*/
        System.IO.Stream stream;
        ExcelSpreadsheetName sheet;
        FileUploadBuild fileUpload,fileUploadBuild;
        DialogGroup dialogUploadGroup;
        FormBuildControl formBuildControl;
        Dialog dialog=new Dialog("Excel Import Example");
        dialogUploadGroup=dialog.addGroup("@SYS54759");
        formBuildControl=dialog.formBuildDesign().control(dialogUploadGroup.name());
        fileUploadBuild=formBuildControl.addControlEx(classStr(fileUpload),"UploadExcel");
        fileUploadBuild.style(FileUploadStyle::MinimalWithFilename);
        fileUploadBuild.fileTypesAccepted(".xlsx");
        if(dialog.run() && dialog.closedOk())
        {
            FileUpload fileUploadControl=dialog.formRun().control(dialog.formRun().controlId("Upload"));
            FileUploadTemporaryStorageResult
            fileUploadResult=file::GetFileFromUser(classStr(FileUploadTemporaryStorageStrategy));
            //fileUploadResult=fileUploadControl.getFileUploadResult();
            /*------------------part 1 end---------------------*/
            /*------------------part 2---------------------*/
            if(fileUploadResult!= null && fileUploadResult.getUploadStatus())
            {
                stream=fileUploadResult.openResult();
                using(ExcelPackage  package= new ExcelPackage(stream))
                {
                    int rowCount, iterator;
                    package.Load(stream);
                    ExcelWorksheet worksheet= package.get_workbook().get_worksheets().get_Item(1);
                    OfficeOpenXml.ExcelRange range=worksheet.Cells;              
                    rowCount = worksheet.Dimension.End.Row - worksheet.Dimension.Start.Row + 1;
                    for(iterator=2;iterator<=rowCount;iterator++)
                    {
                        Info(range.get_Item(iterator,1).Value);
                        Info(range.get_Item(iterator,2).Value);
                        Info(range.get_Item(iterator,3).Value);
                    }
                }
            }
            /*------------------part 2 end---------------------*/
        }
        else
        {
            Error("error occured.");
        }
    }
}

导入

运行上面的Job之后,系统会弹出导入对话框,然后选择下面的文件。

How to Import Records from Excel Using X++ Code in D365FO
How to Import Records from Excel Using X++ Code in D365FO
How to Import Records from Excel Using X++ Code in D365FO

以下是点击“OK”之后的结果:

How to Import Records from Excel Using X++ Code in D365FO

关于Excel导出功能,请参考一下链接:

如何在D365FO导出Excel文件(How to export to EXCEL using X++ in D365 FO)

步骤

创建一个Job

Create and Export excel files in d365 using xpp d365snippets

添加以下代码

class RunnableClass2
{
    public static void main(Args _args)
    {
        CustTable custTable;
        DocuFileSaveResult saveResult =
    DocuFileSave::promptForSaveLocation("@ApplicationPlatform:OfficeDefaultWorkbookFileName","xlsx", null, "excel create and export");
        if (saveResult&& saveResult.parmAction() != DocuFileSaveAction::Cancel)
        {
            saveResult.parmOpenParameters('web=1');
            saveResult.parmOpenInNewWindow(false);
            System.IO.Stream
      workbookStream = new System.IO.MemoryStream();
            System.IO.MemoryStream
      memoryStream = new System.IO.MemoryStream();
            using(var package = new OfficeOpenXml.ExcelPackage(memoryStream))
            {
                var worksheets = package.get_Workbook().get_Worksheets();
                var worksheet = worksheets.Add("Sheet1");
                var cells = worksheet.get_Cells();
                var currentRow=1 ;
                /*-------HEADER PART -------*/
                var cell = cells.get_Item(currentRow,1);
                cell.set_Value("Customer Name");
                cell=null;
                cell = cells.get_Item(currentRow,2);
                cell.set_Value("Customer Address");
                /*-------HEADER PART END-------*/
                /*-------RECORD 1-------*/
                currentRow=2;
                cell= cells.get_Item(currentRow, 1);
                cell.set_Value("ABCD Trading");
                cell= null;
                cell= cells.get_Item(currentRow, 2);
                cell.set_Value("ABCD Complex, P.O Box :xxxxxx, XYZ Street");
                /*-------RECORD 1 END-------*/
                /*-------RECORD 2-------*/
                currentRow=3;
                cell= cells.get_Item(currentRow, 1);
                cell.set_Value("XYZ Trading");
                cell = null;
                cell = cells.get_Item(currentRow, 2);
                cell.set_Value("XYZ Complex, P.O Box :xxxxxx, ABC Street");
                /*-------RECORD 2 END-------*/
                package.Save();
            }
            memoryStream.Seek(0,System.IO.SeekOrigin::Begin);
            //Download the file.
            DocuFileSave::processSaveResult(memoryStream,saveResult);
        }
    }
}

把这个Job设置为启动项,运行它后我们可以看到以下界面:

Create and Export excel files in d365 using xpp d365snippets2

点击下载按钮,Excel就会被导出到下载文件夹里面

Create and Export excel files in d365 using xpp d365snippets2

关于Excel导出功能,请参考一下链接: