Featured image of post Dynamics CRM - 导出报表为 PDF

Dynamics CRM - 导出报表为 PDF

导出报表为 PDF

前言

我们在系统里为用户开发了一张非常复杂的报价单模板(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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
/**
 * 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;
        },
    }
})();
使用 Hugo 构建
主题 StackJimmy 设计
发表了19篇文章 · 总计21.42k字