Featured image of post Dynamics CRM - Export report as PDF

Dynamics CRM - Export report as PDF

Export report as PDF

Introduction

We have developed a very complex quotation template (SSRS report) for users in the system, allowing them to choose from various export formats: Excel, Word, PDF, etc.
Now, we would like to restrict the export options to only allow PDF format. However, the current system interface (as shown in the image below) does not provide a way to customize the display/hide control for the export list, so we need to find an alternative solution.

报表导出页面

This requirement is very easy to understand. For example, when printing a quotation (a report developed based on SSRS, exported for customer quotations), if the standard export interface is used (which allows for various export formats: Excel/Word/PDF), users can choose to export to Excel. They may then modify the document offline and later export it to PDF based on the Excel file. Even so, users can still edit the PDF afterward, but there’s nothing one can do about that. After all, once it reaches this point, it’s just a matter of preventing honest individuals from being taken advantage of by dishonest ones.

There are generally two methods:

  1. Generate PDF on the frontend: This can be achieved using third-party JS libraries (such as PrintJS) to design and implement the solution, along with adding a custom button for invocation.
  2. Add a custom button: Directly export the existing report in PDF format.

For the first method, if the project is just starting and the report is not yet complete, this approach can be directly chosen. However, if significant time and effort have already been invested in the existing report, it would clearly be unwise to abandon it. Therefore, we have decided to adopt the second method this time.

Effect

  • Click the export button (custom) 点击 Export Account Info

  • Open the downloaded PDF file 打开下载的 PDF文件

Implementation

Tips

Assuming we have added a custom SSRS report: PrintAccount.rdl on the customer form.

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

Add Account.js

Don’t forget to publish after adding. 添加Javascript脚本

P.S: For detailed code, please refer to the “Appendix” below.

Add Custom Button

P.S: Here, I am using the new button editing interface. Currently, you can also use Ribbon Workbench to add buttons.

  • Open the solution and navigate to Model Driven App.

  • Select “Edit Command Bar.” 选择编辑命令栏

  • Select “Main Form” 添加 Main Form

  • Add Button 添加按钮

  • Bind Account.js 绑定 Account.js

  • Save and Publish

Remark

  1. Why not use the system’s standard functionality to generate PDFs? 为什么不使用系统的标准功能来实现生成 PDF Converting to PDF is actually quite similar to configuring Word templates, but there is one drawback: associated sub-tables will be displayed regardless of their status, even if they are inactive.
  2. Where can I find CRM_FilteredAccount in the code? CRM_FilteredAccount

Appendix

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;
        },
    }
})();
Built with Hugo
Theme Stack designed by Jimmy
发表了19篇文章 · 总计14.48k字