惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
U
Unit 42
MyScale Blog
MyScale Blog
B
Blog
阮一峰的网络日志
阮一峰的网络日志
量子位
IT之家
IT之家
The GitHub Blog
The GitHub Blog
F
Fortinet All Blogs
Recent Announcements
Recent Announcements
V
Visual Studio Blog
G
Google Developers Blog
Last Week in AI
Last Week in AI
雷峰网
雷峰网
博客园 - 聂微东
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
博客园 - 司徒正美
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
月光博客
月光博客
aimingoo的专栏
aimingoo的专栏

Donghai's Blog

使用DBeaver连接Dynamics 365 Dataverse | Donghai C# 技术备忘 - LINQ | Donghai 为Astro站点添加Google Analytics | Donghai C# 技术备忘 - LINQ - Donghai’s Blog 20260819 说些什么 | Donghai Dynamics 365 interview questions | Donghai C# 技术备忘 (1) | Donghai Dynamics 365 事件框架、事件执行管道 | Donghai 马拉松配速表 | Donghai 马拉松赛事分级 | Donghai 抖音创作日志(2026年) | Donghai 2026年跑步日志(5月开始) | Donghai 2FA | Donghai 初尝Github Actions | Donghai AstroPaper主题添加Waline评论 | Donghai Dynamics 365集中视图/聚焦视图/Focused View | Donghai AstroPaper主题添加GitHub风格的Markdown警告框 | Donghai AstroPaper主题自定义记录 | Donghai 我日常收藏的优质网站资源导航(持续更新) | Donghai 如何从归档页批量获取URL并提交到Bing Webmaster Tools | Donghai PaperMod 使用 Zeoseven 自定义网页字体 | Donghai 我常用的Prompt | Donghai Hugo默认时区导致本地预览文章不显示 | Donghai 通过手动提交工单解决Bing不收录网站问题 | Donghai 如何访问 Power BI 管理门户(Power BI Admin Portal) | Donghai 还在手敲时间戳?Win11输入法一个技巧,秒输当前时间戳 | Donghai 工作术语备忘录(持续更新) | Donghai 使用XrmToolBox导出安全角色表级权限到Excel | Donghai 24年的年终总结 | Donghai World笔记 | Donghai
如何在Dynamics 365中自定义SSRS报表导出为PDF(仅PDF格式) | ...
Donghai · 2024-12-15 · via Donghai's Blog

封面图

本文介绍如何在Dynamics 365中限制SSRS报表只能导出PDF(自定义)

Table of contents

Open Table of contents
  • 背景
  • 解决思路
  • 详细步骤(JS调用SSRS导出PDF)
    • 假设
    • Step 1. 添加Web资源
    • Step 2. 配置自定义按钮
  • 补充说明
    • 1. 为什么不使用“Convert to PDF”功能?
    • 2. Account.js 中的 CRM_FilteredAccount 在哪里找?
    • 3. Account.js 核心方法说明
  • 附录
    • Account.js

背景

最近在实施一个 Dynamics 365 项目,我们为报价单自定义开发了一张蛮复杂的 SSRS 报表(甲方公司各种复杂的计算公式、报价单格式布局、审批人签章),用作销售团队向客户提供的标准报价模板,但是他们管理层反馈现在销售部门的用户在系统里打印报价单可以选择很多种导出格式(如下截图),担心他们导出成 Excel 格式后继续在线下“加工”,希望系统限制一下,只能导出 PDF 格式

报表导出页面

因为 Dynamics 365 标准的报表“另存为”本身就是可以选择怎么多格式,同时我还咨询了微软,这些格式并不能做配置隐藏掉,所以只能想想别的办法了!

NOTE

其实就算系统限制只能导出 PDF 格式,也防止不了用户继续在线下“加工”,毕竟 PDF 也是可以编辑的,这也只能是“防君子不防小人”

解决思路

经过综合考虑(考虑到现有报表已投入大量开发资源),采用了方案二,通过自定义按钮实现直接导出SSRS报表的PDF文件

方案 实现方式 优点 缺点
方案一
前端生成 PDF
使用第三方 JS 库(如 PrintJS)重新设计报表 完全自定义,
控制灵活
需要重新开发,
成本较高
方案二
JS调用SSRS导出PDF
通过 JavaScript 调用现有 SSRS 报表,
强制导出 PDF 格式
复用现有报表,
开发效率高
依赖现有报表结构

详细步骤(JS调用SSRS导出PDF)

假设

客户实体上有一张自定义的 SSRS 报表,报表名称为 PrintAccount.rdl,现在我们将在客户实体上新添自定义按钮,点击按钮后直接导出这张报表的 PDF 文件,下面是详细的实现步骤

Step 1. 添加Web资源

  • Web 资源中创建 Account.js 文件
  • 将附录中的完整代码复制到文件中
  • 保存后务必发布更改

添加Javascript脚本

Step 2. 配置自定义按钮

TIP

这里我使用新版的按钮编辑界面,你也可以使用 Ribbon Workbench 添加按钮,达到目的就行

(1) 登录 Power Apps → 应用 → 选择应用 → 编辑

选择编辑命令栏-01

(2) 选择客户视图 → 编辑命令栏

选择编辑命令栏-02

(3) 选择 Main Form

选择 Main Form

(4) 绑定Account.js,并填写相关属性

  • LabelExport Account info
  • ActionRun JavaScript
  • Librarygdh_/Account/Account.js (根据实际情况填写)
  • Parameter 1Gdh.D365.Account.ExportPrintAccountReportPDF (根据实际情况填写)

绑定Account.js

最后保存并发布

补充说明

1. 为什么不使用“Convert to PDF”功能?

为什么不使用系统的标准功能来实现生成PDF

因为”Convert to PDF”功能存在以下限制:

  • 关联数据过滤问题:无法过滤停用的关联记录
  • 举个例子:客户与联系人是一对多关系,使用 Word 模板转 PDF 时,即使联系人已停用,仍会显示在输出中

2. Account.js 中的 CRM_FilteredAccount 在哪里找?

代码中的 CRM_FilteredAccountSSRS 报表的参数,用于传递 FetchXML 查询条件:

CRM_FilteredAccount

3. Account.js 核心方法说明

  • ExportPrintAccountReportPDF(); → 导出 PDF 主方法
  • GetReportIdByReportFileName(); → 获取报表 ID
  • ExecuteReport(); → 执行报表生成
  • Get_SSRS_Report_PDFBase64(); → 获取 PDF Base64 数据

附录

Account.js

/**
 * Account Entity Javascript.
 */
if (Gdh === undefined) {
  var Gdh = {};
}
if (Gdh.D365 === undefined) {
  Gdh.D365 = {};
}
Gdh.D365.Account = (function () {
  "use strict";
  return {
    Constants: {
      Fields: {
        AccountName: "name",
        Phone: "telephone1",
        Fax: "fax",
        Website: "websiteurl",
      },
      Reports: {
        PrintAccountReport: "PrintAccount.rdl",
      },
      SystemAdminId: "SystemAdminId",
    },
    OnLoad: function (ExecutionContext) {
      try {
        let objFormContext = ExecutionContext.getFormContext();
      } catch (e) {
        console.error("Error during OnLoad: ", e);
      }
    },
    ExportPrintAccountReportPDF: function (primaryControl) {
      let objFormContext = primaryControl;
      let CurrentAccountId = objFormContext.data.entity
        .getId()
        .replace("{", "")
        .replace("}", "");
      let that = this;
      console.log(CurrentAccountId, CurrentAccountId);
      let selectAttributes = `${that.Constants.Fields.AccountName}`;
      console.log(selectAttributes, selectAttributes);
      let accountEn = this.RetrieveSingleRecord(
        "accounts",
        CurrentAccountId,
        selectAttributes
      );
      let accountName = accountEn[this.Constants.Fields.AccountName];
      // CRM_FilteredAccount -> SSRS report argument
      let reportPrefilter =
        "CRM_FilteredAccount=" +
        "<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>" +
        "<entity name='account'>" +
        "  <all-attributes />" +
        "  <filter type='and'>" +
        "    <condition attribute='accountid' operator='eq' value='" +
        CurrentAccountId +
        "' />" +
        "  </filter>" +
        "</entity>" +
        "</fetch>";
      let arrReportSession = this.ExecuteReport(
        this.Constants.Reports.PrintAccountReport,
        reportPrefilter
      );
      this.Get_SSRS_Report_PDFBase64(arrReportSession, 2052)
        .then(function (base64String) {
          // Size of the file in KB
          let fSize =
            encodeURIComponent(base64String).replace(/%../g, "x").length / 1024;
          let openFileOptions = { openMode: 2 };
          let file = {};
          file.fileContent = base64String;
          file.fileSize = fSize;
          // Set file name
          file.fileName = accountName + " - Info" + ".pdf";
          file.mimeType = "application/pdf";
          Xrm.Navigation.openFile(file, openFileOptions);
        })
        .catch(function (error) {
          console.error(error);
        });
    },
    GetReportIdByReportFileName: function (reportFileName) {
      let lValue = "";
      let lResponse = this.RetrieveMultipleRecord(
        "reports",
        "filename eq '" + reportFileName + "'",
        "reportid",
        false
      );
      if (
        lResponse !== null &&
        lResponse !== undefined &&
        lResponse.value.length > 0
      ) {
        lValue = lResponse.value[0]["reportid"];
      }
      return lValue;
    },
    ExecuteReport: function (reportFileName, reportPrefilter) {
      let reportGuid = this.GetReportIdByReportFileName(reportFileName);
      let pth = this.GetClientUrl() + "/CRMReports/rsviewer/ReportViewer.aspx";
      let orgUniqueName = Xrm.Utility.getGlobalContext().getOrgUniqueName();
      let query =
        "id=%7B" +
        reportGuid +
        "%7D&uniquename=" +
        orgUniqueName +
        "&iscustomreport=true&reportnameonsrs=&reportName=" +
        reportFileName +
        "&isScheduledReport=false&p:" +
        reportPrefilter;
      let retrieveEntityReq = new XMLHttpRequest();
      retrieveEntityReq.open("POST", pth, false);
      retrieveEntityReq.setRequestHeader("Accept", "*/*");
      retrieveEntityReq.setRequestHeader(
        "Content-Type",
        "application/x-www-form-urlencoded"
      );
      retrieveEntityReq.send(query);
      let x = retrieveEntityReq.responseText.lastIndexOf("ReportSession=");
      let y = retrieveEntityReq.responseText.lastIndexOf("ControlID=");
      let ret = [];
      ret[0] = retrieveEntityReq.responseText.slice(x + 14, x + 14 + 24);
      ret[1] = retrieveEntityReq.responseText.slice(y + 10, y + 10 + 32);
      return ret;
    },
    /**
     *
     * @param {any} arrResponseSession
     * @param {any} lcId (Language code)
     * @returns
     */
    Get_SSRS_Report_PDFBase64: function (arrResponseSession, lcId) {
      let that = this;
      return new Promise(function (resolve, reject) {
        let pth =
          that.GetClientUrl() +
          "/Reserved.ReportViewerWebControl.axd?ReportSession=" +
          arrResponseSession[0] +
          "&Culture=" +
          lcId +
          "&CultureOverrides=True&UICulture=" +
          lcId +
          "&UICultureOverrides=True&ReportStack=1&ControlID=" +
          arrResponseSession[1] +
          "&OpType=Export&FileName=Public&ContentDisposition=OnlyHtmlInline&Format=PDF";
        let retrieveEntityReq = new XMLHttpRequest();
        retrieveEntityReq.open("GET", pth, true);
        retrieveEntityReq.setRequestHeader("Accept", "*/*");
        retrieveEntityReq.responseType = "arraybuffer";
        retrieveEntityReq.onreadystatechange = function () {
          if (
            retrieveEntityReq.readyState == 4 &&
            retrieveEntityReq.status == 200
          ) {
            let binary = "";
            let bytes = new Uint8Array(this.response);
            for (let i = 0; i < bytes.byteLength; i++) {
              binary += String.fromCharCode(bytes[i]);
            }
            let base64PDFString = btoa(binary);
            resolve(base64PDFString);
          }
        };
        retrieveEntityReq.send();
      });
    },
    GetClientUrl: function () {
      let lGlobalContext = "";
      try {
        lGlobalContext = Xrm.Utility.getGlobalContext();
      } catch (e) {
        lGlobalContext = parent.Xrm.Utility.getGlobalContext();
      }

      if (lGlobalContext !== null) {
        return lGlobalContext.getClientUrl();
      }
      return null;
    },
    RetrieveMultipleRecord: function (
      lEntityName,
      lFilter,
      lCommaSeparatedAttributeNames,
      isAdmin
    ) {
      let lResponse = null;
      let lXMLHttpRequest = new XMLHttpRequest();
      lXMLHttpRequest.open(
        "GET",
        this.GetClientUrl() +
          "/api/data/v9.2/" +
          lEntityName +
          "?$select=" +
          lCommaSeparatedAttributeNames +
          "&$filter=" +
          lFilter,
        false
      );
      lXMLHttpRequest.setRequestHeader("OData-MaxVersion", "4.0");
      lXMLHttpRequest.setRequestHeader("OData-Version", "4.0");
      lXMLHttpRequest.setRequestHeader("Accept", "application/json");
      lXMLHttpRequest.setRequestHeader(
        "Content-Type",
        "application/json; charset=utf-8"
      );
      lXMLHttpRequest.setRequestHeader(
        "Prefer",
        'odata.include-annotations="*"'
      );
      // If IsAdmin is true, it is executed as an administrator
      if (isAdmin) {
        lXMLHttpRequest.setRequestHeader(
          "MSCRMCallerID",
          this.GetConfigurationValue(this.Constants.SystemAdminId)
        );
      }
      lXMLHttpRequest.onreadystatechange = function () {
        if (this.readyState === 4) {
          lXMLHttpRequest.onreadystatechange = null;
          if (this.status === 200) {
            lResponse = JSON.parse(this.response);
          } else {
            Xrm.Navigation.openAlertDialog(
              "An exception has occurred, please contact the system administrator."
            );
            console.log("Error:");
            console.log(this.statusText);
          }
        }
      };
      lXMLHttpRequest.send();
      return lResponse;
    },
    RetrieveSingleRecord: function (
      lEntityName,
      lEntityId,
      lCommaSeparatedAttributeNames,
      admin
    ) {
      let lResponse = null;
      let lXMLHttpRequest = new XMLHttpRequest();
      lXMLHttpRequest.open(
        "GET",
        this.GetClientUrl() +
          "/api/data/v9.2/" +
          lEntityName +
          "(" +
          lEntityId +
          ")" +
          "?$select=" +
          lCommaSeparatedAttributeNames,
        false
      );
      lXMLHttpRequest.setRequestHeader("OData-MaxVersion", "4.0");
      lXMLHttpRequest.setRequestHeader("OData-Version", "4.0");
      lXMLHttpRequest.setRequestHeader("Accept", "application/json");
      lXMLHttpRequest.setRequestHeader(
        "Content-Type",
        "application/json; charset=utf-8"
      );
      lXMLHttpRequest.setRequestHeader(
        "Prefer",
        'odata.include-annotations="*"'
      );
      if (admin) {
        lXMLHttpRequest.setRequestHeader(
          "MSCRMCallerID",
          this.GetConfigurationValue(this.Constants.SystemAdminId)
        );
      }
      lXMLHttpRequest.onreadystatechange = function () {
        if (this.readyState === 4) {
          lXMLHttpRequest.onreadystatechange = null;
          if (this.status === 200) {
            lResponse = JSON.parse(this.response);
          } else {
            Xrm.Navigation.openAlertDialog(
              "An exception has occurred, please contact the system administrator."
            );
            console.log("Error:");
            console.log(this.statusText);
          }
        }
      };
      lXMLHttpRequest.send();
      return lResponse;
    },
    /**
     * Get Configuration Value
     * P.S: This is my own new configuration entity, there are two main fields: (1) name , (2) value.
     * if you need to use, please create and modify the following field information
     * @param {any} configName
     * @returns
     */
    GetConfigurationValue: function (configName) {
      let lValue = "";
      let lResponse = this.RetrieveMultipleRecord(
        "Your Config Entity Logical Collection Name",
        "gdh_name eq '" + configName + "'",
        "gdh_value"
      );
      if (
        lResponse !== null &&
        lResponse !== undefined &&
        lResponse.value.length > 0
      ) {
        lValue = lResponse.value[0][this.Constants.ConfigurationsField.Value];
      }
      return lValue;
    },
  };
})();Account.js

(完)

如果这篇文章刚好帮到了你,欢迎请我喝杯咖啡