前言
我们在系统里为用户开发了一张非常复杂的报价单模板(SSRS报表),用户可以选择很多种导出格式:Excel/Word/PDF等。
现在希望只能导出 PDF 格式的文件,但目前系统界面(下图)的导出列表没有办法做自定义的显示/隐藏控制,所以需要另想办法。
这种需求非常好理解,比如 报价单打印(基于SSRS开发的报表,导出后给客户进行报价),如果使用标准导出界面(可以选择很多种导出格式:Excel/Word/PDF),用户可以选择导出Excel,然后就会出现自己线下进行修改,然后再基于Excel导出PDF。尽管如此,用户还是可以基于PDF再进行编辑,但这也没有办法,毕竟都做到这一步了,也只能是“防君子不放小人”。
大致有两种方法:
- 前端生成 PDF:可以借助第三方 JS 库(如 PrintJS)来设计实现,并添加自定义按钮进行调用
- 添加自定义按钮:直接导出现有报表的 PDF 格式
对于第一种方式,如果项目刚开始且报表尚未完成,可以直接选择这种方法。然而,如果现有的报表已经投入了大量时间和精力,直接弃用显然不够理智。因此,这次我们决定采用第二种方式。
效果
点击导出按钮(自定义)
打开下载的 PDF 文件
实现
Tips
假设我们在客户表单上添加了一张自定义的 SSRS 报表:PrintAccount.rdl
添加 Account.js 脚本
添加完后别忘记发布。
P.S: 详细的代码请看下方的“附录”
添加自定义按钮
P.S: 这里我使用新版的按钮编辑界面。当前,你也可以使用 Ribbon Workbench 添加按钮。
打开解决方案,进入 Model Driven App
选择 编辑命令栏
选择 Main Form
添加按钮
绑定 Account.js
保存并发布
备注
- 为什么不使用系统的标准功能来实现生成 PDF?
Convert to PDF 其实和配置 Word 模板差不多,都有怎么一个不好的地方:
关联的子表,无论状态是否停用,都会显示出来。 - 代码中的
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;
},
}
})();