前言

假定现在系统里的报价单实体上自定义开发(SSRS实现)了一张非常复杂的报表,该报表主要是为销售输出给客户的报价模板,但是现在调用该报表的方式是使用标准的报表按钮,点击报表按钮后再让用户选择报表,这种方式会存在怎么一个问题,在导出报表界面中用户可以选择导出的多种导出格式,例如可以导出 Excel、Word、PDF等,不符合管理规范,甲方爸爸提出只能让销售导出PDF,所以有了这篇文章。

报表导出页面

报表导出页面

题外话,这种需求非常好理解,目前的报价单打印用户可以选择导出Excel,然后就会出现自己线下进行修改,然后再基于Excel导出PDF。尽管如此,用户还是可以基于PDF再进行编辑,但这也没有办法,毕竟都做到这一步了,也只能是防君子不放小人

大致有两种方法:

  1. 前端生成 PDF:可以借助第三方 JS 库(如 PrintJS)来设计实现,并添加自定义按钮进行调用
  2. 添加自定义按钮:直接导出现有报表的 PDF 格式

对于第一种方式,如果项目刚开始且报表尚未完成,可以直接选择这种方法。但是!如果现有的报表已经投入了大量时间和精力,直接弃用显然不够理智。因此,这次我们决定采用第二种方式。

效果

(1)点击导出按钮(自定义)
点击 Export Account Info

点击 Export Account Info

(2)打开下载的 PDF 文件
打开下载的 PDF文件
打开下载的 PDF文件

实现

假定

假设我们在客户表单上添加了一张自定义的 SSRS 报表:PrintAccount.rdl

客户表单上添加了一张自定义的SSRS报表

客户表单上添加了一张自定义的SSRS报表

Step 1. 添加 Account.js 脚本

P.S:

  1. 添加完后别忘记发布
  2. 详细的 account.js 代码请看下方的附录

添加Javascript脚本

添加Javascript脚本

Step 2. 添加自定义按钮

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

打开解决方案,进入 Model Driven App,选择 编辑命令栏

选择编辑命令栏

选择编辑命令栏

选择 Main Form

选择 Main Form

选择 Main Form

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

绑定 Account.js

绑定 Account.js

最后保存并发布

其他说明

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

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

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

Convert to PDF 其实和配置 Word 模板差不多,都有一个不好的地方:关联的子表,无论状态是否停用,都会显示出来。例如,客户和联系人实体是一对多的关系,我们在客户实体上配置了Word模板,模板内容除了有客户实体上的字段之外,还有一个联系人列表,列出当前客户关联了的联系人,现在我们使用Convert to PDF,将Word模板转为PDF,这个时当前客户下有10个联系人(其中2个联系人已被停用),这时生成的PDF,或是生成的Word,都会显示出这2个停用了的联系人。

代码中的 CRM_FilteredAccount 在哪里找?

这其实是报表的参数

CRM_FilteredAccount

CRM_FilteredAccount

附录

Account/Account.js

Account/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;
        },
    }
})();