前言

我们在系统里为用户开发了一张非常复杂的报价单模板(SSRS报表),用户可以选择很多种导出格式:Excel/Word/PDF等。
现在希望只能导出 PDF 格式的文件,但目前系统界面(下图)的导出列表没有办法做自定义的显示/隐藏控制,所以需要另想办法。

报表导出页面

这种需求非常好理解,比如 报价单打印(基于SSRS开发的报表,导出后给客户进行报价),如果使用标准导出界面(可以选择很多种导出格式:Excel/Word/PDF),用户可以选择导出Excel,然后就会出现自己线下进行修改,然后再基于Excel导出PDF。尽管如此,用户还是可以基于PDF再进行编辑,但这也没有办法,毕竟都做到这一步了,也只能是“防君子不放小人”。

大致有两种方法:

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

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

效果

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

  • 打开下载的 PDF 文件 打开下载的 PDF文件

实现

Tips

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

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

添加 Account.js 脚本

添加完后别忘记发布。 添加Javascript脚本

P.S: 详细的代码请看下方的“附录”

添加自定义按钮

P.S: 这里我使用新版的按钮编辑界面。当前,你也可以使用 Ribbon Workbench 添加按钮。

  • 打开解决方案,进入 Model Driven App

  • 选择 编辑命令栏 选择编辑命令栏

  • 选择 Main Form 添加 Main Form

  • 添加按钮 添加按钮

  • 绑定 Account.js 绑定 Account.js

  • 保存并发布

备注

  1. 为什么不使用系统的标准功能来实现生成 PDF? 为什么不使用系统的标准功能来实现生成 PDF Convert to PDF 其实和配置 Word 模板差不多,都有怎么一个不好的地方:
    关联的子表,无论状态是否停用,都会显示出来。
  2. 代码中的 CRM_FilteredAccount 在哪里找? CRM_FilteredAccount

附录

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